package com.ydl.audioim import android.annotation.SuppressLint import android.content.Context import android.os.Handler import android.text.TextUtils import com.alibaba.android.arouter.launcher.ARouter import com.apm.insight.log.VLog import com.google.gson.Gson import com.ydl.audioim.bean.AgoraInvitationBean import com.ydl.audioim.http.AudioApiRequestUtil import com.ydl.audioim.http.command.ConnectExceptionCommand import com.ydl.audioim.router.AudioImIn import com.ydl.audioim.utils.AudioLogUtils import com.ydl.audioim.utils.AudioLogUtils.Companion.writeAgoraLog import com.ydl.audioim.utils.onConfideEvent import com.ydl.consultantim.ConsultantAudioHomeActivity import com.ydl.ydl_av.messge_service.YDLRTMClient import com.ydl.ydl_av.messge_service.bean.RTMMesssage import com.ydl.ydl_av.messge_service.callback.CallListener import com.ydl.ydl_av.messge_service.callback.CancelCallStatusListener import com.ydl.ydl_av.messge_service.callback.InitListener import com.ydl.ydl_av.messge_service.callback.LoginCallback import com.ydl.ydl_av.messge_service.request.LoginParam import com.ydl.ydl_av.messge_service.response.CallLocalResponse import com.ydl.ydl_av.messge_service.response.CallRemoteResponse import com.ydl.ydlcommon.app.Apm import com.ydl.ydlcommon.modular.ModularServiceManager import com.ydl.ydlcommon.utils.ActivityManager import com.ydl.ydlcommon.utils.LogUtil import com.ydl.ydlcommon.utils.log.AliYunLogConfig import com.ydl.ydlcommon.utils.log.AliYunRichLogsHelper import com.ydl.ydlcommon.utils.log.LogHelper import com.yidianling.im.api.bean.IMSendCustomNotificationResultCallBack import com.yidianling.user.api.event.UserLoginEvent import com.yidianling.user.api.event.UserLogoutEvent import com.yidianling.user.api.service.IUserService import de.greenrobot.event.EventBus import io.agora.rtm.RtmStatusCode import io.agora.rtm.RtmStatusCode.ConnectionChangeReason.CONNECTION_CHANGE_REASON_REMOTE_LOGIN import io.reactivex.Observable import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers import java.util.* import java.util.concurrent.TimeUnit /** * @author harvie * @date 2019/9/27 * 语音通话入口类 */ class YDLavManager { companion object { const val FILE_NAME = "consult.log" private const val TAG = "YDLavManager" //当前sdk的登录状态 var sdkStatus = -1 var isOnlineRtm = true // 账号在多端登录情况下判断RTM否在线,仅仅用在RTM互踢情况下判断是否在线,其它场景慎用 val instances: YDLavManager by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { YDLavManager() } const val AUDIO_NO_AUTH_ERROR_CODE = "97"//音频权限未通过错误码 } fun init(context: Context, appId: String) { YDLRTMClient.instances.init(context, appId, listener) EventBus.getDefault().unregister(this) EventBus.getDefault().register(this) //设置回调 setCallback() val uid = ModularServiceManager.provide(IUserService::class.java).getUserInfo()?.uid if (!TextUtils.isEmpty(uid) && !TextUtils.equals("0", uid)) { //不延时,可能会导致请求api报 network not unablibale Handler().postDelayed({ login(uid) }, 300) } } public fun onEvent(event: UserLoginEvent) { instances.login(event.uid) } public fun onEvent(event: UserLogoutEvent) { instances.logout() } private fun setCallback() { YDLRTMClient.instances.setCallListener(object : CallListener { override fun onCallRecivedByPeer(response: CallLocalResponse?) { //返回给主叫:被叫已收到呼叫邀请 LogUtil.e("[agora]${response?.calleeId}已收到呼叫邀请,频道号${response?.ChannelId}") AliYunRichLogsHelper.getInstance().sendRichLog( AliYunLogConfig.AGORA, "${response?.calleeId}已收到呼叫邀请,频道号${response?.ChannelId}" ) val dimension = hashMapOf("call" to "call_received_by_peer") onConfideEvent(dimension, response?.ChannelId) val act = ActivityManager.getInstance().getTopTaskActivity() if (act is AudioHomeActivity) { act.runOnUiThread { act.playWaitingMusic() act.writeAgoraLog("被叫已收到通话邀请") } } } override fun onCallAccepted(response: CallLocalResponse?, msg: String?) { //返回给主叫 LogUtil.e("[agora]${response?.calleeId}已接收呼叫邀请") AliYunRichLogsHelper.getInstance().sendRichLog( AliYunLogConfig.AGORA, "${response?.calleeId}已接收呼叫邀请" ) //加入声网频道时机修改:主叫收到被叫接受邀请的回调后再加入声网频道 val act = ActivityManager.getInstance().getTopTaskActivity() if (act is AudioHomeActivity) { act.runOnUiThread { act.joinChannel() act.onPeerAccepted() } } } override fun onCallRefused(response: CallLocalResponse?, msg: String?) { //返回给主叫 LogUtil.e("[agora]${response?.calleeId}已拒绝呼叫邀请") AliYunRichLogsHelper.getInstance().sendRichLog( AliYunLogConfig.AGORA, "${response?.calleeId}已拒绝呼叫邀请" ) val dimension = hashMapOf("call" to "call_refused") onConfideEvent(dimension, response?.ChannelId) val act = ActivityManager.getInstance().getTopTaskActivity() if (act is AudioHomeActivity) { act.onCallRefused() } } override fun onCallCanceled(response: CallLocalResponse?) { //返回给主叫 LogUtil.e("[agora]主叫已取消呼叫邀请") AliYunRichLogsHelper.getInstance().sendRichLog( AliYunLogConfig.AGORA, "主叫已取消呼叫邀请" ) val dimension = hashMapOf("call" to "call_canceled") onConfideEvent(dimension, response?.ChannelId) val act = ActivityManager.getInstance().getTopTaskActivity() if (act is AudioHomeActivity) { act.runOnUiThread { act.writeAgoraLog("主叫(用户)呼叫取消:超时或主动取消") act.uploadExceptionStatus("已取消", 1) } } } override fun onCallFailure(response: CallLocalResponse?, errorCode: Int) { //返回给主叫 LogUtil.e("[agora]呼叫${response?.calleeId}用户失败:${response?.response}") AliYunRichLogsHelper.getInstance().sendRichLog( AliYunLogConfig.AGORA, "呼叫${response?.calleeId}用户失败:${response?.response}" ) val dimension = hashMapOf( "call" to "call_fail", "call_fail" to "code${errorCode}" ) onConfideEvent(dimension, response?.ChannelId) val act = ActivityManager.getInstance().getTopTaskActivity() //专家离线或者30 秒后仍未收到专家响应,重新再邀请一次 when (errorCode) { //被叫不在线 呼叫邀请发出 30 秒后被叫仍未 ACK 响应呼叫邀请 RtmStatusCode.LocalInvitationError.LOCAL_INVITATION_ERR_PEER_OFFLINE, RtmStatusCode.LocalInvitationError.LOCAL_INVITATION_ERR_PEER_NO_RESPONSE -> { if (act is AudioHomeActivity) { act.runOnUiThread { act.rtcCall() } } } RtmStatusCode.LocalInvitationError.LOCAL_INVITATION_ERR_INVITATION_EXPIRE -> { //呼叫邀请过期。被叫 ACK 响应呼叫邀请后 60 秒呼叫邀请未被取消、接受、拒绝,则呼叫邀请过期。 } } //呼叫失败日志输出 if (act is AudioHomeActivity) { act.runOnUiThread { act.writeAgoraLog("发送通话邀请失败:${errorCode}") LogHelper.getInstance().uploadLog(false) } } } override fun onRemoteInvitationReceived(response: CallRemoteResponse?) { //返回给被叫 LogUtil.e("[agora]收到来自${response?.callerId}的呼叫邀请") AliYunRichLogsHelper.getInstance().sendRichLog( AliYunLogConfig.AGORA, "收到来自${response?.callerId}的呼叫邀请" ) receivedCall(response?.content, "来自RTM") } override fun onRemoteInvitationAccepted(response: CallRemoteResponse?) { //返回给被叫 LogUtil.e("[agora]接受来自${response?.callerId}的呼叫成功") AliYunRichLogsHelper.getInstance().sendRichLog( AliYunLogConfig.AGORA, "接受来自${response?.callerId}的呼叫成功" ) } override fun onRemoteInvitationRefused(response: CallRemoteResponse?) { //返回给被叫 LogUtil.e("[agora]已拒绝来自${response?.callerId}的呼叫") AliYunRichLogsHelper.getInstance().sendRichLog( AliYunLogConfig.AGORA, "已拒绝来自${response?.callerId}的呼叫" ) val act = ActivityManager.getInstance().getTopTaskActivity() if (act is ConsultantAudioHomeActivity) { act.uploadExceptionStatus("已拒绝", 2) } } override fun onRemoteInvitationCanceled(response: CallRemoteResponse?) { callEndStatusUpdate(response?.ChannelId!!, 1, "主叫取消呼叫") //返回给被叫 LogUtil.e("[agora]主叫${response?.callerId}已取消呼叫邀请") AliYunRichLogsHelper.getInstance().sendRichLog( AliYunLogConfig.AGORA, "主叫${response?.callerId}已取消呼叫邀请" ) writeAgoraLog( "呼叫邀请被取消:主叫(专家)主动取消-------Time:${AudioLogUtils.format.format(Calendar.getInstance().time)}", FILE_NAME ) val act = ActivityManager.getInstance().getTopTaskActivity() if (act is ConsultantAudioHomeActivity) { act.uploadExceptionStatus("对方已取消", 1) } closePage() } override fun onRemoteInvitationFailure(response: CallRemoteResponse?, errorCode: Int) { //返回给被叫 LogUtil.e("[agora]来自主叫${response?.callerId}的呼叫邀请进程失败:${response?.response}") if (errorCode == RtmStatusCode.RemoteInvitationError.REMOTE_INVITATION_ERR_INVITATION_EXPIRE) { //呼叫邀请过期 AliYunRichLogsHelper.getInstance().sendRichLog( AliYunLogConfig.AGORA, "呼叫邀请被取消:用户未接听" ) writeAgoraLog( "呼叫邀请被取消:用户未接听-------Time:${AudioLogUtils.format.format(Calendar.getInstance().time)}", FILE_NAME ) val act = ActivityManager.getInstance().getTopTaskActivity() if (act is ConsultantAudioHomeActivity) { act.uploadExceptionStatus("未接听", 3) } } else { writeAgoraLog( "呼叫邀请被取消:错误原因(${errorCode})", FILE_NAME ) AliYunRichLogsHelper.getInstance().sendRichLog( AliYunLogConfig.AGORA, "呼叫邀请被取消:错误原因(${errorCode})" ) } callEndStatusUpdate(response?.ChannelId!!, 2, "超时未接听导致的取消呼叫") //关闭页面 closePage() } override fun onOtherMsg(error: String?) { LogUtil.e("[agora]其它消息:${error}") if (error.equals("呼叫发送成功")) { writeAgoraLog( "声网发送通话邀请成功-------Time:${AudioLogUtils.format.format(Calendar.getInstance().time)}", "confide.log", true ) AliYunRichLogsHelper.getInstance().sendRichLog( AliYunLogConfig.AGORA, "声网发送通话邀请成功" ) } else { writeAgoraLog( "声网发送通话邀请失败${error}-------Time:${ AudioLogUtils.format.format( Calendar.getInstance().time ) }", "confide.log", true ) LogHelper.getInstance().uploadLog(false) AliYunRichLogsHelper.getInstance().sendRichLog( AliYunLogConfig.AGORA, "声网发送通话邀请失败${error}" ) } } }) } fun rtcCall(listenerUid: String?, channelId: String?, sendDoctocrMsg: String?) { YDLRTMClient.instances.call(listenerUid, channelId, sendDoctocrMsg) sendCustomNotification(listenerUid!!, sendDoctocrMsg!!, "1") } fun acceptCall(toUid: String, channelId: String?, data: String) { YDLRTMClient.instances.acceptCall(channelId) sendCustomNotification(toUid, data, "2") } fun refuseCall(toUid: String, channelId: String?, data: String) { YDLRTMClient.instances.refuseCall(channelId) sendCustomNotification(toUid, data, "4") } fun cancelCall( listenerUid: String, channelId: String, data: String, event: (msg: String?, code: Int) -> Unit ) { YDLRTMClient.instances.cancelCall( listenerUid, channelId, object : CancelCallStatusListener { override fun onFailure(errorMsg: String?, errorCode: Int) { event(errorMsg, errorCode) YDLRTMClient.instances.cancelCall(listenerUid, channelId, null) } override fun onSuccess() { } }) callEndStatusUpdate(channelId, 1, "主叫取消呼叫") sendCustomNotification(listenerUid, data, "3") } private fun sendCustomNotification(toUid: String, data: String, callType: String) { val infoBean = AgoraInvitationBean() infoBean.data = data infoBean.callType = callType Handler().postDelayed({ AudioImIn.sendCustomNotification(toUid, Gson().toJson(infoBean), object : IMSendCustomNotificationResultCallBack { override fun onException(throwable: Throwable) { // writeAgoraLog("云信发送通话邀请异常${throwable.message}-------Time:${AudioLogUtils.format.format(Calendar.getInstance().time)}", "confide.log", true) // LogHelper.getInstance().uploadLog(false) AliYunRichLogsHelper.getInstance().sendRichLog( AliYunLogConfig.YUNXIN, "云信发送通话邀请异常${throwable.message}" ) } override fun onFailed(code: Int) { // writeAgoraLog("云信发送通话邀请失败${code}-------Time:${AudioLogUtils.format.format(Calendar.getInstance().time)}", "confide.log", true) // LogHelper.getInstance().uploadLog(false) AliYunRichLogsHelper.getInstance().sendRichLog( AliYunLogConfig.YUNXIN, "云信发送通话邀请失败${code}" ) } override fun onSuccess() { writeAgoraLog( "云信发送通话邀请成功-------Time:${AudioLogUtils.format.format(Calendar.getInstance().time)}", "confide.log", true ) AliYunRichLogsHelper.getInstance().sendRichLog( AliYunLogConfig.YUNXIN, "云信发送通话邀请成功" ) } }) }, 300) } fun login(userId: String?) { login(userId) { _, _ -> } } @SuppressLint("CheckResult") fun login(userId: String?, event: (isSuccess: Boolean, msg: String?) -> Unit) { if (TextUtils.isEmpty(userId) || userId ?: "0" <= "0") { //如果uid为空或小于等于0 ,则不进行登录,因为uid为0也会登录成功,会导致后面uid正确时无法登录 LogUtil.e("[agora]login-uid:$userId") AliYunRichLogsHelper.getInstance() .sendRichLog(AliYunLogConfig.AGORA, "uid为空或小于等于0 ,则不进行登录 login-uid:$userId") return } isOnlineRtm = true //登录实时消息 //获取token AudioApiRequestUtil.getAgoraToken().subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()).subscribe({ if ("200" == it.code) { LogUtil.e("[agora]登录av的login-uid:$userId") YDLRTMClient.instances.login( LoginParam(userId, it.data.token), object : LoginCallback { override fun onSuccess() { //登陆成功,发起呼叫 LogUtil.e("[agora]实时消息登录成功") AliYunRichLogsHelper.getInstance() .sendRichLog(AliYunLogConfig.AGORA, "声网rtm登录成功,uid:$userId") writeAgoraLog( "声网rtm登录成功,uid:$userId-------Time:${ AudioLogUtils.format.format( Calendar.getInstance().time ) }", "confide.log", true ) event(true, "") } override fun onFailure(msg: String?) { if (msg != "LOGIN_ERR_ALREADY_LOGGED_IN") { Apm.reportCustom("agora_login_error", msg ?: "", Exception(msg)) } LogUtil.e("[agora]实时消息登录失败:$msg") writeAgoraLog( "声网rtm登录失败:$msg-------Time:${ AudioLogUtils.format.format( Calendar.getInstance().time ) }", "confide.log", true ) AliYunRichLogsHelper.getInstance() .sendRichLog(AliYunLogConfig.AGORA, "声网rtm登录失败:$msg") event(false, msg) } }) } else { LogUtil.e("声网token获取失败uid:" + userId + " error:" + it.msg) LogHelper.getInstance() .writeLogSync("声网token获取失败uid:" + userId + " error:" + it.msg) AliYunRichLogsHelper.getInstance().sendRichLog( AliYunLogConfig.AGORA, "声网token获取失败uid" + userId + " error:" + it.msg ) } }, { LogUtil.e("声网token获取异常uid:" + userId + " error:" + it.message) AliYunRichLogsHelper.getInstance().sendRichLog( AliYunLogConfig.AGORA, "声网token获取异常uid:" + userId + " error:" + it.message ) }) } /** * 收到邀请 */ @SuppressLint("CheckResult") fun receivedCall(content: String?, from: String = "") { if (!TextUtils.isEmpty(content)) { //如果已经接听了用户电话 再有电话进来 是不能接听的 if (!activityIsExists(ConsultantAudioHomeActivity::class.java) && !activityIsExists( AudioHomeActivity::class.java ) ) { //延时启动通话界面,防止刚打开就被main遮挡 Observable.timer(1000, TimeUnit.MILLISECONDS).subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()).subscribe { LogUtil.e("[agora]启动通话界面") writeAgoraLog( "收到主叫方通话邀请($from)-------Time:${ AudioLogUtils.format.format( Calendar.getInstance().time ) }", FILE_NAME, false ) AliYunRichLogsHelper.getInstance() .sendRichLog(AliYunLogConfig.AGORA, "收到主叫方通话邀请($from)") LogHelper.getInstance().uploadLog(false) //邀请加入频道消息,跳转通话界面 ARouter.getInstance().build("/av/ConsultantAudioHomeActivity") .withString("param", content).navigation() } } else { LogUtil.d("[agora]收到声网邀请,但界面实例已存在") writeAgoraLog( "收到主叫方通话邀请,但界面实例已存在($from)-------Time:${ AudioLogUtils.format.format( Calendar.getInstance().time ) }", FILE_NAME, false ) AliYunRichLogsHelper.getInstance() .sendRichLog(AliYunLogConfig.AGORA, "收到主叫方通话邀请,但界面实例已存在($from)") LogHelper.getInstance().uploadLog(false) } } else { LogUtil.d("[agora]收到声网邀请,但response==null") writeAgoraLog( "收到主叫方通话邀请,但response==null($from)-------Time:${ AudioLogUtils.format.format( Calendar.getInstance().time ) }", FILE_NAME, false ) AliYunRichLogsHelper.getInstance() .sendRichLog(AliYunLogConfig.AGORA, "收到主叫方通话邀请,但response==null($from)") LogHelper.getInstance().uploadLog(false) } } private fun activityIsExists(cls: Class<*>): Boolean { for (activity in ActivityManager.getInstance().getActivitys()) { if (activity.javaClass == cls) { return true } } return false } /** * 关闭通话界面 */ fun closePage() { var act = ActivityManager.getInstance().getTopTaskActivity() if (act is ConsultantAudioHomeActivity) { //未接通时,收到呼叫进程失败关闭页面,已接通无需关闭 if (act.status == ConsultantAudioHomeActivity.STATUS_NOT_ANSWERED) { act.close(ConsultantAudioHomeActivity.RESULT_USER_CANCEL, "") } } } /** * 退出登录 * @param */ private fun logout(isReLogin: Boolean) { EventBus.getDefault().unregister(this) YDLRTMClient.instances.logout(object : LoginCallback { override fun onSuccess() { //退出登陆成功 LogUtil.d("[agora]实时消息退出成功") AliYunRichLogsHelper.getInstance() .sendRichLog(AliYunLogConfig.AGORA, "实时消息退出成功") if (isReLogin) { login( ModularServiceManager.provide(IUserService::class.java).getUserInfo()?.uid ) } } override fun onFailure(msg: String?) { LogUtil.d("[agora]实时消息退出失败:$msg") AliYunRichLogsHelper.getInstance() .sendRichLog(AliYunLogConfig.AGORA, "实时消息退出失败:$msg") } }) } /** * 退出登录默认不重新登录 */ fun logout() { logout(false) } /** * RTM登录异常,上传错误日志 msg * 声网出现异常,上传错误日志 connectException */ @SuppressLint("CheckResult") fun uploadException( connectException: ConnectExceptionCommand, callback: UploadExceptionCallback? ) { AudioApiRequestUtil.connectException(connectException).subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()).subscribe({ callback?.onSuccess() }, { LogUtil.e("agora", "声网上传异常与错误日志接口调用失败:" + it.message) AliYunRichLogsHelper.getInstance() .sendRichLog(AliYunLogConfig.AGORA, "声网上传异常与错误日志接口调用失败: + ${it.message}") }) } /** * 上传异常错误回调 */ interface UploadExceptionCallback { fun onSuccess() } @SuppressLint("CheckResult") fun callEndStatusUpdate(channelId: String, endStatus: Int, msg: String) { AudioApiRequestUtil.callEndStatusUpdate(channelId, endStatus, msg) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ }, { LogUtil.d("callEndStatusUpdate error: ${it.message}") AliYunRichLogsHelper.getInstance() .sendRichLog(AliYunLogConfig.AGORA, "callEndStatusUpdate error: ${it.message}") }) } /** * 实时消息全局监听 */ private val listener = object : InitListener { override fun onTokenExpired() { AliYunRichLogsHelper.getInstance() .sendRichLog(AliYunLogConfig.AGORA, "onTokenExpired") LogUtil.e("[agora]onTokenExpired") instances.login( ModularServiceManager.provide(IUserService::class.java).getUserInfo()?.uid ) } override fun onMessageReceived(message: RTMMesssage, userId: Int) { LogUtil.i("[agora]onMessageReceived:${message.text} -->uid:$userId") AliYunRichLogsHelper.getInstance() .sendRichLog( AliYunLogConfig.AGORA, "onMessageReceived:${message.text} -->uid:$userId" ) } /** * https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler.html#a31b2974a574ec45e62bb768e17d1f49e * */ override fun onConnectionStateChanged(state: Int, reason: Int) { val msg = "state:${state},reason:${reason}" VLog.i(TAG, msg) sdkStatus = state writeAgoraLog( "声网rtm登录状态:${state}-------Time:${AudioLogUtils.format.format(Calendar.getInstance().time)}", "confide.log", true ) LogUtil.i("[agora]onConnectionStateChanged:state:${state} -->reason:$reason") AliYunRichLogsHelper.getInstance() .sendRichLog(AliYunLogConfig.AGORA, "声网rtm登录状态:${state}") /* * 当reason=CONNECTION_CHANGE_REASON_REMOTE_LOGIN的时候,是远端用户以相同UID登录RTM * 如果正在通话中,则不进行退出操作 * */ if (reason == CONNECTION_CHANGE_REASON_REMOTE_LOGIN) { if (!activityIsExists(ConsultantAudioHomeActivity::class.java) && !activityIsExists(AudioHomeActivity::class.java)) { isOnlineRtm = false logout() } else { logout(true) } } } } /** * 倾诉日志 * @param session 通话业务id * @param status 状态:01通话中(不影响通话的事件) 10:拨打 20未拨通 30未接通 40 接通 50挂断 60断线 70重连 80 呼叫方信号 90 被呼叫方信号 * @param res 上报的详细 * @param line 载体:1.网易 2.中国移动(双呼)3.联通 4.华为 5.糖猫-联通 7:声网 8:微信 10:新移动 * */ fun callEventSave( status: String, res: String, session: String?, line: String ) { //保证session有值 session?.let { sessionLet -> AudioApiRequestUtil.callEventSave(sessionLet, line, status, "用户端:${res}") .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe() } } }