From fdcb988e0f4f32f650cc5ccbb8723302f929528c Mon Sep 17 00:00:00 2001 From: wangqiang04 Date: Fri, 3 Nov 2023 18:31:37 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8D=87=E7=BA=A7=E5=91=BC=E5=8F=AB=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=202.2.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- NLiteAVDemo-Android-Java/app/build.gradle | 2 + .../app/src/main/AndroidManifest.xml | 6 + .../yunxin/app/videocall/MainActivity.java | 5 +- .../app/videocall/SelfUserInfoHelper.java | 11 +- .../yunxin/app/videocall/TestActivity.java | 25 + NLiteAVDemo-Android-Java/build.gradle | 10 +- .../call-ui/src/main/AndroidManifest.xml | 8 +- .../com/netease/yunxin/nertc/ui/CallKitUI.kt | 1 + .../yunxin/nertc/ui/CallKitUIService.kt | 24 + .../nertc/ui/base/CommonCallActivity.kt | 214 +++- .../netease/yunxin/nertc/ui/base/Constants.kt | 8 + .../yunxin/nertc/ui/base/TransferHelper.kt | 86 +- .../yunxin/nertc/ui/base/UserInfoHelper.kt | 12 +- .../yunxin/nertc/ui/base/UserInfoHepersEx.kt | 68 +- .../DefaultFloatingTouchEventStrategy.kt | 274 +++++ .../nertc/ui/floating/FloatingPermission.kt | 51 + .../ui/floating/FloatingTouchEventStrategy.kt | 30 + .../ui/floating/FloatingWindowWrapper.kt | 308 ++++++ .../nertc/ui/p2p/CallUIFloatingWindowMgr.kt | 290 ++++++ .../nertc/ui/p2p/CallUIOperationsMgr.kt | 252 ++++- .../yunxin/nertc/ui/p2p/FloatingView.kt | 157 +++ .../yunxin/nertc/ui/p2p/IFloatingView.kt | 30 + .../yunxin/nertc/ui/p2p/P2PCallActivity.kt | 964 ------------------ .../nertc/ui/p2p/P2PCallFragmentActivity.kt | 88 +- .../yunxin/nertc/ui/p2p/P2PUIConfig.kt | 80 +- .../ui/p2p/fragment/BaseP2pCallFragment.kt | 200 +++- .../ui/p2p/fragment/FragmentActionBridge.kt | 32 +- .../nertc/ui/p2p/fragment/P2PUIUpdateType.kt | 5 + .../fragment/callee/AudioCalleeFragment.kt | 93 +- .../fragment/callee/VideoCalleeFragment.kt | 108 +- .../fragment/caller/AudioCallerFragment.kt | 153 ++- .../fragment/caller/VideoCallerFragment.kt | 140 +-- .../onthecall/AudioOnTheCallFragment.kt | 126 +-- .../onthecall/VideoOnTheCallFragment.kt | 280 +++-- .../ui/service/CallKitUIBridgeService.kt | 5 +- .../ui/utils/AppForegroundWatcherHelper.kt | 7 + .../yunxin/nertc/ui/utils/FormatSecondTime.kt | 2 +- .../nertc/ui/utils/PermissionExtends.kt | 137 ++- .../nertc/ui/utils/PermissionTipDialog.kt | 2 +- .../ui/utils/SwitchCallTypeConfirmDialog.kt | 55 +- .../nertc/ui/view/OverLayPermissionDialog.kt | 85 ++ .../drawable-hdpi/avchat_left_type_audio.png | Bin 1399 -> 0 bytes .../drawable-hdpi/avchat_left_type_video.png | Bin 1304 -> 0 bytes .../drawable-hdpi/avchat_mute_icon_normal.png | Bin 1564 -> 0 bytes .../avchat_receive_bg_normal.9.png | Bin 1203 -> 0 bytes .../avchat_receive_bg_pressed.9.png | Bin 1191 -> 0 bytes .../avchat_refuse_bg_normal.9.png | Bin 319 -> 0 bytes .../avchat_refuse_bg_pressed.9.png | Bin 318 -> 0 bytes .../avchat_speaker_icon_checked.png | Bin 1605 -> 0 bytes .../avchat_speaker_icon_pressed.png | Bin 1644 -> 0 bytes .../avchat_switch_mode_video_icon.png | Bin 1213 -> 0 bytes .../nim_actionbar_dark_back_icon.png | Bin 1235 -> 0 bytes .../res/drawable-hdpi/nim_avatar_default.png | Bin 2807 -> 0 bytes .../res/drawable-hdpi/nim_avatar_group.png | Bin 4442 -> 0 bytes .../res/drawable-hdpi/t_avchat_camera.png | Bin 970 -> 0 bytes .../drawable-hdpi/t_avchat_camera_mute.png | Bin 1124 -> 0 bytes .../t_avchat_camera_mute_pressed.png | Bin 1987 -> 0 bytes .../drawable-hdpi/t_avchat_camera_pressed.png | Bin 1815 -> 0 bytes .../res/drawable-hdpi/t_avchat_hangup.png | Bin 782 -> 0 bytes .../drawable-hdpi/t_avchat_hangup_pressed.png | Bin 987 -> 0 bytes .../res/drawable-hdpi/t_avchat_loading.gif | Bin 2513 -> 0 bytes .../res/drawable-hdpi/t_avchat_microphone.png | Bin 838 -> 0 bytes .../t_avchat_microphone_mute.png | Bin 878 -> 0 bytes .../t_avchat_microphone_mute_pressed.png | Bin 1730 -> 0 bytes .../t_avchat_microphone_pressed.png | Bin 1655 -> 0 bytes .../drawable-hdpi/t_avchat_switch_camera.png | Bin 1044 -> 0 bytes .../t_avchat_switch_camera_pressed.png | Bin 1059 -> 0 bytes .../nim_actionbar_dark_back_icon.png | Bin 1220 -> 0 bytes .../res/drawable-xhdpi/t_avchat_camera.png | Bin 1259 -> 0 bytes .../drawable-xhdpi/t_avchat_camera_mute.png | Bin 1612 -> 0 bytes .../t_avchat_camera_mute_pressed.png | Bin 2390 -> 0 bytes .../t_avchat_camera_pressed.png | Bin 1939 -> 0 bytes .../res/drawable-xhdpi/t_avchat_hangup.png | Bin 1025 -> 0 bytes .../t_avchat_hangup_pressed.png | Bin 1305 -> 0 bytes .../res/drawable-xhdpi/t_avchat_loading.gif | Bin 2751 -> 0 bytes .../drawable-xhdpi/t_avchat_microphone.png | Bin 1082 -> 0 bytes .../t_avchat_microphone_mute.png | Bin 1189 -> 0 bytes .../t_avchat_microphone_mute_pressed.png | Bin 2000 -> 0 bytes .../t_avchat_microphone_pressed.png | Bin 1866 -> 0 bytes .../drawable-xhdpi/t_avchat_switch_camera.png | Bin 1286 -> 0 bytes .../t_avchat_switch_camera_pressed.png | Bin 1389 -> 0 bytes .../icon_call_virtual_blur_off.png | Bin 0 -> 634 bytes .../icon_call_virtual_blur_on.png | Bin 0 -> 764 bytes .../res/drawable-xxhdpi/remote_voice_off.png | Bin 843 -> 0 bytes .../res/drawable-xxhdpi/remote_voice_on.png | Bin 865 -> 0 bytes .../drawable-xxhdpi/team_avchat_icon_add.png | Bin 444 -> 0 bytes .../drawable/avchat_receive_bg_selector.xml | 12 - .../drawable/avchat_refuse_bg_selector.xml | 15 - .../res/drawable/floating_window_phone_bg.xml | 6 + .../g2_avchat_remote_voice_icon_selector.xml | 14 - .../drawable/icon_call_floating_window.xml | 14 + .../src/main/res/drawable/icon_call_phone.xml | 15 + .../drawable/nim_actionbar_nest_dark_logo.xml | 13 - .../res/drawable/nim_list_item_selector.xml | 13 - .../shape_call_floating_window_video_bg.xml | 16 + .../t_avchat_camera_mute_selector.xml | 12 - .../res/drawable/t_avchat_camera_selector.xml | 12 - .../res/drawable/t_avchat_hangup_selector.xml | 12 - .../t_avchat_microphone_mute_selector.xml | 12 - .../drawable/t_avchat_microphone_selector.xml | 12 - .../t_avchat_switch_camera_selector.xml | 12 - .../res/layout/activity_p2p_call_fragment.xml | 5 +- .../res/layout/fragment_p2p_audio_caller.xml | 12 + .../layout/fragment_p2p_audio_on_the_call.xml | 11 + .../res/layout/fragment_p2p_base_call.xml | 13 - .../res/layout/fragment_p2p_video_callee.xml | 11 + .../res/layout/fragment_p2p_video_caller.xml | 11 + .../layout/fragment_p2p_video_on_the_call.xml | 47 +- .../main/res/layout/view_floating_window.xml | 93 ++ .../view_overlay_permission_tip_dialog.xml | 57 ++ .../call-ui/src/main/res/values/colors.xml | 6 + .../call-ui/src/main/res/values/strings.xml | 7 + .../call-ui/src/main/res/values/styles.xml | 4 + 113 files changed, 3075 insertions(+), 1826 deletions(-) create mode 100644 NLiteAVDemo-Android-Java/app/src/main/java/com/netease/yunxin/app/videocall/TestActivity.java create mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/floating/DefaultFloatingTouchEventStrategy.kt create mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/floating/FloatingPermission.kt create mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/floating/FloatingTouchEventStrategy.kt create mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/floating/FloatingWindowWrapper.kt create mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/CallUIFloatingWindowMgr.kt create mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/FloatingView.kt create mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/IFloatingView.kt delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/P2PCallActivity.kt create mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/view/OverLayPermissionDialog.kt delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/avchat_left_type_audio.png delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/avchat_left_type_video.png delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/avchat_mute_icon_normal.png delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/avchat_receive_bg_normal.9.png delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/avchat_receive_bg_pressed.9.png delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/avchat_refuse_bg_normal.9.png delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/avchat_refuse_bg_pressed.9.png delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/avchat_speaker_icon_checked.png delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/avchat_speaker_icon_pressed.png delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/avchat_switch_mode_video_icon.png delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/nim_actionbar_dark_back_icon.png delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/nim_avatar_default.png delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/nim_avatar_group.png delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_camera.png delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_camera_mute.png delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_camera_mute_pressed.png delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_camera_pressed.png delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_hangup.png delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_hangup_pressed.png delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_loading.gif delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_microphone.png delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_microphone_mute.png delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_microphone_mute_pressed.png delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_microphone_pressed.png delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_switch_camera.png delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_switch_camera_pressed.png delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/nim_actionbar_dark_back_icon.png delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_camera.png delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_camera_mute.png delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_camera_mute_pressed.png delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_camera_pressed.png delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_hangup.png delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_hangup_pressed.png delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_loading.gif delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_microphone.png delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_microphone_mute.png delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_microphone_mute_pressed.png delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_microphone_pressed.png delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_switch_camera.png delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_switch_camera_pressed.png create mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xxhdpi/icon_call_virtual_blur_off.png create mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xxhdpi/icon_call_virtual_blur_on.png delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xxhdpi/remote_voice_off.png delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xxhdpi/remote_voice_on.png delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xxhdpi/team_avchat_icon_add.png delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/avchat_receive_bg_selector.xml delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/avchat_refuse_bg_selector.xml create mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/floating_window_phone_bg.xml delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/g2_avchat_remote_voice_icon_selector.xml create mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/icon_call_floating_window.xml create mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/icon_call_phone.xml delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/nim_actionbar_nest_dark_logo.xml delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/nim_list_item_selector.xml create mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/shape_call_floating_window_video_bg.xml delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/t_avchat_camera_mute_selector.xml delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/t_avchat_camera_selector.xml delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/t_avchat_hangup_selector.xml delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/t_avchat_microphone_mute_selector.xml delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/t_avchat_microphone_selector.xml delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/t_avchat_switch_camera_selector.xml delete mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/layout/fragment_p2p_base_call.xml create mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/layout/view_floating_window.xml create mode 100644 NLiteAVDemo-Android-Java/call-ui/src/main/res/layout/view_overlay_permission_tip_dialog.xml diff --git a/NLiteAVDemo-Android-Java/app/build.gradle b/NLiteAVDemo-Android-Java/app/build.gradle index b1505cc..5f23573 100644 --- a/NLiteAVDemo-Android-Java/app/build.gradle +++ b/NLiteAVDemo-Android-Java/app/build.gradle @@ -62,5 +62,7 @@ dependencies { implementation(libs.okhttpLoggingInterceptor) implementation(libs.retrofitCore) implementation(libs.retrofitConverterGson) + implementation(libs.nertc.nenn) + implementation(libs.nertc.segment) implementation project(':call-ui') } \ No newline at end of file diff --git a/NLiteAVDemo-Android-Java/app/src/main/AndroidManifest.xml b/NLiteAVDemo-Android-Java/app/src/main/AndroidManifest.xml index 71fa168..8e9ed18 100644 --- a/NLiteAVDemo-Android-Java/app/src/main/AndroidManifest.xml +++ b/NLiteAVDemo-Android-Java/app/src/main/AndroidManifest.xml @@ -94,6 +94,12 @@ android:screenOrientation="portrait" android:theme="@style/Theme.AppCompat.NoActionBar"/> + + result, Throwable exception) { } @Override - public boolean fetchNicknameByTeam(@NotNull String s, @NotNull String s1, @NotNull Function1 function1) { - return false; - } - - @Override - public boolean loadAvatar(@NotNull Context context, @NotNull String s, @NotNull ImageView imageView) { + public boolean fetchAvatar(@NonNull Context context, @NonNull String accId, @NonNull Function2 notify) { return false; } } diff --git a/NLiteAVDemo-Android-Java/app/src/main/java/com/netease/yunxin/app/videocall/TestActivity.java b/NLiteAVDemo-Android-Java/app/src/main/java/com/netease/yunxin/app/videocall/TestActivity.java new file mode 100644 index 0000000..fd1bb8f --- /dev/null +++ b/NLiteAVDemo-Android-Java/app/src/main/java/com/netease/yunxin/app/videocall/TestActivity.java @@ -0,0 +1,25 @@ +// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +package com.netease.yunxin.app.videocall; + +import androidx.annotation.NonNull; + +import com.netease.yunxin.nertc.ui.base.CallParam; +import com.netease.yunxin.nertc.ui.p2p.P2PCallFragmentActivity; +import com.netease.yunxin.nertc.ui.p2p.P2PUIConfig; + +public class TestActivity extends P2PCallFragmentActivity { + + @NonNull + @Override + protected P2PUIConfig provideUIConfig(CallParam param) { + return new P2PUIConfig.Builder() + .enableVirtualBlur(true) + .enableFloatingWindow(true) + .enableVirtualBlur(true) + .enableForegroundService(true) + .build(); + } +} diff --git a/NLiteAVDemo-Android-Java/build.gradle b/NLiteAVDemo-Android-Java/build.gradle index 73115c5..d07b93d 100644 --- a/NLiteAVDemo-Android-Java/build.gradle +++ b/NLiteAVDemo-Android-Java/build.gradle @@ -44,6 +44,7 @@ ext { AppKey = '' BaseUrl = '' libs = new Libs() + libs.nertc = new Libs.Nertc() } class Libs{ @@ -60,9 +61,16 @@ class Libs{ static def alog = "com.netease.yunxin.kit:alog:1.1.0" static def kotlinxCoroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4" static def kotlinxCoroutinesAndroid = "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4" - static def call = "com.netease.yunxin.kit.call:call:2.1.0" + static def call = "com.netease.yunxin.kit.call:call:2.2.0" static def okhttp = "com.squareup.okhttp3:okhttp:4.9.3" static def okhttpLoggingInterceptor = "com.squareup.okhttp3:logging-interceptor:4.9.3" static def retrofitCore = "com.squareup.retrofit2:retrofit:2.9.0" static def retrofitConverterGson = "com.squareup.retrofit2:converter-gson:2.9.0" + + static class Nertc { + def nenn = "com.netease.yunxin:nertc-nenn:5.5.2" + def segment = "com.netease.yunxin:nertc-segment:5.5.2" + } + + static def nertc } \ No newline at end of file diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/AndroidManifest.xml b/NLiteAVDemo-Android-Java/call-ui/src/main/AndroidManifest.xml index c22091b..1be9d7f 100644 --- a/NLiteAVDemo-Android-Java/call-ui/src/main/AndroidManifest.xml +++ b/NLiteAVDemo-Android-Java/call-ui/src/main/AndroidManifest.xml @@ -13,6 +13,7 @@ + @@ -30,14 +31,9 @@ - - , + observer: ResultObserver? + ): Boolean { + observer?.onResult( + ResultInfo( + value = NECallEngine.sharedInstance().callInfo.callStatus == CallState.STATE_IDLE, + success = true + ) + ) + return true + } + } + ) + ) + XKitRouter.registerRouter( Constants.PATH_CALL_SINGLE_PAGE, XKitRouter.RouterValue( diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/base/CommonCallActivity.kt b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/base/CommonCallActivity.kt index 7bd06b9..d543eab 100644 --- a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/base/CommonCallActivity.kt +++ b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/base/CommonCallActivity.kt @@ -11,6 +11,7 @@ import android.view.View import android.view.Window import android.view.WindowManager import androidx.appcompat.app.AppCompatActivity +import com.netease.lava.api.IVideoRender import com.netease.lava.nertc.sdk.NERtcConstants import com.netease.lava.nertc.sdk.NERtcEx import com.netease.lava.nertc.sdk.video.NERtcVideoView @@ -24,11 +25,16 @@ import com.netease.yunxin.kit.call.p2p.model.NECallType import com.netease.yunxin.kit.call.p2p.model.NECallTypeChangeInfo import com.netease.yunxin.kit.call.p2p.model.NEInviteInfo import com.netease.yunxin.nertc.nertcvideocall.bean.CommonResult +import com.netease.yunxin.nertc.nertcvideocall.model.SwitchCallState import com.netease.yunxin.nertc.nertcvideocall.model.impl.state.CallState +import com.netease.yunxin.nertc.ui.floating.FloatingPermission +import com.netease.yunxin.nertc.ui.p2p.CallUIFloatingWindowMgr import com.netease.yunxin.nertc.ui.p2p.CallUIOperationsMgr import com.netease.yunxin.nertc.ui.p2p.P2PUIConfig -import com.netease.yunxin.nertc.ui.service.CallForegroundService +import com.netease.yunxin.nertc.ui.utils.AppForegroundWatcherHelper import com.netease.yunxin.nertc.ui.utils.PermissionTipDialog +import com.netease.yunxin.nertc.ui.utils.SwitchCallTypeConfirmDialog +import com.netease.yunxin.nertc.ui.view.OverLayPermissionDialog abstract class CommonCallActivity : AppCompatActivity(), NECallEngineDelegate { private val tag = "CommonCallActivity" @@ -56,14 +62,42 @@ abstract class CommonCallActivity : AppCompatActivity(), NECallEngineDelegate { protected val cameraDeviceStatus get() = CallUIOperationsMgr.callInfoWithUIState.cameraDeviceStatus + protected val isLocalSmallVideo + get() = CallUIOperationsMgr.callInfoWithUIState.isLocalSmallVideo + + protected val isVirtualBlur + get() = CallUIOperationsMgr.callInfoWithUIState.isVirtualBlur + protected var uiConfig: P2PUIConfig? = null + protected val isFromFloatingWindow: Boolean + get() { + return intent.getBooleanExtra( + Constants.PARAM_KEY_FLAG_IS_FROM_FLOATING_WINDOW, + false + ) || CallUIFloatingWindowMgr.isFloating() + } + private var occurError = false - private var serviceId: String? = null + private var switchConfirmDialog: SwitchCallTypeConfirmDialog? = null private var permissionTipDialog: PermissionTipDialog? = null + private var overLayPermissionDialog: OverLayPermissionDialog? = null + + private val watcher = object : AppForegroundWatcherHelper.Watcher() { + override fun onBackground() { + if (uiConfig?.enableFloatingWindow == true && + uiConfig?.enableAutoFloatingWindowWhenHome == true && + CallUIOperationsMgr.currentCallState() != CallState.STATE_INVITED && + CallUIOperationsMgr.currentCallState() != CallState.STATE_IDLE + ) { + doShowFloatingWindowInner() + } + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // 禁止自动锁屏 @@ -71,36 +105,44 @@ abstract class CommonCallActivity : AppCompatActivity(), NECallEngineDelegate { // 取消呼叫来电通知 CallUIOperationsMgr.cancelCallNotification(this) // 初始化呼叫信息 - CallUIOperationsMgr.initCallInfoAndUIState( - CallUIOperationsMgr.CallInfoWithUIState(callParam = initIntentForCallParam()) - ) channelId = callParam.getChannelId() uiConfig = provideUIConfig(callParam)?.apply { ALog.d(tag, "current P2PUIConfig is $this.") } + if (!isFromFloatingWindow) { + CallUIOperationsMgr.initCallInfoAndUIState( + CallUIOperationsMgr.CallInfoWithUIState( + callParam = initIntentForCallParam() + ), + foregroundServiceConfig = if (uiConfig?.enableForegroundService == true) { + CallUIOperationsMgr.CallForegroundServiceConfig( + this, + intent, + uiConfig?.foregroundNotificationConfig + ) + } else { + null + } + ) + } else { + val info = CallUIOperationsMgr.currentSwitchTypeCallInfo() + if (switchConfirmDialog?.isShowing != true && info?.state == SwitchCallState.INVITE) { + showSwitchCallTypeConfirmDialog(info.callType) + } + } doOnCreate(savedInstanceState) + // 配置前后台监听 + AppForegroundWatcherHelper.addWatcher(watcher) } open fun doOnCreate(savedInstanceState: Bundle?) { - provideLayoutId()?.run { - setContentView(this) - } ?: provideLayoutView()?.run { - setContentView(this) - } ?: throw IllegalArgumentException("provideLayoutId or provideLayoutView must not be null.") - + setContentView(provideLayoutId()) // 由于页面启动耗时,可能出现页面启动中通话被挂断,此时需要销毁页面 if (callParam.isCalled && callEngine.callInfo.callStatus == CallState.STATE_IDLE) { releaseAndFinish(false) return } callEngine.addCallDelegate(this) - if (uiConfig?.enableForegroundService == true) { - serviceId = CallForegroundService.launchForegroundService( - this, - intent, - uiConfig?.foregroundNotificationConfig - ) - } } open fun configWindow() { @@ -113,6 +155,15 @@ abstract class CommonCallActivity : AppCompatActivity(), NECallEngineDelegate { override fun onResume() { super.onResume() + if (overLayPermissionDialog?.isShowing == true && + FloatingPermission.isFloatPermissionValid(this) + ) { + overLayPermissionDialog?.dismiss() + overLayPermissionDialog = null + } + if (CallUIFloatingWindowMgr.isFloating()) { + CallUIFloatingWindowMgr.releaseFloat(false) + } when (cameraDeviceStatus) { NERtcConstants.VideoDeviceState.DISCONNECTED, NERtcConstants.VideoDeviceState.FREEZED, @@ -129,17 +180,28 @@ abstract class CommonCallActivity : AppCompatActivity(), NECallEngineDelegate { } } - override fun onDestroy() { - super.onDestroy() - callEngine.removeCallDelegate(this) - if (uiConfig?.enableForegroundService == true) { - serviceId?.run { - CallForegroundService.stopService(this@CommonCallActivity, this) + override fun onPause() { + super.onPause() + if (isFinishing) { + val floatingEnable = + uiConfig?.enableFloatingWindow != true || !CallUIFloatingWindowMgr.isFloating() + if (floatingEnable) { + stopVideoPreview() } + releaseAndFinish( + CallUIOperationsMgr.currentCallState() != CallState.STATE_IDLE && floatingEnable + ) } + } + + override fun onDestroy() { + super.onDestroy() + // 移除前后台监听 + AppForegroundWatcherHelper.removeWatcher(watcher) permissionTipDialog?.dismiss() permissionTipDialog = null - CallUIOperationsMgr.releaseCallInfoAndUIState(channelId = channelId) + switchConfirmDialog?.dismiss() + switchConfirmDialog = null } override fun onBackPressed() { @@ -147,9 +209,7 @@ abstract class CommonCallActivity : AppCompatActivity(), NECallEngineDelegate { releaseAndFinish(true) } - protected open fun provideLayoutId(): Int? = null - - protected open fun provideLayoutView():View? = null + protected abstract fun provideLayoutId(): Int protected open fun provideUIConfig(callParam: CallParam?): P2PUIConfig? { return P2PUIConfig() @@ -173,6 +233,11 @@ abstract class CommonCallActivity : AppCompatActivity(), NECallEngineDelegate { CallUIOperationsMgr.doMuteAudio(mute) } + @JvmOverloads + protected open fun doVirtualBlur(enable: Boolean = !isVirtualBlur) { + CallUIOperationsMgr.doVirtualBlur(enable) + } + @JvmOverloads protected open fun doMuteVideo(mute: Boolean = !isLocalMuteVideo) { when (uiConfig?.closeVideoType ?: Constants.CLOSE_TYPE_MUTE) { @@ -201,19 +266,24 @@ abstract class CommonCallActivity : AppCompatActivity(), NECallEngineDelegate { CallUIOperationsMgr.doSwitchCamera() } - protected fun doCall( + protected open fun doCall( observer: NEResultObserver>? ) { - CallUIOperationsMgr.doCall(callParam, observer) + CallUIOperationsMgr.doCall( + callParam + ) { result -> + channelId = result?.data?.signalInfo?.channelId + observer?.onResult(result) + } } @JvmOverloads - protected fun doAccept(observer: NEResultObserver>? = null) { + protected open fun doAccept(observer: NEResultObserver>? = null) { CallUIOperationsMgr.doAccept(observer) } @JvmOverloads - protected fun doHangup( + protected open fun doHangup( observer: NEResultObserver>? = null, channelId: String? = null, extraInfo: String? = null @@ -222,7 +292,7 @@ abstract class CommonCallActivity : AppCompatActivity(), NECallEngineDelegate { } @JvmOverloads - protected fun doSwitchCallType( + protected open fun doSwitchCallType( callType: Int, switchCallState: Int, observer: NEResultObserver>? = null @@ -230,11 +300,26 @@ abstract class CommonCallActivity : AppCompatActivity(), NECallEngineDelegate { CallUIOperationsMgr.doSwitchCallType(callType, switchCallState, observer) } - protected open fun setupLocalView(view: NERtcVideoView?) { - CallUIOperationsMgr.setupLocalView(view) + protected open fun setupLocalView( + view: NERtcVideoView?, + action: ((NERtcVideoView?) -> Unit)? = { + it?.run { + setZOrderMediaOverlay(true) + setScalingType(IVideoRender.ScalingType.SCALE_ASPECT_BALANCED) + } + } + ) { + CallUIOperationsMgr.setupLocalView(view, action) } - protected open fun setupRemoteView(view: NERtcVideoView?) { + protected open fun setupRemoteView( + view: NERtcVideoView?, + action: ((NERtcVideoView?) -> Unit)? = { + it?.run { + setScalingType(IVideoRender.ScalingType.SCALE_ASPECT_BALANCED) + } + } + ) { CallUIOperationsMgr.setupRemoteView(view) } @@ -245,15 +330,70 @@ abstract class CommonCallActivity : AppCompatActivity(), NECallEngineDelegate { if (!isFinishing) { finish() } + if (finishCall) { + CallUIOperationsMgr.doHangup(null, channelId = channelId) + CallUIOperationsMgr.releaseCallInfoAndUIState(channelId = channelId, force = true) + } } - protected fun showPermissionDialog(clickListener: View.OnClickListener): PermissionTipDialog { + protected open fun showPermissionDialog(clickListener: View.OnClickListener): PermissionTipDialog { return PermissionTipDialog(this, clickListener).apply { this@CommonCallActivity.permissionTipDialog = this show() } } + protected open fun showSwitchCallTypeConfirmDialog(callType: Int): SwitchCallTypeConfirmDialog { + val dialog = this.switchConfirmDialog + if (dialog?.isShowing == true) { + return dialog + } + return SwitchCallTypeConfirmDialog(this, { + doSwitchCallType(it, SwitchCallState.ACCEPT) + }, { + doSwitchCallType(it, SwitchCallState.REJECT) + }).apply { + this@CommonCallActivity.switchConfirmDialog = this + show(callType) + } + } + + protected open fun showOverlayPermissionDialog(clickListener: View.OnClickListener): OverLayPermissionDialog { + return OverLayPermissionDialog(this, clickListener).apply { + this@CommonCallActivity.overLayPermissionDialog = this + show() + } + } + + protected open fun doShowFloatingWindow() { + ALog.dApi(tag, "doShowFloatingWindow") + doShowFloatingWindowInner() + } + + private fun doShowFloatingWindowInner() { + ALog.d(tag, "doShowFloatingWindowInner") + if (!FloatingPermission.isFloatPermissionValid(this)) { + if (overLayPermissionDialog?.isShowing != true) { + showOverlayPermissionDialog { + FloatingPermission.requireFloatPermission(this) + } + } + return + } + if (!CallUIFloatingWindowMgr.isFloating()) { + CallUIFloatingWindowMgr.showFloat(this.applicationContext) + } + finish() + } + + protected open fun startVideoPreview() { + CallUIOperationsMgr.startVideoPreview() + } + + protected open fun stopVideoPreview() { + CallUIOperationsMgr.stopVideoPreview() + } + override fun onReceiveInvited(info: NEInviteInfo) { } diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/base/Constants.kt b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/base/Constants.kt index 4f5a070..65beae2 100644 --- a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/base/Constants.kt +++ b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/base/Constants.kt @@ -43,9 +43,17 @@ object Constants { */ const val CLOSE_TYPE_COMPAT = 3 + /** + * 是否来自浮窗 + */ + const val PARAM_KEY_FLAG_IS_FROM_FLOATING_WINDOW = "is_from_floating_window" + // path for single callkit const val PATH_CALL_SINGLE_PAGE = "imkit://call/single.page" + // path for current is idle state. + const val PATH_IS_CALL_IDLE = "imkit://call/state/isIdle" + const val KEY_CALLER_ACC_ID = "caller_accid" const val KEY_CALLED_ACC_ID = "called_accid" diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/base/TransferHelper.kt b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/base/TransferHelper.kt index 4c06fb3..efbfa60 100644 --- a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/base/TransferHelper.kt +++ b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/base/TransferHelper.kt @@ -7,17 +7,27 @@ package com.netease.yunxin.nertc.ui.base +import android.app.Activity import android.content.Context import android.content.Intent +import android.content.pm.PackageManager import android.os.Bundle import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityCompat +import com.netease.yunxin.kit.alog.ALog + +private const val TAG = "TransHelper" + +const val KEY_PERMISSION_RESULT_GRANTED = "permissions_result_granted" +const val KEY_PERMISSION_RESULT_DENIED = "permissions_result_denied" +const val KEY_PERMISSION_RESULT_DENIED_FOREVER = "permissions_result_denied_forever" private val transferMap = HashMap() fun launchTask( context: Context, requestId: Int, - action: (Context) -> Unit, + action: (Activity, Int) -> Unit, result: (ResultInfo) -> Unit ) { transferMap[requestId] = TransferHelperParam(action, result) @@ -25,7 +35,7 @@ fun launchTask( } internal class TransferHelperParam( - val action: ((Context) -> Unit)? = null, + val action: ((Activity, Int) -> Unit)? = null, val result: ((ResultInfo) -> Unit)? = null ) @@ -40,6 +50,9 @@ class TransferHelperActivity : AppCompatActivity() { fun launch(context: Context, requestId: Int) { Intent(context, TransferHelperActivity::class.java).apply { putExtra(PARAM_KEY_FOR_REQUEST_ID, requestId) + if (context !is Activity) { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } context.startActivity(this) } } @@ -51,14 +64,75 @@ class TransferHelperActivity : AppCompatActivity() { if (param?.action == null) { finish() } else { - param.action.invoke(this) + param.action.invoke(this, requestId) } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) - val param = transferMap.remove(requestId) - param?.result?.invoke(ResultInfo(data, resultCode == RESULT_OK)) - finish() + try { + val param = transferMap.remove(requestId) + param?.result?.invoke(ResultInfo(data, resultCode == RESULT_OK)) + } catch (ignored: Throwable) { + ALog.e(TAG, "onActivityResult", ignored) + } finally { + finish() + } + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + try { + val param = transferMap.remove(requestId) + val callbackResult = param?.result ?: run { + finish() + return + } + permissions.toList() + val size = permissions.size + val grantedList = ArrayList() + val deniedList = ArrayList() + val deniedForeverList = ArrayList() + for (index in 0 until size) { + val permission = permissions[index] + when (grantResults[index]) { + PackageManager.PERMISSION_GRANTED -> grantedList.add(permission) + PackageManager.PERMISSION_DENIED -> { + if (!ActivityCompat.shouldShowRequestPermissionRationale( + this, + permission + ) + ) { + deniedForeverList.add(permission) + } else { + deniedList.add(permission) + } + } + } + } + + callbackResult.invoke( + ResultInfo( + Intent() + .apply { + putStringArrayListExtra(KEY_PERMISSION_RESULT_GRANTED, grantedList) + putStringArrayListExtra(KEY_PERMISSION_RESULT_DENIED, deniedList) + putStringArrayListExtra( + KEY_PERMISSION_RESULT_DENIED_FOREVER, + deniedForeverList + ) + }, + true + ) + ) + } catch (ignored: Throwable) { + ALog.e(TAG, "onRequestPermissionsResult", ignored) + } finally { + finish() + } } } diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/base/UserInfoHelper.kt b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/base/UserInfoHelper.kt index 7b188ce..c615398 100644 --- a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/base/UserInfoHelper.kt +++ b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/base/UserInfoHelper.kt @@ -7,13 +7,17 @@ package com.netease.yunxin.nertc.ui.base import android.content.Context -import android.widget.ImageView interface UserInfoHelper { + /** + * 用户根据 accId 内容,利用 notify 接口将用户昵称通知组件 + */ fun fetchNickname(accId: String, notify: ((String) -> Unit)): Boolean - fun fetchNicknameByTeam(accId: String, teamId: String, notify: ((String) -> Unit)): Boolean - - fun loadAvatar(context: Context, accId: String, avatar: ImageView?): Boolean + /** + * 用户根据 accId 内容,利用 notify 接口个将用户的头像链接通知组件, + * notify 中的两个字段其中一个为头像的url,另一个为加载头像失败后展示占位的本地资源 id + */ + fun fetchAvatar(context: Context, accId: String, notify: (String?, Int?) -> Unit): Boolean } diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/base/UserInfoHepersEx.kt b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/base/UserInfoHepersEx.kt index d9fc857..0661381 100644 --- a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/base/UserInfoHepersEx.kt +++ b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/base/UserInfoHepersEx.kt @@ -23,7 +23,6 @@ import com.bumptech.glide.request.target.Target import com.netease.nimlib.sdk.NIMClient import com.netease.nimlib.sdk.RequestCallbackWrapper import com.netease.nimlib.sdk.nos.NosService -import com.netease.nimlib.sdk.team.TeamService import com.netease.nimlib.sdk.uinfo.UserService import com.netease.nimlib.sdk.uinfo.model.NimUserInfo import com.netease.nimlib.sdk.uinfo.model.UserInfo @@ -51,6 +50,11 @@ private val bgRes = intArrayOf( */ private const val AVATAR_NAME_LEN = 2 +/** + * 内部缓存用户昵称信息 + */ +private val nameMap = mutableMapOf() + /** * 直接展示用户头像,如果用户只有短链会自动解析长链进行展示 */ @@ -61,17 +65,12 @@ fun String.loadAvatarByAccId( txtView: TextView? = null, enableTextDefaultAvatar: Boolean ) { - val extension = UserInfoExtensionHelper.userInfoHelper - if (extension?.loadAvatar(context, this, imageView) == true) { - return - } val applicationContext = context.applicationContext val defaultResId = R.drawable.t_avchat_avatar_default val thumbSize = applicationContext.resources.getDimension(R.dimen.avatar_max_size).toInt() - val loadAction: (String?, String) -> Unit = { url, name -> - + val loadAction: (String?, Int, String) -> Unit = { url, res, name -> val requestBuilder = Glide.with(applicationContext).asBitmap().load(url) .listener(object : RequestListener { override fun onLoadFailed( @@ -101,7 +100,7 @@ fun String.loadAvatarByAccId( isFirstResource: Boolean ): Boolean = false }).transform(RoundedCornersCenterCrop(4.dip2Px(context))).apply( - RequestOptions().placeholder(defaultResId).error(defaultResId) + RequestOptions().placeholder(res).error(res) .override(thumbSize, thumbSize) ) imageView?.run { @@ -112,12 +111,12 @@ fun String.loadAvatarByAccId( val userInfo: UserInfo? = NIMClient.getService(UserService::class.java).getUserInfo(this) val getLongUrlAndLoad: (String?, String) -> Unit = { url, name -> if (url == null) { - loadAction(null, name) + loadAction(null, defaultResId, name) } else { NIMClient.getService(NosService::class.java).getOriginUrlFromShortUrl(url) .setCallback(object : RequestCallbackWrapper() { override fun onResult(code: Int, result: String?, exception: Throwable?) { - loadAction(result, name) + loadAction(result, defaultResId, name) bgView?.run { Glide.with(applicationContext).asBitmap().load(result) .transform(BlurCenterCorp(51, 3)).into(this) @@ -126,6 +125,24 @@ fun String.loadAvatarByAccId( }) } } + // 用户自定义头像 + val extension = UserInfoExtensionHelper.userInfoHelper + if (extension?.fetchAvatar(context, this) { url, res -> + loadAction( + url, + res ?: defaultResId, + nameMap[this] ?: this@loadAvatarByAccId + ) + bgView?.run { + Glide.with(applicationContext).asBitmap().load(url) + .error(res ?: defaultResId) + .placeholder(res ?: defaultResId) + .transform(BlurCenterCorp(51, 3)).into(this) + } + } == true + ) { + return + } if (userInfo?.avatar != null) { getLongUrlAndLoad(userInfo.avatar, getNameFromInfo(userInfo.name, this@loadAvatarByAccId)) return @@ -134,7 +151,7 @@ fun String.loadAvatarByAccId( .setCallback(object : RequestCallbackWrapper>() { override fun onResult(code: Int, result: List?, exception: Throwable?) { if (result.isNullOrEmpty()) { - loadAction(null, this@loadAvatarByAccId) + loadAction(null, defaultResId, this@loadAvatarByAccId) return } getLongUrlAndLoad( @@ -145,40 +162,13 @@ fun String.loadAvatarByAccId( }) } -/** - * 通过 accId, teamId 获取用户在 teamId 中的昵称,如果昵称为空,则使用使用成员用户昵称,如用户昵称为空则使用 accId - */ -fun String.fetchNicknameByTeam(teamId: String, notify: ((String) -> Unit)) { - val extension = UserInfoExtensionHelper.userInfoHelper - if (extension?.fetchNicknameByTeam(this, teamId) { - notify(it) - } == true - ) { - return - } - val teamMember = - NIMClient.getService(TeamService::class.java).queryTeamMemberBlock(teamId, this) - - val name = if (teamMember?.teamNick?.trim()?.isEmpty() == false) { - teamMember.teamNick - } else { - null - } - if (name == null) { - fetchNickname { - notify(it) - } - } else { - notify(name) - } -} - /** * 获取用户的 昵称信息,如果为空 则返回 accId */ fun String.fetchNickname(notify: ((String) -> Unit)) { val extension = UserInfoExtensionHelper.userInfoHelper if (extension?.fetchNickname(this) { + nameMap[this] = it notify(it) } == true ) { diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/floating/DefaultFloatingTouchEventStrategy.kt b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/floating/DefaultFloatingTouchEventStrategy.kt new file mode 100644 index 0000000..00cd3fc --- /dev/null +++ b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/floating/DefaultFloatingTouchEventStrategy.kt @@ -0,0 +1,274 @@ +/* + * Copyright (c) 2022 NetEase, Inc. All rights reserved. + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ + +package com.netease.yunxin.nertc.ui.floating + +import android.animation.Animator +import android.animation.ValueAnimator +import android.content.Context +import android.graphics.drawable.Drawable +import android.graphics.drawable.GradientDrawable +import android.util.DisplayMetrics +import android.view.Gravity +import android.view.MotionEvent +import android.view.View +import android.view.WindowManager +import android.view.animation.AccelerateDecelerateInterpolator +import androidx.core.content.ContextCompat +import com.netease.yunxin.kit.alog.ALog +import com.netease.yunxin.nertc.ui.R +import kotlin.math.max + +/** + * 实现类似微信浮窗效果,支持拖动以及吸附效果 + */ +class DefaultFloatingTouchEventStrategy( + private val context: Context, + private val bgColor: Int = ContextCompat.getColor( + context, + R.color.transparent + ), + radius: Float = 0f +) : FloatingTouchEventStrategy { + private val logTag = "DefaultFloatingTouchEventStrategy" + + private var widthPixel = -1 + private var density = 1f + + private var currentX = 0 + private var currentY = 0 + private val radiusPixel = radius.dpToPixel().toFloat() + private var animator = BgAndXPosAnimator() + + private var gravity: Int = 0 + + private val leftBg = GradientDrawable().apply { + setColor(bgColor) + cornerRadii = + floatArrayOf(0f, 0f, radiusPixel, radiusPixel, radiusPixel, radiusPixel, 0f, 0f) + } + + private val movingBg = GradientDrawable().apply { + setColor(bgColor) + cornerRadius = radiusPixel + } + + private val rightBg = GradientDrawable().apply { + setColor(bgColor) + cornerRadii = + floatArrayOf(radiusPixel, radiusPixel, 0f, 0f, 0f, 0f, radiusPixel, radiusPixel) + } + + init { + val wm = context.getSystemService(Context.WINDOW_SERVICE) as? WindowManager + wm?.run { + val outMetrics = DisplayMetrics() + wm.defaultDisplay.getMetrics(outMetrics) + widthPixel = outMetrics.widthPixels + density = outMetrics.density + } + } + + override fun initForWrapper(wrapper: FloatingWindowWrapper) { + wrapper.setBgRes(leftBg) + this.gravity = wrapper.config.windowParams.gravity + } + + override fun handScrollEvent(event: MotionEvent, windowWrapper: FloatingWindowWrapper) { + if (animator.isRunning()) { + return + } + ALog.i( + logTag, + "x is ${event.x} , y is ${event.y} , rawX is ${event.rawX}, rawY is ${event.rawY}. detail is $event" + ) + when (event.action) { + MotionEvent.ACTION_DOWN -> { + currentX = event.rawX.toInt() + currentY = event.rawY.toInt() + } + + MotionEvent.ACTION_MOVE -> { + val nowX = event.rawX.toInt() + val nowY = event.rawY.toInt() + val movedX = nowX - currentX + val movedY = nowY - currentY + currentX = nowX + currentY = nowY + windowWrapper.updateWindowParamsPos( + max( + windowWrapper.fetchWindowParamsX() + if (gravity.and(Gravity.END) == Gravity.END) -movedX else movedX, + 0 + ), + max( + windowWrapper.fetchWindowParamsY() + if (gravity.and(Gravity.TOP) == Gravity.TOP) movedY else -movedY, + 0 + ) + ) + windowWrapper.setBgRes(movingBg) + } + + MotionEvent.ACTION_UP -> { + if (widthPixel < 0) { + return + } + toAdsorbEdge(event.rawX.toInt(), windowWrapper) + } + } + } + + override fun toUpdateViewUI(wrapper: FloatingWindowWrapper) { + if (gravity.and(Gravity.END) == 0) { + toAdsorbEdge(wrapper.fetchWindowParamsX(), wrapper) + } + } + + /** + * 吸附至边缘 + */ + private fun toAdsorbEdge(x: Int, windowWrapper: FloatingWindowWrapper) { + windowWrapper.post { + if (x > widthPixel / 2) { + animator.start( + windowWrapper.fetchWindowParamsX(), + if (gravity.and(Gravity.END) == Gravity.END) 0 else widthPixel - windowWrapper.measuredWidth, + rightBg, + windowWrapper + ) + } else { + animator.start( + windowWrapper.fetchWindowParamsX(), + if (gravity.and(Gravity.END) == Gravity.END) widthPixel - windowWrapper.measuredWidth else 0, + leftBg, + windowWrapper + ) + } + } + } + + private var currentBgRes: Drawable? = null + + /** + * 设置页面背景 + */ + private fun View.setBgRes(bg: Drawable) { + if (currentBgRes == bg) { + return + } + background = bg + currentBgRes = bg + } + + /** + * dp 转换至 pixel + */ + private fun Float.dpToPixel(): Int = (this * density + 0.5f).toInt() + + /** + * 背景和 x坐标点的动画控制 + */ + private inner class BgAndXPosAnimator { + private var endXPos: Int? = null + private var finishedBg: Drawable? = null + private var windowWrapper: FloatingWindowWrapper? = null + + /** + * 属性动画更新监听 + */ + private val updateListener = object : ValueAnimator.AnimatorUpdateListener { + override fun onAnimationUpdate(animation: ValueAnimator?) { + animation ?: return + (animation.animatedValue as? Int)?.run { + windowWrapper?.updateWindowParamsPos(xPos = this) + } + } + } + + /** + * 动画状态监听 + */ + private val animatorListener = object : Animator.AnimatorListener { + override fun onAnimationStart(animation: Animator?) { + } + + override fun onAnimationEnd(animation: Animator?) { + ALog.d(logTag, "onAnimationEnd") + finish() + } + + override fun onAnimationCancel(animation: Animator?) { + ALog.d(logTag, "onAnimationCancel") + finish() + } + + override fun onAnimationRepeat(animation: Animator?) { + } + } + + private val animator = ValueAnimator.ofInt(0, 1).setDuration(100) + .apply { + interpolator = AccelerateDecelerateInterpolator() + } + + /** + * 动画开始 + */ + fun start( + startPos: Int, + endPos: Int, + finishedBg: Drawable, + windowWrapper: FloatingWindowWrapper + ) { + if (isRunning()) { + ALog.d(logTag, "running") + return + } + if (endPos == windowWrapper.fetchWindowParamsX()) { + windowWrapper.setBgRes(finishedBg) + return + } + ALog.d(logTag, "start $startPos, $endPos") + + this.endXPos = endPos + this.finishedBg = finishedBg + this.windowWrapper = windowWrapper + + animator.run { + addListener(animatorListener) + addUpdateListener(updateListener) + setIntValues(startPos, endPos) + start() + } + } + + fun isRunning() = animator.isRunning + + fun cancel() { + this.windowWrapper = null + this.finishedBg = null + this.endXPos = null + animator.removeAllUpdateListeners() + animator.removeAllListeners() + + if (isRunning()) { + animator.cancel() + } + } + + private fun finish() { + animator.removeAllUpdateListeners() + animator.removeAllListeners() + windowWrapper?.also { wrapper -> + finishedBg?.also { bg -> + wrapper.setBgRes(bg) + } + endXPos?.run { + wrapper.updateWindowParamsPos(this) + } + } + } + } +} diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/floating/FloatingPermission.kt b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/floating/FloatingPermission.kt new file mode 100644 index 0000000..8d5b564 --- /dev/null +++ b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/floating/FloatingPermission.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2022 NetEase, Inc. All rights reserved. + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ + +package com.netease.yunxin.nertc.ui.floating + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.provider.Settings +import com.netease.yunxin.kit.alog.ALog + +/** + * 浮窗权限请求,以及检测 + */ +object FloatingPermission { + private const val TAG = "FloatPermission" + + fun isFloatPermissionValid(context: Context): Boolean { + // Android 6.0 以下无需申请权限 + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + // 判断是否拥有悬浮窗权限,无则跳转悬浮窗权限授权页面 + Settings.canDrawOverlays(context) + } else { + true + } + } + + /** + * 请求浮窗权限 + */ + fun requireFloatPermission(context: Context) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return + } + try { + val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION) + intent.data = Uri.parse("package:" + context.packageName) + if (context !is Activity) { + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + context.startActivity(intent) + } catch (e: Exception) { + ALog.e(TAG, "requestFloatWindowPermission Exception", e) + } + } +} diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/floating/FloatingTouchEventStrategy.kt b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/floating/FloatingTouchEventStrategy.kt new file mode 100644 index 0000000..3f56d6b --- /dev/null +++ b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/floating/FloatingTouchEventStrategy.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2022 NetEase, Inc. All rights reserved. + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ + +package com.netease.yunxin.nertc.ui.floating + +import android.view.MotionEvent + +/** + * 浮窗手势策略 + * 通过代理将相关手势行为放在此类中实现 + */ +interface FloatingTouchEventStrategy { + /** + * 初始化 + */ + fun initForWrapper(wrapper: FloatingWindowWrapper) + + /** + * 处理 touch 事件 + */ + fun handScrollEvent(event: MotionEvent, windowWrapper: FloatingWindowWrapper) + + /** + * 更新浮窗UI + */ + fun toUpdateViewUI(wrapper: FloatingWindowWrapper) +} diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/floating/FloatingWindowWrapper.kt b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/floating/FloatingWindowWrapper.kt new file mode 100644 index 0000000..46819b8 --- /dev/null +++ b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/floating/FloatingWindowWrapper.kt @@ -0,0 +1,308 @@ +/* + * Copyright (c) 2022 NetEase, Inc. All rights reserved. + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ + +package com.netease.yunxin.nertc.ui.floating + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.PixelFormat +import android.os.Build +import android.view.Gravity +import android.view.MotionEvent +import android.view.View +import android.view.ViewConfiguration +import android.view.WindowManager +import android.widget.FrameLayout +import androidx.annotation.Px +import androidx.core.view.setPadding +import com.netease.yunxin.kit.alog.ALog +import kotlin.math.abs + +/** + * 提供窗口浮窗能力,用户将子 view 添加到此容器内实现浮动功能 + * 浮窗的手势也是作用在此类上,具体实现委托[FloatingTouchEventStrategy] + */ +@SuppressLint("ViewConstructor") +class FloatingWindowWrapper(context: Context, val config: Config) : FrameLayout(context) { + private val logTag = "FloatingWindowWrapper" + + private val wm: WindowManager = + (context.getSystemService(Context.WINDOW_SERVICE) as? WindowManager) + ?: throw IllegalArgumentException() + + /** + * 触摸识别距离 + */ + private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop + + private var currentDownX: Int = 0 + private var currentDownY: Int = 0 + + /** + * [FloatingWindowWrapper] 触摸事件分发处理 + */ + private val onTouchListener = object : OnTouchListener { + + private var handled = false + + @SuppressLint("ClickableViewAccessibility") + override fun onTouch(v: View?, event: MotionEvent?): Boolean { + event ?: return false + ALog.i( + logTag, + "x is ${event.x} , y is ${event.y} , rawX is ${event.rawX}, rawY is ${event.rawY}. detail is $event" + ) + when (event.action) { + MotionEvent.ACTION_DOWN -> { + handled = false + } + + MotionEvent.ACTION_MOVE -> { + handled = canHandle(event) + } + + MotionEvent.ACTION_UP -> { + if (!handled && abs(currentDownX - event.rawX) <= touchSlop && abs( + currentDownY - event.rawY + ) <= touchSlop + ) { + config.onClickListener?.onClick(this@FloatingWindowWrapper) + } + } + } + config.touchEventStrategy?.handScrollEvent(event, this@FloatingWindowWrapper) + return false + } + } + + init { + setOnTouchListener(onTouchListener) + } + + /** + * 拦截子 view 触摸事件 + */ + override fun onInterceptTouchEvent(event: MotionEvent?): Boolean { + event ?: return false + ALog.i( + logTag, + "onInterceptTouchEvent x is ${event.x} , y is ${event.y} , rawX is ${event.rawX}, rawY is ${event.rawY}. detail is $event" + ) + if (event.action == MotionEvent.ACTION_DOWN) { + currentDownX = event.rawX.toInt() + currentDownY = event.rawY.toInt() + config.touchEventStrategy?.handScrollEvent(event, this@FloatingWindowWrapper) + } + return canHandle(event) + } + + private fun canHandle(event: MotionEvent): Boolean { + return abs(currentDownX - event.rawX) > touchSlop || abs(currentDownY - event.rawY) > touchSlop + } + + // -------------------------对外接口--------------------------------- + + /** + * 添加子 view 到浮窗展示 + */ + fun showView(view: View) { + if (view.parent != null) { + throw IllegalArgumentException( + "The view had been added other ViewGroup can't be shown." + ) + } + addView(view) + try { + config.touchEventStrategy?.initForWrapper(this) + wm.addView(this, config.windowParams) + } catch (exception: Exception) { + ALog.w(logTag, "showView", exception) + } + } + + /** + * 页面更新内容及UI时让点击事件感知 + */ + fun toUpdateViewContent() { + config.touchEventStrategy?.toUpdateViewUI(this) + } + + /** + * 隐藏浮窗 + */ + fun dismissView() { + removeAllViews() + try { + background = null + wm.removeView(this) + } catch (exception: Exception) { + ALog.w(logTag, "dismiss", exception) + } + } + + fun fetchWindowParamsX() = config.windowParams.x + + fun fetchWindowParamsY() = config.windowParams.y + + @JvmOverloads + fun updateWindowParamsPos( + xPos: Int = config.windowParams.x, + yPos: Int = config.windowParams.y + ) { + if (!isAttachedToWindow) { + return + } + ALog.i(logTag, "xPos is $xPos, yPos is $yPos") + config.windowParams.x = xPos + config.windowParams.y = yPos + wm.updateViewLayout(this, config.windowParams) + } + + // ------------------------相关辅助类-------------------------------- + /** + * 浮窗配置 + */ + + class Config( + /** + * 触摸事件处理策略 + */ + val touchEventStrategy: FloatingTouchEventStrategy?, + /** + * window 参数配置 + */ + val windowParams: WindowManager.LayoutParams, + /** + * view 点击事件监听 + */ + val onClickListener: OnClickListener? + ) + + /** + * 构建器,将多种配置参数收集 + */ + class Builder { + private var windowParams: WindowManager.LayoutParams? = null + private var windowType: Int? = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY + } else { + WindowManager.LayoutParams.TYPE_SYSTEM_ALERT + } + private var windowFlags: Int? = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or + WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or + WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED or + WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or + WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON + private var windowFormat: Int? = PixelFormat.TRANSLUCENT + private var windowGravity: Int? = Gravity.END or Gravity.TOP + private var windowWidth: Int? = WindowManager.LayoutParams.WRAP_CONTENT + private var windowHeight: Int? = WindowManager.LayoutParams.WRAP_CONTENT + private var windowXPos: Int? = null + private var windowYPos: Int? = null + private var windowAnimations: Int? = null + + private var touchEventStrategy: FloatingTouchEventStrategy? = null + private var onClickListener: OnClickListener? = null + + private var padding: Int? = null + + fun windowLayoutParams(params: WindowManager.LayoutParams) = apply { + this.windowParams = params + } + + fun windowType(type: Int) = apply { + this.windowType = type + } + + fun windowFlags(flags: Int) = apply { + this.windowFlags = flags + } + + fun windowFormat(format: Int) = apply { + this.windowFormat = format + } + + fun windowGravity(gravity: Int) = apply { + this.windowGravity = gravity + } + + fun windowWidth(width: Int) = apply { + this.windowWidth = width + } + + fun windowHeight(height: Int) = apply { + this.windowHeight = height + } + + fun windowXPos(x: Int) = apply { + this.windowXPos = x + } + + fun windowYPos(y: Int) = apply { + this.windowYPos = y + } + + fun windowAnimations(animations: Int) = apply { + this.windowAnimations = animations + } + + fun touchEventStrategy(touchEventStrategy: FloatingTouchEventStrategy) = apply { + this.touchEventStrategy = touchEventStrategy + } + + fun onClickListener(onClickListener: OnClickListener) = apply { + this.onClickListener = onClickListener + } + + fun padding(@Px paddingSize: Int) = apply { + this.padding = paddingSize + } + + fun build(context: Context): FloatingWindowWrapper { + return FloatingWindowWrapper( + context, + Config( + touchEventStrategy ?: DefaultFloatingTouchEventStrategy(context), + (windowParams ?: WindowManager.LayoutParams()).apply { + this@Builder.windowAnimations?.run { + windowAnimations = this + } + windowWidth?.run { + width = this + } + windowHeight?.run { + height = this + } + windowType?.run { + type = this + } + windowFlags?.run { + flags = this + } + windowFormat?.run { + format = this + } + windowGravity?.run { + gravity = this + } + windowXPos?.run { + x = this + } + windowYPos?.run { + y = this + } + }, + onClickListener + ) + ).apply { + padding?.run { + setPadding(this) + } + } + } + } +} diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/CallUIFloatingWindowMgr.kt b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/CallUIFloatingWindowMgr.kt new file mode 100644 index 0000000..331af99 --- /dev/null +++ b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/CallUIFloatingWindowMgr.kt @@ -0,0 +1,290 @@ +/* + * Copyright (c) 2022 NetEase, Inc. All rights reserved. + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ + +package com.netease.yunxin.nertc.ui.p2p + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.view.View +import android.view.WindowManager +import com.netease.yunxin.kit.alog.ALog +import com.netease.yunxin.kit.alog.ParameterMap +import com.netease.yunxin.kit.call.p2p.NECallEngine +import com.netease.yunxin.kit.call.p2p.model.NECallEndInfo +import com.netease.yunxin.kit.call.p2p.model.NECallEngineDelegateAbs +import com.netease.yunxin.kit.call.p2p.model.NECallType +import com.netease.yunxin.kit.call.p2p.model.NECallTypeChangeInfo +import com.netease.yunxin.nertc.nertcvideocall.model.SwitchCallState +import com.netease.yunxin.nertc.nertcvideocall.model.impl.state.CallState +import com.netease.yunxin.nertc.ui.CallKitUI +import com.netease.yunxin.nertc.ui.base.CallParam +import com.netease.yunxin.nertc.ui.base.Constants +import com.netease.yunxin.nertc.ui.base.launchTask +import com.netease.yunxin.nertc.ui.floating.FloatingTouchEventStrategy +import com.netease.yunxin.nertc.ui.floating.FloatingWindowWrapper +import com.netease.yunxin.nertc.ui.utils.SwitchCallTypeConfirmDialog + +/** + * 浮窗整体逻辑: + */ + +object CallUIFloatingWindowMgr { + + private const val TAG = "CallUIFloatingWindowMgr" + private const val REQUEST_CODE_FLOAT = 21301 + + private var floatContentView: IFloatingView? = null + private var floatingWindowWrapper: FloatingWindowWrapper? = null + private var dialog: SwitchCallTypeConfirmDialog? = null + private var dialogFlag: Any? = null + + /** + * 用于用户展示自己的切换弹窗样式 + */ + private var switchCallTypeConfirmDialogProvider: ((Activity) -> SwitchCallTypeConfirmDialog?)? = null + + /** + * 配置通话结束及通话类型变化监听 + */ + private val delegate = object : NECallEngineDelegateAbs() { + override fun onCallTypeChange(info: NECallTypeChangeInfo?) { + super.onCallTypeChange(info) + info ?: return + + if (info.state == SwitchCallState.INVITE && dialog?.isShowing != true) { + showSwitchCallTypeConfirmDialog(info) + return + } + + if (info.state != SwitchCallState.ACCEPT) { + return + } + if (info.callType == NECallType.VIDEO) { + // 视频 + CallUIOperationsMgr.doMuteVideo(false) + // 扬声器 + CallUIOperationsMgr.doConfigSpeaker(true) + // 音频 + CallUIOperationsMgr.doMuteAudio(false) + // 背景虚化 + CallUIOperationsMgr.doVirtualBlur(false) + } else { + // 视频 + CallUIOperationsMgr.doMuteVideo(true) + // 扬声器 + CallUIOperationsMgr.doConfigSpeaker(false) + // 音频 + CallUIOperationsMgr.doMuteAudio(false) + // 背景虚化 + CallUIOperationsMgr.doVirtualBlur(false) + } + // 初始化状态,不同通话类型 + if (info.callType == NECallType.AUDIO) { + floatContentView?.transToAudioUI() + } else if (info.callType == NECallType.VIDEO) { + floatContentView?.transToVideoUI() + } + // 用于浮窗在左侧时变化ui时浮窗没有吸附 + floatingWindowWrapper?.toUpdateViewContent() + } + + override fun onCallEnd(info: NECallEndInfo?) { + super.onCallEnd(info) + innerRelease(true) + } + } + +// ----------------------------------------------------- + + /** + * 展示悬浮窗 + * + * @param context 上下文 + */ + @JvmOverloads + fun showFloat( + context: Context, + uiConfig: P2PUIConfig? = null, + floatingView: IFloatingView? = null, + windowLayoutParams: WindowManager.LayoutParams? = null, + touchEventStrategy: FloatingTouchEventStrategy? = null + + ) { + ALog.dApi( + TAG, + ParameterMap("showFloat").append("context", context).append("uiConfig", uiConfig) + ) + NECallEngine.sharedInstance().addCallDelegate(delegate) + if (floatingView != null && floatingView !is View) { + throw IllegalArgumentException("floatingView$floatingView must be a view instance.") + } + floatContentView = floatingView ?: FloatingView(context) + val builder = FloatingWindowWrapper.Builder() + .windowYPos(400) + windowLayoutParams?.run { + builder.windowLayoutParams(this) + } + touchEventStrategy?.run { + builder.touchEventStrategy(this) + } + floatingWindowWrapper = builder.build(context) + .apply { + showView(floatContentView as View) + } + floatContentView?.run { + toInit() + when (CallUIOperationsMgr.callInfoWithUIState.callParam.callType) { + NECallType.AUDIO -> { + transToAudioUI() + } + + NECallType.VIDEO -> { + transToVideoUI() + } + + else -> { + ALog.e(TAG, "unknown call type") + } + } + } + // 避免在全屏/小窗切换时丢失音视频切换弹窗 + val info = CallUIOperationsMgr.currentSwitchTypeCallInfo() ?: return + if (dialog?.isShowing == true || info.state != SwitchCallState.INVITE) { + return + } + showSwitchCallTypeConfirmDialog(info) + } + + fun registerFullScreenActionForView(context: Context, view: View) { + view.setOnClickListener { + CallUIOperationsMgr.callInfoWithUIState.run { + if (callState != CallState.STATE_IDLE) { + val callType = CallUIOperationsMgr.callInfoWithUIState.callParam.callType + val clazz = + when (CallUIOperationsMgr.callInfoWithUIState.callParam.callType) { + NECallType.AUDIO -> CallKitUI.options?.activityConfig?.p2pAudioActivity + NECallType.VIDEO -> CallKitUI.options?.activityConfig?.p2pVideoActivity + else -> null + } + if (clazz != null) { + innerRelease(false) + launch(context, clazz, callParam) + } else { + innerRelease(true) + ALog.e( + TAG, + "launch activity failed, clazz is null, callType is $callType." + ) + } + } else { + innerRelease(true) + } + } + } + } + + /** + * 配置音视频通话切换确认弹窗 + */ + fun configSwitchCallTypeConfirmDialogProvider(provider: (Activity) -> SwitchCallTypeConfirmDialog?) { + this.switchCallTypeConfirmDialogProvider = provider + } + + /** + * 销毁悬浮窗 + */ + fun releaseFloat(isFinished: Boolean = true) { + ALog.dApi(TAG, ParameterMap("releaseFloat")) + innerRelease(isFinished) + } + + /** + * 是否正在展示浮窗 + */ + fun isFloating(): Boolean { + return floatingWindowWrapper != null + } + + /** + * 展示音视频切换确认弹窗 + */ + private fun showSwitchCallTypeConfirmDialog(info: NECallTypeChangeInfo) { + if (dialogFlag != null) { + return + } + dialogFlag = Any() + floatingWindowWrapper?.context?.run { + launchTask(this, REQUEST_CODE_FLOAT, { activity, _ -> + ALog.d(TAG, "showSwitchCallTypeConfirmDialog") + dialog = + switchCallTypeConfirmDialogProvider?.invoke(activity) ?: SwitchCallTypeConfirmDialog( + activity, + { + CallUIOperationsMgr.doSwitchCallType( + info.callType, + SwitchCallState.ACCEPT, + null + ) + }, + { + CallUIOperationsMgr.doSwitchCallType( + info.callType, + SwitchCallState.REJECT, + null + ) + } + ).apply { + setOnDismissListener { + activity.finish() + } + show(info.callType) + val latestInfo = CallUIOperationsMgr.currentSwitchTypeCallInfo() + if (latestInfo?.state != SwitchCallState.INVITE) { + dismiss() + } + } + dialogFlag = null + }, {}) + } + } + + private fun innerRelease(isFinished: Boolean) { + ALog.d(TAG, ParameterMap("innerRelease").toValue()) + NECallEngine.sharedInstance().removeCallDelegate(delegate) + CallUIOperationsMgr.configTimeTick(null) + floatContentView?.toDestroy(isFinished) + floatingWindowWrapper?.dismissView() + floatingWindowWrapper = null + dialog?.dismiss() + dialog = null + dialogFlag = null + } + + /** + * 启动对应页面 + */ + private fun launch( + context: Context, + clazz: Class, + callParam: CallParam + ) { + ALog.dApi( + TAG, + ParameterMap("launch") + .append("context", context) + .append("callParam", callParam) + ) + val intent = Intent(context, clazz).apply { + putExtra(Constants.PARAM_KEY_CALL, callParam) + if (context !is Activity) { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + putExtra(Constants.PARAM_KEY_FLAG_IS_FROM_FLOATING_WINDOW, true) + } + context.startActivity(intent) + } +} diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/CallUIOperationsMgr.kt b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/CallUIOperationsMgr.kt index ac20207..4247280 100644 --- a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/CallUIOperationsMgr.kt +++ b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/CallUIOperationsMgr.kt @@ -9,15 +9,20 @@ package com.netease.yunxin.nertc.ui.p2p import android.annotation.SuppressLint import android.app.NotificationManager import android.content.Context +import android.content.Intent import android.media.AudioManager +import android.os.Build import com.netease.lava.api.IVideoRender import com.netease.lava.nertc.sdk.NERtcConstants import com.netease.lava.nertc.sdk.NERtcEx import com.netease.lava.nertc.sdk.video.NERtcVideoView +import com.netease.lava.nertc.sdk.video.NERtcVirtualBackgroundSource import com.netease.yunxin.kit.alog.ALog import com.netease.yunxin.kit.alog.ParameterMap import com.netease.yunxin.kit.call.NEResultObserver import com.netease.yunxin.kit.call.p2p.NECallEngine +import com.netease.yunxin.kit.call.p2p.extra.NECallLocalActionMgr +import com.netease.yunxin.kit.call.p2p.extra.NECallLocalActionObserver import com.netease.yunxin.kit.call.p2p.model.NECallEndInfo import com.netease.yunxin.kit.call.p2p.model.NECallEngineDelegateAbs import com.netease.yunxin.kit.call.p2p.model.NECallInfo @@ -26,14 +31,17 @@ import com.netease.yunxin.kit.call.p2p.param.NECallParam import com.netease.yunxin.kit.call.p2p.param.NEHangupParam import com.netease.yunxin.kit.call.p2p.param.NESwitchParam import com.netease.yunxin.nertc.nertcvideocall.bean.CommonResult +import com.netease.yunxin.nertc.nertcvideocall.model.CallLocalAction import com.netease.yunxin.nertc.nertcvideocall.model.SwitchCallState import com.netease.yunxin.nertc.nertcvideocall.model.impl.NERtcCallbackExTemp import com.netease.yunxin.nertc.nertcvideocall.model.impl.NERtcCallbackProxyMgr import com.netease.yunxin.nertc.nertcvideocall.model.impl.state.CallState +import com.netease.yunxin.nertc.ui.CallKitNotificationConfig import com.netease.yunxin.nertc.ui.CallKitUI import com.netease.yunxin.nertc.ui.base.CallParam import com.netease.yunxin.nertc.ui.base.channelId import com.netease.yunxin.nertc.ui.base.getChannelId +import com.netease.yunxin.nertc.ui.service.CallForegroundService import com.netease.yunxin.nertc.ui.service.DefaultIncomingCallEx.Companion.INCOMING_CALL_NOTIFY_ID import com.netease.yunxin.nertc.ui.utils.SecondsTimer @@ -47,11 +55,15 @@ object CallUIOperationsMgr { var callInfoWithUIState: CallInfoWithUIState = CallInfoWithUIState() private set + var timer: SecondsTimer? = null + private set + private val callEngineDelegate = object : NECallEngineDelegateAbs() { override fun onCallTypeChange(info: NECallTypeChangeInfo) { if (info.state == SwitchCallState.ACCEPT) { callInfoWithUIState.callParam.callType = info.callType } + toSwitchCallTypeInfo = info } override fun onVideoAvailable(userId: String?, available: Boolean) { @@ -91,9 +103,21 @@ object CallUIOperationsMgr { this@CallUIOperationsMgr.timer?.cancel() this@CallUIOperationsMgr.timer = null this@CallUIOperationsMgr.timeTickConfig = null + this@CallUIOperationsMgr.foregroundServiceConfig?.stopService(true) + this@CallUIOperationsMgr.doVirtualBlurInner(false) } } + private val localActionObserver = + NECallLocalActionObserver { actionId, _, extraInfo -> + if (actionId == CallLocalAction.ACTION_SWITCH && + extraInfo is NECallTypeChangeInfo && + extraInfo.state != SwitchCallState.INVITE + ) { + toSwitchCallTypeInfo = extraInfo + } + } + private val neRtcCallback = object : NERtcCallbackExTemp() { override fun onVideoDeviceStageChange(deviceState: Int) { if (currentCallState() == CallState.STATE_IDLE) { @@ -103,27 +127,35 @@ object CallUIOperationsMgr { } } - private var timeTickConfig: TimeTickConfig? = null + private lateinit var context: Context - private var timer: SecondsTimer? = null + private var toSwitchCallTypeInfo: NECallTypeChangeInfo? = null - private lateinit var context: Context + private var foregroundServiceConfig: CallForegroundServiceConfig? = null + + private var timeTickConfig: TimeTickConfig? = null + private var startPreviewCode: Int? = null /** * 初始化呼叫信息及状态 */ - fun initCallInfoAndUIState(callInfoWithUIState: CallInfoWithUIState): String? { - callEngine.addCallDelegate(callEngineDelegate) - NERtcCallbackProxyMgr.getInstance().addCallback(neRtcCallback) + @JvmOverloads + fun initCallInfoAndUIState( + callInfoWithUIState: CallInfoWithUIState, + foregroundServiceConfig: CallForegroundServiceConfig? = null + ): String? { ALog.d( TAG, - ParameterMap("initCallInfoAndUIState").append( - "callInfoWithUIState", - callInfoWithUIState - ).toValue() + ParameterMap("initCallInfoAndUIState") + .append("callInfoWithUIState", callInfoWithUIState) + .append("foregroundServiceConfig", foregroundServiceConfig) + .toValue() ) this.callInfoWithUIState = callInfoWithUIState + this.foregroundServiceConfig = foregroundServiceConfig + this.toSwitchCallTypeInfo = null + this.foregroundServiceConfig?.startService() return this.callInfoWithUIState.callParam.getChannelId() } @@ -143,8 +175,9 @@ object CallUIOperationsMgr { ) return } - callEngine.removeCallDelegate(callEngineDelegate) - NERtcCallbackProxyMgr.getInstance().removeCallback(neRtcCallback) + this.foregroundServiceConfig?.stopService(force) + this.foregroundServiceConfig = null + this.toSwitchCallTypeInfo = null this.callInfoWithUIState = CallInfoWithUIState() this.timer?.cancel() this.timer = null @@ -168,7 +201,9 @@ object CallUIOperationsMgr { isLocalMuteVideo: Boolean? = null, isLocalMuteAudio: Boolean? = null, isLocalMuteSpeaker: Boolean? = null, - cameraDeviceStatus: Int? = null + cameraDeviceStatus: Int? = null, + isLocalSmallVideo: Boolean? = null, + isVirtualBlur: Boolean? = null ) { ALog.dApi( TAG, @@ -178,6 +213,8 @@ object CallUIOperationsMgr { .append("isLocalMuteAudio", isLocalMuteAudio) .append("isLocalMuteSpeaker", isLocalMuteSpeaker) .append("cameraDeviceStatus", cameraDeviceStatus) + .append("isLocalSmallVideo", isLocalSmallVideo) + .append("isVirtualBlur", isVirtualBlur) ) isRemoteMuteVideo?.run { callInfoWithUIState.isRemoteMuteVideo = this @@ -194,6 +231,12 @@ object CallUIOperationsMgr { cameraDeviceStatus?.run { callInfoWithUIState.cameraDeviceStatus = this } + isLocalSmallVideo?.run { + callInfoWithUIState.isLocalSmallVideo = this + } + isVirtualBlur?.run { + callInfoWithUIState.isVirtualBlur = this + } } /** @@ -221,7 +264,31 @@ object CallUIOperationsMgr { ) { result -> ALog.d(TAG, "call result is $result.") observer?.onResult(result) - callParam.channelId(callEngine.callInfo.signalInfo.channelId) + callInfoWithUIState.callParam.channelId(callEngine.callInfo.signalInfo.channelId) + } + } + } + + /** + * 发起呼叫 + */ + fun doCall( + callParam: NECallParam, + observer: NEResultObserver>? + ) { + ALog.dApi( + TAG, + ParameterMap("doCall") + .append("callParam", callParam) + .append("observer", observer) + ) + with(callParam) { + callEngine.call( + this + ) { result -> + ALog.d(TAG, "call result is $result.") + observer?.onResult(result) + callInfoWithUIState.callParam.channelId(callEngine.callInfo.signalInfo.channelId) } } } @@ -298,7 +365,9 @@ object CallUIOperationsMgr { ParameterMap("setupLocalView").append("view", view).append("action", action) ) action?.invoke(view) - callEngine.setupLocalView(view) + callEngine.setupLocalView(view).apply { + ALog.d(TAG, "setupLocalView result is $this.") + } } /** @@ -316,7 +385,9 @@ object CallUIOperationsMgr { ParameterMap("setupRemoteView").append("view", view).append("action", action) ) action?.invoke(view) - callEngine.setupRemoteView(view) + callEngine.setupRemoteView(view).apply { + ALog.d(TAG, "setupRemoteView result is $this.") + } } /** @@ -325,8 +396,12 @@ object CallUIOperationsMgr { @JvmOverloads fun doConfigSpeaker(enableSpeaker: Boolean = !isSpeakerOn()) { val action = { - callInfoWithUIState.isLocalMuteSpeaker = !enableSpeaker - NERtcEx.getInstance().isSpeakerphoneOn = enableSpeaker + NERtcEx.getInstance().setSpeakerphoneOn(enableSpeaker).apply { + ALog.d(TAG, "doConfigSpeaker result is $this.") + if (this == NERtcConstants.ErrorCode.OK) { + callInfoWithUIState.isLocalMuteSpeaker = !enableSpeaker + } + } } if (CallKitUI.options?.joinRtcWhenCall == true && !callInfoWithUIState.callParam.isCalled) { action.invoke() @@ -346,7 +421,13 @@ object CallUIOperationsMgr { mode = AudioManager.MODE_NORMAL isSpeakerphoneOn = true } else { - mode = AudioManager.MODE_IN_CALL + // 兼容高版本 sdk + @SuppressLint("ObsoleteSdkInt") + mode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + AudioManager.MODE_IN_COMMUNICATION + } else { + AudioManager.MODE_IN_CALL + } isSpeakerphoneOn = false } } @@ -380,8 +461,12 @@ object CallUIOperationsMgr { @JvmOverloads fun doMuteAudio(mute: Boolean = !callInfoWithUIState.isLocalMuteAudio) { ALog.dApi(TAG, ParameterMap("doMuteAudio").append("mute", mute)) - callInfoWithUIState.isLocalMuteAudio = mute - callEngine.muteLocalAudio(callInfoWithUIState.isLocalMuteAudio) + callEngine.muteLocalAudio(mute).apply { + ALog.d(TAG, "doMuteAudio result is $this.") + if (this == NERtcConstants.ErrorCode.OK) { + callInfoWithUIState.isLocalMuteAudio = mute + } + } } /** @@ -390,8 +475,12 @@ object CallUIOperationsMgr { @JvmOverloads fun doMuteVideo(mute: Boolean = !callInfoWithUIState.isLocalMuteVideo) { ALog.dApi(TAG, ParameterMap("doMuteVideo").append("mute", mute)) - callInfoWithUIState.isLocalMuteVideo = mute - callEngine.muteLocalVideo(callInfoWithUIState.isLocalMuteVideo) + callEngine.muteLocalVideo(mute).apply { + ALog.d(TAG, "doMuteVideo result is $this.") + if (this == NERtcConstants.ErrorCode.OK) { + callInfoWithUIState.isLocalMuteVideo = mute + } + } } /** @@ -399,7 +488,70 @@ object CallUIOperationsMgr { */ fun doSwitchCamera() { ALog.dApi(TAG, ParameterMap("doSwitchCamera")) - callEngine.switchCamera() + callEngine.switchCamera().apply { + ALog.d(TAG, "doSwitchCamera result is $this.") + } + } + + /** + * 开启/关闭背景虚化 + */ + fun doVirtualBlur(enable: Boolean = !callInfoWithUIState.isVirtualBlur) { + ALog.dApi(TAG, ParameterMap("doVirtualBlur").append("enable", enable)) + doVirtualBlurInner(enable) + } + + /** + * 内部开启/关闭背景虚化 + */ + private fun doVirtualBlurInner(enable: Boolean) { + ALog.d(TAG, ParameterMap("doVirtualBlurInner").append("enable", enable).toValue()) + NERtcEx.getInstance().enableVirtualBackground( + enable, + NERtcVirtualBackgroundSource().apply { + backgroundSourceType = NERtcVirtualBackgroundSource.BACKGROUND_BLUR + blur_degree = NERtcVirtualBackgroundSource.BLUR_DEGREE_HIGH + } + ).apply { + ALog.d(TAG, "doVirtualBlurInner result is $this.") + if (this == NERtcConstants.ErrorCode.OK) { + callInfoWithUIState.isVirtualBlur = enable + } + } + } + + /** + * 开启视频预览 + */ + fun startVideoPreview() { + ALog.dApi( + TAG, + ParameterMap("startVideoPreview").append("startPreviewCode", startPreviewCode) + ) + if ((CallKitUI.options?.joinRtcWhenCall == false || callInfoWithUIState.callParam.isCalled) && + startPreviewCode != NERtcConstants.ErrorCode.OK && + startPreviewCode != NERtcConstants.ErrorCode.ENGINE_ERROR_DEVICE_PREVIEW_ALREADY_STARTED + ) { + startPreviewCode = NERtcEx.getInstance().startVideoPreview().apply { + ALog.d(TAG, "startVideoPreview result is $this.") + } + } + } + + /** + * 停止视频预览 + */ + fun stopVideoPreview() { + ALog.dApi( + TAG, + ParameterMap("stopVideoPreview").append("startPreviewCode", startPreviewCode) + ) + if (startPreviewCode == NERtcConstants.ErrorCode.OK || startPreviewCode == NERtcConstants.ErrorCode.ENGINE_ERROR_DEVICE_PREVIEW_ALREADY_STARTED) { + NERtcEx.getInstance().stopVideoPreview().apply { + ALog.d(TAG, "stopVideoPreview result is $this.") + } + startPreviewCode = null + } } /** @@ -426,14 +578,25 @@ object CallUIOperationsMgr { return callInfoWithUIState.callState } + /** + * 获取当前音视频切换事件 + */ + fun currentSwitchTypeCallInfo(): NECallTypeChangeInfo? = toSwitchCallTypeInfo + internal fun load(context: Context) { ALog.dApi(TAG, ParameterMap("load")) - this.context = context.applicationContext releaseCallInfoAndUIState(force = true) + this.context = context.applicationContext + callEngine.addCallDelegate(callEngineDelegate) + NECallLocalActionMgr.getInstance().addCallback(localActionObserver) + NERtcCallbackProxyMgr.getInstance().addCallback(neRtcCallback) } internal fun unload() { ALog.dApi(TAG, ParameterMap("unload")) + callEngine.removeCallDelegate(callEngineDelegate) + NECallLocalActionMgr.getInstance().removeCallback(localActionObserver) + NERtcCallbackProxyMgr.getInstance().removeCallback(neRtcCallback) releaseCallInfoAndUIState(force = true) } @@ -450,13 +613,15 @@ object CallUIOperationsMgr { ?.getSystemService(Context.AUDIO_SERVICE) as? AudioManager )?.isSpeakerphoneOn ?: !NERtcEx.getInstance().isSpeakerphoneOn, - var cameraDeviceStatus: Int = NERtcConstants.VideoDeviceState.OPENED + var cameraDeviceStatus: Int = NERtcConstants.VideoDeviceState.OPENED, + var isLocalSmallVideo: Boolean = true, + var isVirtualBlur: Boolean = false ) { val callState: Int get() = callEngine.callInfo.callStatus override fun toString(): String { - return "CallInfoWithUIState(callParam=$callParam, isRemoteMuteVideo=$isRemoteMuteVideo, isLocalMuteVideo=$isLocalMuteVideo, isLocalMuteAudio=$isLocalMuteAudio, isLocalMuteSpeaker=$isLocalMuteSpeaker, cameraDeviceStatus=$cameraDeviceStatus)" + return "CallInfoWithUIState(callParam=$callParam, isRemoteMuteVideo=$isRemoteMuteVideo, isLocalMuteVideo=$isLocalMuteVideo, isLocalMuteAudio=$isLocalMuteAudio, isLocalMuteSpeaker=$isLocalMuteSpeaker, cameraDeviceStatus=$cameraDeviceStatus, isLocalSmallVideo=$isLocalSmallVideo, isVirtualBlur=$isVirtualBlur)" } } @@ -469,4 +634,37 @@ object CallUIOperationsMgr { return "TimeTickConfig(onTimeTick=$onTimeTick, period=$period, delay=$delay)" } } + + class CallForegroundServiceConfig( + context: Context, + private val intent: Intent, + private val notificationConfig: CallKitNotificationConfig? = null + ) { + + private var serviceId: String? = null + private var context = context.applicationContext + + fun startService() { + serviceId = CallForegroundService.launchForegroundService( + context, + intent, + notificationConfig + ) + } + + fun stopService(force: Boolean = false) { + if (force) { + CallForegroundService.stopService(context) + } else { + serviceId?.run { + CallForegroundService.stopService(context, this) + } + } + serviceId = null + } + + override fun toString(): String { + return "CallForegroundServiceConfig(context=$context, intent=$intent, notificationConfig=$notificationConfig)" + } + } } diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/FloatingView.kt b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/FloatingView.kt new file mode 100644 index 0000000..c65a1eb --- /dev/null +++ b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/FloatingView.kt @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2022 NetEase, Inc. All rights reserved. + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ + +package com.netease.yunxin.nertc.ui.p2p + +import android.Manifest.permission.CAMERA +import android.Manifest.permission.RECORD_AUDIO +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.widget.FrameLayout +import android.widget.Toast +import com.netease.yunxin.kit.alog.ALog +import com.netease.yunxin.kit.alog.ParameterMap +import com.netease.yunxin.kit.call.p2p.NECallEngine +import com.netease.yunxin.kit.call.p2p.model.NECallEngineDelegateAbs +import com.netease.yunxin.nertc.nertcvideocall.model.impl.state.CallState +import com.netease.yunxin.nertc.ui.R +import com.netease.yunxin.nertc.ui.base.loadAvatarByAccId +import com.netease.yunxin.nertc.ui.databinding.ViewFloatingWindowBinding +import com.netease.yunxin.nertc.ui.utils.dip2Px +import com.netease.yunxin.nertc.ui.utils.formatSecondTime +import com.netease.yunxin.nertc.ui.utils.isGranted +import com.netease.yunxin.nertc.ui.utils.requestPermission + +class FloatingView(context: Context) : FrameLayout(context), IFloatingView { + private val logTag = "FloatingView" + private val binding by lazy { + ViewFloatingWindowBinding.inflate(LayoutInflater.from(context), this, true) + } + private val delegate = object : NECallEngineDelegateAbs() { + override fun onVideoAvailable(userId: String?, available: Boolean) { + if (available) { + binding.videoViewSmall.visibility = View.VISIBLE + CallUIOperationsMgr.setupRemoteView(binding.videoViewSmall) + } else { + binding.videoViewSmall.visibility = View.GONE + } + } + + override fun onVideoMuted(userId: String?, mute: Boolean) { + if (!mute) { + binding.videoViewSmall.visibility = View.VISIBLE + CallUIOperationsMgr.setupRemoteView(binding.videoViewSmall) + } else { + binding.videoViewSmall.visibility = View.GONE + } + } + } + + override fun toInit() { + NECallEngine.sharedInstance().addCallDelegate(delegate) + CallUIOperationsMgr.configTimeTick( + CallUIOperationsMgr.TimeTickConfig(onTimeTick = { timestamp: Long -> + postCountDownTxt(timestamp.formatSecondTime()) + }) + ) + CallUIFloatingWindowMgr.registerFullScreenActionForView(context, this) + } + + override fun transToAudioUI() { + ALog.dApi(logTag, ParameterMap("transToAudioUI")) + val action = { _: Any? -> + binding.floatAudioGroup.visibility = View.VISIBLE + binding.videoViewSmall.visibility = View.GONE + binding.ivAvatar.visibility = View.GONE + binding.videoBg.visibility = View.GONE + if (CallUIOperationsMgr.currentCallState() == CallState.STATE_CALL_OUT) { + binding.tvAudioTip.setText(R.string.ui_floating_window_audio_call) + } else { + binding.tvAudioTip.text = "--:--" + } + binding.root.radius = 6.dip2Px(context).toFloat() + } + if (context.isGranted(RECORD_AUDIO)) { + action.invoke(null) + } else { + handPermissionRequest(listOf(RECORD_AUDIO), null, action) + } + } + + override fun transToVideoUI() { + ALog.dApi(logTag, ParameterMap("transToVideoUI")) + val action = { needToEnableLocalVideo: Boolean -> + if (needToEnableLocalVideo) { + NECallEngine.sharedInstance().enableLocalVideo(true) + } + binding.floatAudioGroup.visibility = View.GONE + binding.ivAvatar.visibility = View.VISIBLE + binding.videoBg.visibility = View.VISIBLE + if (CallUIOperationsMgr.currentCallState() == CallState.STATE_DIALOG) { + val isVideoing = !CallUIOperationsMgr.callInfoWithUIState.isRemoteMuteVideo + if (isVideoing) { + binding.videoViewSmall.visibility = View.VISIBLE + CallUIOperationsMgr.setupRemoteView(binding.videoViewSmall) + } else { + binding.videoViewSmall.visibility = View.GONE + } + } else { + binding.videoViewSmall.visibility = View.VISIBLE + CallUIOperationsMgr.setupLocalView(binding.videoViewSmall) + CallUIOperationsMgr.startVideoPreview() + } + CallUIOperationsMgr.callInfoWithUIState.callParam.otherAccId + ?.loadAvatarByAccId(context, binding.ivAvatar, enableTextDefaultAvatar = false) + binding.root.radius = 0f + } + if (context.isGranted(RECORD_AUDIO, CAMERA)) { + action.invoke(false) + } else { + handPermissionRequest(listOf(RECORD_AUDIO, CAMERA), true, action) + } + } + + override fun toDestroy(isFinished: Boolean) { + CallUIOperationsMgr.configTimeTick(null) + if (isFinished) { + CallUIOperationsMgr.stopVideoPreview() + } + NECallEngine.sharedInstance().removeCallDelegate(delegate) + } + + /** + * 处理权限申请逻辑 + */ + private fun handPermissionRequest( + permissionList: List, + param: T, + action: (T) -> Unit + ) { + context.requestPermission( + { + action.invoke(param) + }, + { _, _ -> + Toast.makeText( + context, + R.string.ui_dialog_permission_content, + Toast.LENGTH_LONG + ).show() + CallUIOperationsMgr.doHangup(null) + }, + permissions = permissionList.toTypedArray() + ) + } + + private fun postCountDownTxt(countDown: String) { + post { + if (binding.floatAudioGroup.visibility == View.VISIBLE) { + binding.tvAudioTip.text = countDown + } + } + } +} diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/IFloatingView.kt b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/IFloatingView.kt new file mode 100644 index 0000000..f1404bb --- /dev/null +++ b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/IFloatingView.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2022 NetEase, Inc. All rights reserved. + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ + +package com.netease.yunxin.nertc.ui.p2p + +interface IFloatingView { + + /** + * 初始化 + */ + fun toInit() + + /** + * 展示音频UI + */ + fun transToAudioUI() + + /** + * 展示视频UI + */ + fun transToVideoUI() + + /** + * 销毁 + */ + fun toDestroy(isFinished: Boolean) +} diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/P2PCallActivity.kt b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/P2PCallActivity.kt deleted file mode 100644 index 68c5c43..0000000 --- a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/P2PCallActivity.kt +++ /dev/null @@ -1,964 +0,0 @@ -/* - * Copyright (c) 2022 NetEase, Inc. All rights reserved. - * Use of this source code is governed by a MIT license that can be - * found in the LICENSE file. - */ - -package com.netease.yunxin.nertc.ui.p2p - -import android.Manifest -import android.content.DialogInterface -import android.graphics.Color -import android.os.Bundle -import android.text.TextUtils -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import androidx.appcompat.app.AlertDialog -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.constraintlayout.widget.ConstraintSet -import androidx.core.content.ContextCompat -import com.bumptech.glide.Glide -import com.netease.lava.nertc.sdk.NERtcConstants -import com.netease.lava.nertc.sdk.NERtcConstants.ErrorCode.ENGINE_ERROR_DEVICE_PREVIEW_ALREADY_STARTED -import com.netease.lava.nertc.sdk.NERtcEx -import com.netease.nimlib.sdk.ResponseCode -import com.netease.yunxin.kit.alog.ALog -import com.netease.yunxin.kit.call.NEResultObserver -import com.netease.yunxin.kit.call.p2p.internal.NECallEngineImpl -import com.netease.yunxin.kit.call.p2p.model.NECallEndInfo -import com.netease.yunxin.kit.call.p2p.model.NECallInfo -import com.netease.yunxin.kit.call.p2p.model.NECallInitRtcMode -import com.netease.yunxin.kit.call.p2p.model.NECallType -import com.netease.yunxin.kit.call.p2p.model.NECallTypeChangeInfo -import com.netease.yunxin.kit.call.p2p.model.NEHangupReasonCode -import com.netease.yunxin.nertc.nertcvideocall.bean.CommonResult -import com.netease.yunxin.nertc.nertcvideocall.model.SwitchCallState -import com.netease.yunxin.nertc.nertcvideocall.model.impl.state.CallState -import com.netease.yunxin.nertc.nertcvideocall.utils.NetworkUtils -import com.netease.yunxin.nertc.ui.CallKitUI -import com.netease.yunxin.nertc.ui.R -import com.netease.yunxin.nertc.ui.base.CommonCallActivity -import com.netease.yunxin.nertc.ui.base.currentUserIsCaller -import com.netease.yunxin.nertc.ui.base.fetchNickname -import com.netease.yunxin.nertc.ui.base.loadAvatarByAccId -import com.netease.yunxin.nertc.ui.databinding.ActivityP2PcallBinding -import com.netease.yunxin.nertc.ui.utils.PermissionTipDialog -import com.netease.yunxin.nertc.ui.utils.SwitchCallTypeConfirmDialog -import com.netease.yunxin.nertc.ui.utils.dip2Px -import com.netease.yunxin.nertc.ui.utils.formatSecondTime -import com.netease.yunxin.nertc.ui.utils.isGranted -import com.netease.yunxin.nertc.ui.utils.requestPermission -import com.netease.yunxin.nertc.ui.utils.toastShort - -open class P2PCallActivity : CommonCallActivity() { - private val tag = "P2PCallActivity" - - private val binding: ActivityP2PcallBinding by lazy { - ActivityP2PcallBinding.inflate(layoutInflater) - } - private var startPreviewCode = -1 - - private var callFinished = true - - private var localIsSmallVideo = true - - private val switchConfirmDialog by lazy { - SwitchCallTypeConfirmDialog(this, { - doSwitchCallType(it, SwitchCallState.ACCEPT) - }, { - doSwitchCallType(it, SwitchCallState.REJECT) - }) - } - - private val onClickListener = View.OnClickListener { v -> - when (v) { - binding.ivAccept -> { - v.isEnabled = false - binding.ivSwitchType.isEnabled = false - binding.ivCallSwitchType.isEnabled = false - doAccept() - } - - binding.ivReject -> { - binding.ivAccept.isEnabled = false - v.isEnabled = false - doHangup() - } - - binding.ivCancel -> { - if (!callFinished) { - getString(R.string.tip_invite_was_sending).toastShort(this) - return@OnClickListener - } - v.isEnabled = false - doHangup() - } - - binding.ivHangUp -> { - v.isEnabled = false - doHangup() - } - - binding.ivCallMuteAudio, - binding.ivMuteAudio -> doMuteAudioSwitch(v as ImageView) - - binding.ivMuteVideo -> doMuteVideo(binding.ivMuteVideo) - binding.ivSwitchCamera -> doSwitchCamera() - binding.ivCallSwitchType, - binding.ivSwitchType, - binding.ivCallChannelTypeChange -> doSwitchCallType() - - binding.ivCallSpeaker, - binding.ivMuteSpeaker -> doConfigSpeakerSwitch(v as ImageView) - - binding.videoViewSmall -> doSwitchCanvas() - else -> ALog.d(tag, "can't response this clicked Event for $v") - } - } - - private val uiRender: UIRender - get() = if (callParam.callType == NECallType.AUDIO) { - AudioRender() - } else { - VideoRender() - } - - override fun onCallConnected(info: NECallInfo) { - if (isFinishing) { - return - } - info.otherUserInfo()?.accId.run { - initForOnTheCall(this) - } - - configTimeTick( - CallUIOperationsMgr.TimeTickConfig({ - runOnUiThread { binding.tvCountdown.text = it.formatSecondTime() } - }) - ) - } - - override fun onCallTypeChange(info: NECallTypeChangeInfo) { - if (isFinishing) { - return - } - when (info.state) { - SwitchCallState.ACCEPT -> { - binding.switchTypeTipGroup.visibility = View.GONE - if (callEngine.callInfo.callStatus != CallState.STATE_DIALOG) { - if (callParam.isCalled) { - uiRender.renderForCalled() - } else { - uiRender.renderForCaller() - } - return - } - initForOnTheCall(callParam.otherAccId) - } - - SwitchCallState.INVITE -> { - switchConfirmDialog.show(info.callType) - } - - SwitchCallState.REJECT -> { - binding.switchTypeTipGroup.visibility = View.GONE - getString(R.string.ui_switch_call_type_reject_tip).toastShort( - this@P2PCallActivity - ) - } - } - } - - override fun onCallEnd(info: NECallEndInfo) { - super.onCallEnd(info) - configTimeTick(null) - when (info.reasonCode) { - NEHangupReasonCode.CALLER_REJECTED -> if (!isFinishing && !callParam.isCalled) { - getString(R.string.tip_reject_by_other).toastShort(this@P2PCallActivity) - } - - NEHangupReasonCode.BUSY -> if (!isFinishing && !callParam.isCalled) { - getString(R.string.tip_busy_by_other).toastShort(this@P2PCallActivity) - } - - NEHangupReasonCode.CALLEE_CANCELED -> if (!isFinishing && callParam.isCalled) { - getString(R.string.tip_cancel_by_other).toastShort(this@P2PCallActivity) - } - - NEHangupReasonCode.TIME_OUT -> - if (!callParam.isCalled) { - getString(R.string.tip_timeout_by_other).toastShort(this@P2PCallActivity) - } - - NEHangupReasonCode.OTHER_REJECTED -> getString(R.string.tip_other_client_other_reject).toastShort( - this@P2PCallActivity - ) - - NEHangupReasonCode.OTHER_ACCEPTED -> getString(R.string.tip_other_client_other_accept).toastShort( - this@P2PCallActivity - ) - } - releaseAndFinish(false) - } - - override fun onVideoAvailable(userId: String?, available: Boolean) { - if (isFinishing) { - return - } - uiRender.updateOnTheCallState(UserState(userId, muteVideo = !available)) - } - - override fun onVideoMuted(userId: String?, mute: Boolean) { - if (isFinishing) { - return - } - uiRender.updateOnTheCallState(UserState(userId, muteVideo = mute)) - } - - override fun doOnCreate(savedInstanceState: Bundle?) { - super.doOnCreate(savedInstanceState) - ALog.d(tag, callParam.toString()) - initForLaunchUI() - val dialog: PermissionTipDialog? - if (!isGranted( - Manifest.permission.CAMERA, - Manifest.permission.RECORD_AUDIO - ) - ) { - dialog = showPermissionDialog { - getString(R.string.tip_permission_request_failed).toastShort(this) - releaseAndFinish(true) - } - } else { - if (callParam.isCalled && callEngine.callInfo.callStatus == CallState.STATE_IDLE) { - releaseAndFinish(false) - return - } - initForLaunchAction() - return - } - requestPermission({ granted -> - if (isFinishing) { - return@requestPermission - } - granted.forEach { - ALog.i(tag, "granted:$it") - } - if (granted.containsAll( - listOf( - Manifest.permission.CAMERA, - Manifest.permission.RECORD_AUDIO - ) - ) - ) { - dialog.dismiss() - if (callParam.isCalled && callEngine.callInfo.callStatus == CallState.STATE_IDLE) { - releaseAndFinish(false) - return@requestPermission - } - initForLaunchAction() - } - ALog.i(tag, "extra info is ${callParam.callExtraInfo}") - }, { deniedForever, denied -> - denied.forEach { - ALog.i(tag, "denied:$it") - } - deniedForever.forEach { - ALog.i(tag, "deniedForever:$it") - } - if (deniedForever.isNotEmpty() || denied.isNotEmpty()) { - getString(R.string.tip_permission_request_failed).toastShort(this@P2PCallActivity) - dialog.dismiss() - releaseAndFinish(true) - } - }, Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO) - } - - override fun provideLayoutView(): View? = binding.root - - override fun releaseAndFinish(finishCall: Boolean) { - super.releaseAndFinish(finishCall) - - if (startPreviewCode == 0 || startPreviewCode == ENGINE_ERROR_DEVICE_PREVIEW_ALREADY_STARTED) { - NERtcEx.getInstance().setupLocalVideoCanvas(null) - NERtcEx.getInstance().stopVideoPreview() - } - - if (finishCall) { - doHangup(null) - } - } - - override fun onBackPressed() { - showExitDialog() - } - - override fun onPause() { - super.onPause() - if (isFinishing) { - switchConfirmDialog.dismiss() - releaseAndFinish(true) - } - } - - private fun showExitDialog() { - val confirmDialog = AlertDialog.Builder(this) - confirmDialog.setTitle(R.string.tip_dialog_finish_call_title) - confirmDialog.setMessage(R.string.tip_dialog_finish_call_content) - confirmDialog.setPositiveButton( - R.string.tip_dialog_finish_call_positive - ) { _: DialogInterface?, _: Int -> - if (!callFinished) { - getString(R.string.tip_invite_was_sending).toastShort(this) - return@setPositiveButton - } - finish() - } - confirmDialog.setNegativeButton( - R.string.tip_dialog_finish_call_negative - ) { _: DialogInterface?, _: Int -> } - confirmDialog.show() - } - - private fun initForLaunchUI() { - if (callParam.isCalled) { - // 主叫页面初始化 - uiRender.renderForCalled() - } else { - // 被叫页面初始化 - uiRender.renderForCaller() - } - } - - private fun initForLaunchAction() { - if (callParam.isCalled) { - return - } - doCall() - if (CallKitUI.options?.initRtcMode != NECallInitRtcMode.GLOBAL) { - setupLocalView(binding.videoViewPreview) - } - if (callParam.callType == NECallType.VIDEO && - CallKitUI.options?.joinRtcWhenCall == false && - startPreviewCode != NERtcConstants.ErrorCode.OK - ) { - startPreviewCode = NERtcEx.getInstance().startVideoPreview().apply { - ALog.d(tag, "initForLaunchAction startPreviewCode is $this.") - } - } - } - - /** - * 通话中页面初始化 - */ - private fun initForOnTheCall(userAccId: String? = null) { - uiRender.renderForOnTheCall(userAccId) - } - - private fun doCall() { - callFinished = false - - doCall { result -> - callFinished = true - if (result?.isSuccessful != true && result.code != ResponseCode.RES_PEER_NIM_OFFLINE.toInt() && result.code != ResponseCode.RES_PEER_PUSH_OFFLINE.toInt()) { - getString(R.string.tip_start_call_failed).toastShort(this@P2PCallActivity) - } - } - } - - private fun doAccept() { - if (binding.tvConnectingTip.tag != true) { - binding.tvConnectingTip.tag = true - binding.tvConnectingTip.visibility = View.VISIBLE - } - doAccept { result -> - if (result?.isSuccessful != true) { - getString(R.string.tip_accept_failed).toastShort(this@P2PCallActivity) - finish() - } - } - } - - private fun doHangup() { - releaseAndFinish(true) - } - - private fun doMuteVideo(view: ImageView) { - doMuteVideo() - view.setImageResource(if (isLocalMuteVideo) R.drawable.cam_off else R.drawable.cam_on) - uiRender.updateOnTheCallState( - UserState( - callParam.currentAccId!!, - muteVideo = isLocalMuteVideo - ) - ) - } - - private fun doConfigSpeakerSwitch( - view: ImageView? = null, - speakerEnable: Boolean = !isSpeakerOn() - ) { - doConfigSpeaker(speakerEnable) - binding.ivMuteSpeaker.setImageResource( - if (speakerEnable) R.drawable.speaker_on else R.drawable.speaker_off - ) - binding.ivCallSpeaker.setImageResource( - if (speakerEnable) R.drawable.icon_call_audio_speaker_on else R.drawable.icon_call_audio_speaker_off - ) - } - - private fun doMuteAudioSwitch(view: ImageView? = null) { - super.doMuteAudio(!isLocalMuteAudio) - binding.ivMuteAudio.setImageResource( - if (isLocalMuteAudio) R.drawable.voice_off else R.drawable.voice_on - ) - binding.ivCallMuteAudio.setImageResource( - if (isLocalMuteAudio) R.drawable.icon_call_audio_off else R.drawable.icon_call_audio_on - ) - } - - private fun doSwitchCallType(switchCallState: Int = SwitchCallState.INVITE) { - if (!NetworkUtils.isConnected()) { - getString(R.string.tip_network_error).toastShort(this) - return - } - val toCallType = if (callParam.callType == NECallType.VIDEO) { - NECallType.AUDIO - } else { - NECallType.VIDEO - } - - doSwitchCallType( - toCallType, - switchCallState, - object : NEResultObserver> { - override fun onResult(result: CommonResult?) { - if (result?.isSuccessful != true) { - getString(R.string.tip_switch_call_type_failed).toastShort( - this@P2PCallActivity - ) - ALog.e(tag, "doSwitchCallType to $toCallType error, result is $result.") - return - } - if (switchCallState == SwitchCallState.INVITE) { - binding.switchTypeTipGroup.visibility = View.VISIBLE - } - } - } - ) - } - - private fun doSwitchCanvas() { - if (uiConfig?.enableCanvasSwitch == false) { - return - } - - val rtcUid = callEngine.callInfo.otherUserInfo().uid - if (rtcUid == 0L) { - ALog.e(tag, "doSwitchCanvas rtcUid is 0L with accId ${callParam.otherAccId}.") - return - } - if (isLocalMuteVideo) { - binding.videoViewBig.clearImage() - binding.videoViewSmall.clearImage() - } - - if (localIsSmallVideo) { - NERtcEx.getInstance().setupRemoteVideoCanvas(binding.videoViewSmall, rtcUid) - NERtcEx.getInstance().setupLocalVideoCanvas(binding.videoViewBig) - } else { - NERtcEx.getInstance().setupRemoteVideoCanvas(binding.videoViewBig, rtcUid) - NERtcEx.getInstance().setupLocalVideoCanvas(binding.videoViewSmall) - } - localIsSmallVideo = !localIsSmallVideo - - uiRender.updateOnTheCallState( - UserState( - callParam.currentAccId, - muteVideo = isLocalMuteVideo - ) - ) - uiRender.updateOnTheCallState( - UserState( - callParam.otherAccId, - muteVideo = isRemoteMuteVideo - ) - ) - } - - private open inner class UIRender { - open fun renderForCaller() { - binding.tvSwitchTipClose.setOnClickListener { - binding.switchTypeTipGroup.visibility = View.GONE - } - val enableAutoJoinWhenCalled = (callEngine as? NECallEngineImpl)?.recorder?.isEnableAutoJoinWhenCalled == true - binding.calledSwitchGroup.visibility = View.GONE - binding.callerSwitchGroup.visibility = - if (enableAutoJoinWhenCalled) View.VISIBLE else View.GONE - } - - open fun renderForCalled() { - binding.tvSwitchTipClose.setOnClickListener { - binding.switchTypeTipGroup.visibility = View.GONE - } - binding.callerSwitchGroup.visibility = View.GONE - val enableAutoJoinWhenCalled = (callEngine as? NECallEngineImpl)?.recorder?.isEnableAutoJoinWhenCalled == true - if (enableAutoJoinWhenCalled) { - binding.calledSwitchGroup.visibility = View.VISIBLE - } else { - binding.calledSwitchGroup.visibility = View.GONE - } - } - - open fun renderForOnTheCall(userAccId: String? = null) { - binding.tvSwitchTipClose.setOnClickListener { - binding.switchTypeTipGroup.visibility = View.GONE - } - binding.callerSwitchGroup.visibility = View.GONE - binding.calledSwitchGroup.visibility = View.GONE - if (this is AudioRender) { - binding.ivCallChannelTypeChange.visibility = - if (uiConfig?.showAudio2VideoSwitchOnTheCall == true) View.VISIBLE else View.GONE - } - if (this is VideoRender) { - binding.ivCallChannelTypeChange.visibility = - if (uiConfig?.showVideo2AudioSwitchOnTheCall == true) View.VISIBLE else View.GONE - } - uiConfig?.overlayViewOnTheCall?.run { - if (parent is ViewGroup) { - (parent as ViewGroup).removeView(this) - } - binding.clRoot.addView( - this, - ConstraintLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT - ) - ) - } - } - - open fun updateOnTheCallState(state: UserState) {} - } - - private inner class AudioRender : UIRender() { - override fun renderForCaller() { - super.renderForCaller() - forUserInfoUI(NECallType.AUDIO, callParam.calledAccId, forVideoCaller = true) - doConfigSpeakerSwitch(speakerEnable = false) - - if (isLocalMuteAudio) { - doMuteAudioSwitch() - } - - binding.ivBg.visibility = View.VISIBLE - binding.tvCallSwitchTypeDesc.setText(R.string.tip_switch_to_video) - - binding.videoViewBig.visibility = View.GONE - binding.videoViewPreview.visibility = View.GONE - binding.videoViewSmall.visibility = View.GONE - - binding.llOnTheCallOperation.visibility = View.GONE - binding.calledOperationGroup.visibility = View.GONE - binding.callerOperationGroup.visibility = View.VISIBLE - binding.callerAudioOperationGroup.visibility = View.VISIBLE - - binding.ivCancel.setOnClickListener(onClickListener) - binding.ivCallSwitchType.setOnClickListener(onClickListener) - binding.ivCallSwitchType.setImageResource(R.drawable.icon_call_tip_audio_to_video) - binding.ivCallChannelTypeChange.setOnClickListener(onClickListener) - - binding.ivCallMuteAudio.setOnClickListener(onClickListener) - binding.ivCallSpeaker.setOnClickListener(onClickListener) - binding.ivBg.visibility = View.VISIBLE - if (startPreviewCode == 0 || startPreviewCode == ENGINE_ERROR_DEVICE_PREVIEW_ALREADY_STARTED) { - NERtcEx.getInstance().setupLocalVideoCanvas(null) - startPreviewCode = if (NERtcEx.getInstance().stopVideoPreview() == 0) -1 else 0 - } - } - - override fun renderForCalled() { - super.renderForCalled() - forUserInfoUI(NECallType.AUDIO, callParam.callerAccId) - - binding.ivAccept.setImageResource(R.drawable.icon_call_audio_accept) - binding.ivSwitchType.setImageResource(R.drawable.icon_call_tip_audio_to_video) - binding.tvOtherCallTip.setText(R.string.tip_invite_to_audio_call) - binding.tvSwitchTypeDesc.setText(R.string.tip_switch_to_video) - - binding.videoViewPreview.visibility = View.GONE - binding.videoViewBig.visibility = View.GONE - binding.videoViewSmall.visibility = View.GONE - - binding.llOnTheCallOperation.visibility = View.GONE - binding.calledOperationGroup.visibility = View.VISIBLE - binding.callerOperationGroup.visibility = View.GONE - binding.callerAudioOperationGroup.visibility = View.GONE - - binding.ivAccept.setOnClickListener(onClickListener) - binding.ivReject.setOnClickListener(onClickListener) - binding.ivSwitchType.setOnClickListener(onClickListener) - binding.ivBg.visibility = View.VISIBLE - } - - override fun renderForOnTheCall(userAccId: String?) { - super.renderForOnTheCall(userAccId) - - callParam.run { - forUserInfoUI(NECallType.AUDIO, otherAccId) - } - - binding.tvOtherCallTip.setText(R.string.tip_on_the_call) - binding.tvConnectingTip.visibility = View.GONE - binding.videoViewPreview.visibility = View.GONE - binding.videoViewSmall.visibility = View.GONE - binding.videoViewBig.visibility = View.GONE - binding.ivSmallVideoShade.visibility = View.GONE - - binding.calledOperationGroup.visibility = View.GONE - binding.callerOperationGroup.visibility = View.GONE - binding.callerAudioOperationGroup.visibility = View.GONE - binding.llOnTheCallOperation.visibility = View.VISIBLE - binding.tvCountdown.visibility = View.VISIBLE - - binding.ivCallChannelTypeChange.setImageResource(R.drawable.audio_to_video) - binding.ivCallChannelTypeChange.setOnClickListener(onClickListener) - binding.ivMuteAudio.setOnClickListener(onClickListener) - binding.ivMuteVideo.visibility = View.GONE - binding.ivHangUp.setOnClickListener(onClickListener) - binding.ivMuteSpeaker.setOnClickListener(onClickListener) - - binding.ivSwitchCamera.visibility = View.GONE - - binding.tvRemoteVideoCloseTip.visibility = View.GONE - binding.ivSmallVideoShade.visibility = View.GONE - if (!firstLaunch || callParam.isCalled) { - resetSwitchState(NECallType.AUDIO) - } else { - firstLaunch = false - } - // sdk 版本变更后在加入通话前设置扬声器不生效,在对方接听后,自己加入rtc 后进行扬声器设置补充; - if (!callParam.isCalled) { - doConfigSpeakerSwitch(speakerEnable = isSpeakerOn()) - } - binding.ivBg.visibility = View.VISIBLE - } - } - - private var firstLaunch = true - - private inner class VideoRender : UIRender() { - override fun renderForCaller() { - super.renderForCaller() - forUserInfoUI(NECallType.VIDEO, callParam.calledAccId, forVideoCaller = true) - doConfigSpeakerSwitch(speakerEnable = true) - - binding.videoViewBig.visibility = View.GONE - binding.videoViewPreview.visibility = View.VISIBLE - binding.videoViewSmall.visibility = View.GONE - - binding.llOnTheCallOperation.visibility = View.GONE - binding.calledOperationGroup.visibility = View.GONE - binding.callerOperationGroup.visibility = View.VISIBLE - binding.callerAudioOperationGroup.visibility = View.GONE - binding.ivCancel.setOnClickListener(onClickListener) - binding.ivCallSwitchType.setOnClickListener(onClickListener) - binding.ivCallSwitchType.setImageResource(R.drawable.icon_call_tip_video_to_audio) - binding.tvCallSwitchTypeDesc.setText(R.string.tip_switch_to_audio) - - setupLocalView(binding.videoViewPreview) - if (startPreviewCode != NERtcConstants.ErrorCode.OK && - startPreviewCode != ENGINE_ERROR_DEVICE_PREVIEW_ALREADY_STARTED - ) { - startPreviewCode = NERtcEx.getInstance().startVideoPreview().apply { - ALog.d(tag, "renderForCaller startPreviewCode is $this.") - } - } - binding.ivBg.visibility = View.GONE - } - - override fun renderForCalled() { - super.renderForCalled() - - forUserInfoUI(NECallType.VIDEO, callParam.callerAccId) - - binding.videoViewPreview.visibility = View.GONE - binding.videoViewBig.visibility = View.GONE - binding.videoViewSmall.visibility = View.GONE - - binding.llOnTheCallOperation.visibility = View.GONE - binding.calledOperationGroup.visibility = View.VISIBLE - binding.callerOperationGroup.visibility = View.GONE - binding.callerAudioOperationGroup.visibility = View.GONE - binding.tvOtherCallTip.setText(R.string.tip_invite_to_video_call) - binding.tvSwitchTypeDesc.setText(R.string.tip_switch_to_audio) - - binding.ivAccept.setImageResource(R.drawable.call_accept) - binding.ivSwitchType.setImageResource(R.drawable.icon_call_tip_video_to_audio) - - binding.ivAccept.setOnClickListener(onClickListener) - binding.ivReject.setOnClickListener(onClickListener) - binding.ivSwitchType.setOnClickListener(onClickListener) - - binding.ivBg.visibility = View.VISIBLE - } - - override fun renderForOnTheCall(userAccId: String?) { - super.renderForOnTheCall(userAccId) - - forUserInfoUI(type = NECallType.VIDEO, visible = false) - - binding.tvConnectingTip.visibility = View.GONE - binding.videoViewPreview.visibility = View.GONE - binding.videoViewSmall.visibility = View.VISIBLE - binding.videoViewBig.visibility = View.VISIBLE - binding.ivSmallVideoShade.visibility = View.GONE - - binding.calledOperationGroup.visibility = View.GONE - binding.callerOperationGroup.visibility = View.GONE - binding.callerAudioOperationGroup.visibility = View.GONE - binding.llOnTheCallOperation.visibility = View.VISIBLE - binding.tvCountdown.visibility = View.VISIBLE - - binding.videoViewSmall.setOnClickListener(onClickListener) - binding.ivCallChannelTypeChange.setOnClickListener(onClickListener) - binding.ivCallChannelTypeChange.setImageResource(R.drawable.video_to_audio) - binding.ivMuteAudio.setOnClickListener(onClickListener) - binding.ivMuteVideo.visibility = View.VISIBLE - binding.ivMuteVideo.setOnClickListener(onClickListener) - binding.ivHangUp.setOnClickListener(onClickListener) - binding.ivMuteSpeaker.setOnClickListener(onClickListener) - resetSwitchState(NECallType.VIDEO) - binding.ivBg.visibility = View.GONE - - firstLaunch = false - binding.ivSwitchCamera.run { - visibility = View.VISIBLE - setOnClickListener(onClickListener) - } - if (callParam.currentUserIsCaller()) { - NERtcEx.getInstance().setupLocalVideoCanvas(null) - NERtcEx.getInstance().stopVideoPreview() - } - setupRemoteView(binding.videoViewBig) - binding.videoViewPreview.release() - setupLocalView(binding.videoViewSmall) - } - - override fun updateOnTheCallState(state: UserState) { - super.updateOnTheCallState(state) - if (localIsSmallVideo) { - if (TextUtils.equals(state.userAccId, callParam.currentAccId)) { - state.muteVideo?.run { - loadImg(uiConfig?.closeVideoLocalUrl, binding.ivSmallVideoShade) - binding.ivSmallVideoShade.visibility = if (this) View.VISIBLE else View.GONE - } - } else { - state.muteVideo?.run { - loadImg(uiConfig?.closeVideoRemoteUrl, binding.ivBigVideoShade) - binding.ivBigVideoShade.visibility = if (this) View.VISIBLE else View.GONE - binding.tvRemoteVideoCloseTip.text = - if (TextUtils.isEmpty(uiConfig?.closeVideoRemoteTip?.trim())) { - getString( - R.string.ui_tip_close_camera_by_other - ) - } else { - uiConfig?.closeVideoRemoteTip - } - binding.tvRemoteVideoCloseTip.visibility = if (this) View.VISIBLE else View.GONE - } - } - } else { - if (TextUtils.equals(state.userAccId, callParam.currentAccId)) { - state.muteVideo?.run { - loadImg(uiConfig?.closeVideoLocalUrl, binding.ivBigVideoShade) - binding.ivBigVideoShade.visibility = if (this) View.VISIBLE else View.GONE - binding.tvRemoteVideoCloseTip.text = if (TextUtils.isEmpty( - uiConfig?.closeVideoLocalTip?.trim() - ) - ) { - getString( - R.string.ui_tip_close_camera_by_self - ) - } else { - uiConfig?.closeVideoLocalTip - } - binding.tvRemoteVideoCloseTip.visibility = if (this) View.VISIBLE else View.GONE - } - } else { - state.muteVideo?.run { - loadImg(uiConfig?.closeVideoRemoteUrl, binding.ivSmallVideoShade) - binding.ivSmallVideoShade.visibility = if (this) View.VISIBLE else View.GONE - } - } - } - } - } - - private fun forUserInfoUI( - type: Int, - accId: String? = null, - visible: Boolean = true, - forVideoCaller: Boolean = false - ) { - if (!visible) { - binding.userInfoGroup.visibility = View.GONE - return - } - binding.userInfoGroup.visibility = View.VISIBLE - - accId?.run { - fetchNickname { - binding.tvUserName.text = it - } - loadAvatarByAccId( - this@P2PCallActivity, - binding.ivUserInnerAvatar, - binding.ivBg, - binding.tvUserInnerAvatar, - uiConfig?.enableTextDefaultAvatar ?: true - ) - } - val centerSize = 97.dip2Px(this) - val topSize = 60.dip2Px(this) - - val constraintSet = ConstraintSet() - constraintSet.clone(binding.clRoot) - constraintSet.clear(R.id.flUserAvatar) - constraintSet.clear(R.id.tvOtherCallTip) - constraintSet.clear(R.id.tvUserName) - constraintSet.constrainHeight(R.id.tvOtherCallTip, ConstraintSet.WRAP_CONTENT) - constraintSet.constrainWidth(R.id.tvOtherCallTip, ConstraintSet.WRAP_CONTENT) - constraintSet.constrainWidth(R.id.tvUserName, ConstraintSet.WRAP_CONTENT) - constraintSet.constrainWidth(R.id.tvUserName, ConstraintSet.WRAP_CONTENT) - - if (type == NECallType.VIDEO && forVideoCaller) { - val marginSize16 = 16.dip2Px(this) - binding.flUserAvatar.run { - constraintSet.constrainWidth(id, topSize) - constraintSet.constrainHeight(id, topSize) - constraintSet.connect( - id, - ConstraintSet.END, - ConstraintSet.PARENT_ID, - ConstraintSet.END, - marginSize16 - ) - constraintSet.connect( - id, - ConstraintSet.TOP, - ConstraintSet.PARENT_ID, - ConstraintSet.TOP, - marginSize16 - ) - } - val marginSize10 = 10.dip2Px(this) - val marginSize5 = 5.dip2Px(this) - binding.tvUserName.run { - textSize = 18f - constraintSet.connect( - id, - ConstraintSet.END, - binding.flUserAvatar.id, - ConstraintSet.START, - marginSize10 - ) - constraintSet.connect( - id, - ConstraintSet.TOP, - binding.flUserAvatar.id, - ConstraintSet.TOP, - marginSize5 - ) - } - binding.tvOtherCallTip.run { - setTextColor(ContextCompat.getColor(context, R.color.white)) - constraintSet.connect( - id, - ConstraintSet.TOP, - binding.tvUserName.id, - ConstraintSet.BOTTOM, - marginSize5 - ) - constraintSet.connect( - id, - ConstraintSet.END, - binding.flUserAvatar.id, - ConstraintSet.START, - marginSize10 - ) - } - } else { - binding.flUserAvatar.run { - constraintSet.constrainWidth(id, centerSize) - constraintSet.constrainHeight(id, centerSize) - constraintSet.connect( - id, - ConstraintSet.TOP, - ConstraintSet.PARENT_ID, - ConstraintSet.TOP, - 160.dip2Px(context) - ) - constraintSet.centerHorizontally(id, ConstraintSet.PARENT_ID) - } - binding.tvUserName.run { - textSize = 20f - constraintSet.centerHorizontally(id, ConstraintSet.PARENT_ID) - constraintSet.connect( - id, - ConstraintSet.TOP, - binding.flUserAvatar.id, - ConstraintSet.BOTTOM, - 15.dip2Px(context) - ) - } - binding.tvOtherCallTip.run { - setTextColor(ContextCompat.getColor(context, R.color.color_cccccc)) - constraintSet.connect( - id, - ConstraintSet.TOP, - binding.tvUserName.id, - ConstraintSet.BOTTOM, - 8.dip2Px(context) - ) - constraintSet.centerHorizontally(id, ConstraintSet.PARENT_ID) - } - } - constraintSet.applyTo(binding.clRoot) - } - - private fun resetSwitchState(callType: Int) { - if (callType == NECallType.VIDEO) { - doConfigSpeaker(true) - binding.ivMuteSpeaker.setImageResource(R.drawable.speaker_on) - } else { - doConfigSpeaker(false) - binding.ivMuteSpeaker.setImageResource(R.drawable.speaker_off) - } - // 视频 - localIsSmallVideo = true - - doMuteAudio(false) - binding.ivMuteVideo.setImageResource(R.drawable.cam_on) - binding.tvRemoteVideoCloseTip.visibility = View.GONE - binding.videoViewSmall.setBackgroundColor(Color.TRANSPARENT) - // 音频 - if (isLocalMuteAudio) { - doMuteAudioSwitch(binding.ivMuteAudio) - } - } - - private fun loadImg(url: String?, imageView: ImageView) { - Glide.with(applicationContext).load(url) - .error(R.color.black) - .placeholder(R.color.black) - .centerCrop() - .into(imageView) - } - - class UserState( - val userAccId: String?, - val muteVideo: Boolean? = null - ) -} diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/P2PCallFragmentActivity.kt b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/P2PCallFragmentActivity.kt index c9ad5cf..62f74c5 100644 --- a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/P2PCallFragmentActivity.kt +++ b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/P2PCallFragmentActivity.kt @@ -6,10 +6,10 @@ package com.netease.yunxin.nertc.ui.p2p +import android.app.AlertDialog import android.content.DialogInterface import android.os.Bundle import android.view.View -import androidx.appcompat.app.AlertDialog import com.netease.lava.nertc.sdk.video.NERtcVideoView import com.netease.yunxin.kit.alog.ALog import com.netease.yunxin.kit.call.NEResultObserver @@ -37,13 +37,14 @@ import com.netease.yunxin.nertc.ui.p2p.fragment.P2PCallFragmentType.VIDEO_CALLEE import com.netease.yunxin.nertc.ui.p2p.fragment.P2PCallFragmentType.VIDEO_CALLER import com.netease.yunxin.nertc.ui.p2p.fragment.P2PCallFragmentType.VIDEO_ON_THE_CALL import com.netease.yunxin.nertc.ui.p2p.fragment.P2PUIUpdateType.CHANGE_CALL_TYPE +import com.netease.yunxin.nertc.ui.p2p.fragment.P2PUIUpdateType.FROM_FLOATING_WINDOW +import com.netease.yunxin.nertc.ui.p2p.fragment.P2PUIUpdateType.INIT import com.netease.yunxin.nertc.ui.p2p.fragment.callee.AudioCalleeFragment import com.netease.yunxin.nertc.ui.p2p.fragment.callee.VideoCalleeFragment import com.netease.yunxin.nertc.ui.p2p.fragment.caller.AudioCallerFragment import com.netease.yunxin.nertc.ui.p2p.fragment.caller.VideoCallerFragment import com.netease.yunxin.nertc.ui.p2p.fragment.onthecall.AudioOnTheCallFragment import com.netease.yunxin.nertc.ui.p2p.fragment.onthecall.VideoOnTheCallFragment -import com.netease.yunxin.nertc.ui.utils.SwitchCallTypeConfirmDialog import com.netease.yunxin.nertc.ui.utils.toastShort open class P2PCallFragmentActivity : CommonCallActivity() { @@ -64,6 +65,10 @@ open class P2PCallFragmentActivity : CommonCallActivity() { get() = this@P2PCallFragmentActivity.isLocalMuteVideo override val isLocalMuteSpeaker: Boolean get() = this@P2PCallFragmentActivity.isLocalMuteSpeaker + override val isLocalSmallVideo: Boolean + get() = this@P2PCallFragmentActivity.isLocalSmallVideo + override val isVirtualBlur: Boolean + get() = this@P2PCallFragmentActivity.isVirtualBlur override fun isSpeakerOn(): Boolean = this@P2PCallFragmentActivity.isSpeakerOn() @@ -73,6 +78,9 @@ open class P2PCallFragmentActivity : CommonCallActivity() { override fun doMuteVideo(mute: Boolean) = this@P2PCallFragmentActivity.doMuteVideo(mute) + override fun doVirtualBlur(enable: Boolean) = + this@P2PCallFragmentActivity.doVirtualBlur(enable) + override fun doSwitchCamera() = this@P2PCallFragmentActivity.doSwitchCamera() override fun configTimeTick(config: CallUIOperationsMgr.TimeTickConfig?) = @@ -84,7 +92,11 @@ open class P2PCallFragmentActivity : CommonCallActivity() { override fun doAccept(observer: NEResultObserver>?) = this@P2PCallFragmentActivity.doAccept(observer) - override fun doHangup(observer: NEResultObserver>?, channelId: String?, extraInfo: String?) { + override fun doHangup( + observer: NEResultObserver>?, + channelId: String?, + extraInfo: String? + ) { this@P2PCallFragmentActivity.doHangup(observer, channelId, extraInfo) finish() } @@ -93,16 +105,24 @@ open class P2PCallFragmentActivity : CommonCallActivity() { callType: Int, switchCallState: Int, observer: NEResultObserver>? ) = this@P2PCallFragmentActivity.doSwitchCallType(callType, switchCallState, observer) - override fun setupLocalView(view: NERtcVideoView?) = - this@P2PCallFragmentActivity.setupLocalView(view) + override fun setupLocalView(view: NERtcVideoView?, action: ((NERtcVideoView?) -> Unit)?) { + this@P2PCallFragmentActivity.setupLocalView(view, action) + } - override fun setupRemoteView(view: NERtcVideoView?) = - this@P2PCallFragmentActivity.setupRemoteView(view) + override fun setupRemoteView(view: NERtcVideoView?, action: ((NERtcVideoView?) -> Unit)?) { + this@P2PCallFragmentActivity.setupRemoteView(view, action) + } override fun currentCallState(): Int = this@P2PCallFragmentActivity.currentCallState() override fun showPermissionDialog(clickListener: View.OnClickListener) = this@P2PCallFragmentActivity.showPermissionDialog(clickListener) + + override fun showFloatingWindow() = this@P2PCallFragmentActivity.doShowFloatingWindow() + + override fun startVideoPreview() = this@P2PCallFragmentActivity.startVideoPreview() + + override fun stopVideoPreview() = this@P2PCallFragmentActivity.stopVideoPreview() } private var currentFragment: BaseP2pCallFragment? = null @@ -115,14 +135,6 @@ open class P2PCallFragmentActivity : CommonCallActivity() { private val audioCalleeFragment = AudioCalleeFragment() private val audioOnTheCallFragment = AudioOnTheCallFragment() - private val switchConfirmDialog by lazy { - SwitchCallTypeConfirmDialog(this, { - doSwitchCallType(it, SwitchCallState.ACCEPT) - }, { - doSwitchCallType(it, SwitchCallState.REJECT) - }) - } - override fun onCallEnd(info: NECallEndInfo) { when (info.reasonCode) { NEHangupReasonCode.CALLER_REJECTED -> if (!isFinishing && !callParam.isCalled) { @@ -170,7 +182,7 @@ open class P2PCallFragmentActivity : CommonCallActivity() { } SwitchCallState.INVITE -> { - switchConfirmDialog.show(info.callType) + showSwitchCallTypeConfirmDialog(info.callType) } SwitchCallState.REJECT -> { @@ -229,8 +241,7 @@ open class P2PCallFragmentActivity : CommonCallActivity() { confirmDialog.setPositiveButton( R.string.tip_dialog_finish_call_positive ) { _: DialogInterface?, _: Int -> - doHangup() - finish() + releaseAndFinish(true) } confirmDialog.setNegativeButton( R.string.tip_dialog_finish_call_negative @@ -244,28 +255,36 @@ open class P2PCallFragmentActivity : CommonCallActivity() { ) { val fragment = getFragment(callState, callType) if (fragment == null) { - ALog.e(tag, "currentFragment is null, currentCallState is ${currentCallState()}.") + ALog.e(tag, "currentFragment is null, callState is $callState, callType is $callType.") finish() return } - fragment.configData(bridge) + fragment.configData( + bridge, + if (isFromFloatingWindow) { + FROM_FLOATING_WINDOW + } else { + INIT + } + ) if (fragment != currentFragment) { - supportFragmentManager.beginTransaction().add(R.id.clRoot, fragment) - .commitAllowingStateLoss() + val tag = "${getFragmentKey(callState, callType)}" + val transaction = supportFragmentManager.beginTransaction() + if (supportFragmentManager.findFragmentByTag(tag) == fragment) { + transaction.show(fragment) + } else { + transaction.add(R.id.clRoot, fragment, tag) + } currentFragment?.run { - supportFragmentManager.beginTransaction().remove(this).commitAllowingStateLoss() + transaction.hide(this) } + transaction.commitAllowingStateLoss() } currentFragment = fragment } - protected fun getFragment(callState: Int, callType: Int): BaseP2pCallFragment? { - val key = when (callState) { - STATE_CALL_OUT, STATE_IDLE -> if (callType == NECallType.VIDEO) VIDEO_CALLER else AUDIO_CALLER - STATE_INVITED -> if (callType == NECallType.VIDEO) VIDEO_CALLEE else AUDIO_CALLEE - STATE_DIALOG -> if (callType == NECallType.VIDEO) VIDEO_ON_THE_CALL else AUDIO_ON_THE_CALL - else -> null - } ?: return null + protected open fun getFragment(callState: Int, callType: Int): BaseP2pCallFragment? { + val key = getFragmentKey(callState, callType) ?: return null return uiConfig?.customCallFragmentMap?.get(key) ?: when (key) { VIDEO_CALLEE -> videoCalleeFragment VIDEO_CALLER -> videoCallerFragment @@ -276,4 +295,13 @@ open class P2PCallFragmentActivity : CommonCallActivity() { else -> null } } + + protected open fun getFragmentKey(callState: Int, callType: Int): Int? { + return when (callState) { + STATE_CALL_OUT, STATE_IDLE -> if (callType == NECallType.VIDEO) VIDEO_CALLER else AUDIO_CALLER + STATE_INVITED -> if (callType == NECallType.VIDEO) VIDEO_CALLEE else AUDIO_CALLEE + STATE_DIALOG -> if (callType == NECallType.VIDEO) VIDEO_ON_THE_CALL else AUDIO_ON_THE_CALL + else -> null + } + } } diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/P2PUIConfig.kt b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/P2PUIConfig.kt index da1c120..e179fc6 100644 --- a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/P2PUIConfig.kt +++ b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/P2PUIConfig.kt @@ -25,19 +25,19 @@ class P2PUIConfig constructor( /** * 本端用户关闭摄像头时的图像展示 */ - val closeVideoLocalUrl: String? = null, // TODO: + val closeVideoLocalUrl: String? = null, /** * 本端用户关闭摄像头时的文本提示 */ - val closeVideoLocalTip: CharSequence? = null, // TODO: + val closeVideoLocalTip: CharSequence? = null, /** * 对端用户关闭摄像头时的本端的图像展示 */ - val closeVideoRemoteUrl: String? = null, // TODO: + val closeVideoRemoteUrl: String? = null, /** * 对端用户关闭摄像头时的本端文本提示 */ - val closeVideoRemoteTip: CharSequence? = null, // TODO: + val closeVideoRemoteTip: CharSequence? = null, /** * 关闭视频模式,默认 [CLOSE_TYPE_MUTE],也支持 [CLOSE_TYPE_DISABLE],[CLOSE_TYPE_COMPAT] */ @@ -45,11 +45,11 @@ class P2PUIConfig constructor( /** * 是否支持通话中大小画面点击切换,默认 true */ - val enableCanvasSwitch: Boolean = true, // TODO: + val enableCanvasSwitch: Boolean = true, /** * 通话中页面支持覆盖的view */ - val overlayViewOnTheCall: View? = null, // TODO: + val overlayViewOnTheCall: View? = null, /** * 当没有头像时是否展示文字头像,默认 true */ @@ -65,11 +65,27 @@ class P2PUIConfig constructor( /** * 自定义通话中 fragment 页面 */ - val customCallFragmentMap: Map? = null + val customCallFragmentMap: Map? = null, + /** + * 是否支持小窗,默认false + */ + val enableFloatingWindow: Boolean = false, + /** + * 是否支持在 home 出应用后自动展示小窗,默认false + */ + val enableAutoFloatingWindowWhenHome: Boolean = false, + /** + * 是否开启被叫视频预览,默认false + */ + val enableVideoCalleePreview: Boolean = false, + /** + * 是否支持通话视频虚化,默认false + */ + val enableVirtualBlur: Boolean = false ) { override fun toString(): String { - return "P2PUIConfig(showAudio2VideoSwitchOnTheCall=$showAudio2VideoSwitchOnTheCall, showVideo2AudioSwitchOnTheCall=$showVideo2AudioSwitchOnTheCall, closeVideoLocalUrl=$closeVideoLocalUrl, closeVideoLocalTip=$closeVideoLocalTip, closeVideoRemoteUrl=$closeVideoRemoteUrl, closeVideoRemoteTip=$closeVideoRemoteTip, closeVideoType=$closeVideoType, enableCanvasSwitch=$enableCanvasSwitch, overlayViewOnTheCall=$overlayViewOnTheCall, enableTextDefaultAvatar=$enableTextDefaultAvatar, enableForegroundService=$enableForegroundService, foregroundNotificationConfig=$foregroundNotificationConfig, customCallFragmentMap=$customCallFragmentMap)" + return "P2PUIConfig(showAudio2VideoSwitchOnTheCall=$showAudio2VideoSwitchOnTheCall, showVideo2AudioSwitchOnTheCall=$showVideo2AudioSwitchOnTheCall, closeVideoLocalUrl=$closeVideoLocalUrl, closeVideoLocalTip=$closeVideoLocalTip, closeVideoRemoteUrl=$closeVideoRemoteUrl, closeVideoRemoteTip=$closeVideoRemoteTip, closeVideoType=$closeVideoType, enableCanvasSwitch=$enableCanvasSwitch, overlayViewOnTheCall=$overlayViewOnTheCall, enableTextDefaultAvatar=$enableTextDefaultAvatar, enableForegroundService=$enableForegroundService, foregroundNotificationConfig=$foregroundNotificationConfig, customCallFragmentMap=$customCallFragmentMap, enableFloatingWindow=$enableFloatingWindow, enableAutoFloatingWindowWhenHome=$enableAutoFloatingWindowWhenHome, enableVideoCalleePreview=$enableVideoCalleePreview, enableVirtualBlur=$enableVirtualBlur)" } class Builder { @@ -86,6 +102,10 @@ class P2PUIConfig constructor( private var enableForegroundService: Boolean = false private var foregroundNotificationConfig: CallKitNotificationConfig? = null private var customCallFragmentMap: MutableMap = mutableMapOf() + private var enableFloatingWindow: Boolean = true + private var enableAutoFloatingWindowWhenHome: Boolean = false + private var enableVideoCalleePreview: Boolean = false + private var enableVirtualBlur: Boolean = false fun showAudio2VideoSwitchOnTheCall(enable: Boolean) = apply { this.showAudio2VideoSwitchOnTheCall = enable } @@ -119,21 +139,39 @@ class P2PUIConfig constructor( fun customCallFragmentByKey(key: Int, fragment: BaseP2pCallFragment) = apply { customCallFragmentMap[key] = fragment } + fun enableFloatingWindow(enable: Boolean) = apply { this.enableFloatingWindow = enable } + + fun enableAutoFloatingWindowWhenHome(enable: Boolean) = apply { + this.enableAutoFloatingWindowWhenHome = enable + } + + fun enableVideoCalleePreview(enable: Boolean) = apply { + this.enableVideoCalleePreview = enable + } + + fun enableVirtualBlur(enable: Boolean) = apply { + this.enableVirtualBlur = enable + } + fun build(): P2PUIConfig { return P2PUIConfig( - showAudio2VideoSwitchOnTheCall, - showVideo2AudioSwitchOnTheCall, - closeVideoLocalUrl, - closeVideoLocalTip, - closeVideoRemoteUrl, - closeVideoRemoteTip, - closeVideoType, - enableCanvasSwitch, - overlayViewOnTheCall, - enableTextDefaultAvatar, - enableForegroundService, - foregroundNotificationConfig, - customCallFragmentMap + showAudio2VideoSwitchOnTheCall = showAudio2VideoSwitchOnTheCall, + showVideo2AudioSwitchOnTheCall = showVideo2AudioSwitchOnTheCall, + closeVideoLocalUrl = closeVideoLocalUrl, + closeVideoLocalTip = closeVideoLocalTip, + closeVideoRemoteUrl = closeVideoRemoteUrl, + closeVideoRemoteTip = closeVideoRemoteTip, + closeVideoType = closeVideoType, + enableCanvasSwitch = enableCanvasSwitch, + overlayViewOnTheCall = overlayViewOnTheCall, + enableTextDefaultAvatar = enableTextDefaultAvatar, + enableForegroundService = enableForegroundService, + foregroundNotificationConfig = foregroundNotificationConfig, + customCallFragmentMap = customCallFragmentMap, + enableFloatingWindow = enableFloatingWindow, + enableAutoFloatingWindowWhenHome = enableAutoFloatingWindowWhenHome, + enableVideoCalleePreview = enableVideoCalleePreview, + enableVirtualBlur = enableVirtualBlur ) } } diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/fragment/BaseP2pCallFragment.kt b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/fragment/BaseP2pCallFragment.kt index 9abebdc..8b2c368 100644 --- a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/fragment/BaseP2pCallFragment.kt +++ b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/fragment/BaseP2pCallFragment.kt @@ -11,16 +11,27 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment +import com.netease.yunxin.kit.alog.ALog +import com.netease.yunxin.kit.call.NEResultObserver import com.netease.yunxin.kit.call.p2p.model.NECallEndInfo import com.netease.yunxin.kit.call.p2p.model.NECallEngineDelegate import com.netease.yunxin.kit.call.p2p.model.NECallInfo +import com.netease.yunxin.kit.call.p2p.model.NECallType import com.netease.yunxin.kit.call.p2p.model.NECallTypeChangeInfo import com.netease.yunxin.kit.call.p2p.model.NEInviteInfo +import com.netease.yunxin.nertc.nertcvideocall.bean.CommonResult +import com.netease.yunxin.nertc.nertcvideocall.model.SwitchCallState import com.netease.yunxin.nertc.nertcvideocall.model.impl.NERtcCallbackExTemp import com.netease.yunxin.nertc.nertcvideocall.model.impl.NERtcCallbackProxyMgr +import com.netease.yunxin.nertc.ui.R import com.netease.yunxin.nertc.ui.base.CallParam import com.netease.yunxin.nertc.ui.p2p.P2PUIConfig import com.netease.yunxin.nertc.ui.p2p.fragment.P2PUIUpdateType.INIT +import com.netease.yunxin.nertc.ui.utils.PermissionRequester +import com.netease.yunxin.nertc.ui.utils.PermissionTipDialog +import com.netease.yunxin.nertc.ui.utils.isGranted +import com.netease.yunxin.nertc.ui.utils.registerPermissionRequesterEx +import com.netease.yunxin.nertc.ui.utils.toastShort /** * 处理 ui 相关内容,主要处理 页面点击相关内容,添加相关监听,页面状态切换 @@ -187,6 +198,19 @@ open class BaseP2pCallFragment : Fragment(), NECallEngineDelegate { */ protected val viewKeyTextRemoteVideoCloseTip = "tvRemoteVideoCloseTip" + /** + * 页面中小窗触发控件 + */ + protected val viewKeyImageFloatingWindow = "ivFloatingWindow" + + /** + * 视频背景虚化开关图片控件 + */ + protected val viewKeyImageVirtualBlur = "ivVirtualBlur" + + /** + * fragment和activity交互接口 + */ protected lateinit var bridge: FragmentActionBridge private set @@ -196,40 +220,158 @@ open class BaseP2pCallFragment : Fragment(), NECallEngineDelegate { protected var rootView: View? = null private set + /** + * 申请权限工具 + */ + protected var permissionRequester: PermissionRequester? = null + + /** + * 权限提示弹窗 + */ + protected var permissionTipDialog: PermissionTipDialog? = null + + /** + * 日志标签 + */ + private val logTag = "BaseP2pCallFragment" + + /** + * 页面初始化类型 + */ + private var initType: Int = INIT + /** * 页面元素绑定映射关系 */ private val viewBindMap = mutableMapOf() + /** + * 页面点击事件前置动作行为映射 + */ + private val beforeClickMap = mutableMapOf Boolean>() + + /** + * 页面点击事件后置动作行为映射 + */ + private val afterClickMap = mutableMapOf Unit>() + + /** + * 页面切换通话类型回调 + */ + private val switchObserver = object : NEResultObserver> { + override fun onResult(result: CommonResult?) { + if (result?.isSuccessful != true) { + context?.run { getString(R.string.tip_switch_call_type_failed).toastShort(this) } + ALog.e( + logTag, + "doSwitchCallType to ${NECallType.VIDEO} error, result is $result." + ) + return + } + getView(viewKeySwitchTypeTipGroup)?.run { + visibility = View.VISIBLE + } + } + } + private val rtcDelegate = object : NERtcCallbackExTemp() { override fun onJoinChannel(res: Int, cid: Long, time: Long, uid: Long) { this@BaseP2pCallFragment.onJoinChannel(res, cid, time, uid) } } - fun configData(bridge: FragmentActionBridge) { + fun configData(bridge: FragmentActionBridge, initType: Int = INIT) { this.bridge = bridge + this.initType = initType } open fun toUpdateUIState(type: Int) { } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + registerPermissionRequester() + } + final override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? = toCreateRootView(inflater, container, savedInstanceState).apply { + logApiInvoke("toCreateRootView") this@BaseP2pCallFragment.rootView = this NERtcCallbackProxyMgr.getInstance().addCallback(rtcDelegate) toBindView() + logApiInvoke("toBindView") toRenderView(bridge.callParam, bridge.uiConfig) + logApiInvoke("toRenderView") + onPermissionRequest() + logApiInvoke("onPermissionRequest") onCreateAction() - toUpdateUIState(INIT) + logApiInvoke("onCreateAction") + toUpdateUIState(initType) + logApiInvoke("toUpdateUIState") + initType = INIT } override fun onDestroyView() { super.onDestroyView() NERtcCallbackProxyMgr.getInstance().removeCallback(rtcDelegate) + permissionTipDialog?.dismiss() viewBindMap.clear() - onDestroyAction() + logApiInvoke("onDestroyAction") + } + + protected open fun registerPermissionRequester() { + val permissionList = permissionList() + if (permissionList.isNotEmpty()) { + permissionRequester = registerPermissionRequesterEx() + } + } + + protected open fun arePermissionsGranted(permissionList: List = permissionList()): Boolean { + return context?.isGranted(*permissionList().toTypedArray()) == true + } + + protected open fun onPermissionRequest() { + permissionTipDialog = if (arePermissionsGranted()) { + actionForPermissionGranted() + logApiInvoke("actionForPermissionGranted") + return + } else { + bridge.showPermissionDialog { + activity?.finish() + } + } + val permissionList = permissionList() + permissionRequester?.request( + onGranted = { + if (it.containsAll(permissionList)) { + permissionTipDialog?.dismiss() + actionForPermissionGranted() + logApiInvoke("actionForPermissionGranted") + } + }, + onDenied = { _, _ -> + actionForPermissionDenied() + logApiInvoke("actionForPermissionDenied") + }, + permissionList = permissionList + ) + } + + protected open fun requestPermission( + permissionList: List, + onGranted: ((List) -> Unit)? = null, + onDenied: ((List, List) -> Unit)? = null + ) { + permissionRequester?.request( + onGranted = { + onGranted?.invoke(it) + }, + onDenied = { deniedForeverList, deniedList -> + onDenied?.invoke(deniedForeverList, deniedList) + }, + permissionList = permissionList + ) } protected open fun toCreateRootView( @@ -238,6 +380,17 @@ open class BaseP2pCallFragment : Fragment(), NECallEngineDelegate { savedInstanceState: Bundle? ): View? = null + protected open fun permissionList(): List = emptyList() + + protected open fun actionForPermissionGranted() { + } + + protected open fun actionForPermissionDenied() { + context?.run { + getString(R.string.tip_permission_request_failed).toastShort(this) + } + } + protected open fun toBindView() { } @@ -250,6 +403,14 @@ open class BaseP2pCallFragment : Fragment(), NECallEngineDelegate { protected open fun onDestroyAction() { } + protected open fun switchCallType( + callType: Int, + switchCallState: Int = SwitchCallState.INVITE, + observer: NEResultObserver>? = switchObserver + ) { + bridge.doSwitchCallType(callType, SwitchCallState.INVITE, switchObserver) + } + protected fun bindView(key: String, view: View?) { viewBindMap[key] = view } @@ -259,13 +420,40 @@ open class BaseP2pCallFragment : Fragment(), NECallEngineDelegate { return viewBindMap[key] as? T } + protected open fun View.bindClick( + key: String, + onClick: (View) -> Unit + ) { + setOnClickListener { + if (beforeClickMap[key]?.invoke(this) == true) { + return@setOnClickListener + } + onClick.invoke(this) + afterClickMap[key]?.invoke(this) + } + } + + protected fun bindBeforeClick(key: String, onClick: (View) -> Boolean) { + beforeClickMap[key] = onClick + } + + protected fun bindAfterClick(key: String, onClick: (View) -> Unit) { + afterClickMap[key] = onClick + } + protected fun removeView(key: String) = viewBindMap.remove(key) override fun onReceiveInvited(info: NEInviteInfo) {} override fun onCallConnected(info: NECallInfo) {} - override fun onCallTypeChange(info: NECallTypeChangeInfo) {} + override fun onCallTypeChange(info: NECallTypeChangeInfo) { + if (info.state == SwitchCallState.REJECT || info.state == SwitchCallState.ACCEPT) { + getView(viewKeySwitchTypeTipGroup)?.run { + visibility = View.GONE + } + } + } override fun onCallEnd(info: NECallEndInfo) {} @@ -276,4 +464,8 @@ open class BaseP2pCallFragment : Fragment(), NECallEngineDelegate { override fun onAudioMuted(userId: String?, mute: Boolean) {} open fun onJoinChannel(res: Int, cid: Long, time: Long, uid: Long) {} + + private fun logApiInvoke(name: String?) { + ALog.dApi(logTag, "${this@BaseP2pCallFragment}:$name was invoked.") + } } diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/fragment/FragmentActionBridge.kt b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/fragment/FragmentActionBridge.kt index f7269be..7fe5b43 100644 --- a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/fragment/FragmentActionBridge.kt +++ b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/fragment/FragmentActionBridge.kt @@ -7,6 +7,7 @@ package com.netease.yunxin.nertc.ui.p2p.fragment import android.view.View.OnClickListener +import com.netease.lava.api.IVideoRender import com.netease.lava.nertc.sdk.video.NERtcVideoView import com.netease.yunxin.kit.call.NEResultObserver import com.netease.yunxin.kit.call.p2p.NECallEngine @@ -33,6 +34,10 @@ interface FragmentActionBridge { val isLocalMuteSpeaker: Boolean + val isLocalSmallVideo: Boolean + + val isVirtualBlur: Boolean + fun isSpeakerOn(): Boolean fun configTimeTick(config: CallUIOperationsMgr.TimeTickConfig?) @@ -43,6 +48,8 @@ interface FragmentActionBridge { fun doMuteVideo(mute: Boolean = !isLocalMuteVideo) + fun doVirtualBlur(enable: Boolean = !isVirtualBlur) + fun doSwitchCamera() fun doCall( @@ -61,11 +68,32 @@ interface FragmentActionBridge { callType: Int, switchCallState: Int, observer: NEResultObserver>? = null ) - fun setupLocalView(view: NERtcVideoView?) + fun setupLocalView( + view: NERtcVideoView?, + action: ((NERtcVideoView?) -> Unit)? = { + it?.run { + setZOrderMediaOverlay(true) + setScalingType(IVideoRender.ScalingType.SCALE_ASPECT_BALANCED) + } + } + ) - fun setupRemoteView(view: NERtcVideoView?) + fun setupRemoteView( + view: NERtcVideoView?, + action: ((NERtcVideoView?) -> Unit)? = { + it?.run { + setScalingType(IVideoRender.ScalingType.SCALE_ASPECT_BALANCED) + } + } + ) fun currentCallState(): Int fun showPermissionDialog(clickListener: OnClickListener): PermissionTipDialog + + fun showFloatingWindow() + + fun startVideoPreview() + + fun stopVideoPreview() } diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/fragment/P2PUIUpdateType.kt b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/fragment/P2PUIUpdateType.kt index 6b01a36..fb3997b 100644 --- a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/fragment/P2PUIUpdateType.kt +++ b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/fragment/P2PUIUpdateType.kt @@ -16,4 +16,9 @@ object P2PUIUpdateType { * 变更通话类型 */ const val CHANGE_CALL_TYPE = 2 + + /** + * 从浮窗转换至通话界面 + */ + const val FROM_FLOATING_WINDOW = 3 } diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/fragment/callee/AudioCalleeFragment.kt b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/fragment/callee/AudioCalleeFragment.kt index 495f1ac..6ff0c6e 100644 --- a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/fragment/callee/AudioCalleeFragment.kt +++ b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/fragment/callee/AudioCalleeFragment.kt @@ -6,7 +6,7 @@ package com.netease.yunxin.nertc.ui.p2p.fragment.callee -import android.Manifest +import android.Manifest.permission.CAMERA import android.Manifest.permission.RECORD_AUDIO import android.os.Bundle import android.view.LayoutInflater @@ -14,12 +14,7 @@ import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView -import com.netease.yunxin.kit.alog.ALog -import com.netease.yunxin.kit.call.NEResultObserver import com.netease.yunxin.kit.call.p2p.model.NECallType -import com.netease.yunxin.kit.call.p2p.model.NECallTypeChangeInfo -import com.netease.yunxin.nertc.nertcvideocall.bean.CommonResult -import com.netease.yunxin.nertc.nertcvideocall.model.SwitchCallState import com.netease.yunxin.nertc.nertcvideocall.utils.NetworkUtils import com.netease.yunxin.nertc.ui.CallKitUI import com.netease.yunxin.nertc.ui.R @@ -29,9 +24,6 @@ import com.netease.yunxin.nertc.ui.base.loadAvatarByAccId import com.netease.yunxin.nertc.ui.databinding.FragmentP2pAudioCalleeBinding import com.netease.yunxin.nertc.ui.p2p.P2PUIConfig import com.netease.yunxin.nertc.ui.p2p.fragment.BaseP2pCallFragment -import com.netease.yunxin.nertc.ui.utils.ClickUtils -import com.netease.yunxin.nertc.ui.utils.isGranted -import com.netease.yunxin.nertc.ui.utils.requestPermission import com.netease.yunxin.nertc.ui.utils.toastShort /** @@ -42,23 +34,6 @@ open class AudioCalleeFragment : BaseP2pCallFragment() { protected lateinit var binding: FragmentP2pAudioCalleeBinding - protected val switchObserver = object : NEResultObserver> { - override fun onResult(result: CommonResult?) { - if (result?.isSuccessful != true) { - context?.run { getString(R.string.tip_switch_call_type_failed).toastShort(this) } - - ALog.e( - logTag, - "doSwitchCallType to ${NECallType.VIDEO} error, result is $result." - ) - return - } - getView(viewKeySwitchTypeTipGroup)?.run { - visibility = View.VISIBLE - } - } - } - override fun toCreateRootView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View = FragmentP2pAudioCalleeBinding.inflate(inflater, container, false).run { @@ -113,45 +88,34 @@ open class AudioCalleeFragment : BaseP2pCallFragment() { } protected open fun renderOperations(uiConfig: P2PUIConfig?) { - getView(viewKeyImageCancel)?.setOnClickListener { - bridge.doHangup() - } - val enableAutoJoinWhenCalled = CallKitUI.options?.enableAutoJoinWhenCalled == true getView(viewKeySwitchTypeTipGroup)?.run { visibility = View.GONE } getView(viewKeyImageSwitchType)?.run { visibility = if (enableAutoJoinWhenCalled) View.VISIBLE else View.GONE - setOnClickListener { - if (ClickUtils.isFastClick()) { - return@setOnClickListener + bindClick(viewKeyImageSwitchType) { + if (!NetworkUtils.isConnected()) { + context?.run { getString(R.string.tip_network_error).toastShort(this) } + return@bindClick } val action = Action@{ - if (!NetworkUtils.isConnected()) { - context?.run { getString(R.string.tip_network_error).toastShort(this) } - return@Action - } - bridge.doSwitchCallType( - NECallType.VIDEO, - SwitchCallState.INVITE, - switchObserver - ) + switchCallType(NECallType.VIDEO) } - if (context?.isGranted(Manifest.permission.CAMERA) == true) { + if (arePermissionsGranted(listOf(CAMERA))) { action.invoke() - return@setOnClickListener + return@bindClick } requestPermission( - onGranted = { + listOf(CAMERA), + { action.invoke() }, - onDenied = { _, _ -> + { _, _ -> context?.run { getString(R.string.tip_permission_request_failed).toastShort(this) } - }, - Manifest.permission.CAMERA + } ) } } @@ -159,12 +123,12 @@ open class AudioCalleeFragment : BaseP2pCallFragment() { if (enableAutoJoinWhenCalled) View.VISIBLE else View.GONE getView(viewKeyImageReject)?.run { - setOnClickListener { + bindClick(viewKeyImageReject) { bridge.doHangup() } } getView(viewKeyImageAccept)?.run { - setOnClickListener { + bindClick(viewKeyImageAccept) { getView(viewKeyTextConnectingTip)?.visibility = View.VISIBLE bridge.doAccept { if (!it.isSuccessful) { @@ -176,7 +140,7 @@ open class AudioCalleeFragment : BaseP2pCallFragment() { } } getView(viewKeyImageSwitchTipClose)?.run { - setOnClickListener { + bindClick(viewKeyImageSwitchTipClose) { getView(viewKeySwitchTypeTipGroup)?.run { visibility = View.GONE } @@ -184,34 +148,11 @@ open class AudioCalleeFragment : BaseP2pCallFragment() { } } - override fun onCreateAction() { - val dialog = if (context?.isGranted(RECORD_AUDIO) == true) { - return - } else { - bridge.showPermissionDialog { - bridge.doHangup() - } - } - requestPermission( - onGranted = { - dialog.dismiss() - }, - onDenied = { _, _ -> - context?.run { getString(R.string.tip_permission_request_failed).toastShort(this) } - }, - RECORD_AUDIO - ) + override fun permissionList(): List { + return listOf(RECORD_AUDIO) } override fun toUpdateUIState(type: Int) { bridge.doConfigSpeaker(false) } - - override fun onCallTypeChange(info: NECallTypeChangeInfo) { - if (info.state == SwitchCallState.REJECT) { - getView(viewKeySwitchTypeTipGroup)?.run { - visibility = View.GONE - } - } - } } diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/fragment/callee/VideoCalleeFragment.kt b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/fragment/callee/VideoCalleeFragment.kt index 98f1b49..bcd41aa 100644 --- a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/fragment/callee/VideoCalleeFragment.kt +++ b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/fragment/callee/VideoCalleeFragment.kt @@ -14,12 +14,10 @@ import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView -import com.netease.yunxin.kit.alog.ALog -import com.netease.yunxin.kit.call.NEResultObserver +import com.netease.lava.nertc.sdk.video.NERtcVideoView +import com.netease.yunxin.kit.call.p2p.model.NECallEndInfo +import com.netease.yunxin.kit.call.p2p.model.NECallInitRtcMode import com.netease.yunxin.kit.call.p2p.model.NECallType -import com.netease.yunxin.kit.call.p2p.model.NECallTypeChangeInfo -import com.netease.yunxin.nertc.nertcvideocall.bean.CommonResult -import com.netease.yunxin.nertc.nertcvideocall.model.SwitchCallState import com.netease.yunxin.nertc.nertcvideocall.utils.NetworkUtils import com.netease.yunxin.nertc.ui.CallKitUI import com.netease.yunxin.nertc.ui.R @@ -29,9 +27,6 @@ import com.netease.yunxin.nertc.ui.base.loadAvatarByAccId import com.netease.yunxin.nertc.ui.databinding.FragmentP2pVideoCalleeBinding import com.netease.yunxin.nertc.ui.p2p.P2PUIConfig import com.netease.yunxin.nertc.ui.p2p.fragment.BaseP2pCallFragment -import com.netease.yunxin.nertc.ui.utils.ClickUtils -import com.netease.yunxin.nertc.ui.utils.isGranted -import com.netease.yunxin.nertc.ui.utils.requestPermission import com.netease.yunxin.nertc.ui.utils.toastShort /** @@ -43,22 +38,6 @@ open class VideoCalleeFragment : BaseP2pCallFragment() { protected lateinit var binding: FragmentP2pVideoCalleeBinding - protected val switchObserver = object : NEResultObserver> { - override fun onResult(result: CommonResult?) { - if (result?.isSuccessful != true) { - context?.run { getString(R.string.tip_switch_call_type_failed).toastShort(this) } - ALog.e( - logTag, - "doSwitchCallType to ${NECallType.AUDIO} error, result is $result." - ) - return - } - getView(viewKeySwitchTypeTipGroup)?.run { - visibility = View.VISIBLE - } - } - } - override fun toCreateRootView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View = FragmentP2pVideoCalleeBinding.inflate(inflater, container, false).run { @@ -68,6 +47,7 @@ open class VideoCalleeFragment : BaseP2pCallFragment() { override fun toBindView() { bindView(viewKeyImageBigBackground, binding.ivBg) + bindView(viewKeyVideoViewPreview, binding.videoViewPreview) bindView(viewKeyTextUserInnerAvatar, binding.tvUserInnerAvatar) bindView(viewKeyImageUserInnerAvatar, binding.ivUserInnerAvatar) @@ -113,8 +93,8 @@ open class VideoCalleeFragment : BaseP2pCallFragment() { } protected open fun renderOperations(uiConfig: P2PUIConfig?) { - getView(viewKeyImageCancel)?.setOnClickListener { - bridge.doHangup() + getView(viewKeyVideoViewPreview)?.run { + visibility = if (isEnableVideoCalleePreview()) View.VISIBLE else View.GONE } val enableAutoJoinWhenCalled = CallKitUI.options?.enableAutoJoinWhenCalled == true @@ -123,27 +103,24 @@ open class VideoCalleeFragment : BaseP2pCallFragment() { } getView(viewKeyImageSwitchType)?.run { visibility = if (enableAutoJoinWhenCalled) View.VISIBLE else View.GONE - setOnClickListener { - if (ClickUtils.isFastClick()) { - return@setOnClickListener - } + bindClick(viewKeyImageSwitchType) { if (!NetworkUtils.isConnected()) { context?.run { getString(R.string.tip_network_error).toastShort(this) } - return@setOnClickListener + return@bindClick } - bridge.doSwitchCallType(NECallType.AUDIO, SwitchCallState.INVITE, switchObserver) + switchCallType(NECallType.AUDIO) } } getView(viewKeyTextSwitchTypeDesc)?.visibility = if (enableAutoJoinWhenCalled) View.VISIBLE else View.GONE getView(viewKeyImageReject)?.run { - setOnClickListener { + bindClick(viewKeyImageReject) { bridge.doHangup() } } getView(viewKeyImageAccept)?.run { - setOnClickListener { + bindClick(viewKeyImageAccept) { getView(viewKeyTextConnectingTip)?.visibility = View.VISIBLE bridge.doAccept { if (!it.isSuccessful) { @@ -155,7 +132,7 @@ open class VideoCalleeFragment : BaseP2pCallFragment() { } } getView(viewKeyImageSwitchTipClose)?.run { - setOnClickListener { + bindClick(viewKeyImageSwitchTipClose) { getView(viewKeySwitchTypeTipGroup)?.run { visibility = View.GONE } @@ -163,41 +140,50 @@ open class VideoCalleeFragment : BaseP2pCallFragment() { } } - override fun onCreateAction() { - val dialog = if (context?.isGranted(RECORD_AUDIO, CAMERA) == true) { - return - } else { - bridge.showPermissionDialog { - bridge.doHangup() + override fun actionForPermissionGranted() { + if (isEnableVideoCalleePreview()) { + getView(viewKeyVideoViewPreview)?.run { + bridge.setupLocalView(this) } + bridge.startVideoPreview() } - requestPermission( - onGranted = { - if (it.containsAll(listOf(RECORD_AUDIO, CAMERA))) { - dialog.dismiss() - } else { - context?.run { - getString(R.string.tip_permission_request_failed).toastShort(this) - } - } - }, - onDenied = { _, _ -> - context?.run { getString(R.string.tip_permission_request_failed).toastShort(this) } - }, - RECORD_AUDIO, - CAMERA - ) + } + + override fun permissionList(): List { + return listOf(RECORD_AUDIO, CAMERA) } override fun toUpdateUIState(type: Int) { bridge.doConfigSpeaker(true) } - override fun onCallTypeChange(info: NECallTypeChangeInfo) { - if (info.state == SwitchCallState.REJECT) { - getView(viewKeySwitchTypeTipGroup)?.run { - visibility = View.GONE + override fun onHiddenChanged(hidden: Boolean) { + if (isEnableVideoCalleePreview()) { + if (hidden) { + getView(viewKeyVideoViewPreview)?.run { + visibility = View.GONE + } + bridge.stopVideoPreview() + } else { + getView(viewKeyVideoViewPreview)?.run { + visibility = View.VISIBLE + } + bridge.startVideoPreview() } } } + + override fun onCallEnd(info: NECallEndInfo) { + if (isEnableVideoCalleePreview()) { + bridge.stopVideoPreview() + } + } + + protected open fun isEnableVideoCalleePreview(): Boolean { + return ( + CallKitUI.options?.initRtcMode == NECallInitRtcMode.GLOBAL || + CallKitUI.options?.initRtcMode == NECallInitRtcMode.IN_NEED + ) && + bridge.uiConfig?.enableVideoCalleePreview == true + } } diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/fragment/caller/AudioCallerFragment.kt b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/fragment/caller/AudioCallerFragment.kt index 7b86d80..6facebd 100644 --- a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/fragment/caller/AudioCallerFragment.kt +++ b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/fragment/caller/AudioCallerFragment.kt @@ -15,12 +15,7 @@ import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import com.netease.nimlib.sdk.ResponseCode -import com.netease.yunxin.kit.alog.ALog -import com.netease.yunxin.kit.call.NEResultObserver import com.netease.yunxin.kit.call.p2p.model.NECallType -import com.netease.yunxin.kit.call.p2p.model.NECallTypeChangeInfo -import com.netease.yunxin.nertc.nertcvideocall.bean.CommonResult -import com.netease.yunxin.nertc.nertcvideocall.model.SwitchCallState import com.netease.yunxin.nertc.nertcvideocall.model.impl.state.CallState import com.netease.yunxin.nertc.nertcvideocall.utils.NetworkUtils import com.netease.yunxin.nertc.ui.CallKitUI @@ -31,9 +26,7 @@ import com.netease.yunxin.nertc.ui.base.loadAvatarByAccId import com.netease.yunxin.nertc.ui.databinding.FragmentP2pAudioCallerBinding import com.netease.yunxin.nertc.ui.p2p.P2PUIConfig import com.netease.yunxin.nertc.ui.p2p.fragment.BaseP2pCallFragment -import com.netease.yunxin.nertc.ui.utils.ClickUtils -import com.netease.yunxin.nertc.ui.utils.isGranted -import com.netease.yunxin.nertc.ui.utils.requestPermission +import com.netease.yunxin.nertc.ui.p2p.fragment.P2PUIUpdateType.FROM_FLOATING_WINDOW import com.netease.yunxin.nertc.ui.utils.toastShort /** @@ -45,22 +38,6 @@ open class AudioCallerFragment : BaseP2pCallFragment() { protected lateinit var binding: FragmentP2pAudioCallerBinding - protected val switchObserver = object : NEResultObserver> { - override fun onResult(result: CommonResult?) { - if (result?.isSuccessful != true) { - context?.run { getString(R.string.tip_switch_call_type_failed).toastShort(this) } - ALog.e( - logTag, - "doSwitchCallType to ${NECallType.VIDEO} error, result is $result." - ) - return - } - getView(viewKeySwitchTypeTipGroup)?.run { - visibility = View.VISIBLE - } - } - } - override fun toCreateRootView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? = FragmentP2pAudioCallerBinding.inflate(inflater, container, false).run { @@ -90,6 +67,7 @@ open class AudioCallerFragment : BaseP2pCallFragment() { bindView(viewKeyTextMuteAudioDesc, binding.tvCallMuteAudioTip) bindView(viewKeyImageSpeaker, binding.ivCallSpeaker) bindView(viewKeyTextSpeakerDesc, binding.tvCallSpeakerTip) + bindView(viewKeyImageFloatingWindow, binding.ivFloatingWindow) } override fun toRenderView(callParam: CallParam, uiConfig: P2PUIConfig?) { @@ -115,7 +93,7 @@ open class AudioCallerFragment : BaseP2pCallFragment() { } protected open fun renderOperations(uiConfig: P2PUIConfig?) { - getView(viewKeyImageCancel)?.setOnClickListener { + getView(viewKeyImageCancel)?.bindClick(viewKeyImageCancel) { bridge.doHangup() activity?.finish() } @@ -126,43 +104,32 @@ open class AudioCallerFragment : BaseP2pCallFragment() { } getView(viewKeyImageSwitchType)?.run { visibility = if (enableAutoJoinWhenCalled) View.VISIBLE else View.GONE - setOnClickListener { - if (ClickUtils.isFastClick()) { - return@setOnClickListener + bindClick(viewKeyImageSwitchType) { + if (!NetworkUtils.isConnected()) { + context?.run { getString(R.string.tip_network_error).toastShort(this) } + return@bindClick } val action = Action@{ - if (!NetworkUtils.isConnected()) { - context?.run { getString(R.string.tip_network_error).toastShort(this) } - return@Action - } - bridge.doSwitchCallType( - NECallType.VIDEO, - SwitchCallState.INVITE, - switchObserver - ) + switchCallType(NECallType.VIDEO) } - if (context?.isGranted(CAMERA) == true) { + if (arePermissionsGranted(listOf(CAMERA))) { action.invoke() - return@setOnClickListener + return@bindClick } - requestPermission( - onGranted = { - action.invoke() - }, - onDenied = { _, _ -> - context?.run { - getString(R.string.tip_permission_request_failed).toastShort(this) - } - }, - CAMERA - ) + requestPermission(listOf(CAMERA), { + action.invoke() + }, { _, _ -> + context?.run { + getString(R.string.tip_permission_request_failed).toastShort(this) + } + }) } } getView(viewKeyTextSwitchTypeDesc)?.visibility = if (enableAutoJoinWhenCalled) View.VISIBLE else View.GONE getView(viewKeyMuteImageAudio)?.run { - setOnClickListener { + bindClick(viewKeyMuteImageAudio) { bridge.doMuteAudio() setImageResource( if (bridge.isLocalMuteAudio) R.drawable.icon_call_audio_off else R.drawable.icon_call_audio_on @@ -171,7 +138,7 @@ open class AudioCallerFragment : BaseP2pCallFragment() { } getView(viewKeyImageSpeaker)?.run { - setOnClickListener { + bindClick(viewKeyImageSpeaker) { val speakerEnable = !bridge.isSpeakerOn() bridge.doConfigSpeaker(speakerEnable) setImageResource( @@ -180,64 +147,60 @@ open class AudioCallerFragment : BaseP2pCallFragment() { } } getView(viewKeyImageSwitchTipClose)?.run { - setOnClickListener { + bindClick(viewKeyImageSwitchTipClose) { getView(viewKeySwitchTypeTipGroup)?.run { visibility = View.GONE } } } - } - - override fun onCreateAction() { - val action = { - if (bridge.currentCallState() == CallState.STATE_IDLE) { - bridge.doCall { result -> - if (result?.isSuccessful != true && - result.code != ResponseCode.RES_PEER_NIM_OFFLINE.toInt() && - result.code != ResponseCode.RES_PEER_PUSH_OFFLINE.toInt() - ) { - context?.run { getString(R.string.tip_start_call_failed).toastShort(this) } - } - } + getView(viewKeyImageFloatingWindow)?.run { + visibility = if (uiConfig?.enableFloatingWindow == true) View.VISIBLE else View.GONE + bindClick(viewKeyImageFloatingWindow) { + bridge.showFloatingWindow() } } + } - val dialog = if (context?.isGranted(RECORD_AUDIO) == true) { - action.invoke() + override fun permissionList(): List { + return listOf(RECORD_AUDIO) + } + + override fun actionForPermissionGranted() { + if (bridge.currentCallState() != CallState.STATE_IDLE) { return - } else { - bridge.showPermissionDialog { - activity?.finish() + } + bridge.doCall { result -> + if (result?.isSuccessful != true && + result.code != ResponseCode.RES_PEER_NIM_OFFLINE.toInt() && + result.code != ResponseCode.RES_PEER_PUSH_OFFLINE.toInt() + ) { + context?.run { getString(R.string.tip_start_call_failed).toastShort(this) } } } - - requestPermission( - onGranted = { - dialog.dismiss() - action.invoke() - }, - onDenied = { _, _ -> - context?.run { - getString(R.string.tip_permission_request_failed).toastShort(this) - } - }, - RECORD_AUDIO - ) } override fun toUpdateUIState(type: Int) { - bridge.doConfigSpeaker(false) - getView(viewKeyImageSpeaker)?.run { - setImageResource( - if (bridge.isSpeakerOn()) R.drawable.icon_call_audio_speaker_on else R.drawable.icon_call_audio_speaker_off - ) - } - } + when (type) { + FROM_FLOATING_WINDOW -> { + getView(viewKeyImageSpeaker)?.run { + setImageResource( + if (bridge.isSpeakerOn()) R.drawable.icon_call_audio_speaker_on else R.drawable.icon_call_audio_speaker_off + ) + } + getView(viewKeyMuteImageAudio)?.run { + setImageResource( + if (bridge.isLocalMuteAudio) R.drawable.icon_call_audio_off else R.drawable.icon_call_audio_on + ) + } + } - override fun onCallTypeChange(info: NECallTypeChangeInfo) { - if (info.state == SwitchCallState.REJECT) { - getView(viewKeySwitchTypeTipGroup)?.run { - visibility = View.GONE + else -> { + bridge.doConfigSpeaker(false) + getView(viewKeyImageSpeaker)?.run { + setImageResource( + if (bridge.isSpeakerOn()) R.drawable.icon_call_audio_speaker_on else R.drawable.icon_call_audio_speaker_off + ) + } } } } diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/fragment/caller/VideoCallerFragment.kt b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/fragment/caller/VideoCallerFragment.kt index 1e23398..a1a20ca 100644 --- a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/fragment/caller/VideoCallerFragment.kt +++ b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/fragment/caller/VideoCallerFragment.kt @@ -14,18 +14,10 @@ import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView -import com.netease.lava.nertc.sdk.NERtcConstants.ErrorCode.ENGINE_ERROR_DEVICE_PREVIEW_ALREADY_STARTED -import com.netease.lava.nertc.sdk.NERtcConstants.ErrorCode.OK -import com.netease.lava.nertc.sdk.NERtcEx import com.netease.lava.nertc.sdk.video.NERtcVideoView import com.netease.nimlib.sdk.ResponseCode -import com.netease.yunxin.kit.alog.ALog -import com.netease.yunxin.kit.call.NEResultObserver import com.netease.yunxin.kit.call.p2p.model.NECallEndInfo import com.netease.yunxin.kit.call.p2p.model.NECallType -import com.netease.yunxin.kit.call.p2p.model.NECallTypeChangeInfo -import com.netease.yunxin.nertc.nertcvideocall.bean.CommonResult -import com.netease.yunxin.nertc.nertcvideocall.model.SwitchCallState import com.netease.yunxin.nertc.nertcvideocall.model.impl.state.CallState import com.netease.yunxin.nertc.nertcvideocall.utils.NetworkUtils import com.netease.yunxin.nertc.ui.CallKitUI @@ -36,9 +28,6 @@ import com.netease.yunxin.nertc.ui.base.loadAvatarByAccId import com.netease.yunxin.nertc.ui.databinding.FragmentP2pVideoCallerBinding import com.netease.yunxin.nertc.ui.p2p.P2PUIConfig import com.netease.yunxin.nertc.ui.p2p.fragment.BaseP2pCallFragment -import com.netease.yunxin.nertc.ui.utils.ClickUtils -import com.netease.yunxin.nertc.ui.utils.isGranted -import com.netease.yunxin.nertc.ui.utils.requestPermission import com.netease.yunxin.nertc.ui.utils.toastShort /** @@ -47,26 +36,8 @@ import com.netease.yunxin.nertc.ui.utils.toastShort open class VideoCallerFragment : BaseP2pCallFragment() { protected val logTag = "VideoCallerFragment" - protected val switchObserver = object : NEResultObserver> { - override fun onResult(result: CommonResult?) { - if (result?.isSuccessful != true) { - context?.run { getString(R.string.tip_switch_call_type_failed).toastShort(this) } - ALog.e( - logTag, - "doSwitchCallType to ${NECallType.AUDIO} error, result is $result." - ) - return - } - getView(viewKeySwitchTypeTipGroup)?.run { - visibility = View.VISIBLE - } - } - } - protected lateinit var binding: FragmentP2pVideoCallerBinding - protected var startPreviewCode: Int? = null - override fun toCreateRootView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? = FragmentP2pVideoCallerBinding.inflate(inflater, container, false).run { @@ -92,6 +63,7 @@ open class VideoCallerFragment : BaseP2pCallFragment() { bindView(viewKeyTextSwitchTip, binding.tvSwitchTip) bindView(viewKeyImageSwitchTipClose, binding.ivSwitchTipClose) bindView(viewKeySwitchTypeTipGroup, binding.switchTypeTipGroup) + bindView(viewKeyImageFloatingWindow, binding.ivFloatingWindow) } override fun toRenderView(callParam: CallParam, uiConfig: P2PUIConfig?) { @@ -117,7 +89,7 @@ open class VideoCallerFragment : BaseP2pCallFragment() { } protected open fun renderOperations(uiConfig: P2PUIConfig?) { - getView(viewKeyImageCancel)?.setOnClickListener { + getView(viewKeyImageCancel)?.bindClick(viewKeyImageCancel) { bridge.doHangup() activity?.finish() } @@ -128,103 +100,71 @@ open class VideoCallerFragment : BaseP2pCallFragment() { } getView(viewKeyImageSwitchType)?.run { visibility = if (enableAutoJoinWhenCalled) View.VISIBLE else View.GONE - setOnClickListener { - if (ClickUtils.isFastClick()) { - return@setOnClickListener - } + bindClick(viewKeyImageSwitchType) { if (!NetworkUtils.isConnected()) { context?.run { getString(R.string.tip_network_error).toastShort(this) } - return@setOnClickListener + return@bindClick } - bridge.doSwitchCallType(NECallType.AUDIO, SwitchCallState.INVITE, switchObserver) + switchCallType(NECallType.AUDIO) } } getView(viewKeyTextSwitchTypeDesc)?.visibility = if (enableAutoJoinWhenCalled) View.VISIBLE else View.GONE getView(viewKeyImageSwitchTipClose)?.run { - setOnClickListener { + bindClick(viewKeyImageSwitchTipClose) { getView(viewKeySwitchTypeTipGroup)?.run { visibility = View.GONE } } } + getView(viewKeyImageFloatingWindow)?.run { + visibility = if (uiConfig?.enableFloatingWindow == true) View.VISIBLE else View.GONE + bindClick(viewKeyImageFloatingWindow) { + bridge.showFloatingWindow() + } + } + } + + override fun permissionList(): List { + return listOf(RECORD_AUDIO, CAMERA) } - override fun onCreateAction() { - val action = { - if (bridge.currentCallState() == CallState.STATE_IDLE) { - bridge.doCall { result -> - if (result?.isSuccessful != true && - result.code != ResponseCode.RES_PEER_NIM_OFFLINE.toInt() && - result.code != ResponseCode.RES_PEER_PUSH_OFFLINE.toInt() - ) { - context?.run { getString(R.string.tip_start_call_failed).toastShort(this) } - } + override fun actionForPermissionGranted() { + if (bridge.currentCallState() == CallState.STATE_IDLE) { + bridge.doCall { result -> + if (result?.isSuccessful != true && + result.code != ResponseCode.RES_PEER_NIM_OFFLINE.toInt() && + result.code != ResponseCode.RES_PEER_PUSH_OFFLINE.toInt() + ) { + context?.run { getString(R.string.tip_start_call_failed).toastShort(this) } } } - getView(viewKeyVideoViewPreview)?.run { - bridge.setupLocalView(this) - } - if (CallKitUI.options?.joinRtcWhenCall == false && - startPreviewCode != OK && - startPreviewCode != ENGINE_ERROR_DEVICE_PREVIEW_ALREADY_STARTED - ) { - startPreviewCode = startVideoPreview() - } } - - val dialog = if (context?.isGranted(RECORD_AUDIO, CAMERA) == true) { - action.invoke() - return - } else { - bridge.showPermissionDialog { - activity?.finish() - } + getView(viewKeyVideoViewPreview)?.run { + bridge.setupLocalView(this) } - - requestPermission( - onGranted = { - if (it.containsAll(listOf(RECORD_AUDIO, CAMERA))) { - dialog.dismiss() - action.invoke() - } else { - context?.run { - getString(R.string.tip_permission_request_failed).toastShort(this) - } - } - }, - onDenied = { _, _ -> - context?.run { getString(R.string.tip_permission_request_failed).toastShort(this) } - }, - RECORD_AUDIO, - CAMERA - ) + bridge.startVideoPreview() } override fun toUpdateUIState(type: Int) { bridge.doConfigSpeaker(true) } - override fun onCallEnd(info: NECallEndInfo) { - if (startPreviewCode == OK || startPreviewCode == ENGINE_ERROR_DEVICE_PREVIEW_ALREADY_STARTED) { - stopVideoPreview() - startPreviewCode = null - } - } - - protected open fun startVideoPreview(): Int = NERtcEx.getInstance().startVideoPreview().apply { - ALog.d(logTag, "startVideoPreview is $this, this fragment is ${this@VideoCallerFragment}") - } - - protected open fun stopVideoPreview(): Int = NERtcEx.getInstance().stopVideoPreview().apply { - ALog.d(logTag, "stopVideoPreview is $this, this fragment is ${this@VideoCallerFragment}") - } - - override fun onCallTypeChange(info: NECallTypeChangeInfo) { - if (info.state == SwitchCallState.REJECT) { - getView(viewKeySwitchTypeTipGroup)?.run { + override fun onHiddenChanged(hidden: Boolean) { + if (hidden) { + getView(viewKeyVideoViewPreview)?.run { visibility = View.GONE } + bridge.stopVideoPreview() + } else { + getView(viewKeyVideoViewPreview)?.run { + visibility = View.VISIBLE + } + bridge.startVideoPreview() } } + + override fun onCallEnd(info: NECallEndInfo) { + bridge.stopVideoPreview() + } } diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/fragment/onthecall/AudioOnTheCallFragment.kt b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/fragment/onthecall/AudioOnTheCallFragment.kt index b92f6ac..6c6e299 100644 --- a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/fragment/onthecall/AudioOnTheCallFragment.kt +++ b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/fragment/onthecall/AudioOnTheCallFragment.kt @@ -6,21 +6,16 @@ package com.netease.yunxin.nertc.ui.p2p.fragment.onthecall -import android.Manifest +import android.Manifest.permission.CAMERA +import android.Manifest.permission.RECORD_AUDIO import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView -import com.netease.yunxin.kit.alog.ALog -import com.netease.yunxin.kit.call.NEResultObserver import com.netease.yunxin.kit.call.p2p.model.NECallEndInfo import com.netease.yunxin.kit.call.p2p.model.NECallType -import com.netease.yunxin.kit.call.p2p.model.NECallTypeChangeInfo -import com.netease.yunxin.nertc.nertcvideocall.bean.CommonResult -import com.netease.yunxin.nertc.nertcvideocall.model.SwitchCallState -import com.netease.yunxin.nertc.nertcvideocall.model.SwitchCallState.REJECT import com.netease.yunxin.nertc.nertcvideocall.model.impl.state.CallState import com.netease.yunxin.nertc.nertcvideocall.utils.NetworkUtils import com.netease.yunxin.nertc.ui.R @@ -32,11 +27,9 @@ import com.netease.yunxin.nertc.ui.p2p.CallUIOperationsMgr import com.netease.yunxin.nertc.ui.p2p.P2PUIConfig import com.netease.yunxin.nertc.ui.p2p.fragment.BaseP2pCallFragment import com.netease.yunxin.nertc.ui.p2p.fragment.P2PUIUpdateType.CHANGE_CALL_TYPE +import com.netease.yunxin.nertc.ui.p2p.fragment.P2PUIUpdateType.FROM_FLOATING_WINDOW import com.netease.yunxin.nertc.ui.p2p.fragment.P2PUIUpdateType.INIT -import com.netease.yunxin.nertc.ui.utils.ClickUtils import com.netease.yunxin.nertc.ui.utils.formatSecondTime -import com.netease.yunxin.nertc.ui.utils.isGranted -import com.netease.yunxin.nertc.ui.utils.requestPermission import com.netease.yunxin.nertc.ui.utils.toastShort import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -51,24 +44,6 @@ open class AudioOnTheCallFragment : BaseP2pCallFragment() { protected lateinit var binding: FragmentP2pAudioOnTheCallBinding - protected val switchObserver = object : NEResultObserver> { - override fun onResult(result: CommonResult?) { - if (result?.isSuccessful != true) { - context?.run { - getString(R.string.tip_switch_call_type_failed).toastShort(this) - } - ALog.e( - logTag, - "doSwitchCallType to ${NECallType.VIDEO} error, result is $result." - ) - return - } - getView(viewKeySwitchTypeTipGroup)?.run { - visibility = View.VISIBLE - } - } - } - override fun toCreateRootView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View = FragmentP2pAudioOnTheCallBinding.inflate(inflater, container, false).run { @@ -95,6 +70,7 @@ open class AudioOnTheCallFragment : BaseP2pCallFragment() { bindView(viewKeyTextSwitchTip, binding.tvSwitchTip) bindView(viewKeyImageSwitchTipClose, binding.ivSwitchTipClose) bindView(viewKeySwitchTypeTipGroup, binding.switchTypeTipGroup) + bindView(viewKeyImageFloatingWindow, binding.ivFloatingWindow) } override fun toRenderView(callParam: CallParam, uiConfig: P2PUIConfig?) { @@ -120,38 +96,25 @@ open class AudioOnTheCallFragment : BaseP2pCallFragment() { } protected open fun renderOperations(uiConfig: P2PUIConfig?) { - getView(viewKeyImageCancel)?.setOnClickListener { - bridge.doHangup() - } - - getView(viewKeySwitchTypeTipGroup)?.run { - visibility = View.GONE - } getView(viewKeyImageSwitchType)?.run { visibility = if (uiConfig?.showAudio2VideoSwitchOnTheCall == true) View.VISIBLE else View.GONE - setOnClickListener { - if (ClickUtils.isFastClick()) { - return@setOnClickListener + bindClick(viewKeyImageSwitchType) { + if (!NetworkUtils.isConnected()) { + context?.run { + getString(R.string.tip_network_error).toastShort(this) + } + return@bindClick } val action = Action@{ - if (!NetworkUtils.isConnected()) { - context?.run { - getString(R.string.tip_network_error).toastShort(this) - } - return@Action - } - bridge.doSwitchCallType( - NECallType.VIDEO, - SwitchCallState.INVITE, - switchObserver - ) + switchCallType(NECallType.VIDEO) } - if (context?.isGranted(Manifest.permission.CAMERA) == true) { + if (arePermissionsGranted(listOf(CAMERA))) { action.invoke() - return@setOnClickListener + return@bindClick } requestPermission( + listOf(CAMERA), onGranted = { action.invoke() }, @@ -159,13 +122,12 @@ open class AudioOnTheCallFragment : BaseP2pCallFragment() { context?.run { getString(R.string.tip_permission_request_failed).toastShort(this) } - }, - Manifest.permission.CAMERA + } ) } } getView(viewKeyMuteImageAudio)?.run { - setOnClickListener { + bindClick(viewKeyMuteImageAudio) { bridge.doMuteAudio() setImageResource( if (bridge.isLocalMuteAudio) R.drawable.voice_off else R.drawable.voice_on @@ -173,7 +135,7 @@ open class AudioOnTheCallFragment : BaseP2pCallFragment() { } } getView(viewKeyImageSpeaker)?.run { - setOnClickListener { + bindClick(viewKeyImageSpeaker) { val speakerEnable = !bridge.isSpeakerOn() bridge.doConfigSpeaker(speakerEnable) setImageResource( @@ -182,27 +144,27 @@ open class AudioOnTheCallFragment : BaseP2pCallFragment() { } } getView(viewKeyImageHangup)?.run { - setOnClickListener { + bindClick(viewKeyImageHangup) { bridge.doHangup() } } - - getView(viewKeyTextTimeCountdown)?.run { - bridge.configTimeTick( - CallUIOperationsMgr.TimeTickConfig({ - CoroutineScope(Dispatchers.Main).launch { - this@run.text = it.formatSecondTime() - } - }) - ) - } getView(viewKeyImageSwitchTipClose)?.run { - setOnClickListener { + bindClick(viewKeyImageSwitchTipClose) { getView(viewKeySwitchTypeTipGroup)?.run { visibility = View.GONE } } } + getView(viewKeyImageFloatingWindow)?.run { + visibility = if (uiConfig?.enableFloatingWindow == true) View.VISIBLE else View.GONE + bindClick(viewKeyImageFloatingWindow) { + bridge.showFloatingWindow() + } + } + } + + override fun permissionList(): List { + return listOf(RECORD_AUDIO) } override fun onCreateAction() { @@ -241,14 +203,34 @@ open class AudioOnTheCallFragment : BaseP2pCallFragment() { setImageResource(R.drawable.voice_on) } } + FROM_FLOATING_WINDOW -> { + getView(viewKeyImageSpeaker)?.run { + setImageResource( + if (bridge.isSpeakerOn()) R.drawable.speaker_on else R.drawable.speaker_off + ) + } + getView(viewKeyMuteImageAudio)?.run { + setImageResource( + if (bridge.isLocalMuteAudio) R.drawable.voice_off else R.drawable.voice_on + ) + } + } } + toInitState() } - override fun onCallTypeChange(info: NECallTypeChangeInfo) { - if (info.state == REJECT) { - getView(viewKeySwitchTypeTipGroup)?.run { - visibility = View.GONE - } + protected open fun toInitState() { + getView(viewKeyTextTimeCountdown)?.run { + bridge.configTimeTick( + CallUIOperationsMgr.TimeTickConfig({ + CoroutineScope(Dispatchers.Main).launch { + this@run.text = it.formatSecondTime() + } + }) + ) + } + getView(viewKeySwitchTypeTipGroup)?.run { + visibility = View.GONE } } } diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/fragment/onthecall/VideoOnTheCallFragment.kt b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/fragment/onthecall/VideoOnTheCallFragment.kt index c71cc5a..662e198 100644 --- a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/fragment/onthecall/VideoOnTheCallFragment.kt +++ b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/p2p/fragment/onthecall/VideoOnTheCallFragment.kt @@ -6,6 +6,8 @@ package com.netease.yunxin.nertc.ui.p2p.fragment.onthecall +import android.Manifest.permission.CAMERA +import android.Manifest.permission.RECORD_AUDIO import android.graphics.Color import android.os.Bundle import android.text.TextUtils @@ -14,15 +16,16 @@ import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView +import android.widget.Toast import com.bumptech.glide.Glide +import com.netease.lava.api.IVideoRender +import com.netease.lava.nertc.sdk.NERtcConstants import com.netease.lava.nertc.sdk.video.NERtcVideoView import com.netease.yunxin.kit.alog.ALog -import com.netease.yunxin.kit.call.NEResultObserver import com.netease.yunxin.kit.call.p2p.model.NECallEndInfo import com.netease.yunxin.kit.call.p2p.model.NECallType -import com.netease.yunxin.kit.call.p2p.model.NECallTypeChangeInfo -import com.netease.yunxin.nertc.nertcvideocall.bean.CommonResult -import com.netease.yunxin.nertc.nertcvideocall.model.SwitchCallState +import com.netease.yunxin.nertc.nertcvideocall.model.impl.NERtcCallbackExTemp +import com.netease.yunxin.nertc.nertcvideocall.model.impl.NERtcCallbackProxyMgr import com.netease.yunxin.nertc.nertcvideocall.model.impl.state.CallState import com.netease.yunxin.nertc.nertcvideocall.utils.NetworkUtils import com.netease.yunxin.nertc.ui.R @@ -33,7 +36,9 @@ import com.netease.yunxin.nertc.ui.databinding.FragmentP2pVideoOnTheCallBinding import com.netease.yunxin.nertc.ui.p2p.CallUIOperationsMgr import com.netease.yunxin.nertc.ui.p2p.P2PUIConfig import com.netease.yunxin.nertc.ui.p2p.fragment.BaseP2pCallFragment -import com.netease.yunxin.nertc.ui.utils.ClickUtils +import com.netease.yunxin.nertc.ui.p2p.fragment.P2PUIUpdateType.CHANGE_CALL_TYPE +import com.netease.yunxin.nertc.ui.p2p.fragment.P2PUIUpdateType.FROM_FLOATING_WINDOW +import com.netease.yunxin.nertc.ui.p2p.fragment.P2PUIUpdateType.INIT import com.netease.yunxin.nertc.ui.utils.formatSecondTime import com.netease.yunxin.nertc.ui.utils.toastShort import kotlinx.coroutines.CoroutineScope @@ -47,27 +52,33 @@ open class VideoOnTheCallFragment : BaseP2pCallFragment() { protected val logTag = "VideoOnTheCallFragment" - protected val switchObserver = object : NEResultObserver> { - override fun onResult(result: CommonResult?) { - if (result?.isSuccessful != true) { - context?.run { - getString(R.string.tip_switch_call_type_failed).toastShort(this) + protected lateinit var binding: FragmentP2pVideoOnTheCallBinding + + protected open val rtcCallback = object : NERtcCallbackExTemp() { + override fun onVirtualBackgroundSourceEnabled(enabled: Boolean, reason: Int) { + var tipRes: Int? = null + when (reason) { + NERtcConstants.NERtcVirtualBackgroundSourceStateReason.VBS_STATE_REASON_SUCCESS -> { + tipRes = R.string.ui_tip_virtual_blur_success + } + + NERtcConstants.NERtcVirtualBackgroundSourceStateReason.VBS_STATE_REASON_DEVICE_NOT_SUPPORTED -> { + tipRes = R.string.ui_tip_virtual_blur_device_not_supported + getView(viewKeyImageVirtualBlur)?.run { + setImageResource(R.drawable.icon_call_virtual_blur_off) + } + CallUIOperationsMgr.updateUIState(isVirtualBlur = false) } - ALog.e( - logTag, - "doSwitchCallType to ${NECallType.AUDIO} error, result is $result." - ) - return } - getView(viewKeySwitchTypeTipGroup)?.run { - visibility = View.VISIBLE + + tipRes ?: return + context?.run { + Toast.makeText(this, tipRes, Toast.LENGTH_SHORT).show() } } } - protected lateinit var binding: FragmentP2pVideoOnTheCallBinding - - protected var localIsSmallVideo = true + private var permissionAllowed = false override fun toCreateRootView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? @@ -95,6 +106,8 @@ open class VideoOnTheCallFragment : BaseP2pCallFragment() { bindView(viewKeyTextSwitchTip, binding.tvSwitchTip) bindView(viewKeyImageSwitchTipClose, binding.ivSwitchTipClose) bindView(viewKeySwitchTypeTipGroup, binding.switchTypeTipGroup) + bindView(viewKeyImageFloatingWindow, binding.ivFloatingWindow) + bindView(viewKeyImageVirtualBlur, binding.ivVirtualBlur) } override fun toRenderView(callParam: CallParam, uiConfig: P2PUIConfig?) { @@ -120,39 +133,27 @@ open class VideoOnTheCallFragment : BaseP2pCallFragment() { } protected open fun renderOperations(uiConfig: P2PUIConfig?) { - getView(viewKeyImageCancel)?.setOnClickListener { - bridge.doHangup() - } - getView(viewKeySwitchTypeTipGroup)?.run { - visibility = View.GONE - } getView(viewKeyImageSwitchType)?.run { visibility = if (uiConfig?.showVideo2AudioSwitchOnTheCall == true) View.VISIBLE else View.GONE - setOnClickListener { - if (ClickUtils.isFastClick()) { - return@setOnClickListener - } + bindClick(viewKeyImageSwitchType) { if (!NetworkUtils.isConnected()) { context?.run { getString(R.string.tip_network_error).toastShort(this) } - return@setOnClickListener + return@bindClick } - bridge.doSwitchCallType(NECallType.AUDIO, SwitchCallState.INVITE, switchObserver) + switchCallType(NECallType.AUDIO) } } getView(viewKeyMuteImageAudio)?.run { - setOnClickListener { + bindClick(viewKeyMuteImageAudio) { bridge.doMuteAudio() setImageResource( if (bridge.isLocalMuteAudio) R.drawable.voice_off else R.drawable.voice_on ) } } - getView(viewKeyImageVideoShadeSmall)?.run { - setBackgroundColor(Color.BLACK) - } getView(viewKeyMuteImageVideo)?.run { - setOnClickListener { + bindClick(viewKeyMuteImageVideo) { bridge.doMuteVideo() setImageResource( if (bridge.isLocalMuteVideo) R.drawable.cam_off else R.drawable.cam_on @@ -161,7 +162,7 @@ open class VideoOnTheCallFragment : BaseP2pCallFragment() { } } getView(viewKeyImageSpeaker)?.run { - setOnClickListener { + bindClick(viewKeyImageSpeaker) { val speakerEnable = !bridge.isSpeakerOn() bridge.doConfigSpeaker(speakerEnable) setImageResource( @@ -170,64 +171,82 @@ open class VideoOnTheCallFragment : BaseP2pCallFragment() { } } getView(viewKeyImageHangup)?.run { - setOnClickListener { + bindClick(viewKeyImageHangup) { bridge.doHangup() } } getView(viewKeyImageSwitchCamera)?.run { - setOnClickListener { + bindClick(viewKeyImageSwitchCamera) { bridge.doSwitchCamera() } } - getView(viewKeyTextTimeCountdown)?.run { - bridge.configTimeTick( - CallUIOperationsMgr.TimeTickConfig({ - CoroutineScope(Dispatchers.Main).launch { - this@run.text = it.formatSecondTime() - } - }) - ) - } - getView(viewKeyVideoViewBig)?.run { - bridge.setupRemoteView(this) - } getView(viewKeyVideoViewSmall)?.run { - bridge.setupLocalView(this) if (uiConfig?.enableCanvasSwitch == true) { - setOnClickListener { + bindClick(viewKeyVideoViewSmall) { doSwitchCanvas() } } } getView(viewKeyImageSwitchTipClose)?.run { - setOnClickListener { + bindClick(viewKeyImageSwitchTipClose) { getView(viewKeySwitchTypeTipGroup)?.run { visibility = View.GONE } } } + getView(viewKeyImageFloatingWindow)?.run { + visibility = if (uiConfig?.enableFloatingWindow == true) View.VISIBLE else View.GONE + bindClick(viewKeyImageFloatingWindow) { + bridge.showFloatingWindow() + } + } + getView(viewKeyImageVirtualBlur)?.run { + visibility = if (uiConfig?.enableVirtualBlur == true) View.VISIBLE else View.GONE + bindClick(viewKeyImageVirtualBlur) { + bridge.doVirtualBlur() + setImageResource( + if (bridge.isVirtualBlur) R.drawable.icon_call_virtual_blur_on else R.drawable.icon_call_virtual_blur_off + ) + } + } + } + + override fun permissionList(): List { + return listOf(RECORD_AUDIO, CAMERA) + } + + override fun actionForPermissionGranted() { + if (!permissionAllowed) { + bridge.callEngine.enableLocalVideo(true) + } + } + + override fun onPermissionRequest() { + permissionAllowed = arePermissionsGranted() + super.onPermissionRequest() } override fun onCreateAction() { + NERtcCallbackProxyMgr.getInstance().addCallback(rtcCallback) if (bridge.currentCallState() == CallState.STATE_IDLE) { bridge.doCall() } } + override fun onDestroyAction() { + super.onDestroyAction() + NERtcCallbackProxyMgr.getInstance().removeCallback(rtcCallback) + } + override fun onCallEnd(info: NECallEndInfo) { bridge.configTimeTick(null) } override fun toUpdateUIState(type: Int) { - bridge.doConfigSpeaker(true) - bridge.doMuteAudio(false) - bridge.doMuteVideo(false) - } - - override fun onCallTypeChange(info: NECallTypeChangeInfo) { - if (info.state == SwitchCallState.REJECT) { - getView(viewKeySwitchTypeTipGroup)?.run { - visibility = View.GONE + when (type) { + INIT, CHANGE_CALL_TYPE -> toInitState() + FROM_FLOATING_WINDOW -> { + toFromFloatingWindowState() } } } @@ -237,6 +256,11 @@ open class VideoOnTheCallFragment : BaseP2pCallFragment() { updateCloseVideoTipUI(userId, mute) } + override fun onVideoAvailable(userId: String?, available: Boolean) { + userId ?: return + updateCloseVideoTipUI(userId, !available) + } + protected open fun doSwitchCanvas() { val videoViewSmall = getView(viewKeyVideoViewSmall) val videoViewBig = getView(viewKeyVideoViewBig) @@ -244,14 +268,23 @@ open class VideoOnTheCallFragment : BaseP2pCallFragment() { videoViewBig?.clearImage() videoViewSmall?.clearImage() } - if (localIsSmallVideo) { - bridge.setupRemoteView(videoViewSmall) - bridge.setupLocalView(videoViewBig) + if (bridge.isLocalSmallVideo) { + bridge.setupRemoteView(videoViewSmall, action = { + it?.run { + setScalingType(IVideoRender.ScalingType.SCALE_ASPECT_BALANCED) + } + }) + bridge.setupLocalView(videoViewBig, action = { + it?.run { + setZOrderMediaOverlay(true) + setScalingType(IVideoRender.ScalingType.SCALE_ASPECT_BALANCED) + } + }) } else { bridge.setupRemoteView(videoViewBig) bridge.setupLocalView(videoViewSmall) } - localIsSmallVideo = !localIsSmallVideo + CallUIOperationsMgr.updateUIState(isLocalSmallVideo = !bridge.isLocalSmallVideo) updateCloseVideoTipUI(bridge.callParam.currentAccId, muteVideo = bridge.isLocalMuteVideo) updateCloseVideoTipUI(bridge.callParam.otherAccId, muteVideo = bridge.isRemoteMuteVideo) @@ -262,7 +295,7 @@ open class VideoOnTheCallFragment : BaseP2pCallFragment() { val ivBigVideoShade = getView(viewKeyImageVideoShadeBig) val tvRemoteVideoCloseTip = getView(viewKeyTextRemoteVideoCloseTip) - if (localIsSmallVideo) { + if (bridge.isLocalSmallVideo) { if (TextUtils.equals(userAccId, bridge.callParam.currentAccId)) { muteVideo?.run { loadImg(bridge.uiConfig?.closeVideoLocalUrl, ivSmallVideoShade) @@ -324,4 +357,113 @@ open class VideoOnTheCallFragment : BaseP2pCallFragment() { .centerCrop() .into(imageView) } + + protected open fun toInitState() { + getView(viewKeySwitchTypeTipGroup)?.run { + visibility = View.GONE + } + getView(viewKeyImageVideoShadeSmall)?.run { + setBackgroundColor(Color.BLACK) + } + getView(viewKeyTextTimeCountdown)?.run { + bridge.configTimeTick( + CallUIOperationsMgr.TimeTickConfig({ + CoroutineScope(Dispatchers.Main).launch { + this@run.text = it.formatSecondTime() + } + }) + ) + } + bridge.doVirtualBlur(false) + getView(viewKeyImageVirtualBlur)?.run { + setImageResource( + if (bridge.isVirtualBlur) R.drawable.icon_call_virtual_blur_on else R.drawable.icon_call_virtual_blur_off + ) + } + bridge.doConfigSpeaker(true) + getView(viewKeyImageSpeaker)?.run { + setImageResource( + if (bridge.isSpeakerOn()) R.drawable.speaker_on else R.drawable.speaker_off + ) + } + bridge.doMuteAudio(false) + getView(viewKeyMuteImageAudio)?.run { + setImageResource( + if (bridge.isLocalMuteAudio) R.drawable.voice_off else R.drawable.voice_on + ) + } + bridge.doMuteVideo(false) + getView(viewKeyMuteImageVideo)?.run { + setImageResource( + if (bridge.isLocalMuteVideo) R.drawable.cam_off else R.drawable.cam_on + ) + updateCloseVideoTipUI(bridge.callParam.currentAccId, bridge.isLocalMuteVideo) + } + updateCloseVideoTipUI(bridge.callParam.otherAccId, muteVideo = false) + + getView(viewKeyVideoViewBig)?.run { + bridge.setupRemoteView(this) + } + getView(viewKeyVideoViewSmall)?.run { + bridge.setupLocalView(this) + } + } + + protected open fun toFromFloatingWindowState() { + getView(viewKeyTextTimeCountdown)?.run { + bridge.configTimeTick( + CallUIOperationsMgr.TimeTickConfig({ + CoroutineScope(Dispatchers.Main).launch { + this@run.text = it.formatSecondTime() + } + }) + ) + } + getView(viewKeyImageVirtualBlur)?.run { + setImageResource( + if (bridge.isVirtualBlur) R.drawable.icon_call_virtual_blur_on else R.drawable.icon_call_virtual_blur_off + ) + } + getView(viewKeyImageSpeaker)?.run { + setImageResource( + if (bridge.isSpeakerOn()) R.drawable.speaker_on else R.drawable.speaker_off + ) + } + getView(viewKeyMuteImageAudio)?.run { + setImageResource( + if (bridge.isLocalMuteAudio) R.drawable.voice_off else R.drawable.voice_on + ) + } + getView(viewKeyMuteImageVideo)?.run { + setImageResource( + if (bridge.isLocalMuteVideo) R.drawable.cam_off else R.drawable.cam_on + ) + } + if (bridge.isLocalSmallVideo) { + getView(viewKeyVideoViewBig)?.run { + bridge.setupRemoteView(this) + } + getView(viewKeyVideoViewSmall)?.run { + bridge.setupLocalView(this) + } + } else { + getView(viewKeyVideoViewBig)?.run { + bridge.setupLocalView(this, action = { + it?.run { + setScalingType(IVideoRender.ScalingType.SCALE_ASPECT_BALANCED) + } + }) + } + getView(viewKeyVideoViewSmall)?.run { + bridge.setupRemoteView(this, action = { + it?.run { + setZOrderMediaOverlay(true) + setScalingType(IVideoRender.ScalingType.SCALE_ASPECT_BALANCED) + } + }) + } + } + updateCloseVideoTipUI(bridge.callParam.currentAccId, muteVideo = bridge.isLocalMuteVideo) + updateCloseVideoTipUI(bridge.callParam.otherAccId, muteVideo = bridge.isRemoteMuteVideo) + } } diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/service/CallKitUIBridgeService.kt b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/service/CallKitUIBridgeService.kt index 2adda7c..3042b24 100644 --- a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/service/CallKitUIBridgeService.kt +++ b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/service/CallKitUIBridgeService.kt @@ -99,7 +99,7 @@ open class CallKitUIBridgeService @JvmOverloads constructor( /** * 点对点本地行为监听 */ - private val localActionObserver = NECallLocalActionObserver { actionId, resultCode -> + private val localActionObserver = NECallLocalActionObserver { actionId, resultCode, _ -> this@CallKitUIBridgeService.onLocalAction(actionId, resultCode) } @@ -214,6 +214,7 @@ open class CallKitUIBridgeService @JvmOverloads constructor( AVChatSoundPlayer.play(context, AVChatSoundPlayer.RingerTypeEnum.CONNECTING) } else if (canStopAudioPlay && callerAccId != null && actionId != CallLocalAction.ACTION_BEFORE_RESET && + actionId != CallLocalAction.ACTION_SWITCH && (resultCode == CallErrorCode.SUCCESS || resultCode == ResponseCode.RES_SUCCESS.toInt()) ) { AVChatSoundPlayer.stop(context) @@ -229,7 +230,7 @@ open class CallKitUIBridgeService @JvmOverloads constructor( * 点对点通话行为监听 */ open fun onReceiveInvited(info: NEInviteInfo) { - ALog.dApi(logTag, ParameterMap("onInvited").append("info", info)) + ALog.dApi(logTag, ParameterMap("onReceiveInvited").append("info", info)) // 检查参数合理性 if (!isValidParam(info)) { ALog.d(logTag, "onIncomingCall, param is invalid.") diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/utils/AppForegroundWatcherHelper.kt b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/utils/AppForegroundWatcherHelper.kt index f44989e..3298a6e 100644 --- a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/utils/AppForegroundWatcherHelper.kt +++ b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/utils/AppForegroundWatcherHelper.kt @@ -10,8 +10,11 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.OnLifecycleEvent import androidx.lifecycle.ProcessLifecycleOwner +import com.netease.yunxin.kit.alog.ALog +import com.netease.yunxin.kit.alog.ParameterMap object AppForegroundWatcherHelper { + private const val TAG = "AppForegroundWatcherHelper" private val watchers = mutableListOf() private var background = true @@ -19,6 +22,7 @@ object AppForegroundWatcherHelper { private val callServiceObserver = object : LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) fun onForeground() { + ALog.dApi(TAG, ParameterMap("onForeground")) background = false watchers.forEach { it.onForeground() @@ -27,6 +31,7 @@ object AppForegroundWatcherHelper { @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) fun onBackground() { + ALog.dApi(TAG, ParameterMap("onBackground")) background = true watchers.forEach { it.onBackground() @@ -41,6 +46,7 @@ object AppForegroundWatcherHelper { fun isBackground() = background fun addWatcher(watcher: Watcher): Boolean { + ALog.dApi(TAG, ParameterMap("addWatcher").append("watcher", watcher)) if (watchers.contains(watcher)) { return false } @@ -48,6 +54,7 @@ object AppForegroundWatcherHelper { } fun removeWatcher(watcher: Watcher): Boolean { + ALog.dApi(TAG, ParameterMap("removeWatcher").append("watcher", watcher)) return watchers.remove(watcher) } diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/utils/FormatSecondTime.kt b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/utils/FormatSecondTime.kt index 4b0ad2c..02f9e30 100644 --- a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/utils/FormatSecondTime.kt +++ b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/utils/FormatSecondTime.kt @@ -6,7 +6,7 @@ package com.netease.yunxin.nertc.ui.utils -import java.util.* +import java.util.Locale fun Long.formatSecondTime(): String { if (this <= 0) { diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/utils/PermissionExtends.kt b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/utils/PermissionExtends.kt index ea2620c..97f10e0 100644 --- a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/utils/PermissionExtends.kt +++ b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/utils/PermissionExtends.kt @@ -6,22 +6,32 @@ package com.netease.yunxin.nertc.ui.utils +import android.app.Activity import android.content.Context +import android.content.Intent import android.content.pm.PackageManager import android.os.Build +import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import com.netease.yunxin.kit.alog.ALog +import com.netease.yunxin.nertc.ui.base.KEY_PERMISSION_RESULT_DENIED +import com.netease.yunxin.nertc.ui.base.KEY_PERMISSION_RESULT_DENIED_FOREVER +import com.netease.yunxin.nertc.ui.base.KEY_PERMISSION_RESULT_GRANTED +import com.netease.yunxin.nertc.ui.base.launchTask import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch private const val TAG = "PermissionExtends" +private const val REQUEST_CODE_PERMISSION = 21302 + fun Context.isGranted(vararg permissions: String?): Boolean { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || permissions.isEmpty()) { return true } return !permissions @@ -32,6 +42,48 @@ fun Context.isGranted(vararg permissions: String?): Boolean { } } +fun Context.requestPermission( + onGranted: ((List) -> Unit)? = null, + onDenied: ((List, List) -> Unit)? = null, + vararg permissions: String? +) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + onGranted?.invoke(permissions.filterNotNull()) + return + } + launchTask( + this, + REQUEST_CODE_PERMISSION, + { activity: Activity?, _: Int? -> + ActivityCompat.requestPermissions( + activity!!, + permissions, + REQUEST_CODE_PERMISSION + ) + } + ) { intentResultInfo -> + val intent: Intent? = intentResultInfo.value + if (intent == null) { + ALog.e(TAG, "requestPermission, intent is null") + return@launchTask + } + val grantedList = + intent.getStringArrayListExtra(KEY_PERMISSION_RESULT_GRANTED) + if (!grantedList.isNullOrEmpty()) { + onGranted?.invoke(grantedList) + } + val deniedList: List? = + intent.getStringArrayListExtra(KEY_PERMISSION_RESULT_DENIED) + val deniedForeverList: List? = + intent.getStringArrayListExtra(KEY_PERMISSION_RESULT_DENIED_FOREVER) + if (!deniedList.isNullOrEmpty() || + !deniedForeverList.isNullOrEmpty() + ) { + onDenied?.invoke(deniedList ?: emptyList(), deniedForeverList ?: emptyList()) + } + } +} + fun AppCompatActivity.requestPermission( onGranted: ((List) -> Unit)? = null, onDenied: ((List, List) -> Unit)? = null, @@ -107,3 +159,86 @@ fun Fragment.requestPermission( launcher.launch(permissionList.toTypedArray()) } } + +fun Fragment.registerPermissionRequesterEx(): PermissionRequester { + val permissionRequester = PermissionRequester() + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return permissionRequester + } + permissionRequester.launcher = registerForActivityResult( + ActivityResultContracts.RequestMultiplePermissions() + ) { permissionMap -> + ALog.d(TAG, "$permissionMap") + permissionRequester.handlePermissionResult(permissionMap) + } + permissionRequester.shouldShowRequestPermissionRationale = { + shouldShowRequestPermissionRationale(it) + } + return permissionRequester +} + +fun AppCompatActivity.registerPermissionRequesterEx(): PermissionRequester { + val permissionRequester = PermissionRequester() + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return permissionRequester + } + permissionRequester.launcher = registerForActivityResult( + ActivityResultContracts.RequestMultiplePermissions() + ) { permissionMap -> + ALog.d(TAG, "$permissionMap") + permissionRequester.handlePermissionResult(permissionMap) + } + permissionRequester.shouldShowRequestPermissionRationale = { + shouldShowRequestPermissionRationale(it) + } + return permissionRequester +} + +class PermissionRequester { + internal var launcher: ActivityResultLauncher>? = null + internal var shouldShowRequestPermissionRationale: (String) -> Boolean = { true } + private var onGranted: ((List) -> Unit)? = null + private var onDenied: ((List, List) -> Unit)? = null + private var permissionList: List = emptyList() + + internal fun handlePermissionResult(permissionMap: Map) { + ALog.d(TAG, "$permissionMap") + val grantedList = mutableListOf() + val deniedList = mutableListOf() + val deniedForeverList = mutableListOf() + permissionList.forEach { + if (permissionMap[it] == true) { + grantedList.add(it) + } else if (!shouldShowRequestPermissionRationale(it)) { + deniedForeverList.add(it) + } else { + deniedList.add(it) + } + } + if (grantedList.isNotEmpty()) { + onGranted?.invoke(grantedList) + } + if (deniedForeverList.isNotEmpty() || deniedList.isNotEmpty()) { + onDenied?.invoke(deniedForeverList, deniedList) + } + } + + fun request( + permissionList: List, + onGranted: ((List) -> Unit)? = null, + onDenied: ((List, List) -> Unit)? = null + ) { + CoroutineScope(Dispatchers.Main).launch { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || permissionList.isEmpty()) { + onGranted?.invoke(permissionList) + return@launch + } + this@PermissionRequester.onGranted = onGranted + this@PermissionRequester.onDenied = onDenied + this@PermissionRequester.permissionList = permissionList + launcher?.launch(permissionList.toTypedArray()) + } + } +} diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/utils/PermissionTipDialog.kt b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/utils/PermissionTipDialog.kt index 6f5e7f7..d80cddb 100644 --- a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/utils/PermissionTipDialog.kt +++ b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/utils/PermissionTipDialog.kt @@ -19,7 +19,7 @@ import com.netease.yunxin.nertc.ui.R /** * 底部弹窗基类,子类需要实现 顶部view,以及底部view 的渲染即可 */ -class PermissionTipDialog(activity: Activity, private val clickListener: View.OnClickListener) : +open class PermissionTipDialog(activity: Activity, private val clickListener: View.OnClickListener) : Dialog( activity, R.style.BottomDialogTheme diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/utils/SwitchCallTypeConfirmDialog.kt b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/utils/SwitchCallTypeConfirmDialog.kt index 3f1ed14..b2a52b0 100644 --- a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/utils/SwitchCallTypeConfirmDialog.kt +++ b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/utils/SwitchCallTypeConfirmDialog.kt @@ -13,15 +13,14 @@ import android.view.Gravity import android.view.LayoutInflater import android.view.View import android.view.WindowManager -import android.widget.TextView -import androidx.annotation.LayoutRes import com.netease.yunxin.kit.call.p2p.model.NECallType import com.netease.yunxin.nertc.ui.R +import com.netease.yunxin.nertc.ui.databinding.ViewSwitchCallTypeTipDialogBinding /** * 底部弹窗基类,子类需要实现 顶部view,以及底部view 的渲染即可 */ -class SwitchCallTypeConfirmDialog( +open class SwitchCallTypeConfirmDialog( activity: Activity, val onPositive: (Int) -> Unit, val onNegative: (Int) -> Unit @@ -30,7 +29,9 @@ class SwitchCallTypeConfirmDialog( activity, R.style.BottomDialogTheme ) { - private var rootView: View + private val binding: ViewSwitchCallTypeTipDialogBinding by lazy { + ViewSwitchCallTypeTipDialogBinding.inflate(LayoutInflater.from(context)) + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -43,47 +44,43 @@ class SwitchCallTypeConfirmDialog( wlp.height = WindowManager.LayoutParams.WRAP_CONTENT window.attributes = wlp } - setContentView(rootView) + setContentView(binding.root) setCanceledOnTouchOutside(false) setCancelable(false) } - @LayoutRes - private fun contentLayoutId(): Int { - return R.layout.view_switch_call_type_tip_dialog - } - /** * 页面渲染 */ - private fun renderRootView(rootView: View?, type: Int) { + protected open fun renderRootView(rootView: View?, type: Int) { if (rootView == null) { return } - rootView.findViewById(R.id.tv_tip_accept)?.run { - setOnClickListener { - onPositive(type) - dismiss() - } + + binding.tvTipAccept.setOnClickListener { + onPositive(type) + dismiss() } - rootView.findViewById(R.id.tv_tip_reject)?.run { - setOnClickListener { - onNegative(type) - dismiss() - } + binding.tvTipReject.setOnClickListener { + onNegative(type) + dismiss() } } - fun show(type: Int) { + final override fun show() { + super.show() + } + + open fun show(type: Int) { if (isShowing) { return } - rootView.findViewById(R.id.tv_tip_content) - ?.setText( - if (type == NECallType.AUDIO) R.string.ui_dialog_switch_call_type_content_audio else R.string.ui_dialog_switch_call_type_content_video - ) - renderRootView(rootView, type) + + binding.tvTipContent.setText( + if (type == NECallType.AUDIO) R.string.ui_dialog_switch_call_type_content_audio else R.string.ui_dialog_switch_call_type_content_video + ) + renderRootView(binding.root, type) try { show() } catch (ignored: Throwable) { @@ -100,8 +97,4 @@ class SwitchCallTypeConfirmDialog( } catch (ignored: Throwable) { } } - - init { - rootView = LayoutInflater.from(context).inflate(contentLayoutId(), null) - } } diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/view/OverLayPermissionDialog.kt b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/view/OverLayPermissionDialog.kt new file mode 100644 index 0000000..beb08cd --- /dev/null +++ b/NLiteAVDemo-Android-Java/call-ui/src/main/java/com/netease/yunxin/nertc/ui/view/OverLayPermissionDialog.kt @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2022 NetEase, Inc. All rights reserved. + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ + +package com.netease.yunxin.nertc.ui.view + +import android.app.Activity +import android.app.Dialog +import android.os.Bundle +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.WindowManager +import androidx.annotation.LayoutRes +import com.netease.yunxin.nertc.ui.R +import com.netease.yunxin.nertc.ui.utils.dip2Px + +open class OverLayPermissionDialog(activity: Activity, private val clickListener: View.OnClickListener) : + Dialog( + activity, + R.style.BottomDialogTheme + ) { + private var rootView: View + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val window = window + if (window != null) { + window.decorView.setPadding(20.dip2Px(context), 0, 20.dip2Px(context), 0) + val wlp = window.attributes + wlp.gravity = Gravity.CENTER + wlp.width = WindowManager.LayoutParams.MATCH_PARENT + wlp.height = WindowManager.LayoutParams.WRAP_CONTENT + window.attributes = wlp + } + setContentView(rootView) + setCanceledOnTouchOutside(false) + setCancelable(true) + } + + @LayoutRes + private fun contentLayoutId(): Int { + return R.layout.view_overlay_permission_tip_dialog + } + + /** + * 页面渲染 + */ + private fun renderRootView(rootView: View?) { + if (rootView == null) { + return + } + val button = rootView.findViewById(R.id.tv_tip_ok) + button.setOnClickListener { + clickListener.onClick(it) + } + } + + override fun show() { + if (isShowing) { + return + } + renderRootView(rootView) + try { + super.show() + } catch (ignored: Throwable) { + ignored.printStackTrace() + } + } + + override fun dismiss() { + if (!isShowing) { + return + } + try { + super.dismiss() + } catch (ignored: Throwable) { + } + } + + init { + rootView = LayoutInflater.from(context).inflate(contentLayoutId(), null) + } +} diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/avchat_left_type_audio.png b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/avchat_left_type_audio.png deleted file mode 100644 index 6099b9c63b6a092661098460b1749604bd279628..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1399 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 diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/avchat_receive_bg_normal.9.png b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/avchat_receive_bg_normal.9.png deleted file mode 100644 index 7002b142b9162642705396cfd832e10e2ec06432..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1203 zcmeAS@N?(olHy`uVBq!ia0vp^3P7B~!3HFsZ#L=!Qj#UE5hcO-X(i=}MX3yqDfvmM z3ZA)%>8U}fi7AzZCsS=07?>3^Ln2Bde0{8v^KDjP)EVYz|dIVz*yhVSl7_V%D~df&{P2mlz_GsrKDK}xwt{?0`hE?GD=Dctn~HE z%ggo3jrH=2()A53EiLs8jP#9+bb%^#i!1X=5-W7`ij`p|xTF>*7iAWdWaj57fJ{tG z$}cUkRRX#c;)UD-xUqS~&|m@vn0`fKfxe-h0mw@*g}%P{mFDKcRTq~8r6Sym)!^cg z%7Rq=pw#00(xPNw#HA^NtSYc_E=o--$uA1Y&(DE{Vn9ZINq%ugeu09svw}u=W?o8u zd9fx~xv#I4XI^nhVqS8pr;Du;&;Y&6%oHm(3qunZOIKGn3t&hax|&$H7&|)~n>w1C znY+3fSUSV>y5uL9=BDPA!1Sgd^t$2H3rY#O1wfl!Qj0RnQd8WD@^clyKDNrl?G|U8 z=0WwQ;C71(PQCg-$LNFN6)B!!LcqiWV!~4`kONQmsd>P}UIa|lJ3N=9FfcG`c)B=- zRNQ)V+tKfkgT&E~eTI*kq$^m34zd=oJ!C3*?iRD_3x}Me-v#amx;=rHvrg8k~P|5to>6sXsXWoBPkxoA&JjsH$36OgMLG%YT@~5b&!( ziE+UPM(wAzNk5O3{$sM6I`fm*!aI3iZk4{f%lfVMe`nG9JsPD4F5Oz2d0lSzy7`6w hH{4j-@%Eok0z=CY-#<$PBOO47d%F6$taD0e0swCrm$?7{ diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/avchat_receive_bg_pressed.9.png b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/avchat_receive_bg_pressed.9.png deleted file mode 100644 index 31c3b0005e115a46cc1a181e5e8045359b05d163..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1191 zcmeAS@N?(olHy`uVBq!ia0vp^3P7B~!3HFsZ#L=!Qj#UE5hcO-X(i=}MX3yqDfvmM z3ZA)%>8U}fi7AzZCsS=07?>3^Ln2Bde0{8v^KDjP)EVYz|dIVz*yhVSl7_V%D~df&{P2mlz_GsrKDK}xwt{?0`hE?GD=Dctn~HE z%ggo3jrH=2()A53EiLs8jP#9+bb%^#i!1X=5-W7`ij`p|xTF>*7iAWdWaj57fJ{tG z$}cUkRRX#c;)UD-xUqS~&|m@vn0`fKfxe-h0mw@*g}%P{mFDKcRTq~8r6Sym)!^cg z%7Rq=pw#00(xPNw#HA^NtSYc_E=o--$uA1Y&(DE{Vn9ZINq%ugeu09svw}u=W?o8u zd9fx~xv#I4XI^nhVqS8pr;Du;&;Y&6%oHmFLknXQLpK9A3lmF2Lst_=LkmY|Cl?n> zBXd_v6GtbQUYGpj(%jU%5}4i;gkD3OdO;~6w*Y9fOKMSOS!#+~QGTuh*vD3xINjpv ziqkx(-V~f}adpF~S0CsYeNen2#WPF@n0P=;c&Y_*;ORa!5181CfT?<-R(3Q41EY+m zi(^Q|tv9!Q`3?q%v^@+~ohadXluPT7m(EF{H^(MSHMpSqL()@4V|kUfx@^#QzCG$c zlbrTH51PEq{FR>ErH{R1{@3?ubwx&?{pUR z{#(xv&agNLhX{vK_CNLe8}r}&Ef!)zQPrTvSlRGyHR~H@20yV0tPCk)8?MUOOp}j& zE?qGF)5L1|jz!5&t&YCa*O>P`-s|U*zaFP}`QEm9<-aq(-+A-F2ey`70iiK+Ra^!P X{E-G3)4sFRfz0=G^>bP0l+XkKCa;9m diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/avchat_refuse_bg_normal.9.png b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/avchat_refuse_bg_normal.9.png deleted file mode 100644 index 4f0d7d3b4eb33425031940427e03585756bf2881..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 319 zcmeAS@N?(olHy`uVBq!ia0vp^3P7B~!3HFsZ#L>PQXWX5~L*nesqcG0KOW-K+E zJ+bGAW80%O%B5EvCO(~4m4EqSWkc4<0{g4>UhF!x)320lWyq54|Ju8{`IA-nZ~u^n zB-Nz$r>Q>Ag%j@YESeK6JfWK5&TNLK%0O4)0V^0^y=SapMpf}Zox$!{)n~N_j~ELo zd&}A%y{Op|U0&9H+qk{qOv$!mzq`Md=_wcfuG;xdV_MQR>-c~;-7?|dR+IE)3Ovr- h`*yOLU1rL17XFj(1=n?oAN&t8-P6_2Wt~$(69DVJcZ&c3 diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/avchat_refuse_bg_pressed.9.png b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/avchat_refuse_bg_pressed.9.png deleted file mode 100644 index a7db35d4b23e9dd29ca17dcddddc5177d26f91c8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 318 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 Ij!ez}2c|rT8~^|S diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/avchat_switch_mode_video_icon.png b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/avchat_switch_mode_video_icon.png deleted file mode 100644 index 5c60e18634b9922501860e0c3c3828f61b70194f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1213 zcmeAS@N?(olHy`uVBq!ia0vp^(m*W4!3HE(-CjBuNJ*BsMwA5Srr5%(GQ`zk9!uLS~AsQn;zFfp39xYDT62(s|s5su(?)1Hb_`sNdc^+B->UA;;0DU00rm#qErP_J!9Qu14BavGc!Fy z6H_xYLmdSp14AQy10XWfH8im@HM24@SAYT~plwAdX;wilZcw{`JX@uVl9B=|ef{$C za=mh6z5JqdeM3u2OOP2xM!G;1y2X`wC5aWfdBw^w6I@b@lZ!G7N;32F6hI~>Cgqow z*eU^C3h_d20o>TUVrVb{15Cdnu|VHY&j92lm_lD){7Q3k;i`*Ef>IIg#cFVINM%8) zeo$(0erZuMFyhjbK~@!5ITxiSmgEy45rs5Ke;qFHLnDwHwB^B6sKNLV#qB3+U$~Alv$RV;#QQOs{r=2RVEg^ zJ2^R7;xrGcHwBAZpn4r~>eUB2MjsTlNKp+F0;V4j6P|E^9C*@C%>$zm)7C6tGWTtQ~`O3ws$!tbk3nRK*cF$ujlo$BU z{4w6dGSJ%k_U*4Ea@sQaDXSO5QCZ_wq7XIA|A`}=#&<@ehVs4JYD@<);T3K0RXD~xv&5L diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/nim_actionbar_dark_back_icon.png b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/nim_actionbar_dark_back_icon.png deleted file mode 100644 index 41e226defa25946478e8db20daf4416acb34d493..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1235 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 diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_camera.png b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_camera.png deleted file mode 100644 index b8ce2aa049f4ca5c63bc2488b8976609f2497561..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 970 zcmV;*12z1KP)Px&gh@m}R9FesnLB6{Q51%E#Rt(82Eha^f*Q1yAXo&fQyJTUU}fuCr?L&Px3HCz z!6s!02&oKWYw$q?6~Pb{@kL<$ez}|M-nq}&nO$<=XJ+pC&pH22X6`+6(`Xz*YgVPX z-ENPP>oPcJaE9X!_-L@zY&L&zK3qfSIJgT|!EX>U`Nb3e?7)>%bPOzkm{2HAQ=H<* z1|pt9EwBr!Fy*;dn{bkkfaNNbI&vuQNTuRQI02qJid8wJ{1cT3Cz0-OuHe_iJOn!C znCkR@$4OG=k(kmQBX%lBvEeSq?)H%ofg~;b^C3B5NSyEhq{y8?ay}oqxOKg zp5s_X@E&m+;1y8cq5k6vm;t_8xSXc?H^F14*B<0ft4GMOZ;%&&b6M87@r##ZtF|`h zH5o@iPEWB3|GYDqH7FhsKh>l|-DB7wO{ZprCjlqJ8GF6rn>Y?hHr<<{(QwVv>W5S8 zJES>Yl%FZ&4#)8I&L}>`3VQkxy@-GhDOGKbiCeVCJ=aO1^~T3cv&5qqDdJbW^2Ds5 z$91PQRmE(ByOppCS*;U>C7 zZ;@R~XvhlBS%@6)#qvmp^z>LkG|!qo^VfP<#S1OXRah*~n$go!t03BV)mDaVhGoII zWQT&feI{N#NyuV44q{2D{fyXBzHktce@1@wDj`$-y!{aZV*Rw5pQN6{{z-}hvF6F! zPuuzSuSu%TEVjQ#XKYr1 zl4Q!$>ds%=IfXPGY95a*Kv0~fIK`Kn*82Hn6z%*c;05PB!y5 sl&%Lx+$6^f=Df_|9YNb=0uN=(KVw!k8lsNCtpET307*qoM6N<$f@$p8VE_OC diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_camera_mute.png b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_camera_mute.png deleted file mode 100644 index 55484f253b32e1eb79306a8716433ee619f2acec..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1124 zcmV-q1e^PbP)Px(9!W$&R9Fe!nLTS2K@^4)iGiR5L^M$g5kbL962U?wtpq=sl!8?(RWKrUHmUL# zEJeFiv6TD(MQn|rC}JZ~5Cs(_KF?+5viD|p?(E&0NIdW|yK~<2o^vPradt8^v;%E; zP&IFCZ0tqr1UMY9pK%?03|Jc;9{$GhzcmV-0Q2Ap_yJO;U!MGf+g>?I_kkr43tbE6 zml>zQAut+7&z+YqPczQ*ecMtziEe@~;5T>xPSr^)Kg1cY&W$?pwYZ3nfk!}F0kw+l zPG)@2VQA@&b#oe>BRUD5fj{6*BeAYv^@6VYlU?rqP7!j275oZjb5!6R2e%zF`Ll4< zH&62wN5o6Q70xQyBH_LU+UuO6LyU!+=TUA z*U=so-}(w2Lu5 zj=SPPVYG?YY~E9L7v~``M|1xvMU1|{n+tKqr!3}=^;C2DW#0ALXcY_J2=L~5T*XPv z8yDg$pDOt6$F+OiT4gKkCz9spQk9*<;Tx;pKEXKS)v2#&{<+qBZ*Gyf^9D;>Yu@NenU1O2{w zkDgr~!uArrntQ*o3hO98#4Q%4I}Y*3F0bDH2!Y9d8vmBO5z+0hpY~@H-1Ex?&cJ&Q zH2eQ@35CaR#L8`hP`JHB<1P=4J3wo;frCKLuW>kDDvkPS;%9+B+_z;jLnM3O1Flb% z8BpuX1-=CjZr{V7j9_txX`$INbVj4S09;`=Y6i2z6jLC$v-a|0;f@sV&FOxK+Y3HV z>wf(DNHflOb^63;1PK79>B9PFbqo`WQjpA@279?P^nPLc%@gv!xk|;5c z&zA88T&7qu%avlPEm}l}&vMm&awA{Kbr?dcCo)SB1NuIqjxu0c%JasB=-<@BVRIrm zf+)!=^Pe?!rbn~XBz?FHAo?i zOfvOdTMNe|DZQ(q-Q-hARF*j+P}(Xh?^(Kjo>pp3*!)A>)(Bd=e1T7(?{+aQ=sfIY zK3w-2JvCP6xA`u3D|W%}PO@Ef6&1*3OGo4SpPn|cS3hiRTb$7?m@r;04DS{!9JqPY z&E8L)+Un`C(^=5k7Vu%HTZNN70y^Pz3^;1@D!^Q({uTVo$T(H%-3VNBdcqwWJQPlv zPAn$e8FXLapS&M8Uu5;(QXQpTc3ww7TW&DHgLr0s!u z^FL7R^Wb@Q_aNZkp>k}7a_gPi$Ci2wW*tgvW99K+KPjHYUYjLHeoYab*ebkQ=r0P`%s1;j)_h(>Is)UD>#FJ{!%@Y`N&3 z<+?^P#q6kb`Da95dH(r{Qrn_4O`i{v64BRtL+GBjzda%n6)&qknc&}QbG`Hm%CZ+`(3e&v~a&OOt+Lj7B%#iE(-W;FlgbZp>vhFhbJ8W z_odRh-j&Tf^W(4g?zQ5Oz3FyE8O+!0Isx9P|Fvo)ntP+LprMvejp2A`Oz*zsyre_X z*PDsITR=qjYc8;th4eTnzAP*f)ANto5ZamLfo<_;^Qv0*Tc*4;jVYy)s<*w#lVhS9 zYnXLU?b@F%KOnCvm3594*B&h_N~&v5{o=HF*=JVPIdS>zOI_D5R?*LA);|62i=tzc zy=Amf?~QP3c9vo?=uEq;2d*5=@LqAU_pOBNZfvDr_l?T@cFqq=>`O;?bbH3M9`1#k z6X1#?EzBcL4Y+;vwnEGM-N#Nv4Q+KG^lcH$Sj`rTL?cYjoKa&6mK@{J+ws%%a9O5=R$Lv;0%mRR?D$aQ6U zz#R|lo93pk!!qsSR(@T6*C5&{+Zj@QSIoQDr|j(7Ffen4`{s_P?bWCA9@d8+{5;&t zUYFgczc&L6Jn&XLnDgzy;G9}fT)>I?0N1)1=8vCN-!jLcySt+1O%4w;?a+|vjnqNs0E5?Pfp55rFK{WQ!8*!vU+DO)Dr9le J^ zUSPifgTYt_25>^rm0*0a7U-8@f43A}K0y2iP6TzIvZUn8Gb5Agup5x3@6M~Q6Pm8SS^K>fS;eSYBXRa zPb8KpjK)>dEFO;$s8Ay!g%}FtuyLpkQ6`fxs1&LXmCp5`P)NRHvOmp-;_1ir^`KGw zX>@mzuip%p1B;X7kOGqqN8mz?Mb`hq$zyz^j) zOPQ7EX&h;MUL;^C(Yc(qrs{ZIQv*_F>Gy^%t(E!L*!vks)=QJ|$9C}+T@AFGNd;v! z2Q~KUm!!gsL)p?t7CL;i&Ew%6>?#-QNv7S1$-@|I8#~$}o|uehk;azl6aR?K>nrSj zF|VsGe~f-@G}xsgD+`RSyd3hP(yqp~`X0a3@k>(++ZE?@v=1v{hWLqWn15i+dTN%~+M@2SATB+QY-Xe}l8z{k_D>=ZMsv9Z^8#&)Da=p5eU(7a?I;GzH zRLMM17%PHgWYOv!*n`1%zE@|-NO+dsiDcs2TRu1> zfPH;fJg?1$NdJJ4%xhp+q_)z#bz(_#P{6~iD-S9hZN^^I3fQhrWs76?L2ZTSLYN-S z0Rc2~eJ8%Drm43k2z*ka3 z?Sq?yD;6<0`FEQZ?qFtxY29^*(~lxwof^XwRNq_XxY#w$mJ6S;Pbavic0}Ry3V??X$ijfm=Oj zm;MXW(J&rGsZ6rPg$<@@d;9yUnlA8{oz&L!b|2X`vCWLARZg1iO#ZAZzBf!)o8Q~s dbMh~~&IHr+O#c!|T|ND)3iRK=srHTD{SWjg%a8y7 diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_hangup.png b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_hangup.png deleted file mode 100644 index ce1d5d7f38a25d0648f8f5b4d4d60a1a3b3beb03..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 782 zcmV+p1M&QcP)Px%$Vo&&R9FesnN3I)Q547D*vhiBLZJkAwn>S;BqahDt=hOrgaQc?-6*uLXw{-! zPiUbFbFB+;8AVX^MHC9@!fhl3eJmnHG+)0HGw`^*d2{b{&%3V!e`fBzXU_S}ojZ5t z&J+s&p+#xTrBbPrw)3bReM2kgeH2AM*w&W;+luDUE;4PKisxt?wbTVUARXuQG`~J5Bo}x%j)1H>Gwfv#CS+~ zzXp6m9JE^@_%Jb-0Y9q{yHFr-GkS_NY?Gi@w7~Ze{1%eASi|lSG`X^V&w;=v&}$89 zVlJRYzr=Ag0?TZ29eqK*?Ii&ZsensRf*(YaXa`lYUsT>l?WhS|L!VKWeZb*}8h8xf zMUp25$y>_3W@74&qBn+brQ(|Sn#X%DljIVbLW8K<8)75v^C-*Yfp*wCXx@Op$bC`F zqNe>7+M73z%5ebQwg6wmM`t?BdsYD5umCUSjjwi`CHROMK;FQB-^u9lE!zK@k@DvA zb{tR-GQihq*PX*%&jz3dBsX3)drV{MllXFGzw=JuSH>(5~z_*m3a<2F-7fCGt-@0E_FNMQ||a z81^RH@A1CP=97(+x({P{%yK--gXow#kWB(WWKkd=ezI)NCSJ{apgoDIeXW(BS9SSr zown$epI03<>p_=9fXVmq^0|A3@_AihA#qF@5`qxxFd{%eo?2Bu#_r9|qc#Q*>R M07*qoM6N<$f_cGhF#rGn diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_hangup_pressed.png b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_hangup_pressed.png deleted file mode 100644 index 9904c24ab75d5f3635bf624104c1bb60decf1a68..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 987 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�bwri>0P*l4ZuqEzoTsLFOC2N1_PCy)SCd>ULq=Do8gWpWcf^A)EC2ui diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_microphone.png b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_microphone.png deleted file mode 100644 index 1f05681270833346ddbcab4f1899797f7e61e17b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 838 zcmV-M1G)T(P)Px&07*naR9Fe^m_KV&K@`PzLy)KhQlv4GA|fKFRSG2_Y#M`p1MwTA%xYyJwrTSr zw6|0U5*3@IFpWa6(MZ5QAd4cJWc{7&%)0J-v+vEF7g5;@XXnk_d(S<4cNyMHtyYn> z+wDoP1XjTg`0hr{wLV!_RC+lsKF9l(F>iZ~*4s^ha29+4 zn_#BvNO*wfl;accYywp{72X|1>}Ooe~{8PK^HF8z6WB@jU$3@A~I5qeXP;i6x^8(NQ{n0@_mUuEs4t QQ~&?~07*qoM6N<$g0Pj4)c^nh diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_microphone_mute.png b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_microphone_mute.png deleted file mode 100644 index f69ac45ee86f95c2a9018d0020c99937734be43a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 878 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 diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/nim_actionbar_dark_back_icon.png b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/nim_actionbar_dark_back_icon.png deleted file mode 100644 index c244ad41551ba5a891878e0d0e526d31a9fe86dd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1220 zcmeAS@N?(olHy`uVBq!ia0vp^N7Qj#UE5hcO-X(i=}MX3yqDfvmM z3ZA)%>8U}fi7AzZCsS=07?>3^Ln2Bde0{8v^KDjP)EVYz|dIVz*yhVSl7_V%D~df&{P2mlz_GsrKDK}xwt{?0`hE?GD=Dctn~HE z%ggo3jrH=2()A53EiLs8jP#9+bb%^#i!1X=5-W7`ij`p|xTF>*7iAWdWaj57fJ{tG z$}cUkRRX#c;)UD-xUqS~&|m@vn0`fKfxe-h0mw@*g}%P{mFDKcRTq~8r6Sym)!^cg z%7Rq=pw#00(xPNw#HA^NtSYc_E=o--$uA1Y&(DE{Vn9ZINq%ugeu09svw}u=W?o8u zd9fx~xv#I4XI^nhVqS8pr;Du;&;Y&6%oHnEXJcmr6IU~5Ggn7LLst_EBTF|&BLf2` z7ZYa-XLC20UYGpj(%jU%5}4i;gkE!;dO;~6w*Y9fOKMSOS!#+~QGTuh*vD3xxZPrc z(>$o&6x?nx#i>^x=oo!ayduRjObD2GKumb51#;l&J~a=R*o%Ov`li|AP6h@>OHUWa zkcwMJ!VU8dIS80cpR(jg=qE;%Sx*!LBI=yDIQ$np;_M54!YRgT!y=_(Hz6dT&FI7# zukI2h%iALJVl%hwwu+WIJLh`&+)X;lWlJ(|x!z**KOp#G_X)LUUQS72B1fki2RxR{ zT=kai#FBy(p>mJ;4NrtWaGCm*D0P@WaD81qAtGK(Ca~B2uC&noP3C_}mnm%$m)kPW zsaM`gaD}0ZROW7`&Zl3E`%H|Da{KRz3vU$tX>9z$#mTtYYe`nx22sv)JW>;P%{&;g zq$tgx^H0^++R5U#l#CepqMmP9mF~n6_GaR-TgMKxPgQbCYus|dtWm3zb7Hh^*3=Wa zVq5>M&&(2hZKEXi*0wuISSjn`kV!Z diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_camera.png b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_camera.png deleted file mode 100644 index 540461cfd2b4c142e42dff822f25fce4071bf337..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1259 zcmVPx(rAb6VRA>e5nmcP0Q5c3d3Tgz4lxjf1;H66;q&0$t3K6Zh5R^3OtONN4DO7Cy z1D2XX6OuyP;15tt6a_C}BPd=$*XLc%$Yhyw=6rM6*){p#WoFKNm-n5Sopa8dZMAmn z|Gokvb;|2>I-}%22d2SEFagHE0U-MVK7mc}96SbVBO@c<*$#&xatvGr_rX_SHN_}a zIdpLb(;&AW%!ALMDpM}y+&?g?A+sCIg7=^br=lIn`Iu6l{aZ|at)l$%@*fy7joT4 z8uz<#A-P`SCXTt5JC=r=MmZYm4&vQS3;e*r1mo~;>Bipum^*xfpg|WaUE(ONK?~kt z?yd&!Z$L7;2__qWlbU@U;Vx?imT7r+_!3dWB31&$Rh+#RBiI{kIPn(WJKzF10YpsD zzS?w7K2=yyu(tq@^C&1XpC#;{y<_h>$ercL$N4IzjES>8JlKq&nKD(0T>|ExgJN7P zo5D8ZQyY4u&-#KQ+Hc`=nU{xNA|`V&mkGLU3f$_fPf9l9+AbHozPhua3mHX3Ie>B% zvE`^DX(y^|6sV-%pZPIP*G=y$7bxAKMIPfcq4d7XwUBO$-1l<# z4m`NBKS{nPvVh#wtsI5qZ$nJ3D?Idue+~Z4vT}o3tzYoio8=d}!&&~Jwxz1Ys+h~U z6e?^v7ZPlJ!#=bWHu_p~d%1@Md&i?5vM)XDRWw_3d%1@MYf5Ob_ZDZPuQj)qdq}X> zdWO}!+(Ux3uw}7B7H6ZcHMf_0Nbnk@f?p4MNFVDtuJX{5Tb#u`)4YP|4zy&sni-Yd zcrm-Vl2%LZRmVNEuvf72w)B6O+#n&dP|-%0(=x}kaNbIbVp{99EPr0#nxXs=^);W4 zHF5P0E2G$uU_Q1Qeoapv=A%S~oDiXfZp(^8Q`19ntY+byM6T(y^W0c5K(Q7Rx7_b$Cfy_~vY4jdblo5EOp9sW`Eb_NfV`F_ zpMZT?=L2n;E33QEtVa`pWNfSvlm->*XVvvs=DfJKettR~76F!FcehsO|n#ogLhY;%Ww@&WRdT^Sywjwz(qyVN_Kxmfb}&$*cTMozJ0EPjxl zcEmrjjb5CVFHnqZ+ETMoU&yX>Df98bN`$!NgTsn7wh?~_Pdc$**qT4LC> zOHoMP>r(A>7ACj(h3$D)vivOCnYUk=Pt`SEV=?{tOdKaK~@w3iS61`~~Wo V6v{Mr9qRx9002ovPDHLkV1kpnT=@V1 diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_camera_mute.png b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_camera_mute.png deleted file mode 100644 index 91ca2823affcade4b2e671b993e2fdd3a9aa0120..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1612 zcmV-S2DABzP)Px*21!IgRA>e5n!SrvM;OL;W2Gd8U9xLsf+U@t%?etikbnu6vV~2y5mpE!)&2zm zBW#;LKrmoz62&c2iX?qL{AdINK@cK|F(lsnerM-puXnv?&YX|EYuFc_JLjJBzR&y2 znK^Ur%-p%TJ^O#(f%ze;HyVxh!~Z@w3El%-pgjSP!CmkZ_zrB$&(A-ld^L

#cvTfTjze7*W3U=e%|!ftVJyssp3 z0Xf6Wwb&Pb5f6!vfoRox9G@v7C~%e=+y|OD?e;roBwPkhfo9k1eQ=qqjX6>-iYoW= zq|YGVNoHlk);ZX*=gM5boMFROnk;tXXxR&G?kMr6z=MQSEjca_uc>92?hUa6R6Mim zZG;*Fhm{R`(19mfT21{9WDwewVl?)dZQYHD?W@9SQj4ax)|$56bjVg@#T<=04N$~) z?$RMpElCr_XeG{fDj_-mqAL&f>S-$yZN9QJHTr9>n?bTB8+NN3({Ryhs}!QOQx-_< zGQTIXrA}l62BU8&X;f?bM~+};EZW9^dV8T(W2F?$b%d|gUF6z_>H3w7U+rat90KRT zx8P^+B*A0PdF@awN&ezj$t5)BBgQ=tupc-#eGZCeJ#rDZ0KNqOfSl>0uhYe1BhFct zTGi~rLzxkD8fdKsE8>ig=l^exuYj}Q2zWigW6ya_AI+H4WokFQiHpIjJf7f7Kr0^k zh*<^y8jM{9Z|5qYPx7UxVpnr9lh=t`GZ?!xc`f^$pzBpAEs%8{b0Ef+wX)*Vu}h5( zjTwtCYANFEE!y=)$;O=sx>JRYMkmJ4*B#@(0HPZgS2noeu+o}bjh|_JPZ)&V39iN^ zL653Xp1Gz$_&exmSDVI6^OK`uf^(YB)&%sc8HjO{`9yTsk5T|j(dEbA|Sgx_ccrQ-2EfdGJ#M$)Sn$=oY zVV6iXmr|RUORSnA`?BB)?i_Du4ei_f>ni*>c{d$2q8tCPElc|o!4=#&rqCMNxB1sq z_-^tJl5N2gC&JIg_P#8*f;-0>exZGve_e&$>(LHx7Bl(GK9duwLThNBxI8ykS7A4{ zw5HEbX=`k<*MG({8Qk^WTUtYV&VTDVLwnKW0e@#b+Sx6ZZbx!ejBw|e@M|=AnA*Z= z%kn0P>{d;F;<;g_eY9Eh@)=ygT}P@$wcbqH>wI5+jl0%(0RZ2b{_r6G2GGvEyfzKa z5^54X%{2UcrX9$f;^j;qedFh0ZIj@)o(R>{?`B$LhPc&sDRI8CdRwN2 zYxwQbu}UwKFwDE`9C$~}Tp(Ij4a+-~*hZXl%}5cKxGr(@GLAf*8FytbPD2P4h2358qPalv^{`FauOmaR~6aS^?48 z99CWLW6I(!8e@R5(S13rxXsadyeOSuPrVO_Df#PR%x`@K1)RJ)_qMx+uHE<7L&Uvc zRNTSES(bkg{R@i$0LA5r}q(=XsX=Csc# zUmc^uj;xxaYxddGC;3tgH{uU=2N8W+<=FYAN%XBEh;16PEBp|BG70~Uvhn7$zQ7pm zjjKaS#2))-){c68AtL)d7lEhSx0$|g>m zl3UG86y3IDhogkBC3_FM#L-TnBIk5w+G(FZPG>#OTEENlz3=ya-}nBpas&LfX=-fL zKp+sB-d-Mo@|CQ3)l}qP($X({`CfC+4OiQ5TY~p?d^c% zh?O?1;7~Y}=1XF5*=V{V2A#_0$ISSX&wp&(zk+(7~?CIuOSr#aAg z?oa~DD~%5YrTGOj()KWjOr*=-0CFlxPQZp>I*`gv;s{8o6y#@Kl6sf|M)$vjjHpD=kOxrA_jNVN&Tl42X76l(Za3qy7I-Hv4O| z01kxy()&M&1;P7x5GD{3aFh8A`QYL#6sdS5cRoaix%^-*H)*+x0SR1~D@fq-0C#sq z*PH+w8lA!7C@yV3qi8geH%9=|ISk0#gMyUvpjj*?3FiRfT|qY|9L~WV1gTDLI7cTc z-oeqC-~>9kfv(F~4=y8_4RPRQEb}j{^Ea^yL$G=B%pMS*C4`usd@dXKTsVpKZ7uk3 z@_oTFzpaJvO)N$(2BR47e~o+jO70=W_N#g2hp+aBIC9VP<>t1T;SVAZtJ=IhT!T}G zrz2=yks*4SxX|r)X^iq0@jLU}j^4K(GGsKXc?nf=YY0cJl~y0BO*Ah*+UvG*|G6UV z!{+VOsl<&P+G;c(TgSas?zsTUcM6LS3UGJ5!VU5zUUvdW9> zoSuKBC^y&4US2q0MSq~{W(aH-H`r0hy1iSKY+Cb+Jl1`0Y(Aj1CY5Z{YNljWzQR5o zfSOcMTlIUarzjLa)%RXmd@hjp4!hP^b@ZYvFmC_Hxt^fPiiWE>bBNNV@ZCLnGpOI| z*c<6mwkLjt@$Vr}%nEi^Zfy4crKW&VrQwAq+`DH^+>y!-KK9Eqs0!I^xd`N+T;H5u zPFj7_j~ks`|Mq(w&l1L^hf2q$Z|k#F^`FP2mOYE~d;`8*+P(e5uV$AkOv3>Gj5dSO z6UgREeNVD#O=XvFIv%VvT1m9qLTAm$6I7K&q|@O^Sb^zyrJA-HDK%*$sEG>FGMnfRk|C~bXzc-GP8vwo zyBVQ-s}4vXCyDCy?}C_Scj&8}FmY|x8pHVX1TXy*>K~5cHrI=7 z$J%4n@b7PE=$726S>R9AUS{ThJU5>+9Ho`(6Ir?lmYYpYVx7{J$iG!(mX#&HoZOXW)m8nRA~iYm@bBUX7i!D4k+9rXRFrJ( zfYoa8cb8OdoIEe{oANH&mbEE!Yssw<4WYms%qQ%toLZ-z(vOU6C^3nNx;irrxM)0H zjeoF6ne!+sA}l4I5Ia_mNusE!=O?7Dx)S%f>16E>?Xhh|gh5-zVJxjn{7AT7W{sk+ z4Ol0^5t!fo7;p2>!c{VBqt+I3P|jq3UzbuMQZ$(?5~tZd{n36QscD#8Y#JSB(-5;x zVhwihpqNODdxpY~&)DthGnk5vI?U7AIn%M$uI0PN8np%+^`eO-3 zg|i0Q!JGh=Q=YS7Ys;n^w8%=|f-@FY`i0}zuy8-Yj!{$Cx2xdDjtL04B-La!5^07P zTrZkQt4j%fIbq?=mdySV{;$lQYQW#Fo$^Slq)ke;c$9V%ZwgPjMm&5wUf6+Bx&Yhu z8AKB?{vV#hGm`RY_acij(N8-5D_h=NQVP2|DQK^^>~xtQ)+4$6VLwdwbhi5qCzanrd%1rn{WH~qo&NbxpN6&v$*>t;?CeA`c z8`x7J`|GWQUczY|mxt5?={v`&I=0@6=PV9$G+So|o*HNTTlo*WbE8lRp>qgpDx?$+ zMovsf7MySWkpEK)x16NBv}&YLdv6I_YsaoJ%5>U`{O-b5Ldw`%zTW1#o09#jg!6~a oo^E{h>2%pb1y<3dvKO86Oq5uE@ diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_camera_pressed.png b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_camera_pressed.png deleted file mode 100644 index 62761d4fa873ba2b162b4fd296c4192b3a65baaf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1939 zcmaJ?e>{|T96$XqnPL=4cswMOyXV}AyY6uAROdRx6{Ys-areZH=bm%-$kBMENzufn zp_@e%!lK=!Yi&)lX?jImKTsJ`LnG_QDy6pPTC_iA&+GL(-yg5{`~CU#e!ZUTu#mO$ zO_!LGNTm7vb-Zw5Wf@)*W8%x4>`@SlEglqwM_@bfWLSZaI1(%d0r)5!i-aSvBqgyG zAsDmWq@pN1O1PdW#!xbBz>rm_oM01T{wg^vjz@4H28or**x>lNA3#7VVS|z0LP#j* zB5~4nsR|?_HAEy%jTbW{puZo$QZWeu6v1IYg(k?9Ocfh^qst`rhHDB4yn*2HZ1A6? zqJ&`p7gHdBH+hw(m`d{nd>CY^H^bM9z7p_)sH-RtgF>ZyLR2QiU_w;j?E@0g6p|gx zaNe4?v4|5JjKgs`lR`;OP9`VQ$e1FQLS--*6v&I><>g5rJe4Uj99DVCl+IHMJVYs0 zNaeT`lK}=rI0j3?*&vbWKP8~@X<3=_ZJUUOQB<&;LM20nlBR${;r~NXbQ-P1!;yFS z{->}~lp;qc;fNATQizGc?Qk}Pk~6sq1jaFi2*VPlx)>IR;g~WGlLK6?p=&;Xn-CUD zWrj=lH!Pu$$(Jc{SSCjJJT^$^kfl-ylgeAe;n3(D8V%x7sX;!0G`dfaH$?YcIqZGS{YcwRx6yYTz^P!eg55ue8qscw!&v)66qSfnps z!)VBf+<(e6&SUnHyX$Q&R&yGH>sFf0+j{(~uDRN)9u^(o7pFcq*#~sylW#l4z>5-c z7w-=Y&=v3gEqc=7m*&S~qgNhpYtP8@e9$)far%(VtI+G=Q^&aDb-?sQmylmqK ze>q}JPYa-Q%*AJ+0fnWajEH^nrro#bN8EJ)@O*Pu<%VU5g2>uoI3I4dh&QJ-+t$-3i4v zD>k{eCBeccIVu>*HC9>G+3z2QfohyPq+t<_zAx8D6bC?#S5y=Z|L)| zzpoBnk!F)OaMAj!LS1fc@S+nmfnB9OKJa-)kff+z_b|~dsc0qGiv;Sf$}hLy%FoR| zQYAcrt1V=-;kX``T(N#vPr&K6%m(f28olP;(MDNkqt@Zp_`boGlMjr(gf0ygY8qF; z(vM`jwsQDrq|iiQ-jv6(EWGLG?ewwR=-qB-_{133>c{suuP$ZG8(_oB#~kLZL&pzW z{CY2NYb29^b}yg|N-|TBvnz(-yeUcg~`UF4z@j-w{<)%8JSxSTfSP zIByGRwOP!H&bfRt_aWq|=ZIPC?0jxzEsy@#CQm`E{6fDS znUFX6cO>_p9vj=M`Nk!`%H`#3RmEBTmSYF?ukJUyms0LDS6MnH)eEkl`Jnd&@M+wM z+8Yl2HVy7!y~je#(QCrYW|J>mu1rjzt+9vFN;quZRr5*X8}rxKMVu*Z40jxWyzE?; z?Pc1XFCUFLzR}cj;dAfuk;pq5++&&YS*i4|vy)caNi_w%41ZQ1X!uR?gF<*G1Gn$_ E8*-QyxBvhE diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_hangup.png b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_hangup.png deleted file mode 100644 index 914a30da828b9923546c14e98755c9f419693bdb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1025 zcmV+c1pfPpP)Px&yGcYrRA>e5noDTaKoo|1TVGWWtrZ_ANEbdRQt(xuxDdpRSc)KBC?d655CoSN zaUob#M9`g!qM)v%pn@p4kP8+UqTr*5pl(!Xp;fHnt9t#85Q*W&&Loq`OzDB2B$G4e z{Qu14HJOTvF&o`2Q23v`QmHhR^ZUS7uo%>U0niJcfg-q5C=>?S`fZ490q?;`_7jJD zpdHNcQZ>XoK@$5$rHf#hH^L#?nnY=tI=3lXvnb<8*}C(J`c?=llkGYW~DLDttWDazVuN~<&}r*|jmQqWtLQuc+nNHHzF z3e5;4tp;a+yfWIh7bLWPUH@q!5;p?5#%$XF3D;@%lNomsSAhfI1F&gwFKIHPB0&_A z&jMZG7f>~M>_}tn0B6Bxpku>0?@2({Tonkc1U)0L9^(A|XjaPV!2J>GMo&uvAB@Jb zdJKUA*a_Z(unD%Gk3x!2q5IVqF}kZFO16>o|lgEaizSdNJRcR1j~QtO+g&j z1)qXwe?T_Hc4JW2X~C@F9D-;^KsLp;K1E%-Rk{8uf&7U~V|{j6r_6>@x$&g2;edkE zLpmVI(C%UXa&Vkr>)-Nbx_2)4n6R6RK_hw5!$Ou`e#ho$KwNWsL(|_4cY1dmc&ypW zvN~8+_WNm&mm8SQ`*n+-N<*v>th&s3)J*8fa*O96&SV(3hT6#gT?91%SrHI6IhG|- vI`d!tGzP-9gMRP?909ZaVL!$+n-=&Di70tIV^ObO00000NkvXXu0mjfJU!bn diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_hangup_pressed.png b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_hangup_pressed.png deleted file mode 100644 index edb02e4646eb091869baa7ca8669b28cdbd8e8b4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1305 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 A;s5{u diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_microphone_mute.png b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_microphone_mute.png deleted file mode 100644 index 51bef69611368dfb5af97d38c4be0fbe7df4df07..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1189 zcmV;W1X}xvP)Px(Ur9tkRA>e5n$JrWQ545}329^-K_L?=1EHiaC}|P2Yf)&f`wN25m9@!sZCun& z2y@q_T9`rwQA-PI9Ym2CB(`W(CX{5;=R5D-ht$0JdFMIieBt}OY z-iH@p4{SBmvHCP78$mvUn`*WsJPg;MFb#DXhe9^Smd5uT33VvSAjzt-nyh#^X`K#_PBkyjb0%nnTEP%&~zrL1r+r?o($WrYh=j9gMFE1b(|Es$tg;Q|#SmsH9M=W<#LBpR#2 zu78udo_~`3Volg}f*YwpAwF8qE~+0L?E(-&Ux+i_ z=UY{Ct3>H08b^4-h>wRkVlxPT!?ar-t{7kW4@ATo$3~b4vr9FG=m}ri^Hyl>${47{ z_y|BLS~lX4O4Y98j2d<@}xzP#s6MR{YVO56P&+7VO}ip!+)az{F^QS_cHh?npm_6rKb;3hkowlHOTCGfe1n!0NP*{SeGYJMbOVeo6c<*6vfLY_ZNT3ZqgBCL!9L|~n8akP? zR+RQBY|Es#r=z? z(Rt+-ubgmhr?w!%Tdu;HS81(0jVvCkc&41UT!sGtiqn|OCtN4=00000NkvXXu0mjf D1zarZ diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_microphone_mute_pressed.png b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_microphone_mute_pressed.png deleted file mode 100644 index d2349f80e43f054193e156d6a15d030d18e4974f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2000 zcmeAS@N?(olHy`uVBq!ia0vp^HXzKw1|+Ti+$;i8k|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ-L~|H8MjYN+NuHtdjF{^%6m9^eS=-fVzQ(*;f=KCTFLXC?ut( zXXe=|z2CiGNg*@ERw>-n*TA>HIW;5GqpB!1xXLdixhgx^GDXSWPQj+a3TQxXYDuC( zMQ%=Bu~mhw64+cTAR8pCucQE0Qj%?}1aVY^Z-9bxeo?A|sh*i`vZ1ksg1M!hp{b>X zk%^9ik%6I^zJaB_p^2`6p_PH9m4TrG6et00D@sYT3UYCS+6CmEg{v+u2}(t{7puX=A(aKG z`a!A1`K3k4z=%sz23b{L-fad6BW~Nvfy0|$y8k?9I8yh$q8oHW08Jn29ni`l` zn3)@zI~h8{^t$9Hm*%GCmB93-AoQB!)C)=wxdlL*T~doO%TiO^it=+6z#g~C#O)Rn zoaRCGrr>sqDNeomK*#8Vq8BN;VM4$(1Y*JyFOUOI{;7GuG+zWv+?m%_moqT1*m=4* zhE&{o6Xoj>5-3uqoc35riTkpvqG5`gi0&E54vh`23tWUnLR^@)WkiZKUOE>w#cfMw zqM|O3(Jw`D-C3^n59D1OMV7eOyQpQkObB}RW$qs~`Tm*T@1410zOS?T;o~#scRru< z{%-a8z3=x5d#DJt1#`17x#{uRg*jYd-0Hx(VNSg-!{P%11`PI(Eq@h0d!(9pL$BF_ z;SEcd%$4*;iwnH0;ocev)&3=-{vG!{#+#`gqwFGaA zkI_~&v$}kRQ!8e+0RGe(@j9#ShWpx5MmQ9l>W_1f9CVCNF>aMV0pqziB5#+0AC2yK?>F^WZ+0r%7^V&$73~o#<)0D{of2 zN8QJL68AU8K)=G9Npj5o4b$_kOI?5IHgCbU;`P&Nf0`9=m!--6dfvVv_4Ez?H%78q zPcn}dKQLYX*ZlP1rqn5~8_yoAIog-er4}> x$_J*|Kl4pR)z{wJ!)Vtb+aK^Ou&{)1ysay zFw_bHTC|QBra-BP;u#MREFvJHR)s231W8Z|cDk{E{ZYCzyWjV|^E}UceD62AJ2Ygq zjrC${91ds0Tf+^!JvR16b}y)h9Id@l=tL53vZ>TSI@^;*5A*_QE4=Ac zI-43W$>k#AWEhg8lU&JXZosr$(?GxqEHW2T%C*4kbYU}O+D;8GW=xGgB*$i6i4Aw}m%GQYJ5$T!21IHe^?%2g5(4b> zYR)+$AR>A#Je3YCuF;Od?n--o<;6X4mw<(* zdS6*qVriEn8gC~a$x((bNLtXB_oAtk@YSUE7p|`&_q0SEJhl|fia45Lb#9lH zZ{M$?w8w4r_QmI7`)-Q;LI!6m$m3rsMYC#Z*WyzY28*gxR5EU9b?8ExM zzSko-5d*$*ad_aklQE}>lQ{I$*0GCTS(Yaa%~c1C zsJ`Ue3NNEw#ps6R&fSfkg3-K(L49F^V-ZRF(&FcERabbgEY(S|gZ|k?E6LEw`UhQtXtAKkUbLDL-3&u zKh2Skr5L>M9rLwC2XlV$k=yEaluDj(noCya`Z$7eRmRyyUDdOWs|4F>=|cZ`{_I(g z-_d$i#zsR4uHs;ajkc);_$HlTaJxJxHHtE92Ga%01hJkr{OBQkYjEuPJDbd`aF5qy U_PaJ~-A#WtUSJ5ff*qgrH@TbYGynhq diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_switch_camera.png b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_switch_camera.png deleted file mode 100644 index 8b255ec64779477340d3a1f2cfc7932b0ed0c1fa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1286 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 diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xxhdpi/icon_call_virtual_blur_off.png b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xxhdpi/icon_call_virtual_blur_off.png new file mode 100644 index 0000000000000000000000000000000000000000..802c93b60b0957450b1012507872231afab2aa41 GIT binary patch literal 634 zcmV-=0)_pFP)8NwHTyXuZ#jhr&M$@0g$% z86u^WQmR#5Y=dQ4j`rbf7vmNQ?sgfwu6vJd7z||N^!DLum!4#r?Q*lBX5R(}3cHfr zIPf?D+`HsY-avMz6UmPA1F!MyTNyfo&a@xKw;~kimG;Bb}W?VbjT&_EF{#Dnp} zL}?h>_Nf^g>=|Nw8gm0pc91m?lN(Z;XuTme8n(y2Y&`Zu^|zRdbD{Ny|NN`KRW;DF z$AM~rT8INx0<{tciU(>b4ipX4S{x`AsA8NWF%$_@D~@~KH)6;hsB)Yw&-+W(K&{7t zY=I=ifh>Wf#DV02B*lTGfuzNOWPv2cfh2*X#sPL9$#H;tMbS{ED}f=U;sEyy>S?`Z z;y~%k9Zki7l7VW)nTerPpjvSTVty@EB2cwBDK4_*4%BoU$Qh{lIFKulh&YfVkeE1- zI*_P1kTQ_CIFKrk$T*N9kk~lD4fG*74sZi~(ndPFBS2?&1nBIJ0G-_t&|(}AJ-Z_X zv9mi;VSlUP<{^iiXua7T+Yp~225z997?ZRs9CtOnS0h3LMQEUiP@swSp7#Mx8jV~x z)~2|!USN{VVds;*z_^gRqY5(bOnn~q`;jKxt2E7dP zIpKTr=ht}rf2Vt={od2Vg&FoTg&*EVXFRm~vv2zSxodyOL%s=eYd)+ZH1^gLf+`UFPmOJY$(N#c zf^NI6JF@Fy!;Yo`mLk<_Z@tPs@v^rXf<HO4OVv(gBV>;tp8A96hLT624=0GKN)&Ec*Po^+S{cws=oL!jv!<=9!nfMVjW z3jz|Bf&^^XS^M~(Pudieu{(I~yMVcy7CZ{+%-XN7_Dy}T-~f{| zhn|H3JFA#feBnYyON|W=9!%!uIpNsY>i#Sy!hw%#&4=x+++09`ssAn)Dgsp$9NcW( z#wGqbCB@ob=D6y$`KAlz2RvX|?q%`$&w~9Doo2h0b95ha%)Tp_xY=51o5619)JYB3 zr21CKU6^%P@Yd|esw3)?te?C#{K)e4!dacN?dN;%zBQi2Dw4?P%EMZ6sBDficN^2H zV%yA_^MI_rq)!fw_BRgXKK#e<&2Wps58hpO-T!sGp0Om literal 0 HcmV?d00001 diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xxhdpi/remote_voice_off.png b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xxhdpi/remote_voice_off.png deleted file mode 100644 index a6a1e88a5d2cadc830aca84332223dfe2b9d9816..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 843 zcmV-R1GM~!P)l z+b|4<>0Sr$2;3kZL75=lpzZ|q22m!6H&8c7H;^+y(g~zaV0Qu(u;?BQf#NfXa_9e! zXVU>eek4c=ic(iyb=7|XK!v5jvcsa)XA%y01()hlA^UC$37K6 z!!Yx<+R*kX5H%D8Fg`26$2P`64g~F`HV44?%+f5KA1RcyWVmq=ra%M&a8)X7GIo-Y9mClWFpl3kU|{5DDcJE< zIE?IIe3F!as67DEaXGScz*ath4$HbAMC};Zn)w*dt`b+Y@c}q5jzoxh!e7gLYy}(5 z`$+~!gwPYd)**evINVMv^+hFUu~;1O;}*-S@Mn#swFiy%{e-(cmZMd#K)xIHLO*E$ zW>8TO9zU-_d#*pMe!4W10j$omAyB`SFgT_otEY#MtwIn-5m;z7ui?!MxvF#PG}`) zK>S$LYFLTe^CgF$1VCRq?hA%Q$qu33th5t=UUKm#(!W?0@JsrLBl#I5Jrp__g7RsfkKbhTy-_stz*rWHN z^z}x$MX|YNziBMg0mb*jd1D!E$S3~x{rp4ho-pc}-GfRM49S1zn?bltQc9+$jmi}a z+0hfYbq`@#Rtkhxr37*Nrgr$qI-N>DXmE#=7ho0;W)Qw(xc0Q41w;kz)wm02KMM#7 zDSmG6BHH^PLh^=uLB>D%JP=WjFdwD~h|d8)5+FKhA1u_&1;aa9iv3^gQRS*x=pUEZ V_yL^1qSgQa002ovPDHLkV1h4BZV~_h diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xxhdpi/remote_voice_on.png b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xxhdpi/remote_voice_on.png deleted file mode 100644 index d54aa1d3cee8d60a4e3af79c05df910887aa8698..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 865 zcmV-n1D^beP)qWjuOvd?7iJq$IcSR^X_c zLN$aQJT^{)J7J>!i1mm)_)_sM>j4Y_CMG%J>xex$PvZRp^bP!L3;Zl}Jll}O zH0J+?hA`#D=K3qW`=pqyMVS%VuE{o&WD&Llw??Q`?(kX*C|n`vj!LeQqgUoX0DmMT zA3MyniO{!>bXp62VcT7SKR8Xe$v;(^(hr{vSAl?}GSZj;CDtvj{J+WV8gyh(jnE&G zl;qhb*Ii)eDU$5O-nz|0^OM5=9m;)SQK}#3sK~QU69;+>ql)cPu?d7tT0Co$LVaT+ z^gC%_Qni$BML!sW0|}HX7dkp~BJ_ZNF7ANI_ZM+HP)vUb{oIkq&;pE~Lf4kivSW`X zg&0jnwM^)va-a#+HPcuen76eVivu@8&lH=vW-}HCLZQDXHmZ{scYwzHNZd*sOBFdX zot+#*3#^2`lLnY?lB;C6>3*3g7T!5F~4FO3Oy+eS!A~%j5RAXSizm?H{b_8c3^ly?ZMt^M4vB}0}J2H73lWJ=qP9JG!KQH<~oArG} zav;J5v_l~D2S?|X%j?8e&MCy{Ko#N38*qc8bFWllXFA#1bECt{=pu#q-OA8uT37~X z4Q+w>=0A0$R9d7IHa}LGWBsv;ENcs0f%)!ek8Z56pM$NH;OKs^B??$#bHtw9VONf( z1#xSneLCRaJi6>V&BQ!rlYD?K9jgjHPzWqNBYA+Kf%P9a?mT$xv)AE2aEQXW`FEo7 rUNbi4ed~mdV@rwYBj1Dx6E2Itc4YjW4Gu~V00000NkvXXu0mjfEv$ft diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xxhdpi/team_avchat_icon_add.png b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xxhdpi/team_avchat_icon_add.png deleted file mode 100644 index 05698840172031abdb37cdc8cfb16bf8e6c47bd6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 444 zcmeAS@N?(olHy`uVBq!ia0vp^HXzKw1|+Ti+$>^XU~KkuaSX|DemiTUFLR)P%lzsC zj2=r&9GNUz6y`WNFe(Hx3yC-|XmU8saekyWC#s2|%b~lb{ioH#Bb(zNJ>$CZbF=mP zFQwJYRo{53o{3BQ&H8aG=DD%w`l%=9eGn^PlsWHTz#hYNS?BT1w(||E58bgW_#4l= zpTW9yXL!Q(HdombeOuIdEG{iFsh?iDWa?ekGaDqu+|TfIOXswm-C&WF7I7!RIH&EX zpz+~HY_k)Mb^0R256?beR-kq1-LH=26LvAG9mv^OG5w+Y1Bb#UF4Or9hxhZWXU-Sp zTQ=nm!#(N7qC*>*eg>=xUt!B~YpP%Pl6>(nMbUHHgHA>~ouiX%@y-7CfqPy{>$_6- zZaH?Kc<1&{zTPHRm#xW<+Lzg>oYr=f*X(dwgp8rFPTmGdv%`-%W+oOkpT%(F1LX(% zE=Vx)C*9pv^Q|R0U$lO|`RX|ltw-WN?f+))_4#I-wEo6Roy%hzHGK7c3(Q{{>$?0} i%jy}t7w<{_W3n(Rus!)i^an6j7(8A5T-G@yGywqTqr)@+ diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/avchat_receive_bg_selector.xml b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/avchat_receive_bg_selector.xml deleted file mode 100644 index 3b0892d..0000000 --- a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/avchat_receive_bg_selector.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/avchat_refuse_bg_selector.xml b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/avchat_refuse_bg_selector.xml deleted file mode 100644 index 960ad8f..0000000 --- a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/avchat_refuse_bg_selector.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/floating_window_phone_bg.xml b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/floating_window_phone_bg.xml new file mode 100644 index 0000000..3b1cff5 --- /dev/null +++ b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/floating_window_phone_bg.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/g2_avchat_remote_voice_icon_selector.xml b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/g2_avchat_remote_voice_icon_selector.xml deleted file mode 100644 index e82222f..0000000 --- a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/g2_avchat_remote_voice_icon_selector.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/icon_call_floating_window.xml b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/icon_call_floating_window.xml new file mode 100644 index 0000000..362c53c --- /dev/null +++ b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/icon_call_floating_window.xml @@ -0,0 +1,14 @@ + + + + diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/icon_call_phone.xml b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/icon_call_phone.xml new file mode 100644 index 0000000..b6948cf --- /dev/null +++ b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/icon_call_phone.xml @@ -0,0 +1,15 @@ + + + + diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/nim_actionbar_nest_dark_logo.xml b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/nim_actionbar_nest_dark_logo.xml deleted file mode 100644 index 442719e..0000000 --- a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/nim_actionbar_nest_dark_logo.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/nim_list_item_selector.xml b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/nim_list_item_selector.xml deleted file mode 100644 index f611f28..0000000 --- a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/nim_list_item_selector.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/shape_call_floating_window_video_bg.xml b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/shape_call_floating_window_video_bg.xml new file mode 100644 index 0000000..9f67fa5 --- /dev/null +++ b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/shape_call_floating_window_video_bg.xml @@ -0,0 +1,16 @@ + + + + + + + \ No newline at end of file diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/t_avchat_camera_mute_selector.xml b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/t_avchat_camera_mute_selector.xml deleted file mode 100644 index 0addec1..0000000 --- a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/t_avchat_camera_mute_selector.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/t_avchat_camera_selector.xml b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/t_avchat_camera_selector.xml deleted file mode 100644 index 3de24cf..0000000 --- a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/t_avchat_camera_selector.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/t_avchat_hangup_selector.xml b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/t_avchat_hangup_selector.xml deleted file mode 100644 index d131960..0000000 --- a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/t_avchat_hangup_selector.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/t_avchat_microphone_mute_selector.xml b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/t_avchat_microphone_mute_selector.xml deleted file mode 100644 index 83991e0..0000000 --- a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/t_avchat_microphone_mute_selector.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/t_avchat_microphone_selector.xml b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/t_avchat_microphone_selector.xml deleted file mode 100644 index 052d12e..0000000 --- a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/t_avchat_microphone_selector.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/t_avchat_switch_camera_selector.xml b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/t_avchat_switch_camera_selector.xml deleted file mode 100644 index 574aac1..0000000 --- a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable/t_avchat_switch_camera_selector.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/layout/activity_p2p_call_fragment.xml b/NLiteAVDemo-Android-Java/call-ui/src/main/res/layout/activity_p2p_call_fragment.xml index 1cd8a07..a849bfe 100644 --- a/NLiteAVDemo-Android-Java/call-ui/src/main/res/layout/activity_p2p_call_fragment.xml +++ b/NLiteAVDemo-Android-Java/call-ui/src/main/res/layout/activity_p2p_call_fragment.xml @@ -5,10 +5,7 @@ --> + android:layout_height="match_parent" /> diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/layout/fragment_p2p_audio_caller.xml b/NLiteAVDemo-Android-Java/call-ui/src/main/res/layout/fragment_p2p_audio_caller.xml index ed49747..e1a4b75 100644 --- a/NLiteAVDemo-Android-Java/call-ui/src/main/res/layout/fragment_p2p_audio_caller.xml +++ b/NLiteAVDemo-Android-Java/call-ui/src/main/res/layout/fragment_p2p_audio_caller.xml @@ -194,4 +194,16 @@ android:layout_width="wrap_content" app:constraint_referenced_ids="tvSwitchTip,ivSwitchTipClose" android:layout_height="wrap_content"/> + + + \ No newline at end of file diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/layout/fragment_p2p_audio_on_the_call.xml b/NLiteAVDemo-Android-Java/call-ui/src/main/res/layout/fragment_p2p_audio_on_the_call.xml index f14774b..50f2ead 100644 --- a/NLiteAVDemo-Android-Java/call-ui/src/main/res/layout/fragment_p2p_audio_on_the_call.xml +++ b/NLiteAVDemo-Android-Java/call-ui/src/main/res/layout/fragment_p2p_audio_on_the_call.xml @@ -161,5 +161,16 @@ android:layout_width="wrap_content" app:constraint_referenced_ids="tvSwitchTip,ivSwitchTipClose" android:layout_height="wrap_content"/> + + diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/layout/fragment_p2p_base_call.xml b/NLiteAVDemo-Android-Java/call-ui/src/main/res/layout/fragment_p2p_base_call.xml deleted file mode 100644 index 8eb7aaf..0000000 --- a/NLiteAVDemo-Android-Java/call-ui/src/main/res/layout/fragment_p2p_base_call.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/layout/fragment_p2p_video_callee.xml b/NLiteAVDemo-Android-Java/call-ui/src/main/res/layout/fragment_p2p_video_callee.xml index b575d3f..063f066 100644 --- a/NLiteAVDemo-Android-Java/call-ui/src/main/res/layout/fragment_p2p_video_callee.xml +++ b/NLiteAVDemo-Android-Java/call-ui/src/main/res/layout/fragment_p2p_video_callee.xml @@ -18,6 +18,17 @@ android:layout_width="match_parent" android:layout_height="match_parent" /> + + + + + diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/layout/fragment_p2p_video_on_the_call.xml b/NLiteAVDemo-Android-Java/call-ui/src/main/res/layout/fragment_p2p_video_on_the_call.xml index 67bed51..4e9dd35 100644 --- a/NLiteAVDemo-Android-Java/call-ui/src/main/res/layout/fragment_p2p_video_on_the_call.xml +++ b/NLiteAVDemo-Android-Java/call-ui/src/main/res/layout/fragment_p2p_video_on_the_call.xml @@ -52,13 +52,25 @@ app:layout_constraintStart_toStartOf="@id/videoViewSmall" app:layout_constraintTop_toTopOf="@id/videoViewSmall" /> + + @@ -70,10 +82,9 @@ android:layout_marginBottom="50dp" android:background="@drawable/calling_control_bg" android:orientation="horizontal" - android:paddingStart="30dp" - android:paddingTop="14dp" - android:paddingEnd="30dp" - android:paddingBottom="14dp" + android:paddingHorizontal="15dp" + android:paddingVertical="14dp" + android:minWidth="310dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent"> @@ -82,38 +93,54 @@ android:id="@+id/ivMuteAudio" android:layout_width="24dp" android:layout_height="24dp" + android:layout_weight="1" + android:layout_marginHorizontal="12dp" android:layout_gravity="center_vertical" - android:layout_marginEnd="30dp" android:src="@drawable/voice_on" /> + + diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/layout/view_floating_window.xml b/NLiteAVDemo-Android-Java/call-ui/src/main/res/layout/view_floating_window.xml new file mode 100644 index 0000000..9fc242b --- /dev/null +++ b/NLiteAVDemo-Android-Java/call-ui/src/main/res/layout/view_floating_window.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/layout/view_overlay_permission_tip_dialog.xml b/NLiteAVDemo-Android-Java/call-ui/src/main/res/layout/view_overlay_permission_tip_dialog.xml new file mode 100644 index 0000000..e6fd69c --- /dev/null +++ b/NLiteAVDemo-Android-Java/call-ui/src/main/res/layout/view_overlay_permission_tip_dialog.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/values/colors.xml b/NLiteAVDemo-Android-Java/call-ui/src/main/res/values/colors.xml index d8805a3..b5da532 100644 --- a/NLiteAVDemo-Android-Java/call-ui/src/main/res/values/colors.xml +++ b/NLiteAVDemo-Android-Java/call-ui/src/main/res/values/colors.xml @@ -44,4 +44,10 @@ #537FF4 #53C3F3 #60CFA7 + #D9E1E1E1 + #FF666666 + #FF1BBF52 + #232529 + #5E6471 + #E6E7EB \ No newline at end of file diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/values/strings.xml b/NLiteAVDemo-Android-Java/call-ui/src/main/res/values/strings.xml index 54a1af2..0fadcca 100644 --- a/NLiteAVDemo-Android-Java/call-ui/src/main/res/values/strings.xml +++ b/NLiteAVDemo-Android-Java/call-ui/src/main/res/values/strings.xml @@ -45,6 +45,11 @@ 同意 拒绝 确定 + 浮窗权限未获取 + 你的手机没有授权浮窗权限,音视频通话最小化不能正常使用 + 开启 + 呼叫中 + 待接听 正在等待对方响应… 对方拒绝了您的请求 切换到语音通话 @@ -58,6 +63,8 @@ 我方关闭了摄像头 正在等待对方接受邀请… 等待对方接听… + 虚化开启 + 该设备不支持虚化功能 正在接通中… 视频通话 待接听 diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/values/styles.xml b/NLiteAVDemo-Android-Java/call-ui/src/main/res/values/styles.xml index 061dd5d..187e6fc 100644 --- a/NLiteAVDemo-Android-Java/call-ui/src/main/res/values/styles.xml +++ b/NLiteAVDemo-Android-Java/call-ui/src/main/res/values/styles.xml @@ -12,6 +12,10 @@ true false @android:style/Animation.Translucent + true + @color/colorWhite + true + true