初始代码
@@ -0,0 +1,19 @@
|
||||
*.iml
|
||||
.gradle
|
||||
*/.gradle/
|
||||
*.gradle/
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
*.idea/
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import org.gradle.util.VersionNumber
|
||||
|
||||
plugins {
|
||||
id 'com.android.library'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk 34
|
||||
namespace "com.tencent.qcloud.tuikit.tuicallkit"
|
||||
|
||||
defaultConfig {
|
||||
versionName "3.0"
|
||||
versionCode 1
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 34
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
def callSourceJavaVersion = JavaVersion.VERSION_1_8
|
||||
VersionNumber currentGradleVersion = VersionNumber.parse(gradle.gradleVersion)
|
||||
if (currentGradleVersion.major >= 8) {
|
||||
callSourceJavaVersion = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility callSourceJavaVersion
|
||||
targetCompatibility callSourceJavaVersion
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api fileTree(include: ['*.jar', '*.aar'], dir: 'libs')
|
||||
|
||||
api 'androidx.appcompat:appcompat:1.3.1'
|
||||
api 'com.google.android.material:material:1.4.0'
|
||||
api 'androidx.recyclerview:recyclerview:1.2.1'
|
||||
api 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
api "com.google.code.gson:gson:2.9.1"
|
||||
|
||||
def projects = this.rootProject.getAllprojects().stream().map { project -> project.name }.collect()
|
||||
// if (projects.contains("tuicore")) {
|
||||
api project(':tuicore')
|
||||
// } else {
|
||||
// api "com.tencent.imsdk:imsdk-plus:8.6.7019"
|
||||
// api "com.tencent.liteav.tuikit:tuicore:8.6.7020"
|
||||
// }
|
||||
if (projects.contains("engine_source")) {
|
||||
api project(':engine_source')
|
||||
} else {
|
||||
api rootProject.getProperties().containsKey("liteavSdk") ? rootProject.ext.liteavSdk : "com.tencent.liteav:LiteAVSDK_Professional:12.4+"
|
||||
api rootProject.getProperties().containsKey("roomEngineSdk") ? rootProject.ext.roomEngineSdk : "io.trtc.uikit:rtc_room_engine:3.1.+"
|
||||
}
|
||||
if (projects.contains("common")) {
|
||||
api project(':common')
|
||||
} else {
|
||||
api 'io.trtc.uikit:common:3.1.0.946'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
|
||||
<uses-feature android:name="android.hardware.camera" />
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" />
|
||||
|
||||
<uses-permission
|
||||
android:name="android.permission.BLUETOOTH"
|
||||
android:maxSdkVersion="30" />
|
||||
<uses-permission
|
||||
android:name="android.permission.BLUETOOTH_ADMIN"
|
||||
android:maxSdkVersion="30" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<application>
|
||||
<activity
|
||||
android:name=".view.CallMainActivity"
|
||||
android:configChanges="orientation|screenSize"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/Theme.AppCompat.NoActionBar" />
|
||||
<activity
|
||||
android:name=".view.component.inviteuser.SelectGroupMemberActivity"
|
||||
android:configChanges="orientation|screenSize"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/Theme.AppCompat.NoActionBar" />
|
||||
|
||||
<provider
|
||||
android:name="com.tencent.qcloud.tuikit.tuicallkit.manager.bridge.Initializer"
|
||||
android:authorities="${applicationId}.Initializer"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
|
||||
<receiver
|
||||
android:name=".view.component.incomingbanner.IncomingCallReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="reject_call_action" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -0,0 +1,150 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit
|
||||
|
||||
import android.content.Context
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallDefine
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallDefine.CallParams
|
||||
import com.tencent.cloud.tuikit.engine.common.TUICommonDefine
|
||||
import com.tencent.cloud.tuikit.engine.common.TUICommonDefine.RoomId
|
||||
|
||||
abstract class TUICallKit {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun createInstance(context: Context): TUICallKit = TUICallKitImpl.createInstance(context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set user profile
|
||||
*
|
||||
* @param nickname User name, which can contain up to 500 bytes
|
||||
* @param avatar User profile photo URL, which can contain up to 500 bytes
|
||||
* For example: https://liteav.sdk.qcloud.com/app/res/picture/voiceroom/avatar/user_avatar1.png
|
||||
* @param callback Set the result callback
|
||||
*/
|
||||
open fun setSelfInfo(nickname: String?, avatar: String?, callback: TUICommonDefine.Callback?) {}
|
||||
|
||||
/**
|
||||
* Make a 1VN calls
|
||||
*
|
||||
* @param userIdList List of userId
|
||||
* @param mediaType Call type
|
||||
* @param params Extension param: eg: offlinePushInfo
|
||||
*/
|
||||
open fun calls(
|
||||
userIdList: List<String?>?, mediaType: TUICallDefine.MediaType,
|
||||
params: TUICallDefine.CallParams?, callback: TUICommonDefine.Callback?
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Join a current call
|
||||
*
|
||||
* @param callId call Id
|
||||
*/
|
||||
open fun join(callId: String?, callback: TUICommonDefine.Callback?) {}
|
||||
|
||||
|
||||
/**
|
||||
* Set the ringtone (preferably shorter than 30s)
|
||||
*
|
||||
* @param filePath Callee ringtone path
|
||||
*/
|
||||
open fun setCallingBell(filePath: String?) {}
|
||||
|
||||
/**
|
||||
* Enable the mute mode (the callee doesn't ring)
|
||||
*/
|
||||
open fun enableMuteMode(enable: Boolean) {}
|
||||
|
||||
/**
|
||||
* Enable the floating window
|
||||
*/
|
||||
open fun enableFloatWindow(enable: Boolean) {}
|
||||
|
||||
/**
|
||||
* Enable Virtual Background
|
||||
*/
|
||||
open fun enableVirtualBackground(enable: Boolean) {}
|
||||
|
||||
/**
|
||||
* Enable callee show banner view when received an new invitation
|
||||
* default: false
|
||||
*/
|
||||
open fun enableIncomingBanner(enable: Boolean) {}
|
||||
|
||||
/**
|
||||
* Set the display direction of the CallKit interface. The default value is portrait
|
||||
* @param orientation: 0-Portrait, 1-LandScape, 2-Auto; default value: 0
|
||||
* Note: You are advised to use portrait mode to avoid abnormal display for small screen devices such as mobile phone
|
||||
*/
|
||||
open fun setScreenOrientation(orientation: Int) {}
|
||||
|
||||
/**
|
||||
* Call experimental API
|
||||
*
|
||||
* @param jsonStr
|
||||
*/
|
||||
open fun callExperimentalAPI(jsonStr: String) {}
|
||||
|
||||
|
||||
/**
|
||||
* Make a call
|
||||
*
|
||||
* @param userId callees
|
||||
* @param callMediaType Call type
|
||||
*/
|
||||
@Deprecated("Use NewInterface instead", ReplaceWith("calls"))
|
||||
open fun call(userId: String, callMediaType: TUICallDefine.MediaType) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a call
|
||||
*
|
||||
* @param userId callees
|
||||
* @param callMediaType Call type
|
||||
* @param params Extension param: eg: offlinePushInfo
|
||||
*/
|
||||
@Deprecated("Use NewInterface instead", ReplaceWith("calls"))
|
||||
open fun call(
|
||||
userId: String, callMediaType: TUICallDefine.MediaType,
|
||||
params: CallParams?, callback: TUICommonDefine.Callback?
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a group call
|
||||
*
|
||||
* @param groupId GroupId
|
||||
* @param userIdList List of userId
|
||||
* @param callMediaType Call type
|
||||
*/
|
||||
@Deprecated("Use NewInterface instead", ReplaceWith("calls"))
|
||||
open fun groupCall(groupId: String, userIdList: List<String?>?, callMediaType: TUICallDefine.MediaType) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a group call
|
||||
*
|
||||
* @param groupId GroupId
|
||||
* @param userIdList List of userId
|
||||
* @param callMediaType Call type
|
||||
* @param params Extension param: eg: offlinePushInfo
|
||||
*/
|
||||
@Deprecated("Use NewInterface instead", ReplaceWith("calls"))
|
||||
open fun groupCall(
|
||||
groupId: String, userIdList: List<String?>?,
|
||||
callMediaType: TUICallDefine.MediaType, params: CallParams?,
|
||||
callback: TUICommonDefine.Callback?
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Join a current call
|
||||
*
|
||||
* @param roomId current call room ID
|
||||
* @param callMediaType call type
|
||||
*/
|
||||
@Deprecated("Use NewInterface instead", ReplaceWith("join"))
|
||||
open fun joinInGroupCall(roomId: RoomId?, groupId: String?, callMediaType: TUICallDefine.MediaType?) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,466 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallDefine
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallDefine.CallParams
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallEngine
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallObserver
|
||||
import com.tencent.cloud.tuikit.engine.common.TUICommonDefine
|
||||
import com.tencent.cloud.tuikit.engine.common.TUICommonDefine.Callback
|
||||
import com.tencent.cloud.tuikit.engine.common.TUICommonDefine.RoomId
|
||||
import com.tencent.qcloud.tuicore.TUIConstants
|
||||
import com.tencent.qcloud.tuicore.TUICore
|
||||
import com.tencent.qcloud.tuicore.permission.PermissionCallback
|
||||
import com.tencent.qcloud.tuicore.permission.PermissionRequester
|
||||
import com.tencent.qcloud.tuicore.util.ToastUtil
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.common.config.OfflinePushInfoConfig
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.common.data.Constants
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.common.data.Logger
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.common.utils.DeviceUtils
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.common.utils.PermissionRequest
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.manager.CallManager
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.manager.PushManager
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.manager.UserManager
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.state.GlobalState
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.state.UserState
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.state.ViewState
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.view.CallMainActivity
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.view.component.floatwindow.FloatWindowView
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.view.component.incomingbanner.IncomingFloatBanner
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.view.component.incomingbanner.IncomingNotificationBanner
|
||||
import com.trtc.tuikit.common.livedata.Observer
|
||||
import com.trtc.tuikit.common.ui.floatwindow.FloatWindowManager
|
||||
import com.trtc.tuikit.common.ui.floatwindow.FloatWindowObserver
|
||||
|
||||
|
||||
class TUICallKitImpl private constructor(context: Context) : TUICallKit() {
|
||||
private val context = context.applicationContext
|
||||
|
||||
private val callStatusObserver = Observer<TUICallDefine.Status> {
|
||||
showAntiFraudReminder()
|
||||
|
||||
if (it != TUICallDefine.Status.Waiting
|
||||
|| CallManager.instance.userState.selfUser.get().callRole == TUICallDefine.Role.Caller) {
|
||||
return@Observer
|
||||
}
|
||||
handleNewCall()
|
||||
}
|
||||
|
||||
private val floatWindowObserver = object : FloatWindowObserver() {
|
||||
override fun onFloatWindowClick() {
|
||||
startCallActivity()
|
||||
FloatWindowManager.sharedInstance().dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private val callObserver = object : TUICallObserver() {
|
||||
override fun onUserReject(userId: String?) {
|
||||
if (TUICallDefine.Scene.SINGLE_CALL == CallManager.instance.callState.scene.get()) {
|
||||
ToastUtil.toastShortMessage(context.getString(R.string.tuicallkit_toast_callee_reject))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onUserLineBusy(userId: String?) {
|
||||
ToastUtil.toastShortMessage(context.getString(R.string.tuicallkit_text_line_busy))
|
||||
}
|
||||
|
||||
override fun onUserNoResponse(userId: String?) {
|
||||
if (TUICallDefine.Scene.SINGLE_CALL == CallManager.instance.callState.scene.get()) {
|
||||
ToastUtil.toastShortMessage(context.getString(R.string.tuicallkit_toast_callee_no_response))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onUserLeave(userId: String?) {
|
||||
if (TUICallDefine.Scene.SINGLE_CALL == CallManager.instance.callState.scene.get()) {
|
||||
ToastUtil.toastShortMessage(context.getString(R.string.tuicallkit_toast_callee_hangup))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
registerObserver()
|
||||
registerEvent()
|
||||
}
|
||||
|
||||
override fun setSelfInfo(nickname: String?, avatar: String?, callback: Callback?) {
|
||||
CallManager.instance.setSelfInfo(nickname, avatar, callback)
|
||||
}
|
||||
|
||||
override fun calls(
|
||||
userIdList: List<String?>?, mediaType: TUICallDefine.MediaType, params: CallParams?, callback: Callback?
|
||||
) {
|
||||
PermissionRequest.requestPermissions(context, mediaType, object : PermissionCallback() {
|
||||
override fun onGranted() {
|
||||
CallManager.instance.calls(userIdList, mediaType, createDefaultCallParams(params), object : Callback {
|
||||
override fun onSuccess() {
|
||||
startCallActivity()
|
||||
callback?.onSuccess()
|
||||
}
|
||||
|
||||
override fun onError(errCode: Int, errMsg: String?) {
|
||||
callback?.onError(errCode, errMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onDenied() {
|
||||
Logger.w(TAG, "calls, request Permissions failed")
|
||||
callback?.onError(TUICallDefine.ERROR_PERMISSION_DENIED, "request Permissions failed")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun join(callId: String?, callback: Callback?) {
|
||||
PermissionRequest.requestPermissions(context, TUICallDefine.MediaType.Audio, object : PermissionCallback() {
|
||||
override fun onGranted() {
|
||||
CallManager.instance.join(callId, object : Callback {
|
||||
override fun onSuccess() {
|
||||
startCallActivity()
|
||||
callback?.onSuccess()
|
||||
}
|
||||
|
||||
override fun onError(errCode: Int, errMsg: String?) {
|
||||
callback?.onError(errCode, errMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onDenied() {
|
||||
Logger.w(TAG, "join, request Permissions failed")
|
||||
callback?.onError(TUICallDefine.ERROR_PERMISSION_DENIED, "request Permissions failed")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun callExperimentalAPI(jsonStr: String) {
|
||||
CallManager.instance.callExperimentalAPI(jsonStr)
|
||||
}
|
||||
|
||||
override fun setCallingBell(filePath: String?) {
|
||||
CallManager.instance.setCallingBell(filePath)
|
||||
}
|
||||
|
||||
override fun enableMuteMode(enable: Boolean) {
|
||||
CallManager.instance.enableMuteMode(enable)
|
||||
}
|
||||
|
||||
override fun enableFloatWindow(enable: Boolean) {
|
||||
CallManager.instance.enableFloatWindow(enable)
|
||||
}
|
||||
|
||||
override fun enableVirtualBackground(enable: Boolean) {
|
||||
CallManager.instance.enableVirtualBackground(enable)
|
||||
}
|
||||
|
||||
override fun enableIncomingBanner(enable: Boolean) {
|
||||
CallManager.instance.enableIncomingBanner(enable)
|
||||
}
|
||||
|
||||
override fun setScreenOrientation(orientation: Int) {
|
||||
CallManager.instance.setScreenOrientation(orientation)
|
||||
}
|
||||
|
||||
override fun call(userId: String, callMediaType: TUICallDefine.MediaType) {
|
||||
Logger.i(TAG, "call, userId: $userId, mediaType: $callMediaType")
|
||||
val params = CallParams()
|
||||
params.offlinePushInfo = OfflinePushInfoConfig.createOfflinePushInfo(context)
|
||||
params.timeout = Constants.CALL_WAITING_MAX_TIME
|
||||
call(userId, callMediaType, params, null)
|
||||
}
|
||||
|
||||
override fun call(userId: String, mediaType: TUICallDefine.MediaType, params: CallParams?, callback: Callback?) {
|
||||
PermissionRequest.requestPermissions(context, mediaType, object : PermissionCallback() {
|
||||
override fun onGranted() {
|
||||
CallManager.instance.call(userId, mediaType, createDefaultCallParams(params), object : Callback {
|
||||
override fun onSuccess() {
|
||||
startCallActivity()
|
||||
callback?.onSuccess()
|
||||
}
|
||||
|
||||
override fun onError(errCode: Int, errMsg: String?) {
|
||||
callback?.onError(errCode, errMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onDenied() {
|
||||
Logger.w(TAG, "call, request Permissions failed")
|
||||
callback?.onError(TUICallDefine.ERROR_PERMISSION_DENIED, "request Permissions failed")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun groupCall(groupId: String, userIdList: List<String?>?, mediaType: TUICallDefine.MediaType) {
|
||||
val params = CallParams()
|
||||
params.offlinePushInfo = OfflinePushInfoConfig.createOfflinePushInfo(context)
|
||||
params.timeout = Constants.CALL_WAITING_MAX_TIME
|
||||
groupCall(groupId, userIdList, mediaType, params, null)
|
||||
}
|
||||
|
||||
override fun groupCall(
|
||||
groupId: String, userIdList: List<String?>?, mediaType: TUICallDefine.MediaType,
|
||||
params: CallParams?, callback: Callback?
|
||||
) {
|
||||
PermissionRequest.requestPermissions(context, mediaType, object : PermissionCallback() {
|
||||
override fun onGranted() {
|
||||
CallManager.instance.groupCall(groupId, userIdList, mediaType, createDefaultCallParams(params),
|
||||
object : Callback {
|
||||
override fun onSuccess() {
|
||||
startCallActivity()
|
||||
callback?.onSuccess()
|
||||
}
|
||||
|
||||
override fun onError(errCode: Int, errMsg: String?) {
|
||||
callback?.onError(errCode, errMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onDenied() {
|
||||
Logger.w(TAG, "groupCall, request Permissions failed")
|
||||
callback?.onError(TUICallDefine.ERROR_PERMISSION_DENIED, "request Permissions failed")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun joinInGroupCall(roomId: RoomId?, groupId: String?, mediaType: TUICallDefine.MediaType?) {
|
||||
PermissionRequest.requestPermissions(context, mediaType!!, object : PermissionCallback() {
|
||||
override fun onGranted() {
|
||||
CallManager.instance.joinInGroupCall(roomId, groupId, mediaType, object : Callback {
|
||||
override fun onSuccess() {
|
||||
startCallActivity()
|
||||
}
|
||||
|
||||
override fun onError(errCode: Int, errMsg: String?) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onDenied() {
|
||||
Logger.w(TAG, "joinInGroupCall, request Permissions failed")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
fun queryOfflineCall() {
|
||||
if (FloatWindowManager.sharedInstance().isShowing) {
|
||||
Logger.w(TAG, "queryOfflineCall, float window is showing")
|
||||
return
|
||||
}
|
||||
|
||||
Logger.i(TAG, "queryOfflineCall start")
|
||||
val selfUser = CallManager.instance.userState.selfUser.get()
|
||||
if (TUICallDefine.Status.Accept == selfUser.callStatus.get()) {
|
||||
return
|
||||
}
|
||||
|
||||
val role = selfUser.callRole
|
||||
val mediaType = CallManager.instance.callState.mediaType.get()
|
||||
if (TUICallDefine.Role.None == role || TUICallDefine.MediaType.Unknown == mediaType) {
|
||||
Logger.w(TAG, "queryOfflineCall, current status is Unknown")
|
||||
return
|
||||
}
|
||||
|
||||
//The received call has been processed in #onCallReceived
|
||||
if (TUICallDefine.Role.Called == role && PermissionRequester.newInstance(PermissionRequester.BG_START_PERMISSION)
|
||||
.has()
|
||||
) {
|
||||
return
|
||||
}
|
||||
startCallActivity()
|
||||
}
|
||||
|
||||
private fun handleNewCall() {
|
||||
val floatPermission = PermissionRequester.newInstance(PermissionRequester.FLOAT_PERMISSION).has()
|
||||
val isAppInBackground = !DeviceUtils.isAppRunningForeground(context)
|
||||
val bgPermission = PermissionRequester.newInstance(PermissionRequester.BG_START_PERMISSION).has()
|
||||
val notificationPermission = PermissionRequest.isNotificationEnabled()
|
||||
|
||||
val enableIncomingBanner = GlobalState.instance.enableIncomingBanner
|
||||
val pushData = PushManager.getPushData()
|
||||
val isFCMDataChannel = pushData.channelId == TUIConstants.DeviceInfo.BRAND_GOOGLE_ELSE
|
||||
|
||||
Logger.i(
|
||||
TAG, "handleNewCall, isAppInBackground: $isAppInBackground, floatPermission: $floatPermission" +
|
||||
", backgroundStartPermission: $bgPermission, notificationPermission: $notificationPermission , " +
|
||||
"pushData: $pushData, enableIncomingBanner:$enableIncomingBanner"
|
||||
)
|
||||
|
||||
if (DeviceUtils.isScreenLocked(context)) {
|
||||
handleScreenLocked(isAppInBackground, isFCMDataChannel, notificationPermission)
|
||||
return
|
||||
}
|
||||
|
||||
if (isAppInBackground) {
|
||||
handleAppInBackground(floatPermission, isFCMDataChannel, notificationPermission, bgPermission)
|
||||
return
|
||||
}
|
||||
|
||||
if (enableIncomingBanner) {
|
||||
when {
|
||||
floatPermission -> startSmallScreenView(IncomingFloatBanner(context))
|
||||
notificationPermission -> startSmallScreenView(IncomingNotificationBanner(context))
|
||||
else -> startFullScreenView()
|
||||
}
|
||||
} else {
|
||||
startFullScreenView()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleScreenLocked(appInBackground: Boolean, fcmData: Boolean, notificationPermission: Boolean) {
|
||||
Logger.i(TAG, "handleScreenLocked, screen is locked")
|
||||
if (appInBackground && fcmData && notificationPermission) {
|
||||
startSmallScreenView(IncomingNotificationBanner(context))
|
||||
} else {
|
||||
startFullScreenView()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAppInBackground(
|
||||
floatPermission: Boolean, fcmData: Boolean,
|
||||
notificationPermission: Boolean, bgPermission: Boolean
|
||||
) {
|
||||
when {
|
||||
floatPermission -> startSmallScreenView(IncomingFloatBanner(context))
|
||||
fcmData && notificationPermission -> startSmallScreenView(IncomingNotificationBanner(context))
|
||||
bgPermission -> startFullScreenView()
|
||||
else -> Logger.w(TAG, "App is in background with no permission")
|
||||
}
|
||||
}
|
||||
|
||||
private fun startFullScreenView() {
|
||||
Logger.i(TAG, "startFullScreenView")
|
||||
if (CallManager.instance.userState.selfUser.get().callStatus.get() == TUICallDefine.Status.None) {
|
||||
Logger.i(TAG, "startFullScreenView, current status: None, ignore")
|
||||
return
|
||||
}
|
||||
PermissionRequest.requestPermissions(context, CallManager.instance.callState.mediaType.get(), object
|
||||
: PermissionCallback() {
|
||||
override fun onGranted() {
|
||||
if (TUICallDefine.Status.None != CallManager.instance.userState.selfUser.get().callStatus.get()) {
|
||||
Logger.i(TAG, "startFullScreenView requestPermissions onGranted")
|
||||
startCallActivity()
|
||||
} else {
|
||||
CallManager.instance.reset()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDenied() {
|
||||
if (CallManager.instance.userState.selfUser.get().callRole == TUICallDefine.Role.Called) {
|
||||
CallManager.instance.reject(null)
|
||||
}
|
||||
CallManager.instance.reset()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun startSmallScreenView(view: Any) {
|
||||
var caller = CallManager.instance.userState.selfUser.get()
|
||||
for (user in CallManager.instance.userState.remoteUserList.get()) {
|
||||
if (user.callRole == TUICallDefine.Role.Caller) {
|
||||
caller = user
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
val list = ArrayList<String?>()
|
||||
list.add(caller.id)
|
||||
UserManager.instance.updateUserListInfo(list, object : TUICommonDefine.ValueCallback<List<UserState.User>?> {
|
||||
override fun onSuccess(data: List<UserState.User>?) {
|
||||
if (CallManager.instance.userState.selfUser.get().callStatus.get() == TUICallDefine.Status.None) {
|
||||
Logger.w(TAG, "startSmallScreenView, current status: None, ignore")
|
||||
return
|
||||
}
|
||||
caller.avatar.set(data!![0].avatar.get())
|
||||
caller.nickname.set(data[0].nickname.get())
|
||||
|
||||
if (view is IncomingFloatBanner) {
|
||||
view.showIncomingView(caller)
|
||||
} else if (view is IncomingNotificationBanner) {
|
||||
view.showNotification(caller)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(errCode: Int, errMsg: String?) {
|
||||
if (view is IncomingFloatBanner) {
|
||||
view.showIncomingView(caller)
|
||||
} else if (view is IncomingNotificationBanner) {
|
||||
view.showNotification(caller)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun registerObserver() {
|
||||
TUICallEngine.createInstance(context).addObserver(callObserver)
|
||||
CallManager.instance.userState.selfUser.get().callStatus.observe(callStatusObserver)
|
||||
}
|
||||
|
||||
private fun registerEvent() {
|
||||
TUICore.registerEvent(Constants.KEY_TUI_CALLKIT, Constants.SUB_KEY_SHOW_FLOAT_WINDOW) { key, subKey, _ ->
|
||||
startFloatWindow()
|
||||
}
|
||||
}
|
||||
|
||||
private fun startFloatWindow() {
|
||||
if (FloatWindowManager.sharedInstance().isShowing) {
|
||||
Logger.w(TAG, "There is already a floatWindow on display, do not open it again.")
|
||||
return
|
||||
}
|
||||
FloatWindowManager.sharedInstance().addObserver(floatWindowObserver)
|
||||
|
||||
if (PermissionRequester.newInstance(PermissionRequester.FLOAT_PERMISSION).has()) {
|
||||
CallManager.instance.viewState.router.set(ViewState.ViewRouter.FloatView)
|
||||
FloatWindowManager.sharedInstance().show(FloatWindowView(context.applicationContext))
|
||||
} else {
|
||||
PermissionRequester.newInstance(PermissionRequester.FLOAT_PERMISSION).request()
|
||||
}
|
||||
}
|
||||
|
||||
private fun startCallActivity() {
|
||||
val intent = Intent(context, CallMainActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
private fun showAntiFraudReminder() {
|
||||
if (TUICallDefine.Status.Accept != CallManager.instance.userState.selfUser.get().callStatus.get()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (TUICore.getService(TUIConstants.Service.TUI_PRIVACY) == null) {
|
||||
return
|
||||
}
|
||||
val map = HashMap<String, Any?>()
|
||||
map[TUIConstants.Privacy.PARAM_DIALOG_CONTEXT] = context
|
||||
TUICore.callService(
|
||||
TUIConstants.Service.TUI_PRIVACY, TUIConstants.Privacy.METHOD_ANTO_FRAUD_REMINDER, map, null
|
||||
)
|
||||
}
|
||||
|
||||
private fun createDefaultCallParams(params: CallParams?): CallParams {
|
||||
val callParams = params ?: CallParams().apply {
|
||||
offlinePushInfo = OfflinePushInfoConfig.createOfflinePushInfo(context)
|
||||
timeout = Constants.CALL_WAITING_MAX_TIME
|
||||
}
|
||||
callParams.offlinePushInfo = callParams.offlinePushInfo ?: OfflinePushInfoConfig.createOfflinePushInfo(context)
|
||||
return callParams
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "IncomingView"
|
||||
private var instance: TUICallKitImpl? = null
|
||||
fun createInstance(context: Context): TUICallKitImpl {
|
||||
if (instance == null) {
|
||||
synchronized(TUICallKitImpl::class.java) {
|
||||
if (instance == null) {
|
||||
instance = TUICallKitImpl(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance!!
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.common.config
|
||||
|
||||
import android.content.Context
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallDefine
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallDefine.OfflinePushInfo
|
||||
import com.tencent.qcloud.tuicore.TUILogin
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.R
|
||||
|
||||
object OfflinePushInfoConfig {
|
||||
fun createOfflinePushInfo(context: Context): OfflinePushInfo {
|
||||
val pushInfo = OfflinePushInfo()
|
||||
pushInfo.title = if (TUILogin.getNickName().isNullOrEmpty()) TUILogin.getLoginUser() else TUILogin.getNickName()
|
||||
pushInfo.desc = context.getString(R.string.tuicallkit_have_a_new_call)
|
||||
//OPPO must set a ChannelID to receive push messages. If you set it on the console, you don't need set here.
|
||||
//pushInfo.androidOPPOChannelID = "tuikit"
|
||||
pushInfo.isIgnoreIOSBadge = false
|
||||
pushInfo.iosSound = "phone_ringing.mp3"
|
||||
pushInfo.androidSound = "phone_ringing"
|
||||
//FCM channel ID, If you set it on the console, you don't need set here.
|
||||
//pushInfo.androidFCMChannelID = "fcm_push_channel"
|
||||
//HuaWei message type: https://developer.huawei.com/consumer/cn/doc/development/HMSCore-Guides/message-classification-0000001149358835
|
||||
pushInfo.androidHuaWeiCategory = "IM"
|
||||
//IOS push type, if you want use VoIP, please modify type to TUICallDefine.IOSOfflinePushType.VoIP
|
||||
pushInfo.iosPushType = TUICallDefine.IOSOfflinePushType.APNs
|
||||
return pushInfo
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.common.data
|
||||
|
||||
object Constants {
|
||||
const val CALL_WAITING_MAX_TIME = 30 //unit:s
|
||||
const val MAX_USER = 9
|
||||
const val MIN_AUDIO_VOLUME = 10
|
||||
|
||||
const val REJECT_CALL_ACTION = "reject_call_action"
|
||||
const val ACCEPT_CALL_ACTION = "accept_call_action"
|
||||
|
||||
const val KEY_TUI_CALLKIT = "keyTUICallKit"
|
||||
const val SUB_KEY_SHOW_FLOAT_WINDOW = "subKeyShowFloatWindow"
|
||||
const val AI_TRANSLATION_ROBOT = "TAI_Robot"
|
||||
|
||||
/**
|
||||
* framework: native(1),uni-app(11),flutter(7),RN(22)
|
||||
* component: CallKit(14),Chat_CallKit(15)
|
||||
* language: Java(1),Kotlin(2),Swift(3),OC(4),Vue2(5),Vue3(6),React(7),C++(8),Dart(9),uts(10)
|
||||
*/
|
||||
const val CALL_FRAMEWORK_NATIVE: Int = 1
|
||||
const val CALL_COMPONENT: Int = 14
|
||||
const val CALL_COMPONENT_CHAT: Int = 15
|
||||
const val CALL_LANGUAGE_KOTLIN: Int = 2
|
||||
var framework: Int = CALL_FRAMEWORK_NATIVE
|
||||
var component: Int = CALL_COMPONENT
|
||||
var language: Int = CALL_LANGUAGE_KOTLIN
|
||||
|
||||
enum class NetworkQualityHint {
|
||||
None,
|
||||
Local,
|
||||
Remote
|
||||
}
|
||||
|
||||
enum class Orientation {
|
||||
Portrait,
|
||||
LandScape,
|
||||
Auto
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.common.data
|
||||
|
||||
import com.tencent.trtc.TRTCCloud
|
||||
import com.trtc.tuikit.common.system.ContextProvider
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
||||
object Logger {
|
||||
private const val API = "TuikitLog"
|
||||
private const val LOG_KEY_API = "api"
|
||||
private const val LOG_KEY_PARAMS = "params"
|
||||
private const val LOG_KEY_PARAMS_LEVEL = "level"
|
||||
private const val LOG_KEY_PARAMS_MESSAGE = "message"
|
||||
private const val LOG_KEY_PARAMS_FILE = "file"
|
||||
private const val LOG_KEY_PARAMS_MODULE = "module"
|
||||
private const val LOG_KEY_PARAMS_LINE = "line"
|
||||
private const val LOG_KEY_PARAMS_FILE_VALUE = "Logger"
|
||||
private const val LOG_KEY_PARAMS_MODULE_VALUE = "TUICallKit"
|
||||
private const val LOG_KEY_PARAMS_LINE_VALUE = 0
|
||||
private const val LOG_LEVEL_INFO = 0
|
||||
private const val LOG_LEVEL_WARNING = 1
|
||||
private const val LOG_LEVEL_ERROR = 2
|
||||
|
||||
fun e(tag: String, message: String) {
|
||||
error(tag, message, LOG_KEY_PARAMS_MODULE_VALUE, LOG_KEY_PARAMS_FILE_VALUE, LOG_KEY_PARAMS_LINE_VALUE)
|
||||
}
|
||||
|
||||
fun w(tag: String, message: String) {
|
||||
warn(tag, message, LOG_KEY_PARAMS_MODULE_VALUE, LOG_KEY_PARAMS_FILE_VALUE, LOG_KEY_PARAMS_LINE_VALUE)
|
||||
}
|
||||
|
||||
fun i(tag: String, message: String) {
|
||||
info(tag, message, LOG_KEY_PARAMS_MODULE_VALUE, LOG_KEY_PARAMS_FILE_VALUE, LOG_KEY_PARAMS_LINE_VALUE)
|
||||
}
|
||||
|
||||
private fun error(tag: String, message: String, module: String, file: String, line: Int) {
|
||||
log(tag, message, LOG_LEVEL_ERROR, module, file, line)
|
||||
}
|
||||
|
||||
private fun warn(tag: String, message: String, module: String, file: String, line: Int) {
|
||||
log(tag, message, LOG_LEVEL_WARNING, module, file, line)
|
||||
}
|
||||
|
||||
private fun info(tag: String, message: String, module: String, file: String, line: Int) {
|
||||
log(tag, message, LOG_LEVEL_INFO, module, file, line)
|
||||
}
|
||||
|
||||
private fun log(tag: String, message: String, level: Int, module: String, file: String, line: Int) {
|
||||
val builder = StringBuilder().apply { append(tag).append(", ").append(message) }
|
||||
try {
|
||||
val paramsJson = JSONObject()
|
||||
paramsJson.put(LOG_KEY_PARAMS_LEVEL, level)
|
||||
paramsJson.put(LOG_KEY_PARAMS_MESSAGE, builder.toString())
|
||||
paramsJson.put(LOG_KEY_PARAMS_MODULE, module)
|
||||
paramsJson.put(LOG_KEY_PARAMS_FILE, file)
|
||||
paramsJson.put(LOG_KEY_PARAMS_LINE, line)
|
||||
|
||||
val loggerJson = JSONObject()
|
||||
loggerJson.put(LOG_KEY_API, API)
|
||||
loggerJson.put(LOG_KEY_PARAMS, paramsJson)
|
||||
|
||||
TRTCCloud.sharedInstance(ContextProvider.getApplicationContext())
|
||||
.callExperimentalAPI(loggerJson.toString())
|
||||
} catch (e: JSONException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.common.utils
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.app.KeyguardManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.view.Window
|
||||
import android.view.WindowManager
|
||||
import com.tencent.qcloud.tuicore.util.TUIBuild
|
||||
|
||||
object DeviceUtils {
|
||||
fun setScreenLockParams(window: Window) {
|
||||
window.addFlags(
|
||||
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
|
||||
or WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
|
||||
)
|
||||
}
|
||||
|
||||
fun isScreenLocked(context: Context): Boolean {
|
||||
val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
||||
return if (TUIBuild.getVersionInt() >= Build.VERSION_CODES.LOLLIPOP_MR1) {
|
||||
keyguardManager.isDeviceLocked()
|
||||
} else {
|
||||
keyguardManager.inKeyguardRestrictedInputMode()
|
||||
}
|
||||
}
|
||||
|
||||
fun isAppRunningForeground(context: Context): Boolean {
|
||||
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager?
|
||||
val runningAppProcessInfos = activityManager?.runningAppProcesses ?: return false
|
||||
val packageName = context.packageName
|
||||
for (appProcessInfo in runningAppProcessInfos) {
|
||||
if (appProcessInfo.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
|
||||
&& appProcessInfo.processName == packageName) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.common.utils
|
||||
|
||||
import android.Manifest
|
||||
import android.app.AppOpsManager
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallDefine
|
||||
import com.tencent.qcloud.tuicore.TUIConfig
|
||||
import com.tencent.qcloud.tuicore.TUIConstants
|
||||
import com.tencent.qcloud.tuicore.TUICore
|
||||
import com.tencent.qcloud.tuicore.permission.PermissionCallback
|
||||
import com.tencent.qcloud.tuicore.permission.PermissionRequester
|
||||
import com.tencent.qcloud.tuicore.util.TUIBuild
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.R
|
||||
|
||||
object PermissionRequest {
|
||||
fun requestPermissions(context: Context, type: TUICallDefine.MediaType, callback: PermissionCallback?) {
|
||||
val title = StringBuilder().append(context.getString(R.string.tuicallkit_permission_microphone))
|
||||
val reason = StringBuilder()
|
||||
reason.append(getMicrophonePermissionHint(context))
|
||||
|
||||
val permissionList: MutableList<String> = ArrayList()
|
||||
permissionList.add(Manifest.permission.RECORD_AUDIO)
|
||||
if (TUICallDefine.MediaType.Video == type) {
|
||||
title.append(context.getString(R.string.tuicallkit_permission_separator))
|
||||
title.append(context.getString(R.string.tuicallkit_permission_camera))
|
||||
reason.append(getCameraPermissionHint(context))
|
||||
permissionList.add(Manifest.permission.CAMERA)
|
||||
}
|
||||
|
||||
if (PermissionRequester.newInstance(*permissionList.toTypedArray()).has()) {
|
||||
callback?.onGranted()
|
||||
return
|
||||
}
|
||||
|
||||
val permissionCallback: PermissionCallback = object : PermissionCallback() {
|
||||
override fun onGranted() {
|
||||
requestBluetoothPermission(context, object : PermissionCallback() {
|
||||
override fun onGranted() {
|
||||
callback?.onGranted()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onDenied() {
|
||||
super.onDenied()
|
||||
callback?.onDenied()
|
||||
}
|
||||
}
|
||||
val applicationInfo = context.applicationInfo
|
||||
val appName = context.packageManager.getApplicationLabel(applicationInfo).toString()
|
||||
PermissionRequester.newInstance(*permissionList.toTypedArray())
|
||||
.title(context.getString(R.string.tuicallkit_permission_title, appName, title))
|
||||
.description(reason.toString())
|
||||
.settingsTip("${context.getString(R.string.tuicallkit_permission_tips, title)} $reason".trimIndent())
|
||||
.callback(permissionCallback)
|
||||
.request()
|
||||
}
|
||||
|
||||
fun requestCameraPermission(context: Context, callback: PermissionCallback?) {
|
||||
if (PermissionRequester.newInstance(Manifest.permission.CAMERA).has()) {
|
||||
callback?.onGranted()
|
||||
return
|
||||
}
|
||||
|
||||
val permissionCallback: PermissionCallback = object : PermissionCallback() {
|
||||
override fun onGranted() {
|
||||
callback?.onGranted()
|
||||
}
|
||||
|
||||
override fun onDenied() {
|
||||
super.onDenied()
|
||||
callback?.onDenied()
|
||||
}
|
||||
}
|
||||
|
||||
val title = context.getString(R.string.tuicallkit_permission_camera)
|
||||
val reason = getCameraPermissionHint(context)
|
||||
val appName = context.packageManager.getApplicationLabel(context.applicationInfo).toString()
|
||||
|
||||
PermissionRequester.newInstance(Manifest.permission.CAMERA)
|
||||
.title(context.getString(R.string.tuicallkit_permission_title, appName, title))
|
||||
.description(reason)
|
||||
.settingsTip("${context.getString(R.string.tuicallkit_permission_tips, title)} $reason".trimIndent())
|
||||
.callback(permissionCallback)
|
||||
.request()
|
||||
}
|
||||
|
||||
/**
|
||||
* Android S(31) need apply for Nearby devices(Bluetooth) permission to support bluetooth headsets.
|
||||
* Please refer to: https://developer.android.com/guide/topics/connectivity/bluetooth/permissions
|
||||
*/
|
||||
private fun requestBluetoothPermission(context: Context, callback: PermissionCallback) {
|
||||
if (TUIBuild.getVersionInt() < Build.VERSION_CODES.S) {
|
||||
callback.onGranted()
|
||||
return
|
||||
}
|
||||
if (PermissionRequester.newInstance(Manifest.permission.BLUETOOTH_CONNECT).has()) {
|
||||
callback.onGranted()
|
||||
return
|
||||
}
|
||||
|
||||
val title = context.getString(R.string.tuicallkit_permission_bluetooth)
|
||||
val reason = context.getString(R.string.tuicallkit_permission_bluetooth_reason)
|
||||
val applicationInfo = context.applicationInfo
|
||||
val appName = context.packageManager.getApplicationLabel(applicationInfo).toString()
|
||||
PermissionRequester.newInstance(Manifest.permission.BLUETOOTH_CONNECT)
|
||||
.title(context.getString(R.string.tuicallkit_permission_title, appName, title))
|
||||
.description(reason)
|
||||
.settingsTip(reason)
|
||||
.callback(object : PermissionCallback() {
|
||||
override fun onGranted() {
|
||||
callback.onGranted()
|
||||
}
|
||||
|
||||
override fun onDenied() {
|
||||
super.onDenied()
|
||||
//bluetooth is unnecessary permission, return permission granted
|
||||
callback.onGranted()
|
||||
}
|
||||
})
|
||||
.request()
|
||||
}
|
||||
|
||||
private fun getMicrophonePermissionHint(context: Context): String {
|
||||
val microphonePermissionsDescription = TUICore.createObject(
|
||||
TUIConstants.Privacy.PermissionsFactory.FACTORY_NAME,
|
||||
TUIConstants.Privacy.PermissionsFactory.PermissionsName.MICROPHONE_PERMISSIONS, null
|
||||
) as String?
|
||||
return if (!microphonePermissionsDescription.isNullOrEmpty()) {
|
||||
microphonePermissionsDescription
|
||||
} else {
|
||||
context.getString(R.string.tuicallkit_permission_mic_reason)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCameraPermissionHint(context: Context): String {
|
||||
val cameraPermissionsDescription = TUICore.createObject(
|
||||
TUIConstants.Privacy.PermissionsFactory.FACTORY_NAME,
|
||||
TUIConstants.Privacy.PermissionsFactory.PermissionsName.CAMERA_PERMISSIONS, null
|
||||
) as String?
|
||||
return if (!cameraPermissionsDescription.isNullOrEmpty()) {
|
||||
cameraPermissionsDescription
|
||||
} else {
|
||||
context.getString(R.string.tuicallkit_permission_camera_reason)
|
||||
}
|
||||
}
|
||||
|
||||
fun isNotificationEnabled(): Boolean {
|
||||
val context = TUIConfig.getAppContext()
|
||||
if (TUIBuild.getVersionInt() >= Build.VERSION_CODES.O) {
|
||||
// For Android Oreo and above
|
||||
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
return manager.areNotificationsEnabled()
|
||||
}
|
||||
if (TUIBuild.getVersionInt() >= Build.VERSION_CODES.KITKAT) {
|
||||
// For versions prior to Android Oreo
|
||||
var appOps: AppOpsManager = context.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager
|
||||
val appInfo = context.applicationInfo
|
||||
val packageName = context.applicationContext.packageName
|
||||
val uid = appInfo.uid
|
||||
try {
|
||||
var appOpsClass: Class<*> = Class.forName(AppOpsManager::class.java.name)
|
||||
val checkOpNoThrowMethod = appOpsClass.getMethod(
|
||||
"checkOpNoThrow", Integer.TYPE, Integer.TYPE, String::class.java
|
||||
)
|
||||
val opPostNotificationValue = appOpsClass.getDeclaredField("OP_POST_NOTIFICATION")
|
||||
val value = opPostNotificationValue[Int::class.java] as Int
|
||||
return checkOpNoThrowMethod.invoke(appOps, value, uid, packageName) as Int == AppOpsManager.MODE_ALLOWED
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.debug
|
||||
|
||||
import android.util.Base64
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import java.nio.charset.Charset
|
||||
import java.util.Arrays
|
||||
import java.util.zip.Deflater
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
/*
|
||||
* Description: Generates UserSig for testing. UserSig is a security signature designed
|
||||
* by Tencent Cloud for its cloud services.
|
||||
* It is calculated based on `SDKAppID`, `UserID`,
|
||||
* and `EXPIRETIME` using the HMAC-SHA256 encryption algorithm.
|
||||
*
|
||||
* Attention: For the following reasons, do not use the code below in your commercial application.
|
||||
*
|
||||
* The code may be able to calculate UserSig correctly, b
|
||||
* ut it is only for quick testing of the SDK’s basic features,
|
||||
* not for commercial applications.
|
||||
* `SECRETKEY` in client code can be easily decompiled and reversed, especially on web.
|
||||
* Once your key is disclosed, attackers will be able to steal your Tencent Cloud traffic.
|
||||
*
|
||||
* The correct method is to deploy the `UserSig` calculation code and encryption key on your server
|
||||
* so that your application can request a `UserSig` from your server,
|
||||
* which will calculate it whenever one is needed.
|
||||
* Given that it is more difficult to hack a server than a client application,
|
||||
* server-end calculation can better protect your key.
|
||||
*
|
||||
* Reference: https://www.tencentcloud.com/document/product/1047/34385
|
||||
*/
|
||||
object GenerateTestUserSig {
|
||||
/**
|
||||
* Signature validity period, which should not be set too short
|
||||
*
|
||||
*
|
||||
* Unit: Second
|
||||
* Default value: 7 x 24 x 60 x 60 = 604800 (seven days)
|
||||
*/
|
||||
private const val EXPIRETIME = 604800
|
||||
|
||||
/**
|
||||
* Calculating UserSig
|
||||
*
|
||||
*
|
||||
* The asymmetric encryption algorithm HMAC-SHA256 is used in the
|
||||
* function to calculate UserSig based on `SDKAppID`, `UserID`, and `EXPIRETIME`.
|
||||
*
|
||||
* @note: For the following reasons, do not use the code below in your commercial application.
|
||||
*
|
||||
*
|
||||
* The code may be able to calculate UserSig correctly,
|
||||
* but it is only for quick testing of the SDK’s basic features,
|
||||
* not for commercial applications.
|
||||
* SECRETKEY in client code can be easily decompiled and reversed, especially on web.
|
||||
* Once your key is disclosed, attackers will be able to steal your Tencent Cloud traffic.
|
||||
*
|
||||
*
|
||||
* The correct method is to deploy the `UserSig` calculation code and encryption key on your server
|
||||
* so that your application can request a `UserSig` from your server,
|
||||
* which will calculate it whenever one is needed.
|
||||
* Given that it is more difficult to hack a server than a client application,
|
||||
* server-end calculation can better protect your key.
|
||||
*
|
||||
*
|
||||
* Documentation: https://www.tencentcloud.com/document/product/1047/34385
|
||||
*/
|
||||
@JvmStatic
|
||||
fun genTestUserSig(userId: String, sdkAppId: Int, secretKey: String): String {
|
||||
return genTLSSignature(sdkAppId.toLong(), userId, EXPIRETIME.toLong(), null, secretKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generating a TLS Ticket
|
||||
*
|
||||
* @param sdkappId `appid` of your application
|
||||
* @param userId User ID
|
||||
* @param expire Validity period in seconds
|
||||
* @param userbuf `null` by default
|
||||
* @param priKeyContent Private key required for generating a TLS ticket
|
||||
* @return If an error occurs, an empty string will be returned or exceptions printed.
|
||||
* If the operation succeeds, a valid ticket will be returned.
|
||||
*/
|
||||
private fun genTLSSignature(
|
||||
sdkappId: Long, userId: String, expire: Long, userbuf: ByteArray?, priKeyContent: String
|
||||
): String {
|
||||
if (priKeyContent.isNullOrEmpty()) {
|
||||
return ""
|
||||
}
|
||||
val currTime = System.currentTimeMillis() / 1000
|
||||
val sigDoc = JSONObject()
|
||||
try {
|
||||
sigDoc.put("TLS.ver", "2.0")
|
||||
sigDoc.put("TLS.identifier", userId)
|
||||
sigDoc.put("TLS.sdkappid", sdkappId)
|
||||
sigDoc.put("TLS.expire", expire)
|
||||
sigDoc.put("TLS.time", currTime)
|
||||
} catch (e: JSONException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
var base64UserBuf: String? = null
|
||||
if (null != userbuf) {
|
||||
base64UserBuf = Base64.encodeToString(userbuf, Base64.NO_WRAP)
|
||||
try {
|
||||
sigDoc.put("TLS.userbuf", base64UserBuf)
|
||||
} catch (e: JSONException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
val sig = hmacsha256(sdkappId, userId, currTime, expire, priKeyContent, base64UserBuf)
|
||||
if (sig.length == 0) {
|
||||
return ""
|
||||
}
|
||||
try {
|
||||
sigDoc.put("TLS.sig", sig)
|
||||
} catch (e: JSONException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
val compressor = Deflater()
|
||||
compressor.setInput(sigDoc.toString().toByteArray(Charset.forName("UTF-8")))
|
||||
compressor.finish()
|
||||
val compressedBytes = ByteArray(2048)
|
||||
val compressedBytesLength = compressor.deflate(compressedBytes)
|
||||
compressor.end()
|
||||
return String(base64EncodeUrl(Arrays.copyOfRange(compressedBytes, 0, compressedBytesLength)))
|
||||
}
|
||||
|
||||
|
||||
private fun hmacsha256(
|
||||
sdkappid: Long, userId: String, currTime: Long, expire: Long, priKeyContent: String,
|
||||
base64Userbuf: String?
|
||||
): String {
|
||||
var contentToBeSigned = """
|
||||
TLS.identifier:$userId
|
||||
TLS.sdkappid:$sdkappid
|
||||
TLS.time:$currTime
|
||||
TLS.expire:$expire
|
||||
|
||||
""".trimIndent()
|
||||
if (null != base64Userbuf) {
|
||||
contentToBeSigned += "TLS.userbuf:$base64Userbuf\n"
|
||||
}
|
||||
try {
|
||||
val byteKey = priKeyContent.toByteArray(charset("UTF-8"))
|
||||
val hmac = Mac.getInstance("HmacSHA256")
|
||||
val keySpec = SecretKeySpec(byteKey, "HmacSHA256")
|
||||
hmac.init(keySpec)
|
||||
val byteSig = hmac.doFinal(contentToBeSigned.toByteArray(charset("UTF-8")))
|
||||
return String(Base64.encode(byteSig, Base64.NO_WRAP))
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
private fun base64EncodeUrl(input: ByteArray): ByteArray {
|
||||
val base64 = String(Base64.encode(input, Base64.NO_WRAP)).toByteArray()
|
||||
for (i in base64.indices) {
|
||||
when (base64[i]) {
|
||||
'+'.code.toByte() -> base64[i] = '*'.code.toByte()
|
||||
'/'.code.toByte() -> base64[i] = '-'.code.toByte()
|
||||
'='.code.toByte() -> base64[i] = '_'.code.toByte()
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
return base64
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,676 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.manager
|
||||
|
||||
import android.content.Context
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallDefine
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallEngine
|
||||
import com.tencent.cloud.tuikit.engine.common.TUICommonDefine
|
||||
import com.tencent.imsdk.BaseConstants
|
||||
import com.tencent.qcloud.tuicore.TUIConfig
|
||||
import com.tencent.qcloud.tuicore.TUIConstants
|
||||
import com.tencent.qcloud.tuicore.TUICore
|
||||
import com.tencent.qcloud.tuicore.TUILogin
|
||||
import com.tencent.qcloud.tuicore.interfaces.ITUINotification
|
||||
import com.tencent.qcloud.tuicore.permission.PermissionCallback
|
||||
import com.tencent.qcloud.tuicore.util.ErrorMessageConverter
|
||||
import com.tencent.qcloud.tuicore.util.SPUtils
|
||||
import com.tencent.qcloud.tuicore.util.ToastUtil
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.R
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.common.config.OfflinePushInfoConfig
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.common.data.Constants
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.common.data.Logger
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.common.utils.PermissionRequest
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.manager.feature.CallingBellFeature
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.manager.feature.CallingVibratorFeature
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.manager.feature.NotificationFeature
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.manager.observer.CallEngineObserver
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.state.CallState
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.state.GlobalState
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.state.MediaState
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.state.UserState
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.state.ViewState
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.view.component.videolayout.VideoFactory
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.view.component.videolayout.VideoView
|
||||
import com.tencent.trtc.TRTCCloud
|
||||
import org.json.JSONObject
|
||||
import java.util.Collections
|
||||
|
||||
class CallManager private constructor(context: Context) : ITUINotification {
|
||||
private val context: Context = context.applicationContext
|
||||
private val callEngineObserver: CallEngineObserver = CallEngineObserver()
|
||||
|
||||
val callState: CallState = CallState()
|
||||
val mediaState: MediaState = MediaState()
|
||||
val viewState: ViewState = ViewState()
|
||||
val userState: UserState = UserState()
|
||||
|
||||
init {
|
||||
registerCallEvent()
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
userState.reset()
|
||||
callState.reset()
|
||||
mediaState.reset()
|
||||
viewState.reset()
|
||||
VideoFactory.instance.clearVideoView()
|
||||
}
|
||||
|
||||
fun call(
|
||||
userId: String, mediaType: TUICallDefine.MediaType, params: TUICallDefine.CallParams?,
|
||||
callback: TUICommonDefine.Callback?
|
||||
) {
|
||||
Logger.i(TAG, "call, userId: $userId, mediaType: $mediaType, params: $params}")
|
||||
if (userId.isNullOrEmpty()) {
|
||||
Logger.e(TAG, "call failed, userId is empty")
|
||||
callback?.onError(TUICallDefine.ERROR_PARAM_INVALID, "call failed, userId is empty")
|
||||
return
|
||||
}
|
||||
|
||||
userState.selfUser.get().id = TUILogin.getLoginUser() ?: ""
|
||||
userState.selfUser.get().avatar.set(TUILogin.getFaceUrl())
|
||||
userState.selfUser.get().nickname.set(TUILogin.getNickName())
|
||||
|
||||
TUICallEngine.createInstance(context).call(userId, mediaType, params, object : TUICommonDefine.Callback {
|
||||
override fun onSuccess() {
|
||||
val user = UserState.User()
|
||||
user.id = userId
|
||||
UserManager.instance.updateUserInfo(user)
|
||||
user.callRole = TUICallDefine.Role.Called
|
||||
user.callStatus.set(TUICallDefine.Status.Waiting)
|
||||
userState.remoteUserList.add(user)
|
||||
|
||||
userState.selfUser.get().callRole = TUICallDefine.Role.Caller
|
||||
userState.selfUser.get().callStatus.set(TUICallDefine.Status.Waiting)
|
||||
callState.scene.set(TUICallDefine.Scene.SINGLE_CALL)
|
||||
callState.mediaType.set(mediaType)
|
||||
|
||||
initAudioPlayDevice()
|
||||
callback?.onSuccess()
|
||||
}
|
||||
|
||||
override fun onError(errCode: Int, errMsg: String) {
|
||||
val errMessage: String = convertErrorMsg(errCode, errMsg)
|
||||
ToastUtil.toastLongMessage(errMessage)
|
||||
callback?.onError(errCode, errMessage)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun groupCall(
|
||||
groupId: String?, userIdList: List<String?>?, mediaType: TUICallDefine.MediaType,
|
||||
params: TUICallDefine.CallParams?, callback: TUICommonDefine.Callback?
|
||||
) {
|
||||
Logger.i(TAG, "groupCall, groupId: $groupId, userIdList: $userIdList, mediaType: $mediaType, params: $params")
|
||||
|
||||
if (groupId.isNullOrEmpty()) {
|
||||
Logger.e(TAG, "groupCall failed, groupId is empty")
|
||||
callback?.onError(TUICallDefine.ERROR_PARAM_INVALID, "groupCall failed, groupId is empty")
|
||||
return
|
||||
}
|
||||
|
||||
val list = userIdList?.toHashSet()?.toMutableList()
|
||||
list?.remove(TUILogin.getLoginUser())
|
||||
list?.removeAll(Collections.singleton(null))
|
||||
|
||||
if (list.isNullOrEmpty()) {
|
||||
Logger.e(TAG, "groupCall failed, userIdList is empty")
|
||||
callback?.onError(TUICallDefine.ERROR_PARAM_INVALID, "groupCall failed, userIdList is empty")
|
||||
return
|
||||
}
|
||||
if (list.size >= Constants.MAX_USER) {
|
||||
ToastUtil.toastLongMessage(context.getString(R.string.tuicallkit_user_exceed_limit))
|
||||
Logger.e(TAG, "groupCall failed, exceeding max user number: 9")
|
||||
callback?.onError(TUICallDefine.ERROR_PARAM_INVALID, "groupCall failed, exceeding max user number")
|
||||
return
|
||||
}
|
||||
userState.selfUser.get().id = TUILogin.getLoginUser() ?: ""
|
||||
userState.selfUser.get().avatar.set(TUILogin.getFaceUrl())
|
||||
userState.selfUser.get().nickname.set(TUILogin.getNickName())
|
||||
|
||||
TUICallEngine.createInstance(context).groupCall(
|
||||
groupId, list, mediaType, params, object : TUICommonDefine.Callback {
|
||||
override fun onSuccess() {
|
||||
val userList = ArrayList<UserState.User>()
|
||||
for (userId in list) {
|
||||
if (userId.isNullOrEmpty()) {
|
||||
continue
|
||||
}
|
||||
val user = UserState.User()
|
||||
user.id = userId
|
||||
UserManager.instance.updateUserInfo(user)
|
||||
user.callRole = TUICallDefine.Role.Called
|
||||
user.callStatus.set(TUICallDefine.Status.Waiting)
|
||||
userList.add(user)
|
||||
}
|
||||
userState.remoteUserList.addAll(userList)
|
||||
UserManager.instance.updateUserListInfo(list, null)
|
||||
|
||||
userState.selfUser.get().callRole = TUICallDefine.Role.Caller
|
||||
userState.selfUser.get().callStatus.set(TUICallDefine.Status.Waiting)
|
||||
|
||||
callState.scene.set(TUICallDefine.Scene.GROUP_CALL)
|
||||
callState.chatGroupId = groupId
|
||||
callState.mediaType.set(mediaType)
|
||||
|
||||
initAudioPlayDevice()
|
||||
callback?.onSuccess()
|
||||
}
|
||||
|
||||
override fun onError(errCode: Int, errMsg: String) {
|
||||
val errMessage: String = convertErrorMsg(errCode, errMsg)
|
||||
ToastUtil.toastLongMessage(errMessage)
|
||||
callback?.onError(errCode, errMessage)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun calls(
|
||||
userIdList: List<String?>?, mediaType: TUICallDefine.MediaType?, params: TUICallDefine.CallParams?,
|
||||
callback: TUICommonDefine.Callback?
|
||||
) {
|
||||
Logger.i(TAG, "calls, userIdList: $userIdList, mediaType: $mediaType, params: $params")
|
||||
val list = userIdList?.toHashSet()?.toMutableList()
|
||||
list?.remove(TUILogin.getLoginUser())
|
||||
list?.removeAll(Collections.singleton(null))
|
||||
|
||||
if (list.isNullOrEmpty()) {
|
||||
Logger.e(TAG, "calls failed, userIdList is empty")
|
||||
callback?.onError(TUICallDefine.ERROR_PARAM_INVALID, "calls failed, userIdList is empty")
|
||||
return
|
||||
}
|
||||
if (list.size >= Constants.MAX_USER) {
|
||||
ToastUtil.toastLongMessage(context.getString(R.string.tuicallkit_user_exceed_limit))
|
||||
Logger.e(TAG, "calls failed, exceeding max user number: 9")
|
||||
callback?.onError(TUICallDefine.ERROR_PARAM_INVALID, "calls failed, exceeding max user number")
|
||||
return
|
||||
}
|
||||
|
||||
userState.selfUser.get().id = TUILogin.getLoginUser() ?: ""
|
||||
userState.selfUser.get().avatar.set(TUILogin.getFaceUrl())
|
||||
userState.selfUser.get().nickname.set(TUILogin.getNickName())
|
||||
|
||||
TUICallEngine.createInstance(context).calls(list, mediaType, params, object : TUICommonDefine.Callback {
|
||||
override fun onSuccess() {
|
||||
Logger.i(TAG, "calls success. list: $list")
|
||||
val userList = ArrayList<UserState.User>()
|
||||
for (userId in list) {
|
||||
if (userId.isNullOrEmpty()) {
|
||||
continue
|
||||
}
|
||||
val user = UserState.User()
|
||||
user.id = userId
|
||||
UserManager.instance.updateUserInfo(user)
|
||||
user.callRole = TUICallDefine.Role.Called
|
||||
user.callStatus.set(TUICallDefine.Status.Waiting)
|
||||
userList.add(user)
|
||||
}
|
||||
userState.remoteUserList.addAll(userList)
|
||||
UserManager.instance.updateUserListInfo(list, null)
|
||||
|
||||
userState.selfUser.get().callRole = TUICallDefine.Role.Caller
|
||||
userState.selfUser.get().callStatus.set(TUICallDefine.Status.Waiting)
|
||||
var scene = TUICallDefine.Scene.SINGLE_CALL
|
||||
if (list.size >= 2 || (params != null && !params.chatGroupId.isNullOrEmpty())) {
|
||||
scene = TUICallDefine.Scene.GROUP_CALL
|
||||
}
|
||||
callState.scene.set(scene)
|
||||
callState.chatGroupId = params?.chatGroupId
|
||||
callState.mediaType.set(mediaType)
|
||||
|
||||
initAudioPlayDevice()
|
||||
callback?.onSuccess()
|
||||
}
|
||||
|
||||
override fun onError(errCode: Int, errMsg: String) {
|
||||
Logger.e(TAG, "calls failed, errCode: $errCode, errMsg: $errMsg")
|
||||
val errMessage: String = convertErrorMsg(errCode, errMsg)
|
||||
ToastUtil.toastLongMessage(errMessage)
|
||||
callback?.onError(errCode, errMessage)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun join(callId: String?, callback: TUICommonDefine.Callback?) {
|
||||
Logger.i(TAG, "join, callId: $callId")
|
||||
if (callId.isNullOrEmpty()) {
|
||||
Logger.e(TAG, "join failed, callId is empty")
|
||||
callback?.onError(TUICallDefine.ERROR_PARAM_INVALID, "join failed, callId is empty")
|
||||
return
|
||||
}
|
||||
|
||||
userState.selfUser.get().id = TUILogin.getLoginUser() ?: ""
|
||||
userState.selfUser.get().avatar.set(TUILogin.getFaceUrl())
|
||||
userState.selfUser.get().nickname.set(TUILogin.getNickName())
|
||||
|
||||
TUICallEngine.createInstance(context).join(callId, object : TUICommonDefine.Callback {
|
||||
override fun onSuccess() {
|
||||
userState.selfUser.get().callRole = TUICallDefine.Role.Called
|
||||
|
||||
callState.scene.set(TUICallDefine.Scene.GROUP_CALL)
|
||||
callState.mediaType.set(TUICallDefine.MediaType.Audio)
|
||||
|
||||
initAudioPlayDevice()
|
||||
callback?.onSuccess()
|
||||
}
|
||||
|
||||
override fun onError(errCode: Int, errMsg: String) {
|
||||
Logger.e(TAG, "join failed callId: $callId, errCode: $errCode, errMsg: $errMsg")
|
||||
ToastUtil.toastLongMessage(convertErrorMsg(errCode, errMsg))
|
||||
callback?.onError(errCode, errMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun joinInGroupCall(
|
||||
roomId: TUICommonDefine.RoomId?, groupId: String?, mediaType: TUICallDefine.MediaType?,
|
||||
callback: TUICommonDefine.Callback?
|
||||
) {
|
||||
Logger.i(TAG, "joinInGroupCall, roomId: $roomId, groupId: $groupId, mediaType: $mediaType")
|
||||
|
||||
val intRoomId = roomId?.intRoomId ?: 0
|
||||
val strRoomId = roomId?.strRoomId ?: ""
|
||||
if (intRoomId <= 0 && strRoomId.isNullOrEmpty()) {
|
||||
Logger.e(TAG, "joinInGroupCall failed, roomId is invalid")
|
||||
callback?.onError(TUICallDefine.ERROR_PARAM_INVALID, "joinInGroupCall failed, roomId is invalid")
|
||||
return
|
||||
}
|
||||
if (groupId.isNullOrEmpty()) {
|
||||
Logger.e(TAG, "joinInGroupCall failed, groupId is empty")
|
||||
callback?.onError(TUICallDefine.ERROR_PARAM_INVALID, "joinInGroupCall failed, groupId is invalid")
|
||||
return
|
||||
}
|
||||
if (TUICallDefine.MediaType.Unknown == mediaType) {
|
||||
Logger.e(TAG, "joinInGroupCall failed, mediaType is unknown")
|
||||
callback?.onError(TUICallDefine.ERROR_PARAM_INVALID, "joinInGroupCall failed, mediaType is unknown")
|
||||
return
|
||||
}
|
||||
userState.selfUser.get().id = TUILogin.getLoginUser() ?: ""
|
||||
userState.selfUser.get().avatar.set(TUILogin.getFaceUrl())
|
||||
userState.selfUser.get().nickname.set(TUILogin.getNickName())
|
||||
|
||||
TUICallEngine.createInstance(context).joinInGroupCall(roomId, groupId, mediaType,
|
||||
object : TUICommonDefine.Callback {
|
||||
override fun onSuccess() {
|
||||
userState.selfUser.get().callRole = TUICallDefine.Role.Called
|
||||
userState.selfUser.get().callStatus.set(TUICallDefine.Status.Accept)
|
||||
|
||||
callState.scene.set(TUICallDefine.Scene.GROUP_CALL)
|
||||
callState.chatGroupId = groupId
|
||||
callState.roomId = roomId
|
||||
callState.mediaType.set(mediaType)
|
||||
|
||||
initAudioPlayDevice()
|
||||
callback?.onSuccess()
|
||||
}
|
||||
|
||||
override fun onError(errCode: Int, errMsg: String) {
|
||||
ToastUtil.toastLongMessage(convertErrorMsg(errCode, errMsg))
|
||||
callback?.onError(errCode, errMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun inviteUser(userIdList: List<String?>?, callback: TUICommonDefine.Callback?) {
|
||||
Logger.i(TAG, "inviteUser, userIdList: $userIdList")
|
||||
val params = TUICallDefine.CallParams()
|
||||
params.offlinePushInfo = OfflinePushInfoConfig.createOfflinePushInfo(context)
|
||||
params.timeout = Constants.CALL_WAITING_MAX_TIME
|
||||
TUICallEngine.createInstance(context)
|
||||
.inviteUser(userIdList, params, object : TUICommonDefine.ValueCallback<List<String>> {
|
||||
override fun onSuccess(data: List<String>?) {
|
||||
if (data.isNullOrEmpty()) {
|
||||
Logger.e(TAG, "inviteUser failed, list: $data")
|
||||
callback?.onError(TUICommonDefine.Error.FAILED.value, "inviteUser failed, userList is empty")
|
||||
return
|
||||
}
|
||||
Logger.i(TAG, "inviteUser success, userList: $data")
|
||||
callState.scene.set(TUICallDefine.Scene.GROUP_CALL)
|
||||
callback?.onSuccess()
|
||||
}
|
||||
|
||||
override fun onError(errCode: Int, errMsg: String) {
|
||||
Logger.e(TAG, "inviteUser failed, errCode: $errCode, errMsg: $errMsg")
|
||||
callback?.onError(errCode, errMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun accept(callback: TUICommonDefine.Callback?) {
|
||||
Logger.i(TAG, "accept")
|
||||
TUICallEngine.createInstance(context).accept(object : TUICommonDefine.Callback {
|
||||
override fun onSuccess() {
|
||||
callback?.onSuccess()
|
||||
}
|
||||
|
||||
override fun onError(errCode: Int, errMsg: String) {
|
||||
Logger.e(TAG, "accept failed, errorCode: $errCode, errMsg: $errMsg ")
|
||||
reset()
|
||||
callback?.onError(errCode, errMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun reject(callback: TUICommonDefine.Callback?) {
|
||||
Logger.i(TAG, "reject")
|
||||
TUICallEngine.createInstance(context).reject(object : TUICommonDefine.Callback {
|
||||
override fun onSuccess() {
|
||||
reset()
|
||||
callback?.onSuccess()
|
||||
}
|
||||
|
||||
override fun onError(errCode: Int, errMsg: String) {
|
||||
Logger.e(TAG, "reject failed, errorCode: $errCode, errMsg: $errMsg ")
|
||||
reset()
|
||||
callback?.onError(errCode, errMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun hangup(callback: TUICommonDefine.Callback?) {
|
||||
Logger.i(TAG, "hangup")
|
||||
TUICallEngine.createInstance(context).hangup(object : TUICommonDefine.Callback {
|
||||
override fun onSuccess() {
|
||||
reset()
|
||||
callback?.onSuccess()
|
||||
}
|
||||
|
||||
override fun onError(errCode: Int, errMsg: String) {
|
||||
Logger.e(TAG, "hangup failed, errorCode: $errCode, errMsg: $errMsg ")
|
||||
reset()
|
||||
callback?.onError(errCode, errMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun openCamera(camera: TUICommonDefine.Camera, videoView: VideoView?, callback: TUICommonDefine.Callback?) {
|
||||
Logger.i(TAG, "openCamera, camera: $camera, videoView: $videoView")
|
||||
PermissionRequest.requestCameraPermission(context, object : PermissionCallback() {
|
||||
override fun onGranted() {
|
||||
TUICallEngine.createInstance(context)
|
||||
.openCamera(camera, videoView?.getVideoView(), object : TUICommonDefine.Callback {
|
||||
override fun onSuccess() {
|
||||
val status: TUICallDefine.Status = userState.selfUser.get().callStatus.get()
|
||||
Logger.i(TAG, "openCamera success, current callStatus: $status")
|
||||
if (TUICallDefine.Status.None != status) {
|
||||
userState.selfUser.get().videoAvailable.set(true)
|
||||
mediaState.isCameraOpened.set(true)
|
||||
mediaState.isFrontCamera.set(camera)
|
||||
}
|
||||
callback?.onSuccess()
|
||||
}
|
||||
|
||||
override fun onError(errCode: Int, errMsg: String) {
|
||||
Logger.e(TAG, "openCamera failed, errorCode: $errCode, errMsg: $errMsg")
|
||||
callback?.onError(errCode, errMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onDenied() {
|
||||
super.onDenied()
|
||||
Logger.e(TAG, "openCamera failed, errMsg: camera permission denied")
|
||||
callback?.onError(TUICallDefine.ERROR_PERMISSION_DENIED, "camera permission denied")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun closeCamera() {
|
||||
Logger.i(TAG, "closeCamera")
|
||||
TUICallEngine.createInstance(context).closeCamera()
|
||||
userState.selfUser.get().videoAvailable.set(false)
|
||||
mediaState.isCameraOpened.set(false)
|
||||
}
|
||||
|
||||
fun switchCamera(camera: TUICommonDefine.Camera) {
|
||||
Logger.i(TAG, "switchCamera, camera: $camera")
|
||||
TUICallEngine.createInstance(context).switchCamera(camera)
|
||||
mediaState.isFrontCamera.set(camera)
|
||||
}
|
||||
|
||||
fun openMicrophone(callback: TUICommonDefine.Callback?) {
|
||||
TUICallEngine.createInstance(context).openMicrophone(object : TUICommonDefine.Callback {
|
||||
override fun onSuccess() {
|
||||
Logger.i(TAG, "openMicrophone success")
|
||||
mediaState.isMicrophoneMuted.set(false)
|
||||
userState.selfUser.get().audioAvailable.set(true)
|
||||
callback?.onSuccess()
|
||||
}
|
||||
|
||||
override fun onError(errCode: Int, errMsg: String) {
|
||||
Logger.e(TAG, "openMicrophone failed, errCode: $errCode, errorMsg: $errMsg")
|
||||
callback?.onError(errCode, errMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun closeMicrophone() {
|
||||
TUICallEngine.createInstance(context).closeMicrophone()
|
||||
mediaState.isMicrophoneMuted.set(true)
|
||||
userState.selfUser.get().audioAvailable.set(false)
|
||||
}
|
||||
|
||||
fun selectAudioPlaybackDevice(device: TUICommonDefine.AudioPlaybackDevice) {
|
||||
TUICallEngine.createInstance(context).selectAudioPlaybackDevice(device)
|
||||
mediaState.audioPlayoutDevice.set(device)
|
||||
}
|
||||
|
||||
fun startRemoteView(userId: String, videoView: VideoView?, callback: TUICommonDefine.PlayCallback?) {
|
||||
Logger.i(TAG, "startRemoteView, userId: $userId, videoView: $videoView")
|
||||
TUICallEngine.createInstance(context).startRemoteView(userId, videoView?.getVideoView(), callback)
|
||||
}
|
||||
|
||||
fun stopRemoteView(userId: String) {
|
||||
TUICallEngine.createInstance(context).stopRemoteView(userId)
|
||||
}
|
||||
|
||||
fun enableMultiDeviceAbility(enable: Boolean, callback: TUICommonDefine.Callback?) {
|
||||
Logger.i(TAG, "enableMultiDeviceAbility, enable: $enable")
|
||||
TUICallEngine.createInstance(context).enableMultiDeviceAbility(enable, callback)
|
||||
}
|
||||
|
||||
fun setSelfInfo(nickname: String?, avatar: String?, callback: TUICommonDefine.Callback?) {
|
||||
Logger.i(TAG, "setSelfInfo, nickname: $nickname, avatar: $avatar")
|
||||
TUICallEngine.createInstance(context).setSelfInfo(nickname, avatar, callback)
|
||||
}
|
||||
|
||||
fun setCallingBell(filePath: String?) {
|
||||
Logger.i(TAG, "setCallingBell, filePath: $filePath")
|
||||
SPUtils.getInstance(CallingBellFeature.PROFILE_TUICALLKIT).put(CallingBellFeature.PROFILE_CALL_BELL, filePath)
|
||||
}
|
||||
|
||||
fun enableMuteMode(enable: Boolean) {
|
||||
Logger.i(TAG, "enableMuteMode, enable: $enable")
|
||||
GlobalState.instance.enableMuteMode = enable
|
||||
SPUtils.getInstance(CallingBellFeature.PROFILE_TUICALLKIT).put(CallingBellFeature.PROFILE_MUTE_MODE, enable)
|
||||
}
|
||||
|
||||
fun enableFloatWindow(enable: Boolean) {
|
||||
Logger.i(TAG, "enableFloatWindow, enable: $enable")
|
||||
GlobalState.instance.enableFloatWindow = enable
|
||||
}
|
||||
|
||||
fun enableVirtualBackground(enable: Boolean) {
|
||||
Logger.i(TAG, "enableVirtualBackground, enable: $enable")
|
||||
GlobalState.instance.enableVirtualBackground = enable
|
||||
}
|
||||
|
||||
fun enableIncomingBanner(enable: Boolean) {
|
||||
Logger.i(TAG, "enableIncomingBanner, enable: $enable")
|
||||
GlobalState.instance.enableIncomingBanner = enable
|
||||
}
|
||||
|
||||
fun setScreenOrientation(orientation: Int) {
|
||||
Logger.i(TAG, "setScreenOrientation, orientation: $orientation")
|
||||
if (orientation in 0..2) {
|
||||
GlobalState.instance.orientation = Constants.Orientation.values()[orientation]
|
||||
}
|
||||
|
||||
if (orientation == Constants.Orientation.LandScape.ordinal) {
|
||||
val videoEncoderParams = TUICommonDefine.VideoEncoderParams()
|
||||
videoEncoderParams.resolutionMode = TUICommonDefine.VideoEncoderParams.ResolutionMode.Landscape
|
||||
TUICallEngine.createInstance(context).setVideoEncoderParams(videoEncoderParams, null)
|
||||
}
|
||||
}
|
||||
|
||||
fun setBlurBackground(enable: Boolean) {
|
||||
Logger.i(TAG, "setBlurBackground, enable: $enable")
|
||||
val level = if (enable) BLUR_LEVEL_HIGH else BLUR_LEVEL_CLOSE
|
||||
viewState.isVirtualBackgroundOpened.set(enable)
|
||||
|
||||
TUICallEngine.createInstance(context).setBlurBackground(level, object : TUICommonDefine.Callback {
|
||||
override fun onSuccess() {
|
||||
Logger.i(TAG, "setBlurBackground success.")
|
||||
}
|
||||
|
||||
override fun onError(errCode: Int, errMsg: String?) {
|
||||
viewState.isVirtualBackgroundOpened.set(false)
|
||||
Logger.e(TAG, "setBlurBackground failed, errCode: $errCode, errMsg: $errMsg")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun reverse1v1CallRenderView(reverse: Boolean) {
|
||||
viewState.reverse1v1CallRenderView = reverse
|
||||
}
|
||||
|
||||
fun setGroupLargeViewUserId(userId: String?) {
|
||||
viewState.showLargeViewUserId.set(userId)
|
||||
}
|
||||
|
||||
fun getTRTCCloudInstance(): TRTCCloud {
|
||||
return TUICallEngine.createInstance(context).trtcCloudInstance
|
||||
}
|
||||
|
||||
fun callExperimentalAPI(jsonStr: String) {
|
||||
if (jsonStr.isNullOrEmpty()) {
|
||||
Logger.w(TAG, "callExperimentalAPI, jsonStr is empty");
|
||||
return
|
||||
}
|
||||
try {
|
||||
val json = JSONObject(jsonStr)
|
||||
if (!json.has("api") || !json.has("params")) {
|
||||
Logger.e(TAG, "callExperimentalAPI[lack api or illegal params]: $jsonStr")
|
||||
return
|
||||
}
|
||||
val api = json.getString("api")
|
||||
val params = json.getJSONObject("params")
|
||||
Logger.i(TAG, "callExperimentalAPI, api: $api, params: $params")
|
||||
if (api == "forceUseV2API") {
|
||||
if (params.has("enable")) {
|
||||
GlobalState.instance.enableForceUseV2API = params.getBoolean("enable")
|
||||
}
|
||||
}
|
||||
if (api == "setFramework") {
|
||||
if (params.has("framework")) {
|
||||
Constants.framework = params.getInt("framework")
|
||||
}
|
||||
if (params.has("component")) {
|
||||
Constants.component = params.getInt("component")
|
||||
}
|
||||
if (params.has("language")) {
|
||||
Constants.language = params.getInt("language")
|
||||
}
|
||||
val params = JSONObject()
|
||||
params.put("framework", Constants.framework)
|
||||
params.put("component", Constants.component)
|
||||
params.put("language", Constants.language)
|
||||
|
||||
val jsonObject = JSONObject()
|
||||
jsonObject.put("api", "setFramework")
|
||||
jsonObject.put("params", params)
|
||||
TUICallEngine.createInstance(context).callExperimentalAPI(jsonObject.toString())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, "callExperimentalAPI json parse fail,json: $jsonStr, error: $e")
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerCallEvent() {
|
||||
TUICore.registerEvent(
|
||||
TUIConstants.TUILogin.EVENT_LOGIN_STATE_CHANGED,
|
||||
TUIConstants.TUILogin.EVENT_SUB_KEY_USER_LOGIN_SUCCESS, this
|
||||
)
|
||||
TUICore.registerEvent(
|
||||
TUIConstants.TUILogin.EVENT_LOGIN_STATE_CHANGED,
|
||||
TUIConstants.TUILogin.EVENT_SUB_KEY_USER_LOGOUT_SUCCESS, this
|
||||
)
|
||||
}
|
||||
|
||||
override fun onNotifyEvent(key: String?, subKey: String?, param: MutableMap<String, Any>?) {
|
||||
if (TUIConstants.TUILogin.EVENT_LOGIN_STATE_CHANGED == key) {
|
||||
if (TUIConstants.TUILogin.EVENT_SUB_KEY_USER_LOGOUT_SUCCESS == subKey) {
|
||||
TUICallEngine.createInstance(context).hangup(null)
|
||||
TUICallEngine.destroyInstance()
|
||||
reset()
|
||||
} else if (TUIConstants.TUILogin.EVENT_SUB_KEY_USER_LOGIN_SUCCESS == subKey) {
|
||||
TUICallEngine.createInstance(context).addObserver(callEngineObserver)
|
||||
initCallEngine()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initCallEngine() {
|
||||
TUICallEngine.createInstance(context).init(TUILogin.getSdkAppId(), TUILogin.getLoginUser(),
|
||||
TUILogin.getUserSig(), object : TUICommonDefine.Callback {
|
||||
override fun onSuccess() {
|
||||
TUICallEngine.createInstance(context).addObserver(callEngineObserver)
|
||||
|
||||
if (GlobalState.instance.enableMultiDevice) {
|
||||
TUICallEngine.createInstance(context).enableMultiDeviceAbility(true, null)
|
||||
}
|
||||
|
||||
val notificationFeature = NotificationFeature(context)
|
||||
notificationFeature.registerNotificationBannerChannel()
|
||||
CallingBellFeature(context)
|
||||
CallingVibratorFeature(context)
|
||||
}
|
||||
|
||||
override fun onError(errCode: Int, errMsg: String) {
|
||||
Logger.e(TAG, "callEngine init failed, errCode: $errCode, errMsg: $errMsg")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun initAudioPlayDevice() {
|
||||
if (TUICallDefine.MediaType.Video == callState.mediaType.get()) {
|
||||
selectAudioPlaybackDevice(TUICommonDefine.AudioPlaybackDevice.Speakerphone)
|
||||
mediaState.isCameraOpened.set(true)
|
||||
} else {
|
||||
selectAudioPlaybackDevice(TUICommonDefine.AudioPlaybackDevice.Earpiece)
|
||||
mediaState.isCameraOpened.set(false)
|
||||
}
|
||||
mediaState.isMicrophoneMuted.set(false)
|
||||
userState.selfUser.get().audioAvailable.set(true)
|
||||
}
|
||||
|
||||
private fun convertErrorMsg(errorCode: Int, errMsg: String): String {
|
||||
if (errorCode == BaseConstants.ERR_SVR_MSG_IN_PEER_BLACKLIST) {
|
||||
return context.getString(R.string.tuicallkit_error_in_peer_blacklist)
|
||||
}
|
||||
|
||||
val commonErrorMap = getCommonErrorMap()
|
||||
if (commonErrorMap.containsKey(errorCode)) {
|
||||
return commonErrorMap[errorCode]!!
|
||||
}
|
||||
return ErrorMessageConverter.convertIMError(errorCode, errMsg)
|
||||
}
|
||||
|
||||
private fun getCommonErrorMap(): Map<Int, String> {
|
||||
val map = HashMap<Int, String>()
|
||||
map[TUICallDefine.ERROR_PACKAGE_NOT_PURCHASED] = context.getString(R.string.tuicallkit_package_not_purchased)
|
||||
map[TUICallDefine.ERROR_PACKAGE_NOT_SUPPORTED] = context.getString(R.string.tuicallkit_package_not_support)
|
||||
map[TUICallDefine.ERROR_INIT_FAIL] = context.getString(R.string.tuicallkit_error_invalid_login)
|
||||
map[TUICallDefine.ERROR_PARAM_INVALID] = context.getString(R.string.tuicallkit_error_parameter_invalid)
|
||||
map[TUICallDefine.ERROR_REQUEST_REFUSED] = context.getString(R.string.tuicallkit_error_request_refused)
|
||||
map[TUICallDefine.ERROR_REQUEST_REPEATED] = context.getString(R.string.tuicallkit_error_request_repeated)
|
||||
map[TUICallDefine.ERROR_SCENE_NOT_SUPPORTED] = context.getString(R.string.tuicallkit_error_scene_not_support)
|
||||
return map
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "CallManager"
|
||||
val instance: CallManager = CallManager(TUIConfig.getAppContext())
|
||||
private const val BLUR_LEVEL_HIGH = 3
|
||||
private const val BLUR_LEVEL_CLOSE = 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.manager
|
||||
|
||||
import com.tencent.qcloud.tuicore.TUIConfig
|
||||
import com.tencent.qcloud.tuicore.TUIConstants
|
||||
import com.tencent.qcloud.tuicore.TUICore
|
||||
|
||||
object PushManager {
|
||||
const val PUSH_UNREGISTER = -1
|
||||
const val PUSH_REGISTER_SUCCESS = 0
|
||||
const val PUSH_REGISTER_FAILED = 1
|
||||
|
||||
fun getPushData(): PushData {
|
||||
if (TUICore.getService(TUIConstants.TIMPush.SERVICE_NAME) == null) {
|
||||
return PushData()
|
||||
}
|
||||
|
||||
val pushData = PushData()
|
||||
pushData.channelId = TUIConfig.getCustomData("pushChannelId") as? Int ?: -1
|
||||
pushData.status = TUIConfig.getCustomData("pushStatus") as? Int ?: PUSH_UNREGISTER
|
||||
return pushData
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see TUIConstants.DeviceInfo
|
||||
*/
|
||||
class PushData {
|
||||
var channelId: Int = -1 // current Push channelId
|
||||
var status: Int = PushManager.PUSH_UNREGISTER // current Push register status: -1-unregister, 0-success; 1-failed
|
||||
|
||||
override fun toString(): String {
|
||||
var channel = ""
|
||||
when (channelId) {
|
||||
TUIConstants.DeviceInfo.BRAND_XIAOMI -> channel = "Xiaomi"
|
||||
TUIConstants.DeviceInfo.BRAND_HUAWEI -> channel = "Huawei"
|
||||
TUIConstants.DeviceInfo.BRAND_GOOGLE_ELSE -> channel = "Google"
|
||||
TUIConstants.DeviceInfo.BRAND_MEIZU -> channel = "Meizu"
|
||||
TUIConstants.DeviceInfo.BRAND_OPPO -> channel = "OPPO"
|
||||
TUIConstants.DeviceInfo.BRAND_VIVO -> channel = "VIVO"
|
||||
TUIConstants.DeviceInfo.BRAND_HONOR -> channel = "Honor"
|
||||
}
|
||||
var pushStatus = ""
|
||||
when (this.status) {
|
||||
PushManager.PUSH_UNREGISTER -> pushStatus = "unregister"
|
||||
PushManager.PUSH_REGISTER_SUCCESS -> pushStatus = "success"
|
||||
PushManager.PUSH_REGISTER_FAILED -> pushStatus = "failed"
|
||||
}
|
||||
return "PushData(channelId=$channel, status=$pushStatus)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.manager
|
||||
|
||||
import com.tencent.cloud.tuikit.engine.common.TUICommonDefine
|
||||
import com.tencent.imsdk.v2.V2TIMFriendInfoResult
|
||||
import com.tencent.imsdk.v2.V2TIMManager
|
||||
import com.tencent.imsdk.v2.V2TIMValueCallback
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.common.data.Logger
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.state.UserState
|
||||
|
||||
class UserManager {
|
||||
fun updateUserInfo(user: UserState.User) {
|
||||
if (user.id.isNullOrEmpty()) {
|
||||
Logger.e(TAG, "updateUserInfo, user.userId isEmpty")
|
||||
return
|
||||
}
|
||||
val userList: MutableList<String> = ArrayList()
|
||||
userList.add(user.id)
|
||||
|
||||
getUserListInfo(userList, object : TUICommonDefine.ValueCallback<List<UserInfo>> {
|
||||
override fun onSuccess(data: List<UserInfo>) {
|
||||
val userInfo = data[0]
|
||||
Logger.i(TAG, "updateUserInfo: $userInfo")
|
||||
var name = userInfo.id
|
||||
if (!userInfo.remark.isNullOrEmpty()) {
|
||||
name = userInfo.remark
|
||||
} else if (!userInfo.nickname.isNullOrEmpty()) {
|
||||
name = userInfo.nickname
|
||||
}
|
||||
user.nickname.set(name)
|
||||
user.avatar.set(userInfo.avatar)
|
||||
}
|
||||
|
||||
override fun onError(errCode: Int, errMsg: String?) {
|
||||
Logger.e(TAG, "updateUserInfo userId:${user.id} error, errorCode: $errCode, errorMsg: $errMsg")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun updateUserListInfo(userList: List<String?>?, callback: TUICommonDefine.ValueCallback<List<UserState.User>?>?) {
|
||||
getUserListInfo(userList, object : TUICommonDefine.ValueCallback<List<UserInfo>> {
|
||||
override fun onSuccess(data: List<UserInfo>) {
|
||||
val userList: MutableList<UserState.User> = ArrayList()
|
||||
for (i in data.indices) {
|
||||
val userInfo = data[i]
|
||||
Logger.i(TAG, "updateUserListInfo: $userInfo")
|
||||
if (userInfo.id.isNullOrEmpty()) {
|
||||
continue
|
||||
}
|
||||
|
||||
val user = UserState.User()
|
||||
user.id = userInfo.id
|
||||
var name: String? = userInfo.id
|
||||
if (!userInfo.remark.isNullOrEmpty()) {
|
||||
name = userInfo.remark
|
||||
} else if (!userInfo.nickname.isNullOrEmpty()) {
|
||||
name = userInfo.nickname
|
||||
}
|
||||
user.nickname.set(name)
|
||||
user.avatar.set(userInfo.avatar)
|
||||
userList.add(user)
|
||||
}
|
||||
callback?.onSuccess(userList)
|
||||
}
|
||||
|
||||
override fun onError(errCode: Int, errMsg: String?) {
|
||||
Logger.e(TAG, "updateUserListInfo error, errorCode: $errCode, errorMsg: $errMsg")
|
||||
callback?.onError(errCode, errMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun getUserListInfo(userList: List<String?>?, callback: TUICommonDefine.ValueCallback<List<UserInfo>>?) {
|
||||
V2TIMManager.getFriendshipManager()
|
||||
.getFriendsInfo(userList, object : V2TIMValueCallback<List<V2TIMFriendInfoResult>> {
|
||||
override fun onSuccess(list: List<V2TIMFriendInfoResult>) {
|
||||
if (list.isNullOrEmpty()) {
|
||||
Logger.e(TAG, "getUserListInfo result is empty")
|
||||
callback?.onError(ERROR_CODE, ERROR_MSG)
|
||||
return
|
||||
}
|
||||
|
||||
val userList: MutableList<UserInfo> = ArrayList()
|
||||
for (i in list.indices) {
|
||||
val friendInfo = list[i].friendInfo
|
||||
|
||||
val userInfo = UserInfo()
|
||||
userInfo.id = friendInfo?.userID ?: ""
|
||||
userInfo.remark = friendInfo?.friendRemark ?: ""
|
||||
userInfo.nickname = friendInfo?.userProfile?.nickName ?: ""
|
||||
userInfo.avatar = friendInfo?.userProfile?.faceUrl ?: ""
|
||||
userList.add(userInfo)
|
||||
}
|
||||
callback?.onSuccess(userList)
|
||||
}
|
||||
|
||||
override fun onError(errorCode: Int, errorMsg: String?) {
|
||||
callback?.onError(errorCode, errorMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun getUserDisplayName(userId: String, callback: TUICommonDefine.ValueCallback<String>) {
|
||||
var displayUser: UserState.User? = UserState.User()
|
||||
if (userId == CallManager.instance.userState.selfUser.get().id) {
|
||||
displayUser = CallManager.instance.userState.selfUser.get()
|
||||
}
|
||||
|
||||
for (user in CallManager.instance.userState.remoteUserList.get()) {
|
||||
if (user.id == userId) {
|
||||
displayUser = user
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!displayUser?.nickname?.get().isNullOrEmpty()) {
|
||||
callback.onSuccess(displayUser?.nickname?.get())
|
||||
return
|
||||
}
|
||||
callback.onSuccess(userId)
|
||||
}
|
||||
|
||||
internal class UserInfo {
|
||||
var id: String = ""
|
||||
var avatar: String = ""
|
||||
var nickname: String = ""
|
||||
var remark: String = ""
|
||||
|
||||
override fun toString(): String {
|
||||
return "UserInfo{userId: $id, nickname: $nickname, avatar: $avatar, remark: $remark}"
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "UserManager"
|
||||
val instance: UserManager = UserManager()
|
||||
private const val ERROR_CODE = -1
|
||||
private const val ERROR_MSG = "getUserInfo result is empty"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,491 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.manager.bridge
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.ActivityResultCaller
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallDefine
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallEngine
|
||||
import com.tencent.qcloud.tuicore.TUIConstants
|
||||
import com.tencent.qcloud.tuicore.TUIConstants.TUICalling.ObjectFactory.RecentCalls
|
||||
import com.tencent.qcloud.tuicore.TUICore
|
||||
import com.tencent.qcloud.tuicore.interfaces.ITUIExtension
|
||||
import com.tencent.qcloud.tuicore.interfaces.ITUINotification
|
||||
import com.tencent.qcloud.tuicore.interfaces.ITUIObjectFactory
|
||||
import com.tencent.qcloud.tuicore.interfaces.ITUIService
|
||||
import com.tencent.qcloud.tuicore.interfaces.TUIExtensionEventListener
|
||||
import com.tencent.qcloud.tuicore.interfaces.TUIExtensionInfo
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.R
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.TUICallKit
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.common.config.OfflinePushInfoConfig
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.common.data.Constants
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.common.data.Logger
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.manager.CallManager
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.state.GlobalState
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.view.component.joiningroupcall.JoinCallView
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.view.component.joiningroupcall.JoinCallViewManager
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.view.component.recents.RecentCallsFragment
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
||||
class CallKitService private constructor(context: Context) : ITUINotification, ITUIService, ITUIExtension,
|
||||
ITUIObjectFactory {
|
||||
private var appContext: Context = context.applicationContext
|
||||
private var joinCallViewManager: JoinCallViewManager? = null
|
||||
|
||||
init {
|
||||
TUICore.registerEvent(
|
||||
TUIConstants.TUILogin.EVENT_IMSDK_INIT_STATE_CHANGED,
|
||||
TUIConstants.TUILogin.EVENT_SUB_KEY_START_INIT, this
|
||||
)
|
||||
TUICore.registerEvent(
|
||||
TUIConstants.TIMPush.EVENT_IM_LOGIN_AFTER_APP_WAKEUP_KEY,
|
||||
TUIConstants.TIMPush.EVENT_IM_LOGIN_AFTER_APP_WAKEUP_SUB_KEY, this
|
||||
)
|
||||
|
||||
TUICore.registerService(TUIConstants.TUICalling.SERVICE_NAME, this)
|
||||
|
||||
TUICore.registerExtension(TUIConstants.TUIChat.Extension.InputMore.CLASSIC_EXTENSION_ID, this)
|
||||
TUICore.registerExtension(TUIConstants.TUIChat.Extension.InputMore.MINIMALIST_EXTENSION_ID, this)
|
||||
TUICore.registerExtension(TUIConstants.TUIContact.Extension.GroupProfileItem.MINIMALIST_EXTENSION_ID, this)
|
||||
TUICore.registerExtension(TUIConstants.TUIContact.Extension.GroupProfileItem.CLASSIC_EXTENSION_ID, this)
|
||||
TUICore.registerExtension(TUIConstants.TUIContact.Extension.FriendProfileItem.CLASSIC_EXTENSION_ID, this)
|
||||
TUICore.registerExtension(TUIConstants.TUIContact.Extension.FriendProfileItem.MINIMALIST_EXTENSION_ID, this)
|
||||
TUICore.registerExtension(TUIConstants.TUIChat.Extension.ChatNavigationMoreItem.CLASSIC_EXTENSION_ID, this)
|
||||
TUICore.registerExtension(TUIConstants.TUIChat.Extension.ChatNavigationMoreItem.MINIMALIST_EXTENSION_ID, this)
|
||||
|
||||
TUICore.registerObjectFactory(TUIConstants.TUICalling.ObjectFactory.FACTORY_NAME, this)
|
||||
TUICore.registerExtension(TUIConstants.TUIChat.Extension.ChatViewTopAreaExtension.EXTENSION_ID, this)
|
||||
}
|
||||
|
||||
override fun onNotifyEvent(key: String?, subKey: String?, param: Map<String, Any>?) {
|
||||
if (TextUtils.isEmpty(key) || TextUtils.isEmpty(subKey)) {
|
||||
return
|
||||
}
|
||||
if (TUIConstants.TUILogin.EVENT_IMSDK_INIT_STATE_CHANGED == key
|
||||
&& TUIConstants.TUILogin.EVENT_SUB_KEY_START_INIT == subKey
|
||||
) {
|
||||
Logger.i(TAG, "onNotifyEvent, start, framework: " + Constants.framework)
|
||||
if (Constants.framework == Constants.CALL_FRAMEWORK_NATIVE) {
|
||||
TUICallKit.createInstance(appContext)
|
||||
adaptiveComponentReport()
|
||||
}
|
||||
setExcludeFromHistoryMessage()
|
||||
}
|
||||
if (TUIConstants.TIMPush.EVENT_IM_LOGIN_AFTER_APP_WAKEUP_KEY == key
|
||||
&& TUIConstants.TIMPush.EVENT_IM_LOGIN_AFTER_APP_WAKEUP_SUB_KEY == subKey
|
||||
) {
|
||||
val data =
|
||||
param?.get(TUIConstants.TIMPush.EVENT_IM_LOGIN_AFTER_APP_WAKEUP_PUSH_MESSAGE_KEY) as Map<String, String>
|
||||
Log.i(TAG, "onNotifyEvent: callOfflineData : $data")
|
||||
|
||||
val map = HashMap<String, Any?>()
|
||||
map[TUIConstants.TIMPush.NOTIFICATION.PUSH_ID] = data[TUIConstants.TIMPush.NOTIFICATION.PUSH_ID]
|
||||
map[TUIConstants.TIMPush.NOTIFICATION.PUSH_EVENT_TIME_KEY] = System.currentTimeMillis() / 1000
|
||||
map[TUIConstants.TIMPush.NOTIFICATION.PUSH_EVENT_TYPE_KEY] = 0
|
||||
|
||||
TUICore.callService(
|
||||
TUIConstants.TIMPush.SERVICE_NAME, TUIConstants.TIMPush.METHOD_REPORT_NOTIFICATION_CLICKED, map
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun adaptiveComponentReport() {
|
||||
try {
|
||||
val params = JSONObject()
|
||||
params.put("framework", 1)
|
||||
when {
|
||||
TUICore.getService(TUIConstants.TUIChat.SERVICE_NAME) != null ->
|
||||
params.put("component", Constants.CALL_COMPONENT_CHAT)
|
||||
|
||||
else -> params.put("component", Constants.CALL_COMPONENT)
|
||||
}
|
||||
params.put("language", Constants.CALL_LANGUAGE_KOTLIN)
|
||||
|
||||
val jsonObject = JSONObject()
|
||||
jsonObject.put("api", "setFramework")
|
||||
jsonObject.put("params", params)
|
||||
CallManager.instance.callExperimentalAPI(jsonObject.toString())
|
||||
} catch (e: JSONException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setExcludeFromHistoryMessage() {
|
||||
if (TUICore.getService(TUIConstants.TUIChat.SERVICE_NAME) == null) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
val params = JSONObject()
|
||||
params.put("excludeFromHistoryMessage", false)
|
||||
val jsonObject = JSONObject()
|
||||
jsonObject.put("api", "setExcludeFromHistoryMessage")
|
||||
jsonObject.put("params", params)
|
||||
TUICallEngine.createInstance(appContext).callExperimentalAPI(jsonObject.toString())
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "CallKitService"
|
||||
private const val CALL_MEMBER_LIMIT = 9
|
||||
|
||||
fun sharedInstance(context: Context): CallKitService {
|
||||
return CallKitService(context)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRaiseExtension(extensionID: String?, parentView: View?, param: MutableMap<String, Any>?): Boolean {
|
||||
if (extensionID != TUIConstants.TUIChat.Extension.ChatViewTopAreaExtension.EXTENSION_ID || param == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
val isGroupChat = param[TUIConstants.TUIChat.Extension.ChatViewTopAreaExtension.IS_GROUP] as? Boolean
|
||||
if (isGroupChat == null || !isGroupChat) {
|
||||
return false
|
||||
}
|
||||
|
||||
val groupId = param[TUIConstants.TUIChat.Extension.ChatViewTopAreaExtension.CHAT_ID] as? String
|
||||
if (groupId.isNullOrEmpty()) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (parentView !is ViewGroup) {
|
||||
return false
|
||||
}
|
||||
parentView.removeAllViews()
|
||||
|
||||
Log.i(TAG, "JoinInGroupCall, groupId: $groupId")
|
||||
|
||||
val manager = getJoinCallViewManager()
|
||||
val callView = JoinCallView(appContext)
|
||||
manager.setJoinCallView(callView)
|
||||
manager.getGroupAttributes(groupId)
|
||||
parentView.addView(callView)
|
||||
parentView.visibility = View.VISIBLE
|
||||
return true
|
||||
}
|
||||
|
||||
private fun getJoinCallViewManager(): JoinCallViewManager {
|
||||
if (joinCallViewManager == null) {
|
||||
joinCallViewManager = JoinCallViewManager()
|
||||
}
|
||||
return joinCallViewManager as JoinCallViewManager
|
||||
}
|
||||
|
||||
override fun onGetExtension(extensionID: String?, param: Map<String?, Any?>?): List<TUIExtensionInfo?>? {
|
||||
if (TextUtils.equals(extensionID, TUIConstants.TUIChat.Extension.InputMore.CLASSIC_EXTENSION_ID)) {
|
||||
return getClassicChatInputMoreExtension(param)
|
||||
} else if (TextUtils.equals(
|
||||
extensionID, TUIConstants.TUIContact.Extension.GroupProfileItem.MINIMALIST_EXTENSION_ID
|
||||
)
|
||||
) {
|
||||
return getMinimalistGroupProfileExtension(param)
|
||||
} else if (TextUtils.equals(
|
||||
extensionID, TUIConstants.TUIContact.Extension.FriendProfileItem.CLASSIC_EXTENSION_ID
|
||||
)
|
||||
) {
|
||||
return getClassicFriendProfileExtension(param)
|
||||
} else if (TextUtils.equals(
|
||||
extensionID, TUIConstants.TUIContact.Extension.FriendProfileItem.MINIMALIST_EXTENSION_ID
|
||||
)
|
||||
) {
|
||||
return getMinimalistFriendProfileExtension(param)
|
||||
} else if (TextUtils.equals(
|
||||
extensionID, TUIConstants.TUIChat.Extension.ChatNavigationMoreItem.MINIMALIST_EXTENSION_ID
|
||||
)
|
||||
) {
|
||||
return getMinimalistChatNavigationMoreExtension(param)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun getClassicChatInputMoreExtension(param: Map<String?, Any?>?): List<TUIExtensionInfo>? {
|
||||
val voiceCallExtension = TUIExtensionInfo()
|
||||
voiceCallExtension.weight = 600
|
||||
val videoCallExtension = TUIExtensionInfo()
|
||||
videoCallExtension.weight = 500
|
||||
val userID: String? = getOrDefault<String>(param, TUIConstants.TUIChat.Extension.InputMore.USER_ID, null)
|
||||
val groupID: String? = getOrDefault<String>(param, TUIConstants.TUIChat.Extension.InputMore.GROUP_ID, null)
|
||||
val voiceListener: ResultTUIExtensionEventListener = ResultTUIExtensionEventListener()
|
||||
voiceListener.mediaType = TUICallDefine.MediaType.Audio
|
||||
voiceListener.userID = userID
|
||||
voiceListener.groupID = groupID
|
||||
val videoListener: ResultTUIExtensionEventListener = ResultTUIExtensionEventListener()
|
||||
videoListener.mediaType = TUICallDefine.MediaType.Video
|
||||
videoListener.userID = userID
|
||||
videoListener.groupID = groupID
|
||||
voiceCallExtension.text = appContext.getString(R.string.tuicallkit_audio_call)
|
||||
voiceCallExtension.icon = R.drawable.tuicallkit_ic_audio_call
|
||||
voiceCallExtension.extensionListener = voiceListener
|
||||
voiceListener.activityResultCaller = getOrDefault<ActivityResultCaller>(
|
||||
param, TUIConstants.TUIChat.Extension.InputMore.CONTEXT, null
|
||||
)
|
||||
videoCallExtension.text = appContext.getString(R.string.tuicallkit_video_call)
|
||||
videoCallExtension.icon = R.drawable.tuicallkit_ic_video_call
|
||||
videoCallExtension.extensionListener = videoListener
|
||||
videoListener.activityResultCaller = getOrDefault<ActivityResultCaller>(
|
||||
param, TUIConstants.TUIChat.Extension.InputMore.CONTEXT, null
|
||||
)
|
||||
val filterVoice: Boolean =
|
||||
getOrDefault(param, TUIConstants.TUIChat.Extension.InputMore.FILTER_VOICE_CALL, false) == true
|
||||
val filterVideo: Boolean =
|
||||
getOrDefault(param, TUIConstants.TUIChat.Extension.InputMore.FILTER_VIDEO_CALL, false) == true
|
||||
val extensionInfoList: MutableList<TUIExtensionInfo> = ArrayList()
|
||||
if (!filterVoice) {
|
||||
extensionInfoList.add(voiceCallExtension)
|
||||
}
|
||||
if (!filterVideo) {
|
||||
extensionInfoList.add(videoCallExtension)
|
||||
}
|
||||
return extensionInfoList
|
||||
}
|
||||
|
||||
inner class ResultTUIExtensionEventListener : TUIExtensionEventListener() {
|
||||
var activityResultCaller: ActivityResultCaller? = null
|
||||
var mediaType: TUICallDefine.MediaType = TUICallDefine.MediaType.Audio
|
||||
var isClassicUI = true
|
||||
var userID: String? = null
|
||||
var groupID: String? = null
|
||||
override fun onClicked(param: Map<String, Any>?) {
|
||||
if (!groupID.isNullOrEmpty()) {
|
||||
var groupMemberSelectActivityName =
|
||||
TUIConstants.TUIContact.StartActivity.GroupMemberSelect.CLASSIC_ACTIVITY_NAME
|
||||
if (!isClassicUI) {
|
||||
groupMemberSelectActivityName =
|
||||
TUIConstants.TUIContact.StartActivity.GroupMemberSelect.MINIMALIST_ACTIVITY_NAME
|
||||
}
|
||||
val bundle = Bundle()
|
||||
bundle.putString(TUIConstants.TUIContact.StartActivity.GroupMemberSelect.GROUP_ID, groupID)
|
||||
bundle.putBoolean(TUIConstants.TUIContact.StartActivity.GroupMemberSelect.SELECT_FOR_CALL, true)
|
||||
bundle.putInt(TUIConstants.TUIContact.StartActivity.GroupMemberSelect.MEMBER_LIMIT, CALL_MEMBER_LIMIT)
|
||||
TUICore.startActivityForResult(
|
||||
activityResultCaller, groupMemberSelectActivityName, bundle
|
||||
) { result: ActivityResult ->
|
||||
val stringList: ArrayList<String>? = result.data?.getStringArrayListExtra(
|
||||
TUIConstants.TUIContact.StartActivity.GroupMemberSelect.DATA_LIST
|
||||
)
|
||||
startCall(groupID, stringList, mediaType)
|
||||
}
|
||||
} else if (!userID.isNullOrEmpty()) {
|
||||
val userList = mutableListOf<String>()
|
||||
userList.add(userID!!)
|
||||
startCall(groupID, userList, mediaType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMinimalistGroupProfileExtension(param: Map<String?, Any?>?): List<TUIExtensionInfo>? {
|
||||
val voiceCallExtension = TUIExtensionInfo()
|
||||
voiceCallExtension.weight = 200
|
||||
val videoCallExtension = TUIExtensionInfo()
|
||||
videoCallExtension.weight = 100
|
||||
val groupID = getOrDefault<String?>(param, TUIConstants.TUIContact.Extension.GroupProfileItem.GROUP_ID, null)
|
||||
val voiceListener = ResultTUIExtensionEventListener()
|
||||
voiceListener.mediaType = TUICallDefine.MediaType.Audio
|
||||
voiceListener.groupID = groupID
|
||||
voiceListener.isClassicUI = false
|
||||
val videoListener = ResultTUIExtensionEventListener()
|
||||
videoListener.mediaType = TUICallDefine.MediaType.Video
|
||||
videoListener.groupID = groupID
|
||||
videoListener.isClassicUI = false
|
||||
voiceCallExtension.text = appContext.getString(R.string.tuicallkit_audio_call)
|
||||
voiceCallExtension.icon = R.drawable.tuicallkit_profile_minimalist_audio_icon
|
||||
voiceCallExtension.extensionListener = voiceListener
|
||||
voiceListener.activityResultCaller = getOrDefault<ActivityResultCaller?>(
|
||||
param, TUIConstants.TUIContact.Extension.GroupProfileItem.CONTEXT, null
|
||||
)
|
||||
voiceListener.isClassicUI = false
|
||||
videoCallExtension.text = appContext.getString(R.string.tuicallkit_video_call)
|
||||
videoCallExtension.icon = R.drawable.tuicallkit_profile_minimalist_video_icon
|
||||
videoCallExtension.extensionListener = videoListener
|
||||
videoListener.isClassicUI = false
|
||||
videoListener.activityResultCaller = getOrDefault<ActivityResultCaller?>(
|
||||
param, TUIConstants.TUIContact.Extension.GroupProfileItem.CONTEXT, null
|
||||
)
|
||||
val extensionInfoList: MutableList<TUIExtensionInfo> = java.util.ArrayList()
|
||||
extensionInfoList.add(videoCallExtension)
|
||||
extensionInfoList.add(voiceCallExtension)
|
||||
return extensionInfoList
|
||||
}
|
||||
|
||||
private fun getClassicFriendProfileExtension(param: Map<String?, Any?>?): List<TUIExtensionInfo>? {
|
||||
val voiceCallExtension = TUIExtensionInfo()
|
||||
voiceCallExtension.weight = 300
|
||||
val videoCallExtension = TUIExtensionInfo()
|
||||
videoCallExtension.weight = 200
|
||||
val userID = getOrDefault<String?>(param, TUIConstants.TUIContact.Extension.FriendProfileItem.USER_ID, null)
|
||||
val voiceListener = ResultTUIExtensionEventListener()
|
||||
voiceListener.mediaType = TUICallDefine.MediaType.Audio
|
||||
voiceListener.userID = userID
|
||||
val videoListener = ResultTUIExtensionEventListener()
|
||||
videoListener.mediaType = TUICallDefine.MediaType.Video
|
||||
videoListener.userID = userID
|
||||
voiceCallExtension.text = appContext.getString(R.string.tuicallkit_audio_call)
|
||||
voiceCallExtension.extensionListener = voiceListener
|
||||
videoCallExtension.text = appContext.getString(R.string.tuicallkit_video_call)
|
||||
videoCallExtension.extensionListener = videoListener
|
||||
val extensionInfoList: MutableList<TUIExtensionInfo> = java.util.ArrayList()
|
||||
extensionInfoList.add(videoCallExtension)
|
||||
extensionInfoList.add(voiceCallExtension)
|
||||
return extensionInfoList
|
||||
}
|
||||
|
||||
private fun getMinimalistFriendProfileExtension(param: Map<String?, Any?>?): List<TUIExtensionInfo>? {
|
||||
val voiceCallExtension = TUIExtensionInfo()
|
||||
voiceCallExtension.weight = 300
|
||||
val videoCallExtension = TUIExtensionInfo()
|
||||
videoCallExtension.weight = 200
|
||||
val userID = getOrDefault<String?>(param, TUIConstants.TUIContact.Extension.FriendProfileItem.USER_ID, null)
|
||||
val voiceListener = ResultTUIExtensionEventListener()
|
||||
voiceListener.mediaType = TUICallDefine.MediaType.Audio
|
||||
voiceListener.userID = userID
|
||||
voiceListener.isClassicUI = false
|
||||
val videoListener = ResultTUIExtensionEventListener()
|
||||
videoListener.mediaType = TUICallDefine.MediaType.Video
|
||||
videoListener.userID = userID
|
||||
videoListener.isClassicUI = false
|
||||
voiceCallExtension.icon = R.drawable.tuicallkit_profile_minimalist_audio_icon
|
||||
voiceCallExtension.text = appContext.getString(R.string.tuicallkit_audio_call)
|
||||
voiceCallExtension.extensionListener = voiceListener
|
||||
videoCallExtension.icon = R.drawable.tuicallkit_profile_minimalist_video_icon
|
||||
videoCallExtension.text = appContext.getString(R.string.tuicallkit_video_call)
|
||||
videoCallExtension.extensionListener = videoListener
|
||||
val extensionInfoList: MutableList<TUIExtensionInfo> = java.util.ArrayList()
|
||||
extensionInfoList.add(videoCallExtension)
|
||||
extensionInfoList.add(voiceCallExtension)
|
||||
return extensionInfoList
|
||||
}
|
||||
|
||||
private fun getMinimalistChatNavigationMoreExtension(param: Map<String?, Any?>?): List<TUIExtensionInfo>? {
|
||||
val userID = getOrDefault<String?>(param, TUIConstants.TUIChat.Extension.ChatNavigationMoreItem.USER_ID, null)
|
||||
val groupID = getOrDefault<String?>(param, TUIConstants.TUIChat.Extension.ChatNavigationMoreItem.GROUP_ID, null)
|
||||
val voiceListener = ResultTUIExtensionEventListener()
|
||||
voiceListener.mediaType = TUICallDefine.MediaType.Audio
|
||||
voiceListener.groupID = groupID
|
||||
voiceListener.userID = userID
|
||||
voiceListener.isClassicUI = false
|
||||
voiceListener.activityResultCaller = getOrDefault<ActivityResultCaller?>(
|
||||
param, TUIConstants.TUIChat.Extension.ChatNavigationMoreItem.CONTEXT, null
|
||||
)
|
||||
val videoListener = ResultTUIExtensionEventListener()
|
||||
videoListener.mediaType = TUICallDefine.MediaType.Video
|
||||
videoListener.groupID = groupID
|
||||
videoListener.userID = userID
|
||||
videoListener.isClassicUI = false
|
||||
videoListener.activityResultCaller = getOrDefault<ActivityResultCaller?>(
|
||||
param, TUIConstants.TUIChat.Extension.ChatNavigationMoreItem.CONTEXT, null
|
||||
)
|
||||
val voiceCallExtension = TUIExtensionInfo()
|
||||
val videoCallExtension = TUIExtensionInfo()
|
||||
voiceCallExtension.icon = R.drawable.tuicallkit_chat_title_bar_minimalist_audio_call_icon
|
||||
voiceCallExtension.extensionListener = voiceListener
|
||||
videoCallExtension.icon = R.drawable.tuicallkit_chat_title_bar_minimalist_video_call_icon
|
||||
videoCallExtension.extensionListener = videoListener
|
||||
val extensionInfoList: MutableList<TUIExtensionInfo> = java.util.ArrayList()
|
||||
extensionInfoList.add(voiceCallExtension)
|
||||
extensionInfoList.add(videoCallExtension)
|
||||
return extensionInfoList
|
||||
}
|
||||
|
||||
private fun <T> getOrDefault(map: Map<*, *>?, key: Any, defaultValue: T?): T? {
|
||||
if (map == null || map.isEmpty()) {
|
||||
return defaultValue
|
||||
}
|
||||
val value = map[key]
|
||||
try {
|
||||
if (value != null) {
|
||||
return value as T
|
||||
}
|
||||
} catch (e: ClassCastException) {
|
||||
return defaultValue
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
override fun onCall(method: String?, param: Map<String?, Any?>?): Any? {
|
||||
Log.i(TAG, "onCall, method: $method ,param: $param")
|
||||
if (TextUtils.isEmpty(method)) {
|
||||
return null
|
||||
}
|
||||
if (null != param && TextUtils.equals(TUIConstants.TUICalling.METHOD_NAME_ENABLE_FLOAT_WINDOW, method)) {
|
||||
val enableFloatWindow = param[TUIConstants.TUICalling.PARAM_NAME_ENABLE_FLOAT_WINDOW] as Boolean
|
||||
Log.i(TAG, "onCall, enableFloatWindow: $enableFloatWindow")
|
||||
TUICallKit.createInstance(appContext).enableFloatWindow(enableFloatWindow)
|
||||
return null
|
||||
}
|
||||
if (null != param && TextUtils.equals(TUIConstants.TUICalling.METHOD_NAME_ENABLE_MULTI_DEVICE, method)) {
|
||||
val enable = param[TUIConstants.TUICalling.PARAM_NAME_ENABLE_MULTI_DEVICE] as Boolean
|
||||
Log.i(TAG, "onCall, enableMultiDevice: $enable")
|
||||
GlobalState.instance.enableMultiDevice = enable
|
||||
return null
|
||||
}
|
||||
if (param != null && TextUtils.equals(TUIConstants.TUICalling.METHOD_NAME_ENABLE_INCOMING_BANNER, method)) {
|
||||
val enable = param[TUIConstants.TUICalling.PARAM_NAME_ENABLE_INCOMING_BANNER] as Boolean
|
||||
TUICallKit.createInstance(appContext).enableIncomingBanner(enable)
|
||||
return null
|
||||
}
|
||||
if (param != null && TextUtils.equals(TUIConstants.TUICalling.METHOD_NAME_ENABLE_VIRTUAL_BACKGROUND, method)) {
|
||||
val enable = param[TUIConstants.TUICalling.PARAM_NAME_ENABLE_VIRTUAL_BACKGROUND] as Boolean
|
||||
TUICallKit.createInstance(appContext).enableVirtualBackground(enable)
|
||||
return null
|
||||
}
|
||||
if (null != param && TextUtils.equals(TUIConstants.TUICalling.METHOD_NAME_CALL, method)) {
|
||||
val userIDs = getOrDefault<Array<String>>(param, TUIConstants.TUICalling.PARAM_NAME_USERIDS, null)
|
||||
val typeString = getOrDefault<String>(param, TUIConstants.TUICalling.PARAM_NAME_TYPE, "")
|
||||
val groupID = getOrDefault<String>(param, TUIConstants.TUICalling.PARAM_NAME_GROUPID, "")
|
||||
|
||||
var userIdList: List<String?>? = userIDs?.toList() ?: ArrayList()
|
||||
Logger.i(TAG, "onCall, groupID: $groupID, userIdList: $userIdList")
|
||||
userIdList = userIdList?.filterNotNull()
|
||||
|
||||
var mediaType = TUICallDefine.MediaType.Unknown
|
||||
if (TUIConstants.TUICalling.TYPE_AUDIO == typeString) {
|
||||
mediaType = TUICallDefine.MediaType.Audio
|
||||
} else if (TUIConstants.TUICalling.TYPE_VIDEO == typeString) {
|
||||
mediaType = TUICallDefine.MediaType.Video
|
||||
}
|
||||
startCall(groupID, userIdList, mediaType)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun startCall(groupID: String?, userIdList: List<String>?, mediaType: TUICallDefine.MediaType) {
|
||||
if (GlobalState.instance.enableForceUseV2API) {
|
||||
Logger.w(TAG, "startCall, enableForceUseV2API is true, use deprecated api")
|
||||
if (!groupID.isNullOrEmpty()) {
|
||||
TUICallKit.createInstance(appContext).groupCall(groupID, userIdList, mediaType)
|
||||
} else if (userIdList?.size == 1) {
|
||||
TUICallKit.createInstance(appContext).call(userIdList[0], mediaType)
|
||||
} else {
|
||||
Logger.e(TAG, "startCall ignored, enableForceUseV2API and groupId is empty and userList is not 1")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val params = TUICallDefine.CallParams()
|
||||
params.offlinePushInfo = OfflinePushInfoConfig.createOfflinePushInfo(appContext)
|
||||
params.timeout = Constants.CALL_WAITING_MAX_TIME
|
||||
if (!groupID.isNullOrEmpty()) {
|
||||
params.chatGroupId = groupID
|
||||
}
|
||||
TUICallKit.createInstance(appContext).calls(userIdList, mediaType, params, null)
|
||||
}
|
||||
|
||||
override fun onCreateObject(objectName: String?, param: MutableMap<String?, Any?>?): Any? {
|
||||
if (TextUtils.equals(objectName, RecentCalls.OBJECT_NAME)) {
|
||||
var style = RecentCalls.UI_STYLE_MINIMALIST
|
||||
if (param != null && param[RecentCalls.UI_STYLE] != null
|
||||
&& RecentCalls.UI_STYLE_CLASSIC == param[RecentCalls.UI_STYLE]!!) {
|
||||
style = RecentCalls.UI_STYLE_CLASSIC
|
||||
}
|
||||
return RecentCallsFragment(style)
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.manager.bridge
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallDefine
|
||||
import com.tencent.qcloud.tuicore.ServiceInitializer
|
||||
import com.tencent.qcloud.tuicore.TUIConstants
|
||||
import com.tencent.qcloud.tuicore.TUICore
|
||||
import com.tencent.qcloud.tuicore.TUILogin
|
||||
import com.tencent.qcloud.tuicore.permission.PermissionRequester
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.TUICallKitImpl
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.common.data.Constants
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.manager.CallManager
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.view.CallMainActivity
|
||||
|
||||
/**
|
||||
* `TUICallKit` uses `ContentProvider` to be registered with `TUICore`.
|
||||
* (`TUICore` is the connection and communication class of each component)
|
||||
*/
|
||||
class Initializer : ServiceInitializer() {
|
||||
override fun init(context: Context?) {
|
||||
val callingService: CallKitService = CallKitService.sharedInstance(context!!)
|
||||
TUICore.registerService(TUIConstants.TUICalling.SERVICE_NAME, callingService)
|
||||
|
||||
val audioRecordService = TUIAudioMessageRecordService(context)
|
||||
TUICore.registerService(TUIConstants.TUICalling.SERVICE_NAME_AUDIO_RECORD, audioRecordService)
|
||||
|
||||
if (context is Application) {
|
||||
context.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks {
|
||||
private var foregroundActivities = 0
|
||||
private var isChangingConfiguration = false
|
||||
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {}
|
||||
override fun onActivityStarted(activity: Activity) {
|
||||
if (isMiPushActivity(activity)) {
|
||||
return
|
||||
}
|
||||
|
||||
foregroundActivities++
|
||||
if (foregroundActivities == 1 && !isChangingConfiguration) {
|
||||
// The Call page exits the background and re-enters without repeatedly pulling up the page.
|
||||
if (Constants.CALL_FRAMEWORK_NATIVE == Constants.framework &&
|
||||
TUILogin.isUserLogined() && activity !is CallMainActivity) {
|
||||
TUICallKitImpl.createInstance(context).queryOfflineCall()
|
||||
}
|
||||
}
|
||||
isChangingConfiguration = false
|
||||
}
|
||||
|
||||
override fun onActivityResumed(activity: Activity) {}
|
||||
override fun onActivityPaused(activity: Activity) {}
|
||||
override fun onActivityStopped(activity: Activity) {
|
||||
if (isMiPushActivity(activity)) {
|
||||
return
|
||||
}
|
||||
foregroundActivities--
|
||||
isChangingConfiguration = activity.isChangingConfigurations
|
||||
if (foregroundActivities == 0 && !isChangingConfiguration) {
|
||||
checkToShowFloatWindow()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
|
||||
override fun onActivityDestroyed(activity: Activity) {}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun isMiPushActivity(activity: Activity): Boolean {
|
||||
try {
|
||||
val clazzName = activity.componentName.className
|
||||
return clazzName.contains("mipush.sdk")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun checkToShowFloatWindow() {
|
||||
if (TUICallDefine.Status.None == CallManager.instance.userState.selfUser.get().callStatus.get()) {
|
||||
return
|
||||
}
|
||||
if (!PermissionRequester.newInstance(PermissionRequester.FLOAT_PERMISSION).has()) {
|
||||
return
|
||||
}
|
||||
TUICore.notifyEvent(Constants.KEY_TUI_CALLKIT, Constants.SUB_KEY_SHOW_FLOAT_WINDOW, null)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.manager.bridge
|
||||
|
||||
import android.content.Context
|
||||
import android.media.AudioAttributes
|
||||
import android.media.AudioFocusRequest
|
||||
import android.media.AudioManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallDefine
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallEngine
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallObserver
|
||||
import com.tencent.qcloud.tuicore.TUIConstants
|
||||
import com.tencent.qcloud.tuicore.TUICore
|
||||
import com.tencent.qcloud.tuicore.interfaces.ITUINotification
|
||||
import com.tencent.qcloud.tuicore.interfaces.ITUIService
|
||||
import com.tencent.qcloud.tuicore.interfaces.TUIServiceCallback
|
||||
import com.tencent.qcloud.tuicore.permission.PermissionCallback
|
||||
import com.tencent.qcloud.tuicore.util.TUIBuild
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.common.data.Logger
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.common.utils.PermissionRequest
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.manager.CallManager
|
||||
import com.tencent.trtc.TRTCCloud
|
||||
import com.tencent.trtc.TRTCCloudDef
|
||||
import com.tencent.trtc.TRTCCloudListener
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
||||
class TUIAudioMessageRecordService(context: Context) : ITUIService, ITUINotification {
|
||||
private var context: Context = context.applicationContext
|
||||
private var audioRecordInfo: AudioRecordInfo? = null
|
||||
private var focusRequest: AudioFocusRequest? = null
|
||||
private var audioManager: AudioManager? = null
|
||||
private var focusChangeListener: AudioManager.OnAudioFocusChangeListener? = null
|
||||
private var audioRecordValueCallback: TUIServiceCallback? = null
|
||||
|
||||
init {
|
||||
TUICore.registerEvent(
|
||||
TUIConstants.TUILogin.EVENT_LOGIN_STATE_CHANGED,
|
||||
TUIConstants.TUILogin.EVENT_SUB_KEY_USER_LOGIN_SUCCESS, this
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCall(method: String?, param: Map<String?, Any?>?, callback: TUIServiceCallback?): Any {
|
||||
audioRecordValueCallback = callback
|
||||
if (TextUtils.equals(TUIConstants.TUICalling.METHOD_NAME_START_RECORD_AUDIO_MESSAGE, method)) {
|
||||
if (param == null) {
|
||||
Logger.e(TAG, "startRecordAudioMessage failed, param is empty")
|
||||
notifyAudioMessageRecordEvent(
|
||||
TUIConstants.TUICalling.EVENT_SUB_KEY_RECORD_START,
|
||||
TUIConstants.TUICalling.ERROR_INVALID_PARAM, null
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
if (TUICallDefine.Status.None != CallManager.instance.userState.selfUser.get().callStatus.get()) {
|
||||
Logger.e(TAG, "startRecordAudioMessage failed, The current call status does not support recording")
|
||||
notifyAudioMessageRecordEvent(
|
||||
TUIConstants.TUICalling.EVENT_SUB_KEY_RECORD_START,
|
||||
TUIConstants.TUICalling.ERROR_STATUS_IN_CALL, null
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
if (audioRecordInfo != null) {
|
||||
Logger.e(TAG, "startRecordAudioMessage failed, The recording is not over, It cannot be called again")
|
||||
notifyAudioMessageRecordEvent(
|
||||
TUIConstants.TUICalling.EVENT_SUB_KEY_RECORD_START,
|
||||
TUIConstants.TUICalling.ERROR_STATUS_IS_AUDIO_RECORDING, null
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
PermissionRequest.requestPermissions(context, TUICallDefine.MediaType.Audio, object : PermissionCallback() {
|
||||
override fun onGranted() {
|
||||
initAudioFocusManager()
|
||||
if (requestAudioFocus() != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
|
||||
Logger.e(TAG, "startRecordAudioMessage failed, Failed to obtain audio focus")
|
||||
notifyAudioMessageRecordEvent(
|
||||
TUIConstants.TUICalling.EVENT_SUB_KEY_RECORD_START,
|
||||
TUIConstants.TUICalling.ERROR_REQUEST_AUDIO_FOCUS_FAILED, null
|
||||
)
|
||||
return
|
||||
}
|
||||
audioRecordInfo = AudioRecordInfo()
|
||||
if (param.containsKey(TUIConstants.TUICalling.PARAM_NAME_AUDIO_PATH)) {
|
||||
audioRecordInfo!!.path = param[TUIConstants.TUICalling.PARAM_NAME_AUDIO_PATH] as String
|
||||
}
|
||||
if (param.containsKey(TUIConstants.TUICalling.PARAM_NAME_SDK_APP_ID)) {
|
||||
audioRecordInfo!!.sdkAppId = param[TUIConstants.TUICalling.PARAM_NAME_SDK_APP_ID] as Int
|
||||
}
|
||||
if (param.containsKey(TUIConstants.TUICalling.PARAM_NAME_AUDIO_SIGNATURE)) {
|
||||
audioRecordInfo!!.signature =
|
||||
param[TUIConstants.TUICalling.PARAM_NAME_AUDIO_SIGNATURE] as String?
|
||||
}
|
||||
|
||||
TRTCCloud.sharedInstance(context).addListener(trtcCloudListener)
|
||||
startRecordAudioMessage()
|
||||
}
|
||||
|
||||
override fun onDenied() {
|
||||
super.onDenied()
|
||||
notifyAudioMessageRecordEvent(
|
||||
TUIConstants.TUICalling.EVENT_SUB_KEY_RECORD_START,
|
||||
TUIConstants.TUICalling.ERROR_MIC_PERMISSION_REFUSED, null
|
||||
)
|
||||
}
|
||||
})
|
||||
return true
|
||||
}
|
||||
if (TextUtils.equals(TUIConstants.TUICalling.METHOD_NAME_STOP_RECORD_AUDIO_MESSAGE, method)) {
|
||||
stopRecordAudioMessage()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private val trtcCloudListener: TRTCCloudListener = object : TRTCCloudListener() {
|
||||
override fun onError(errCode: Int, errMsg: String, extraInfo: Bundle) {
|
||||
super.onError(errCode, errMsg, extraInfo)
|
||||
if (errCode == TUIConstants.TUICalling.ERR_MIC_START_FAIL
|
||||
|| errCode == TUIConstants.TUICalling.ERR_MIC_NOT_AUTHORIZED
|
||||
|| errCode == TUIConstants.TUICalling.ERR_MIC_SET_PARAM_FAIL
|
||||
|| errCode == TUIConstants.TUICalling.ERR_MIC_OCCUPY
|
||||
) {
|
||||
notifyAudioMessageRecordEvent(TUIConstants.TUICalling.EVENT_SUB_KEY_RECORD_START, errCode, null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLocalRecordBegin(errCode: Int, storagePath: String) {
|
||||
super.onLocalRecordBegin(errCode, storagePath)
|
||||
val code = convertErrorCode("onLocalRecordBegin", errCode)
|
||||
if (errCode == TUIConstants.TUICalling.ERROR_NONE) {
|
||||
TRTCCloud.sharedInstance(context).startLocalAudio(TRTCCloudDef.TRTC_AUDIO_QUALITY_SPEECH)
|
||||
}
|
||||
notifyAudioMessageRecordEvent(TUIConstants.TUICalling.EVENT_SUB_KEY_RECORD_START, code, storagePath)
|
||||
}
|
||||
|
||||
override fun onLocalRecordComplete(errCode: Int, storagePath: String) {
|
||||
super.onLocalRecordComplete(errCode, storagePath)
|
||||
val code = convertErrorCode("onLocalRecordComplete", errCode)
|
||||
notifyAudioMessageRecordEvent(TUIConstants.TUICalling.EVENT_SUB_KEY_RECORD_STOP, code, storagePath)
|
||||
}
|
||||
}
|
||||
private val callObserver: TUICallObserver = object : TUICallObserver() {
|
||||
override fun onCallReceived(
|
||||
callerId: String?,
|
||||
calleeIdList: List<String?>?,
|
||||
groupId: String?,
|
||||
callMediaType: TUICallDefine.MediaType?,
|
||||
userData: String?
|
||||
) {
|
||||
super.onCallReceived(callerId, calleeIdList, groupId, callMediaType, userData)
|
||||
stopRecordAudioMessage()
|
||||
}
|
||||
}
|
||||
|
||||
private fun startRecordAudioMessage() {
|
||||
if (audioRecordInfo == null) {
|
||||
Logger.e(TAG, "startRecordAudioMessage failed, audioRecordInfo is empty")
|
||||
return
|
||||
}
|
||||
Logger.i(TAG, "startRecordAudioMessage, mAudioRecordInfo: $audioRecordInfo")
|
||||
val jsonObject = JSONObject()
|
||||
try {
|
||||
jsonObject.put("api", "startRecordAudioMessage")
|
||||
val params = JSONObject()
|
||||
params.put(TUIConstants.TUICalling.PARAM_NAME_SDK_APP_ID, audioRecordInfo!!.sdkAppId)
|
||||
params.put(TUIConstants.TUICalling.PARAM_NAME_AUDIO_PATH, audioRecordInfo!!.path)
|
||||
params.put("key", audioRecordInfo!!.signature)
|
||||
jsonObject.put("params", params)
|
||||
TRTCCloud.sharedInstance(context).callExperimentalAPI(jsonObject.toString())
|
||||
} catch (e: JSONException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopRecordAudioMessage() {
|
||||
if (audioRecordInfo == null) {
|
||||
Logger.w(TAG, "stopRecordAudioMessage, current recording status is Idle,do not need to stop")
|
||||
return
|
||||
}
|
||||
val jsonObject = JSONObject()
|
||||
try {
|
||||
jsonObject.put("api", "stopRecordAudioMessage")
|
||||
val params = JSONObject()
|
||||
jsonObject.put("params", params)
|
||||
TRTCCloud.sharedInstance(context).callExperimentalAPI(jsonObject.toString())
|
||||
} catch (e: JSONException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
Logger.i(TAG, "stopRecordAudioMessage, stopLocalAudio")
|
||||
TRTCCloud.sharedInstance(context).stopLocalAudio()
|
||||
|
||||
audioRecordInfo = null
|
||||
abandonAudioFocus()
|
||||
}
|
||||
|
||||
private fun notifyAudioMessageRecordEvent(method: String, errCode: Int, path: String?) {
|
||||
Logger.i(TAG, "notifyAudioMessageRecordEvent, method: $method, errCode: $errCode,path: $path")
|
||||
if (audioRecordValueCallback != null) {
|
||||
val bundleInfo = Bundle()
|
||||
bundleInfo.putString(TUIConstants.TUICalling.EVENT_KEY_RECORD_AUDIO_MESSAGE, method)
|
||||
bundleInfo.putString(TUIConstants.TUICalling.PARAM_NAME_AUDIO_PATH, path)
|
||||
audioRecordValueCallback?.onServiceCallback(errCode, "", bundleInfo)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNotifyEvent(key: String, subKey: String, param: Map<String, Any>?) {
|
||||
if (TUIConstants.TUILogin.EVENT_LOGIN_STATE_CHANGED == key && TUIConstants.TUILogin.EVENT_SUB_KEY_USER_LOGIN_SUCCESS == subKey) {
|
||||
TUICallEngine.createInstance(context).addObserver(callObserver)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initAudioFocusManager() {
|
||||
if (audioManager == null) {
|
||||
audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
}
|
||||
if (focusChangeListener == null) {
|
||||
focusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
|
||||
when (focusChange) {
|
||||
AudioManager.AUDIOFOCUS_GAIN -> {}
|
||||
AudioManager.AUDIOFOCUS_LOSS, AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> stopRecordAudioMessage()
|
||||
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
|
||||
//transient lost audio focus and the new focus owner doesn't require others to be silent.
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (TUIBuild.getVersionInt() >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
val attributes: AudioAttributes = AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA)
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC).build()
|
||||
|
||||
if (TUIBuild.getVersionInt() >= Build.VERSION_CODES.O) {
|
||||
focusRequest = AudioFocusRequest
|
||||
.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
|
||||
.setWillPauseWhenDucked(true)
|
||||
.setAudioAttributes(attributes)
|
||||
.setOnAudioFocusChangeListener(focusChangeListener!!)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestAudioFocus(): Int {
|
||||
if (audioManager == null) {
|
||||
return AudioManager.AUDIOFOCUS_REQUEST_FAILED
|
||||
}
|
||||
val result: Int = if (TUIBuild.getVersionInt() >= Build.VERSION_CODES.O) {
|
||||
audioManager!!.requestAudioFocus(focusRequest!!)
|
||||
} else {
|
||||
audioManager!!.requestAudioFocus(
|
||||
focusChangeListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT
|
||||
)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun abandonAudioFocus(): Int {
|
||||
if (audioManager == null) {
|
||||
return AudioManager.AUDIOFOCUS_REQUEST_FAILED
|
||||
}
|
||||
val result: Int = if (TUIBuild.getVersionInt() >= Build.VERSION_CODES.O) {
|
||||
audioManager!!.abandonAudioFocusRequest(focusRequest!!)
|
||||
} else {
|
||||
audioManager!!.abandonAudioFocus(focusChangeListener)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun convertErrorCode(method: String, errorCode: Int): Int {
|
||||
val targetCode: Int = when (errorCode) {
|
||||
-1 -> if ("onLocalRecordBegin" == method) TUIConstants.TUICalling.ERROR_RECORD_INIT_FAILED
|
||||
else TUIConstants.TUICalling.ERROR_RECORD_FAILED
|
||||
-2 -> TUIConstants.TUICalling.ERROR_PATH_FORMAT_NOT_SUPPORT
|
||||
-3 -> TUIConstants.TUICalling.ERROR_NO_MESSAGE_TO_RECORD
|
||||
-4 -> TUIConstants.TUICalling.ERROR_SIGNATURE_ERROR
|
||||
-5 -> TUIConstants.TUICalling.ERROR_SIGNATURE_EXPIRED
|
||||
else -> TUIConstants.TUICalling.ERROR_NONE
|
||||
}
|
||||
return targetCode
|
||||
}
|
||||
|
||||
internal class AudioRecordInfo {
|
||||
var path: String? = null
|
||||
var sdkAppId = 0
|
||||
var signature: String? = null
|
||||
override fun toString(): String {
|
||||
return ("AudioRecordInfo{path=$path, SDKAppID=$sdkAppId}")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "TUIAudioMessageRecordService"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.manager.feature
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.AssetFileDescriptor
|
||||
import android.media.AudioAttributes
|
||||
import android.media.AudioManager
|
||||
import android.media.MediaPlayer
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
import android.text.TextUtils
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallDefine
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallEngine
|
||||
import com.tencent.liteav.audio.TXAudioEffectManager.AudioMusicParam
|
||||
import com.tencent.qcloud.tuicore.TUIConfig
|
||||
import com.tencent.qcloud.tuicore.TUIConstants
|
||||
import com.tencent.qcloud.tuicore.util.SPUtils
|
||||
import com.tencent.qcloud.tuicore.util.TUIBuild
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.R
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.common.data.Logger
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.common.utils.DeviceUtils
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.common.utils.PermissionRequest
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.manager.CallManager
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.manager.PushManager
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.state.GlobalState
|
||||
import com.trtc.tuikit.common.livedata.Observer
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
|
||||
class CallingBellFeature(context: Context) {
|
||||
private val context: Context = context.applicationContext
|
||||
private var mediaPlayer: MediaPlayer? = null
|
||||
private var handler: Handler? = null
|
||||
private var bellResourceId: Int
|
||||
private var bellResourcePath: String = ""
|
||||
private var dialPath: String? = null
|
||||
|
||||
private val callStatusObserver = Observer<TUICallDefine.Status> {
|
||||
if (it != TUICallDefine.Status.Waiting) {
|
||||
stopMusic()
|
||||
return@Observer
|
||||
}
|
||||
if (CallManager.instance.userState.selfUser.get().callRole == TUICallDefine.Role.Caller) {
|
||||
startDialingMusic()
|
||||
return@Observer
|
||||
}
|
||||
if (isLocalRingtonePlaybackNeeded()) {
|
||||
startRinging()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
bellResourceId = -1
|
||||
bellResourcePath = ""
|
||||
registerObserver()
|
||||
}
|
||||
|
||||
fun registerObserver() {
|
||||
CallManager.instance.userState.selfUser.get().callStatus.observe(callStatusObserver)
|
||||
}
|
||||
|
||||
private fun isLocalRingtonePlaybackNeeded(): Boolean {
|
||||
if (DeviceUtils.isAppRunningForeground(TUIConfig.getAppContext())) {
|
||||
Logger.i(TAG, "isLocalRingtonePlaybackNeeded, appRunningForeground, play local ringtone")
|
||||
return true
|
||||
}
|
||||
val notificationPermission = PermissionRequest.isNotificationEnabled()
|
||||
if (!notificationPermission) {
|
||||
Logger.w(TAG, "isLocalRingtonePlaybackNeeded, call has no notification permission, play local ringtone")
|
||||
return true
|
||||
}
|
||||
val pushData = PushManager.getPushData()
|
||||
val isFCMDataChannel = pushData.channelId == TUIConstants.DeviceInfo.BRAND_GOOGLE_ELSE
|
||||
val isPushRegisterSuccess = pushData.status == PushManager.PUSH_REGISTER_SUCCESS
|
||||
|
||||
val hint = if (isFCMDataChannel || !isPushRegisterSuccess) "local" else "push"
|
||||
Logger.i(
|
||||
TAG, "isLocalRingtonePlaybackNeeded, pushData:$pushData, play $hint ringtone" +
|
||||
"(only when channel is Google or register failed, play local ringtone)"
|
||||
)
|
||||
|
||||
return isFCMDataChannel || !isPushRegisterSuccess
|
||||
}
|
||||
|
||||
private fun startRinging() {
|
||||
if (GlobalState.instance.enableMuteMode) {
|
||||
return
|
||||
}
|
||||
val path = SPUtils.getInstance(PROFILE_TUICALLKIT).getString(PROFILE_CALL_BELL, "")
|
||||
if (TextUtils.isEmpty(path)) {
|
||||
start("", R.raw.phone_ringing)
|
||||
} else {
|
||||
start(path, -1)
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopMusic() {
|
||||
if (CallManager.instance.userState.selfUser.get()?.callRole == TUICallDefine.Role.Caller) {
|
||||
TUICallEngine.createInstance(context).trtcCloudInstance.audioEffectManager.stopPlayMusic(AUDIO_DIAL_ID)
|
||||
} else {
|
||||
stopRinging()
|
||||
}
|
||||
}
|
||||
|
||||
private fun startDialingMusic() {
|
||||
if (TextUtils.isEmpty(dialPath)) {
|
||||
dialPath = getBellPath(context, R.raw.phone_dialing, "phone_dialing.mp3")
|
||||
}
|
||||
TUICallEngine.createInstance(context).trtcCloudInstance
|
||||
.audioEffectManager.setMusicPlayoutVolume(AUDIO_DIAL_ID, 100)
|
||||
val param = AudioMusicParam(AUDIO_DIAL_ID, dialPath)
|
||||
param.isShortFile = true
|
||||
TUICallEngine.createInstance(context).trtcCloudInstance.audioEffectManager.startPlayMusic(param)
|
||||
}
|
||||
|
||||
private fun start(resPath: String, resId: Int) {
|
||||
preHandler()
|
||||
if (TextUtils.isEmpty(resPath) && resId == -1) {
|
||||
return
|
||||
}
|
||||
if (resId != -1 && bellResourceId == resId
|
||||
|| !TextUtils.isEmpty(resPath) && TextUtils.equals(bellResourcePath, resPath)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(resPath) && isUrl(resPath)) {
|
||||
return
|
||||
}
|
||||
|
||||
var assetFileDescriptor: AssetFileDescriptor? = null
|
||||
if (!TextUtils.isEmpty(resPath) && File(resPath).exists()) {
|
||||
bellResourcePath = resPath
|
||||
} else if (-1 != resId) {
|
||||
bellResourceId = resId
|
||||
assetFileDescriptor = context.resources.openRawResourceFd(resId)
|
||||
}
|
||||
|
||||
val afd = assetFileDescriptor
|
||||
handler?.post(Runnable {
|
||||
if (mediaPlayer == null) {
|
||||
mediaPlayer = MediaPlayer()
|
||||
}
|
||||
if (mediaPlayer?.isPlaying == true) {
|
||||
mediaPlayer?.stop()
|
||||
}
|
||||
mediaPlayer?.reset()
|
||||
|
||||
if (TUIBuild.getVersionInt() >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
val attrs = AudioAttributes.Builder()
|
||||
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
||||
.build()
|
||||
mediaPlayer?.setAudioAttributes(attrs)
|
||||
}
|
||||
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager?
|
||||
audioManager?.mode = AudioManager.MODE_RINGTONE
|
||||
mediaPlayer?.setAudioStreamType(AudioManager.STREAM_VOICE_CALL)
|
||||
|
||||
try {
|
||||
if (null != afd) {
|
||||
mediaPlayer?.setDataSource(afd.fileDescriptor, afd.startOffset, afd.length)
|
||||
} else if (!TextUtils.isEmpty(bellResourcePath)) {
|
||||
mediaPlayer?.setDataSource(bellResourcePath)
|
||||
} else {
|
||||
return@Runnable
|
||||
}
|
||||
mediaPlayer?.isLooping = true
|
||||
mediaPlayer?.prepare()
|
||||
mediaPlayer?.start()
|
||||
} catch (e: java.lang.Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun isUrl(url: String): Boolean {
|
||||
return url.startsWith("http://") || url.startsWith("https://")
|
||||
}
|
||||
|
||||
private fun preHandler() {
|
||||
if (null != handler) {
|
||||
return
|
||||
}
|
||||
val thread = HandlerThread("Handler-MediaPlayer")
|
||||
thread.start()
|
||||
handler = Handler(thread.looper)
|
||||
}
|
||||
|
||||
private fun stopRinging() {
|
||||
if (-1 == bellResourceId && TextUtils.isEmpty(bellResourcePath)) {
|
||||
return
|
||||
}
|
||||
handler?.post {
|
||||
if (mediaPlayer?.isPlaying == true) {
|
||||
mediaPlayer?.stop()
|
||||
}
|
||||
bellResourceId = -1
|
||||
bellResourcePath = ""
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBellPath(context: Context, resId: Int, name: String): String? {
|
||||
val savePath = ContextCompat.getExternalFilesDirs(context, null)[0].absolutePath
|
||||
val dir = File(savePath)
|
||||
if (!dir.exists()) {
|
||||
dir.mkdir()
|
||||
}
|
||||
try {
|
||||
val file = File("$savePath/$name")
|
||||
if (file.exists()) {
|
||||
return file.absolutePath
|
||||
}
|
||||
val inputStream = context.resources.openRawResource(resId)
|
||||
val outputStream = FileOutputStream(file)
|
||||
val buffer = ByteArray(2048)
|
||||
var length: Int
|
||||
while (inputStream.read(buffer).also { length = it } > 0) {
|
||||
outputStream.write(buffer, 0, length)
|
||||
}
|
||||
outputStream.flush()
|
||||
outputStream.close()
|
||||
inputStream.close()
|
||||
return file.absolutePath
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "CallingBellFeature"
|
||||
const val PROFILE_TUICALLKIT = "per_profile_tuicallkit"
|
||||
const val PROFILE_CALL_BELL = "per_call_bell"
|
||||
const val PROFILE_MUTE_MODE = "per_mute_mode"
|
||||
const val AUDIO_DIAL_ID = 48
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.manager.feature
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.VibrationEffect
|
||||
import android.os.Vibrator
|
||||
import android.os.VibratorManager
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallDefine
|
||||
import com.tencent.qcloud.tuicore.util.TUIBuild
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.manager.CallManager
|
||||
import com.trtc.tuikit.common.livedata.Observer
|
||||
|
||||
class CallingVibratorFeature(context: Context) {
|
||||
private val context: Context = context.applicationContext
|
||||
private val vibrator: Vibrator
|
||||
|
||||
private var callStatusObserver = Observer<TUICallDefine.Status> {
|
||||
if (CallManager.instance.userState.selfUser.get().callRole == TUICallDefine.Role.Caller) {
|
||||
return@Observer
|
||||
}
|
||||
when (it) {
|
||||
TUICallDefine.Status.Waiting -> startVibrating()
|
||||
else -> stopVibrating()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
if (TUIBuild.getVersionInt() >= Build.VERSION_CODES.S) {
|
||||
val vibratorManager = this.context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
|
||||
vibrator = vibratorManager.defaultVibrator
|
||||
} else {
|
||||
vibrator = this.context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||
}
|
||||
|
||||
registerObserver()
|
||||
}
|
||||
|
||||
fun registerObserver() {
|
||||
CallManager.instance.userState.selfUser.get().callStatus.observe(callStatusObserver)
|
||||
}
|
||||
|
||||
private fun startVibrating() {
|
||||
if (vibrator.hasVibrator()) {
|
||||
val pattern = longArrayOf(0, 500, 1500)
|
||||
if (TUIBuild.getVersionInt() >= Build.VERSION_CODES.O) {
|
||||
val vibrationEffect = VibrationEffect.createWaveform(pattern, 1)
|
||||
vibrator.vibrate(vibrationEffect)
|
||||
} else {
|
||||
vibrator.vibrate(pattern, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopVibrating() {
|
||||
vibrator.cancel()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.manager.feature
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationChannelGroup
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.R
|
||||
|
||||
class NotificationFeature(ctx: Context) {
|
||||
private val context: Context = ctx.applicationContext
|
||||
private val channelGroupId = "callKitChannelGroupId"
|
||||
|
||||
// channelId only takes effect for the first time. If you want to change it,you need to uninstall the app and reinstall it.
|
||||
fun registerNotificationBannerChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
val channelGroupName = context.getString(R.string.tuicallkit_notification_channel_group_id)
|
||||
val channelGroup = NotificationChannelGroup(channelGroupId, channelGroupName)
|
||||
nm.createNotificationChannelGroup(channelGroup)
|
||||
|
||||
val channelName = context.getString(R.string.tuicallkit_notification_channel_id)
|
||||
val channel = NotificationChannel(CHANNEL_ID, channelName, NotificationManager.IMPORTANCE_HIGH)
|
||||
channel.group = channelGroupId
|
||||
channel.enableLights(true)
|
||||
channel.enableVibration(true)
|
||||
channel.setShowBadge(true)
|
||||
channel.setSound(null, null)
|
||||
channel.setBypassDnd(true)
|
||||
channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
||||
nm.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val CHANNEL_ID = "CallKitChannelId"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.manager.hybird
|
||||
|
||||
import android.content.Context
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallDefine
|
||||
import com.tencent.cloud.tuikit.engine.common.TUICommonDefine
|
||||
import com.tencent.liteav.base.ThreadUtils
|
||||
import com.tencent.qcloud.tuicore.TUIConfig
|
||||
import com.tencent.qcloud.tuicore.TUILogin
|
||||
import com.tencent.qcloud.tuicore.interfaces.TUICallback
|
||||
import com.tencent.qcloud.tuicore.permission.PermissionCallback
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.common.data.Constants
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.common.data.Logger
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.common.utils.PermissionRequest
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.manager.CallManager
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.state.ViewState
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.view.component.floatwindow.FloatWindowView
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.view.component.videolayout.VideoFactory
|
||||
import com.trtc.tuikit.common.permission.PermissionRequester
|
||||
import com.trtc.tuikit.common.ui.floatwindow.FloatWindowManager
|
||||
import com.trtc.tuikit.common.ui.floatwindow.FloatWindowObserver
|
||||
import org.json.JSONObject
|
||||
|
||||
class CallBridge private constructor(context: Context) {
|
||||
private val context = context
|
||||
fun login(
|
||||
context: Context, sdkAppId: Int, userId: String, userSig: String, frameWork: Int, component: Int, language: Int,
|
||||
callback: TUICommonDefine.Callback?
|
||||
|
||||
) {
|
||||
runOnUiThread {
|
||||
Constants.framework = frameWork
|
||||
Constants.component = component
|
||||
Constants.language = language
|
||||
val params = JSONObject()
|
||||
params.put("framework", frameWork)
|
||||
params.put("component", component)
|
||||
params.put("language", language)
|
||||
|
||||
val jsonObject = JSONObject()
|
||||
jsonObject.put("api", "setFramework")
|
||||
jsonObject.put("params", params)
|
||||
CallManager.instance.callExperimentalAPI(jsonObject.toString())
|
||||
|
||||
TUILogin.login(context, sdkAppId, userId, userSig, object : TUICallback() {
|
||||
override fun onSuccess() {
|
||||
Logger.i(TAG, "login success, sdkAppId: $sdkAppId, userId: $userId")
|
||||
callback?.onSuccess()
|
||||
}
|
||||
|
||||
override fun onError(errCode: Int, errMsg: String?) {
|
||||
Logger.e(TAG, "login failed, errCode: $errCode, errMsg: $errMsg")
|
||||
callback?.onError(errCode, errMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fun logout(callback: TUICommonDefine.Callback?) {
|
||||
runOnUiThread {
|
||||
TUILogin.logout(object : TUICallback() {
|
||||
override fun onSuccess() {
|
||||
Logger.i(TAG, "logout success")
|
||||
callback?.onSuccess()
|
||||
}
|
||||
|
||||
override fun onError(errCode: Int, errMsg: String?) {
|
||||
Logger.e(TAG, "logout failed, errCode: $errCode, errMsg: $errMsg")
|
||||
callback?.onError(errCode, errMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fun calls(
|
||||
userIdList: List<String?>?, mediaType: TUICallDefine.MediaType?, params: TUICallDefine.CallParams?,
|
||||
callback: TUICommonDefine.Callback?
|
||||
) {
|
||||
runOnUiThread {
|
||||
CallManager.instance.calls(userIdList, mediaType, params, callback)
|
||||
}
|
||||
}
|
||||
|
||||
fun join(callId: String?, callback: TUICommonDefine.Callback?) {
|
||||
runOnUiThread {
|
||||
CallManager.instance.join(callId, callback)
|
||||
}
|
||||
}
|
||||
|
||||
fun inviteUser(userIdList: List<String?>?, callback: TUICommonDefine.Callback?) {
|
||||
runOnUiThread {
|
||||
CallManager.instance.inviteUser(userIdList, callback)
|
||||
}
|
||||
}
|
||||
|
||||
fun accept(callback: TUICommonDefine.Callback?) {
|
||||
runOnUiThread {
|
||||
CallManager.instance.accept(callback)
|
||||
}
|
||||
}
|
||||
|
||||
fun reject(callback: TUICommonDefine.Callback?) {
|
||||
runOnUiThread {
|
||||
CallManager.instance.reject(callback)
|
||||
}
|
||||
}
|
||||
|
||||
fun hangup(callback: TUICommonDefine.Callback?) {
|
||||
runOnUiThread {
|
||||
CallManager.instance.hangup(callback)
|
||||
}
|
||||
}
|
||||
|
||||
fun openCamera(callback: TUICommonDefine.Callback?) {
|
||||
runOnUiThread {
|
||||
val selfUser = CallManager.instance.userState.selfUser.get()
|
||||
val videoView = VideoFactory.instance.createVideoView(context, selfUser)
|
||||
val camera = CallManager.instance.mediaState.isFrontCamera.get()
|
||||
CallManager.instance.openCamera(camera, videoView, callback)
|
||||
}
|
||||
}
|
||||
|
||||
fun closeCamera() {
|
||||
runOnUiThread {
|
||||
CallManager.instance.closeCamera()
|
||||
}
|
||||
}
|
||||
|
||||
fun switchCamera(camera: TUICommonDefine.Camera) {
|
||||
runOnUiThread {
|
||||
CallManager.instance.switchCamera(camera)
|
||||
}
|
||||
}
|
||||
|
||||
fun openMicrophone(callback: TUICommonDefine.Callback?) {
|
||||
runOnUiThread {
|
||||
CallManager.instance.openMicrophone(callback)
|
||||
}
|
||||
}
|
||||
|
||||
fun closeMicrophone() {
|
||||
runOnUiThread {
|
||||
CallManager.instance.closeMicrophone()
|
||||
}
|
||||
}
|
||||
|
||||
fun selectAudioPlaybackDevice(device: TUICommonDefine.AudioPlaybackDevice) {
|
||||
runOnUiThread {
|
||||
CallManager.instance.selectAudioPlaybackDevice(device)
|
||||
}
|
||||
}
|
||||
|
||||
fun enableMultiDeviceAbility(enable: Boolean, callback: TUICommonDefine.Callback?) {
|
||||
runOnUiThread {
|
||||
CallManager.instance.enableMultiDeviceAbility(enable, callback)
|
||||
}
|
||||
}
|
||||
|
||||
fun setSelfInfo(nickname: String?, avatar: String?, callback: TUICommonDefine.Callback?) {
|
||||
runOnUiThread {
|
||||
CallManager.instance.setSelfInfo(nickname, avatar, callback)
|
||||
}
|
||||
}
|
||||
|
||||
fun setCallingBell(filePath: String?) {
|
||||
runOnUiThread {
|
||||
CallManager.instance.setCallingBell(filePath)
|
||||
}
|
||||
}
|
||||
|
||||
fun enableMuteMode(enable: Boolean) {
|
||||
runOnUiThread {
|
||||
CallManager.instance.enableMuteMode(enable)
|
||||
}
|
||||
}
|
||||
|
||||
fun enableFloatWindow(enable: Boolean) {
|
||||
runOnUiThread {
|
||||
CallManager.instance.enableFloatWindow(enable)
|
||||
}
|
||||
}
|
||||
|
||||
fun enableVirtualBackground(enable: Boolean) {
|
||||
runOnUiThread {
|
||||
CallManager.instance.enableVirtualBackground(enable)
|
||||
}
|
||||
}
|
||||
|
||||
fun setScreenOrientation(orientation: Int) {
|
||||
runOnUiThread {
|
||||
CallManager.instance.setScreenOrientation(orientation)
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: only support 0-close 3-high
|
||||
fun setBlurBackground(level: Int) {
|
||||
runOnUiThread {
|
||||
CallManager.instance.setBlurBackground(level > 0)
|
||||
}
|
||||
}
|
||||
|
||||
fun addFloatWindowObserver(observer: FloatWindowObserver) {
|
||||
runOnUiThread {
|
||||
Logger.i(TAG, "addFloatWindowObserver, observer: $observer")
|
||||
FloatWindowManager.sharedInstance().addObserver(observer)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeFloatWindowObserver(observer: FloatWindowObserver) {
|
||||
runOnUiThread {
|
||||
Logger.i(TAG, "removeFloatWindowObserver, observer: $observer")
|
||||
FloatWindowManager.sharedInstance().removeObserver(observer)
|
||||
}
|
||||
}
|
||||
|
||||
fun startFloatWindow() {
|
||||
runOnUiThread {
|
||||
if (FloatWindowManager.sharedInstance().isShowing) {
|
||||
Logger.w(TAG, "There is already a floatWindow on display, do not open it again.")
|
||||
return@runOnUiThread
|
||||
}
|
||||
|
||||
if (PermissionRequester.newInstance(PermissionRequester.FLOAT_PERMISSION).has()) {
|
||||
CallManager.instance.viewState.router.set(ViewState.ViewRouter.FloatView)
|
||||
FloatWindowManager.sharedInstance().show(FloatWindowView(context.applicationContext))
|
||||
} else {
|
||||
PermissionRequester.newInstance(PermissionRequester.FLOAT_PERMISSION).request()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun stopFloatWindow() {
|
||||
runOnUiThread {
|
||||
FloatWindowManager.sharedInstance().dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
fun callExperimentalAPI(jsonStr: String) {
|
||||
runOnUiThread {
|
||||
CallManager.instance.callExperimentalAPI(jsonStr)
|
||||
}
|
||||
}
|
||||
|
||||
fun hasPermission(mediaType: TUICallDefine.MediaType?, callback: TUICommonDefine.Callback?) {
|
||||
runOnUiThread {
|
||||
if (mediaType == null) {
|
||||
callback?.onError(TUICommonDefine.Error.FAILED.value, "mediaType is null")
|
||||
return@runOnUiThread
|
||||
}
|
||||
PermissionRequest.requestPermissions(context, mediaType, object : PermissionCallback() {
|
||||
override fun onGranted() {
|
||||
callback?.onSuccess()
|
||||
}
|
||||
|
||||
override fun onDenied() {
|
||||
callback?.onError(TUICommonDefine.Error.PERMISSION_DENIED.value, "Permission denied")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun runOnUiThread(r: Runnable) {
|
||||
if (ThreadUtils.runningOnUiThread()) {
|
||||
r.run()
|
||||
} else {
|
||||
ThreadUtils.getUiThreadHandler().post(r)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "CallBridge"
|
||||
val instance: CallBridge = CallBridge(TUIConfig.getAppContext())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.manager.observer
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallDefine
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallObserver
|
||||
import com.tencent.cloud.tuikit.engine.common.TUICommonDefine
|
||||
import com.tencent.qcloud.tuicore.TUIConfig
|
||||
import com.tencent.qcloud.tuicore.TUILogin
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.common.data.Constants
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.common.data.Logger
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.manager.CallManager
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.manager.UserManager
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.state.UserState
|
||||
import com.trtc.tuikit.common.foregroundservice.AudioForegroundService
|
||||
import com.trtc.tuikit.common.foregroundservice.VideoForegroundService
|
||||
|
||||
class CallEngineObserver : TUICallObserver() {
|
||||
private var timeHandlerThread: HandlerThread? = null
|
||||
private var timeHandler: Handler? = null
|
||||
private var timeRunnable: Runnable? = null
|
||||
|
||||
override fun onError(errCode: Int, errMsg: String) {
|
||||
Logger.e(TAG, "onError, errCode: $errCode, errMsg: $errMsg")
|
||||
}
|
||||
|
||||
override fun onCallReceived(
|
||||
callId: String?, callerId: String?, calleeIdList: MutableList<String>?, mediaType: TUICallDefine.MediaType?,
|
||||
info: TUICallDefine.CallObserverExtraInfo?
|
||||
) {
|
||||
super.onCallReceived(callId, callerId, calleeIdList, mediaType, info)
|
||||
Logger.i(
|
||||
TAG, "onCallReceived, callId: $callId, callerId: $callerId, calleeIdList: $calleeIdList," +
|
||||
" mediaType: $mediaType, info: $info"
|
||||
)
|
||||
if (TUICallDefine.MediaType.Unknown == mediaType || calleeIdList.isNullOrEmpty()) {
|
||||
Logger.e(TAG, "mediaType is unknown or calleeIdList is empty")
|
||||
return
|
||||
}
|
||||
|
||||
if (calleeIdList.size >= Constants.MAX_USER) {
|
||||
Logger.w(TAG, "The users is limited to 9. For larger conference calls,try using TUIRoomKit")
|
||||
return
|
||||
}
|
||||
val groupId = info?.chatGroupId
|
||||
if (!groupId.isNullOrEmpty() || calleeIdList.size > 1) {
|
||||
CallManager.instance.callState.scene.set(TUICallDefine.Scene.GROUP_CALL)
|
||||
} else {
|
||||
CallManager.instance.callState.scene.set(TUICallDefine.Scene.SINGLE_CALL)
|
||||
}
|
||||
CallManager.instance.callState.callId = callId ?: ""
|
||||
CallManager.instance.callState.chatGroupId = groupId
|
||||
CallManager.instance.callState.mediaType.set(mediaType)
|
||||
|
||||
if (!callerId.isNullOrEmpty()) {
|
||||
val user = UserState.User()
|
||||
user.id = callerId
|
||||
UserManager.instance.updateUserInfo(user)
|
||||
user.callRole = TUICallDefine.Role.Caller
|
||||
user.callStatus.set(TUICallDefine.Status.Waiting)
|
||||
CallManager.instance.userState.remoteUserList.add(user)
|
||||
}
|
||||
|
||||
val userList = ArrayList<UserState.User>()
|
||||
for (userId in calleeIdList) {
|
||||
if (!userId.isNullOrEmpty() && userId != TUILogin.getLoginUser()) {
|
||||
val user = UserState.User()
|
||||
user.id = userId
|
||||
UserManager.instance.updateUserInfo(user)
|
||||
user.callRole = TUICallDefine.Role.Called
|
||||
user.callStatus.set(TUICallDefine.Status.Waiting)
|
||||
userList.add(user)
|
||||
}
|
||||
}
|
||||
CallManager.instance.userState.remoteUserList.addAll(userList)
|
||||
CallManager.instance.userState.selfUser.get().id = TUILogin.getUserId() ?: ""
|
||||
CallManager.instance.userState.selfUser.get().avatar.set(TUILogin.getFaceUrl())
|
||||
CallManager.instance.userState.selfUser.get().nickname.set(TUILogin.getNickName())
|
||||
CallManager.instance.userState.selfUser.get().callRole = TUICallDefine.Role.Called
|
||||
CallManager.instance.userState.selfUser.get().callStatus.set(TUICallDefine.Status.Waiting)
|
||||
|
||||
if (TUICallDefine.MediaType.Video == CallManager.instance.callState.mediaType.get()) {
|
||||
CallManager.instance.selectAudioPlaybackDevice(TUICommonDefine.AudioPlaybackDevice.Speakerphone)
|
||||
CallManager.instance.mediaState.isCameraOpened.set(true)
|
||||
} else {
|
||||
CallManager.instance.selectAudioPlaybackDevice(TUICommonDefine.AudioPlaybackDevice.Earpiece)
|
||||
CallManager.instance.mediaState.isCameraOpened.set(false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCallNotConnected(
|
||||
callId: String?, mediaType: TUICallDefine.MediaType?, reason: TUICallDefine.CallEndReason?, userId: String?,
|
||||
info: TUICallDefine.CallObserverExtraInfo?
|
||||
) {
|
||||
super.onCallNotConnected(callId, mediaType, reason, userId, info)
|
||||
Logger.i(
|
||||
TAG, "onCallNotConnected, callId: $callId, mediaType: $mediaType," +
|
||||
" reason: $reason, userId: $userId, info: $info"
|
||||
)
|
||||
CallManager.instance.reset()
|
||||
}
|
||||
|
||||
override fun onCallBegin(
|
||||
callId: String?, mediaType: TUICallDefine.MediaType?, info: TUICallDefine.CallObserverExtraInfo?
|
||||
) {
|
||||
super.onCallBegin(callId, mediaType, info)
|
||||
Logger.i(TAG, "onCallBegin, callId: $callId, mediaType: $mediaType, info: $info")
|
||||
CallManager.instance.callState.callId = callId ?: ""
|
||||
CallManager.instance.callState.roomId = info?.roomId
|
||||
CallManager.instance.callState.chatGroupId = info?.chatGroupId
|
||||
|
||||
val selfUser = CallManager.instance.userState.selfUser.get()
|
||||
if (selfUser.callStatus.get() != TUICallDefine.Status.Accept) {
|
||||
selfUser.callStatus.set(TUICallDefine.Status.Accept)
|
||||
}
|
||||
if (CallManager.instance.mediaState.isMicrophoneMuted.get()) {
|
||||
CallManager.instance.closeMicrophone()
|
||||
} else {
|
||||
CallManager.instance.openMicrophone(null)
|
||||
}
|
||||
startForegroundService()
|
||||
startTimeCount()
|
||||
}
|
||||
|
||||
override fun onCallEnd(
|
||||
callId: String?, mediaType: TUICallDefine.MediaType?, reason: TUICallDefine.CallEndReason?, userId: String?,
|
||||
totalTime: Long, info: TUICallDefine.CallObserverExtraInfo?
|
||||
) {
|
||||
super.onCallEnd(callId, mediaType, reason, userId, totalTime, info)
|
||||
Logger.i(
|
||||
TAG, "onCallEnd, callId: $callId, mediaType: $mediaType," +
|
||||
" reason: $reason, userId: $userId, totalTime: $totalTime, info: $info"
|
||||
)
|
||||
resetCall()
|
||||
}
|
||||
|
||||
override fun onCallMediaTypeChanged(oldMediaType: TUICallDefine.MediaType, newMediaType: TUICallDefine.MediaType) {
|
||||
if (oldMediaType != newMediaType) {
|
||||
CallManager.instance.callState.mediaType.set(newMediaType)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onUserReject(userId: String?) {
|
||||
Logger.i(TAG, "onUserReject, userId: $userId")
|
||||
removeUserOnLeave(userId)
|
||||
}
|
||||
|
||||
override fun onUserNoResponse(userId: String?) {
|
||||
Logger.i(TAG, "onUserNoResponse, userId: $userId")
|
||||
removeUserOnLeave(userId)
|
||||
}
|
||||
|
||||
override fun onUserLineBusy(userId: String?) {
|
||||
Logger.i(TAG, "onUserLineBusy, userId: $userId")
|
||||
removeUserOnLeave(userId)
|
||||
}
|
||||
|
||||
override fun onUserJoin(userId: String?) {
|
||||
Logger.i(TAG, "onUserJoin, userId: $userId")
|
||||
if (userId.isNullOrEmpty() || userId.contains(Constants.AI_TRANSLATION_ROBOT)) {
|
||||
return
|
||||
}
|
||||
|
||||
var user = findUser(userId)
|
||||
if (user == null) {
|
||||
user = UserState.User()
|
||||
user.id = userId
|
||||
}
|
||||
|
||||
user.callStatus.set(TUICallDefine.Status.Accept)
|
||||
|
||||
val remoteUserList = CallManager.instance.userState.remoteUserList.get()
|
||||
val selfUser = CallManager.instance.userState.selfUser.get()
|
||||
if (remoteUserList != null && !remoteUserList.contains(user) && user.id != selfUser.id) {
|
||||
CallManager.instance.userState.remoteUserList.add(user)
|
||||
}
|
||||
|
||||
if (user.nickname.get().isNullOrEmpty() || user.avatar.get().isNullOrEmpty()) {
|
||||
UserManager.instance.updateUserInfo(user)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onUserLeave(userId: String?) {
|
||||
Logger.i(TAG, "onUserLeave, userId: $userId")
|
||||
removeUserOnLeave(userId)
|
||||
}
|
||||
|
||||
override fun onUserInviting(userId: String?) {
|
||||
Logger.i(TAG, "onUserInviting, userId: $userId")
|
||||
|
||||
if (userId.isNullOrEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
var user = findUser(userId)
|
||||
if (user == null) {
|
||||
user = UserState.User()
|
||||
user.id = userId
|
||||
user.callRole = TUICallDefine.Role.Called
|
||||
}
|
||||
|
||||
val remoteUserList = CallManager.instance.userState.remoteUserList.get()
|
||||
if (remoteUserList != null && !remoteUserList.contains(user)) {
|
||||
user.callStatus.set(TUICallDefine.Status.Waiting)
|
||||
CallManager.instance.userState.remoteUserList.add(user)
|
||||
}
|
||||
|
||||
if (user.nickname.get().isNullOrEmpty() || user.avatar.get().isNullOrEmpty()) {
|
||||
UserManager.instance.updateUserInfo(user)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onUserVideoAvailable(userId: String, isVideoAvailable: Boolean) {
|
||||
Logger.i(TAG, "onUserVideoAvailable, userId: $userId, isVideoAvailable: $isVideoAvailable")
|
||||
|
||||
val user = findUser(userId)
|
||||
if (user != null && user.videoAvailable.get() != isVideoAvailable) {
|
||||
user.videoAvailable.set(isVideoAvailable)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onUserAudioAvailable(userId: String, isAudioAvailable: Boolean) {
|
||||
Logger.i(TAG, "onUserAudioAvailable, userId: $userId, isAudioAvailable: $isAudioAvailable")
|
||||
|
||||
val user = findUser(userId)
|
||||
if (user != null && user.audioAvailable.get() != isAudioAvailable) {
|
||||
user.audioAvailable.set(isAudioAvailable)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onUserVoiceVolumeChanged(volumeMap: MutableMap<String, Int>?) {
|
||||
if (TUICallDefine.Scene.SINGLE_CALL == CallManager.instance.callState.scene.get() || volumeMap.isNullOrEmpty()) {
|
||||
return
|
||||
}
|
||||
for (entry in volumeMap.entries) {
|
||||
if (null != entry && !entry.key.isNullOrEmpty()) {
|
||||
val user = findUser(entry.key)
|
||||
if (user != null && user.playoutVolume.get() != entry.value) {
|
||||
user.playoutVolume.set(entry.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onUserNetworkQualityChanged(networkQualityList: MutableList<TUICommonDefine.NetworkQualityInfo>?) {
|
||||
if (networkQualityList.isNullOrEmpty()) {
|
||||
return
|
||||
}
|
||||
val scene = CallManager.instance.callState.scene
|
||||
val iterator = networkQualityList.iterator()
|
||||
if (scene.get() == TUICallDefine.Scene.GROUP_CALL) {
|
||||
while (iterator.hasNext()) {
|
||||
val info = iterator.next()
|
||||
val user = findUser(info.userId)
|
||||
user?.networkQualityReminder?.set(isBadNetwork(info.quality))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (scene.get() == TUICallDefine.Scene.SINGLE_CALL) {
|
||||
var localQuality: TUICommonDefine.NetworkQuality = TUICommonDefine.NetworkQuality.UNKNOWN
|
||||
var remoteQuality: TUICommonDefine.NetworkQuality = TUICommonDefine.NetworkQuality.UNKNOWN
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
val info = iterator.next()
|
||||
if (CallManager.instance.userState.selfUser.get().id == info.userId) {
|
||||
localQuality = info.quality
|
||||
} else {
|
||||
remoteQuality = info.quality
|
||||
}
|
||||
}
|
||||
|
||||
val networkQualityReminder = CallManager.instance.callState.networkQualityReminder
|
||||
if (isBadNetwork(localQuality)) {
|
||||
networkQualityReminder.set(Constants.NetworkQualityHint.Local)
|
||||
} else if (isBadNetwork(remoteQuality)) {
|
||||
networkQualityReminder.set(Constants.NetworkQualityHint.Remote)
|
||||
} else {
|
||||
networkQualityReminder.set(Constants.NetworkQualityHint.None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onKickedOffline() {
|
||||
Logger.i(TAG, "onKickedOffline")
|
||||
CallManager.instance.hangup(null)
|
||||
resetCall()
|
||||
}
|
||||
|
||||
override fun onUserSigExpired() {
|
||||
Logger.i(TAG, "onUserSigExpired")
|
||||
CallManager.instance.hangup(null)
|
||||
resetCall()
|
||||
}
|
||||
|
||||
private fun resetCall() {
|
||||
stopTimeCount()
|
||||
stopForegroundService()
|
||||
CallManager.instance.reset()
|
||||
}
|
||||
|
||||
private fun startTimeCount() {
|
||||
if (timeHandlerThread == null) {
|
||||
timeHandlerThread = HandlerThread("time-count-thread")
|
||||
timeHandlerThread?.start()
|
||||
timeHandler = Handler(timeHandlerThread!!.looper)
|
||||
}
|
||||
timeRunnable = Runnable {
|
||||
val count = CallManager.instance.callState.callDurationCount.get() + 1
|
||||
CallManager.instance.callState.callDurationCount.set(count)
|
||||
timeHandler?.let {
|
||||
it.postDelayed(timeRunnable!!, 1000)
|
||||
}
|
||||
}
|
||||
timeHandler?.post(timeRunnable!!)
|
||||
}
|
||||
|
||||
private fun stopTimeCount() {
|
||||
if (timeHandler != null) {
|
||||
timeHandler?.removeCallbacks(timeRunnable!!)
|
||||
timeHandler = null
|
||||
}
|
||||
timeRunnable = null
|
||||
if (timeHandlerThread != null) {
|
||||
timeHandlerThread?.quitSafely()
|
||||
timeHandlerThread = null
|
||||
}
|
||||
CallManager.instance.callState.callDurationCount.set(0)
|
||||
}
|
||||
|
||||
private fun findUser(userId: String?): UserState.User? {
|
||||
if (userId.isNullOrEmpty()) {
|
||||
return null
|
||||
}
|
||||
if (userId == CallManager.instance.userState.selfUser.get().id) {
|
||||
return CallManager.instance.userState.selfUser.get()
|
||||
}
|
||||
for (user in CallManager.instance.userState.remoteUserList.get()) {
|
||||
if (!user.id.isNullOrEmpty() && userId == user.id) {
|
||||
return user
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun removeUserOnLeave(userId: String?) {
|
||||
val user = findUser(userId)
|
||||
if (user == null || user.id.isNullOrEmpty()) {
|
||||
return
|
||||
}
|
||||
user.reset()
|
||||
if (CallManager.instance.userState.remoteUserList.get().contains(user)) {
|
||||
CallManager.instance.userState.remoteUserList.remove(user)
|
||||
}
|
||||
if (TUICallDefine.Scene.SINGLE_CALL == CallManager.instance.callState.scene.get()) {
|
||||
resetCall()
|
||||
}
|
||||
}
|
||||
|
||||
private fun isBadNetwork(quality: TUICommonDefine.NetworkQuality?): Boolean {
|
||||
return quality == TUICommonDefine.NetworkQuality.BAD || quality == TUICommonDefine.NetworkQuality.VERY_BAD
|
||||
|| quality == TUICommonDefine.NetworkQuality.DOWN
|
||||
}
|
||||
|
||||
private fun startForegroundService() {
|
||||
if (CallManager.instance.callState.scene.get() == TUICallDefine.Scene.GROUP_CALL
|
||||
|| CallManager.instance.callState.mediaType.get() == TUICallDefine.MediaType.Video) {
|
||||
VideoForegroundService.start(TUIConfig.getAppContext(), "", "", 0)
|
||||
} else if (CallManager.instance.callState.mediaType.get() == TUICallDefine.MediaType.Audio) {
|
||||
AudioForegroundService.start(TUIConfig.getAppContext(), "", "", 0)
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopForegroundService() {
|
||||
VideoForegroundService.stop(TUIConfig.getAppContext())
|
||||
AudioForegroundService.stop(TUIConfig.getAppContext())
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "CallEngineObserver"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.state
|
||||
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallDefine
|
||||
import com.tencent.cloud.tuikit.engine.common.TUICommonDefine
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.common.data.Constants
|
||||
import com.trtc.tuikit.common.livedata.LiveData
|
||||
|
||||
class CallState {
|
||||
var callId: String = ""
|
||||
var roomId: TUICommonDefine.RoomId? = null
|
||||
var chatGroupId: String? = ""
|
||||
var scene: LiveData<TUICallDefine.Scene> = LiveData(TUICallDefine.Scene.SINGLE_CALL)
|
||||
var mediaType: LiveData<TUICallDefine.MediaType> = LiveData(TUICallDefine.MediaType.Unknown)
|
||||
var callDurationCount: LiveData<Int> = LiveData(0)
|
||||
var networkQualityReminder: LiveData<Constants.NetworkQualityHint> = LiveData(Constants.NetworkQualityHint.None)
|
||||
|
||||
fun reset() {
|
||||
callId = ""
|
||||
roomId = null
|
||||
chatGroupId = ""
|
||||
scene.set(TUICallDefine.Scene.SINGLE_CALL)
|
||||
mediaType.set(TUICallDefine.MediaType.Unknown)
|
||||
callDurationCount.set(0)
|
||||
networkQualityReminder.set(Constants.NetworkQualityHint.None)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "CallState{roomId=$roomId, callId=$callId, chatGroupId=$chatGroupId, scene=${scene.get()}, " +
|
||||
"mediaType=${mediaType.get()}," + " callDurationCount=${callDurationCount.get()}," +
|
||||
" networkQualityReminder=${networkQualityReminder.get()}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.state
|
||||
|
||||
import com.tencent.qcloud.tuicore.util.SPUtils
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.common.data.Constants
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.manager.feature.CallingBellFeature
|
||||
|
||||
class GlobalState private constructor() {
|
||||
var enableMultiDevice: Boolean = false
|
||||
var enableMuteMode: Boolean = SPUtils.getInstance(CallingBellFeature.PROFILE_TUICALLKIT)
|
||||
.getBoolean(CallingBellFeature.PROFILE_MUTE_MODE, false)
|
||||
var enableFloatWindow: Boolean = true
|
||||
var enableIncomingBanner: Boolean = false
|
||||
var enableVirtualBackground: Boolean = false
|
||||
var orientation = Constants.Orientation.Portrait
|
||||
var enableForceUseV2API = false
|
||||
|
||||
companion object {
|
||||
val instance: GlobalState by lazy { GlobalState() }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.state
|
||||
|
||||
import com.tencent.cloud.tuikit.engine.common.TUICommonDefine
|
||||
import com.tencent.cloud.tuikit.engine.common.TUICommonDefine.AudioPlaybackDevice
|
||||
import com.trtc.tuikit.common.livedata.LiveData
|
||||
|
||||
class MediaState {
|
||||
var isMicrophoneMuted: LiveData<Boolean> = LiveData(false)
|
||||
var audioPlayoutDevice: LiveData<AudioPlaybackDevice> = LiveData(AudioPlaybackDevice.Speakerphone)
|
||||
|
||||
var isCameraOpened: LiveData<Boolean> = LiveData(false)
|
||||
var isFrontCamera: LiveData<TUICommonDefine.Camera> = LiveData(TUICommonDefine.Camera.Front)
|
||||
|
||||
fun reset() {
|
||||
isMicrophoneMuted.set(false)
|
||||
audioPlayoutDevice.set(AudioPlaybackDevice.Speakerphone)
|
||||
|
||||
isCameraOpened.set(false)
|
||||
isFrontCamera.set(TUICommonDefine.Camera.Front)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.state
|
||||
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallDefine
|
||||
import com.trtc.tuikit.common.livedata.LiveData
|
||||
|
||||
class UserState {
|
||||
var selfUser: LiveData<User> = LiveData(User())
|
||||
var remoteUserList: LiveData<LinkedHashSet<User>> = LiveData(LinkedHashSet())
|
||||
|
||||
fun reset() {
|
||||
selfUser.get().reset()
|
||||
for (user in remoteUserList.get()) {
|
||||
user.reset()
|
||||
}
|
||||
remoteUserList.set(LinkedHashSet())
|
||||
}
|
||||
|
||||
class User {
|
||||
var id: String = ""
|
||||
var nickname: LiveData<String> = LiveData("")
|
||||
var avatar: LiveData<String> = LiveData("")
|
||||
|
||||
var callRole: TUICallDefine.Role = TUICallDefine.Role.None
|
||||
var callStatus: LiveData<TUICallDefine.Status> = LiveData(TUICallDefine.Status.None)
|
||||
|
||||
var audioAvailable: LiveData<Boolean> = LiveData(false)
|
||||
var videoAvailable: LiveData<Boolean> = LiveData(false)
|
||||
var playoutVolume: LiveData<Int> = LiveData(0)
|
||||
var networkQualityReminder: LiveData<Boolean> = LiveData(false)
|
||||
|
||||
fun reset() {
|
||||
avatar.set("")
|
||||
nickname.set("")
|
||||
callStatus.set(TUICallDefine.Status.None)
|
||||
callRole = TUICallDefine.Role.None
|
||||
audioAvailable.set(false)
|
||||
videoAvailable.set(false)
|
||||
playoutVolume.set(0)
|
||||
networkQualityReminder.set(false)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other == null || this.javaClass != other.javaClass) {
|
||||
return false
|
||||
}
|
||||
if (this === other) {
|
||||
return true
|
||||
}
|
||||
val model = other as User
|
||||
return id == model.id
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "User {id: $id, avatar: ${avatar.get()}, nickname: ${nickname.get()}, " +
|
||||
"callRole: $callRole, callStatus: ${callStatus.get()}, " +
|
||||
"audioAvailable: ${audioAvailable.get()}, videoAvailable: ${videoAvailable.get()}, " +
|
||||
"playoutVolume: ${playoutVolume.get()}, networkQualityReminder: ${networkQualityReminder.get()}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.state
|
||||
|
||||
import com.trtc.tuikit.common.livedata.LiveData
|
||||
|
||||
class ViewState {
|
||||
var reverse1v1CallRenderView = false
|
||||
var showLargeViewUserId: LiveData<String> = LiveData("")
|
||||
var isVirtualBackgroundOpened: LiveData<Boolean> = LiveData(false)
|
||||
val isScreenCleaned: LiveData<Boolean> = LiveData(false)
|
||||
val router: LiveData<ViewRouter> = LiveData(ViewRouter.None)
|
||||
|
||||
fun reset() {
|
||||
reverse1v1CallRenderView = false
|
||||
showLargeViewUserId.set("")
|
||||
isVirtualBackgroundOpened.set(false)
|
||||
isScreenCleaned.set(false)
|
||||
router.set(ViewRouter.None)
|
||||
}
|
||||
|
||||
enum class ViewRouter {
|
||||
None,
|
||||
Banner,
|
||||
FullView,
|
||||
FloatView,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.view
|
||||
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallDefine
|
||||
import com.tencent.qcloud.tuicore.permission.PermissionCallback
|
||||
import com.tencent.qcloud.tuicore.util.TUIBuild
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.R
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.common.data.Constants
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.common.data.Logger
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.common.utils.DeviceUtils
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.common.utils.PermissionRequest
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.manager.CallManager
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.state.GlobalState
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.state.ViewState
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.view.component.videolayout.VideoFactory
|
||||
import com.trtc.tuikit.common.FullScreenActivity
|
||||
import com.trtc.tuikit.common.livedata.Observer
|
||||
import com.trtc.tuikit.common.ui.floatwindow.FloatWindowManager
|
||||
|
||||
class CallMainActivity : FullScreenActivity() {
|
||||
private var callView: ConstraintLayout? = null
|
||||
|
||||
private val callStatusObserver = Observer<TUICallDefine.Status> {
|
||||
if (it == TUICallDefine.Status.None) {
|
||||
Logger.i(TAG, "callStatusObserver, callStatus: $it")
|
||||
VideoFactory.instance.clearVideoView()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
private val viewRouterObserver = Observer<ViewState.ViewRouter> {
|
||||
if (it != ViewState.ViewRouter.FullView) {
|
||||
Logger.i(TAG, "viewRouterObserver, viewRouter: $it")
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
DeviceUtils.setScreenLockParams(window)
|
||||
if (TUIBuild.getVersionInt() >= Build.VERSION_CODES.O_MR1) {
|
||||
setShowWhenLocked(true)
|
||||
setTurnScreenOn(true)
|
||||
}
|
||||
setContentView(R.layout.tuicallkit_activity_call_kit)
|
||||
requestedOrientation = when (GlobalState.instance.orientation) {
|
||||
Constants.Orientation.Portrait -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
Constants.Orientation.LandScape -> ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
||||
else -> ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
PermissionRequest.requestPermissions(application, CallManager.instance.callState.mediaType.get(),
|
||||
object : PermissionCallback() {
|
||||
override fun onGranted() {
|
||||
initView()
|
||||
handleCallAcceptAction()
|
||||
registerObserver()
|
||||
}
|
||||
|
||||
override fun onDenied() {
|
||||
if (CallManager.instance.userState.selfUser.get().callRole == TUICallDefine.Role.Called) {
|
||||
CallManager.instance.reject(null)
|
||||
}
|
||||
finish()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
val scene = CallManager.instance.callState.scene
|
||||
val callStatus = CallManager.instance.userState.selfUser.get().callStatus.get()
|
||||
|
||||
Logger.i(TAG, "initView, scene: ${scene.get()} ,callStatus: $callStatus")
|
||||
if (TUICallDefine.Status.None == callStatus) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
callView?.removeAllViews()
|
||||
if (scene.get() == TUICallDefine.Scene.GROUP_CALL) {
|
||||
callView = GroupCallView(this)
|
||||
} else if (scene.get() == TUICallDefine.Scene.SINGLE_CALL) {
|
||||
callView = SingleCallView(this)
|
||||
}
|
||||
|
||||
val rootView = window.decorView.findViewById<View>(android.R.id.content) as ViewGroup
|
||||
rootView.addView(callView)
|
||||
|
||||
FloatWindowManager.sharedInstance().dismiss()
|
||||
CallManager.instance.viewState.router.set(ViewState.ViewRouter.FullView)
|
||||
}
|
||||
|
||||
private fun handleCallAcceptAction() {
|
||||
val selfUser = CallManager.instance.userState.selfUser.get()
|
||||
if (selfUser.callStatus.get() == TUICallDefine.Status.Accept) {
|
||||
return
|
||||
}
|
||||
if (intent.action == Constants.ACCEPT_CALL_ACTION) {
|
||||
Logger.i(TAG, "IncomingView -> handleCallAcceptAction")
|
||||
CallManager.instance.accept(null)
|
||||
if (CallManager.instance.callState.mediaType.get() == TUICallDefine.MediaType.Video) {
|
||||
val videoView = VideoFactory.instance.createVideoView(application, selfUser)
|
||||
val camera = CallManager.instance.mediaState.isFrontCamera.get()
|
||||
CallManager.instance.openCamera(camera, videoView, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerObserver() {
|
||||
CallManager.instance.userState.selfUser.get().callStatus.observe(callStatusObserver)
|
||||
CallManager.instance.viewState.router.observe(viewRouterObserver)
|
||||
}
|
||||
|
||||
private fun unregisterObserver() {
|
||||
CallManager.instance.userState.selfUser.get().callStatus.removeObserver(callStatusObserver)
|
||||
CallManager.instance.viewState.router.removeObserver(viewRouterObserver)
|
||||
}
|
||||
|
||||
override fun onBackPressed() {}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
Logger.i(TAG, "onDestroy")
|
||||
unregisterObserver()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "CallMainActivity"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.view
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallDefine
|
||||
import com.tencent.qcloud.tuicore.TUICore
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.R
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.common.data.Constants
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.manager.CallManager
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.state.GlobalState
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.view.component.function.CallFunctionLayout
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.view.component.hint.CallHintView
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.view.component.hint.CallTimerView
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.view.component.inviteuser.InviteUserButton
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.view.component.videolayout.CallVideoLayout
|
||||
import com.trtc.tuikit.common.imageloader.ImageLoader
|
||||
import com.trtc.tuikit.common.imageloader.ImageOptions
|
||||
|
||||
class GroupCallView(context: Context) : ConstraintLayout(context) {
|
||||
private val activityContext: Context = context
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
initView()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
LayoutInflater.from(activityContext).inflate(R.layout.tuicallkit_root_view_group, this)
|
||||
|
||||
addVideoLayout()
|
||||
addFunctionLayout()
|
||||
addCallTimeView()
|
||||
addCallHintView()
|
||||
addFloatButton()
|
||||
addInviteUserButton()
|
||||
setBackground()
|
||||
}
|
||||
|
||||
private fun addVideoLayout() {
|
||||
val layoutVideo: FrameLayout = findViewById(R.id.rl_layout_video_group)
|
||||
layoutVideo.addView(CallVideoLayout(activityContext))
|
||||
}
|
||||
|
||||
private fun addFunctionLayout() {
|
||||
val layoutFunction: FrameLayout = findViewById(R.id.rl_layout_function)
|
||||
layoutFunction.addView(CallFunctionLayout(activityContext))
|
||||
}
|
||||
|
||||
private fun addCallTimeView() {
|
||||
val layoutTimer: FrameLayout = findViewById(R.id.rl_layout_call_time)
|
||||
layoutTimer.addView(CallTimerView(activityContext))
|
||||
}
|
||||
|
||||
private fun addCallHintView() {
|
||||
val layoutCallHint: FrameLayout = findViewById(R.id.rl_layout_call_hint)
|
||||
layoutCallHint.addView(CallHintView(activityContext))
|
||||
|
||||
if (CallManager.instance.userState.selfUser.get().callRole == TUICallDefine.Role.Caller) {
|
||||
val lp = LayoutParams(LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
lp.startToStart = LayoutParams.PARENT_ID
|
||||
lp.endToEnd = LayoutParams.PARENT_ID
|
||||
lp.topToTop = R.id.gl_horizontal_top
|
||||
lp.topMargin = 20
|
||||
layoutCallHint.layoutParams = lp
|
||||
}
|
||||
}
|
||||
|
||||
private fun addFloatButton() {
|
||||
val floatButton: ImageView = findViewById(R.id.image_float_icon_group)
|
||||
if (!GlobalState.instance.enableFloatWindow) {
|
||||
floatButton.visibility = GONE
|
||||
}
|
||||
// TODO: 仅支持悬浮窗, 后续支持画中画
|
||||
floatButton.setOnClickListener {
|
||||
TUICore.notifyEvent(Constants.KEY_TUI_CALLKIT, Constants.SUB_KEY_SHOW_FLOAT_WINDOW, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun addInviteUserButton() {
|
||||
if (CallManager.instance.callState.chatGroupId.isNullOrEmpty()) {
|
||||
return
|
||||
}
|
||||
val inviteUserButton: FrameLayout = findViewById(R.id.rl_layout_invite_user)
|
||||
inviteUserButton.addView(InviteUserButton(activityContext))
|
||||
}
|
||||
|
||||
private fun setBackground() {
|
||||
val imageBackground: ImageView = findViewById(R.id.img_group_view_background)
|
||||
val selfUser = CallManager.instance.userState.selfUser.get()
|
||||
val option = ImageOptions.Builder().setPlaceImage(R.drawable.tuicallkit_ic_avatar).setBlurEffect(80f).build()
|
||||
ImageLoader.load(activityContext, imageBackground, selfUser.avatar.get(), option)
|
||||
imageBackground.setColorFilter(ContextCompat.getColor(activityContext, R.color.tuicallkit_color_blur_mask))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.view
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import com.tencent.qcloud.tuicore.TUICore
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.R
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.common.data.Constants
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.manager.CallManager
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.state.GlobalState
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.view.component.function.CallFunctionLayout
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.view.component.hint.CallHintView
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.view.component.hint.CallTimerView
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.view.component.videolayout.CallVideoLayout
|
||||
import com.trtc.tuikit.common.livedata.Observer
|
||||
|
||||
class SingleCallView(context: Context) : ConstraintLayout(context) {
|
||||
private val activityContext = context
|
||||
private lateinit var layoutFunction: FrameLayout
|
||||
private lateinit var layoutTimer: FrameLayout
|
||||
private lateinit var layoutCallHint: FrameLayout
|
||||
private lateinit var floatButton: ImageView
|
||||
|
||||
private var isScreenCleanedObserver = Observer<Boolean> {
|
||||
layoutFunction.visibility = if (it) View.GONE else View.VISIBLE
|
||||
layoutTimer.visibility = if (it) View.GONE else View.VISIBLE
|
||||
layoutCallHint.visibility = if (it) View.GONE else View.VISIBLE
|
||||
floatButton.visibility = if (it) View.GONE else View.VISIBLE
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
initView()
|
||||
registerObserver()
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
unregisterObserver()
|
||||
}
|
||||
|
||||
private fun registerObserver() {
|
||||
CallManager.instance.viewState.isScreenCleaned.observe(isScreenCleanedObserver)
|
||||
}
|
||||
|
||||
private fun unregisterObserver() {
|
||||
CallManager.instance.viewState.isScreenCleaned.removeObserver(isScreenCleanedObserver)
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
LayoutInflater.from(activityContext).inflate(R.layout.tuicallkit_root_view_single, this)
|
||||
|
||||
addVideoLayout()
|
||||
addFunctionLayout()
|
||||
addCallTimeView()
|
||||
addCallHintView()
|
||||
addFloatButton()
|
||||
}
|
||||
|
||||
private fun addVideoLayout() {
|
||||
val layoutVideo: FrameLayout = findViewById(R.id.fl_video)
|
||||
layoutVideo.addView(CallVideoLayout(activityContext))
|
||||
}
|
||||
|
||||
private fun addFunctionLayout() {
|
||||
layoutFunction = findViewById(R.id.rl_single_function)
|
||||
layoutFunction.addView(CallFunctionLayout(activityContext))
|
||||
}
|
||||
|
||||
private fun addCallTimeView() {
|
||||
layoutTimer = findViewById(R.id.rl_single_time)
|
||||
layoutTimer.addView(CallTimerView(activityContext))
|
||||
}
|
||||
|
||||
private fun addCallHintView() {
|
||||
layoutCallHint = findViewById(R.id.fl_call_hint)
|
||||
layoutCallHint.addView(CallHintView(activityContext))
|
||||
}
|
||||
|
||||
private fun addFloatButton() {
|
||||
floatButton = findViewById(R.id.image_float_icon)
|
||||
if (!GlobalState.instance.enableFloatWindow) {
|
||||
floatButton.visibility = GONE
|
||||
}
|
||||
floatButton.setOnClickListener {
|
||||
TUICore.notifyEvent(Constants.KEY_TUI_CALLKIT, Constants.SUB_KEY_SHOW_FLOAT_WINDOW, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.view.component.aisubtitle
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.AttributeSet
|
||||
import androidx.appcompat.widget.AppCompatTextView
|
||||
import com.tencent.cloud.tuikit.engine.common.TUICommonDefine
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.common.data.Logger
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.manager.CallManager
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.manager.UserManager
|
||||
import com.tencent.trtc.TRTCCloudListener
|
||||
import org.json.JSONObject
|
||||
import java.util.Timer
|
||||
import java.util.TimerTask
|
||||
|
||||
class AISubtitle(context: Context, attrs: AttributeSet?) : AppCompatTextView(context, attrs) {
|
||||
private val messages = mutableListOf<Message>()
|
||||
private var hideTimer: Timer? = null
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
data class Message(
|
||||
val sender: String,
|
||||
val text: String
|
||||
)
|
||||
|
||||
private val trtcCloudListener = object : TRTCCloudListener() {
|
||||
override fun onRecvCustomCmdMsg(
|
||||
userId: String?, cmdID: Int, seq: Int, message: ByteArray?
|
||||
) {
|
||||
super.onRecvCustomCmdMsg(userId, cmdID, seq, message)
|
||||
|
||||
if (userId == null || message == null) return
|
||||
|
||||
try {
|
||||
val messageString = String(message)
|
||||
|
||||
val jsonObject = JSONObject(messageString)
|
||||
val type = jsonObject.optInt("type")
|
||||
|
||||
if (type == AI_MESSAGE_TYPE) {
|
||||
val sender = jsonObject.optString("sender")
|
||||
val payload = jsonObject.optJSONObject("payload")
|
||||
val translationText = payload?.optString("translation_text")
|
||||
|
||||
if (sender.isNotEmpty() && !translationText.isNullOrEmpty()) {
|
||||
updateSubtitle(sender, translationText)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, "Parse custom message failed: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSubtitle(sender: String, text: String) {
|
||||
messages.add(Message(sender, text))
|
||||
if (messages.size > 2) {
|
||||
messages.removeAt(0)
|
||||
}
|
||||
|
||||
UserManager.instance.getUserDisplayName(
|
||||
sender,
|
||||
object : TUICommonDefine.ValueCallback<String> {
|
||||
override fun onSuccess(data: String?) {
|
||||
val formattedText = messages.joinToString("\n") {
|
||||
"${data}: ${it.text}"
|
||||
}
|
||||
|
||||
mainHandler.post {
|
||||
setText(formattedText)
|
||||
visibility = VISIBLE
|
||||
|
||||
hideTimer?.cancel()
|
||||
hideTimer = Timer().apply {
|
||||
schedule(object : TimerTask() {
|
||||
override fun run() {
|
||||
mainHandler.post {
|
||||
visibility = GONE
|
||||
}
|
||||
}
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(errCode: Int, errMsg: String?) {
|
||||
Logger.e(TAG, "Parse custom message failed: $errMsg")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
CallManager.instance.getTRTCCloudInstance().addListener(trtcCloudListener)
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
CallManager.instance.getTRTCCloudInstance().removeListener(trtcCloudListener)
|
||||
hideTimer?.cancel()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "AISubtitle"
|
||||
private const val AI_MESSAGE_TYPE = 10000
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.view.component.floatwindow
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallDefine
|
||||
import com.tencent.qcloud.tuicore.util.DateTimeUtil
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.R
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.common.data.Constants
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.manager.CallManager
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.state.UserState
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.view.component.videolayout.VideoFactory
|
||||
import com.trtc.tuikit.common.livedata.Observer
|
||||
import com.trtc.tuikit.common.ui.floatwindow.FloatWindowManager
|
||||
|
||||
class FloatWindowView(context: Context) : RelativeLayout(context) {
|
||||
private val context: Context = context.applicationContext
|
||||
private lateinit var layoutVideoView: FrameLayout
|
||||
private lateinit var imageAudioView: ImageView
|
||||
private lateinit var textCallStatus: TextView
|
||||
private lateinit var imageFloatVideoMark: ImageView
|
||||
private lateinit var imageFloatAudioMark: ImageView
|
||||
|
||||
private var remoteUser: UserState.User = UserState.User()
|
||||
private var currentShowVideoUserId: String = ""
|
||||
|
||||
private val observer: Observer<Any> = Observer {
|
||||
updateView()
|
||||
}
|
||||
|
||||
private val callDurationObserver: Observer<Int> = Observer {
|
||||
post {
|
||||
updateCallStatusHint()
|
||||
}
|
||||
}
|
||||
|
||||
private val mediaAvailableObserver: Observer<Boolean> = Observer {
|
||||
updateSelfAudioAndVideoView()
|
||||
}
|
||||
|
||||
init {
|
||||
val remoteUserList = CallManager.instance.userState.remoteUserList.get()
|
||||
if (remoteUserList != null && remoteUserList.size > 0) {
|
||||
remoteUser = remoteUserList.first()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
initView()
|
||||
registerObserver()
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
unregisterObserver()
|
||||
}
|
||||
|
||||
private fun registerObserver() {
|
||||
CallManager.instance.userState.selfUser.get().callStatus.observe(observer)
|
||||
CallManager.instance.userState.selfUser.get().videoAvailable.observe(mediaAvailableObserver)
|
||||
CallManager.instance.userState.selfUser.get().audioAvailable.observe(mediaAvailableObserver)
|
||||
CallManager.instance.callState.callDurationCount.observe(callDurationObserver)
|
||||
|
||||
if (CallManager.instance.callState.scene.get() == TUICallDefine.Scene.GROUP_CALL) {
|
||||
CallManager.instance.userState.selfUser.get().playoutVolume.observe(observer)
|
||||
for (user in CallManager.instance.userState.remoteUserList.get()) {
|
||||
user.playoutVolume.observe(observer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun unregisterObserver() {
|
||||
CallManager.instance.userState.selfUser.get().callStatus.removeObserver(observer)
|
||||
CallManager.instance.userState.selfUser.get().videoAvailable.removeObserver(mediaAvailableObserver)
|
||||
CallManager.instance.userState.selfUser.get().audioAvailable.removeObserver(mediaAvailableObserver)
|
||||
CallManager.instance.callState.callDurationCount.removeObserver(callDurationObserver)
|
||||
|
||||
if (CallManager.instance.callState.scene.get() == TUICallDefine.Scene.GROUP_CALL) {
|
||||
CallManager.instance.userState.selfUser.get().playoutVolume.removeObserver(observer)
|
||||
for (user in CallManager.instance.userState.remoteUserList.get()) {
|
||||
user.playoutVolume.removeObserver(observer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
LayoutInflater.from(context).inflate(R.layout.tuicallkit_float_call_view, this)
|
||||
layoutVideoView = findViewById(R.id.rl_video_view)
|
||||
imageAudioView = findViewById(R.id.iv_audio_view)
|
||||
textCallStatus = findViewById(R.id.tv_float_call_status)
|
||||
imageFloatVideoMark = findViewById(R.id.iv_float_video_mark)
|
||||
imageFloatAudioMark = findViewById(R.id.iv_float_audio_mark)
|
||||
|
||||
updateView()
|
||||
updateCallStatusHint()
|
||||
}
|
||||
|
||||
private fun updateView() {
|
||||
if (CallManager.instance.userState.selfUser.get().callStatus.get() == TUICallDefine.Status.None) {
|
||||
VideoFactory.instance.clearVideoView()
|
||||
FloatWindowManager.sharedInstance().dismiss()
|
||||
return
|
||||
}
|
||||
|
||||
if (CallManager.instance.callState.scene.get() == TUICallDefine.Scene.GROUP_CALL) {
|
||||
updateGroupCallLayout()
|
||||
} else {
|
||||
updateSingleCallLayout()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSingleCallLayout() {
|
||||
val isAudio = CallManager.instance.callState.mediaType.get() == TUICallDefine.MediaType.Audio
|
||||
if (isAudio) {
|
||||
imageAudioView.visibility = VISIBLE
|
||||
layoutVideoView.visibility = GONE
|
||||
return
|
||||
}
|
||||
|
||||
imageAudioView.visibility = GONE
|
||||
layoutVideoView.visibility = VISIBLE
|
||||
val wWidth = context.resources.getDimension(R.dimen.tuicallkit_video_small_view_width).toInt()
|
||||
val hHeight = context.resources.getDimension(R.dimen.tuicallkit_video_small_view_height).toInt()
|
||||
layoutVideoView.layoutParams.width = wWidth
|
||||
layoutVideoView.layoutParams.height = hHeight
|
||||
|
||||
val selfUser = CallManager.instance.userState.selfUser.get()
|
||||
textCallStatus.visibility = when {
|
||||
selfUser.callStatus.get() == TUICallDefine.Status.Waiting -> VISIBLE
|
||||
else -> GONE
|
||||
}
|
||||
|
||||
if (selfUser.callStatus.get() == TUICallDefine.Status.Accept) {
|
||||
val videoView = VideoFactory.instance.createVideoView(context, remoteUser)
|
||||
resetView(videoView)
|
||||
CallManager.instance.startRemoteView(remoteUser.id, videoView, null)
|
||||
layoutVideoView.addView(videoView)
|
||||
} else {
|
||||
val videoView = VideoFactory.instance.createVideoView(context, selfUser)
|
||||
resetView(videoView)
|
||||
layoutVideoView.addView(videoView)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateGroupCallLayout() {
|
||||
updateSelfAudioAndVideoView()
|
||||
if (CallManager.instance.userState.selfUser.get().callStatus.get() == TUICallDefine.Status.Waiting) {
|
||||
imageAudioView.visibility = VISIBLE
|
||||
layoutVideoView.visibility = GONE
|
||||
return
|
||||
}
|
||||
|
||||
val list = ArrayList<UserState.User>()
|
||||
list.add(CallManager.instance.userState.selfUser.get())
|
||||
list.addAll(CallManager.instance.userState.remoteUserList.get())
|
||||
|
||||
for (user in list) {
|
||||
if (user.playoutVolume.get() > Constants.MIN_AUDIO_VOLUME) {
|
||||
if (user.id == currentShowVideoUserId) {
|
||||
return
|
||||
}
|
||||
currentShowVideoUserId = user.id
|
||||
imageAudioView.visibility = GONE
|
||||
textCallStatus.visibility = GONE
|
||||
layoutVideoView.visibility = VISIBLE
|
||||
|
||||
var videoView = VideoFactory.instance.findVideoView(user.id)
|
||||
resetView(videoView)
|
||||
if (videoView == null) {
|
||||
videoView = VideoFactory.instance.createVideoView(context, user)
|
||||
CallManager.instance.startRemoteView(user.id, videoView, null)
|
||||
}
|
||||
|
||||
layoutVideoView.removeAllViews()
|
||||
layoutVideoView.addView(videoView)
|
||||
return
|
||||
}
|
||||
}
|
||||
currentShowVideoUserId = ""
|
||||
imageAudioView.visibility = VISIBLE
|
||||
textCallStatus.visibility = VISIBLE
|
||||
layoutVideoView.visibility = GONE
|
||||
}
|
||||
|
||||
private fun updateCallStatusHint() {
|
||||
val callStatus = CallManager.instance.userState.selfUser.get().callStatus.get()
|
||||
val isAudio = CallManager.instance.callState.mediaType.get() == TUICallDefine.MediaType.Audio
|
||||
val isSingleCall = CallManager.instance.callState.scene.get() == TUICallDefine.Scene.SINGLE_CALL
|
||||
|
||||
textCallStatus.text = when (callStatus) {
|
||||
TUICallDefine.Status.Waiting -> context.getString(R.string.tuicallkit_wait_response)
|
||||
else -> DateTimeUtil.formatSecondsTo00(CallManager.instance.callState.callDurationCount.get())
|
||||
}
|
||||
|
||||
if (isSingleCall && !isAudio && callStatus == TUICallDefine.Status.Waiting) {
|
||||
textCallStatus.setTextColor(ContextCompat.getColor(context, R.color.tuicallkit_color_white))
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSelfAudioAndVideoView() {
|
||||
if (CallManager.instance.callState.scene.get() != TUICallDefine.Scene.GROUP_CALL) {
|
||||
return
|
||||
}
|
||||
val selfUser = CallManager.instance.userState.selfUser.get()
|
||||
val videoResId = when {
|
||||
selfUser.videoAvailable.get() -> R.drawable.tuicallkit_ic_video_incoming
|
||||
else -> R.drawable.tuicallkit_ic_float_video_off
|
||||
}
|
||||
imageFloatVideoMark.setImageResource(videoResId)
|
||||
imageFloatVideoMark.visibility = VISIBLE
|
||||
|
||||
val audioResId = when {
|
||||
selfUser.audioAvailable.get() -> R.drawable.tuicallkit_ic_float_audio_on
|
||||
else -> R.drawable.tuicallkit_ic_float_audio_off
|
||||
}
|
||||
imageFloatAudioMark.setImageResource(audioResId)
|
||||
imageFloatAudioMark.visibility = VISIBLE
|
||||
}
|
||||
|
||||
private fun resetView(view: View?) {
|
||||
if (view?.parent != null) {
|
||||
(view.parent as ViewGroup).removeView(view)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.view.component.floatwindow
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Path
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffXfermode
|
||||
import android.graphics.RectF
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.util.AttributeSet
|
||||
import android.widget.RelativeLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.R
|
||||
|
||||
class RoundShadowLayout(context: Context, attrs: AttributeSet?) : RelativeLayout(context, attrs) {
|
||||
private val radiusArray = FloatArray(8)
|
||||
|
||||
private var shadowPaint: Paint = Paint()
|
||||
private var shadowRect: RectF = RectF()
|
||||
private var shadowPath: Path = Path()
|
||||
private var shadowRadius = 15f
|
||||
private var shadowColor = 0
|
||||
private var shadowX = 0f
|
||||
private var shadowY = 0f
|
||||
|
||||
private var roundRadius = 32f
|
||||
private var roundPaint: Paint = Paint()
|
||||
private var roundRect: RectF = RectF()
|
||||
private var roundPath: Path = Path()
|
||||
|
||||
constructor(context: Context) : this(context, null) {}
|
||||
|
||||
init {
|
||||
shadowColor = ContextCompat.getColor(context, R.color.tuicallkit_color_bg_float_view)
|
||||
for (i in radiusArray.indices) {
|
||||
radiusArray[i] = roundRadius
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
var width = 0
|
||||
var height = 0
|
||||
for (i in 0 until childCount) {
|
||||
val child = getChildAt(i)
|
||||
val lp = child.layoutParams
|
||||
val childWidthSpec = getChildMeasureSpec(widthMeasureSpec - shadowRadius.toInt() * 2, 0, lp.width)
|
||||
val childHeightSpec = getChildMeasureSpec(heightMeasureSpec - shadowRadius.toInt() * 2, 0, lp.height)
|
||||
measureChild(child, childWidthSpec, childHeightSpec)
|
||||
|
||||
val mlp = child.layoutParams as MarginLayoutParams
|
||||
val childWidth = child.measuredWidth + mlp.leftMargin + mlp.rightMargin
|
||||
val childHeight = child.measuredHeight + mlp.topMargin + mlp.bottomMargin
|
||||
width = width.coerceAtLeast(childWidth)
|
||||
height = height.coerceAtLeast(childHeight)
|
||||
}
|
||||
setMeasuredDimension(
|
||||
width + paddingLeft + paddingRight + shadowRadius.toInt() * 2,
|
||||
height + paddingTop + paddingBottom + shadowRadius.toInt() * 2
|
||||
)
|
||||
}
|
||||
|
||||
override fun onSizeChanged(width: Int, height: Int, oldw: Int, oldh: Int) {
|
||||
super.onSizeChanged(width, height, oldw, oldh)
|
||||
if (width > 0 && height > 0 && shadowRadius > 0) {
|
||||
setBackgroundCompat(width, height)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
||||
for (i in 0 until childCount) {
|
||||
val child = getChildAt(i)
|
||||
val lp = child.layoutParams as MarginLayoutParams
|
||||
val lc = shadowRadius.toInt() + lp.leftMargin + paddingLeft
|
||||
val tc = shadowRadius.toInt() + lp.topMargin + paddingTop
|
||||
val rc = lc + child.measuredWidth
|
||||
val bc = tc + child.measuredHeight
|
||||
child.layout(lc, tc, rc, bc)
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispatchDraw(canvas: Canvas) {
|
||||
roundRect[shadowRadius, shadowRadius, width - shadowRadius] = height - shadowRadius
|
||||
canvas.saveLayer(roundRect, null, Canvas.ALL_SAVE_FLAG)
|
||||
super.dispatchDraw(canvas)
|
||||
roundPath.reset()
|
||||
roundPath.addRoundRect(roundRect, radiusArray, Path.Direction.CW)
|
||||
|
||||
clipRound(canvas)
|
||||
canvas.restore()
|
||||
}
|
||||
|
||||
private fun clipRound(canvas: Canvas) {
|
||||
roundPaint.color = Color.WHITE
|
||||
roundPaint.isAntiAlias = true
|
||||
roundPaint.style = Paint.Style.FILL
|
||||
roundPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT)
|
||||
val path = Path()
|
||||
path.addRect(0f, 0f, width.toFloat(), height.toFloat(), Path.Direction.CW)
|
||||
path.op(roundPath, Path.Op.DIFFERENCE)
|
||||
canvas.drawPath(path, roundPaint)
|
||||
}
|
||||
|
||||
private fun setBackgroundCompat(width: Int, height: Int) {
|
||||
val bitmap: Bitmap = createShadowBitmap(width, height, shadowRadius, shadowX, shadowY, shadowColor)
|
||||
val drawable = BitmapDrawable(resources, bitmap)
|
||||
background = drawable
|
||||
}
|
||||
|
||||
private fun createShadowBitmap(
|
||||
shadowWidth: Int, shadowHeight: Int, shadowRadius: Float, dx: Float, dy: Float, shadowColor: Int
|
||||
): Bitmap {
|
||||
val output: Bitmap = Bitmap.createBitmap(shadowWidth, shadowHeight, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(output)
|
||||
shadowRect[shadowRadius, shadowRadius, shadowWidth - shadowRadius] = shadowHeight - shadowRadius
|
||||
shadowRect.top += dy
|
||||
shadowRect.bottom -= dy
|
||||
shadowRect.left += dx
|
||||
shadowRect.right -= dx
|
||||
shadowPaint.isAntiAlias = true
|
||||
shadowPaint.style = Paint.Style.FILL
|
||||
shadowPaint.color = shadowColor
|
||||
if (!isInEditMode) {
|
||||
shadowPaint.setShadowLayer(shadowRadius, dx, dy, shadowColor)
|
||||
}
|
||||
shadowPath.reset()
|
||||
shadowPath.addRoundRect(shadowRect, radiusArray, Path.Direction.CW)
|
||||
canvas.drawPath(shadowPath, shadowPaint)
|
||||
return output
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.view.component.function
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.RelativeLayout
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.R
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.manager.CallManager
|
||||
|
||||
class AudioAndVideoCalleeWaitingView(context: Context) : RelativeLayout(context) {
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
this.layoutParams?.width = LayoutParams.MATCH_PARENT
|
||||
this.layoutParams?.height = LayoutParams.MATCH_PARENT
|
||||
initView()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
LayoutInflater.from(context).inflate(R.layout.tuicallkit_function_view_invited_waiting, this)
|
||||
val layoutReject: LinearLayout = findViewById(R.id.ll_reject)
|
||||
val layoutDialing: LinearLayout = findViewById(R.id.ll_answer)
|
||||
|
||||
layoutReject.setOnClickListener {
|
||||
CallManager.instance.reject(null)
|
||||
}
|
||||
layoutDialing.setOnClickListener {
|
||||
CallManager.instance.accept(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.view.component.function
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import com.tencent.cloud.tuikit.engine.common.TUICommonDefine.AudioPlaybackDevice
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.R
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.manager.CallManager
|
||||
import com.trtc.tuikit.common.livedata.Observer
|
||||
|
||||
class AudioCallerWaitingAndAcceptedView(context: Context) : RelativeLayout(context) {
|
||||
private lateinit var layoutMute: LinearLayout
|
||||
private lateinit var layoutHangup: LinearLayout
|
||||
private lateinit var layoutAudioDevice: LinearLayout
|
||||
private lateinit var imageMute: ImageView
|
||||
private lateinit var imageHandsFree: ImageView
|
||||
private lateinit var textMute: TextView
|
||||
private lateinit var textAudioDevice: TextView
|
||||
|
||||
private var isMicMuteObserver = Observer<Boolean> {
|
||||
imageMute.isActivated = it
|
||||
textMute.text = getMicText()
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
this.layoutParams?.width = LayoutParams.MATCH_PARENT
|
||||
this.layoutParams?.height = LayoutParams.MATCH_PARENT
|
||||
initView()
|
||||
registerObserver()
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
unregisterObserver()
|
||||
}
|
||||
|
||||
private fun registerObserver() {
|
||||
CallManager.instance.mediaState.isMicrophoneMuted.observe(isMicMuteObserver)
|
||||
}
|
||||
|
||||
private fun unregisterObserver() {
|
||||
CallManager.instance.mediaState.isMicrophoneMuted.removeObserver(isMicMuteObserver)
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
LayoutInflater.from(context).inflate(R.layout.tuicallkit_function_view_audio, this)
|
||||
layoutMute = findViewById(R.id.ll_mute)
|
||||
imageMute = findViewById(R.id.img_mute)
|
||||
layoutHangup = findViewById(R.id.ll_hangup)
|
||||
layoutAudioDevice = findViewById(R.id.ll_audio_device)
|
||||
imageHandsFree = findViewById(R.id.img_audio_device)
|
||||
textMute = findViewById(R.id.tv_mic)
|
||||
textAudioDevice = findViewById(R.id.tv_audio_device)
|
||||
|
||||
imageMute.isActivated = CallManager.instance.mediaState.isMicrophoneMuted.get()
|
||||
imageHandsFree.isActivated =
|
||||
CallManager.instance.mediaState.audioPlayoutDevice.get() == AudioPlaybackDevice.Speakerphone
|
||||
textMute.text = getMicText()
|
||||
textAudioDevice.text = getAudioDeviceText()
|
||||
|
||||
initViewListener()
|
||||
}
|
||||
|
||||
private fun initViewListener() {
|
||||
layoutMute.setOnClickListener {
|
||||
if (CallManager.instance.mediaState.isMicrophoneMuted.get()) {
|
||||
CallManager.instance.openMicrophone(null)
|
||||
} else {
|
||||
CallManager.instance.closeMicrophone()
|
||||
}
|
||||
}
|
||||
layoutHangup.setOnClickListener {
|
||||
CallManager.instance.hangup(null)
|
||||
}
|
||||
layoutAudioDevice.setOnClickListener {
|
||||
val isSpeaker = CallManager.instance.mediaState.audioPlayoutDevice.get() == AudioPlaybackDevice.Speakerphone
|
||||
val device = if (isSpeaker) AudioPlaybackDevice.Earpiece else AudioPlaybackDevice.Speakerphone
|
||||
CallManager.instance.selectAudioPlaybackDevice(device)
|
||||
textAudioDevice.text = getAudioDeviceText()
|
||||
imageHandsFree.isActivated = !isSpeaker
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMicText(): String {
|
||||
return if (CallManager.instance.mediaState.isMicrophoneMuted.get()) {
|
||||
context.getString(R.string.tuicallkit_toast_enable_mute)
|
||||
} else {
|
||||
context.getString(R.string.tuicallkit_toast_disable_mute)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAudioDeviceText(): String {
|
||||
return if (CallManager.instance.mediaState.audioPlayoutDevice.get() == AudioPlaybackDevice.Speakerphone) {
|
||||
context.getString(R.string.tuicallkit_toast_speaker)
|
||||
} else {
|
||||
context.getString(R.string.tuicallkit_toast_use_earpiece)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.view.component.function
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.RelativeLayout
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallDefine
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.manager.CallManager
|
||||
import com.trtc.tuikit.common.livedata.Observer
|
||||
|
||||
class CallFunctionLayout(context: Context) : ConstraintLayout(context) {
|
||||
private var functionLayout: RelativeLayout? = null
|
||||
private val callStatusObserver = Observer<TUICallDefine.Status> {
|
||||
if (it == TUICallDefine.Status.Accept) {
|
||||
updateLayout()
|
||||
}
|
||||
}
|
||||
private var sceneObserver = Observer<TUICallDefine.Scene> {
|
||||
if (it == TUICallDefine.Scene.GROUP_CALL) {
|
||||
if (functionLayout is VideoCallerAndCalleeAcceptedView) {
|
||||
return@Observer
|
||||
}
|
||||
updateLayout()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
updateLayout()
|
||||
registerObserver()
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
unregisterObserver()
|
||||
}
|
||||
|
||||
private fun registerObserver() {
|
||||
CallManager.instance.userState.selfUser.get().callStatus.observe(callStatusObserver)
|
||||
CallManager.instance.callState.scene.observe(sceneObserver)
|
||||
}
|
||||
|
||||
private fun unregisterObserver() {
|
||||
CallManager.instance.userState.selfUser.get().callStatus.removeObserver(callStatusObserver)
|
||||
CallManager.instance.callState.scene.removeObserver(sceneObserver)
|
||||
}
|
||||
|
||||
private fun updateLayout() {
|
||||
if (CallManager.instance.callState.scene.get() == TUICallDefine.Scene.GROUP_CALL) {
|
||||
updateGroupCallLayout()
|
||||
} else {
|
||||
updateSingleCallLayout()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSingleCallLayout() {
|
||||
val mediaType = CallManager.instance.callState.mediaType.get()
|
||||
val role = CallManager.instance.userState.selfUser.get().callRole
|
||||
val callStatus = CallManager.instance.userState.selfUser.get().callStatus.get()
|
||||
|
||||
when {
|
||||
callStatus == TUICallDefine.Status.Waiting && role == TUICallDefine.Role.Called -> {
|
||||
functionLayout = AudioAndVideoCalleeWaitingView(context)
|
||||
}
|
||||
callStatus == TUICallDefine.Status.Waiting && role == TUICallDefine.Role.Caller -> {
|
||||
functionLayout = when (mediaType) {
|
||||
TUICallDefine.MediaType.Video -> VideoCallerWaitingView(context)
|
||||
else -> AudioCallerWaitingAndAcceptedView(context)
|
||||
}
|
||||
}
|
||||
callStatus == TUICallDefine.Status.Accept -> {
|
||||
functionLayout = when (mediaType) {
|
||||
TUICallDefine.MediaType.Video -> VideoCallerAndCalleeAcceptedView(context)
|
||||
else -> AudioCallerWaitingAndAcceptedView(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removeAllViews()
|
||||
addView(functionLayout)
|
||||
}
|
||||
|
||||
private fun updateGroupCallLayout() {
|
||||
val role = CallManager.instance.userState.selfUser.get().callRole
|
||||
val callStatus = CallManager.instance.userState.selfUser.get().callStatus.get()
|
||||
val functionLayout = if (callStatus == TUICallDefine.Status.Waiting && role == TUICallDefine.Role.Called) {
|
||||
AudioAndVideoCalleeWaitingView(context)
|
||||
} else {
|
||||
VideoCallerAndCalleeAcceptedView(context)
|
||||
}
|
||||
removeAllViews()
|
||||
addView(functionLayout)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.view.component.function
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.ImageView
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.motion.widget.MotionLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallDefine
|
||||
import com.tencent.cloud.tuikit.engine.common.TUICommonDefine
|
||||
import com.tencent.cloud.tuikit.engine.common.TUICommonDefine.AudioPlaybackDevice
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.R
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.manager.CallManager
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.state.GlobalState
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.view.component.videolayout.VideoFactory
|
||||
import com.trtc.tuikit.common.livedata.Observer
|
||||
|
||||
class VideoCallerAndCalleeAcceptedView(context: Context) : RelativeLayout(context) {
|
||||
lateinit var rootView: MotionLayout
|
||||
private lateinit var imageHangup: ImageView
|
||||
private lateinit var imageSwitchCamera: ImageView
|
||||
private lateinit var imageExpandView: ImageView
|
||||
private lateinit var imageBlurBackground: ImageView
|
||||
private lateinit var imageMic: ImageView
|
||||
private lateinit var imageSpeaker: ImageView
|
||||
private lateinit var imageCamera: ImageView
|
||||
private lateinit var textMic: TextView
|
||||
private lateinit var textSpeaker: TextView
|
||||
private lateinit var textCamera: TextView
|
||||
|
||||
private var isBottomViewExpand: Boolean = true
|
||||
|
||||
private var isCameraOpenObserver = Observer<Boolean> {
|
||||
imageCamera.isActivated = it
|
||||
textCamera.text = getCameraHint(it)
|
||||
|
||||
if (it && CallManager.instance.callState.scene.get() == TUICallDefine.Scene.SINGLE_CALL) {
|
||||
imageSwitchCamera.visibility = VISIBLE
|
||||
imageBlurBackground.visibility = if (GlobalState.instance.enableVirtualBackground) VISIBLE else GONE
|
||||
} else {
|
||||
imageSwitchCamera.visibility = GONE
|
||||
imageBlurBackground.visibility = GONE
|
||||
}
|
||||
}
|
||||
|
||||
private val isMicOpenObserver = Observer<Boolean> {
|
||||
val resId = if (it) {
|
||||
R.string.tuicallkit_toast_enable_mute
|
||||
} else {
|
||||
R.string.tuicallkit_toast_disable_mute
|
||||
}
|
||||
textMic.text = context.getString(resId)
|
||||
imageMic.isActivated = it
|
||||
}
|
||||
|
||||
private val showLargeViewUserObserver = Observer<String> {
|
||||
startAnimation(it.isNullOrEmpty())
|
||||
enableSwipeFunctionView(true)
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
this.layoutParams?.width = LayoutParams.MATCH_PARENT
|
||||
this.layoutParams?.height = LayoutParams.MATCH_PARENT
|
||||
initView()
|
||||
enableSwipeFunctionView(false)
|
||||
registerObserver()
|
||||
initViewListener()
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
unregisterObserver()
|
||||
}
|
||||
|
||||
private fun registerObserver() {
|
||||
CallManager.instance.mediaState.isCameraOpened.observe(isCameraOpenObserver)
|
||||
CallManager.instance.mediaState.isMicrophoneMuted.observe(isMicOpenObserver)
|
||||
CallManager.instance.viewState.showLargeViewUserId.observe(showLargeViewUserObserver)
|
||||
}
|
||||
|
||||
private fun unregisterObserver() {
|
||||
CallManager.instance.mediaState.isCameraOpened.removeObserver(isCameraOpenObserver)
|
||||
CallManager.instance.mediaState.isMicrophoneMuted.removeObserver(isMicOpenObserver)
|
||||
CallManager.instance.viewState.showLargeViewUserId.removeObserver(showLargeViewUserObserver)
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
LayoutInflater.from(context).inflate(R.layout.tuicallkit_function_view_video, this)
|
||||
rootView = findViewById(R.id.cl_view_video)
|
||||
imageMic = findViewById(R.id.iv_mic)
|
||||
textMic = findViewById(R.id.tv_mic)
|
||||
imageSpeaker = findViewById(R.id.iv_speaker)
|
||||
textSpeaker = findViewById(R.id.tv_speaker)
|
||||
imageCamera = findViewById(R.id.iv_camera)
|
||||
textCamera = findViewById(R.id.tv_video_camera)
|
||||
|
||||
imageHangup = findViewById(R.id.iv_hang_up)
|
||||
imageSwitchCamera = findViewById(R.id.iv_function_switch_camera)
|
||||
imageBlurBackground = findViewById(R.id.img_blur_background)
|
||||
imageExpandView = findViewById(R.id.iv_expanded)
|
||||
|
||||
val isMute = CallManager.instance.mediaState.isMicrophoneMuted.get()
|
||||
imageMic.isActivated = isMute
|
||||
val micResId = if (isMute) R.string.tuicallkit_toast_disable_mute else R.string.tuicallkit_toast_enable_mute
|
||||
textMic.text = context.getString(micResId)
|
||||
|
||||
val isSpeaker = CallManager.instance.mediaState.audioPlayoutDevice.get() == AudioPlaybackDevice.Speakerphone
|
||||
val speakerResId = if (isSpeaker) R.string.tuicallkit_toast_speaker else R.string.tuicallkit_toast_use_earpiece
|
||||
imageSpeaker.isActivated = isSpeaker
|
||||
textSpeaker.text = context.getString(speakerResId)
|
||||
|
||||
val isCameraOpened = CallManager.instance.mediaState.isCameraOpened.get()
|
||||
if (CallManager.instance.callState.scene.get() == TUICallDefine.Scene.SINGLE_CALL && isCameraOpened
|
||||
) {
|
||||
imageSwitchCamera.visibility = VISIBLE
|
||||
imageBlurBackground.visibility = if (GlobalState.instance.enableVirtualBackground) VISIBLE else GONE
|
||||
} else {
|
||||
imageSwitchCamera.visibility = GONE
|
||||
imageBlurBackground.visibility = GONE
|
||||
}
|
||||
|
||||
if (!CallManager.instance.viewState.showLargeViewUserId.get().isNullOrEmpty()) {
|
||||
startAnimation(false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCameraHint(isCameraOpened: Boolean): String {
|
||||
return when {
|
||||
isCameraOpened -> context.getString(R.string.tuicallkit_toast_enable_camera)
|
||||
else -> context.getString(R.string.tuicallkit_toast_disable_camera)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initViewListener() {
|
||||
imageMic.setOnClickListener {
|
||||
if (CallManager.instance.mediaState.isMicrophoneMuted.get()) {
|
||||
CallManager.instance.openMicrophone(null)
|
||||
} else {
|
||||
CallManager.instance.closeMicrophone()
|
||||
}
|
||||
}
|
||||
imageSpeaker.setOnClickListener {
|
||||
val device =
|
||||
if (CallManager.instance.mediaState.audioPlayoutDevice.get() == AudioPlaybackDevice.Speakerphone) {
|
||||
AudioPlaybackDevice.Earpiece
|
||||
} else {
|
||||
AudioPlaybackDevice.Speakerphone
|
||||
}
|
||||
val resId = if (device == AudioPlaybackDevice.Speakerphone) {
|
||||
R.string.tuicallkit_toast_speaker
|
||||
} else {
|
||||
R.string.tuicallkit_toast_use_earpiece
|
||||
}
|
||||
|
||||
CallManager.instance.selectAudioPlaybackDevice(device)
|
||||
textSpeaker.text = context.getString(resId)
|
||||
imageSpeaker.isActivated = device == AudioPlaybackDevice.Speakerphone
|
||||
}
|
||||
imageCamera.setOnClickListener {
|
||||
if (CallManager.instance.mediaState.isCameraOpened.get()) {
|
||||
CallManager.instance.closeCamera()
|
||||
} else {
|
||||
val selfUser = CallManager.instance.userState.selfUser.get()
|
||||
val camera: TUICommonDefine.Camera = CallManager.instance.mediaState.isFrontCamera.get()
|
||||
val videoView = VideoFactory.instance.findVideoView(selfUser.id)
|
||||
|
||||
CallManager.instance.openCamera(camera, videoView, null)
|
||||
if (CallManager.instance.callState.scene.get() == TUICallDefine.Scene.GROUP_CALL) {
|
||||
if (CallManager.instance.viewState.showLargeViewUserId.get() != selfUser.id) {
|
||||
CallManager.instance.viewState.showLargeViewUserId.set(selfUser.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
imageHangup.setOnClickListener {
|
||||
CallManager.instance.hangup(null)
|
||||
}
|
||||
|
||||
imageExpandView.setOnClickListener() {
|
||||
startAnimation(!isBottomViewExpand)
|
||||
}
|
||||
|
||||
imageBlurBackground.setOnClickListener {
|
||||
CallManager.instance.setBlurBackground(!CallManager.instance.viewState.isVirtualBackgroundOpened.get())
|
||||
}
|
||||
|
||||
imageSwitchCamera.setOnClickListener() {
|
||||
var camera = TUICommonDefine.Camera.Back
|
||||
if (CallManager.instance.mediaState.isFrontCamera.get() == TUICommonDefine.Camera.Back) {
|
||||
camera = TUICommonDefine.Camera.Front
|
||||
}
|
||||
CallManager.instance.switchCamera(camera)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startAnimation(isExpand: Boolean) {
|
||||
if (CallManager.instance.callState.scene.get() == TUICallDefine.Scene.SINGLE_CALL) {
|
||||
return
|
||||
}
|
||||
if (rootView.getTransition(R.id.video_function_view_transition)?.isEnabled == false) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isExpand) {
|
||||
rootView.transitionToStart()
|
||||
rootView.getConstraintSet(R.id.start)?.getConstraint(R.id.iv_expanded)?.propertySet?.visibility = VISIBLE
|
||||
} else {
|
||||
rootView.transitionToEnd()
|
||||
}
|
||||
rootView.background = ContextCompat.getDrawable(context, R.drawable.tuicallkit_bg_group_call_bottom)
|
||||
isBottomViewExpand = isExpand
|
||||
}
|
||||
|
||||
private fun enableSwipeFunctionView(enable: Boolean) {
|
||||
val enableSwipe = (CallManager.instance.callState.scene.get() != TUICallDefine.Scene.SINGLE_CALL) && enable
|
||||
rootView.enableTransition(R.id.video_function_view_transition, enableSwipe)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.view.component.function
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import com.tencent.cloud.tuikit.engine.common.TUICommonDefine
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.R
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.manager.CallManager
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.state.GlobalState
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.view.component.videolayout.VideoFactory
|
||||
import com.trtc.tuikit.common.livedata.Observer
|
||||
|
||||
class VideoCallerWaitingView(context: Context) : RelativeLayout(context) {
|
||||
private lateinit var layoutCancel: LinearLayout
|
||||
private lateinit var imageSwitchCamera: ImageView
|
||||
private lateinit var imageViewBlur: ImageView
|
||||
private lateinit var imageOpenCamera: ImageView
|
||||
private lateinit var layoutBlurBackground: LinearLayout
|
||||
private lateinit var textCamera: TextView
|
||||
|
||||
private var isCameraOpenObserver = Observer<Boolean> {
|
||||
imageOpenCamera.isActivated = it
|
||||
}
|
||||
private var isVirtualBackgroundObserver = Observer<Boolean> {
|
||||
imageViewBlur.isActivated = it
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
this.layoutParams?.width = LayoutParams.MATCH_PARENT
|
||||
this.layoutParams?.height = LayoutParams.MATCH_PARENT
|
||||
initView()
|
||||
registerObserver()
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
unregisterObserver()
|
||||
}
|
||||
|
||||
private fun registerObserver() {
|
||||
CallManager.instance.viewState.isVirtualBackgroundOpened.observe(isVirtualBackgroundObserver)
|
||||
CallManager.instance.mediaState.isCameraOpened.observe(isCameraOpenObserver)
|
||||
}
|
||||
|
||||
private fun unregisterObserver() {
|
||||
CallManager.instance.viewState.isVirtualBackgroundOpened.removeObserver(isVirtualBackgroundObserver)
|
||||
CallManager.instance.mediaState.isCameraOpened.removeObserver(isCameraOpenObserver)
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
LayoutInflater.from(context).inflate(R.layout.tuicallkit_function_view_video_inviting, this)
|
||||
layoutCancel = findViewById(R.id.ll_cancel)
|
||||
imageSwitchCamera = findViewById(R.id.img_switch_camera)
|
||||
imageOpenCamera = findViewById(R.id.img_camera)
|
||||
textCamera = findViewById(R.id.tv_camera)
|
||||
layoutBlurBackground = findViewById(R.id.ll_blur)
|
||||
imageViewBlur = findViewById(R.id.iv_video_blur)
|
||||
|
||||
imageOpenCamera.isActivated = CallManager.instance.mediaState.isCameraOpened.get()
|
||||
if (!GlobalState.instance.enableVirtualBackground) {
|
||||
layoutBlurBackground.visibility = GONE
|
||||
reLayoutView()
|
||||
}
|
||||
|
||||
initViewListener()
|
||||
}
|
||||
|
||||
private fun reLayoutView() {
|
||||
val size = ConstraintLayout.LayoutParams.WRAP_CONTENT
|
||||
val parentId = ConstraintLayout.LayoutParams.PARENT_ID
|
||||
|
||||
layoutCancel.layoutParams = ConstraintLayout.LayoutParams(size, size).apply {
|
||||
topToTop = parentId
|
||||
startToStart = parentId
|
||||
endToEnd = parentId
|
||||
}
|
||||
|
||||
findViewById<LinearLayout>(R.id.ll_switch).layoutParams = ConstraintLayout.LayoutParams(size, size).apply {
|
||||
startToStart = parentId
|
||||
endToStart = layoutCancel.id
|
||||
topToTop = layoutCancel.id
|
||||
bottomToBottom = layoutCancel.id
|
||||
}
|
||||
|
||||
findViewById<LinearLayout>(R.id.ll_camera).layoutParams = ConstraintLayout.LayoutParams(size, size).apply {
|
||||
startToEnd = layoutCancel.id
|
||||
endToEnd = parentId
|
||||
topToTop = layoutCancel.id
|
||||
bottomToBottom = layoutCancel.id
|
||||
}
|
||||
}
|
||||
|
||||
private fun initViewListener() {
|
||||
layoutCancel.setOnClickListener {
|
||||
CallManager.instance.hangup(null)
|
||||
}
|
||||
imageSwitchCamera.setOnClickListener {
|
||||
var camera = TUICommonDefine.Camera.Back
|
||||
if (CallManager.instance.mediaState.isFrontCamera.get() == TUICommonDefine.Camera.Back) {
|
||||
camera = TUICommonDefine.Camera.Front
|
||||
}
|
||||
CallManager.instance.switchCamera(camera)
|
||||
}
|
||||
imageOpenCamera.setOnClickListener {
|
||||
val isCameraOpened = CallManager.instance.mediaState.isCameraOpened.get()
|
||||
imageOpenCamera.isActivated = !isCameraOpened
|
||||
imageSwitchCamera.isEnabled = !isCameraOpened
|
||||
layoutBlurBackground.isEnabled = !isCameraOpened
|
||||
|
||||
if (isCameraOpened) {
|
||||
CallManager.instance.closeCamera()
|
||||
textCamera.text = context.resources.getString(R.string.tuicallkit_toast_disable_camera)
|
||||
} else {
|
||||
val camera = CallManager.instance.mediaState.isFrontCamera.get()
|
||||
val videoView = VideoFactory.instance.findVideoView(CallManager.instance.userState.selfUser.get().id)
|
||||
CallManager.instance.openCamera(camera, videoView, null)
|
||||
textCamera.text = context.resources.getString(R.string.tuicallkit_toast_enable_camera)
|
||||
}
|
||||
}
|
||||
layoutBlurBackground.setOnClickListener {
|
||||
CallManager.instance.setBlurBackground(!CallManager.instance.viewState.isVirtualBackgroundOpened.get())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.view.component.hint
|
||||
|
||||
import android.content.Context
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.AppCompatTextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallDefine
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallDefine.Scene
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.R
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.common.data.Constants
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.manager.CallManager
|
||||
import com.trtc.tuikit.common.livedata.Observer
|
||||
|
||||
class CallHintView(context: Context) : AppCompatTextView(context) {
|
||||
private var isFirstShowAccept = true
|
||||
|
||||
private var callStatusObserver = Observer<TUICallDefine.Status> {
|
||||
updateStatusText()
|
||||
}
|
||||
|
||||
private var networkQualityObserver = Observer<Constants.NetworkQualityHint> {
|
||||
when (it) {
|
||||
Constants.NetworkQualityHint.Local -> text = context.getString(R.string.tuicallkit_self_network_low_quality)
|
||||
Constants.NetworkQualityHint.Remote -> text =
|
||||
context.getString(R.string.tuicallkit_other_party_network_low_quality)
|
||||
else -> updateStatusText()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
initView()
|
||||
registerObserver()
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
unregisterObserver()
|
||||
}
|
||||
|
||||
private fun registerObserver() {
|
||||
CallManager.instance.userState.selfUser.get().callStatus.observe(callStatusObserver)
|
||||
CallManager.instance.callState.networkQualityReminder.observe(networkQualityObserver)
|
||||
}
|
||||
|
||||
private fun unregisterObserver() {
|
||||
CallManager.instance.userState.selfUser.get().callStatus.removeObserver(callStatusObserver)
|
||||
CallManager.instance.callState.networkQualityReminder.removeObserver(networkQualityObserver)
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
setTextColor(ContextCompat.getColor(context, R.color.tuicallkit_color_white))
|
||||
gravity = Gravity.CENTER
|
||||
|
||||
text = if (Scene.GROUP_CALL == CallManager.instance.callState.scene.get()) {
|
||||
if (TUICallDefine.Role.Caller == CallManager.instance.userState.selfUser.get().callRole) {
|
||||
context.getString(R.string.tuicallkit_wait_response)
|
||||
} else {
|
||||
context.getString(R.string.tuicallkit_wait_accept_group)
|
||||
}
|
||||
} else {
|
||||
updateStatusText()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSingleCallWaitingText(): String {
|
||||
return if (TUICallDefine.Role.Caller == CallManager.instance.userState.selfUser.get().callRole) {
|
||||
context.getString(R.string.tuicallkit_waiting_accept)
|
||||
} else {
|
||||
if (TUICallDefine.MediaType.Video == CallManager.instance.callState.mediaType.get()) {
|
||||
context.getString(R.string.tuicallkit_invite_video_call)
|
||||
} else {
|
||||
context.getString(R.string.tuicallkit_invite_audio_call)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateStatusText(): String {
|
||||
val callStatus = CallManager.instance.userState.selfUser.get().callStatus.get()
|
||||
if (Scene.GROUP_CALL == CallManager.instance.callState.scene.get() && TUICallDefine.Status.Accept == callStatus) {
|
||||
visibility = View.GONE
|
||||
return ""
|
||||
}
|
||||
|
||||
if (callStatus == TUICallDefine.Status.Waiting) {
|
||||
return updateSingleCallWaitingText()
|
||||
}
|
||||
|
||||
text = ""
|
||||
val role = CallManager.instance.userState.selfUser.get().callRole
|
||||
if (callStatus == TUICallDefine.Status.Accept && role == TUICallDefine.Role.Caller && isFirstShowAccept) {
|
||||
text = context.getString(R.string.tuicallkit_accept_single)
|
||||
postDelayed({
|
||||
isFirstShowAccept = false
|
||||
}, 2000)
|
||||
}
|
||||
return text.toString()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.view.component.hint
|
||||
|
||||
import android.content.Context
|
||||
import androidx.appcompat.widget.AppCompatTextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallDefine
|
||||
import com.tencent.qcloud.tuicore.util.DateTimeUtil
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.R
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.manager.CallManager
|
||||
import com.trtc.tuikit.common.livedata.Observer
|
||||
|
||||
class CallTimerView(context: Context) : AppCompatTextView(context) {
|
||||
private var callDurationObserver = Observer<Int> {
|
||||
this.post {
|
||||
if (CallManager.instance.userState.selfUser.get().callStatus.get() == TUICallDefine.Status.Accept) {
|
||||
text = DateTimeUtil.formatSecondsTo00(it)
|
||||
visibility = VISIBLE
|
||||
} else {
|
||||
visibility = GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
initView()
|
||||
registerObserver()
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
unregisterObserver()
|
||||
}
|
||||
|
||||
private fun registerObserver() {
|
||||
CallManager.instance.callState.callDurationCount.observe(callDurationObserver)
|
||||
}
|
||||
|
||||
private fun unregisterObserver() {
|
||||
CallManager.instance.callState.callDurationCount.removeObserver(callDurationObserver)
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
setTextColor(ContextCompat.getColor(context, R.color.tuicallkit_color_white))
|
||||
|
||||
if (CallManager.instance.userState.selfUser.get().callStatus.get() == TUICallDefine.Status.Accept) {
|
||||
text = DateTimeUtil.formatSecondsTo00(CallManager.instance.callState.callDurationCount.get())
|
||||
visibility = VISIBLE
|
||||
} else {
|
||||
visibility = GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.view.component.incomingbanner
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.common.data.Constants
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.manager.CallManager
|
||||
|
||||
class IncomingCallReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
if (intent != null && intent.action == Constants.REJECT_CALL_ACTION) {
|
||||
CallManager.instance.reject(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.view.component.incomingbanner
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Context.WINDOW_SERVICE
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.PixelFormat
|
||||
import android.os.Build
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.widget.ImageView
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallDefine
|
||||
import com.tencent.qcloud.tuicore.util.ScreenUtil
|
||||
import com.tencent.qcloud.tuicore.util.TUIBuild
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.R
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.common.data.Constants
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.common.data.Logger
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.manager.CallManager
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.state.UserState
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.state.ViewState
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.view.CallMainActivity
|
||||
import com.trtc.tuikit.common.imageloader.ImageLoader
|
||||
import com.trtc.tuikit.common.livedata.Observer
|
||||
|
||||
class IncomingFloatBanner(context: Context) : RelativeLayout(context) {
|
||||
private var appContext: Context = context.applicationContext
|
||||
private val windowManager: WindowManager = appContext.getSystemService(WINDOW_SERVICE) as WindowManager
|
||||
|
||||
private lateinit var layoutView: View
|
||||
private lateinit var imageFloatAvatar: ImageView
|
||||
private lateinit var textFloatTitle: TextView
|
||||
private lateinit var textFloatDescription: TextView
|
||||
private lateinit var imageReject: ImageView
|
||||
private lateinit var imageAccept: ImageView
|
||||
private val padding = 40
|
||||
|
||||
private lateinit var caller: UserState.User
|
||||
|
||||
private var callStatusObserver = Observer<TUICallDefine.Status> {
|
||||
if (it == TUICallDefine.Status.None || it == TUICallDefine.Status.Accept) {
|
||||
cancelIncomingView()
|
||||
}
|
||||
}
|
||||
|
||||
private val viewRouterObserver = Observer<ViewState.ViewRouter> {
|
||||
if (it == ViewState.ViewRouter.FullView || it == ViewState.ViewRouter.FloatView) {
|
||||
Logger.i(TAG, "viewRouterObserver, viewRouter: $it")
|
||||
cancelIncomingView()
|
||||
}
|
||||
}
|
||||
|
||||
fun showIncomingView(user: UserState.User) {
|
||||
Logger.i(TAG, "showIncomingView")
|
||||
caller = user
|
||||
initView()
|
||||
registerObserver()
|
||||
}
|
||||
|
||||
private fun cancelIncomingView() {
|
||||
Logger.i(TAG, "cancelIncomingView")
|
||||
if (layoutView.isAttachedToWindow) {
|
||||
windowManager.removeView(layoutView)
|
||||
}
|
||||
unregisterObserver()
|
||||
}
|
||||
|
||||
private fun registerObserver() {
|
||||
CallManager.instance.userState.selfUser.get().callStatus.observe(callStatusObserver)
|
||||
CallManager.instance.viewState.router.observe(viewRouterObserver)
|
||||
}
|
||||
|
||||
private fun unregisterObserver() {
|
||||
CallManager.instance.userState.selfUser.get().callStatus.removeObserver(callStatusObserver)
|
||||
CallManager.instance.viewState.router.removeObserver(viewRouterObserver)
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
layoutView = LayoutInflater.from(context).inflate(R.layout.tuicallkit_incoming_float_view, this)
|
||||
imageFloatAvatar = layoutView.findViewById(R.id.img_float_avatar)
|
||||
textFloatTitle = layoutView.findViewById(R.id.tv_float_title)
|
||||
textFloatDescription = layoutView.findViewById(R.id.tv_float_desc)
|
||||
imageReject = layoutView.findViewById(R.id.btn_float_decline)
|
||||
imageAccept = layoutView.findViewById(R.id.btn_float_accept)
|
||||
|
||||
ImageLoader.load(appContext, imageFloatAvatar, caller.avatar?.get(), R.drawable.tuicallkit_ic_avatar)
|
||||
textFloatTitle.text = caller.nickname.get()
|
||||
|
||||
val mediaType = CallManager.instance.callState.mediaType.get()
|
||||
textFloatDescription.text =
|
||||
if (mediaType == TUICallDefine.MediaType.Video) {
|
||||
appContext.resources.getString(R.string.tuicallkit_invite_video_call)
|
||||
} else {
|
||||
appContext.resources.getString(R.string.tuicallkit_invite_audio_call)
|
||||
}
|
||||
|
||||
imageReject.setOnClickListener {
|
||||
CallManager.instance.reject(null)
|
||||
cancelIncomingView()
|
||||
}
|
||||
|
||||
layoutView.setOnClickListener {
|
||||
cancelIncomingView()
|
||||
val intent = Intent(context, CallMainActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
when (mediaType) {
|
||||
TUICallDefine.MediaType.Video -> imageAccept.setBackgroundResource(R.drawable.tuicallkit_ic_dialing_video)
|
||||
else -> imageAccept.setBackgroundResource(R.drawable.tuicallkit_bg_dialing)
|
||||
}
|
||||
imageAccept.setOnClickListener {
|
||||
if (CallManager.instance.userState.selfUser.get().callStatus.get() == TUICallDefine.Status.None) {
|
||||
Logger.w(TAG, "current status is None, ignore")
|
||||
cancelIncomingView()
|
||||
return@setOnClickListener
|
||||
}
|
||||
|
||||
Logger.i(TAG, "accept the call")
|
||||
val intent = Intent(context, CallMainActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
intent.action = Constants.ACCEPT_CALL_ACTION
|
||||
context.startActivity(intent)
|
||||
cancelIncomingView()
|
||||
}
|
||||
|
||||
CallManager.instance.viewState.router.set(ViewState.ViewRouter.Banner)
|
||||
windowManager.addView(layoutView, viewParams)
|
||||
}
|
||||
|
||||
private val viewParams: WindowManager.LayoutParams
|
||||
get() {
|
||||
val windowLayoutParams = WindowManager.LayoutParams()
|
||||
if (TUIBuild.getVersionInt() >= Build.VERSION_CODES.O) {
|
||||
windowLayoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
|
||||
} else {
|
||||
windowLayoutParams.type = WindowManager.LayoutParams.TYPE_PHONE
|
||||
}
|
||||
windowLayoutParams.flags = (WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
|
||||
or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
|
||||
or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)
|
||||
windowLayoutParams.gravity = Gravity.START or Gravity.TOP
|
||||
windowLayoutParams.x = padding
|
||||
windowLayoutParams.y = 0
|
||||
windowLayoutParams.width = ScreenUtil.getScreenWidth(appContext) - padding * 2
|
||||
windowLayoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT
|
||||
windowLayoutParams.format = PixelFormat.TRANSPARENT
|
||||
return windowLayoutParams
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration?) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
layoutView.let { windowManager?.updateViewLayout(layoutView, viewParams) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "IncomingViewFloat"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.view.component.incomingbanner
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.widget.RemoteViews
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.bumptech.glide.request.target.SimpleTarget
|
||||
import com.bumptech.glide.request.transition.Transition
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallDefine
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallDefine.MediaType
|
||||
import com.tencent.qcloud.tuicore.util.TUIBuild
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.R
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.common.data.Constants
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.common.data.Logger
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.manager.CallManager
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.manager.feature.NotificationFeature
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.state.UserState
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.view.CallMainActivity
|
||||
import com.trtc.tuikit.common.livedata.Observer
|
||||
|
||||
class IncomingNotificationBanner(context: Context) {
|
||||
private val context: Context = context.applicationContext
|
||||
private var notificationManager: NotificationManager =
|
||||
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
private val notificationId = 9909
|
||||
private var remoteViews: RemoteViews? = null
|
||||
private var notification: Notification? = null
|
||||
|
||||
private var callStatusObserver = Observer<TUICallDefine.Status> {
|
||||
if (it == TUICallDefine.Status.None || it == TUICallDefine.Status.Accept) {
|
||||
cancelNotification()
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerObserver() {
|
||||
CallManager.instance.userState.selfUser.get().callStatus.observe(callStatusObserver)
|
||||
}
|
||||
|
||||
private fun unregisterObserver() {
|
||||
CallManager.instance.userState.selfUser.get().callStatus.removeObserver(callStatusObserver)
|
||||
}
|
||||
|
||||
fun showNotification(user: UserState.User) {
|
||||
Logger.i(TAG, "showNotification, user: $user")
|
||||
registerObserver()
|
||||
notification = createNotification()
|
||||
|
||||
if (user.nickname.get().isNullOrEmpty()) {
|
||||
remoteViews?.setTextViewText(R.id.tv_incoming_title, user.id)
|
||||
} else {
|
||||
remoteViews?.setTextViewText(R.id.tv_incoming_title, user.nickname.get())
|
||||
}
|
||||
|
||||
val mediaType = CallManager.instance.callState.mediaType.get()
|
||||
if (mediaType == MediaType.Video) {
|
||||
remoteViews?.setTextViewText(R.id.tv_desc, context.getString(R.string.tuicallkit_invite_video_call))
|
||||
remoteViews?.setImageViewResource(R.id.img_media_type, R.drawable.tuicallkit_ic_video_incoming)
|
||||
remoteViews?.setImageViewResource(R.id.btn_accept, R.drawable.tuicallkit_ic_dialing_video)
|
||||
} else {
|
||||
remoteViews?.setTextViewText(R.id.tv_desc, context.getString(R.string.tuicallkit_invite_audio_call))
|
||||
remoteViews?.setImageViewResource(R.id.img_media_type, R.drawable.tuicallkit_ic_float)
|
||||
remoteViews?.setImageViewResource(R.id.btn_accept, R.drawable.tuicallkit_bg_dialing)
|
||||
}
|
||||
|
||||
if (user.avatar.get().isNullOrEmpty()) {
|
||||
remoteViews?.setImageViewResource(R.id.img_incoming_avatar, R.drawable.tuicallkit_ic_avatar)
|
||||
notificationManager.notify(notificationId, notification)
|
||||
} else {
|
||||
val uri = Uri.parse(user.avatar.get())
|
||||
|
||||
Glide.with(context).asBitmap().load(uri)
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL).placeholder(R.drawable.tuicallkit_ic_avatar)
|
||||
.apply(RequestOptions.bitmapTransform(RoundedCorners(15)))
|
||||
.into(object : SimpleTarget<Bitmap>() {
|
||||
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
|
||||
remoteViews?.setImageViewBitmap(R.id.img_incoming_avatar, resource)
|
||||
notificationManager.notify(notificationId, notification)
|
||||
}
|
||||
|
||||
override fun onLoadFailed(errorDrawable: Drawable?) {
|
||||
remoteViews?.setImageViewResource(R.id.img_incoming_avatar, R.drawable.tuicallkit_ic_avatar)
|
||||
notificationManager.notify(notificationId, notification)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancelNotification() {
|
||||
Logger.i(TAG, "cancelNotification")
|
||||
notificationManager.cancel(notificationId)
|
||||
unregisterObserver()
|
||||
}
|
||||
|
||||
private fun createNotification(): Notification {
|
||||
val channelId = NotificationFeature.CHANNEL_ID
|
||||
val builder = NotificationCompat.Builder(context)
|
||||
.setOngoing(true)
|
||||
.setAutoCancel(true)
|
||||
.setWhen(System.currentTimeMillis())
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setTimeoutAfter(Constants.CALL_WAITING_MAX_TIME * 1000L)
|
||||
|
||||
if (TUIBuild.getVersionInt() >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
builder.setCategory(NotificationCompat.CATEGORY_CALL)
|
||||
builder.priority = NotificationCompat.PRIORITY_MAX
|
||||
}
|
||||
|
||||
builder.setChannelId(channelId)
|
||||
builder.setSmallIcon(R.drawable.tuicallkit_ic_avatar)
|
||||
builder.setSound(null)
|
||||
|
||||
builder.setContentIntent(getPendingIntent())
|
||||
builder.setFullScreenIntent(getPendingIntent(), true)
|
||||
|
||||
remoteViews = RemoteViews(context.packageName, R.layout.tuicallkit_incoming_notification_view)
|
||||
remoteViews?.setOnClickPendingIntent(R.id.btn_decline, getDeclineIntent())
|
||||
remoteViews?.setOnClickPendingIntent(R.id.btn_accept, getAcceptIntent())
|
||||
|
||||
builder.setCustomContentView(remoteViews)
|
||||
builder.setCustomBigContentView(remoteViews)
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
private fun getPendingIntent(): PendingIntent {
|
||||
val intent = Intent(context, CallMainActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
|
||||
}
|
||||
|
||||
private fun getDeclineIntent(): PendingIntent {
|
||||
val intent = Intent(context, IncomingCallReceiver::class.java)
|
||||
intent.action = Constants.REJECT_CALL_ACTION
|
||||
return PendingIntent.getBroadcast(context, 1, intent, PendingIntent.FLAG_IMMUTABLE)
|
||||
}
|
||||
|
||||
private fun getAcceptIntent(): PendingIntent {
|
||||
val intent = Intent(context, CallMainActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
intent.action = Constants.ACCEPT_CALL_ACTION
|
||||
return PendingIntent.getActivity(context, 2, intent, PendingIntent.FLAG_IMMUTABLE)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "IncomingViewNotification"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.view.component.inviteuser
|
||||
|
||||
class GroupMemberInfo {
|
||||
var userId: String? = ""
|
||||
var userName: String? = ""
|
||||
var avatar: String? = ""
|
||||
var isSelected: Boolean = false
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.view.component.inviteuser
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import com.tencent.qcloud.tuicore.TUICore
|
||||
import com.tencent.qcloud.tuicore.TUILogin
|
||||
import com.tencent.qcloud.tuicore.util.ToastUtil
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.R
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.common.data.Logger
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.manager.CallManager
|
||||
|
||||
@SuppressLint("AppCompatCustomView")
|
||||
class InviteUserButton(context: Context) : ImageView(context) {
|
||||
private val context: Context = context.applicationContext
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
initView()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
this.setBackgroundResource(R.drawable.tuicallkit_ic_add_user_black)
|
||||
this.setOnClickListener {
|
||||
inviteUser()
|
||||
}
|
||||
layoutParams = layoutParams.apply {
|
||||
width = ViewGroup.LayoutParams.MATCH_PARENT
|
||||
height = ViewGroup.LayoutParams.MATCH_PARENT
|
||||
}
|
||||
visibility = VISIBLE
|
||||
}
|
||||
|
||||
private fun inviteUser() {
|
||||
// TODO: 3.0 上这里不止是groupId
|
||||
val groupId = CallManager.instance.callState.chatGroupId
|
||||
if (groupId.isNullOrEmpty()) {
|
||||
ToastUtil.toastShortMessage(context.getString(R.string.tuicallkit_group_id_is_empty))
|
||||
return
|
||||
}
|
||||
val list = ArrayList<String?>()
|
||||
for (model in CallManager.instance.userState.remoteUserList.get()) {
|
||||
if (!model.id.isNullOrEmpty() && !list.contains(model.id)) {
|
||||
list.add(model.id)
|
||||
}
|
||||
}
|
||||
if (!list.contains(TUILogin.getLoginUser())) {
|
||||
list.add(TUILogin.getLoginUser())
|
||||
}
|
||||
Logger.i(TAG, "InviteUserButton clicked, groupId: $groupId ,list: $list")
|
||||
val bundle = Bundle()
|
||||
bundle.putString("groupId", groupId)
|
||||
bundle.putStringArrayList("selectMemberList", list)
|
||||
TUICore.startActivity("SelectGroupMemberActivity", bundle)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "InviteUserButton"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.view.component.inviteuser
|
||||
|
||||
import android.os.Bundle
|
||||
import android.widget.Button
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallDefine
|
||||
import com.tencent.imsdk.v2.V2TIMGroupMemberFullInfo
|
||||
import com.tencent.imsdk.v2.V2TIMGroupMemberInfoResult
|
||||
import com.tencent.imsdk.v2.V2TIMManager
|
||||
import com.tencent.imsdk.v2.V2TIMValueCallback
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.R
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.manager.CallManager
|
||||
import com.trtc.tuikit.common.FullScreenActivity
|
||||
import com.trtc.tuikit.common.livedata.Observer
|
||||
|
||||
class SelectGroupMemberActivity : FullScreenActivity() {
|
||||
private lateinit var recyclerUserList: RecyclerView
|
||||
private lateinit var adapter: SelectGroupMemberAdapter
|
||||
private var groupId: String? = null
|
||||
private val groupMemberList: MutableList<GroupMemberInfo> = ArrayList()
|
||||
private var alreadySelectList: List<String?> = ArrayList()
|
||||
|
||||
private var callStatusObserver = Observer<TUICallDefine.Status> {
|
||||
if (it == TUICallDefine.Status.None) {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.tuicallkit_activity_group_user)
|
||||
initView()
|
||||
initData()
|
||||
registerObserver()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
val toolbar = findViewById<Toolbar>(R.id.toolbar_group)
|
||||
toolbar.navigationIcon?.isAutoMirrored = true
|
||||
toolbar.setNavigationOnClickListener {
|
||||
finish()
|
||||
}
|
||||
val btnOK: Button = findViewById(R.id.btn_group_ok)
|
||||
btnOK.setOnClickListener {
|
||||
val selectUsers: MutableList<String?> = ArrayList()
|
||||
for (info in groupMemberList) {
|
||||
if (!info.userId.isNullOrEmpty() && info.isSelected && !alreadySelectList.contains(info.userId)) {
|
||||
selectUsers.add(info.userId)
|
||||
}
|
||||
}
|
||||
if (selectUsers.isNotEmpty()) {
|
||||
CallManager.instance.inviteUser(selectUsers, null)
|
||||
}
|
||||
finish()
|
||||
}
|
||||
recyclerUserList = findViewById(R.id.rv_user_list)
|
||||
}
|
||||
|
||||
private fun initData() {
|
||||
groupId = intent.getStringExtra("groupId")
|
||||
alreadySelectList = ArrayList(intent.getStringArrayListExtra("selectMemberList"))
|
||||
adapter = SelectGroupMemberAdapter()
|
||||
recyclerUserList.layoutManager = LinearLayoutManager(applicationContext)
|
||||
recyclerUserList.adapter = adapter
|
||||
updateGroupUserList()
|
||||
}
|
||||
|
||||
private fun updateGroupUserList() {
|
||||
val filter = V2TIMGroupMemberFullInfo.V2TIM_GROUP_MEMBER_FILTER_ALL
|
||||
V2TIMManager.getGroupManager().getGroupMemberList(groupId, filter, 0,
|
||||
object : V2TIMValueCallback<V2TIMGroupMemberInfoResult?> {
|
||||
override fun onError(errorCode: Int, errorMsg: String) {}
|
||||
override fun onSuccess(v2TIMGroupMemberInfoResult: V2TIMGroupMemberInfoResult?) {
|
||||
val results = v2TIMGroupMemberInfoResult?.memberInfoList
|
||||
groupMemberList.clear()
|
||||
if (results == null) {
|
||||
return
|
||||
}
|
||||
for (info in results) {
|
||||
val userInfo = GroupMemberInfo()
|
||||
userInfo.userId = info.userID
|
||||
userInfo.avatar = info.faceUrl
|
||||
userInfo.userName = info.nickName
|
||||
userInfo.isSelected = alreadySelectList.contains(userInfo.userId)
|
||||
groupMemberList.add(userInfo)
|
||||
}
|
||||
adapter.setDataSource(groupMemberList)
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun registerObserver() {
|
||||
CallManager.instance.userState.selfUser.get().callStatus.observe(callStatusObserver)
|
||||
}
|
||||
|
||||
private fun unregisterObserver() {
|
||||
CallManager.instance.userState.selfUser.get().callStatus.removeObserver(callStatusObserver)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
unregisterObserver()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.view.component.inviteuser
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.CheckBox
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.tencent.qcloud.tuicore.TUILogin
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.R
|
||||
import com.trtc.tuikit.common.imageloader.ImageLoader
|
||||
|
||||
class SelectGroupMemberAdapter : RecyclerView.Adapter<SelectGroupMemberAdapter.GroupMemberViewHolder>() {
|
||||
private lateinit var context: Context
|
||||
private var groupMemberList: List<GroupMemberInfo> = ArrayList()
|
||||
|
||||
fun setDataSource(userList: List<GroupMemberInfo>) {
|
||||
groupMemberList = userList
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GroupMemberViewHolder {
|
||||
context = parent.context
|
||||
val view = LayoutInflater.from(context).inflate(R.layout.tuicallkit_list_item_group_user, parent, false)
|
||||
return GroupMemberViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: GroupMemberViewHolder, position: Int) {
|
||||
val userInfo = groupMemberList[position]
|
||||
holder.itemView.setOnClickListener {
|
||||
holder.checkBox.isChecked = !holder.checkBox.isChecked
|
||||
userInfo.isSelected = holder.checkBox.isChecked
|
||||
}
|
||||
holder.layoutView(userInfo)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return groupMemberList.size
|
||||
}
|
||||
|
||||
inner class GroupMemberViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
private val imageAvatar: ImageView
|
||||
private val textName: TextView
|
||||
private val textHint: TextView
|
||||
val checkBox: CheckBox
|
||||
|
||||
init {
|
||||
checkBox = itemView.findViewById(R.id.group_user_check_box)
|
||||
imageAvatar = itemView.findViewById(R.id.group_user_avatar)
|
||||
textName = itemView.findViewById(R.id.group_user_name)
|
||||
textHint = itemView.findViewById(R.id.group_user_hint)
|
||||
}
|
||||
|
||||
fun layoutView(userInfo: GroupMemberInfo?) {
|
||||
if (userInfo == null || userInfo.userId.isNullOrEmpty()) {
|
||||
return
|
||||
}
|
||||
itemView.isEnabled = !userInfo.isSelected
|
||||
checkBox.isEnabled = !userInfo.isSelected
|
||||
checkBox.isChecked = userInfo.isSelected
|
||||
checkBox.isSelected = userInfo.isSelected
|
||||
textName.text = if (userInfo.userName.isNullOrEmpty()) userInfo.userId else userInfo.userName
|
||||
textHint.visibility = if (userInfo.userId == TUILogin.getLoginUser()) View.VISIBLE else View.GONE
|
||||
ImageLoader.load(context, imageAvatar, userInfo.avatar, R.drawable.tuicallkit_ic_avatar)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.view.component.joiningroupcall
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.Button
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.utils.widget.ImageFilterView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallDefine.MediaType
|
||||
import com.tencent.cloud.tuikit.engine.common.TUICommonDefine
|
||||
import com.tencent.cloud.tuikit.engine.common.TUICommonDefine.ValueCallback
|
||||
import com.tencent.qcloud.tuicore.util.ScreenUtil
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.R
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.TUICallKit
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.manager.UserManager
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.state.UserState
|
||||
import com.trtc.tuikit.common.imageloader.ImageLoader
|
||||
|
||||
class JoinCallView(context: Context) : FrameLayout(context) {
|
||||
private lateinit var layoutExpand: ConstraintLayout
|
||||
private lateinit var layoutUserListView: LinearLayout
|
||||
private lateinit var imageIconExpand: ImageView
|
||||
private lateinit var textUserHint: TextView
|
||||
private lateinit var btnJoinCall: Button
|
||||
|
||||
private var context = context.applicationContext
|
||||
private var isViewExpand: Boolean = false
|
||||
|
||||
init {
|
||||
initView()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
val view = LayoutInflater.from(context).inflate(R.layout.tuicallkit_join_call_view, this)
|
||||
textUserHint = view.findViewById(R.id.tv_user_hint)
|
||||
imageIconExpand = view.findViewById(R.id.img_ic_expand)
|
||||
btnJoinCall = view.findViewById(R.id.btn_join_call)
|
||||
layoutUserListView = view.findViewById(R.id.ll_layout_avatar)
|
||||
layoutExpand = view.findViewById(R.id.cl_expand_view)
|
||||
|
||||
imageIconExpand.setBackgroundResource(R.drawable.tuicallkit_ic_join_expand)
|
||||
layoutExpand.visibility = LinearLayout.GONE
|
||||
}
|
||||
|
||||
fun updateView(
|
||||
mediaType: MediaType, userList: List<String>?,
|
||||
callId: String = "", groupId: String? = "", roomId: TUICommonDefine.RoomId? = null,
|
||||
) {
|
||||
btnJoinCall.text = context.resources.getString(R.string.tuicallkit_join_group_call)
|
||||
|
||||
if (userList.isNullOrEmpty()) {
|
||||
return
|
||||
}
|
||||
updateUserAvatarView(userList)
|
||||
|
||||
textUserHint.text = context.resources.getString(R.string.tuicallkit_join_group_call_users, userList.size)
|
||||
|
||||
btnJoinCall.setOnClickListener {
|
||||
if (!callId.isNullOrEmpty()) {
|
||||
TUICallKit.createInstance(context).join(callId, null)
|
||||
} else {
|
||||
TUICallKit.createInstance(context).joinInGroupCall(roomId, groupId, mediaType)
|
||||
}
|
||||
}
|
||||
|
||||
imageIconExpand.setOnClickListener {
|
||||
displayExpandView()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateUserAvatarView(list: List<String>) {
|
||||
UserManager.instance.updateUserListInfo(list, object : ValueCallback<List<UserState.User>?> {
|
||||
override fun onSuccess(data: List<UserState.User>?) {
|
||||
data?.let { setAvatar(it) }
|
||||
}
|
||||
|
||||
override fun onError(errCode: Int, errMsg: String?) {
|
||||
setAvatar(list)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun setAvatar(userList: List<Any>) {
|
||||
layoutUserListView.removeAllViews()
|
||||
val width = ScreenUtil.dip2px(50f)
|
||||
val startMargin = ScreenUtil.dip2px(12f)
|
||||
|
||||
for ((index, user) in userList.withIndex()) {
|
||||
val imageView = ImageFilterView(context)
|
||||
val layoutParams = LinearLayout.LayoutParams(width, width)
|
||||
if (index != 0) {
|
||||
layoutParams.marginStart = startMargin
|
||||
}
|
||||
imageView.round = 12f
|
||||
imageView.scaleType = ImageView.ScaleType.CENTER_CROP
|
||||
imageView.layoutParams = layoutParams
|
||||
if (user is UserState.User) {
|
||||
ImageLoader.load(context, imageView, user.avatar.get(), R.drawable.tuicallkit_ic_avatar)
|
||||
} else {
|
||||
ImageLoader.load(context, imageView, R.drawable.tuicallkit_ic_avatar, R.drawable.tuicallkit_ic_avatar)
|
||||
}
|
||||
layoutUserListView.addView(imageView)
|
||||
}
|
||||
}
|
||||
|
||||
private fun displayExpandView() {
|
||||
val resId = when {
|
||||
isViewExpand -> R.drawable.tuicallkit_ic_join_expand
|
||||
else -> R.drawable.tuicallkit_ic_join_compress
|
||||
}
|
||||
imageIconExpand.setBackgroundResource(resId)
|
||||
layoutExpand.visibility = if (isViewExpand) LinearLayout.GONE else LinearLayout.VISIBLE
|
||||
isViewExpand = !isViewExpand
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.view.component.joiningroupcall
|
||||
|
||||
import android.view.View
|
||||
import com.google.gson.Gson
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallDefine
|
||||
import com.tencent.cloud.tuikit.engine.common.TUICommonDefine
|
||||
import com.tencent.imsdk.v2.V2TIMGroupListener
|
||||
import com.tencent.imsdk.v2.V2TIMManager
|
||||
import com.tencent.imsdk.v2.V2TIMValueCallback
|
||||
import com.tencent.qcloud.tuicore.TUILogin
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.common.data.Logger
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.manager.CallManager
|
||||
import com.trtc.tuikit.common.livedata.Observer
|
||||
|
||||
class JoinCallViewManager() {
|
||||
private var callView: JoinCallView? = null
|
||||
private var currentGroupId: String? = null
|
||||
|
||||
private var callStatusObserver = Observer<TUICallDefine.Status> {
|
||||
if (it == TUICallDefine.Status.None) {
|
||||
currentGroupId?.let { it1 -> getGroupAttributes(it1) }
|
||||
}
|
||||
}
|
||||
|
||||
private val groupListener: V2TIMGroupListener = object : V2TIMGroupListener() {
|
||||
override fun onGroupAttributeChanged(groupID: String?, groupAttributeMap: MutableMap<String?, String>?) {
|
||||
if (groupID.isNullOrEmpty() || currentGroupId != groupID) {
|
||||
Logger.w(TAG, "onGroupAttributes, not same group(current:$currentGroupId, $groupID)}, ignore")
|
||||
return
|
||||
}
|
||||
|
||||
parseGroupAttributes(groupID, groupAttributeMap)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
V2TIMManager.getInstance().addGroupListener(groupListener)
|
||||
CallManager.instance.userState.selfUser.get().callStatus.observe(callStatusObserver)
|
||||
}
|
||||
|
||||
fun setJoinCallView(joinCallView: JoinCallView) {
|
||||
callView = joinCallView
|
||||
callView?.visibility = View.GONE
|
||||
}
|
||||
|
||||
fun getGroupAttributes(groupId: String) {
|
||||
Logger.i(TAG, "getGroupAttributes, groupId: $groupId")
|
||||
currentGroupId = groupId
|
||||
|
||||
V2TIMManager.getGroupManager().getGroupAttributes(groupId, listOf(KEY_GROUP_ATTRIBUTE),
|
||||
object : V2TIMValueCallback<Map<String?, String?>?> {
|
||||
override fun onSuccess(map: Map<String?, String?>?) {
|
||||
|
||||
parseGroupAttributes(groupId, map)
|
||||
}
|
||||
|
||||
override fun onError(code: Int, desc: String) {
|
||||
Logger.e(TAG, "getGroupAttributes failed, errorCode: $code , errorMsg: $desc")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun parseGroupAttributes(groupId: String, map: Map<String?, String?>?) {
|
||||
if (CallManager.instance.userState.selfUser.get().callStatus.get() != TUICallDefine.Status.None) {
|
||||
removeCallView()
|
||||
Logger.w(TAG, "parseGroupAttributes, user is in the call, ignore")
|
||||
return
|
||||
}
|
||||
|
||||
if (map == null || map[KEY_GROUP_ATTRIBUTE].isNullOrEmpty()) {
|
||||
Logger.w(TAG, "parseGroupAttributes is empty, map: $map")
|
||||
removeCallView()
|
||||
return
|
||||
}
|
||||
|
||||
Logger.i(TAG, "parseGroupAttributes, groupId: $groupId, map: $map")
|
||||
|
||||
val data: String? = map[KEY_GROUP_ATTRIBUTE]
|
||||
val extraMap: Map<String, Any> = Gson().fromJson<Map<String, Any>>(data, Map::class.java)
|
||||
|
||||
val businessType = extraMap[KEY_BUSINESS_TYPE] as? String
|
||||
if (businessType.isNullOrEmpty() || businessType != VALUE_BUSINESS_TYPE) {
|
||||
Logger.w(TAG, "this is not callkit, ignore")
|
||||
removeCallView()
|
||||
return
|
||||
}
|
||||
val userList = parseUserList(extraMap)
|
||||
if (userList.isEmpty() || userList.contains(TUILogin.getLoginUser()) || userList.size <= 1) {
|
||||
Logger.w(TAG, "userList is empty or current user is in the call, ignore")
|
||||
removeCallView()
|
||||
return
|
||||
}
|
||||
|
||||
val callId = extraMap[KEY_CALL_ID] as? String
|
||||
if (!callId.isNullOrEmpty()) {
|
||||
callView?.updateView(TUICallDefine.MediaType.Audio, userList, callId = callId)
|
||||
callView?.visibility = View.VISIBLE
|
||||
return
|
||||
}
|
||||
|
||||
var mediaType = TUICallDefine.MediaType.Audio
|
||||
if (extraMap[KEY_CALL_MEDIA_TYPE] == VALUE_MEDIA_TYPE_VIDEO) {
|
||||
mediaType = TUICallDefine.MediaType.Video
|
||||
}
|
||||
|
||||
val roomId = parseRoomId(extraMap)
|
||||
callView?.updateView(mediaType, userList, groupId = groupId, roomId = roomId)
|
||||
callView?.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
private fun parseRoomId(extraMap: Map<String, Any>): TUICommonDefine.RoomId {
|
||||
val roomIdType = (extraMap[KEY_ROOM_ID_TYPE] as? Double)?.toInt()
|
||||
val strRoomId = extraMap[KEY_ROOM_ID] as? String
|
||||
|
||||
val roomId = TUICommonDefine.RoomId()
|
||||
if (roomIdType == VALUE_ROOM_ID_TYPE_STRING) {
|
||||
roomId.strRoomId = strRoomId
|
||||
return roomId
|
||||
}
|
||||
|
||||
val intRoomId = strRoomId?.toIntOrNull()
|
||||
if (intRoomId != null && intRoomId != 0) {
|
||||
roomId.intRoomId = intRoomId
|
||||
} else {
|
||||
roomId.strRoomId = strRoomId
|
||||
}
|
||||
return roomId
|
||||
}
|
||||
|
||||
private fun parseUserList(extraMap: Map<String, Any>): List<String> {
|
||||
val userIds = extraMap[KEY_USER_LIST] as? List<Map<String, String>>
|
||||
if (userIds == null) {
|
||||
Logger.w(TAG, "parseUserList, userList is empty, ignore")
|
||||
return ArrayList()
|
||||
}
|
||||
val list = ArrayList<String>()
|
||||
|
||||
for (temp in userIds) {
|
||||
val userId = temp[KEY_USER_ID]
|
||||
if (userId != null) {
|
||||
list.add(userId)
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
private fun removeCallView() {
|
||||
callView?.visibility = View.GONE
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "JoinCallViewManager"
|
||||
|
||||
//Do not change these items
|
||||
private const val KEY_GROUP_ATTRIBUTE = "inner_attr_kit_info"
|
||||
private const val KEY_BUSINESS_TYPE = "business_type"
|
||||
private const val KEY_ROOM_ID_TYPE = "room_id_type"
|
||||
private const val KEY_CALL_ID = "call_id"
|
||||
private const val KEY_ROOM_ID = "room_id"
|
||||
private const val KEY_GROUP_ID = "group_id"
|
||||
private const val KEY_CALL_MEDIA_TYPE = "call_media_type"
|
||||
private const val KEY_USER_LIST = "user_list"
|
||||
private const val KEY_USER_ID = "userid"
|
||||
|
||||
private const val VALUE_BUSINESS_TYPE = "callkit"
|
||||
private const val VALUE_MEDIA_TYPE_AUDIO = "audio"
|
||||
private const val VALUE_MEDIA_TYPE_VIDEO = "video"
|
||||
private const val VALUE_ROOM_ID_TYPE_HISTORY = 0
|
||||
private const val VALUE_ROOM_ID_TYPE_INT = 1
|
||||
private const val VALUE_ROOM_ID_TYPE_STRING = 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.view.component.recents
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallDefine
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallDefine.CallRecords
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallDefine.RecentCallsFilter
|
||||
import com.tencent.qcloud.tuicore.TUIConstants
|
||||
import com.tencent.qcloud.tuicore.TUIConstants.TUICalling.ObjectFactory.RecentCalls
|
||||
import com.tencent.qcloud.tuicore.TUICore
|
||||
import com.tencent.qcloud.tuicore.TUILogin
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.R
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.TUICallKit
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.state.GlobalState
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.view.component.recents.interfaces.ICallRecordItemListener
|
||||
import com.trtc.tuikit.common.livedata.LiveListObserver
|
||||
import com.trtc.tuikit.common.ui.PopupDialog
|
||||
import com.trtc.tuikit.common.util.ToastUtil
|
||||
|
||||
class RecentCallsFragment(style: String) : Fragment() {
|
||||
private lateinit var buttonEdit: Button
|
||||
private lateinit var buttonStartCall: Button
|
||||
private lateinit var buttonEditDone: Button
|
||||
private lateinit var buttonClear: Button
|
||||
private lateinit var recyclerRecent: SlideRecyclerView
|
||||
private lateinit var listAdapter: RecentCallsListAdapter
|
||||
private lateinit var recentCallsManager: RecentCallsManager
|
||||
private var bottomDialog: PopupDialog? = null
|
||||
|
||||
private var chatViewStyle = style
|
||||
private var type = TYPE_ALL
|
||||
private var needCloseMultiMode = false
|
||||
|
||||
private val callHistoryObserver = object : LiveListObserver<CallRecords>() {
|
||||
override fun onDataChanged(list: List<CallRecords>) {
|
||||
if (listAdapter != null && TYPE_ALL == type) {
|
||||
listAdapter.onDataSourceChanged(list)
|
||||
}
|
||||
}
|
||||
}
|
||||
private val callMissObserver = object : LiveListObserver<CallRecords>() {
|
||||
override fun onDataChanged(list: List<CallRecords>) {
|
||||
if (listAdapter != null && TYPE_MISS == type) {
|
||||
listAdapter.onDataSourceChanged(list)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
val rootView = inflater.inflate(R.layout.tuicallkit_record_fragment_main, container, false)
|
||||
initView(rootView)
|
||||
initData()
|
||||
registerObserver()
|
||||
return rootView
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
refreshData()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
unregisterObserver()
|
||||
}
|
||||
|
||||
private fun registerObserver() {
|
||||
recentCallsManager.callMissedList.observe(callMissObserver)
|
||||
recentCallsManager.callHistoryList.observe(callHistoryObserver)
|
||||
}
|
||||
|
||||
private fun unregisterObserver() {
|
||||
recentCallsManager.callMissedList.removeObserver(callMissObserver)
|
||||
recentCallsManager.callHistoryList.removeObserver(callHistoryObserver)
|
||||
}
|
||||
|
||||
private fun initView(rootView: View) {
|
||||
buttonEdit = rootView.findViewById(R.id.btn_call_edit)
|
||||
buttonStartCall = rootView.findViewById(R.id.btn_start_call)
|
||||
buttonEditDone = rootView.findViewById(R.id.btn_edit_done)
|
||||
buttonClear = rootView.findViewById(R.id.btn_clear)
|
||||
recyclerRecent = rootView.findViewById(R.id.recycle_view_list)
|
||||
val layoutTab: TabLayout = rootView.findViewById(R.id.tab_layout)
|
||||
val layoutTitle: ConstraintLayout = rootView.findViewById(R.id.cl_record_title)
|
||||
if (RecentCalls.UI_STYLE_MINIMALIST == chatViewStyle) {
|
||||
layoutTitle.setBackgroundColor(ContextCompat.getColor(context!!, R.color.tuicallkit_color_white))
|
||||
}
|
||||
|
||||
buttonEdit.setOnClickListener {
|
||||
startMultiSelect()
|
||||
updateTabViews(true)
|
||||
}
|
||||
buttonStartCall.setOnClickListener {
|
||||
TUICore.startActivity("StartC2CChatMinimalistActivity", null)
|
||||
}
|
||||
buttonEditDone.setOnClickListener {
|
||||
needCloseMultiMode = true
|
||||
stopMultiSelect()
|
||||
updateTabViews(false)
|
||||
}
|
||||
buttonClear.setOnClickListener { v: View? -> showDeleteHistoryDialog() }
|
||||
layoutTab.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
||||
override fun onTabSelected(tab: TabLayout.Tab) {
|
||||
type = if (tab.position == 1) TYPE_MISS else TYPE_ALL
|
||||
updateTabViews(false)
|
||||
needCloseMultiMode = true
|
||||
stopMultiSelect()
|
||||
refreshData()
|
||||
}
|
||||
|
||||
override fun onTabUnselected(tab: TabLayout.Tab) {}
|
||||
override fun onTabReselected(tab: TabLayout.Tab) {}
|
||||
})
|
||||
}
|
||||
|
||||
private fun initData() {
|
||||
listAdapter = RecentCallsListAdapter()
|
||||
listAdapter.setHasStableIds(true)
|
||||
recyclerRecent.layoutManager = LinearLayoutManager(context)
|
||||
recyclerRecent.adapter = listAdapter
|
||||
setAdapterListener()
|
||||
recentCallsManager = RecentCallsManager(requireContext())
|
||||
recentCallsManager.queryRecentCalls(filter)
|
||||
}
|
||||
|
||||
private val filter: RecentCallsFilter
|
||||
private get() {
|
||||
val filter = RecentCallsFilter()
|
||||
if (TYPE_MISS == type) {
|
||||
filter.result = CallRecords.Result.Missed
|
||||
}
|
||||
return filter
|
||||
}
|
||||
|
||||
private fun refreshData() {
|
||||
recentCallsManager.queryRecentCalls(filter)
|
||||
}
|
||||
|
||||
private fun updateTabViews(isEditable: Boolean) {
|
||||
if (isEditable) {
|
||||
buttonEdit.visibility = View.GONE
|
||||
buttonStartCall.visibility = View.GONE
|
||||
buttonEditDone.visibility = View.VISIBLE
|
||||
buttonClear.visibility = View.VISIBLE
|
||||
} else {
|
||||
buttonEdit.visibility = View.VISIBLE
|
||||
buttonStartCall.visibility = View.GONE
|
||||
buttonEditDone.visibility = View.GONE
|
||||
buttonClear.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun setAdapterListener() {
|
||||
listAdapter.setOnCallRecordItemListener(object : ICallRecordItemListener {
|
||||
override fun onItemClick(view: View?, viewType: Int, callRecords: CallRecords?) {
|
||||
if (callRecords == null) {
|
||||
return
|
||||
}
|
||||
if (listAdapter.isMultiSelectMode) {
|
||||
return
|
||||
}
|
||||
if (!callRecords.groupId.isNullOrEmpty()) {
|
||||
startGroupInfoActivity(callRecords)
|
||||
ToastUtil.toastLongMessage(getString(R.string.tuicallkit_group_recall_unsupport))
|
||||
return
|
||||
}
|
||||
if (GlobalState.instance.enableForceUseV2API) {
|
||||
var user = callRecords.inviter
|
||||
if (TUICallDefine.Role.Caller == callRecords.role) {
|
||||
user = callRecords.inviteList[0]
|
||||
}
|
||||
TUICallKit.createInstance(context!!).call(user, callRecords.mediaType)
|
||||
} else {
|
||||
val userList = ArrayList<String>()
|
||||
userList.add(callRecords.inviter)
|
||||
userList.addAll(callRecords.inviteList)
|
||||
userList.remove(TUILogin.getLoginUser())
|
||||
|
||||
TUICallKit.createInstance(context!!).calls(userList, callRecords.mediaType, null, null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemDeleteClick(view: View?, viewType: Int, callRecords: CallRecords?) {
|
||||
if (callRecords == null) {
|
||||
return
|
||||
}
|
||||
val list: MutableList<CallRecords> = ArrayList()
|
||||
list.add(callRecords)
|
||||
deleteRecordCalls(list)
|
||||
}
|
||||
|
||||
override fun onDetailViewClick(view: View?, records: CallRecords?) {
|
||||
if (records == null) {
|
||||
return
|
||||
}
|
||||
if (!records.groupId.isNullOrEmpty()) {
|
||||
startGroupInfoActivity(records)
|
||||
return
|
||||
}
|
||||
|
||||
if (records.inviteList.size <= 1) {
|
||||
startFriendProfileActivity(records)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun startFriendProfileActivity(records: CallRecords) {
|
||||
val bundle = Bundle()
|
||||
if (TUICallDefine.Role.Caller == records.role) {
|
||||
bundle.putString(TUIConstants.TUIChat.CHAT_ID, records.inviteList[0])
|
||||
} else {
|
||||
bundle.putString(TUIConstants.TUIChat.CHAT_ID, records.inviter)
|
||||
}
|
||||
var activityName = "FriendProfileActivity"
|
||||
if (RecentCalls.UI_STYLE_MINIMALIST == chatViewStyle) {
|
||||
activityName = "FriendProfileMinimalistActivity"
|
||||
}
|
||||
TUICore.startActivity(activityName, bundle)
|
||||
}
|
||||
|
||||
private fun startGroupInfoActivity(records: CallRecords) {
|
||||
val bundle = Bundle()
|
||||
bundle.putString("group_id", records.groupId)
|
||||
var activityName = "GroupInfoActivity"
|
||||
if (RecentCalls.UI_STYLE_MINIMALIST == chatViewStyle) {
|
||||
activityName = "GroupInfoMinimalistActivity"
|
||||
}
|
||||
TUICore.startActivity(context, activityName, bundle)
|
||||
}
|
||||
|
||||
private fun startMultiSelect() {
|
||||
val adapter = recyclerRecent.adapter as RecentCallsListAdapter?
|
||||
if (adapter != null) {
|
||||
adapter.setShowMultiSelectCheckBox(true)
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
recyclerRecent.disableRecyclerViewSlide(true)
|
||||
recyclerRecent.closeMenu()
|
||||
}
|
||||
|
||||
private fun stopMultiSelect() {
|
||||
val adapter = recyclerRecent.adapter as RecentCallsListAdapter?
|
||||
if (adapter != null) {
|
||||
if (needCloseMultiMode) {
|
||||
adapter.setShowMultiSelectCheckBox(false)
|
||||
}
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
if (needCloseMultiMode) {
|
||||
recyclerRecent.disableRecyclerViewSlide(false)
|
||||
}
|
||||
recyclerRecent.closeMenu()
|
||||
}
|
||||
|
||||
private fun deleteRecordCalls(selectItem: List<CallRecords>) {
|
||||
recentCallsManager.deleteRecordCalls(selectItem)
|
||||
needCloseMultiMode = !listAdapter.isMultiSelectMode
|
||||
stopMultiSelect()
|
||||
}
|
||||
|
||||
private fun clearRecentCalls() {
|
||||
val selectedItems: List<CallRecords?>?
|
||||
selectedItems = listAdapter.selectedItem
|
||||
if (selectedItems == null) {
|
||||
return
|
||||
}
|
||||
val recordList: MutableList<CallRecords> = ArrayList()
|
||||
for (records in selectedItems) {
|
||||
if (records != null && !TextUtils.isEmpty(records.callId)) {
|
||||
recordList.add(records)
|
||||
}
|
||||
}
|
||||
recentCallsManager.deleteRecordCalls(recordList)
|
||||
}
|
||||
|
||||
private fun showDeleteHistoryDialog() {
|
||||
if (bottomDialog == null) {
|
||||
bottomDialog = PopupDialog(requireContext())
|
||||
}
|
||||
val view = LayoutInflater.from(activity).inflate(R.layout.tuicallkit_record_dialog, null)
|
||||
bottomDialog!!.setView(view)
|
||||
val textPositive = view.findViewById<TextView>(R.id.tv_clear_call_history)
|
||||
val textCancel = view.findViewById<TextView>(R.id.tv_clear_cancel)
|
||||
textPositive?.setOnClickListener {
|
||||
clearRecentCalls()
|
||||
bottomDialog?.dismiss()
|
||||
needCloseMultiMode = true
|
||||
stopMultiSelect()
|
||||
}
|
||||
textCancel?.setOnClickListener { v: View? -> bottomDialog?.dismiss() }
|
||||
bottomDialog?.show()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TYPE_ALL = "AllCall"
|
||||
private const val TYPE_MISS = "MissedCall"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.view.component.recents
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.widget.CheckBox
|
||||
import android.widget.ImageView
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallDefine
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallDefine.CallRecords
|
||||
import com.tencent.cloud.tuikit.engine.common.TUICommonDefine.ValueCallback
|
||||
import com.tencent.qcloud.tuicore.TUILogin
|
||||
import com.tencent.qcloud.tuicore.util.DateTimeUtil
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.R
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.manager.UserManager
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.state.UserState
|
||||
import java.util.Date
|
||||
|
||||
class RecentCallsItemHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
private lateinit var textUserTitle: TextView
|
||||
private lateinit var imageMediaType: ImageView
|
||||
private lateinit var textCallStatus: TextView
|
||||
private lateinit var textCallTime: TextView
|
||||
lateinit var imageDetails: ImageView
|
||||
lateinit var layoutDelete: RelativeLayout
|
||||
lateinit var checkBoxSelectCall: CheckBox
|
||||
lateinit var layoutView: ConstraintLayout
|
||||
lateinit var callIconView: RecordsIconView
|
||||
|
||||
init {
|
||||
initView()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
layoutDelete = itemView.findViewById(R.id.ll_call_delete)
|
||||
checkBoxSelectCall = itemView.findViewById(R.id.cb_call_select)
|
||||
callIconView = itemView.findViewById(R.id.call_icon)
|
||||
textUserTitle = itemView.findViewById(R.id.tv_call_user_id)
|
||||
imageMediaType = itemView.findViewById(R.id.call_media_type)
|
||||
textCallStatus = itemView.findViewById(R.id.tv_call_status)
|
||||
textCallTime = itemView.findViewById(R.id.tv_call_time)
|
||||
imageDetails = itemView.findViewById(R.id.img_call_details)
|
||||
layoutView = itemView.findViewById(R.id.cl_info_layout)
|
||||
}
|
||||
|
||||
fun layoutViews(context: Context, records: CallRecords?) {
|
||||
if (records == null) {
|
||||
return
|
||||
}
|
||||
val colorId = when (CallRecords.Result.Missed) {
|
||||
records.result -> R.color.tuicallkit_record_text_red
|
||||
else -> R.color.tuicallkit_color_black
|
||||
}
|
||||
textUserTitle.setTextColor(ContextCompat.getColor(context, colorId))
|
||||
val imageId = when (TUICallDefine.MediaType.Video) {
|
||||
records.mediaType -> R.drawable.tuicallkit_record_ic_video_call
|
||||
else -> R.drawable.tuicallkit_record_ic_audio_call
|
||||
}
|
||||
imageMediaType.setImageDrawable(ContextCompat.getDrawable(context, imageId))
|
||||
var resultMsg = context.getString(R.string.tuicallkit_record_result_unknown)
|
||||
if (CallRecords.Result.Missed == records.result) {
|
||||
resultMsg = context.getString(R.string.tuicallkit_record_result_missed)
|
||||
} else if (CallRecords.Result.Incoming == records.result) {
|
||||
resultMsg = context.getString(R.string.tuicallkit_record_result_incoming)
|
||||
} else if (CallRecords.Result.Outgoing == records.result) {
|
||||
resultMsg = context.getString(R.string.tuicallkit_record_result_outgoing)
|
||||
}
|
||||
textCallStatus.text = resultMsg
|
||||
textCallTime.text = DateTimeUtil.getTimeFormatText(Date(records.beginTime))
|
||||
val list: MutableList<String?> = ArrayList()
|
||||
if (records.inviteList != null) {
|
||||
list.addAll(records.inviteList)
|
||||
}
|
||||
list.add(records.inviter.trim { it <= ' ' })
|
||||
list.remove(TUILogin.getLoginUser())
|
||||
callIconView.tag = list
|
||||
UserManager.instance.updateUserListInfo(list, object : ValueCallback<List<UserState.User>?> {
|
||||
override fun onSuccess(userFullInfoList: List<UserState.User>?) {
|
||||
if (userFullInfoList.isNullOrEmpty()) {
|
||||
return
|
||||
}
|
||||
val avatarList: MutableList<Any?> = ArrayList()
|
||||
val newUserList: MutableList<String> = ArrayList()
|
||||
val nameList: MutableList<String> = ArrayList()
|
||||
for (i in userFullInfoList.indices) {
|
||||
avatarList.add(userFullInfoList[i].avatar.get())
|
||||
newUserList.add(userFullInfoList[i].id)
|
||||
nameList.add(userFullInfoList[i].nickname.get())
|
||||
}
|
||||
|
||||
if (!records.groupId.isNullOrEmpty()
|
||||
|| (records.scene == TUICallDefine.Scene.MULTI_CALL && records.inviteList.size > 1)) {
|
||||
avatarList.add(TUILogin.getFaceUrl())
|
||||
}
|
||||
val oldUserList: List<String> = ArrayList(
|
||||
callIconView.tag as List<String>
|
||||
)
|
||||
if (oldUserList.size == newUserList.size && oldUserList.containsAll(newUserList)) {
|
||||
callIconView.setImageId(records.callId)
|
||||
callIconView.displayImage(avatarList).load(records.callId)
|
||||
textUserTitle.text = nameList.toString().replace("[\\[\\]]".toRegex(), "")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(code: Int, desc: String?) {
|
||||
val list: MutableList<Any?> = ArrayList()
|
||||
list.add(TUILogin.getFaceUrl())
|
||||
callIconView.displayImage(list).load(records.callId)
|
||||
textUserTitle.text = TUILogin.getNickName()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.view.component.recents
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallDefine.CallRecords
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.R
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.view.component.recents.interfaces.ICallRecordItemListener
|
||||
|
||||
class RecentCallsListAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
private lateinit var context: Context
|
||||
private val dataSource: MutableList<CallRecords> = ArrayList()
|
||||
private var itemListener: ICallRecordItemListener? = null
|
||||
private val selectedPositions = HashMap<String, Boolean>()
|
||||
var isMultiSelectMode = false
|
||||
private set
|
||||
|
||||
fun setOnCallRecordItemListener(listener: ICallRecordItemListener?) {
|
||||
itemListener = listener
|
||||
}
|
||||
|
||||
fun onDataSourceChanged(dataSource: List<CallRecords>?) {
|
||||
dataSource?.let {
|
||||
this.dataSource.clear()
|
||||
this.dataSource.addAll(it)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
context = parent.context.applicationContext
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
return if (viewType == ITEM_TYPE_HEADER) {
|
||||
val view = inflater.inflate(R.layout.tuicallkit_item_head_view, parent, false)
|
||||
HeaderViewHolder(view)
|
||||
} else {
|
||||
val view = inflater.inflate(R.layout.tuicallkit_layout_call_list_item, parent, false)
|
||||
RecentCallsItemHolder(view)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
if (holder is RecentCallsItemHolder) {
|
||||
holder.layoutView.setOnClickListener {
|
||||
val curPos = holder.bindingAdapterPosition
|
||||
itemListener?.onItemClick(holder.itemView, getItemViewType(curPos), getItem(curPos))
|
||||
}
|
||||
holder.imageDetails.setOnClickListener { view: View? ->
|
||||
itemListener?.onDetailViewClick(view, getItem(holder.bindingAdapterPosition))
|
||||
}
|
||||
|
||||
if (!holder.layoutDelete.hasOnClickListeners()) {
|
||||
holder.layoutDelete.setOnClickListener { view: View? ->
|
||||
val curPos = holder.bindingAdapterPosition
|
||||
val record = getItem(curPos)
|
||||
if (record != null && !record.callId.isNullOrEmpty()) {
|
||||
setItemChecked(record.callId, true)
|
||||
itemListener?.onItemDeleteClick(view, getItemViewType(curPos), record)
|
||||
}
|
||||
}
|
||||
}
|
||||
holder.checkBoxSelectCall.setOnClickListener {
|
||||
holder.itemView.scrollX = if (isRTL) -holder.layoutDelete.width else holder.layoutDelete.width
|
||||
}
|
||||
holder.layoutViews(context, getItem(position))
|
||||
setCheckBoxStatus(holder)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
|
||||
if (holder is RecentCallsItemHolder) {
|
||||
holder.callIconView.clear()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getItem(position: Int): CallRecords? {
|
||||
if (dataSource.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
val dataPosition = position - HEADER_COUNT
|
||||
return if (dataPosition < dataSource.size && dataPosition >= 0) {
|
||||
dataSource[dataPosition]
|
||||
} else null
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return position.toLong()
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return dataSource.size + HEADER_COUNT
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return if (position == 0) {
|
||||
ITEM_TYPE_HEADER
|
||||
} else ITEM_TYPE_NORMAL
|
||||
}
|
||||
|
||||
private fun setCheckBoxStatus(holder: RecentCallsItemHolder?) {
|
||||
if (holder?.checkBoxSelectCall == null) {
|
||||
return
|
||||
}
|
||||
if (!isMultiSelectMode) {
|
||||
holder.checkBoxSelectCall.visibility = View.GONE
|
||||
holder.itemView.setOnClickListener(null)
|
||||
} else {
|
||||
holder.checkBoxSelectCall.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
private fun getIndexInAdapter(records: CallRecords): Int {
|
||||
var position = -1
|
||||
if (dataSource.size > 0) {
|
||||
val indexInData = dataSource.indexOf(records)
|
||||
if (indexInData != -1) {
|
||||
position = indexInData + HEADER_COUNT
|
||||
}
|
||||
}
|
||||
return position
|
||||
}
|
||||
|
||||
fun setShowMultiSelectCheckBox(show: Boolean) {
|
||||
isMultiSelectMode = show
|
||||
for (records in dataSource) {
|
||||
if (!records.callId.isNullOrEmpty()) {
|
||||
setItemChecked(records.callId, show)
|
||||
val currentPosition = getIndexInAdapter(records)
|
||||
if (currentPosition != -1) {
|
||||
notifyItemChanged(currentPosition)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setItemChecked(callId: String, isChecked: Boolean) {
|
||||
selectedPositions[callId] = isChecked
|
||||
}
|
||||
|
||||
val selectedItem: List<CallRecords>?
|
||||
get() {
|
||||
if (selectedPositions.size == 0) {
|
||||
return null
|
||||
}
|
||||
val selectList: MutableList<CallRecords> = ArrayList()
|
||||
for (i in 0 until itemCount) {
|
||||
val records = getItem(i)
|
||||
if (records != null && isItemChecked(records.callId)) {
|
||||
selectList.add(records)
|
||||
}
|
||||
}
|
||||
return selectList
|
||||
}
|
||||
|
||||
private fun isItemChecked(id: String): Boolean {
|
||||
if (selectedPositions.size <= 0) {
|
||||
return false
|
||||
}
|
||||
return if (selectedPositions.containsKey(id)) {
|
||||
selectedPositions[id] ?: false
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private val isRTL: Boolean
|
||||
private get() {
|
||||
return context.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL
|
||||
}
|
||||
|
||||
internal class HeaderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
|
||||
companion object {
|
||||
private const val ITEM_TYPE_HEADER = 101
|
||||
private const val ITEM_TYPE_NORMAL = -98
|
||||
private const val HEADER_COUNT = 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.view.component.recents
|
||||
|
||||
import android.content.Context
|
||||
import android.text.TextUtils
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallDefine.CallRecords
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallDefine.RecentCallsFilter
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallEngine
|
||||
import com.tencent.cloud.tuikit.engine.common.TUICommonDefine
|
||||
import com.trtc.tuikit.common.livedata.LiveListData
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
class RecentCallsManager(context: Context) {
|
||||
private val context: Context = context.applicationContext
|
||||
var callHistoryList: LiveListData<CallRecords> = LiveListData<CallRecords>(CopyOnWriteArrayList())
|
||||
var callMissedList: LiveListData<CallRecords> = LiveListData<CallRecords>(CopyOnWriteArrayList())
|
||||
|
||||
fun queryRecentCalls(filter: RecentCallsFilter?) {
|
||||
TUICallEngine.createInstance(context).queryRecentCalls(filter, object : TUICommonDefine.ValueCallback<Any?> {
|
||||
override fun onSuccess(data: Any?) {
|
||||
if (data == null || data !is List<*>) {
|
||||
return
|
||||
}
|
||||
val queryList = data as ArrayList<CallRecords>
|
||||
if (filter != null && CallRecords.Result.Missed == filter.result) {
|
||||
val missList: ArrayList<CallRecords> = ArrayList(callMissedList.list)
|
||||
missList.removeAll(queryList)
|
||||
missList.addAll(queryList)
|
||||
callMissedList.replaceAll(missList)
|
||||
} else {
|
||||
val historyList: ArrayList<CallRecords> = ArrayList(callHistoryList.list)
|
||||
historyList.removeAll(queryList)
|
||||
historyList.addAll(queryList)
|
||||
callHistoryList.replaceAll(historyList.toList())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(errCode: Int, errMsg: String) {}
|
||||
})
|
||||
}
|
||||
|
||||
fun deleteRecordCalls(list: List<CallRecords>?) {
|
||||
if (list.isNullOrEmpty()) {
|
||||
return
|
||||
}
|
||||
val missList = ArrayList(callMissedList.list)
|
||||
missList.removeAll(list)
|
||||
callMissedList.replaceAll(missList)
|
||||
val allList: MutableList<CallRecords> = ArrayList(callHistoryList.list)
|
||||
allList.removeAll(list)
|
||||
callHistoryList.replaceAll(allList)
|
||||
val callIdList: MutableList<String> = ArrayList()
|
||||
for (record in list) {
|
||||
if (!TextUtils.isEmpty(record.callId)) {
|
||||
callIdList.add(record.callId)
|
||||
}
|
||||
}
|
||||
TUICallEngine.createInstance(context).deleteRecordCalls(callIdList,
|
||||
object : TUICommonDefine.ValueCallback<Any?> {
|
||||
override fun onSuccess(data: Any?) {}
|
||||
override fun onError(errCode: Int, errMsg: String) {}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.view.component.recents
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.util.AttributeSet
|
||||
import androidx.constraintlayout.utils.widget.ImageFilterView
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.R
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.view.component.recents.gridimage.GridImageSynthesizer
|
||||
|
||||
class RecordsIconView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) :
|
||||
ImageFilterView(context, attrs, defStyle) {
|
||||
private var imageSize = 100
|
||||
private var imageGap = 6
|
||||
private val gridImageSynthesizer: GridImageSynthesizer = GridImageSynthesizer(context, this)
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
initView()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
gridImageSynthesizer.setMaxSize(imageSize, imageSize)
|
||||
gridImageSynthesizer.setDefaultImage(R.drawable.tuicallkit_ic_avatar)
|
||||
gridImageSynthesizer.setBackgroundColor(Color.parseColor("#cfd3d8"))
|
||||
gridImageSynthesizer.setGap(imageGap)
|
||||
}
|
||||
|
||||
fun displayImage(imageUrls: List<Any?>?): RecordsIconView {
|
||||
gridImageSynthesizer.setImageUrls(imageUrls)
|
||||
return this
|
||||
}
|
||||
|
||||
fun setImageId(id: String?) {
|
||||
gridImageSynthesizer.setImageId(id)
|
||||
}
|
||||
|
||||
fun load(imageId: String?) {
|
||||
gridImageSynthesizer.load(imageId)
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
gridImageSynthesizer.clearImage()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.view.component.recents
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Rect
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.view.VelocityTracker
|
||||
import android.view.View
|
||||
import android.view.ViewConfiguration
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Scroller
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlin.math.abs
|
||||
|
||||
class SlideRecyclerView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) :
|
||||
RecyclerView(context, attrs, defStyle) {
|
||||
private val touchSlop: Int = ViewConfiguration.get(context).scaledTouchSlop
|
||||
private val scroller: Scroller = Scroller(context)
|
||||
|
||||
private var flingView: ViewGroup? = null
|
||||
private var velocityTracker: VelocityTracker? = null
|
||||
private var touchFrame: Rect? = null
|
||||
private var lastX: Float = 0f
|
||||
private var firstX: Float = 0f
|
||||
private var firstY: Float = 0f
|
||||
private var isSlide = false
|
||||
private var position: Int = 0
|
||||
private var menuViewWidth: Int = 0
|
||||
private var disableRecyclerViewSlide: Boolean = false
|
||||
|
||||
fun disableRecyclerViewSlide(disable: Boolean) {
|
||||
disableRecyclerViewSlide = disable
|
||||
}
|
||||
|
||||
override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
|
||||
val x = e.x.toInt()
|
||||
val y = e.y.toInt()
|
||||
obtainVelocity(e)
|
||||
when (e.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
if (!scroller.isFinished) {
|
||||
scroller.abortAnimation()
|
||||
}
|
||||
run {
|
||||
lastX = x.toFloat()
|
||||
firstX = lastX
|
||||
}
|
||||
firstY = y.toFloat()
|
||||
position = pointToPosition(x, y)
|
||||
if (position != INVALID_POSITION) {
|
||||
val view: View? = flingView
|
||||
flingView = getChildAt(
|
||||
position - (layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
|
||||
) as ViewGroup
|
||||
if (view != null && flingView !== view && view.scrollX != 0) {
|
||||
view.scrollTo(0, 0)
|
||||
}
|
||||
menuViewWidth = if (flingView?.childCount == 2) {
|
||||
flingView?.getChildAt(1)?.width ?: INVALID_CHILD_WIDTH
|
||||
} else {
|
||||
INVALID_CHILD_WIDTH
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
velocityTracker?.computeCurrentVelocity(1000)
|
||||
val xVelocity = (if (velocityTracker?.xVelocity != null) velocityTracker?.xVelocity else 0f) as Float
|
||||
val yVelocity = (if (velocityTracker?.yVelocity != null) velocityTracker?.yVelocity else 0f) as Float
|
||||
|
||||
val topView = (layoutManager as LinearLayoutManager?)?.findViewByPosition(0)
|
||||
if (topView === flingView) {
|
||||
isSlide = false
|
||||
} else if (abs(xVelocity) > SNAP_VELOCITY && abs(xVelocity) > abs(yVelocity)
|
||||
|| abs(x - firstX) >= touchSlop && abs(x - firstX) > abs(y - firstY)
|
||||
) {
|
||||
isSlide = true
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_UP -> releaseVelocity()
|
||||
else -> {}
|
||||
}
|
||||
return super.onInterceptTouchEvent(e)
|
||||
}
|
||||
|
||||
override fun onTouchEvent(e: MotionEvent): Boolean {
|
||||
if (isSlide && position != INVALID_POSITION) {
|
||||
val x = e.x
|
||||
obtainVelocity(e)
|
||||
when (e.action) {
|
||||
MotionEvent.ACTION_DOWN -> {}
|
||||
MotionEvent.ACTION_MOVE ->
|
||||
flingView?.let {
|
||||
if (menuViewWidth != INVALID_CHILD_WIDTH) {
|
||||
val dx = Math.abs(it.scrollX + (lastX - x))
|
||||
if (dx <= menuViewWidth && dx > 0) {
|
||||
if (menuViewWidth != INVALID_CHILD_WIDTH) {
|
||||
val scrollX = it.scrollX
|
||||
velocityTracker?.computeCurrentVelocity(1000)
|
||||
if (isRTL) {
|
||||
openRightExtendView(scrollX)
|
||||
} else {
|
||||
openLeftExtendView(scrollX)
|
||||
}
|
||||
invalidate()
|
||||
}
|
||||
menuViewWidth = INVALID_CHILD_WIDTH
|
||||
isSlide = false
|
||||
position = INVALID_POSITION
|
||||
}
|
||||
lastX = x
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_UP -> releaseVelocity()
|
||||
else -> {}
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
closeMenu()
|
||||
releaseVelocity()
|
||||
}
|
||||
return super.onTouchEvent(e)
|
||||
}
|
||||
|
||||
private fun openRightExtendView(scrollX: Int) {
|
||||
velocityTracker?.let {
|
||||
if (it.xVelocity >= SNAP_VELOCITY) {
|
||||
startScroll(scrollX, scrollX - menuViewWidth)
|
||||
} else if (it.xVelocity < -SNAP_VELOCITY) {
|
||||
startScroll(scrollX, -scrollX)
|
||||
} else if (scrollX >= menuViewWidth / 2) {
|
||||
startScroll(scrollX, scrollX - menuViewWidth)
|
||||
} else {
|
||||
startScroll(scrollX, -scrollX)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun openLeftExtendView(scrollX: Int) {
|
||||
velocityTracker?.let {
|
||||
if (it.xVelocity < -SNAP_VELOCITY) {
|
||||
startScroll(scrollX, menuViewWidth - scrollX)
|
||||
} else if (it.xVelocity >= SNAP_VELOCITY) {
|
||||
startScroll(scrollX, -scrollX)
|
||||
} else if (scrollX >= menuViewWidth / 2) {
|
||||
startScroll(scrollX, menuViewWidth - scrollX)
|
||||
} else {
|
||||
startScroll(scrollX, -scrollX)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startScroll(startX: Int, dx: Int) {
|
||||
scroller.startScroll(startX, 0, dx, 0)
|
||||
}
|
||||
|
||||
private fun releaseVelocity() {
|
||||
if (velocityTracker != null) {
|
||||
velocityTracker?.clear()
|
||||
velocityTracker?.recycle()
|
||||
velocityTracker = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun obtainVelocity(event: MotionEvent) {
|
||||
if (velocityTracker == null) {
|
||||
velocityTracker = VelocityTracker.obtain()
|
||||
}
|
||||
velocityTracker?.addMovement(event)
|
||||
}
|
||||
|
||||
private fun pointToPosition(x: Int, y: Int): Int {
|
||||
val firstPosition = (layoutManager as LinearLayoutManager?)?.findFirstVisibleItemPosition()
|
||||
firstPosition?.let {
|
||||
var frame = touchFrame
|
||||
if (frame == null) {
|
||||
touchFrame = Rect()
|
||||
frame = touchFrame
|
||||
}
|
||||
val count = childCount
|
||||
for (i in count - 1 downTo 0) {
|
||||
val child = getChildAt(i)
|
||||
if (child.visibility == View.VISIBLE) {
|
||||
child.getHitRect(frame)
|
||||
frame?.let {
|
||||
if (it.contains(x, y)) {
|
||||
return firstPosition + i
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return INVALID_POSITION
|
||||
}
|
||||
|
||||
override fun computeScroll() {
|
||||
if (scroller.computeScrollOffset()) {
|
||||
if (disableRecyclerViewSlide) {
|
||||
flingView?.scrollTo(0, 0)
|
||||
} else {
|
||||
flingView?.scrollTo(scroller.currX, scroller.currY)
|
||||
}
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
fun closeMenu() {
|
||||
if (flingView != null && flingView?.scrollX != 0) {
|
||||
flingView?.scrollTo(0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
private val isRTL: Boolean = context.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL
|
||||
|
||||
companion object {
|
||||
private const val INVALID_POSITION = -1
|
||||
private const val INVALID_CHILD_WIDTH = -1
|
||||
private const val SNAP_VELOCITY = 600
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.view.component.recents.gridimage
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Rect
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.text.TextUtils
|
||||
import android.widget.ImageView
|
||||
import com.tencent.qcloud.tuicore.TUIConfig
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.R
|
||||
import com.trtc.tuikit.common.imageloader.ImageLoader
|
||||
import com.trtc.tuikit.common.imageloader.ImageOptions
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.ExecutionException
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.SynchronousQueue
|
||||
import java.util.concurrent.ThreadPoolExecutor
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class GridImageSynthesizer(context: Context, view: ImageView) {
|
||||
private val context: Context = context.applicationContext
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
private var executors: ExecutorService? = null
|
||||
|
||||
private val imageView: ImageView = view
|
||||
private var gridImageData: GridImageData = GridImageData()
|
||||
private var imageId = ""
|
||||
|
||||
fun setImageId(id: String?) {
|
||||
imageId = id ?: ""
|
||||
}
|
||||
|
||||
fun setImageUrls(list: List<Any?>?) {
|
||||
gridImageData.imageUrlList = list
|
||||
}
|
||||
|
||||
fun setMaxSize(maxWidth: Int, maxHeight: Int) {
|
||||
gridImageData.maxWidth = maxWidth
|
||||
gridImageData.maxHeight = maxHeight
|
||||
}
|
||||
|
||||
fun setBackgroundColor(bgColor: Int) {
|
||||
gridImageData.bgColor = bgColor
|
||||
}
|
||||
|
||||
fun setGap(gap: Int) {
|
||||
gridImageData.gap = gap
|
||||
}
|
||||
|
||||
fun setDefaultImage(resId: Int) {
|
||||
gridImageData.defaultImageResId = resId
|
||||
}
|
||||
|
||||
private fun getDefaultImage(): Int {
|
||||
return gridImageData.defaultImageResId
|
||||
}
|
||||
|
||||
private fun calculateGridParam(imagesSize: Int): IntArray {
|
||||
val gridParam = IntArray(2)
|
||||
if (imagesSize < 3) {
|
||||
gridParam[0] = 1
|
||||
gridParam[1] = imagesSize
|
||||
} else if (imagesSize <= 4) {
|
||||
gridParam[0] = 2
|
||||
gridParam[1] = 2
|
||||
} else {
|
||||
gridParam[0] = imagesSize / 3 + if (imagesSize % 3 == 0) 0 else 1
|
||||
gridParam[1] = 3
|
||||
}
|
||||
return gridParam
|
||||
}
|
||||
|
||||
private fun asyncLoadImageList(imageData: GridImageData): Boolean {
|
||||
val loadSuccess = true
|
||||
val imageUrlList = imageData.imageUrlList
|
||||
imageUrlList?.let {
|
||||
for (i in it.indices) {
|
||||
val defaultIcon = BitmapFactory.decodeResource(context.resources, R.drawable.tuicallkit_ic_avatar)
|
||||
try {
|
||||
val bitmap = asyncLoadImage(it[i] ?: "", imageData.targetImageSize)
|
||||
if (bitmap != null) {
|
||||
imageData.putBitmap(bitmap, i)
|
||||
} else {
|
||||
imageData.putBitmap(defaultIcon, i)
|
||||
}
|
||||
} catch (e: InterruptedException) {
|
||||
e.printStackTrace()
|
||||
imageData.putBitmap(defaultIcon, i)
|
||||
} catch (e: ExecutionException) {
|
||||
e.printStackTrace()
|
||||
imageData.putBitmap(defaultIcon, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
return loadSuccess
|
||||
}
|
||||
|
||||
private fun synthesizeImageList(imageData: GridImageData): Bitmap {
|
||||
val mergeBitmap = Bitmap.createBitmap(imageData.maxWidth, imageData.maxHeight, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(mergeBitmap)
|
||||
drawDrawable(canvas, imageData)
|
||||
canvas.save()
|
||||
canvas.restore()
|
||||
return mergeBitmap
|
||||
}
|
||||
|
||||
private fun drawDrawable(canvas: Canvas, imageData: GridImageData) {
|
||||
canvas.drawColor(imageData.bgColor)
|
||||
val size = imageData.size()
|
||||
val tCenter = (imageData.maxHeight + imageData.gap) / 2
|
||||
val bCenter = (imageData.maxHeight - imageData.gap) / 2
|
||||
val lCenter = (imageData.maxWidth + imageData.gap) / 2
|
||||
val rCenter = (imageData.maxWidth - imageData.gap) / 2
|
||||
val center = (imageData.maxHeight - imageData.targetImageSize) / 2
|
||||
for (i in 0 until size) {
|
||||
val rowNum = i / imageData.columnCount
|
||||
val columnNum = i % imageData.columnCount
|
||||
val left: Int =
|
||||
(imageData.targetImageSize * (if (imageData.columnCount == 1) columnNum + 0.5 else columnNum).toFloat()
|
||||
+ imageData.gap * (columnNum + 1)).toInt()
|
||||
val top = (imageData.targetImageSize * (if (imageData.columnCount == 1) rowNum + 0.5 else rowNum).toFloat()
|
||||
+ imageData.gap * (rowNum + 1)).toInt()
|
||||
val right = left + imageData.targetImageSize
|
||||
val bottom = top + imageData.targetImageSize
|
||||
val bitmap = imageData.getBitmap(i)
|
||||
|
||||
if (size == 1 || size == 4 || size == 9) {
|
||||
drawBitmapAtPosition(canvas, left, top, right, bottom, bitmap)
|
||||
continue
|
||||
}
|
||||
if (size == 2) {
|
||||
drawBitmapAtPosition(canvas, left, center, right, center + imageData.targetImageSize, bitmap)
|
||||
continue
|
||||
}
|
||||
if (size == 3) {
|
||||
when (i) {
|
||||
0 -> drawBitmapAtPosition(canvas, center, top, center + imageData.targetImageSize, bottom, bitmap)
|
||||
else -> drawBitmapAtPosition(
|
||||
canvas, imageData.gap * i + imageData.targetImageSize * (i - 1), tCenter,
|
||||
imageData.gap * i + imageData.targetImageSize * i, tCenter + imageData.targetImageSize,
|
||||
bitmap
|
||||
)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (size == 5) {
|
||||
when (i) {
|
||||
0 -> drawBitmapAtPosition(
|
||||
canvas, rCenter - imageData.targetImageSize,
|
||||
rCenter - imageData.targetImageSize, rCenter, rCenter, bitmap
|
||||
)
|
||||
1 -> drawBitmapAtPosition(
|
||||
canvas, lCenter, rCenter - imageData.targetImageSize,
|
||||
lCenter + imageData.targetImageSize, rCenter, bitmap
|
||||
)
|
||||
else -> drawBitmapAtPosition(
|
||||
canvas, imageData.gap * (i - 1) + imageData.targetImageSize * (i - 2),
|
||||
tCenter, imageData.gap * (i - 1) + imageData.targetImageSize * (i - 1),
|
||||
tCenter + imageData.targetImageSize, bitmap
|
||||
)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (size == 6) {
|
||||
when {
|
||||
i < 3 -> drawBitmapAtPosition(
|
||||
canvas, imageData.gap * (i + 1) + imageData.targetImageSize * i,
|
||||
bCenter - imageData.targetImageSize,
|
||||
imageData.gap * (i + 1) + imageData.targetImageSize * (i + 1), bCenter, bitmap
|
||||
)
|
||||
else -> drawBitmapAtPosition(
|
||||
canvas, imageData.gap * (i - 2) + imageData.targetImageSize * (i - 3),
|
||||
tCenter, imageData.gap * (i - 2) + imageData.targetImageSize * (i - 2),
|
||||
tCenter + imageData.targetImageSize, bitmap
|
||||
)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (size == 7) {
|
||||
when (i) {
|
||||
0 -> drawBitmapAtPosition(
|
||||
canvas, center, imageData.gap, center + imageData.targetImageSize,
|
||||
imageData.gap + imageData.targetImageSize, bitmap
|
||||
)
|
||||
in 1..3 -> drawBitmapAtPosition(
|
||||
canvas, imageData.gap * i + imageData.targetImageSize * (i - 1), center,
|
||||
imageData.gap * i + imageData.targetImageSize * i, center + imageData.targetImageSize,
|
||||
bitmap
|
||||
)
|
||||
else -> drawBitmapAtPosition(
|
||||
canvas, imageData.gap * (i - 3) + imageData.targetImageSize * (i - 4),
|
||||
tCenter + imageData.targetImageSize / 2,
|
||||
imageData.gap * (i - 3) + imageData.targetImageSize * (i - 3),
|
||||
tCenter + imageData.targetImageSize / 2 + imageData.targetImageSize, bitmap
|
||||
)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (size == 8) {
|
||||
when (i) {
|
||||
0 -> drawBitmapAtPosition(
|
||||
canvas, rCenter - imageData.targetImageSize, imageData.gap, rCenter,
|
||||
imageData.gap + imageData.targetImageSize, bitmap
|
||||
)
|
||||
1 -> drawBitmapAtPosition(
|
||||
canvas, lCenter, imageData.gap, lCenter + imageData.targetImageSize,
|
||||
imageData.gap + imageData.targetImageSize, bitmap
|
||||
)
|
||||
in 2..4 -> drawBitmapAtPosition(
|
||||
canvas, imageData.gap * (i - 1) + imageData.targetImageSize * (i - 2),
|
||||
center, imageData.gap * (i - 1) + imageData.targetImageSize * (i - 1),
|
||||
center + imageData.targetImageSize, bitmap
|
||||
)
|
||||
else -> drawBitmapAtPosition(
|
||||
canvas, imageData.gap * (i - 4) + imageData.targetImageSize * (i - 5),
|
||||
tCenter + imageData.targetImageSize / 2,
|
||||
imageData.gap * (i - 4) + imageData.targetImageSize * (i - 4),
|
||||
tCenter + imageData.targetImageSize / 2 + imageData.targetImageSize, bitmap
|
||||
)
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawBitmapAtPosition(canvas: Canvas, left: Int, top: Int, right: Int, bottom: Int, bitmap: Bitmap?) {
|
||||
if (null != bitmap) {
|
||||
val rect = Rect(left, top, right, bottom)
|
||||
canvas.drawBitmap(bitmap, null, rect, null)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(ExecutionException::class, InterruptedException::class)
|
||||
private fun asyncLoadImage(imgUrl: Any, targetImageSize: Int): Bitmap? {
|
||||
val option = ImageOptions.Builder().setPlaceImage(R.drawable.tuicallkit_ic_avatar).build()
|
||||
return ImageLoader.transformBitmap(context, imgUrl, targetImageSize, targetImageSize, option)
|
||||
}
|
||||
|
||||
fun load(imageId: String?) {
|
||||
if (gridImageData.size() == 0) {
|
||||
if (imageId != null && !TextUtils.equals(imageId, this.imageId)) {
|
||||
return
|
||||
}
|
||||
ImageLoader.load(context, imageView, getDefaultImage(), R.drawable.tuicallkit_ic_avatar)
|
||||
return
|
||||
}
|
||||
if (gridImageData.size() == 1) {
|
||||
if (imageId != null && !TextUtils.equals(imageId, this.imageId)) {
|
||||
return
|
||||
}
|
||||
if (gridImageData.imageUrlList != null) {
|
||||
ImageLoader.load(context, imageView, gridImageData.imageUrlList!![0], R.drawable.tuicallkit_ic_avatar)
|
||||
}
|
||||
return
|
||||
}
|
||||
clearImage()
|
||||
val copyGridImageData = try {
|
||||
gridImageData.clone()
|
||||
} catch (e: CloneNotSupportedException) {
|
||||
e.printStackTrace()
|
||||
val urlList: ArrayList<Any?> = ArrayList()
|
||||
if (gridImageData.imageUrlList != null) {
|
||||
urlList.addAll(gridImageData.imageUrlList!!)
|
||||
}
|
||||
GridImageData(urlList, gridImageData.defaultImageResId)
|
||||
}
|
||||
val gridParam = calculateGridParam(gridImageData.size())
|
||||
copyGridImageData.rowCount = gridParam[0]
|
||||
copyGridImageData.columnCount = gridParam[1]
|
||||
copyGridImageData.targetImageSize = (copyGridImageData.maxWidth - (copyGridImageData.columnCount + 1)
|
||||
* copyGridImageData.gap) / if (copyGridImageData.columnCount == 1) 2 else copyGridImageData.columnCount
|
||||
val finalImageId = imageId
|
||||
execute {
|
||||
val file = File(TUIConfig.getImageBaseDir() + finalImageId)
|
||||
var cacheBitmapExists = false
|
||||
var existsBitmap: Bitmap? = null
|
||||
if (file.exists() && file.isFile) {
|
||||
val options = BitmapFactory.Options()
|
||||
existsBitmap = BitmapFactory.decodeFile(file.path, options)
|
||||
if (options.outWidth > 0 && options.outHeight > 0) {
|
||||
cacheBitmapExists = true
|
||||
}
|
||||
}
|
||||
if (!cacheBitmapExists) {
|
||||
asyncLoadImageList(copyGridImageData)
|
||||
existsBitmap = synthesizeImageList(copyGridImageData)
|
||||
storeBitmap(file, existsBitmap)
|
||||
}
|
||||
loadImage(existsBitmap, finalImageId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadImage(bitmap: Bitmap?, targetId: String?) {
|
||||
mainHandler.post {
|
||||
if (TextUtils.equals(imageId, targetId)) {
|
||||
ImageLoader.load(context, imageView, bitmap, R.drawable.tuicallkit_ic_avatar)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearImage() {
|
||||
ImageLoader.clear(context, imageView)
|
||||
}
|
||||
|
||||
private fun storeBitmap(outFile: File, bitmap: Bitmap) {
|
||||
if (!outFile.exists() || outFile.isDirectory) {
|
||||
outFile.parentFile.mkdirs()
|
||||
}
|
||||
var fOut: FileOutputStream? = null
|
||||
try {
|
||||
outFile.deleteOnExit()
|
||||
outFile.createNewFile()
|
||||
fOut = FileOutputStream(outFile)
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fOut)
|
||||
fOut.flush()
|
||||
} catch (e: IOException) {
|
||||
outFile.deleteOnExit()
|
||||
} finally {
|
||||
if (null != fOut) {
|
||||
try {
|
||||
fOut.close()
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
outFile.deleteOnExit()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun execute(runnable: Runnable) {
|
||||
if (executors == null) {
|
||||
executors = ThreadPoolExecutor(
|
||||
0, Int.MAX_VALUE, 60L, TimeUnit.SECONDS,
|
||||
SynchronousQueue()
|
||||
)
|
||||
}
|
||||
executors?.execute(runnable)
|
||||
}
|
||||
|
||||
internal class GridImageData : Cloneable {
|
||||
var imageUrlList: List<Any?>? = ArrayList()
|
||||
var defaultImageResId: Int = 0
|
||||
var bgColor: Int = Color.parseColor("#cfd3d8")
|
||||
var targetImageSize: Int = 0
|
||||
var maxWidth: Int = 100
|
||||
var maxHeight: Int = 100
|
||||
var rowCount: Int = 0
|
||||
var columnCount: Int = 0
|
||||
var gap: Int = 6
|
||||
|
||||
private var bitmapMap: MutableMap<Int, Bitmap?> = HashMap()
|
||||
|
||||
constructor()
|
||||
|
||||
constructor(imageUrlList: List<Any?>?, defaultImageResId: Int) {
|
||||
this.imageUrlList = imageUrlList
|
||||
this.defaultImageResId = defaultImageResId
|
||||
}
|
||||
|
||||
fun putBitmap(bitmap: Bitmap, position: Int) {
|
||||
synchronized(bitmapMap) { bitmapMap.put(position, bitmap) }
|
||||
}
|
||||
|
||||
fun getBitmap(position: Int): Bitmap? {
|
||||
synchronized(bitmapMap) { return bitmapMap[position] }
|
||||
return null
|
||||
}
|
||||
|
||||
fun size(): Int {
|
||||
imageUrlList?.let {
|
||||
return Integer.min(it.size, MAX_SIZE)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
@Throws(CloneNotSupportedException::class)
|
||||
public override fun clone(): GridImageData {
|
||||
val gridImageData = super.clone() as GridImageData
|
||||
imageUrlList?.let {
|
||||
gridImageData.imageUrlList = ArrayList(it.size)
|
||||
(gridImageData.imageUrlList as ArrayList).addAll(it)
|
||||
}
|
||||
gridImageData.bitmapMap.putAll(bitmapMap)
|
||||
return gridImageData
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MAX_SIZE = 9
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.view.component.recents.interfaces
|
||||
|
||||
import android.view.View
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallDefine.CallRecords
|
||||
|
||||
interface ICallRecordItemListener {
|
||||
fun onItemClick(view: View?, viewType: Int, callRecords: CallRecords?)
|
||||
fun onItemDeleteClick(view: View?, viewType: Int, callRecords: CallRecords?)
|
||||
fun onDetailViewClick(view: View?, callRecords: CallRecords?)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.view.component.videolayout
|
||||
|
||||
import android.content.Context
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallDefine
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.manager.CallManager
|
||||
|
||||
class CallVideoLayout(context: Context) : ConstraintLayout(context) {
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
initView()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
layoutParams.width = LayoutParams.MATCH_PARENT
|
||||
layoutParams.height = LayoutParams.MATCH_PARENT
|
||||
|
||||
if (CallManager.instance.callState.scene.get() == TUICallDefine.Scene.GROUP_CALL) {
|
||||
addView(MultiCallVideoLayout(context), layoutParams)
|
||||
} else {
|
||||
addView(SingleCallVideoLayout(context), layoutParams)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.view.component.videolayout
|
||||
|
||||
import android.content.Context
|
||||
import android.view.GestureDetector
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.View.OnTouchListener
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.transition.TransitionManager
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallDefine
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallDefine.Status
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.R
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.manager.CallManager
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.state.UserState
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.view.component.videolayout.multicall.MultiCallGridLayout
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.view.component.videolayout.multicall.MultiCallGridLayout.Companion.DEFAULT_INDEX
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.view.component.videolayout.multicall.MultiCallWaitingView
|
||||
import com.trtc.tuikit.common.livedata.Observer
|
||||
import java.util.concurrent.CopyOnWriteArraySet
|
||||
|
||||
class MultiCallVideoLayout(context: Context) : ConstraintLayout(context) {
|
||||
private lateinit var layoutCalleeWaitingView: LinearLayout
|
||||
private lateinit var layoutVideoGrid: MultiCallGridLayout
|
||||
|
||||
private var userList: CopyOnWriteArraySet<UserState.User> = CopyOnWriteArraySet()
|
||||
|
||||
private val callStatusObserver = Observer<Status> {
|
||||
if (it == Status.Accept) {
|
||||
if (CallManager.instance.userState.selfUser.get().callRole == TUICallDefine.Role.Called) {
|
||||
updateVideoGrid(userList)
|
||||
}
|
||||
updateWaitingView()
|
||||
}
|
||||
}
|
||||
|
||||
private var remoteUserListObserver = Observer<LinkedHashSet<UserState.User>> {
|
||||
if (it == null || it.size <= 0) {
|
||||
return@Observer
|
||||
}
|
||||
val newUsers = it - userList
|
||||
if (!newUsers.isNullOrEmpty()) {
|
||||
userList.addAll(newUsers)
|
||||
updateVideoGrid(newUsers)
|
||||
return@Observer
|
||||
}
|
||||
|
||||
val removedUsers = userList - it - CallManager.instance.userState.selfUser.get()
|
||||
if (!removedUsers.isNullOrEmpty()) {
|
||||
userList.removeAll(removedUsers)
|
||||
updateVideoGrid(removedUsers)
|
||||
}
|
||||
}
|
||||
|
||||
private var showLargeViewUserIdObserver = Observer<String> {
|
||||
var index = DEFAULT_INDEX
|
||||
for ((i, user) in userList.withIndex()) {
|
||||
if (it == user.id) {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
TransitionManager.beginDelayedTransition(layoutVideoGrid)
|
||||
setLargeViewIndex(index)
|
||||
}
|
||||
|
||||
init {
|
||||
userList.add(CallManager.instance.userState.selfUser.get())
|
||||
userList.addAll(CallManager.instance.userState.remoteUserList.get())
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
initView()
|
||||
registerObserver()
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
unregisterObserver()
|
||||
layoutVideoGrid.removeAllViews()
|
||||
userList.clear()
|
||||
}
|
||||
|
||||
private fun registerObserver() {
|
||||
CallManager.instance.userState.selfUser.get().callStatus.observe(callStatusObserver)
|
||||
CallManager.instance.userState.remoteUserList.observe(remoteUserListObserver)
|
||||
CallManager.instance.viewState.showLargeViewUserId.observe(showLargeViewUserIdObserver)
|
||||
}
|
||||
|
||||
private fun unregisterObserver() {
|
||||
CallManager.instance.userState.selfUser.get().callStatus.removeObserver(callStatusObserver)
|
||||
CallManager.instance.userState.remoteUserList.removeObserver(remoteUserListObserver)
|
||||
CallManager.instance.viewState.showLargeViewUserId.removeObserver(
|
||||
showLargeViewUserIdObserver
|
||||
)
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
LayoutInflater.from(context).inflate(R.layout.tuicallkit_video_layout_group, this)
|
||||
layoutCalleeWaitingView = findViewById(R.id.ll_callee_waiting_view)
|
||||
layoutVideoGrid = findViewById(R.id.grid_layout)
|
||||
|
||||
updateVideoGrid(userList)
|
||||
updateWaitingView()
|
||||
}
|
||||
|
||||
private fun updateVideoGrid(userList: Set<UserState.User>) {
|
||||
for ((index, user) in userList.withIndex()) {
|
||||
if (CallManager.instance.viewState.showLargeViewUserId.get() == user.id) {
|
||||
setLargeViewIndex(index)
|
||||
}
|
||||
setRender(user)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setRender(user: UserState.User) {
|
||||
if (user.callStatus.get() == Status.None) {
|
||||
val videoView = VideoFactory.instance.findVideoView(user.id)
|
||||
videoView?.setOnClickListener(null)
|
||||
VideoFactory.instance.removeVideoView(user)
|
||||
resetView(videoView)
|
||||
layoutVideoGrid.removeView(videoView)
|
||||
if (CallManager.instance.viewState.showLargeViewUserId.get() == user.id) {
|
||||
TransitionManager.beginDelayedTransition(this)
|
||||
updateShowLargeViewUserId("")
|
||||
setLargeViewIndex(DEFAULT_INDEX)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val videoView = VideoFactory.instance.createVideoView(context, user)
|
||||
resetView(videoView)
|
||||
layoutVideoGrid.addView(videoView)
|
||||
initGestureListener(user)
|
||||
|
||||
if (user.id == CallManager.instance.userState.selfUser.get().id) {
|
||||
val isCameraOpen = CallManager.instance.mediaState.isCameraOpened.get()
|
||||
val isFrontCamera = CallManager.instance.mediaState.isFrontCamera.get()
|
||||
if (isCameraOpen) {
|
||||
CallManager.instance.openCamera(isFrontCamera, videoView, null)
|
||||
updateShowLargeViewUserId(user.id)
|
||||
}
|
||||
} else {
|
||||
CallManager.instance.startRemoteView(user.id, videoView, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateWaitingView() {
|
||||
val selfUser = CallManager.instance.userState.selfUser.get()
|
||||
if (Status.Waiting == selfUser.callStatus.get() && TUICallDefine.Role.Called == selfUser.callRole) {
|
||||
layoutCalleeWaitingView.addView(MultiCallWaitingView(context))
|
||||
layoutCalleeWaitingView.visibility = VISIBLE
|
||||
layoutVideoGrid.visibility = GONE
|
||||
} else {
|
||||
layoutCalleeWaitingView.visibility = GONE
|
||||
layoutVideoGrid.visibility = VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
private fun initGestureListener(user: UserState.User) {
|
||||
val videoView = VideoFactory.instance.findVideoView(user.id)
|
||||
videoView?.setOnClickListener() {
|
||||
post {
|
||||
val index: Int = userList.indexOf(user)
|
||||
val showLargeViewUserId =
|
||||
if (index != layoutVideoGrid.showLargeViewIndex) user.id else ""
|
||||
updateShowLargeViewUserId(showLargeViewUserId)
|
||||
}
|
||||
}
|
||||
|
||||
val detector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
|
||||
override fun onSingleTapUp(e: MotionEvent): Boolean {
|
||||
videoView?.performClick()
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onDown(e: MotionEvent): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onScroll(
|
||||
e1: MotionEvent?,
|
||||
e2: MotionEvent,
|
||||
distanceX: Float,
|
||||
distanceY: Float
|
||||
): Boolean {
|
||||
return true
|
||||
}
|
||||
})
|
||||
videoView?.setOnTouchListener(OnTouchListener { v, event -> detector.onTouchEvent(event) })
|
||||
}
|
||||
|
||||
private fun resetView(view: View?) {
|
||||
if (view?.parent != null) {
|
||||
(view.parent as ViewGroup).removeView(view)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setLargeViewIndex(index: Int) {
|
||||
layoutVideoGrid.showLargeViewIndex = when {
|
||||
index == layoutVideoGrid.showLargeViewIndex -> DEFAULT_INDEX
|
||||
else -> index
|
||||
}
|
||||
layoutVideoGrid.requestLayout()
|
||||
}
|
||||
|
||||
private fun updateShowLargeViewUserId(userId: String?) {
|
||||
CallManager.instance.setGroupLargeViewUserId(userId)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.view.component.videolayout
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.view.GestureDetector
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallDefine
|
||||
import com.tencent.qcloud.tuicore.util.ScreenUtil
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.R
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.common.data.Constants
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.manager.CallManager
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.state.GlobalState
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.state.UserState
|
||||
import com.trtc.tuikit.common.imageloader.ImageLoader
|
||||
import com.trtc.tuikit.common.imageloader.ImageOptions
|
||||
import com.trtc.tuikit.common.livedata.Observer
|
||||
|
||||
class SingleCallVideoLayout(context: Context) : ConstraintLayout(context) {
|
||||
private lateinit var layoutRenderBig: FrameLayout
|
||||
private lateinit var layoutRenderSmall: FrameLayout
|
||||
private lateinit var imageAvatar: ImageView
|
||||
private lateinit var textUserName: TextView
|
||||
private lateinit var imageBackground: ImageView
|
||||
private var videoViewSmall: VideoView? = null
|
||||
private var videoViewBig: VideoView? = null
|
||||
|
||||
private var remoteUser: UserState.User = UserState.User()
|
||||
|
||||
private var remoteUserAvatarObserver = Observer<String> {
|
||||
if (!it.isNullOrEmpty()) {
|
||||
ImageLoader.load(
|
||||
context.applicationContext, imageAvatar, it, R.drawable.tuicallkit_ic_avatar
|
||||
)
|
||||
}
|
||||
}
|
||||
private var remoteUserNicknameObserver = Observer<String> {
|
||||
if (!it.isNullOrEmpty()) {
|
||||
textUserName.text = it
|
||||
}
|
||||
}
|
||||
private var callStatusObserver = Observer<TUICallDefine.Status> {
|
||||
if (CallManager.instance.callState.mediaType.get() == TUICallDefine.MediaType.Audio) {
|
||||
return@Observer
|
||||
}
|
||||
if (it == TUICallDefine.Status.Accept) {
|
||||
updateUserInfoView(false)
|
||||
initSmallVideoView()
|
||||
switchRenderLayout()
|
||||
}
|
||||
}
|
||||
|
||||
private var isVirtualBackgroundOpenedObserver = Observer<Boolean> {
|
||||
if (it == true && CallManager.instance.viewState.reverse1v1CallRenderView) {
|
||||
switchRenderLayout()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
val remoteUserList = CallManager.instance.userState.remoteUserList.get()
|
||||
if (remoteUserList != null && remoteUserList.size > 0) {
|
||||
remoteUser = remoteUserList.first()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
initView()
|
||||
registerObserver()
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
unregisterObserver()
|
||||
layoutRenderBig.removeAllViews()
|
||||
layoutRenderSmall.removeAllViews()
|
||||
}
|
||||
|
||||
private fun registerObserver() {
|
||||
remoteUser.avatar.observe(remoteUserAvatarObserver)
|
||||
remoteUser.nickname.observe(remoteUserNicknameObserver)
|
||||
CallManager.instance.userState.selfUser.get().callStatus.observe(callStatusObserver)
|
||||
CallManager.instance.viewState.isVirtualBackgroundOpened.observe(
|
||||
isVirtualBackgroundOpenedObserver
|
||||
)
|
||||
}
|
||||
|
||||
private fun unregisterObserver() {
|
||||
remoteUser.avatar.removeObserver(remoteUserAvatarObserver)
|
||||
remoteUser.nickname.removeObserver(remoteUserNicknameObserver)
|
||||
CallManager.instance.userState.selfUser.get().callStatus.removeObserver(callStatusObserver)
|
||||
CallManager.instance.viewState.isVirtualBackgroundOpened.removeObserver(
|
||||
isVirtualBackgroundOpenedObserver
|
||||
)
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
LayoutInflater.from(context).inflate(R.layout.tuicallkit_video_layout_single, this)
|
||||
layoutRenderBig = findViewById(R.id.rl_video_big)
|
||||
layoutRenderBig.setOnClickListener {
|
||||
if (CallManager.instance.userState.selfUser.get().callStatus.get() == TUICallDefine.Status.Accept) {
|
||||
CallManager.instance.viewState.isScreenCleaned.set(!CallManager.instance.viewState.isScreenCleaned.get())
|
||||
}
|
||||
}
|
||||
layoutRenderSmall = findViewById(R.id.rl_video_small)
|
||||
layoutRenderSmall.setOnClickListener {
|
||||
switchRenderLayout()
|
||||
}
|
||||
imageAvatar = findViewById(R.id.iv_user_avatar)
|
||||
textUserName = findViewById(R.id.tv_user_name)
|
||||
imageBackground = findViewById(R.id.img_user_background)
|
||||
|
||||
initVideoLayout()
|
||||
setBackground()
|
||||
}
|
||||
|
||||
private fun initVideoLayout() {
|
||||
val mediaType = CallManager.instance.callState.mediaType.get()
|
||||
val callStatus = CallManager.instance.userState.selfUser.get().callStatus.get()
|
||||
|
||||
if (mediaType == TUICallDefine.MediaType.Audio) {
|
||||
updateUserInfoView(true)
|
||||
layoutRenderBig.visibility = GONE
|
||||
layoutRenderSmall.visibility = GONE
|
||||
} else if (mediaType == TUICallDefine.MediaType.Video) {
|
||||
initBigVideoView()
|
||||
initGestureListener(layoutRenderSmall)
|
||||
layoutRenderBig.visibility = VISIBLE
|
||||
|
||||
if (callStatus == TUICallDefine.Status.Accept) {
|
||||
updateUserInfoView(false)
|
||||
layoutRenderSmall.visibility = VISIBLE
|
||||
initSmallVideoView()
|
||||
} else {
|
||||
updateUserInfoView(true)
|
||||
layoutRenderSmall.visibility = GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateUserInfoView(show: Boolean) {
|
||||
val visibility = if (show) VISIBLE else GONE
|
||||
imageAvatar.visibility = visibility
|
||||
textUserName.visibility = visibility
|
||||
if (show) {
|
||||
ImageLoader.load(
|
||||
context, imageAvatar, remoteUser.avatar.get(), R.drawable.tuicallkit_ic_avatar
|
||||
)
|
||||
textUserName.text = remoteUser.nickname.get()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initBigVideoView() {
|
||||
videoViewBig = VideoFactory.instance.createVideoView(
|
||||
context,
|
||||
CallManager.instance.userState.selfUser.get()
|
||||
)
|
||||
resetView(videoViewBig)
|
||||
layoutRenderBig.removeAllViews()
|
||||
layoutRenderBig.addView(videoViewBig)
|
||||
|
||||
val isCameraOpen = CallManager.instance.mediaState.isCameraOpened.get()
|
||||
val isFrontCamera = CallManager.instance.mediaState.isFrontCamera.get()
|
||||
if (isCameraOpen) {
|
||||
CallManager.instance.openCamera(isFrontCamera, videoViewBig, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initSmallVideoView() {
|
||||
updateSmallViewSize()
|
||||
videoViewSmall = VideoFactory.instance.createVideoView(context, remoteUser)
|
||||
resetView(videoViewSmall)
|
||||
layoutRenderSmall.removeAllViews()
|
||||
layoutRenderSmall.addView(videoViewSmall)
|
||||
layoutRenderSmall.visibility = VISIBLE
|
||||
|
||||
CallManager.instance.startRemoteView(remoteUser.id, videoViewSmall, null)
|
||||
}
|
||||
|
||||
private fun switchRenderLayout() {
|
||||
if (CallManager.instance.userState.selfUser.get().callStatus.get() == TUICallDefine.Status.Accept) {
|
||||
resetView(videoViewBig)
|
||||
resetView(videoViewSmall)
|
||||
layoutRenderSmall.removeAllViews()
|
||||
layoutRenderBig.removeAllViews()
|
||||
|
||||
updateSmallViewSize()
|
||||
val isViewReversed = CallManager.instance.viewState.reverse1v1CallRenderView
|
||||
if (isViewReversed) {
|
||||
layoutRenderSmall.addView(videoViewSmall)
|
||||
layoutRenderBig.addView(videoViewBig)
|
||||
} else {
|
||||
layoutRenderSmall.addView(videoViewBig)
|
||||
layoutRenderBig.addView(videoViewSmall)
|
||||
}
|
||||
CallManager.instance.reverse1v1CallRenderView(!isViewReversed)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setBackground() {
|
||||
if (CallManager.instance.callState.mediaType.get() == TUICallDefine.MediaType.Audio) {
|
||||
imageBackground.visibility = VISIBLE
|
||||
val option = ImageOptions.Builder().setPlaceImage(R.drawable.tuicallkit_ic_avatar)
|
||||
.setBlurEffect(80f).build()
|
||||
ImageLoader.load(context, imageBackground, remoteUser.avatar.get(), option)
|
||||
imageBackground.setColorFilter(
|
||||
ContextCompat.getColor(
|
||||
context,
|
||||
R.color.tuicallkit_color_blur_mask
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun resetView(view: View?) {
|
||||
if (view?.parent != null) {
|
||||
(view.parent as ViewGroup).removeView(view)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration?) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
layoutRenderSmall.requestLayout()
|
||||
}
|
||||
|
||||
private fun updateSmallViewSize() {
|
||||
val isLandScape = when (GlobalState.instance.orientation) {
|
||||
Constants.Orientation.Portrait -> false
|
||||
Constants.Orientation.LandScape -> true
|
||||
else -> ScreenUtil.getRealScreenWidth(context) > ScreenUtil.getRealScreenHeight(context)
|
||||
}
|
||||
|
||||
val wWidth =
|
||||
context.resources.getDimension(R.dimen.tuicallkit_video_small_view_width).toInt()
|
||||
val hHeight =
|
||||
context.resources.getDimension(R.dimen.tuicallkit_video_small_view_height).toInt()
|
||||
|
||||
val lp = layoutRenderSmall.layoutParams
|
||||
lp?.width = if (isLandScape) hHeight else wWidth
|
||||
lp?.height = if (isLandScape) wWidth else hHeight
|
||||
layoutRenderSmall.layoutParams = lp
|
||||
}
|
||||
|
||||
private fun initGestureListener(view: FrameLayout) {
|
||||
val detector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
|
||||
override fun onSingleTapUp(e: MotionEvent): Boolean {
|
||||
view.performClick()
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onDown(e: MotionEvent): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onScroll(
|
||||
e1: MotionEvent?,
|
||||
e2: MotionEvent,
|
||||
distanceX: Float,
|
||||
distanceY: Float
|
||||
): Boolean {
|
||||
val offsetX = if (isRTL) (e2.x - (e1?.x ?: 0f)) else ((e1?.x ?: 0f) - e2.x)
|
||||
val layoutParams = view.layoutParams as ConstraintLayout.LayoutParams
|
||||
val newX = (layoutParams.marginEnd + offsetX).toInt()
|
||||
val newY = (layoutParams.topMargin + (e2.y - (e1?.y ?: 0f))).toInt()
|
||||
if (newX >= 0 && newX <= width - view.width && newY >= 0 && newY <= height - view.height) {
|
||||
layoutParams.marginEnd = newX
|
||||
layoutParams.topMargin = newY
|
||||
view.layoutParams = layoutParams
|
||||
}
|
||||
return true
|
||||
}
|
||||
})
|
||||
view.setOnTouchListener { v, event -> detector.onTouchEvent(event) }
|
||||
}
|
||||
|
||||
private val isRTL: Boolean =
|
||||
context.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.view.component.videolayout
|
||||
|
||||
import android.content.Context
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.common.data.Logger
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.state.UserState
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class VideoFactory {
|
||||
private var videoEntityList: ConcurrentHashMap<String, UserVideoEntity> = ConcurrentHashMap()
|
||||
|
||||
fun createVideoView(context: Context, user: UserState.User?): VideoView? {
|
||||
if (null == user || user.id.isEmpty()) {
|
||||
Logger.e(TAG, "createVideoView failed, user is invalid: $user")
|
||||
return null
|
||||
}
|
||||
var videoView = findVideoView(user.id)
|
||||
if (null != videoView) {
|
||||
return videoView
|
||||
}
|
||||
videoView = VideoView(context.applicationContext, user)
|
||||
val entity = UserVideoEntity(user.id, videoView, user)
|
||||
videoEntityList[user.id] = entity
|
||||
return videoEntityList[user.id]?.videoView
|
||||
}
|
||||
|
||||
fun findVideoView(userId: String): VideoView? {
|
||||
if (videoEntityList.containsKey(userId)) {
|
||||
return videoEntityList[userId]?.videoView
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun removeVideoView(user: UserState.User) {
|
||||
videoEntityList[user.id]?.videoView?.removeAllViews()
|
||||
videoEntityList.remove(user.id)
|
||||
}
|
||||
|
||||
fun clearVideoView() {
|
||||
if (videoEntityList.isEmpty()) {
|
||||
return
|
||||
}
|
||||
videoEntityList.forEach {
|
||||
it.value.videoView.removeAllViews()
|
||||
}
|
||||
videoEntityList.clear()
|
||||
}
|
||||
|
||||
class UserVideoEntity(id: String, view: VideoView, user: UserState.User) {
|
||||
var userId: String = id
|
||||
var videoView: VideoView = view
|
||||
var user: UserState.User = user
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "VideoFactory"
|
||||
val instance: VideoFactory = VideoFactory()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.view.component.videolayout
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.utils.widget.ImageFilterView
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallDefine
|
||||
import com.tencent.cloud.tuikit.engine.common.TUICommonDefine
|
||||
import com.tencent.cloud.tuikit.engine.common.TUIVideoView
|
||||
import com.tencent.qcloud.tuicore.TUILogin
|
||||
import com.tencent.qcloud.tuicore.util.ScreenUtil
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.R
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.common.data.Constants
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.manager.CallManager
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.state.GlobalState
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.state.UserState
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.state.ViewState
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.view.component.videolayout.multicall.MultiCallLoadingView
|
||||
import com.trtc.tuikit.common.imageloader.ImageLoader
|
||||
import com.trtc.tuikit.common.imageloader.ImageOptions
|
||||
import com.trtc.tuikit.common.livedata.Observer
|
||||
|
||||
class VideoView(context: Context, userInfo: UserState.User) : RelativeLayout(context) {
|
||||
private val context = context.applicationContext
|
||||
private var user: UserState.User = userInfo
|
||||
private var isShowFloatWindow: Boolean = false
|
||||
|
||||
private lateinit var videoView: TUIVideoView
|
||||
private lateinit var imageAvatar: ImageFilterView
|
||||
private lateinit var buttonSwitchCamera: ImageView
|
||||
private lateinit var buttonBlur: ImageView
|
||||
private lateinit var imageNetworkBad: ImageView
|
||||
private lateinit var textUserName: TextView
|
||||
private lateinit var imageAudioInput: ImageView
|
||||
private lateinit var imageBackground: ImageView
|
||||
private lateinit var imageLoading: MultiCallLoadingView
|
||||
|
||||
private var videoAvailableObserver = Observer<Boolean> {
|
||||
updateVideoView()
|
||||
updateBackground()
|
||||
updateUserAvatarView()
|
||||
updateUserNameView()
|
||||
updateSwitchCameraButton()
|
||||
updateBlurButton()
|
||||
}
|
||||
|
||||
private var audioAvailableObserver = Observer<Boolean> {
|
||||
updateAudioInputIcon()
|
||||
}
|
||||
|
||||
private var playoutVolumeObserver = Observer<Int> {
|
||||
updateAudioInputIcon()
|
||||
}
|
||||
|
||||
private var callStatusObserver = Observer<TUICallDefine.Status> {
|
||||
if (it == TUICallDefine.Status.None) {
|
||||
unregisterObserver()
|
||||
return@Observer
|
||||
}
|
||||
updateImageLoadingView()
|
||||
updateUserAvatarView()
|
||||
}
|
||||
|
||||
private var avatarObserver = Observer<String> {
|
||||
updateUserAvatarView()
|
||||
updateBackground()
|
||||
}
|
||||
|
||||
private var nicknameObserver = Observer<String> {
|
||||
textUserName.text = it
|
||||
}
|
||||
|
||||
private var showLargeViewUserIdObserver = Observer<String> {
|
||||
updateUserNameView()
|
||||
updateSwitchCameraButton()
|
||||
updateBlurButton()
|
||||
}
|
||||
|
||||
private var networkQualityObserver = Observer<Boolean> {
|
||||
updateNetworkHint()
|
||||
}
|
||||
|
||||
private val viewRouterObserver = Observer<ViewState.ViewRouter> {
|
||||
isShowFloatWindow = it == ViewState.ViewRouter.FloatView
|
||||
updateNetworkHint()
|
||||
updateAudioInputIcon()
|
||||
updateSwitchCameraButton()
|
||||
updateBlurButton()
|
||||
updateUserNameView()
|
||||
}
|
||||
|
||||
init {
|
||||
initView()
|
||||
registerObserver()
|
||||
}
|
||||
|
||||
private fun registerObserver() {
|
||||
user.videoAvailable.observe(videoAvailableObserver)
|
||||
user.avatar.observe(avatarObserver)
|
||||
user.nickname.observe(nicknameObserver)
|
||||
user.callStatus.observe(callStatusObserver)
|
||||
user.audioAvailable.observe(audioAvailableObserver)
|
||||
user.playoutVolume.observe(playoutVolumeObserver)
|
||||
user.networkQualityReminder.observe(networkQualityObserver)
|
||||
CallManager.instance.viewState.showLargeViewUserId.observe(showLargeViewUserIdObserver)
|
||||
CallManager.instance.viewState.router.observe(viewRouterObserver)
|
||||
}
|
||||
|
||||
private fun unregisterObserver() {
|
||||
user.videoAvailable.removeObserver(videoAvailableObserver)
|
||||
user.avatar.removeObserver(avatarObserver)
|
||||
user.nickname.removeObserver(nicknameObserver)
|
||||
user.callStatus.removeObserver(callStatusObserver)
|
||||
user.audioAvailable.removeObserver(audioAvailableObserver)
|
||||
user.playoutVolume.removeObserver(playoutVolumeObserver)
|
||||
user.networkQualityReminder.removeObserver(networkQualityObserver)
|
||||
CallManager.instance.viewState.showLargeViewUserId.removeObserver(showLargeViewUserIdObserver)
|
||||
CallManager.instance.viewState.router.removeObserver(viewRouterObserver)
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
LayoutInflater.from(context).inflate(R.layout.tuicallkit_video_view, this, true)
|
||||
videoView = findViewById(R.id.tx_cloud_view)
|
||||
imageAvatar = findViewById(R.id.img_head)
|
||||
textUserName = findViewById(R.id.tv_name)
|
||||
imageAudioInput = findViewById(R.id.iv_audio_input)
|
||||
imageBackground = findViewById(R.id.img_video_background)
|
||||
imageNetworkBad = findViewById(R.id.iv_network)
|
||||
imageLoading = findViewById(R.id.img_loading)
|
||||
buttonSwitchCamera = findViewById(R.id.iv_switch_camera)
|
||||
buttonSwitchCamera.setOnClickListener {
|
||||
var camera = TUICommonDefine.Camera.Back
|
||||
if (CallManager.instance.mediaState.isFrontCamera.get() == TUICommonDefine.Camera.Back) {
|
||||
camera = TUICommonDefine.Camera.Front
|
||||
}
|
||||
CallManager.instance.switchCamera(camera)
|
||||
}
|
||||
buttonBlur = findViewById(R.id.iv_blur)
|
||||
buttonBlur.setOnClickListener {
|
||||
CallManager.instance.setBlurBackground(!CallManager.instance.viewState.isVirtualBackgroundOpened.get())
|
||||
}
|
||||
|
||||
updateVideoView()
|
||||
updateImageLoadingView()
|
||||
updateUserAvatarView()
|
||||
updateUserNameView()
|
||||
updateBackground()
|
||||
}
|
||||
|
||||
fun getVideoView(): TUIVideoView {
|
||||
return videoView
|
||||
}
|
||||
|
||||
private fun updateVideoView() {
|
||||
if (user.videoAvailable.get()) {
|
||||
CallManager.instance.startRemoteView(user.id, this, null)
|
||||
videoView.visibility = View.VISIBLE
|
||||
} else {
|
||||
videoView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateImageLoadingView() {
|
||||
if (TUICallDefine.Scene.GROUP_CALL == CallManager.instance.callState.scene.get()
|
||||
&& TUICallDefine.Status.Waiting == user.callStatus.get() && user.id != TUILogin.getLoginUser()) {
|
||||
imageLoading.visibility = View.VISIBLE
|
||||
imageLoading.startLoading()
|
||||
} else {
|
||||
imageLoading.visibility = View.GONE
|
||||
imageLoading.stopLoading()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateAudioInputIcon() {
|
||||
if (isShowFloatWindow || CallManager.instance.callState.scene.get() == TUICallDefine.Scene.SINGLE_CALL) {
|
||||
imageAudioInput.visibility = View.GONE
|
||||
return
|
||||
}
|
||||
|
||||
when {
|
||||
!user.audioAvailable.get() && user.id == CallManager.instance.userState.selfUser.get().id -> {
|
||||
imageAudioInput.setImageResource(R.drawable.tuicallkit_ic_self_mute)
|
||||
imageAudioInput.visibility = View.VISIBLE
|
||||
}
|
||||
user.playoutVolume.get() > Constants.MIN_AUDIO_VOLUME -> {
|
||||
imageAudioInput.setImageResource(R.drawable.tuicallkit_ic_audio_input)
|
||||
imageAudioInput.visibility = View.VISIBLE
|
||||
}
|
||||
else -> imageAudioInput.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateNetworkHint() {
|
||||
if (isShowFloatWindow || CallManager.instance.callState.scene.get() == TUICallDefine.Scene.SINGLE_CALL) {
|
||||
imageNetworkBad.visibility = View.GONE
|
||||
return
|
||||
}
|
||||
when {
|
||||
user.networkQualityReminder.get() -> imageNetworkBad.visibility = View.VISIBLE
|
||||
else -> imageNetworkBad.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSwitchCameraButton() {
|
||||
if (isShowFloatWindow || CallManager.instance.callState.scene.get() == TUICallDefine.Scene.SINGLE_CALL) {
|
||||
buttonSwitchCamera.visibility = View.GONE
|
||||
return
|
||||
}
|
||||
val selfUser = CallManager.instance.userState.selfUser.get()
|
||||
val largeUserId = CallManager.instance.viewState.showLargeViewUserId.get()
|
||||
|
||||
if (user.videoAvailable.get() && user.id == selfUser.id && user.id == largeUserId) {
|
||||
buttonSwitchCamera.visibility = View.VISIBLE
|
||||
} else {
|
||||
buttonSwitchCamera.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateBlurButton() {
|
||||
if (!GlobalState.instance.enableVirtualBackground) {
|
||||
buttonBlur.visibility = View.GONE
|
||||
return
|
||||
}
|
||||
if (isShowFloatWindow || CallManager.instance.callState.scene.get() == TUICallDefine.Scene.SINGLE_CALL) {
|
||||
buttonBlur.visibility = View.GONE
|
||||
return
|
||||
}
|
||||
val selfUser = CallManager.instance.userState.selfUser.get()
|
||||
val largeUserId = CallManager.instance.viewState.showLargeViewUserId.get()
|
||||
|
||||
if (user.videoAvailable.get() && user.id == selfUser.id && user.id == largeUserId) {
|
||||
buttonBlur.visibility = View.VISIBLE
|
||||
} else {
|
||||
buttonBlur.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateUserAvatarView() {
|
||||
if (user.videoAvailable.get()) {
|
||||
imageAvatar.visibility = View.GONE
|
||||
return
|
||||
}
|
||||
|
||||
val selfUser = CallManager.instance.userState.selfUser.get()
|
||||
if (user.id == selfUser.id && selfUser.callStatus.get() == TUICallDefine.Status.Waiting
|
||||
&& CallManager.instance.callState.scene.get() == TUICallDefine.Scene.SINGLE_CALL
|
||||
) {
|
||||
imageAvatar.visibility = View.GONE
|
||||
return
|
||||
}
|
||||
|
||||
val layoutParams = imageAvatar.layoutParams
|
||||
if (TUICallDefine.Scene.GROUP_CALL == CallManager.instance.callState.scene.get()) {
|
||||
layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT
|
||||
layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
|
||||
imageAvatar.round = 0f
|
||||
} else {
|
||||
layoutParams.width = ScreenUtil.dip2px(80f)
|
||||
layoutParams.height = ScreenUtil.dip2px(80f)
|
||||
imageAvatar.round = 12f
|
||||
}
|
||||
imageAvatar.layoutParams = layoutParams
|
||||
ImageLoader.load(context, imageAvatar, user.avatar.get(), R.drawable.tuicallkit_ic_avatar)
|
||||
imageAvatar.visibility = VISIBLE
|
||||
}
|
||||
|
||||
private fun updateUserNameView() {
|
||||
if (isShowFloatWindow || CallManager.instance.callState.scene.get() == TUICallDefine.Scene.SINGLE_CALL) {
|
||||
textUserName.visibility = View.GONE
|
||||
return
|
||||
}
|
||||
val shouldShowUserId = CallManager.instance.viewState.showLargeViewUserId.get() == user.id
|
||||
|
||||
textUserName.visibility = if (shouldShowUserId) View.VISIBLE else View.GONE
|
||||
textUserName.text = user.nickname.get()
|
||||
}
|
||||
|
||||
private fun updateBackground() {
|
||||
if (user.videoAvailable.get() || CallManager.instance.callState.scene.get() == TUICallDefine.Scene.GROUP_CALL) {
|
||||
imageBackground.visibility = View.GONE
|
||||
return
|
||||
}
|
||||
|
||||
imageBackground.visibility = View.VISIBLE
|
||||
val option = ImageOptions.Builder().setPlaceImage(R.drawable.tuicallkit_ic_avatar).setBlurEffect(80f).build()
|
||||
ImageLoader.load(context, imageBackground, user.avatar.get(), option)
|
||||
imageBackground.setColorFilter(ContextCompat.getColor(context, R.color.tuicallkit_color_blur_mask))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.view.component.videolayout.multicall
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import com.tencent.qcloud.tuicore.util.ScreenUtil
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.common.data.Constants
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.state.GlobalState
|
||||
|
||||
open class MultiCallGridLayout(context: Context, attrs: AttributeSet?) : ConstraintLayout(context, attrs) {
|
||||
var showLargeViewIndex = DEFAULT_INDEX
|
||||
private var measureWidth: Int = 0
|
||||
private var screenWidth: Int = 0
|
||||
private var screenHeight: Int = 0
|
||||
private val changeList = ArrayList<View>()
|
||||
private var startMargin: Int = 0
|
||||
|
||||
init {
|
||||
setViewWidth()
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration?) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
setViewWidth()
|
||||
requestLayout()
|
||||
}
|
||||
|
||||
private fun setViewWidth() {
|
||||
screenWidth = ScreenUtil.getRealScreenWidth(context)
|
||||
screenHeight = ScreenUtil.getRealScreenHeight(context)
|
||||
measureWidth = screenWidth
|
||||
startMargin = 0
|
||||
|
||||
val isLandScape = when (GlobalState.instance.orientation) {
|
||||
Constants.Orientation.Portrait -> false
|
||||
Constants.Orientation.LandScape -> true
|
||||
else -> ScreenUtil.getRealScreenWidth(context) > ScreenUtil.getRealScreenHeight(context)
|
||||
}
|
||||
|
||||
if (isLandScape) {
|
||||
// Set grid's actual width to 0.6 times the minimum value of width and height in landscape mode
|
||||
measureWidth = (screenHeight * 0.6).toInt()
|
||||
startMargin = (screenWidth - measureWidth) / 2
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val width = MeasureSpec.makeMeasureSpec(screenWidth, MeasureSpec.EXACTLY)
|
||||
var height = MeasureSpec.makeMeasureSpec(measureWidth + measureWidth / 3, MeasureSpec.EXACTLY)
|
||||
|
||||
if (showLargeViewIndex < 0) {
|
||||
height = if (childCount <= 2) {
|
||||
MeasureSpec.makeMeasureSpec(measureWidth / 2 + getTopMargin(childCount), MeasureSpec.EXACTLY)
|
||||
} else {
|
||||
MeasureSpec.makeMeasureSpec(measureWidth, MeasureSpec.EXACTLY)
|
||||
}
|
||||
}
|
||||
setMeasuredDimension(width, height)
|
||||
|
||||
for (i in 0 until childCount) {
|
||||
val child: View = getChildAt(i)
|
||||
val childSize = MeasureSpec.makeMeasureSpec(getMeasureSize(i, childCount), MeasureSpec.EXACTLY)
|
||||
measureChild(child, childSize, childSize)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
|
||||
val params = getLocation(childCount)
|
||||
|
||||
if (showLargeViewIndex > 0 && childCount <= 4) {
|
||||
for (i in 0 until childCount) {
|
||||
val child = getChildAt(i)
|
||||
if (i != showLargeViewIndex) {
|
||||
changeList.add(child)
|
||||
} else {
|
||||
changeList.add(0, child)
|
||||
}
|
||||
}
|
||||
|
||||
for (i in 0 until changeList.size) {
|
||||
val view = changeList[i]
|
||||
val pos = params[i]
|
||||
view.layout(pos.x, pos.y, view.measuredWidth + pos.x, view.measuredHeight + pos.y)
|
||||
}
|
||||
changeList.clear()
|
||||
return
|
||||
}
|
||||
|
||||
val topMargin = getTopMargin(childCount)
|
||||
|
||||
for (i in 0 until childCount) {
|
||||
val child = getChildAt(i)
|
||||
val pos = params[i]
|
||||
child.layout(
|
||||
pos.x, pos.y + topMargin,
|
||||
child.measuredWidth + pos.x, child.measuredHeight + pos.y + topMargin
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMeasureSize(index: Int, count: Int): Int {
|
||||
return when {
|
||||
count <= 4 && showLargeViewIndex == index -> measureWidth
|
||||
count <= 4 && showLargeViewIndex < 0 -> measureWidth / 2
|
||||
count > 4 && showLargeViewIndex == index -> measureWidth / 3 * 2
|
||||
else -> measureWidth / 3
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTopMargin(count: Int): Int {
|
||||
return if (count <= 2 && showLargeViewIndex < 0 && screenWidth < screenHeight) {
|
||||
measureWidth / 4
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLocation(count: Int): List<Position> {
|
||||
val width = measureWidth / 3
|
||||
val height = measureWidth / 3
|
||||
|
||||
var currentIndex = 0
|
||||
var lastFrame = 0
|
||||
var segment: SegmentStyle = getSegment(count, currentIndex)
|
||||
|
||||
val list = ArrayList<Position>()
|
||||
while (currentIndex < count) {
|
||||
when (segment) {
|
||||
SegmentStyle.FULL_WIDTH -> {
|
||||
list.add(fullWidth(startMargin, lastFrame, width, height))
|
||||
lastFrame += height * 3
|
||||
currentIndex += 1
|
||||
}
|
||||
SegmentStyle.FIFTY_FIFTY -> {
|
||||
list.addAll(fiftyFifty(startMargin, lastFrame, width, height))
|
||||
lastFrame += height * 3
|
||||
currentIndex += 4
|
||||
}
|
||||
SegmentStyle.THREE_ONE_THIRDS -> {
|
||||
list.addAll(threeOneThird(startMargin, lastFrame, width, height))
|
||||
lastFrame += height
|
||||
currentIndex += 3
|
||||
}
|
||||
SegmentStyle.TWO_THIRDS_ONE_THIRD_CENTER -> {
|
||||
list.addAll(twoThirdsOneThirdCenter(startMargin, lastFrame, width, height))
|
||||
lastFrame += height * 2
|
||||
currentIndex += 3
|
||||
}
|
||||
SegmentStyle.TWO_THIRDS_ONE_THIRD_RIGHT -> {
|
||||
list.addAll(twoThirdsOneThirdRight(startMargin, lastFrame, width, height))
|
||||
lastFrame += height * 2
|
||||
currentIndex += 3
|
||||
}
|
||||
SegmentStyle.ONE_THIRD_TWO_THIRDS -> {
|
||||
list.addAll(oneThirdTwoThirds(startMargin, lastFrame, width, height))
|
||||
lastFrame += height * 2
|
||||
currentIndex += 3
|
||||
}
|
||||
SegmentStyle.ONE_THIRD -> {
|
||||
list.addAll(oneThird(startMargin, lastFrame, width, height))
|
||||
lastFrame += height * 3
|
||||
currentIndex += 3
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
segment = getSegment(count, currentIndex)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
private fun oneThirdTwoThirds(x: Int, y: Int, width: Int, height: Int): List<Position> {
|
||||
val list = ArrayList<Position>()
|
||||
list.add(Position(x, y, width * 2, height * 2))
|
||||
list.add(Position(x + width * 2, y, width, height))
|
||||
list.add(Position(x + width * 2, y + height, width, height))
|
||||
return list
|
||||
}
|
||||
|
||||
private fun threeOneThird(x: Int, y: Int, width: Int, height: Int): List<Position> {
|
||||
val list = ArrayList<Position>()
|
||||
list.add(Position(x, y, width, height))
|
||||
list.add(Position(x + width, y, width, height))
|
||||
list.add(Position(x + width * 2, y, width, height))
|
||||
return list
|
||||
}
|
||||
|
||||
private fun fullWidth(x: Int, y: Int, width: Int, height: Int): Position = Position(x, y, width * 3, height * 3)
|
||||
|
||||
private fun twoThirdsOneThirdCenter(x: Int, y: Int, width: Int, height: Int): List<Position> {
|
||||
val list = ArrayList<Position>()
|
||||
list.add(Position(x, y, width, height))
|
||||
list.add(Position(x + width, y, width * 2, height * 2))
|
||||
list.add(Position(x, y + height, width, height))
|
||||
return list
|
||||
}
|
||||
|
||||
private fun twoThirdsOneThirdRight(x: Int, y: Int, width: Int, height: Int): List<Position> {
|
||||
val list = ArrayList<Position>()
|
||||
list.add(Position(x, y, width, height))
|
||||
list.add(Position(x, y + height, width, height))
|
||||
list.add(Position(x + width, y, width * 2, height * 2))
|
||||
return list
|
||||
}
|
||||
|
||||
private fun fiftyFifty(x: Int, y: Int, childWidth: Int, childHeight: Int): List<Position> {
|
||||
val width = childWidth * 3 / 2
|
||||
val height = childHeight * 3 / 2
|
||||
|
||||
val list = ArrayList<Position>()
|
||||
list.add(Position(x, y, width, height))
|
||||
list.add(Position(x + width, y, width, height))
|
||||
list.add(Position(x, y + height, width, height))
|
||||
list.add(Position(x + width, y + height, width, height))
|
||||
return list
|
||||
}
|
||||
|
||||
private fun oneThird(x: Int, y: Int, childWidth: Int, childHeight: Int): List<Position> {
|
||||
val width = childWidth * 3 / 2
|
||||
val height = childHeight * 3 / 2
|
||||
|
||||
val list = ArrayList<Position>()
|
||||
list.add(Position(x, y, width, height))
|
||||
list.add(Position(x + width, y, width, height))
|
||||
list.add(Position(x + width / 2, y + height, width, height))
|
||||
return list
|
||||
}
|
||||
|
||||
private fun getSegment(count: Int, currentIndex: Int): SegmentStyle {
|
||||
var segment = SegmentStyle.THREE_ONE_THIRDS
|
||||
if (currentIndex == 0) {
|
||||
when {
|
||||
count == 1 -> segment = SegmentStyle.FULL_WIDTH
|
||||
count == 2 || count == 4 -> segment =
|
||||
if (showLargeViewIndex >= 0) SegmentStyle.FULL_WIDTH else SegmentStyle.FIFTY_FIFTY
|
||||
count == 3 -> segment =
|
||||
if (showLargeViewIndex >= 0) SegmentStyle.FULL_WIDTH else SegmentStyle.ONE_THIRD
|
||||
showLargeViewIndex == 0 -> segment = SegmentStyle.ONE_THIRD_TWO_THIRDS
|
||||
showLargeViewIndex == 1 -> segment = SegmentStyle.TWO_THIRDS_ONE_THIRD_CENTER
|
||||
showLargeViewIndex == 2 -> segment = SegmentStyle.TWO_THIRDS_ONE_THIRD_RIGHT
|
||||
}
|
||||
return segment
|
||||
}
|
||||
when (count - currentIndex) {
|
||||
1 -> segment = when {
|
||||
count == 3 -> SegmentStyle.ONE_THIRD
|
||||
count > 4 && showLargeViewIndex == count - 1 -> SegmentStyle.ONE_THIRD_TWO_THIRDS
|
||||
count == 4 -> SegmentStyle.FIFTY_FIFTY
|
||||
count > 4 && showLargeViewIndex == currentIndex -> SegmentStyle.ONE_THIRD_TWO_THIRDS
|
||||
count > 4 && showLargeViewIndex == currentIndex + 1 -> SegmentStyle.TWO_THIRDS_ONE_THIRD_CENTER
|
||||
count > 4 && showLargeViewIndex == currentIndex + 2 -> SegmentStyle.TWO_THIRDS_ONE_THIRD_RIGHT
|
||||
else -> SegmentStyle.THREE_ONE_THIRDS
|
||||
}
|
||||
2 -> segment = when {
|
||||
count == 4 -> SegmentStyle.FIFTY_FIFTY
|
||||
count > 4 && showLargeViewIndex == currentIndex -> SegmentStyle.ONE_THIRD_TWO_THIRDS
|
||||
count > 4 && showLargeViewIndex == currentIndex + 1 -> SegmentStyle.TWO_THIRDS_ONE_THIRD_CENTER
|
||||
count > 4 && showLargeViewIndex == currentIndex + 2 -> SegmentStyle.TWO_THIRDS_ONE_THIRD_RIGHT
|
||||
else -> SegmentStyle.THREE_ONE_THIRDS
|
||||
}
|
||||
else -> segment =
|
||||
when {
|
||||
count > 4 && showLargeViewIndex == currentIndex -> SegmentStyle.ONE_THIRD_TWO_THIRDS
|
||||
count > 4 && showLargeViewIndex == currentIndex + 1 -> SegmentStyle.TWO_THIRDS_ONE_THIRD_CENTER
|
||||
count > 4 && showLargeViewIndex == currentIndex + 2 -> SegmentStyle.TWO_THIRDS_ONE_THIRD_RIGHT
|
||||
else -> SegmentStyle.THREE_ONE_THIRDS
|
||||
}
|
||||
}
|
||||
return segment
|
||||
}
|
||||
|
||||
class Position(xx: Int, yy: Int, wWidth: Int, hHeight: Int) {
|
||||
var x = 0
|
||||
var y = 0
|
||||
var width = 0
|
||||
var height = 0
|
||||
|
||||
init {
|
||||
x = xx
|
||||
y = yy
|
||||
width = wWidth
|
||||
height = hHeight
|
||||
}
|
||||
}
|
||||
|
||||
enum class SegmentStyle {
|
||||
FULL_WIDTH,
|
||||
ONE_THIRD,
|
||||
FIFTY_FIFTY,
|
||||
THREE_ONE_THIRDS,
|
||||
ONE_THIRD_TWO_THIRDS,
|
||||
TWO_THIRDS_ONE_THIRD_CENTER,
|
||||
TWO_THIRDS_ONE_THIRD_RIGHT
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val DEFAULT_INDEX: Int = -99
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.view.component.videolayout.multicall
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.AlphaAnimation
|
||||
import android.view.animation.Animation
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import androidx.constraintlayout.utils.widget.ImageFilterView
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.R
|
||||
|
||||
class MultiCallLoadingView(context: Context, attrs: AttributeSet?) : LinearLayout(context, attrs) {
|
||||
private var leftDot: ImageView
|
||||
private var centerDot: ImageView
|
||||
private var rightDot: ImageView
|
||||
|
||||
private var isLoading = false
|
||||
|
||||
init {
|
||||
this.orientation = HORIZONTAL
|
||||
this.gravity = Gravity.CENTER
|
||||
this.layoutParams = LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
leftDot = createImageView()
|
||||
centerDot = createImageView()
|
||||
rightDot = createImageView()
|
||||
addView(leftDot)
|
||||
addView(centerDot)
|
||||
addView(rightDot)
|
||||
}
|
||||
|
||||
private fun createImageView(): ImageView {
|
||||
val lp = LayoutParams(24, 24)
|
||||
lp.marginEnd = 24
|
||||
val imageView = ImageFilterView(context)
|
||||
imageView.layoutParams = lp
|
||||
imageView.roundPercent = 1f
|
||||
imageView.setBackgroundColor(resources.getColor(R.color.tuicallkit_color_white))
|
||||
return imageView
|
||||
}
|
||||
|
||||
private fun createAnimation(startAlpha: Float, endAlpha: Float): Animation {
|
||||
val animation = AlphaAnimation(startAlpha, endAlpha)
|
||||
animation.duration = DURATION
|
||||
return animation
|
||||
}
|
||||
|
||||
private fun startAnimation1() {
|
||||
startAnimation(leftDot, 1.0f, 0.2f)
|
||||
centerDot.startAnimation(createAnimation(0.6f, 1.0f))
|
||||
rightDot.startAnimation(createAnimation(0.2f, 0.6f))
|
||||
}
|
||||
|
||||
private fun startAnimation2() {
|
||||
leftDot.startAnimation(createAnimation(0.2f, 0.2f))
|
||||
startAnimation(centerDot, 1.0f, 0.6f)
|
||||
rightDot.startAnimation(createAnimation(0.6f, 1.0f))
|
||||
}
|
||||
|
||||
private fun startAnimation3() {
|
||||
leftDot.startAnimation(createAnimation(0.2f, 0.2f))
|
||||
centerDot.startAnimation(createAnimation(0.6f, 0.6f))
|
||||
startAnimation(rightDot, 1.0f, 0.6f)
|
||||
}
|
||||
|
||||
private fun startAnimation4() {
|
||||
startAnimation(leftDot, 0.2f, 1.0f)
|
||||
centerDot.startAnimation(createAnimation(0.6f, 0.6f))
|
||||
rightDot.startAnimation(createAnimation(0.6f, 0.2f))
|
||||
}
|
||||
|
||||
private fun startAnimation(view: View?, fromAlpha: Float, toAlpha: Float) {
|
||||
val animation = createAnimation(fromAlpha, toAlpha)
|
||||
animation.setAnimationListener(object : Animation.AnimationListener {
|
||||
override fun onAnimationStart(animation: Animation) {}
|
||||
override fun onAnimationEnd(animation: Animation) {
|
||||
if (!isLoading) {
|
||||
return
|
||||
}
|
||||
when {
|
||||
view === leftDot && fromAlpha > toAlpha -> startAnimation2()
|
||||
view === leftDot && fromAlpha < toAlpha -> startAnimation1()
|
||||
view === centerDot -> startAnimation3()
|
||||
view === rightDot -> startAnimation4()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(animation: Animation) {}
|
||||
})
|
||||
view!!.startAnimation(animation)
|
||||
}
|
||||
|
||||
fun startLoading() {
|
||||
isLoading = true
|
||||
startAnimation1()
|
||||
}
|
||||
|
||||
fun stopLoading() {
|
||||
isLoading = false
|
||||
leftDot.clearAnimation()
|
||||
centerDot.clearAnimation()
|
||||
rightDot.clearAnimation()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DURATION: Long = 500
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package com.tencent.qcloud.tuikit.tuicallkit.view.component.videolayout.multicall
|
||||
|
||||
import android.content.Context
|
||||
import android.view.Gravity
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.utils.widget.ImageFilterView
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.tencent.cloud.tuikit.engine.call.TUICallDefine
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.R
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.manager.CallManager
|
||||
import com.tencent.qcloud.tuikit.tuicallkit.state.UserState
|
||||
import com.trtc.tuikit.common.imageloader.ImageLoader
|
||||
import com.trtc.tuikit.common.livedata.Observer
|
||||
import com.trtc.tuikit.common.util.ScreenUtil
|
||||
|
||||
class MultiCallWaitingView(context: Context) : LinearLayout(context) {
|
||||
private var caller: UserState.User = UserState.User()
|
||||
private val squareWidth = context.resources.getDimensionPixelOffset(R.dimen.tuicallkit_small_image_size)
|
||||
private val defaultMargin = context.resources.getDimensionPixelOffset(R.dimen.tuicallkit_small_image_left_margin)
|
||||
|
||||
private lateinit var textWaitingUserName: TextView
|
||||
private lateinit var imageCallerAvatar: ImageFilterView
|
||||
private lateinit var layoutAvatarList: LinearLayout
|
||||
|
||||
private var remoteUserListObserver = Observer<LinkedHashSet<UserState.User>> {
|
||||
for (user in it) {
|
||||
if (TUICallDefine.Role.Called == user.callRole) {
|
||||
user.avatar.observe(avatarObserver)
|
||||
}
|
||||
}
|
||||
updateAvatarListView()
|
||||
}
|
||||
|
||||
private var selfUserAvatarObserver = Observer<String> {
|
||||
if (!it.isNullOrEmpty()) {
|
||||
ImageLoader.load(context, imageCallerAvatar, caller.avatar.get(), R.drawable.tuicallkit_ic_avatar)
|
||||
textWaitingUserName.text = caller.nickname.get()
|
||||
}
|
||||
}
|
||||
|
||||
private var avatarObserver = Observer<String> {
|
||||
if (!it.isNullOrEmpty()) {
|
||||
updateAvatarListView()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
this.orientation = VERTICAL
|
||||
this.gravity = Gravity.CENTER
|
||||
caller = CallManager.instance.userState.remoteUserList.get()
|
||||
.find { user -> user.callRole == TUICallDefine.Role.Caller } ?: UserState.User()
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
initView()
|
||||
registerObserver()
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
unregisterObserver()
|
||||
}
|
||||
|
||||
private fun registerObserver() {
|
||||
CallManager.instance.userState.remoteUserList.observe(remoteUserListObserver)
|
||||
if (!caller.id.isNullOrEmpty()) {
|
||||
caller.avatar.observe(selfUserAvatarObserver)
|
||||
}
|
||||
for (user in CallManager.instance.userState.remoteUserList.get()) {
|
||||
user.avatar.observe(avatarObserver)
|
||||
}
|
||||
}
|
||||
|
||||
private fun unregisterObserver() {
|
||||
CallManager.instance.userState.remoteUserList.removeObserver(remoteUserListObserver)
|
||||
caller.avatar.removeObserver(selfUserAvatarObserver)
|
||||
for (user in CallManager.instance.userState.remoteUserList.get()) {
|
||||
user.avatar.removeObserver(avatarObserver)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
imageCallerAvatar = createImageView(caller, ScreenUtil.dip2px(100f), 0, 20)
|
||||
textWaitingUserName = createTextView(caller.nickname.get(), 48)
|
||||
textWaitingUserName.textSize = 18f
|
||||
layoutAvatarList = LinearLayout(context)
|
||||
layoutAvatarList.layoutParams =
|
||||
LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
|
||||
addView(imageCallerAvatar)
|
||||
addView(textWaitingUserName)
|
||||
addView(createTextView(context.getString(R.string.tuicallkit_invitee_user_list), 24))
|
||||
addView(layoutAvatarList)
|
||||
}
|
||||
|
||||
private fun updateAvatarListView() {
|
||||
layoutAvatarList.removeAllViews()
|
||||
val list = HashSet<UserState.User>()
|
||||
list.add(CallManager.instance.userState.selfUser.get())
|
||||
list.addAll(CallManager.instance.userState.remoteUserList.get() - caller)
|
||||
|
||||
for (user in list) {
|
||||
layoutAvatarList.addView(createImageView(user, squareWidth, defaultMargin, 0))
|
||||
}
|
||||
}
|
||||
|
||||
private fun createImageView(user: UserState.User, width: Int, start: Int, bottom: Int): ImageFilterView {
|
||||
val imageView = ImageFilterView(context)
|
||||
val layoutParams = LayoutParams(width, width)
|
||||
layoutParams.marginStart = start
|
||||
layoutParams.bottomMargin = bottom
|
||||
imageView.round = 12f
|
||||
imageView.scaleType = ImageView.ScaleType.CENTER_CROP
|
||||
imageView.layoutParams = layoutParams
|
||||
ImageLoader.load(context, imageView, user.avatar.get(), R.drawable.tuicallkit_ic_avatar)
|
||||
return imageView
|
||||
}
|
||||
|
||||
private fun createTextView(text: String, margin: Int): TextView {
|
||||
val textView = TextView(context)
|
||||
textView.text = text
|
||||
textView.textSize = 12f
|
||||
textView.setTextColor(ContextCompat.getColor(context, R.color.tuicallkit_color_white))
|
||||
val param = LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
param.bottomMargin = margin
|
||||
param.gravity = Gravity.CENTER
|
||||
textView.layoutParams = param
|
||||
return textView
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 531 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 389 B |
|
After Width: | Height: | Size: 1006 B |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 321 B |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 319 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 357 B |
|
After Width: | Height: | Size: 378 B |
|
After Width: | Height: | Size: 240 B |
|
After Width: | Height: | Size: 370 B |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 389 B |
|
After Width: | Height: | Size: 227 B |
|
After Width: | Height: | Size: 231 B |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 3.7 KiB |