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 6099b9c..0000000 Binary files a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/avchat_left_type_audio.png and /dev/null differ diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/avchat_left_type_video.png b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/avchat_left_type_video.png deleted file mode 100644 index 501e1fa..0000000 Binary files a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/avchat_left_type_video.png and /dev/null differ diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/avchat_mute_icon_normal.png b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/avchat_mute_icon_normal.png deleted file mode 100644 index 09985c3..0000000 Binary files a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/avchat_mute_icon_normal.png and /dev/null differ 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 7002b14..0000000 Binary files a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/avchat_receive_bg_normal.9.png and /dev/null differ 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 31c3b00..0000000 Binary files a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/avchat_receive_bg_pressed.9.png and /dev/null differ 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 4f0d7d3..0000000 Binary files a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/avchat_refuse_bg_normal.9.png and /dev/null differ 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 a7db35d..0000000 Binary files a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/avchat_refuse_bg_pressed.9.png and /dev/null differ diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/avchat_speaker_icon_checked.png b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/avchat_speaker_icon_checked.png deleted file mode 100644 index d74e7af..0000000 Binary files a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/avchat_speaker_icon_checked.png and /dev/null differ diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/avchat_speaker_icon_pressed.png b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/avchat_speaker_icon_pressed.png deleted file mode 100644 index 56743b1..0000000 Binary files a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/avchat_speaker_icon_pressed.png and /dev/null differ 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 5c60e18..0000000 Binary files a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/avchat_switch_mode_video_icon.png and /dev/null differ 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 41e226d..0000000 Binary files a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/nim_actionbar_dark_back_icon.png and /dev/null differ diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/nim_avatar_default.png b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/nim_avatar_default.png deleted file mode 100644 index 3767ba6..0000000 Binary files a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/nim_avatar_default.png and /dev/null differ diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/nim_avatar_group.png b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/nim_avatar_group.png deleted file mode 100644 index 73d12b7..0000000 Binary files a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/nim_avatar_group.png and /dev/null differ 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 b8ce2aa..0000000 Binary files a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_camera.png and /dev/null differ 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 55484f2..0000000 Binary files a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_camera_mute.png and /dev/null differ diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_camera_mute_pressed.png b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_camera_mute_pressed.png deleted file mode 100644 index 8e939a5..0000000 Binary files a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_camera_mute_pressed.png and /dev/null differ diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_camera_pressed.png b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_camera_pressed.png deleted file mode 100644 index ac4de98..0000000 Binary files a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_camera_pressed.png and /dev/null differ 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 ce1d5d7..0000000 Binary files a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_hangup.png and /dev/null differ 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 9904c24..0000000 Binary files a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_hangup_pressed.png and /dev/null differ diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_loading.gif b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_loading.gif deleted file mode 100644 index e508c78..0000000 Binary files a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_loading.gif and /dev/null differ 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 1f05681..0000000 Binary files a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_microphone.png and /dev/null differ 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 f69ac45..0000000 Binary files a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_microphone_mute.png and /dev/null differ diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_microphone_mute_pressed.png b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_microphone_mute_pressed.png deleted file mode 100644 index 8a8c8d6..0000000 Binary files a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_microphone_mute_pressed.png and /dev/null differ diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_microphone_pressed.png b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_microphone_pressed.png deleted file mode 100644 index 4b66e69..0000000 Binary files a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_microphone_pressed.png and /dev/null differ diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_switch_camera.png b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_switch_camera.png deleted file mode 100644 index c8d0484..0000000 Binary files a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_switch_camera.png and /dev/null differ diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_switch_camera_pressed.png b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_switch_camera_pressed.png deleted file mode 100644 index 43d1bfa..0000000 Binary files a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-hdpi/t_avchat_switch_camera_pressed.png and /dev/null differ 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 c244ad4..0000000 Binary files a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/nim_actionbar_dark_back_icon.png and /dev/null differ 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 540461c..0000000 Binary files a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_camera.png and /dev/null differ 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 91ca282..0000000 Binary files a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_camera_mute.png and /dev/null differ diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_camera_mute_pressed.png b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_camera_mute_pressed.png deleted file mode 100644 index ed00743..0000000 Binary files a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_camera_mute_pressed.png and /dev/null differ 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 62761d4..0000000 Binary files a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_camera_pressed.png and /dev/null differ 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 914a30d..0000000 Binary files a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_hangup.png and /dev/null differ 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 edb02e4..0000000 Binary files a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_hangup_pressed.png and /dev/null differ diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_loading.gif b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_loading.gif deleted file mode 100644 index e4aa9f9..0000000 Binary files a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_loading.gif and /dev/null differ diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_microphone.png b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_microphone.png deleted file mode 100644 index f74b517..0000000 Binary files a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_microphone.png and /dev/null differ 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 51bef69..0000000 Binary files a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_microphone_mute.png and /dev/null differ 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 d2349f8..0000000 Binary files a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_microphone_mute_pressed.png and /dev/null differ diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_microphone_pressed.png b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_microphone_pressed.png deleted file mode 100644 index 4239866..0000000 Binary files a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_microphone_pressed.png and /dev/null differ 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 8b255ec..0000000 Binary files a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_switch_camera.png and /dev/null differ diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_switch_camera_pressed.png b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_switch_camera_pressed.png deleted file mode 100644 index d3ab851..0000000 Binary files a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xhdpi/t_avchat_switch_camera_pressed.png and /dev/null differ 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 0000000..802c93b Binary files /dev/null and b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xxhdpi/icon_call_virtual_blur_off.png differ diff --git a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xxhdpi/icon_call_virtual_blur_on.png b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xxhdpi/icon_call_virtual_blur_on.png new file mode 100644 index 0000000..9d06752 Binary files /dev/null and b/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xxhdpi/icon_call_virtual_blur_on.png differ 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 a6a1e88..0000000 Binary files a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xxhdpi/remote_voice_off.png and /dev/null differ 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 d54aa1d..0000000 Binary files a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xxhdpi/remote_voice_on.png and /dev/null differ 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 0569884..0000000 Binary files a/NLiteAVDemo-Android-Java/call-ui/src/main/res/drawable-xxhdpi/team_avchat_icon_add.png and /dev/null differ 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