初始代码

This commit is contained in:
2025-09-03 18:52:07 +08:00
parent 2d21a47e87
commit 6e3e30efa5
3249 changed files with 239147 additions and 0 deletions
@@ -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 SDKs 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 SDKs 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 failjson: $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
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 531 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1006 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Some files were not shown because too many files have changed in this diff Show More