Просмотр исходного кода

feat: 实时匹配通话 (#24)

* feat: 匹配入口

* feat: 匹配界面

* feat: 匹配流程框架

* feat: 匹配数据结构

* feat: 匹配入口调整

* feat: 匹配流程

* feat: 通话增加matchID参数;匹配开启视频通话,关闭视频流

* feat: 合并分支问题

* feat: 实时匹配数据结构

* feat: 匹配UI动画

* feat: 匹配状态机

* feat: 匹配状态机

* feat: 状态机悲观更新

* feat: 接口更新

* feat: 接口联调、转视频邀请弹窗

* feat: 转视频邀请弹窗

* feat: 匹配流程、转视频联调

* feat: 匹配流程、匹配开启视频通话模式,关闭视频流

* feat: 语音转视频流程

* feat: 语音转视频邀请错误提示

* todo: pwl

* feat: startCall带上matchId;接受邀请和邀请成功通知后打开视频

* feat: 实时匹配通知

* feat: 匹配入口可挂断

* feat: review代码修改

* feat: review代码修改

* feat: 匹配下一个、匹配通知UI

* feat: 语音转视频切主线程

* feat: 被叫收到匹配通知触发震动

* feat: 匹配请求添加个人信息、CameraOpen逻辑补充

* feat: openCamera状态重置

* feat: network库更新

* feat: 匹配动画添加头像;匹配超时弹窗

* feat: 男性取消匹配或女性被抢单后,关闭匹配通知

* feat: 匹配结算页面;匹配中不允许通话

* feat: 匹配结算页面

* feat: 匹配结算页面优化

* feat: 匹配超时后引导继续匹配

* feat: 通话花费、收益文案修改

* feat: matchId等待匹配通话结束才清理

* feat: matchId问题

* feat: 语音转视频切换问题

* feat: 转视频邀请弹窗30s后自动关闭

* feat: 语音转视频超时toast,匹配成功关闭匹配中activity,匹配前余额不足跳转充值页

* feat: 最小化通话窗口状态

* feat: matchRemoteUser不需要livedata

* fix: 匹配结束页,通过notify来驱动进行展示

* feat: 取消匹配失败也正常流转状态

* feat: 转视频邀请超时、拒绝提示

* fix: 补充部分多语言

* feat: 匹配中进房间,成功后退房直接通话

* fix: CallMatchActivity,activity复用问题

* fix: CallSettlementActivity,启动模式standard

* fix: 匹配中,进房提示弹窗,rtl入口动画

* feat: 匹配中界面修改

* fix: call_fail_for_match_mode

* feat: 文案修改

* feat: 文案修改

* feat: 匹配完成后呼叫前检查过程失败,清除匹配数据

* fix: log tag统一

* fix: 多语言

* feat: 资源替换;连接中状态修改;匹配模式接通后直接退媒体

* feat: 校正匹配模式通话状态

* fix: Log.i日志

* fix: handleNewIntent问题

* feat: 转视频邀请通知修改

* feat: 女性匹配成功后10s内没接通电话,清除数据

* fix: 匹配前,补充权限判断

* fix: 在房间内,匹配成功,不需要权限判断

* fix: 多语言文案补充

* feat: 匹配页动画

* feat: 多语言文案修改

* fix: 自动accept的时候,不需要检查冲突

* fix: 提高CALL_MATCH_NOTIFY横幅的优先级

* feat: 阿语适配、资源替换

* fix: 一个男生给多个女生打电话,其中一个女生接听后,另一个女生邀请弹窗需要等待5秒才能收起

* fix: 代码完善

* feat: UI走查修改

* feat: 匹配失败

* fix: 滑动崩溃,继续补充更多日志

* fix: CallSettlementActivity singleTask

* fix: CallSettlementActivity handleNewIntent

* feat: 收到匹配通知响铃

* feat: 匹配动画

* feat: 女性接受匹配前校验权限

---------

Co-authored-by: DoggyZhang <doggyzhang1219@gmail.com>
Co-authored-by: XiaodongLin <450468291@qq.com>
WilliumP 7 месяцев назад
Родитель
Сommit
c91533beec
100 измененных файлов с 3973 добавлено и 405 удалено
  1. 3 3
      app/dependencies/releaseRuntimeClasspath.txt
  2. BIN
      app/src/main/assets/match_entrance_bg.svga
  3. BIN
      app/src/main/assets/match_entrance_bg_rtl.svga
  4. 2 1
      app/src/main/java/com/adealink/weparty/apm/HookTest.kt
  5. 4 0
      app/src/main/java/com/adealink/weparty/commonui/dialogfragment/BaseDialogFragment.kt
  6. 6 3
      app/src/main/java/com/adealink/weparty/commonui/widget/floatview/FloatViewFactory.kt
  7. 3 1
      app/src/main/java/com/adealink/weparty/commonui/widget/floatview/data/IFloatData.kt
  8. 2 1
      app/src/main/java/com/adealink/weparty/commonui/widget/floatview/mode/ApplicationModeWindowManager.kt
  9. 4 0
      app/src/main/java/com/adealink/weparty/effect/WeAnimView.kt
  10. 220 0
      app/src/main/java/com/adealink/weparty/match/MatchStateMachine.kt
  11. 17 0
      app/src/main/java/com/adealink/weparty/module/call/CallModule.kt
  12. 5 0
      app/src/main/java/com/adealink/weparty/module/call/ICallService.kt
  13. 14 0
      app/src/main/java/com/adealink/weparty/module/call/Router.kt
  14. 12 0
      app/src/main/java/com/adealink/weparty/module/call/match/IMatchListener.kt
  15. 36 0
      app/src/main/java/com/adealink/weparty/module/call/match/IMatchManager.kt
  16. 11 0
      app/src/main/java/com/adealink/weparty/module/call/viewmodel/ICallViewModel.kt
  17. 1 0
      app/src/main/java/com/adealink/weparty/module/follow/data/FollowData.kt
  18. 2 1
      app/src/main/java/com/adealink/weparty/module/message/data/MessageData.kt
  19. 147 0
      app/src/main/java/com/adealink/weparty/module/userlist/floatview/CallMatchEntranceFloatView.kt
  20. 61 0
      app/src/main/java/com/adealink/weparty/module/userlist/floatview/CallMatchEntranceFloatViewComp.kt
  21. 6 0
      app/src/main/java/com/adealink/weparty/ui/home/BaseHomeFragment.kt
  22. 5 0
      app/src/main/java/com/adealink/weparty/ui/home/util/HomeUIUtil.kt
  23. BIN
      app/src/main/res/drawable-xhdpi/call_hang_up_ic.webp
  24. BIN
      app/src/main/res/drawable-xhdpi/call_match_entrance_bg.webp
  25. 9 0
      app/src/main/res/drawable/common_cancel_radius_35_bg.xml
  26. 1 1
      app/src/main/res/drawable/common_white_radius_25_bg.xml
  27. 55 0
      app/src/main/res/layout/layout_call_match_entrance_float_view.xml
  28. 2 0
      app/src/main/res/values-ar/strings.xml
  29. 2 0
      app/src/main/res/values-zh/strings.xml
  30. 5 0
      app/src/main/res/values/colors.xml
  31. 3 0
      app/src/main/res/values/strings.xml
  32. 2 1
      gradle/libs.versions.toml
  33. 14 0
      module/call/src/main/AndroidManifest.xml
  34. BIN
      module/call/src/main/assets/match_avatar.svga
  35. 4 1
      module/call/src/main/java/com/adealink/weparty/call/CallActivity.kt
  36. 1 1
      module/call/src/main/java/com/adealink/weparty/call/CallDialog.kt
  37. 18 1
      module/call/src/main/java/com/adealink/weparty/call/CallServiceImpl.kt
  38. 9 0
      module/call/src/main/java/com/adealink/weparty/call/WenextUICallKitImpl.kt
  39. 3 0
      module/call/src/main/java/com/adealink/weparty/call/comp/DebugComp.kt
  40. 1 0
      module/call/src/main/java/com/adealink/weparty/call/constant/Error.kt
  41. 3 1
      module/call/src/main/java/com/adealink/weparty/call/constant/Tags.kt
  42. 46 0
      module/call/src/main/java/com/adealink/weparty/call/data/CallData.kt
  43. 42 1
      module/call/src/main/java/com/adealink/weparty/call/data/CallNotifyData.kt
  44. 57 0
      module/call/src/main/java/com/adealink/weparty/call/data/MatchData.kt
  45. 22 0
      module/call/src/main/java/com/adealink/weparty/call/data/MatchNotifyData.kt
  46. 39 0
      module/call/src/main/java/com/adealink/weparty/call/datasource/remote/CallHttpService.kt
  47. 249 83
      module/call/src/main/java/com/adealink/weparty/call/manager/CallManager.kt
  48. 20 0
      module/call/src/main/java/com/adealink/weparty/call/manager/CallPingManager.kt
  49. 2 0
      module/call/src/main/java/com/adealink/weparty/call/manager/ICallListener.kt
  50. 2 1
      module/call/src/main/java/com/adealink/weparty/call/manager/ICallManager.kt
  51. 203 0
      module/call/src/main/java/com/adealink/weparty/call/match/CallMatchActivity.kt
  52. 52 0
      module/call/src/main/java/com/adealink/weparty/call/match/MatchHttpService.kt
  53. 446 0
      module/call/src/main/java/com/adealink/weparty/call/match/MatchManager.kt
  54. 56 0
      module/call/src/main/java/com/adealink/weparty/call/video/CallSettlementActivity.kt
  55. 201 0
      module/call/src/main/java/com/adealink/weparty/call/video/CallSettlementFragment.kt
  56. 21 1
      module/call/src/main/java/com/adealink/weparty/call/video/VideoFragment.kt
  57. 7 1
      module/call/src/main/java/com/adealink/weparty/call/video/comp/VideoCallerComp.kt
  58. 46 2
      module/call/src/main/java/com/adealink/weparty/call/video/comp/VideoRoomBottomComp.kt
  59. 31 7
      module/call/src/main/java/com/adealink/weparty/call/video/comp/VideoRoomComp.kt
  60. 105 0
      module/call/src/main/java/com/adealink/weparty/call/video/dialog/PayeeReceiveVideoInvitationDialog.kt
  61. 89 0
      module/call/src/main/java/com/adealink/weparty/call/video/dialog/PayeeSendVideoInvitationDialog.kt
  62. 110 0
      module/call/src/main/java/com/adealink/weparty/call/video/dialog/PayerReceiveVideoInvitationDialog.kt
  63. 95 0
      module/call/src/main/java/com/adealink/weparty/call/video/dialog/PayerSendVideoInvitationDialog.kt
  64. 24 3
      module/call/src/main/java/com/adealink/weparty/call/video/fragment/VideoCallerFragment.kt
  65. 1 1
      module/call/src/main/java/com/adealink/weparty/call/video/fragment/VideoRoomFragment.kt
  66. 6 0
      module/call/src/main/java/com/adealink/weparty/call/video/widget/CallerWaitingFunctionView.kt
  67. 20 10
      module/call/src/main/java/com/adealink/weparty/call/view/floatview/calling/CallingView.kt
  68. 9 0
      module/call/src/main/java/com/adealink/weparty/call/view/floatview/matchnotify/IMatchNotify.kt
  69. 11 0
      module/call/src/main/java/com/adealink/weparty/call/view/floatview/matchnotify/MatchNotifyFloatData.kt
  70. 86 0
      module/call/src/main/java/com/adealink/weparty/call/view/floatview/matchnotify/MatchNotifyFloatView.kt
  71. 69 0
      module/call/src/main/java/com/adealink/weparty/call/view/floatview/matchnotify/MatchNotifyView.kt
  72. 85 0
      module/call/src/main/java/com/adealink/weparty/call/viewmodel/CallViewModel.kt
  73. 65 0
      module/call/src/main/java/com/adealink/weparty/call/widget/CameraOpen.kt
  74. 19 0
      module/call/src/main/java/com/adealink/weparty/call/widget/MatchModeCameraOpen.kt
  75. 8 0
      module/call/src/main/java/com/tencent/qcloud/tuikit/tuicallkit/data/User.kt
  76. 1 0
      module/call/src/main/java/com/tencent/qcloud/tuikit/tuicallkit/manager/EngineManager.kt
  77. 13 0
      module/call/src/main/java/com/tencent/qcloud/tuikit/tuicallkit/state/TUICallState.kt
  78. 37 1
      module/call/src/main/java/com/tencent/qcloud/tuikit/tuicallkit/utils/PermissionRequest.kt
  79. BIN
      module/call/src/main/res/drawable-xhdpi/call_match_bg.webp
  80. BIN
      module/call/src/main/res/drawable-xhdpi/call_match_next_bg.webp
  81. BIN
      module/call/src/main/res/drawable-xhdpi/call_payee_invitation_bg.webp
  82. BIN
      module/call/src/main/res/drawable-xhdpi/call_payer_invitation_bg.webp
  83. 11 0
      module/call/src/main/res/drawable/call_match_btn_bg.xml
  84. 7 0
      module/call/src/main/res/drawable/call_settlement_bg.xml
  85. 12 0
      module/call/src/main/res/layout/activity_call_settlement.xml
  86. 2 2
      module/call/src/main/res/layout/call_incoming_float_view.xml
  87. 81 0
      module/call/src/main/res/layout/dialog_payee_video_invitation.xml
  88. 104 0
      module/call/src/main/res/layout/dialog_payer_video_invitation.xml
  89. 197 0
      module/call/src/main/res/layout/fragment_call_settlement.xml
  90. 12 0
      module/call/src/main/res/layout/fragment_call_video_accept.xml
  91. 9 0
      module/call/src/main/res/layout/fragment_call_video_caller_waiting.xml
  92. 101 0
      module/call/src/main/res/layout/layout_call_match_activity.xml
  93. 0 16
      module/call/src/main/res/layout/layout_call_userinfo.xml
  94. 3 1
      module/call/src/main/res/layout/layout_call_video_room_bottom_bar.xml
  95. 96 76
      module/call/src/main/res/values-ar/strings.xml
  96. 96 76
      module/call/src/main/res/values-zh/strings.xml
  97. 96 76
      module/call/src/main/res/values/strings.xml
  98. 1 0
      module/message/src/main/java/com/adealink/weparty/message/conversation/ConversationActivity.kt
  99. 16 26
      module/operation/src/main/java/com/adealink/weparty/operation/newuser/NewUserGreetingDialog.kt
  100. 41 5
      module/room/src/main/java/com/adealink/weparty/room/interceptor/EnterRoomUriInterceptor.kt

+ 3 - 3
app/dependencies/releaseRuntimeClasspath.txt

@@ -233,7 +233,7 @@ com.wenext.android:frame-aab:5.1.8-yoki-beta
 com.wenext.android:frame-apm:5.1.5-yoki-beta
 com.wenext.android:frame-audio:5.1.4
 com.wenext.android:frame-base:5.1.4
-com.wenext.android:frame-bom:5.1.22-yoki-18
+com.wenext.android:frame-bom:5.1.22-yoki-21
 com.wenext.android:frame-coroutine:5.1.4
 com.wenext.android:frame-crash:5.1.5-yoki-beta
 com.wenext.android:frame-data:5.1.5-yoki
@@ -250,7 +250,7 @@ com.wenext.android:frame-locale:5.1.8-yoki
 com.wenext.android:frame-log:5.1.6-yoki
 com.wenext.android:frame-media:5.1.10-yoki-beta
 com.wenext.android:frame-mvvm:5.1.5-beta
-com.wenext.android:frame-network:5.1.6-yoki
+com.wenext.android:frame-network:5.1.8-yoki
 com.wenext.android:frame-oss:5.1.7-yoki-1
 com.wenext.android:frame-push:5.1.4
 com.wenext.android:frame-router-annotation:5.1.4
@@ -277,7 +277,7 @@ io.github.scwang90:refresh-layout-kernel:3.0.0-alpha
 io.reactivex.rxjava3:rxjava:3.0.4
 javax.inject:javax.inject:1
 org.checkerframework:checker-qual:3.43.0
-org.conscrypt:conscrypt-android:2.5.3
+org.conscrypt:conscrypt-android:2.5.2
 org.jetbrains.kotlin:kotlin-android-extensions-runtime:1.9.0
 org.jetbrains.kotlin:kotlin-annotations-jvm:1.3.72
 org.jetbrains.kotlin:kotlin-bom:1.8.22

BIN
app/src/main/assets/match_entrance_bg.svga


BIN
app/src/main/assets/match_entrance_bg_rtl.svga


+ 2 - 1
app/src/main/java/com/adealink/weparty/apm/HookTest.kt

@@ -29,7 +29,8 @@ class ScrollEventAdapterMethod : MatchClassMethod {
                 val viewPagerIdString = mViewPager.resources?.getResourceEntryName(mViewPager.id)
                 Log.i(
                     "ScrollEventAdapterMethod",
-                    "=====updateScrollEventValues=====${mViewPager}, viewPager:$viewPagerIdString"
+                    "=====updateScrollEventValues=====${mViewPager}, viewPager:$viewPagerIdString, itemCount:${mViewPager.childCount}, currentItem:${mViewPager.currentItem}, " +
+                            "${mViewPager.adapter}, ${mViewPager.itemDecorationCount}, ${mViewPager.getChildAt(mViewPager.currentItem)}"
                 )
             }
         } catch (e: Exception) {

+ 4 - 0
app/src/main/java/com/adealink/weparty/commonui/dialogfragment/BaseDialogFragment.kt

@@ -17,6 +17,8 @@ import androidx.fragment.app.FragmentTransaction
 import com.adealink.frame.aab.util.compatInflateView
 import com.adealink.frame.aab.util.installSplitCompat
 import com.adealink.weparty.commonui.dialogchain.DialogShowManager
+import com.google.firebase.Firebase
+import com.google.firebase.crashlytics.crashlytics
 import com.qmuiteam.qmui.widget.dialog.QMUITipDialog
 
 inline fun <reified F : BaseDialogFragment> showDialogFragment(
@@ -77,6 +79,8 @@ open class BaseDialogFragment(@LayoutRes open val layoutId: Int) : DialogFragmen
     @CallSuper
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
+        com.adealink.frame.log.Log.i(TAG," onViewCreated... ${javaClass.simpleName}")
+        Firebase.crashlytics.log("BaseFragment onResume: ${javaClass.simpleName}")
         initViews()
         initComponents()
         observeViewModel()

+ 6 - 3
app/src/main/java/com/adealink/weparty/commonui/widget/floatview/FloatViewFactory.kt

@@ -15,12 +15,14 @@ import com.adealink.weparty.module.game.floatview.GameEntranceFloatView
 import com.adealink.weparty.module.headline.HeadlineModule
 import com.adealink.weparty.module.level.LevelModule
 import com.adealink.weparty.module.message.MessageModule
-import com.adealink.weparty.module.profile.ProfileModule
-import com.adealink.weparty.module.room.RoomModule
 import com.adealink.weparty.module.operation.newuser.HomeBannerEntranceFloatData
 import com.adealink.weparty.module.operation.newuser.HomeBannerEntranceFloatView
+import com.adealink.weparty.module.profile.ProfileModule
+import com.adealink.weparty.module.room.RoomModule
 import com.adealink.weparty.module.task.HomeIncomeFloatData
 import com.adealink.weparty.module.task.HomeIncomeFloatView
+import com.adealink.weparty.module.userlist.floatview.CallMatchEntranceFloatData
+import com.adealink.weparty.module.userlist.floatview.CallMatchEntranceFloatView
 import com.adealink.weparty.module.wallet.WalletModule
 import com.adealink.weparty.network.view.NetworkReconnectFloatData
 import com.adealink.weparty.network.view.NetworkReconnectFloatView
@@ -46,7 +48,8 @@ class FloatViewFactory : IFloatViewFactory {
             FloatWindowType.ROCKET_HEADLINE -> GameModule.getRocketHeadlineFloatView(data)
             FloatWindowType.GAME_ENTRANCE -> GameEntranceFloatView(data as GameEntranceFloatData)
             FloatWindowType.GLOBAL_HEADLINE -> HeadlineModule.getGlobalHeadlineFloatView(data)
-            else -> null
+            FloatWindowType.CALL_MATCH_ENTRANCE -> CallMatchEntranceFloatView(data as CallMatchEntranceFloatData)
+            FloatWindowType.CALL_MATCH_NOTIFY -> CallModule.getMatchNotifyFloatView(data)
         }
         return floatView as? V
     }

+ 3 - 1
app/src/main/java/com/adealink/weparty/commonui/widget/floatview/data/IFloatData.kt

@@ -17,7 +17,9 @@ enum class FloatWindowType(val type: String) {
     MEMORY_USAGE("memory_usage"),
     ROCKET_HEADLINE("rocket_headline"),
     GAME_ENTRANCE("game_entrance"),
-    GLOBAL_HEADLINE("global_headline") //全服横幅,房间内房间外
+    GLOBAL_HEADLINE("global_headline"), //全服横幅,房间内房间外
+    CALL_MATCH_ENTRANCE("call_match_entrance"),
+    CALL_MATCH_NOTIFY("call_match_notify"),
 }
 
 

+ 2 - 1
app/src/main/java/com/adealink/weparty/commonui/widget/floatview/mode/ApplicationModeWindowManager.kt

@@ -88,7 +88,8 @@ open class ApplicationModeWindowManager : BaseWindowManager() {
             }
             if (topViewCount >= MAX_SHOW_VIEW_COUNT
                 && (view.baseFloatData.windowType() != FloatWindowType.CALL_1V1_INCOMING
-                        && view.baseFloatData.windowType() != FloatWindowType.NOTIFICATION_MESSAGE)
+                        && view.baseFloatData.windowType() != FloatWindowType.NOTIFICATION_MESSAGE
+                        && view.baseFloatData.windowType() != FloatWindowType.CALL_MATCH_NOTIFY)
             ) {
                 Log.w(
                     TAG_FLOAT_VIEW,

+ 4 - 0
app/src/main/java/com/adealink/weparty/effect/WeAnimView.kt

@@ -762,6 +762,10 @@ open class WeAnimView @JvmOverloads constructor(
         svgaAnimView?.imageMatrix = matrix
     }
 
+    fun isResourceReady(): Boolean {
+        return path.isNullOrEmpty().not()
+    }
+
     interface IWeAnimPlayListener {
         fun onGetSvgaInfo(svgaInfo: SvgaInfo?) {}
         fun onPlayStart() {}

+ 220 - 0
app/src/main/java/com/adealink/weparty/match/MatchStateMachine.kt

@@ -0,0 +1,220 @@
+package com.adealink.weparty.match
+
+import android.os.Handler
+import android.os.Looper
+import androidx.lifecycle.MutableLiveData
+import com.adealink.frame.aab.util.getCompatString
+import com.adealink.frame.coroutine.dispatcher.Dispatcher
+import com.adealink.frame.log.Log
+import com.adealink.weparty.R
+import com.adealink.weparty.commonui.toast.util.showToast
+import kotlinx.coroutines.withContext
+
+/**
+ * author: PengWuliang
+ * date: 2025/8/15 20:32
+ * desc:
+ */
+class MatchStateMachine {
+
+    companion object {
+        val instance: MatchStateMachine by lazy { MatchStateMachine() }
+        private const val TAG = "MatchStateMachine"
+    }
+
+    private val _currentState = MutableLiveData<MatchStateEnum>(MatchStateEnum.IDLE)
+    val currentState: MutableLiveData<MatchStateEnum> = _currentState
+
+    private val mainHandler = Handler(Looper.getMainLooper())
+    private var callback: MatchTimeoutCallback? = null
+
+    private val matchTimeout = Runnable {
+        callback?.onMatchTimeout()
+    }
+
+    private val answerTimeout = Runnable {
+        showToast(getCompatString(R.string.common_response_timeout))
+    }
+
+    private var matchStartTime: Long = 0L
+
+    fun getCurrentState(): MatchStateEnum {
+        return _currentState.value ?: MatchStateEnum.IDLE
+    }
+
+    fun setTimeoutCallback(callback: MatchTimeoutCallback) {
+        this.callback = callback
+    }
+
+    /**
+     * 检查是否可以处理某个事件
+     */
+    fun canHandleEvent(event: MatchEvent): Boolean {
+        val currentState = getCurrentState()
+        return getNextState(currentState, event) != null
+    }
+
+    //todo pwl 调用的地方改成handleEventAsync?
+    fun handleEvent(event: MatchEvent): Boolean {
+        val currentState = _currentState.value ?: MatchStateEnum.IDLE
+        val nextState = getNextState(currentState, event)
+
+        if (nextState == null) {
+            Log.i(TAG, "Event $event not allowed in state $currentState")
+            return false
+        }
+
+        if (nextState == currentState && currentState != MatchStateEnum.MATCHING) {
+            Log.i(TAG, "Event $event processed, state remains $currentState")
+            return true
+        }
+        transitionTo(nextState, event)
+        return true
+    }
+
+    /**
+     * 异步处理事件,返回Deferred以等待状态更新完成
+     */
+    suspend fun handleEventAsync(event: MatchEvent): Boolean {
+        return withContext(Dispatcher.UI) {
+            val currentState = _currentState.value ?: MatchStateEnum.IDLE
+            val nextState = getNextState(currentState, event)
+
+            if (nextState == null) {
+                Log.i(TAG, "Event $event not allowed in state $currentState")
+                return@withContext false
+            }
+
+            if (nextState == currentState) {
+                Log.i(TAG, "Event $event processed, state remains $currentState")
+                return@withContext true
+            }
+
+            transitionTo(nextState, event)
+            return@withContext true
+        }
+    }
+
+    private fun getNextState(currentState: MatchStateEnum, event: MatchEvent): MatchStateEnum? {
+        return when (currentState) {
+            MatchStateEnum.IDLE -> when (event) {
+                MatchEvent.START_MATCH -> MatchStateEnum.MATCHING
+                MatchEvent.RECEIVE_REQUEST -> MatchStateEnum.RECEIVE
+                MatchEvent.MATCH_FAILED,
+                MatchEvent.RESET -> MatchStateEnum.IDLE
+                else -> null
+            }
+
+            MatchStateEnum.MATCHING -> when (event) {
+                MatchEvent.MATCH_SUCCESS -> MatchStateEnum.SUCCESS
+                MatchEvent.START_MATCH -> MatchStateEnum.MATCHING
+                MatchEvent.MATCH_FAILED,
+                MatchEvent.CANCEL_MATCH,
+                MatchEvent.MATCH_TIMEOUT,
+                MatchEvent.RESET -> MatchStateEnum.IDLE
+                else -> null
+            }
+            MatchStateEnum.RECEIVE -> when (event) {
+                MatchEvent.ACCEPT_REQUEST -> MatchStateEnum.MATCHING
+                MatchEvent.REJECT_REQUEST,
+                MatchEvent.MATCH_TIMEOUT,
+                MatchEvent.RESET -> MatchStateEnum.IDLE
+                else -> null
+            }
+
+            MatchStateEnum.SUCCESS -> when (event) {
+                MatchEvent.MATCH_SUCCESS -> MatchStateEnum.SUCCESS
+                MatchEvent.RESET -> MatchStateEnum.IDLE
+                else -> null
+            }
+        }
+    }
+
+    private fun transitionTo(newState: MatchStateEnum, event: MatchEvent? = null) {
+        val oldState = _currentState.value ?: MatchStateEnum.IDLE
+        // 1、状态切换前的处理
+        onStateExit(oldState)
+
+        // 2、更新状态
+        if (Looper.myLooper() == Looper.getMainLooper()) {
+            _currentState.value = newState
+        } else {
+            _currentState.postValue(newState)
+        }
+
+        Log.i(TAG, "State transition: $oldState -> $newState, event: $event")
+
+        // 3、状态切换后的处理
+        onStateEnter(newState)
+    }
+
+    private fun onStateExit(state: MatchStateEnum) {
+        when (state) {
+            MatchStateEnum.MATCHING -> {
+                mainHandler.removeCallbacks(matchTimeout)
+            }
+            MatchStateEnum.RECEIVE -> {
+                mainHandler.removeCallbacks(answerTimeout)
+            }
+            else -> {
+                // 其他状态无需额外处理
+            }
+        }
+    }
+
+    private fun onStateEnter(state: MatchStateEnum) {
+        when (state) {
+            MatchStateEnum.MATCHING -> {
+                mainHandler.postDelayed(matchTimeout, 60_000)
+            }
+            MatchStateEnum.RECEIVE -> {
+                mainHandler.postDelayed(answerTimeout, 10_000)
+            }
+            MatchStateEnum.IDLE -> {
+                matchStartTime = 0L
+            }
+            else -> {
+                // 其他状态无需额外处理
+            }
+        }
+    }
+}
+
+interface MatchTimeoutCallback {
+    fun onMatchTimeout()
+}
+
+enum class MatchEvent(val description: String) {
+    START_MATCH("开始匹配"),
+    CANCEL_MATCH("取消匹配"),
+    MATCH_SUCCESS("匹配成功"),
+    MATCH_FAILED("匹配失败"),
+    MATCH_TIMEOUT("匹配超时"),
+    RECEIVE_REQUEST("收到匹配请求"),
+    ACCEPT_REQUEST("接受请求"),
+    REJECT_REQUEST("拒绝请求"),
+    RESET("重置状态")
+}
+
+enum class MatchStateEnum(val value: Int, val description: String) {
+    IDLE(0, "空闲"),
+    MATCHING(1, "匹配中"),
+    RECEIVE(2, "收到匹配请求"),
+    SUCCESS(3, "匹配成功")
+}
+
+//实时匹配结果
+enum class MatchResult(val value: Int) {
+    SUCCESS(1),
+    CANCEL(2),
+    TIMEOUT(3),
+    FAILED(4),
+}
+
+//实时匹配应答类型
+enum class AnswerType(val value: Int) {
+    ACCEPT(1),
+    REJECT(2),
+    TIMEOUT(3),
+    BUSY(4),
+}

+ 17 - 0
app/src/main/java/com/adealink/weparty/module/call/CallModule.kt

@@ -13,6 +13,7 @@ import com.adealink.weparty.commonui.widget.floatview.data.IFloatData
 import com.adealink.weparty.commonui.widget.floatview.view.BaseFloatView
 import com.adealink.weparty.module.call.data.CallerSource
 import com.adealink.weparty.module.call.data.MediaType
+import com.adealink.weparty.module.call.match.IMatchManager
 import com.adealink.weparty.module.call.viewmodel.ICallViewModel
 
 object CallModule : BaseDynamicModule<ICallService>(ICallService::class), ICallService {
@@ -37,10 +38,18 @@ object CallModule : BaseDynamicModule<ICallService>(ICallService::class), ICallS
                 return null
             }
 
+            override fun getMatchNotifyFloatView(data: IFloatData): BaseFloatView<out IFloatData>? {
+                return null
+            }
+
             override fun getCallViewModel(owner: ViewModelStoreOwner): ICallViewModel? {
                 return null
             }
 
+            override fun getMatchManager(): IMatchManager? {
+                return null
+            }
+
             override fun isLogin(): Boolean {
                 return false
             }
@@ -114,10 +123,18 @@ object CallModule : BaseDynamicModule<ICallService>(ICallService::class), ICallS
         return getService().getCallingFloatView(data)
     }
 
+    override fun getMatchNotifyFloatView(data: IFloatData): BaseFloatView<out IFloatData>? {
+        return getService().getMatchNotifyFloatView(data)
+    }
+
     override fun getCallViewModel(owner: ViewModelStoreOwner): ICallViewModel? {
         return getService().getCallViewModel(owner)
     }
 
+    override fun getMatchManager(): IMatchManager? {
+        return getService().getMatchManager()
+    }
+
     override fun isLogin(): Boolean {
         return getService().isLogin()
     }

+ 5 - 0
app/src/main/java/com/adealink/weparty/module/call/ICallService.kt

@@ -8,6 +8,7 @@ import com.adealink.weparty.commonui.widget.floatview.data.IFloatData
 import com.adealink.weparty.commonui.widget.floatview.view.BaseFloatView
 import com.adealink.weparty.module.call.data.CallerSource
 import com.adealink.weparty.module.call.data.MediaType
+import com.adealink.weparty.module.call.match.IMatchManager
 import com.adealink.weparty.module.call.viewmodel.ICallViewModel
 
 interface ICallService : IService<ICallService>, IMediaOperatorGet, IAppStartUpTask {
@@ -24,8 +25,12 @@ interface ICallService : IService<ICallService>, IMediaOperatorGet, IAppStartUpT
 
     fun getCallingFloatView(data: IFloatData): BaseFloatView<out IFloatData>?
 
+    fun getMatchNotifyFloatView(data: IFloatData): BaseFloatView<out IFloatData>?
+
     fun getCallViewModel(owner: ViewModelStoreOwner): ICallViewModel?
 
+    fun getMatchManager(): IMatchManager?
+
     fun isLogin(): Boolean
 
     fun getMediaType(): Int

+ 14 - 0
app/src/main/java/com/adealink/weparty/module/call/Router.kt

@@ -14,6 +14,20 @@ interface Call {
         }
     }
 
+    interface CallSettlement {
+        companion object {
+            const val PATH = "/call/settlement"
+            const val EXTRA_DATA = "extra_data"
+        }
+    }
+
+    interface CallMatch {
+        companion object {
+            const val PATH = "/call/match"
+            const val EXTRA_AUTO_MATCH = "extra_auto_match"
+        }
+    }
+
     interface CallDialog {
         companion object {
             const val PATH = "/call/dialog"

+ 12 - 0
app/src/main/java/com/adealink/weparty/module/call/match/IMatchListener.kt

@@ -0,0 +1,12 @@
+package com.adealink.weparty.module.call.match
+
+import com.adealink.frame.frame.IListener
+
+/**
+ * author: PengWuliang
+ * date: 2025/8/15 20:42
+ * desc:
+ */
+interface IMatchListener: IListener {
+
+}

+ 36 - 0
app/src/main/java/com/adealink/weparty/module/call/match/IMatchManager.kt

@@ -0,0 +1,36 @@
+package com.adealink.weparty.module.call.match
+
+import com.adealink.frame.frame.IBaseFrame
+
+/**
+ * author: PengWuliang
+ * date: 2025/8/15 20:36
+ * desc:
+ */
+interface IMatchManager: IBaseFrame<IMatchListener> {
+
+    fun init()
+
+    fun startMatch()
+
+    fun cancelMatch()
+
+    /**
+     * 匹配结果确认
+     */
+    fun resultAck()
+
+    fun reject()
+
+    fun accept()
+
+    fun timeout()
+
+    fun isMatchIdValid(): Boolean
+
+    fun getRoomId(): Long
+
+    fun getMatchId(): Long
+
+    fun clearData()
+}

+ 11 - 0
app/src/main/java/com/adealink/weparty/module/call/viewmodel/ICallViewModel.kt

@@ -5,10 +5,13 @@ import com.adealink.frame.base.Rlt
 import com.adealink.frame.network.data.Res
 import com.adealink.weparty.module.backpack.GetExperienceCardRes
 import com.adealink.weparty.module.call.data.CallStatus
+import com.adealink.weparty.module.profile.data.UserInfo
 
 interface ICallViewModel {
     val callStatusLD: LiveData<CallStatus>
 
+    val showMatchEntrance: LiveData<Boolean>
+
     val experienceCardLd: LiveData<Rlt<Res<GetExperienceCardRes>>>
 
     fun getCallStatus()
@@ -16,4 +19,12 @@ interface ICallViewModel {
     fun getExperienceCard(type: Int)
 
     fun getMerchantFreeCall(uid: Long, peerUid: Long)
+
+    fun switchMode(roomId: String, callMode: Int): LiveData<Rlt<Boolean>>
+
+    fun switchModeAnswer(roomId: String, answerType: Int): LiveData<Rlt<Boolean>>
+
+    fun getCallMatchEntrance()
+
+    fun getMatchUserList(): LiveData<Rlt<List<UserInfo>>>
 }

+ 1 - 0
app/src/main/java/com/adealink/weparty/module/follow/data/FollowData.kt

@@ -116,6 +116,7 @@ enum class FollowOpFrom(val from: String) {
     MOMENT_RESULT("moment_result"), //Moment
     FAMILY_MEMBER_LIST("family_member_list"), //家族成员列表
     GAME_RESULT("game_result"), //Game Result
+    CALL_MATCH("call_match"), //实时匹配通话
 }
 
 data class SpecialFollowOnlineNotify(

+ 2 - 1
app/src/main/java/com/adealink/weparty/module/message/data/MessageData.kt

@@ -242,7 +242,8 @@ enum class EnterConversationFrom {
     Push,
     IntimacyLimited,
     IntimacyRelationship,
-    FamilyInvite
+    FamilyInvite,
+    VideoCall
 }
 
 @Parcelize

+ 147 - 0
app/src/main/java/com/adealink/weparty/module/userlist/floatview/CallMatchEntranceFloatView.kt

@@ -0,0 +1,147 @@
+package com.adealink.weparty.module.userlist.floatview
+
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.content.Intent
+import android.view.LayoutInflater
+import android.view.View
+import com.adealink.frame.aab.util.getCompatString
+import com.adealink.frame.router.Router
+import com.adealink.frame.util.DisplayUtil
+import com.adealink.weparty.AppModule
+import com.adealink.weparty.R
+import com.adealink.weparty.commonui.ext.dp
+import com.adealink.weparty.commonui.ext.gone
+import com.adealink.weparty.commonui.ext.show
+import com.adealink.weparty.commonui.widget.floatview.data.FloatWindowType
+import com.adealink.weparty.commonui.widget.floatview.data.IWindowFloatData
+import com.adealink.weparty.commonui.widget.floatview.data.MODE_APPLICATION
+import com.adealink.weparty.commonui.widget.floatview.view.BaseDragFloatView
+import com.adealink.weparty.databinding.LayoutCallMatchEntranceFloatViewBinding
+import com.adealink.weparty.match.MatchStateEnum
+import com.adealink.weparty.match.MatchStateMachine
+import com.adealink.weparty.module.call.Call
+import com.adealink.weparty.module.call.CallModule
+import com.adealink.weparty.ui.home.util.HomeUIUtil
+
+/**
+ * author: PengWuliang
+ * date: 2025/8/15 12:10
+ * desc:
+ */
+class CallMatchEntranceFloatData : IWindowFloatData {
+    override fun windowType(): FloatWindowType = FloatWindowType.CALL_MATCH_ENTRANCE
+
+    override fun windowMode() = MODE_APPLICATION
+}
+
+@SuppressLint("ViewConstructor")
+class CallMatchEntranceFloatView(floatData: CallMatchEntranceFloatData): BaseDragFloatView(floatData) {
+    private var binding: LayoutCallMatchEntranceFloatViewBinding? = null
+
+    private val show: Boolean
+        get() {
+            /**
+             * 1.CallMatchActivity 或者 匹配成功 一直不可见
+             * 2.匹配前首页列表tab可见
+             * 3.匹配中一直可见
+             */
+            val activity = windowManager?.currentActivity?.get() ?: return false
+            if (activity.javaClass.name == Router.getClazz(Call.CallMatch.PATH)?.name || MatchStateMachine.instance.getCurrentState() == MatchStateEnum.SUCCESS) {
+                return false
+            }
+            if (MatchStateMachine.instance.getCurrentState() == MatchStateEnum.MATCHING || (activity.javaClass.name == Router.getClazz(
+                    AppModule.Main.PATH
+                )?.name && HomeUIUtil.isInUserListTab())) {
+                return true
+            }
+            return false
+        }
+
+    override fun onCreate() {
+        super.onCreate()
+        val binding = LayoutCallMatchEntranceFloatViewBinding.inflate(LayoutInflater.from(context))
+        this.binding = binding
+        setContentView(binding.root, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
+        binding.btnClose.setOnClickListener {
+            removeSelf("")
+        }
+        binding.root.setOnClickListener {
+            Router.build(context, Call.CallMatch.PATH)
+                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                .start()
+        }
+        binding.ivHangUp.setOnClickListener {
+            CallModule.getMatchManager()?.cancelMatch()
+        }
+        MatchStateMachine.instance.currentState.observe(this) {
+            when (it) {
+                MatchStateEnum.IDLE -> showIdleState()
+                MatchStateEnum.MATCHING -> showMatchingState()
+                else -> {
+                    showIdleState()
+                }
+            }
+        }
+        visibility = if (show) View.VISIBLE else View.GONE
+
+        if (DisplayUtil.isRtlLayout()) {
+            binding.svgaBg.setAsset("match_entrance_bg_rtl.svga")
+        } else {
+            binding.svgaBg.setAsset("match_entrance_bg.svga")
+        }
+    }
+
+    override fun onActivityChange(activity: Activity) {
+        super.onActivityChange(activity)
+        visibility = if (show) View.VISIBLE else View.GONE
+    }
+
+    fun onHomeTabChange() {
+        visibility = if (show) View.VISIBLE else View.GONE
+    }
+
+    override fun getLayoutParamWidth(): Int = 125.dp()
+
+    override fun getLayoutParamHeight(): Int = 45.dp()
+
+    override fun getLayoutParamX(): Int {
+        if(DisplayUtil.isRtlLayout()) {
+            return DisplayUtil.getScreenWidth() - getLayoutParamWidth()
+        } else {
+            return 0
+        }
+    }
+
+    override fun getLayoutParamY(): Int {
+        val homeTabHeight = 54.dp()
+        return DisplayUtil.getScreenHeight() - getLayoutParamHeight() - 157.dp() - homeTabHeight
+    }
+
+    override fun applySnapToEdge(): Boolean = true
+
+    override fun getClickableViews(): List<View>? {
+        val binding = binding ?: return null
+        return listOf(binding.btnClose, binding.ivHangUp, binding.root)
+    }
+
+    override fun getFixedLocation(): Int {
+        return if (DisplayUtil.isRtlLayout()) {
+            LOCATION_FIXED_RIGHT
+        } else {
+            LOCATION_FIXED_LEFT
+        }
+    }
+
+    private fun showIdleState() {
+        binding?.tvContent?.text = getCompatString(R.string.common_random_chat)
+        binding?.btnClose?.show()
+        binding?.ivHangUp?.gone()
+    }
+
+    private fun showMatchingState() {
+        binding?.tvContent?.text = getCompatString(R.string.common_matching)
+        binding?.btnClose?.gone()
+        binding?.ivHangUp?.show()
+    }
+}

+ 61 - 0
app/src/main/java/com/adealink/weparty/module/userlist/floatview/CallMatchEntranceFloatViewComp.kt

@@ -0,0 +1,61 @@
+package com.adealink.weparty.module.userlist.floatview
+
+import androidx.lifecycle.LifecycleOwner
+import com.adealink.frame.log.Log
+import com.adealink.frame.mvvm.view.ViewComponent
+import com.adealink.weparty.commonui.widget.floatview.WindowManagerProxy
+import com.adealink.weparty.module.call.CallModule
+
+/**
+ * author: PengWuliang
+ * date: 2025/8/18 14:59
+ * desc:
+ */
+class CallMatchEntranceFloatViewComp(lifecycleOwner: LifecycleOwner) : ViewComponent(lifecycleOwner) {
+    private var floatView: CallMatchEntranceFloatView? = null
+    private val callViewModel by lazy { CallModule.getCallViewModel(requireActivity()) }
+
+    companion object {
+        private const val TAG = "CallMatchEntranceFloatViewComp"
+    }
+
+    override fun onCreate() {
+        super.onCreate()
+        callViewModel?.showMatchEntrance?.observe(lifecycleOwner) {
+            if(it) {
+                addFloatView()
+            } else {
+                removeFloatView()
+            }
+        }
+        callViewModel?.getCallMatchEntrance()
+    }
+
+    private fun addFloatView() {
+        Log.d(TAG, "addFloatView")
+        if (floatView != null) {
+            return
+        }
+        floatView = CallMatchEntranceFloatView(CallMatchEntranceFloatData())
+        floatView?.let {
+            WindowManagerProxy.getWindowManager().addView(it)
+        }
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        removeFloatView()
+    }
+
+    private fun removeFloatView() {
+        Log.d(TAG, "removeFloatView")
+        floatView?.let {
+            WindowManagerProxy.getWindowManager().removeView(floatView!!)
+        }
+        floatView = null
+    }
+
+    fun onHomeTabChange() {
+        floatView?.onHomeTabChange()
+    }
+}

+ 6 - 0
app/src/main/java/com/adealink/weparty/ui/home/BaseHomeFragment.kt

@@ -53,6 +53,7 @@ import com.adealink.weparty.module.task.CommonActivityRewardDialogTask
 import com.adealink.weparty.module.task.DailySignInComp
 import com.adealink.weparty.module.task.HomeIncomeViewComp
 import com.adealink.weparty.module.userlist.UserList
+import com.adealink.weparty.module.userlist.floatview.CallMatchEntranceFloatViewComp
 import com.adealink.weparty.ui.IScrollManager
 import com.adealink.weparty.ui.home.util.HomeLocalService
 import com.adealink.weparty.ui.home.util.HomeUIUtil
@@ -100,6 +101,8 @@ abstract class BaseHomeFragment : BaseFragment, ITabManager {
 
     private var gameEntranceFloatViewComp: GameEntranceFloatViewComp? = null
 
+    private var callMatchEntranceFloatViewComp: CallMatchEntranceFloatViewComp? = null
+
     abstract fun initTabs()
 
     @CallSuper
@@ -143,6 +146,7 @@ abstract class BaseHomeFragment : BaseFragment, ITabManager {
                 if (getTab(tab?.position ?: 0)?.type == HomeTab.MESSAGE) {
                     HomeLocalService.hasVisitMessageTab = true
                 }
+                callMatchEntranceFloatViewComp?.onHomeTabChange()
                 homeBannerEntranceFloatViewComp?.onHomeTabChange()
                 gameEntranceFloatViewComp?.onHomeTabChange()
             }
@@ -169,6 +173,7 @@ abstract class BaseHomeFragment : BaseFragment, ITabManager {
     override fun initComponents() {
         super.initComponents()
         homeBannerEntranceFloatViewComp = HomeBannerEntranceFloatViewComp(this).also { it.attach() }
+        callMatchEntranceFloatViewComp = CallMatchEntranceFloatViewComp(this).also { it.attach() }
         HomeIncomeViewComp(this).attach()
         DailySignInComp(this).attach()
         if (ProfileModule.getMyUserInfo()?.gender == Gender.MALE.gender) {
@@ -362,6 +367,7 @@ abstract class BaseHomeFragment : BaseFragment, ITabManager {
         }
         val tabIndex = HOME_TABS.indexOfFirst { it.type.tab == tab }
         if (tabIndex != -1) {
+            HomeUIUtil.selectedHomeTab = HOME_TABS[tabIndex].type
             vpContent.setCurrentItem(tabIndex, false)
         }
     }

+ 5 - 0
app/src/main/java/com/adealink/weparty/ui/home/util/HomeUIUtil.kt

@@ -16,4 +16,9 @@ object HomeUIUtil {
         val currentActivity = AppUtil.currentActivity ?: return false
         return currentActivity is MainActivity && selectedHomeTab == HomeTab.MESSAGE
     }
+
+    fun isInUserListTab(): Boolean {
+        val currentActivity = AppUtil.currentActivity ?: return false
+        return currentActivity is MainActivity && (selectedHomeTab == HomeTab.USER_LIST)
+    }
 }

BIN
app/src/main/res/drawable-xhdpi/call_hang_up_ic.webp


BIN
app/src/main/res/drawable-xhdpi/call_match_entrance_bg.webp


+ 9 - 0
app/src/main/res/drawable/common_cancel_radius_35_bg.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <corners android:radius="35dp" />
+    <solid android:color="@color/white" />
+    <stroke
+        android:width="1dp"
+        android:color="@color/color_app_main" />
+</shape>

+ 1 - 1
app/src/main/res/drawable/common_white_radius_25_bg.xml

@@ -2,5 +2,5 @@
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
     android:shape="rectangle">
     <solid android:color="@color/white" />
-    <corners android:radius="20dp" />
+    <corners android:radius="25dp" />
 </shape>

+ 55 - 0
app/src/main/res/layout/layout_call_match_entrance_float_view.xml

@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:layout_width="125dp"
+    tools:layout_height="45dp">
+
+    <!-- todo: pwl: 适配阿语了么 ?  -->
+    <com.opensource.svgaplayer.WenextSvgaView
+        android:id="@+id/svga_bg"
+        android:layout_width="120dp"
+        android:layout_height="40dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:loopCount="-1"
+        tools:srcCompat="@drawable/call_match_entrance_bg"/>
+
+    <androidx.appcompat.widget.AppCompatImageView
+        android:id="@+id/iv_hang_up"
+        android:layout_width="30dp"
+        android:layout_height="30dp"
+        android:src="@drawable/call_hang_up_ic"
+        android:layout_marginEnd="5dp"
+        app:layout_constraintEnd_toEndOf="@id/svga_bg"
+        app:layout_constraintTop_toTopOf="@id/svga_bg"
+        app:layout_constraintBottom_toBottomOf="@id/svga_bg"
+        android:visibility="gone"/>
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/tv_content"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="5dp"
+        android:textColor="@color/white"
+        android:textSize="13sp"
+        android:gravity="center"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toStartOf="@id/iv_hang_up"
+        app:layout_constraintTop_toTopOf="@id/svga_bg"
+        app:layout_constraintBottom_toBottomOf="@id/svga_bg"
+        android:text="@string/common_random_chat"/>
+
+    <androidx.appcompat.widget.AppCompatImageView
+        android:id="@+id/btn_close"
+        android:layout_width="16dp"
+        android:layout_height="16dp"
+        android:src="@drawable/home_icon_float_close"
+        app:layout_constraintEnd_toEndOf="@id/svga_bg"
+        app:layout_constraintTop_toTopOf="@id/svga_bg"
+        android:layout_marginEnd="-5dp"
+        android:layout_marginTop="-5dp"/>
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 2 - 0
app/src/main/res/values-ar/strings.xml

@@ -717,4 +717,6 @@
     <string name="profile_gender_certification">التحقق من الجنس</string>
     <string name="family_role_owner">مالك العائلة</string>
     <string name="family_role_admin">إدارة الأسرة</string>
+    <string name="common_random_chat">دردشة عشوائية</string>
+    <string name="common_response_timeout">مهلة الاستجابة</string>
 </resources>

+ 2 - 0
app/src/main/res/values-zh/strings.xml

@@ -716,4 +716,6 @@
     <string name="profile_gender_certification">性别验证</string>
     <string name="family_role_owner">家庭主人</string>
     <string name="family_role_admin">家庭管理员</string>
+    <string name="common_random_chat">随机聊天</string>
+    <string name="common_response_timeout">响应超时</string>
 </resources>

+ 5 - 0
app/src/main/res/values/colors.xml

@@ -1163,7 +1163,12 @@
     <color name="color_FFB8CACB">#FFB8CACB</color>
     <color name="color_FFF9E88D">#FFF9E88D</color>
     <color name="color_FFFFE6CA">#FFFFE6CA</color>
+    <color name="color_FF901ED0">#FF901ED0</color>
+    <color name="color_FF848484">#FF848484</color>
     <color name="color_31C7A3">#31C7A3</color>
     <color name="color_C75325">#C75325</color>
     <color name="color_FFAA01">#FFAA01</color>
+    <color name="color_FFC251FF">#FFC251FF</color>
+    <color name="color_FFF051FF">#FFF051FF</color>
+    <color name="color_0AFF5784">#0AFF5784</color>
 </resources>

+ 3 - 0
app/src/main/res/values/strings.xml

@@ -830,6 +830,9 @@
     <string name="common_completed">Completed</string>
     <string name="common_mini_slot">Mini Slot</string>
     <string name="common_game">Game</string>
+    <string name="common_random_chat">Random Chat</string>
+    <string name="common_random_chatting">Random Chatting</string>
+    <string name="common_response_timeout">Response timeout</string>
     <string name="profile_avatar_certification">Avatar Verification</string>
     <string name="profile_gender_certification">Gender Verification</string>
     <string name="family_role_owner">Family Owner</string>

+ 2 - 1
gradle/libs.versions.toml

@@ -154,7 +154,8 @@ appleAppauth = "0.11.1"
 tiktok = "2.3.0"
 
 # frame
-frameBom = "5.1.22-yoki-18"
+frameBom = "5.1.22-yoki-21"
+
 frameRouterCompiler = "5.1.6"
 frameTrace = "1.0.0"
 frameBundleTool = "1.0.0"

+ 14 - 0
module/call/src/main/AndroidManifest.xml

@@ -52,6 +52,20 @@
             android:launchMode="singleTask"
             android:screenOrientation="portrait"
             android:theme="@style/AppTheme" />
+        <activity
+            android:name=".match.CallMatchActivity"
+            android:configChanges="orientation|screenSize"
+            android:exported="true"
+            android:launchMode="singleTask"
+            android:screenOrientation="portrait"
+            android:theme="@style/AppTheme" />
+        <activity
+            android:name=".video.CallSettlementActivity"
+            android:configChanges="orientation|screenSize"
+            android:exported="true"
+            android:launchMode="singleTask"
+            android:screenOrientation="portrait"
+            android:theme="@style/AppTheme" />
 
         <receiver
             android:name=".view.floatview.incoming.IncomingCallReceiver"

BIN
module/call/src/main/assets/match_avatar.svga


+ 4 - 1
module/call/src/main/java/com/adealink/weparty/call/CallActivity.kt

@@ -15,6 +15,7 @@ import com.adealink.weparty.call.databinding.ActivityCallBinding
 import com.adealink.weparty.call.databinding.LayoutCallDebugBinding
 import com.adealink.weparty.call.interceptor.EnterCallUriInterceptor
 import com.adealink.weparty.call.manager.callManager
+import com.adealink.weparty.call.match.matchManager
 import com.adealink.weparty.call.widget.BaseCallFragment
 import com.adealink.weparty.commonui.BaseActivity
 import com.adealink.weparty.module.call.Call
@@ -47,8 +48,10 @@ class CallActivity : BaseActivity() {
 
     private var callStatusObserver = Observer<TUICallDefine.Status> {
         if (it == TUICallDefine.Status.None) {
-            finishActivity()
             VideoViewFactory.instance.clear()
+            if(!matchManager.isMatchIdValid()) {
+                finishActivity()
+            }
         }
     }
 

+ 1 - 1
module/call/src/main/java/com/adealink/weparty/call/CallDialog.kt

@@ -246,7 +246,7 @@ class CallDialog : BottomDialogFragment(R.layout.dialog_call) {
     }
 
     companion object {
-        private const val ICON_TAG = "[icon]"
+        const val ICON_TAG = "[icon]"
         private const val CARD_TAG = "[card]"
         const val TAG = "CallDialog"
     }

+ 18 - 1
module/call/src/main/java/com/adealink/weparty/call/CallServiceImpl.kt

@@ -1,6 +1,5 @@
 package com.adealink.weparty.call
 
-import android.app.Activity
 import android.app.Application
 import androidx.lifecycle.ViewModelProvider
 import androidx.lifecycle.ViewModelStoreOwner
@@ -20,18 +19,23 @@ import com.adealink.weparty.call.datasource.local.CallLocalService
 import com.adealink.weparty.call.manager.callLoginManager
 import com.adealink.weparty.call.manager.callManager
 import com.adealink.weparty.call.manager.callPingManager
+import com.adealink.weparty.call.match.matchManager
 import com.adealink.weparty.call.view.floatview.calling.CallingFloatData
 import com.adealink.weparty.call.view.floatview.calling.CallingFloatView
 import com.adealink.weparty.call.view.floatview.incoming.InComingFloatData
 import com.adealink.weparty.call.view.floatview.incoming.InComingFloatView
+import com.adealink.weparty.call.view.floatview.matchnotify.MatchNotifyFloatData
+import com.adealink.weparty.call.view.floatview.matchnotify.MatchNotifyFloatView
 import com.adealink.weparty.call.viewmodel.CallViewModel
 import com.adealink.weparty.call.viewmodel.CallViewModelFactory
 import com.adealink.weparty.commonui.BaseActivity
 import com.adealink.weparty.commonui.widget.floatview.data.IFloatData
+import com.adealink.weparty.commonui.widget.floatview.view.BaseFloatView
 import com.adealink.weparty.module.call.ICallService
 import com.adealink.weparty.module.call.data.CALL_ERROR_UNKNOWN_MEDIA_TYPE
 import com.adealink.weparty.module.call.data.CallerSource
 import com.adealink.weparty.module.call.data.MediaType
+import com.adealink.weparty.module.call.match.IMatchManager
 import com.adealink.weparty.module.call.viewmodel.ICallViewModel
 import com.tencent.cloud.tuikit.engine.call.TUICallDefine
 import com.tencent.cloud.tuikit.engine.common.TUICommonDefine
@@ -58,6 +62,7 @@ class CallServiceImpl : ICallService {
         callLoginManager.init(application)
         callPingManager.init(application)
         callManager.init()
+        matchManager.init()
     }
 
     override fun activityOnCreateMainTask() {
@@ -112,10 +117,18 @@ class CallServiceImpl : ICallService {
         return CallingFloatView(data as CallingFloatData)
     }
 
+    override fun getMatchNotifyFloatView(data: IFloatData): BaseFloatView<out IFloatData> {
+        return MatchNotifyFloatView(data as MatchNotifyFloatData)
+    }
+
     override fun getCallViewModel(owner: ViewModelStoreOwner): ICallViewModel {
         return ViewModelProvider(owner, CallViewModelFactory())[CallViewModel::class.java]
     }
 
+    override fun getMatchManager(): IMatchManager {
+        return matchManager
+    }
+
     override fun isLogin(): Boolean {
         return callLoginManager.isLogin()
     }
@@ -142,6 +155,10 @@ class CallServiceImpl : ICallService {
             }
 
             override suspend fun confirmExitConflictMedia(exitMediaInfo: MediaInfo, enterMediaInfo: MediaInfo): Rlt<Any> {
+                if (matchManager.isMatchIdValid()) {
+                    //匹配模式下不弹窗,直接挂断当前通话
+                    return Rlt.Success(Any())
+                }
                 if (!CallLocalService.mediaConflictRemind) {
                     return Rlt.Success(Any())
                 }

+ 9 - 0
module/call/src/main/java/com/adealink/weparty/call/WenextUICallKitImpl.kt

@@ -9,6 +9,9 @@ import com.adealink.frame.router.Router
 import com.adealink.frame.util.AppUtil
 import com.adealink.weparty.call.constant.TAG_CALL_FLOW
 import com.adealink.weparty.call.constant.TAG_CALL_INCOMING
+import com.adealink.weparty.call.constant.TAG_CALL_MATCH_MODE
+import com.adealink.weparty.call.manager.callManager
+import com.adealink.weparty.call.match.matchManager
 import com.adealink.weparty.call.util.debugToast
 import com.adealink.weparty.call.view.floatview.incoming.IInComing
 import com.adealink.weparty.call.view.floatview.incoming.InComingFloatData
@@ -328,6 +331,12 @@ class WenextUICallKitImpl private constructor(context: Context) : TUICallKit(),
      */
     private fun handleNewCallWhenAppInForeground() {
         //val hasFloatPermission = PermissionRequester.newInstance(PermissionRequester.FLOAT_PERMISSION).has()
+        //如果是匹配通话,直接接受
+        if(matchManager.isMatchIdValid() && TUICallState.instance.callUserData.get().matchId == matchManager.getMatchId()) {
+            Log.i(TAG_CALL_MATCH_MODE, "accept call when match mode, matchId:${matchManager.getMatchId()}")
+            callManager.accept(null)
+            return
+        }
         val isIMPage = isIMActivity()
         when {
             isIMPage -> {

+ 3 - 0
module/call/src/main/java/com/adealink/weparty/call/comp/DebugComp.kt

@@ -143,6 +143,7 @@ class DebugComp(
 
         //房间信息
         val userDataRoomId = TUICallState.instance.callUserData.get().callRoomId
+        val matchId = TUICallState.instance.callUserData.get().matchId
         val callRoomId = TUICallState.instance.roomId.get()?.getRoomId()
         val roomId = if (callRoomId.isNullOrEmpty()) {
             userDataRoomId
@@ -152,6 +153,8 @@ class DebugComp(
         sb.append("RoomID: ").append(roomId)
             .append("\n")
 
+        sb.append("MatchID: ").append(matchId)
+            .append("\n")
 
         val callerUser = TUICallState.instance.getCallerUser()
         //主叫信息

+ 1 - 0
module/call/src/main/java/com/adealink/weparty/call/constant/Error.kt

@@ -10,6 +10,7 @@ const val CALL_ERROR_CALL_YOURSELF = 100002
 const val CALL_ERROR_MEDIA_CONFLICT_CANCEL = 100003
 const val CALL_ERROR_GET_USERINFO_FAIL = 100004
 const val CALL_ERROR_HARDWARE_REQUEST_FAIL = 100005
+const val CALL_ERROR_IN_MATCH_MODE_FAIL = 100006
 
 
 /**

+ 3 - 1
module/call/src/main/java/com/adealink/weparty/call/constant/Tags.kt

@@ -28,4 +28,6 @@ const val TAG_CALL_GIFT = "${TAG_CALL}_gift"
 
 const val TAG_CALL_PUSH = "${TAG_CALL}_push"
 
-const val TAG_CALL_DEVICE = "${TAG_CALL}_device"
+const val TAG_CALL_DEVICE = "${TAG_CALL}_device"
+
+const val TAG_CALL_MATCH_MODE = "${TAG_CALL}_match_mode"

+ 46 - 0
module/call/src/main/java/com/adealink/weparty/call/data/CallData.kt

@@ -39,6 +39,8 @@ data class CallReq(
     @SerializedName("fromInvite") val fromInvite: Boolean, //通过邀请发起
     @GsonNullable
     @SerializedName("callRoomId") val callRoomId: String?, //通过邀请发起,应答时需要携带此参数
+    @GsonNullable
+    @SerializedName("matchId") val matchId: Long?, //通过匹配发起
 )
 
 data class CallRes(
@@ -64,6 +66,10 @@ data class CallPingRes(
     @SerializedName("callStatus") val callStatus: Int, //通话状态, 0:未知, 1:通话中, 2:结束
     @SerializedName("diamond") val diamond: Int, //收益钻石
     @SerializedName("spendCoin") val spendCoin: Int, //花费金币
+    @GsonNullable
+    @SerializedName("matchId") val matchId: Long?, //匹配通话id, id为非0代表通话由匹配建立
+    @SerializedName("callMediaType") val callMediaType: Int, //通话媒体类型,1:语音 2:视频
+    @SerializedName("callMode") val callMode: Int, //当前通话模式(跟计费相关),可从语音切换到视频; 1:语音模式  2:视频模式"
 ) {
     fun isCalling(): Boolean {
         return callStatus == 1
@@ -74,6 +80,8 @@ data class CallUserData(
     @GsonNullable
     @SerializedName("callRoomId") val callRoomId: String? = null,
     @SerializedName("source") val source: Int = CallerSource.CALLER.source,
+    @GsonNullable
+    @SerializedName("matchId") val matchId: Long? = null,
 ) {
     /**
      * 是否邀请
@@ -105,3 +113,41 @@ data class MerchantFreeCallData(
     @SerializedName("freeCallSec") val freeCallSec: Int,
     @SerializedName("merchant") val merchant: Boolean,
 )
+
+data class SwitchCallModeRequest(
+    @SerializedName("roomId") val roomId: String,
+    @SerializedName("callMode") val callMode: Int,
+)
+
+data class SwitchCallModeResponse(
+    @SerializedName("roomId") val roomId: String,
+)
+
+data class SwitchCallModeAnswer(
+    @SerializedName("roomId") val roomId: String,
+    @SerializedName("answerType") val answerType: Int,
+)
+
+data class SwitchCallModeAnswerResponse(
+    @SerializedName("roomId") val roomId: String,
+)
+
+data class SwitchCallModeCancelRequest(
+    @SerializedName("roomId") val roomId: String,
+)
+
+data class SwitchCallModeCancelResponse(
+    @SerializedName("roomId") val roomId: Long
+)
+
+class CommonRequest
+
+data class CallMatchShowResponse(
+    @SerializedName("show") val show: Boolean,
+)
+
+enum class SwitchResult(val value: Int) {
+    SUCCESS(1),
+    REFUSE(2),
+    TIMEOUT(3),
+}

+ 42 - 1
module/call/src/main/java/com/adealink/weparty/call/data/CallNotifyData.kt

@@ -1,6 +1,8 @@
 package com.adealink.weparty.call.data
 
+import android.os.Parcelable
 import com.google.gson.annotations.SerializedName
+import kotlinx.parcelize.Parcelize
 
 
 data class UserCurrencyUpdateNotify(
@@ -9,6 +11,9 @@ data class UserCurrencyUpdateNotify(
     @SerializedName("diamond") val diamond: Int, //收益钻石
     @SerializedName("spendCoin") val spendCoin: Int, //花费金币
     @SerializedName("isMerchant") val isMerchant: Boolean, //是否是币商
+    @SerializedName("matchId") val matchId: Long,
+    @SerializedName("callMediaType") val callMediaType: Int,
+    @SerializedName("callMode") val callMode: Int,
 ) {
     fun isCalling(): Boolean {
         return callStatus == 1
@@ -21,4 +26,40 @@ data class UserCurrencyUpdateNotify(
 data class UserAuditNotify(
     @SerializedName("callId") val callId: String,
     @SerializedName("roomId") val roomId: String,
-)
+)
+
+/**
+ * 1v1通话模式切换请求询问
+ */
+data class SwitchCallModeAsk(
+    @SerializedName("roomId") val roomId: String,
+    @SerializedName("callMode") val callMode: Int,
+)
+
+/**
+ * 用户切换通话模式通知
+ */
+data class SwitchCallModeResultNotify(
+    @SerializedName("roomId") val roomId: String,
+    @SerializedName("fromUid") val fromUid: Long,
+    @SerializedName("toUid") val toUid: Long,
+    @SerializedName("switchResult") val switchResult: Int,
+    @SerializedName("callMode") val callMode: Int,
+)
+
+/**
+ * 匹配通话结束统计
+ */
+@Parcelize
+data class CallEndStatsNotify(
+    @SerializedName("matchId") val matchId: Long,
+    @SerializedName("mediaType") val mediaType: Int, //通话媒体类型; 1:语音 2:视频
+    @SerializedName("callMode") val callMode: Int, //通话模式; 1:语音 2:视频
+    @SerializedName("callDuration") val callDuration: Long, //通话时长
+    @SerializedName("coinCost") val coinCost: Long, //金币消耗
+    @SerializedName("diamondIncome") val diamondIncome: Long,
+
+    @SerializedName("deductUid") val deductUid: Long, // 扣币方uid
+    @SerializedName("receiveUid") val receiveUid: Long, // 收钻方uid
+
+) : Parcelable

+ 57 - 0
module/call/src/main/java/com/adealink/weparty/call/data/MatchData.kt

@@ -0,0 +1,57 @@
+package com.adealink.weparty.call.data
+
+import com.adealink.weparty.module.profile.data.UserInfo
+import com.google.gson.annotations.SerializedName
+
+/**
+ * author: PengWuliang
+ * date: 2025/8/18 11:39
+ * desc:
+ */
+class CallMatchRequest {
+
+}
+
+data class CallMatchResponse(
+    @SerializedName("matchId") val matchId: Long,
+)
+
+data class CallMatchCancelRequest(
+    @SerializedName("matchId") val matchId: Long,
+)
+
+data class CallMatchCancelResponse(
+    @SerializedName("matchId") val matchId: Long,
+)
+
+data class CallMatchReqAsk(
+    @SerializedName("matchId") val matchId: Long,
+    @SerializedName("fromUid") val fromUid: Long,
+    @SerializedName("toUid") val toUid: Long,
+    @SerializedName("matchTimeout") val matchTimeout: Long,
+    @SerializedName("userInfo") val userInfo: UserInfo
+)
+
+data class CallMatchReqAnswer(
+    @SerializedName("matchId") val matchId: Long,
+    @SerializedName("answerUid") val answerUid: Long,
+    @SerializedName("answerType") val answerType: Int, //参考AnswerType
+)
+
+data class CallMatchReqAnswerResponse(
+    @SerializedName("matchId") val matchId: Long,
+)
+
+data class CallMatchUserListResponse(
+    @SerializedName("userList") val userList: List<UserInfo>,
+)
+
+data class CallMatchPullStatusResponse(
+    @SerializedName("status") val status: Int,
+    @SerializedName("matchId") val matchId: Long,
+    @SerializedName("fromUid") val fromUid: Long,
+    @SerializedName("peerUid") val peerUid: Long,
+    @SerializedName("enterRoomType") val enterRoomType: Int,
+    @SerializedName("callMode") val callMode: Int,
+)
+

+ 22 - 0
module/call/src/main/java/com/adealink/weparty/call/data/MatchNotifyData.kt

@@ -0,0 +1,22 @@
+package com.adealink.weparty.call.data
+
+import com.google.gson.annotations.SerializedName
+
+/**
+ * author: PengWuliang
+ * date: 2025/8/18 11:46
+ * desc:
+ * @Params CallData.CallType
+ */
+data class CallMatchResultNotify(
+    @SerializedName("matchId") val matchId: Long,
+    @SerializedName("fromUid") val fromUid: Long,
+    @SerializedName("peerUid") val peerUid: Long,
+    @SerializedName("matchResult") val matchResult: Int, //参考 MatchResult
+    @SerializedName("enterRoomType") val enterRoomType: Int, //匹配后进入房间类型 参考 CallType
+    @SerializedName("callMode") val callMode: Int, //匹配后通话类型 参考 CallType
+)
+
+data class CallMatchResultNotifyAck(
+    @SerializedName("matchId") val matchId: Long,
+)

+ 39 - 0
module/call/src/main/java/com/adealink/weparty/call/datasource/remote/CallHttpService.kt

@@ -4,11 +4,20 @@ import com.adealink.frame.base.Rlt
 import com.adealink.frame.network.data.Res
 import com.adealink.weparty.call.data.CallBeginReq
 import com.adealink.weparty.call.data.CallBeginRes
+import com.adealink.weparty.call.data.CallMatchShowResponse
+import com.adealink.weparty.call.data.CallMatchUserListResponse
 import com.adealink.weparty.call.data.CallPingReq
 import com.adealink.weparty.call.data.CallPingRes
 import com.adealink.weparty.call.data.CallReq
 import com.adealink.weparty.call.data.CallRes
+import com.adealink.weparty.call.data.CommonRequest
 import com.adealink.weparty.call.data.MerchantFreeCallData
+import com.adealink.weparty.call.data.SwitchCallModeAnswer
+import com.adealink.weparty.call.data.SwitchCallModeAnswerResponse
+import com.adealink.weparty.call.data.SwitchCallModeCancelRequest
+import com.adealink.weparty.call.data.SwitchCallModeCancelResponse
+import com.adealink.weparty.call.data.SwitchCallModeRequest
+import com.adealink.weparty.call.data.SwitchCallModeResponse
 import com.adealink.weparty.module.backpack.GetExperienceCardRes
 import retrofit2.http.Body
 import retrofit2.http.GET
@@ -50,4 +59,34 @@ interface CallHttpService {
      */
     @GET("room/call/queryMerchantFreeCall")
     suspend fun queryMerchantFreeCall(@Query("uid") uid: Long, @Query("peerUid") peerUid: Long): Rlt<Res<MerchantFreeCallData>>
+
+    /**
+     * 1v1通话模式切换
+     */
+    @POST("room/call/switchCallMode")
+    suspend fun switchCallMode(@Body req: SwitchCallModeRequest): Rlt<Res<SwitchCallModeResponse>>
+
+    /**
+     * 1v1通话模式切换取消
+     */
+    @POST("room/call/switchCallModeCancel")
+    suspend fun cancelSwitchCallMode(@Body req: SwitchCallModeCancelRequest): Rlt<Res<SwitchCallModeCancelResponse>>
+
+    /**
+     * 1v1通话模式切换答复
+     */
+    @POST("room/call/switchCallModeAnswer")
+    suspend fun answerSwitchCallMode(@Body req: SwitchCallModeAnswer): Rlt<Res<SwitchCallModeAnswerResponse>>
+
+    /**
+     * 匹配入口查询
+     */
+    @POST("room/call/callMatchShow")
+    suspend fun getCallMatchEntrance(@Body req: CommonRequest): Rlt<Res<CallMatchShowResponse>>
+
+    /**
+     * 1v1通话匹配用户头像列表
+     */
+    @POST("room/call/callMatchUserAvatarList")
+    suspend fun getMatchUserList(@Body req: CommonRequest): Rlt<Res<CallMatchUserListResponse>>
 }

+ 249 - 83
module/call/src/main/java/com/adealink/weparty/call/manager/CallManager.kt

@@ -14,19 +14,28 @@ import com.adealink.frame.media.MediaInfo
 import com.adealink.frame.network.ISocketNotify
 import com.adealink.frame.router.Router
 import com.adealink.frame.util.AppUtil
+import com.adealink.frame.util.runOnUiThread
 import com.adealink.weparty.App
 import com.adealink.weparty.call.R
 import com.adealink.weparty.call.WenextUICallKitImpl
 import com.adealink.weparty.call.constant.CALL_ERROR_CALL_FAIL_FOR_CALLING
 import com.adealink.weparty.call.constant.CALL_ERROR_CALL_YOURSELF
+import com.adealink.weparty.call.constant.CALL_ERROR_IN_MATCH_MODE_FAIL
 import com.adealink.weparty.call.constant.CALL_ERROR_MEDIA_CONFLICT_CANCEL
 import com.adealink.weparty.call.constant.TAG_CALL_FLOW
+import com.adealink.weparty.call.constant.TAG_CALL_MATCH_MODE
+import com.adealink.weparty.call.data.CallEndStatsNotify
 import com.adealink.weparty.call.data.CallReq
 import com.adealink.weparty.call.data.CallUserData
+import com.adealink.weparty.call.data.SwitchCallModeAsk
+import com.adealink.weparty.call.data.SwitchCallModeResultNotify
+import com.adealink.weparty.call.data.SwitchResult
 import com.adealink.weparty.call.data.UserAuditNotify
 import com.adealink.weparty.call.data.UserCurrencyUpdateNotify
 import com.adealink.weparty.call.datasource.local.CallLocalService
 import com.adealink.weparty.call.datasource.remote.CallHttpService
+import com.adealink.weparty.call.match.CallMatchActivity
+import com.adealink.weparty.call.match.matchManager
 import com.adealink.weparty.call.util.debugToast
 import com.adealink.weparty.call.util.getRoomId
 import com.adealink.weparty.commonui.toast.util.getFailedMsg
@@ -93,6 +102,72 @@ class CallManager : BaseFrame<ICallListener>(), ICallManager {
         }
     }
 
+    private val switchCallModeNotify = object : ISocketNotify<SwitchCallModeAsk> {
+        override val uri: String = "SWITCH_CALL_MODE_NOTIFY"
+
+        override fun onNotify(data: SwitchCallModeAsk) {
+            dispatch {
+                it.handleVideoInvitation(data)
+            }
+        }
+
+        override fun needHandle(data: SwitchCallModeAsk?): Boolean {
+            data ?: return false
+            val currentRoomId = TUICallState.instance.roomId.get()?.getRoomId() ?: return false
+            return data.roomId == currentRoomId
+        }
+    }
+
+    private val switchCallModeResultNotify = object : ISocketNotify<SwitchCallModeResultNotify> {
+        override val uri: String = "SWITCH_CALL_MODE_RESULT_NOTIFY"
+
+        override fun onNotify(data: SwitchCallModeResultNotify) {
+            Log.d(TAG_CALL_MATCH_MODE, "SwitchCallModeResultNotify: $data")
+            when(data.switchResult) {
+                SwitchResult.SUCCESS.value -> {
+                    TUICallState.instance.matchModeOpenCamera.set(true)
+                }
+                SwitchResult.TIMEOUT.value -> {
+                    TUICallState.instance.matchModeOpenCamera.set(false)
+                    showToast(getCompatString(R.string.call_change_video_timeout))
+                }
+                SwitchResult.REFUSE.value -> {
+                    TUICallState.instance.matchModeOpenCamera.set(false)
+                    showToast(getCompatString(R.string.call_change_video_decline))
+                }
+            }
+        }
+
+        override fun needHandle(data: SwitchCallModeResultNotify?): Boolean {
+            data ?: return false
+            val currentRoomId = TUICallState.instance.roomId.get()?.getRoomId() ?: return false
+            return data.roomId == currentRoomId
+        }
+    }
+
+    private val callEndStatsNotify = object : ISocketNotify<CallEndStatsNotify> {
+        override val uri: String = "CALL_END_STATS_NOTIFY"
+
+        override fun onNotify(data: CallEndStatsNotify) {
+            Log.d(TAG_CALL_MATCH_MODE, "CallEndStatsNotify: $data")
+            //直接弹窗
+            val activity = AppUtil.currentActivity ?: return
+            if (activity is CallMatchActivity) {
+                return
+            }
+            runOnUiThread {
+                Router.build(AppUtil.appContext, Call.CallSettlement.PATH)
+                    .putExtra(Call.CallSettlement.EXTRA_DATA,data)
+                    .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                    .start()
+            }
+        }
+
+        override fun needHandle(data: CallEndStatsNotify?): Boolean {
+            return data != null
+        }
+    }
+
     private val userAuditNotify = object : ISocketNotify<UserAuditNotify> {
 
         override val uri: String = "URI_CALL_1V1_AUDIT_NOTIFY"
@@ -146,7 +221,11 @@ class CallManager : BaseFrame<ICallListener>(), ICallManager {
             TUICallDefine.Status.None,
             TUICallDefine.Status.Waiting,
             null -> {
-                App.instance.hardwareManager.release(HardwareClientId.CALL_1V1.id, Hardware.CAMERA, Hardware.MIC)
+                App.instance.hardwareManager.release(
+                    HardwareClientId.CALL_1V1.id,
+                    Hardware.CAMERA,
+                    Hardware.MIC
+                )
             }
         }
     }
@@ -158,11 +237,20 @@ class CallManager : BaseFrame<ICallListener>(), ICallManager {
     override fun onCallBegin() {
         App.instance.networkService.subscribeNotify(userCurrencyUpdateNotify)
         App.instance.networkService.subscribeNotify(userAuditNotify)
+        App.instance.networkService.subscribeNotify(switchCallModeNotify)
+        App.instance.networkService.subscribeNotify(switchCallModeResultNotify)
+        App.instance.networkService.subscribeNotify(callEndStatsNotify)
     }
 
     override fun onCallEnd() {
         App.instance.networkService.unSubscribeNotify(userCurrencyUpdateNotify)
         App.instance.networkService.unSubscribeNotify(userAuditNotify)
+        App.instance.networkService.unSubscribeNotify(switchCallModeNotify)
+        App.instance.networkService.unSubscribeNotify(switchCallModeResultNotify)
+    }
+
+    override fun clear() {
+        App.instance.networkService.unSubscribeNotify(callEndStatsNotify)
     }
 
     override fun getRoomId(): Long? {
@@ -171,13 +259,21 @@ class CallManager : BaseFrame<ICallListener>(), ICallManager {
 
     /**
      * -. 检查当前是否正在通话
+     * -. 检查是否处于匹配状态
      * -. 检查当前通话信息
      * -. 检查媒体冲突
      * -. 检查/申请权限
      * -. 业务侧发起通话
      * -. 最后回调到EngineManager呼叫
      */
-    override fun call(targetUid: Long, mediaType: TUICallDefine.MediaType, source: CallerSource?, callback: TUICommonDefine.Callback?, startFullScreenView: Boolean) {
+    override fun call(
+        targetUid: Long,
+        mediaType: TUICallDefine.MediaType,
+        source: CallerSource?,
+        callback: TUICommonDefine.Callback?,
+        startFullScreenView: Boolean,
+        matchId: Long?
+    ) {
         launch {
             val callActionCallback = object : TUICommonDefine.Callback {
                 override fun onSuccess() {
@@ -193,6 +289,8 @@ class CallManager : BaseFrame<ICallListener>(), ICallManager {
 
                 override fun onError(errCode: Int, errMsg: String?) {
                     callback?.onError(errCode, errMsg)
+                    //呼叫失败,清除匹配数据
+                    matchManager.clearData()
                     //处理媒体冲突不需要提示
                     if (errCode != CALL_ERROR_MEDIA_CONFLICT_CANCEL) {
                         if (errCode == ServerCode.CURRENCY_NOT_ENOUGH.code) {
@@ -202,7 +300,8 @@ class CallManager : BaseFrame<ICallListener>(), ICallManager {
                         showToast(
                             errMsg ?: getCompatString(
                                 com.adealink.weparty.R.string.commonui_error_msg,
-                                errMsg ?: getCompatString(com.adealink.weparty.R.string.common_failed),
+                                errMsg
+                                    ?: getCompatString(com.adealink.weparty.R.string.common_failed),
                                 errCode
                             )
                         )
@@ -211,39 +310,59 @@ class CallManager : BaseFrame<ICallListener>(), ICallManager {
             }
 
             if (TUICallState.instance.selfUser.get().isCalling()) {
-                callActionCallback.onError(CALL_ERROR_CALL_FAIL_FOR_CALLING, getCompatString(R.string.call_fail_for_calling_now))
+                callActionCallback.onError(
+                    CALL_ERROR_CALL_FAIL_FOR_CALLING,
+                    getCompatString(R.string.call_fail_for_calling_now)
+                )
                 return@launch
             }
 
-            if (targetUid == ProfileModule.getMyUid()) {
-                //不能呼叫自己
-                callActionCallback.onError(CALL_ERROR_CALL_YOURSELF, getCompatString(R.string.call_target_uid_is_self))
+            if (matchId == null && matchManager.isMatchIdValid()) {
+                callActionCallback.onError(
+                    CALL_ERROR_IN_MATCH_MODE_FAIL,
+                    getCompatString(R.string.call_fail_for_match_mode)
+                )
                 return@launch
             }
 
-            //房间媒体冲突处理失败,进房失败
-            val mediaConflictRlt = App.instance.mediaManager.conflictHandle(
-                MediaInfo(
-                    MediaType.CALL_1V1.type,
-                    if (mediaType == TUICallDefine.MediaType.Audio) {
-                        getCompatString(R.string.call_chat)
-                    } else {
-                        getCompatString(R.string.call_video)
-                    }
+            if (targetUid == ProfileModule.getMyUid()) {
+                //不能呼叫自己
+                callActionCallback.onError(
+                    CALL_ERROR_CALL_YOURSELF,
+                    getCompatString(R.string.call_target_uid_is_self)
                 )
-            )
-            if (mediaConflictRlt is Rlt.Failed) {
-                Log.e(TAG_CALL_FLOW, "call fail, for media conflict fail")
-                callActionCallback.onError(CALL_ERROR_MEDIA_CONFLICT_CANCEL, null)
                 return@launch
             }
 
-            //检查/申请权限
-            val permissionRlt = requestPermission(true, mediaType)
-            if (permissionRlt is Rlt.Failed) {
-                Log.e(TAG_CALL_FLOW, "call fail, for permission denied")
-                callActionCallback.onError(TUICallDefine.ERROR_PERMISSION_DENIED, getCompatString(R.string.call_permission_grant_fail))
-                return@launch
+            //有matchId的时候,不需要再检查一下
+            if (matchId ==null){
+                //房间媒体冲突处理失败,进房失败
+                val mediaConflictRlt = App.instance.mediaManager.conflictHandle(
+                    MediaInfo(
+                        MediaType.CALL_1V1.type,
+                        if (mediaType == TUICallDefine.MediaType.Audio) {
+                            getCompatString(R.string.call_chat)
+                        } else {
+                            getCompatString(R.string.call_video)
+                        }
+                    )
+                )
+                if (mediaConflictRlt is Rlt.Failed) {
+                    Log.e(TAG_CALL_FLOW, "call fail, for media conflict fail")
+                    callActionCallback.onError(CALL_ERROR_MEDIA_CONFLICT_CANCEL, null)
+                    return@launch
+                }
+
+                //检查/申请权限
+                val permissionRlt = requestPermission(true, mediaType)
+                if (permissionRlt is Rlt.Failed) {
+                    Log.e(TAG_CALL_FLOW, "call fail, for permission denied")
+                    callActionCallback.onError(
+                        TUICallDefine.ERROR_PERMISSION_DENIED,
+                        getCompatString(R.string.call_permission_grant_fail)
+                    )
+                    return@launch
+                }
             }
 
             val startCallRlt = startCall(
@@ -252,22 +371,34 @@ class CallManager : BaseFrame<ICallListener>(), ICallManager {
                 targetUid,
                 mediaType,
                 source == CallerSource.INVITE,
-                null
+                null,
+                matchId
             )
 
             if (startCallRlt is Rlt.Failed) {
-                callActionCallback.onError(startCallRlt.error.serverCode, getFailedMsg(startCallRlt.error))
+                callActionCallback.onError(
+                    startCallRlt.error.serverCode,
+                    getFailedMsg(startCallRlt.error)
+                )
                 return@launch
             }
             val callRoomId = (startCallRlt as Rlt.Success).data
             if (callRoomId.isNullOrEmpty()) {
                 Log.e(TAG_CALL_FLOW, "call fail, for callRoomId is null")
-                callActionCallback.onError(CALL_ERROR_START_CALL_SERVER_ERROR, getCompatString(R.string.call_start_call_fail_room_id_invalid))
+                callActionCallback.onError(
+                    CALL_ERROR_START_CALL_SERVER_ERROR,
+                    getCompatString(R.string.call_start_call_fail_room_id_invalid)
+                )
                 return@launch
             }
 
-            val callParams = createCallParams(source, callRoomId)
-            EngineManager.instance.calls(listOf(targetUid.toString()), mediaType, callParams, callActionCallback)
+            val callParams = createCallParams(source, callRoomId, matchId)
+            EngineManager.instance.calls(
+                listOf(targetUid.toString()),
+                mediaType,
+                callParams,
+                callActionCallback
+            )
         }
     }
 
@@ -288,13 +419,14 @@ class CallManager : BaseFrame<ICallListener>(), ICallManager {
                             .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                             .start()
                     }
-                    if (TUICallState.instance.mediaType.get() == TUICallDefine.MediaType.Video) {
+                    if (TUICallState.instance.mediaType.get() == TUICallDefine.MediaType.Video && !matchManager.isMatchIdValid()) {
                         val videoView = VideoViewFactory.instance.createVideoView(
                             TUICallState.instance.selfUser.get(), AppUtil.appContext
                         )
-
                         EngineManager.instance.openCamera(
-                            TUICallState.instance.isFrontCamera.get(), videoView?.getVideoView(), null
+                            TUICallState.instance.isFrontCamera.get(),
+                            videoView?.getVideoView(),
+                            null
                         )
                     }
                     callback?.onSuccess()
@@ -308,7 +440,8 @@ class CallManager : BaseFrame<ICallListener>(), ICallManager {
                         showToast(
                             errMsg ?: getCompatString(
                                 com.adealink.weparty.R.string.commonui_error_msg,
-                                errMsg ?: getCompatString(com.adealink.weparty.R.string.common_failed),
+                                errMsg
+                                    ?: getCompatString(com.adealink.weparty.R.string.common_failed),
                                 errCode
                             )
                         )
@@ -321,7 +454,10 @@ class CallManager : BaseFrame<ICallListener>(), ICallManager {
             val mediaType = TUICallState.instance.mediaType.get()
             if (mediaType == null || mediaType == TUICallDefine.MediaType.Unknown) {
                 Log.e(TAG_CALL_FLOW, "accept fail, mediaType($mediaType) is invalid")
-                acceptCallback.onError(TUICallDefine.ERROR_PARAM_INVALID, getCompatString(R.string.call_media_type_unknown))
+                acceptCallback.onError(
+                    TUICallDefine.ERROR_PARAM_INVALID,
+                    getCompatString(R.string.call_media_type_unknown)
+                )
                 return@launch
             }
 
@@ -329,36 +465,48 @@ class CallManager : BaseFrame<ICallListener>(), ICallManager {
             val selfUid = TUICallState.instance.getSelfUid()
             val remoteUid = TUICallState.instance.getRemoteUid()
             if ((selfUid == null || !selfUid.isUidValid()) || (remoteUid == null || !remoteUid.isUidValid())) {
-                Log.e(TAG_CALL_FLOW, "accept fail, callerUid($selfUid)/calledUid($remoteUid) is invalid.")
-                acceptCallback.onError(TUICallDefine.ERROR_PARAM_INVALID, getCompatString(R.string.call_target_uid_is_invalid))
+                Log.e(
+                    TAG_CALL_FLOW,
+                    "accept fail, callerUid($selfUid)/calledUid($remoteUid) is invalid."
+                )
+                acceptCallback.onError(
+                    TUICallDefine.ERROR_PARAM_INVALID,
+                    getCompatString(R.string.call_target_uid_is_invalid)
+                )
                 return@launch
             }
             val callerUid = if (isSelfCaller) selfUid else remoteUid
             val calledUid = if (isSelfCaller) remoteUid else selfUid
             val callUserData = TUICallState.instance.callUserData.get()
 
-            //房间媒体冲突处理失败,进房失败
-            val conflictRlt = App.instance.mediaManager.conflictHandle(
-                MediaInfo(
-                    MediaType.CALL_1V1.type,
-                    if (mediaType == TUICallDefine.MediaType.Audio) {
-                        getCompatString(R.string.call_chat)
-                    } else {
-                        getCompatString(R.string.call_video)
-                    }
+            if (callUserData.matchId == null) {
+                //匹配模式下不检查
+                //房间媒体冲突处理失败,进房失败
+                val conflictRlt = App.instance.mediaManager.conflictHandle(
+                    MediaInfo(
+                        MediaType.CALL_1V1.type,
+                        if (mediaType == TUICallDefine.MediaType.Audio) {
+                            getCompatString(R.string.call_chat)
+                        } else {
+                            getCompatString(R.string.call_video)
+                        }
+                    )
                 )
-            )
-            if (conflictRlt is Rlt.Failed) {
-                Log.e(TAG_CALL_FLOW, "accept fail, for media conflict fail")
-                acceptCallback.onError(CALL_ERROR_MEDIA_CONFLICT_CANCEL, "")
-                return@launch
+                if (conflictRlt is Rlt.Failed) {
+                    Log.e(TAG_CALL_FLOW, "accept fail, for media conflict fail")
+                    acceptCallback.onError(CALL_ERROR_MEDIA_CONFLICT_CANCEL, "")
+                    return@launch
+                }
             }
 
             //检查/申请权限
             val permissionRlt = requestPermission(false, mediaType)
             if (permissionRlt is Rlt.Failed) {
                 Log.e(TAG_CALL_FLOW, "accept fail, permission denied")
-                acceptCallback.onError(TUICallDefine.ERROR_PERMISSION_DENIED, getCompatString(R.string.call_permission_grant_fail))
+                acceptCallback.onError(
+                    TUICallDefine.ERROR_PERMISSION_DENIED,
+                    getCompatString(R.string.call_permission_grant_fail)
+                )
                 return@launch
             }
 
@@ -369,7 +517,8 @@ class CallManager : BaseFrame<ICallListener>(), ICallManager {
                 calledUid,
                 mediaType,
                 callUserData.isInviteSource(),
-                callUserData.callRoomId
+                callUserData.callRoomId,
+                callUserData.matchId
             )
             if (startCallRlt is Rlt.Failed) {
                 acceptCallback.onError(startCallRlt.error.serverCode, startCallRlt.error.msg)
@@ -396,7 +545,8 @@ class CallManager : BaseFrame<ICallListener>(), ICallManager {
     override fun ignore(callback: TUICommonDefine.Callback?) {
         launch {
             TUICallState.instance.clear()
-            WenextUICallKitImpl.createInstance(AppUtil.appContext).getCallingBellFeature()?.stopAndClear()
+            WenextUICallKitImpl.createInstance(AppUtil.appContext).getCallingBellFeature()
+                ?.stopAndClear()
             callback?.onSuccess()
         }
     }
@@ -407,43 +557,54 @@ class CallManager : BaseFrame<ICallListener>(), ICallManager {
         }
     }
 
-    override suspend fun requestPermission(isCaller: Boolean, mediaType: TUICallDefine.MediaType): Rlt<Any> {
+    override suspend fun requestPermission(
+        isCaller: Boolean,
+        mediaType: TUICallDefine.MediaType
+    ): Rlt<Any> {
         return withContext(Dispatcher.UI) {
             suspendCancellableCoroutine { continuation ->
-                PermissionRequest.requestPermissions(AppUtil.appContext, mediaType, object : PermissionCallback() {
-                    override fun onGranted() {
-                        //主叫直接返回结果
-                        if (isCaller) {
-                            if (continuation.isActive) {
-                                continuation.resume(Rlt.Success(Any()), null)
+                PermissionRequest.requestPermissions(
+                    AppUtil.appContext,
+                    mediaType,
+                    object : PermissionCallback() {
+                        override fun onGranted() {
+                            //主叫直接返回结果
+                            if (isCaller) {
+                                if (continuation.isActive) {
+                                    continuation.resume(Rlt.Success(Any()), null)
+                                }
+                                return
+                            }
+                            //被叫需要检查当前呼叫状态
+                            if (TUICallState.instance.selfUser.get().callStatus.get() == TUICallDefine.Status.None) {
+                                if (continuation.isActive) {
+                                    continuation.resume(Rlt.Failed(IError()), null)
+                                }
+                            } else {
+                                if (continuation.isActive) {
+                                    continuation.resume(Rlt.Success(Any()), null)
+                                }
                             }
-                            return
                         }
-                        //被叫需要检查当前呼叫状态
-                        if (TUICallState.instance.selfUser.get().callStatus.get() == TUICallDefine.Status.None) {
+
+                        override fun onDenied() {
                             if (continuation.isActive) {
                                 continuation.resume(Rlt.Failed(IError()), null)
                             }
-                        } else {
-                            if (continuation.isActive) {
-                                continuation.resume(Rlt.Success(Any()), null)
-                            }
                         }
-                    }
-
-                    override fun onDenied() {
-                        if (continuation.isActive) {
-                            continuation.resume(Rlt.Failed(IError()), null)
-                        }
-                    }
-                })
+                    })
             }
         }
     }
 
     private suspend fun startCall(
         isCaller: Boolean,
-        callerUid: Long, calledUid: Long, mediaType: TUICallDefine.MediaType, isInvite: Boolean, callRoomId: String?
+        callerUid: Long,
+        calledUid: Long,
+        mediaType: TUICallDefine.MediaType,
+        isInvite: Boolean,
+        callRoomId: String?,
+        matchId: Long?
     ): Rlt<String?> {
         val startCallRlt = callHttpService.startCall(
             CallReq(
@@ -451,7 +612,8 @@ class CallManager : BaseFrame<ICallListener>(), ICallManager {
                 calledUid,
                 mediaType.value,
                 isInvite,
-                callRoomId
+                callRoomId,
+                matchId
             )
         )
         when (startCallRlt) {
@@ -480,7 +642,8 @@ class CallManager : BaseFrame<ICallListener>(), ICallManager {
 
     private suspend fun createCallParams(
         source: CallerSource?,
-        callRoomId: String
+        callRoomId: String,
+        matchId: Long?,
     ): CallParams {
         return withContext(this.coroutineContext) {
             val callParams = CallParams()
@@ -488,10 +651,13 @@ class CallManager : BaseFrame<ICallListener>(), ICallManager {
             callParams.timeout = SettingsConfig.callTimeOut
             //用户数据( 房间ID,通话来源(计费) )
             callParams.userData = toJsonErrorNull(
-                CallUserData(callRoomId, source?.source ?: CallerSource.CALLER.source)
+                CallUserData(callRoomId, source?.source ?: CallerSource.CALLER.source, matchId)
             )
             if (!SettingsConfig.offlineParams.isNullOrEmpty()) {
-                callParams.offlinePushInfo = froJsonErrorNull(SettingsConfig.offlineParams, TUICallDefine.OfflinePushInfo::class.java)
+                callParams.offlinePushInfo = froJsonErrorNull(
+                    SettingsConfig.offlineParams,
+                    TUICallDefine.OfflinePushInfo::class.java
+                )
             }
             callParams.roomId = TUICommonDefine.RoomId().apply {
 //                intRoomId = callRoomId.safeToInt()
@@ -508,7 +674,7 @@ class CallManager : BaseFrame<ICallListener>(), ICallManager {
     private fun handleUserCurrencyUpdate(notify: UserCurrencyUpdateNotify) {
         launch {
             val selfUser = TUICallState.instance.selfUser.get() ?: return@launch
-            if(notify.isMerchant) {
+            if (notify.isMerchant) {
                 selfUser.refreshMerchantFreeCallLD.set(true)
                 return@launch
             }

+ 20 - 0
module/call/src/main/java/com/adealink/weparty/call/manager/CallPingManager.kt

@@ -16,10 +16,12 @@ import com.adealink.weparty.call.data.CallBeginReq
 import com.adealink.weparty.call.data.CallPingReq
 import com.adealink.weparty.call.datasource.local.CallLocalService.callPingInterval
 import com.adealink.weparty.call.datasource.remote.CallHttpService
+import com.adealink.weparty.call.match.matchManager
 import com.adealink.weparty.call.util.debugToast
 import com.adealink.weparty.call.util.getRoomId
 import com.adealink.weparty.module.account.AccountModule
 import com.adealink.weparty.module.account.ILoginListener
+import com.adealink.weparty.module.call.data.CallType
 import com.tencent.cloud.tuikit.engine.call.TUICallDefine
 import com.tencent.cloud.tuikit.engine.common.TUICommonDefine
 import com.tencent.qcloud.tuikit.tuicallkit.manager.EngineManager
@@ -178,6 +180,24 @@ class CallPingManager :
                 is Rlt.Success -> {
                     val isCalling = result.data.data?.isCalling() ?: false
                     val currentCallRoomId = TUICallState.instance.roomId.get()?.getRoomId()
+
+                    //校正匹配模式通话状态
+                    val res = result.data.data
+                    val matchModeOpenCamera =  TUICallState.instance.matchModeOpenCamera.get()
+                    if(matchManager.isMatchIdValid() && res?.matchId == matchManager.getMatchId()) {
+                        when(res.callMode) {
+                            CallType.Video.type -> {
+                                if(!matchModeOpenCamera) {
+                                    TUICallState.instance.matchModeOpenCamera.set(true)
+                                }
+                            }
+                            CallType.Voice.type -> {
+                                if(matchModeOpenCamera) {
+                                    TUICallState.instance.matchModeOpenCamera.set(false)
+                                }
+                            }
+                        }
+                    }
                     if (!isCalling && !currentCallRoomId.isNullOrEmpty()) {
                         //服务端掉线,客户端发起重新进房
                         Log.d(TAG_CALL_PING, "ping res, client offline, hangup")

+ 2 - 0
module/call/src/main/java/com/adealink/weparty/call/manager/ICallListener.kt

@@ -3,10 +3,12 @@ package com.adealink.weparty.call.manager
 import com.adealink.frame.base.Rlt
 import com.adealink.frame.frame.IListener
 import com.adealink.frame.network.data.Res
+import com.adealink.weparty.call.data.SwitchCallModeAsk
 import com.adealink.weparty.module.backpack.GetExperienceCardRes
 
 interface ICallListener : IListener {
 
     fun onExperienceCardChanged(res: Rlt.Success<Res<GetExperienceCardRes>>){}
 
+    fun handleVideoInvitation(data: SwitchCallModeAsk)
 }

+ 2 - 1
module/call/src/main/java/com/adealink/weparty/call/manager/ICallManager.kt

@@ -18,7 +18,8 @@ interface ICallManager : IBaseFrame<ICallListener> {
         mediaType: TUICallDefine.MediaType,
         source: CallerSource? = CallerSource.CALLER,
         callback: TUICommonDefine.Callback?,
-        startFullScreenView: Boolean = true
+        startFullScreenView: Boolean = true,
+        matchId: Long? = null
     )
 
     /**

+ 203 - 0
module/call/src/main/java/com/adealink/weparty/call/match/CallMatchActivity.kt

@@ -0,0 +1,203 @@
+package com.adealink.weparty.call.match
+
+import android.content.Intent
+import android.os.Bundle
+import android.text.SpannableStringBuilder
+import androidx.activity.viewModels
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.view.updateLayoutParams
+import com.adealink.frame.aab.util.getCompatColor
+import com.adealink.frame.aab.util.getCompatDrawable
+import com.adealink.frame.aab.util.getCompatString
+import com.adealink.frame.base.Rlt
+import com.adealink.frame.ext.findAndSetSpan
+import com.adealink.frame.log.Log
+import com.adealink.frame.mvvm.view.viewBinding
+import com.adealink.frame.oss.ossService
+import com.adealink.frame.router.Router
+import com.adealink.frame.router.annotation.BindExtra
+import com.adealink.frame.router.annotation.RouterUri
+import com.adealink.weparty.call.CallDialog.Companion.ICON_TAG
+import com.adealink.weparty.call.R
+import com.adealink.weparty.call.constant.TAG_CALL_MATCH_MODE
+import com.adealink.weparty.call.databinding.LayoutCallMatchActivityBinding
+import com.adealink.weparty.call.datasource.local.CallLocalService
+import com.adealink.weparty.call.viewmodel.CallViewModel
+import com.adealink.weparty.commonui.BaseActivity
+import com.adealink.weparty.commonui.ext.dp
+import com.adealink.weparty.commonui.ext.gone
+import com.adealink.weparty.commonui.ext.show
+import com.adealink.weparty.commonui.widget.CenterImageSpan
+import com.adealink.weparty.effect.AnimExtraConfig
+import com.adealink.weparty.effect.EffectAnimType
+import com.adealink.weparty.effect.SVGAExtraConfig
+import com.adealink.weparty.match.MatchStateEnum
+import com.adealink.weparty.match.MatchStateMachine
+import com.adealink.weparty.module.call.Call
+import com.opensource.svgaplayer.SVGADynamicEntity
+import com.qmuiteam.qmui.widget.util.QMUIStatusBarHelper
+import com.adealink.weparty.R as APP_R
+
+/**
+ * author: PengWuliang
+ * date: 2025/8/15 17:13
+ * desc:
+ */
+@RouterUri(
+    path = [Call.CallMatch.PATH],
+    desc = "1v1 Call Match"
+)
+class CallMatchActivity : BaseActivity() {
+
+    companion object {
+        private val MATCH_BEFORE_ANIM= ossService.getUrlByPath("/big_img/match/match_before_anim.mp4")
+        private val MATCHING_ANIM= ossService.getUrlByPath("/big_img/match/matching_anim.mp4")
+    }
+
+    @BindExtra(Call.CallMatch.EXTRA_AUTO_MATCH)
+    var autoMatch: Boolean = false
+
+    override fun onBeforeCreate() {
+        super.onBeforeCreate()
+        Router.bind(this)
+    }
+
+    override fun handleNewIntent(intent: Intent?) {
+        super.handleNewIntent(intent)
+        this@CallMatchActivity.intent = intent
+        Router.bind(this)
+        if (autoMatch) {
+            Log.d(TAG_CALL_MATCH_MODE, "autoMatch")
+            matchManager.startMatch()
+        }
+    }
+
+    private val binding by viewBinding(LayoutCallMatchActivityBinding::inflate)
+    private val callViewModel by viewModels<CallViewModel>()
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(binding.root)
+        binding.tvTitle.updateLayoutParams<ConstraintLayout.LayoutParams> {
+            topMargin = QMUIStatusBarHelper.getStatusbarHeight(this@CallMatchActivity)
+        }
+        binding.tvCoinCost.text = chatCostTips()
+        binding.ivBack.setOnClickListener {
+            finish()
+        }
+        binding.ivMinimize.setOnClickListener {
+            finish()
+        }
+        MatchStateMachine.instance.currentState.observe(this) {
+            when (it) {
+                MatchStateEnum.IDLE -> {
+                    binding.animBeforeMatch.show()
+                    binding.animBeforeMatchAvatar.show()
+                    if (binding.animBeforeMatch.isResourceReady() && binding.animBeforeMatchAvatar.isResourceReady()) {
+                        binding.animBeforeMatch.startPlay()
+                        binding.animBeforeMatchAvatar.startPlay()
+                    } else {
+                        setupBeforeMatchAnim()
+                    }
+                    binding.animMatching.gone()
+                    binding.animMatching.pausePlay()
+                    binding.btnMatch.apply {
+                        text = getCompatString(R.string.call_match_start)
+                        setBackgroundResource(R.drawable.call_match_btn_bg)
+                        setTextColor(getCompatColor(APP_R.color.white))
+                        setOnClickListener {
+                            matchManager.startMatch()
+                        }
+                    }
+                    binding.tvMatchTips.gone()
+                    binding.ivMinimize.gone()
+                }
+
+                MatchStateEnum.MATCHING -> {
+                    binding.animMatching.show()
+                    binding.animMatching.setUrl(
+                        MATCHING_ANIM, EffectAnimType.TC, AnimExtraConfig(
+                            loop = -1,
+                            autoPlay = true
+                        )
+                    )
+                    binding.animBeforeMatch.gone()
+                    binding.animBeforeMatch.pausePlay()
+                    binding.animBeforeMatchAvatar.gone()
+                    binding.animBeforeMatchAvatar.pausePlay()
+                    binding.btnMatch.apply {
+                        text = getCompatString(APP_R.string.commonui_cancel)
+                        setBackgroundResource(APP_R.drawable.common_white_radius_25_bg)
+                        setTextColor(getCompatColor(APP_R.color.color_222222))
+                        setOnClickListener {
+                            matchManager.cancelMatch()
+                        }
+                    }
+                    binding.tvMatchTips.show()
+                    binding.ivMinimize.show()
+                }
+
+                else -> {
+                    finish()
+                }
+            }
+        }
+        if (autoMatch) {
+            Log.d(TAG_CALL_MATCH_MODE, "autoMatch")
+            matchManager.startMatch()
+        }
+    }
+
+    private fun setupBeforeMatchAnim() {
+        callViewModel.getMatchUserList().observe(this) { result ->
+            when (result) {
+                is Rlt.Success -> {
+                    binding.animBeforeMatchAvatar.setAsset(
+                        "match_avatar.svga", EffectAnimType.SVGA, AnimExtraConfig(
+                            loop = -1,
+                            autoPlay = true,
+                            svgaExtraConfig = SVGAExtraConfig(
+                                dynamicEntitySupplier = {
+                                    val dynamicEntity = SVGADynamicEntity()
+                                    dynamicEntity.setDynamicImage(result.data.getOrNull(0)?.url ?: "", "text1")
+                                    dynamicEntity.setDynamicImage(result.data.getOrNull(1)?.url ?: "", "text2")
+                                    dynamicEntity.setDynamicImage(result.data.getOrNull(2)?.url ?: "", "text3")
+                                    dynamicEntity.setDynamicImage(result.data.getOrNull(3)?.url ?: "", "text4")
+                                    dynamicEntity.setDynamicImage(result.data.getOrNull(4)?.url ?: "", "text5")
+                                    dynamicEntity
+                                }
+                            )
+                        )
+                    )
+                }
+
+                is Rlt.Failed -> {
+                    Log.e(TAG_CALL_MATCH_MODE, "getMatchUserList failed", result.error)
+                }
+            }
+        }
+
+        binding.animBeforeMatch.setUrl(
+            MATCH_BEFORE_ANIM, EffectAnimType.TC, AnimExtraConfig(
+                loop = -1,
+                autoPlay = true
+            )
+        )
+    }
+
+    private fun chatCostTips(): SpannableStringBuilder {
+        val chatCost = CallLocalService.chatCostPerMin.toString()
+        val text = getCompatString(R.string.call_video_calling_tips, chatCost)
+        return SpannableStringBuilder(text).apply {
+            val i = text.indexOf(ICON_TAG)
+            if (i >= 0) {
+                findAndSetSpan(
+                    CenterImageSpan(getCompatDrawable(com.adealink.weparty.R.drawable.common_coin_32_ic).apply {
+                        setBounds(0, 0, 16.dp(), 16.dp())
+                    }),
+                    ICON_TAG
+                )
+            }
+        }
+    }
+}

+ 52 - 0
module/call/src/main/java/com/adealink/weparty/call/match/MatchHttpService.kt

@@ -0,0 +1,52 @@
+package com.adealink.weparty.call.match
+
+import com.adealink.frame.base.Rlt
+import com.adealink.frame.network.data.Res
+import com.adealink.weparty.call.data.CallMatchCancelRequest
+import com.adealink.weparty.call.data.CallMatchCancelResponse
+import com.adealink.weparty.call.data.CallMatchReqAnswer
+import com.adealink.weparty.call.data.CallMatchReqAnswerResponse
+import com.adealink.weparty.call.data.CallMatchResponse
+import com.adealink.weparty.call.data.CallMatchResultNotifyAck
+import com.adealink.weparty.call.data.CallMatchUserListResponse
+import com.adealink.weparty.call.data.CommonRequest
+import retrofit2.http.Body
+import retrofit2.http.POST
+
+/**
+ * author: PengWuliang
+ * date: 2025/8/15 20:44
+ * desc:
+ */
+interface MatchHttpService {
+
+    /**
+     * 1v1通话匹配请求
+     */
+    @POST("room/call/callMatchRequest")
+    suspend fun startMatch(@Body req: CommonRequest): Rlt<Res<CallMatchResponse>>
+
+    /**
+     * 1v1通话取消匹配
+     */
+    @POST("room/call/callMatchCancel")
+    suspend fun cancelMatch(@Body req: CallMatchCancelRequest): Rlt<Res<CallMatchCancelResponse>>
+
+    /**
+     * 1v1通话匹配应答
+     */
+    @POST("room/call/callMatchReqAnswer")
+    suspend fun matchAnswer(@Body req: CallMatchReqAnswer): Rlt<Res<CallMatchReqAnswerResponse>>
+
+    /**
+     * 1v1通话匹配结果确认
+     */
+    @POST("room/call/callMatchResultNotifyAck")
+    suspend fun matchResultAck(@Body req: CallMatchResultNotifyAck): Rlt<Res<Any>>
+
+    /**
+     * 1v1通话匹配拉取状态请求
+     */
+    @POST("room/call/callMatchUserAvatarList")
+    suspend fun getCallMatchStatus(@Body req: CommonRequest): Rlt<Res<CallMatchUserListResponse>>
+}

+ 446 - 0
module/call/src/main/java/com/adealink/weparty/call/match/MatchManager.kt

@@ -0,0 +1,446 @@
+package com.adealink.weparty.call.match
+
+import android.content.Context
+import android.media.AudioAttributes
+import android.media.AudioManager
+import android.media.MediaPlayer
+import android.os.Build
+import android.os.Vibrator
+import androidx.fragment.app.FragmentActivity
+import com.adealink.frame.aab.util.getCompatString
+import com.adealink.frame.base.Rlt
+import com.adealink.frame.coroutine.dispatcher.Dispatcher
+import com.adealink.frame.frame.BaseFrame
+import com.adealink.frame.log.Log
+import com.adealink.frame.network.ISocketNotify
+import com.adealink.frame.util.AppUtil
+import com.adealink.weparty.App
+import com.adealink.weparty.call.R
+import com.adealink.weparty.call.constant.TAG_CALL_MATCH_MODE
+import com.adealink.weparty.call.data.CallMatchCancelRequest
+import com.adealink.weparty.call.data.CallMatchReqAnswer
+import com.adealink.weparty.call.data.CallMatchReqAsk
+import com.adealink.weparty.call.data.CallMatchResultNotify
+import com.adealink.weparty.call.data.CallMatchResultNotifyAck
+import com.adealink.weparty.call.data.CommonRequest
+import com.adealink.weparty.call.manager.callManager
+import com.adealink.weparty.call.view.floatview.matchnotify.MatchNotifyFloatData
+import com.adealink.weparty.call.view.floatview.matchnotify.MatchNotifyFloatView
+import com.adealink.weparty.commonui.toast.util.showFailedToast
+import com.adealink.weparty.commonui.toast.util.showToast
+import com.adealink.weparty.commonui.widget.CommonDialog
+import com.adealink.weparty.commonui.widget.floatview.data.MODE_APPLICATION
+import com.adealink.weparty.match.AnswerType
+import com.adealink.weparty.match.MatchEvent
+import com.adealink.weparty.match.MatchResult
+import com.adealink.weparty.match.MatchStateMachine
+import com.adealink.weparty.match.MatchTimeoutCallback
+import com.adealink.weparty.module.call.data.CallType
+import com.adealink.weparty.module.call.data.CallerSource
+import com.adealink.weparty.module.call.match.IMatchListener
+import com.adealink.weparty.module.call.match.IMatchManager
+import com.adealink.weparty.module.network.data.ServerCode
+import com.adealink.weparty.module.profile.ProfileModule
+import com.adealink.weparty.module.wallet.WalletModule
+import com.adealink.weparty.module.wallet.data.Currency
+import com.tencent.cloud.tuikit.engine.call.TUICallDefine
+import com.tencent.qcloud.tuikit.tuicallkit.state.TUICallState
+import com.tencent.qcloud.tuikit.tuicallkit.utils.PermissionRequest
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+
+/**
+ * author: PengWuliang
+ * date: 2025/8/15 20:35
+ * desc:
+ * 采用悲观更新的方式
+ * 先检查能否更新状态,服务端返回再更新状态
+ */
+
+val matchManager: IMatchManager by lazy { MatchManager() }
+
+class MatchManager : BaseFrame<IMatchListener>(), IMatchManager {
+
+    private var matchId = 0L
+    private var roomId = 0L
+    private var callMode = CallType.Voice.type
+    private var matchNotifyFloatView: MatchNotifyFloatView? = null
+
+    private val stateMachine = MatchStateMachine.instance
+    private val matchHttpService by lazy {
+        App.instance.networkService.getHttpService(MatchHttpService::class.java)
+    }
+    private val vibrator: Vibrator? by lazy {
+        AppUtil.getSystemService<Vibrator>(Context.VIBRATOR_SERVICE)
+    }
+    private var mediaPlayer: MediaPlayer? = null
+
+    /**
+     * 匹配结果通知
+     */
+    private
+    val callMatchResultNotify = object : ISocketNotify<CallMatchResultNotify> {
+        override val uri: String = "CALL_MATCH_RESULT_NOTIFY"
+
+        override fun onNotify(data: CallMatchResultNotify) {
+            Log.i(TAG_CALL_MATCH_MODE, "CallMatchResultNotify onNotify: $data")
+            if (data.matchId == matchId && data.matchResult == MatchResult.SUCCESS.value) {
+                if (data.fromUid == ProfileModule.getMyUid()) {
+                    stateMachine.handleEvent(MatchEvent.MATCH_SUCCESS)
+                    callMode = data.callMode
+                    callManager.call(
+                        data.peerUid,
+                        TUICallDefine.MediaType.Video,
+                        CallerSource.CALLER,
+                        null,
+                        true,
+                        matchId
+                    )
+                } else if (data.peerUid != ProfileModule.getMyUid()) {
+                    //matchResult为1,说明fromUid和peerUid匹配成功,但不一定是自己
+                    clearData()
+                    stopVibrateAndRing()
+                    matchNotifyFloatView?.cancelNotifyView()
+                    matchNotifyFloatView = null
+                    showToast(getCompatString(R.string.call_match_failed))
+                }else{
+                    //走accept的回包
+                }
+            } else {
+                clearData()
+                stopVibrateAndRing()
+                matchNotifyFloatView?.cancelNotifyView()
+                matchNotifyFloatView = null
+                showToast(getCompatString(R.string.call_match_failed))
+            }
+        }
+
+        override fun needHandle(data: CallMatchResultNotify?): Boolean {
+            return data?.matchId == matchId
+        }
+    }
+
+    private val callMatchReqAskNotify = object : ISocketNotify<CallMatchReqAsk> {
+        override val uri: String = "CALL_MATCH_REQ_ASK"
+
+        override fun onNotify(data: CallMatchReqAsk) {
+            Log.i(TAG_CALL_MATCH_MODE, "CallMatchReqAsk onNotify: $data, matchId: $matchId")
+            if (matchId != 0L) {
+                Log.i(TAG_CALL_MATCH_MODE, "Receive CallMatchReqAsk when matching, data: $data")
+                return
+            }
+            matchId = data.matchId
+            stateMachine.handleEvent(MatchEvent.RECEIVE_REQUEST)
+            vibrate()
+            startRing(R.raw.phone_ringing)
+            launch(Dispatcher.UI) {
+                matchNotifyFloatView =
+                    MatchNotifyFloatView(MatchNotifyFloatData(MODE_APPLICATION)).apply {
+                        showNotifyView(data.userInfo, data.matchTimeout)
+                    }
+            }
+        }
+
+        override fun needHandle(data: CallMatchReqAsk?): Boolean {
+            return data != null
+        }
+    }
+
+    override fun init() {
+        App.instance.networkService.subscribeNotify(callMatchResultNotify)
+        App.instance.networkService.subscribeNotify(callMatchReqAskNotify)
+    }
+
+    override fun startMatch() {
+        if (!stateMachine.canHandleEvent(MatchEvent.START_MATCH)) {
+            Log.i(
+                TAG_CALL_MATCH_MODE,
+                "startMatch, cannot start match in current state: ${stateMachine.getCurrentState()}"
+            )
+            return
+        }
+
+        launch {
+            val permissionRlt = PermissionRequest.requestPermission(mediaType = TUICallDefine.MediaType.Video)
+            if (permissionRlt is Rlt.Failed) {
+                Log.i(TAG_CALL_MATCH_MODE, "startMatch, request permission failed")
+                return@launch
+            }
+
+            when (val rlt = matchHttpService.startMatch(CommonRequest())) {
+                is Rlt.Success -> {
+                    Log.i(
+                        TAG_CALL_MATCH_MODE,
+                        "startMatch success, matchId: ${rlt.data.data?.matchId}"
+                    )
+                    matchId = rlt.data.data?.matchId ?: 0L
+                    stateMachine.handleEvent(MatchEvent.START_MATCH)
+                    stateMachine.setTimeoutCallback(object : MatchTimeoutCallback {
+                        override fun onMatchTimeout() {
+                            val activity = AppUtil.currentActivity as? FragmentActivity ?: return
+                            CommonDialog.Builder()
+                                .message(getCompatString(R.string.call_match_timeout_tips))
+                                .negativeText(getCompatString(R.string.call_go_chat))
+                                .onNegative {
+                                    cancelMatch()
+                                }
+                                .positiveText(getCompatString(R.string.call_continue_match))
+                                .onPositive {
+                                    startMatch()
+                                }
+                                .build()
+                                .show(activity.supportFragmentManager, "MatchTimeoutDialog")
+                        }
+                    })
+                }
+
+                is Rlt.Failed -> {
+                    Log.i(TAG_CALL_MATCH_MODE, "startMatch failed, ${rlt.error}")
+                    showFailedToast(rlt)
+                    if (rlt.error.serverCode == ServerCode.CURRENCY_NOT_ENOUGH.code) {
+                        WalletModule.goWalletRecharge(Currency.Coin)
+                        return@launch
+                    }
+                }
+            }
+        }
+    }
+
+    override fun cancelMatch() {
+        if (!stateMachine.canHandleEvent(MatchEvent.CANCEL_MATCH)) {
+            Log.i(
+                TAG_CALL_MATCH_MODE,
+                "cancelMatch, cannot cancel match in current state: ${stateMachine.getCurrentState()}"
+            )
+            return
+        }
+        launch {
+            when (val rlt = matchHttpService.cancelMatch(CallMatchCancelRequest(matchId))) {
+                is Rlt.Success -> {
+                    Log.i(TAG_CALL_MATCH_MODE, "cancelMatch success")
+                    matchId = 0L
+                    stateMachine.handleEvent(MatchEvent.CANCEL_MATCH)
+                }
+
+                is Rlt.Failed -> {
+                    Log.i(TAG_CALL_MATCH_MODE, "cancelMatch failed, ${rlt.error}")
+                    matchId = 0L
+                    stateMachine.handleEvent(MatchEvent.CANCEL_MATCH)
+                }
+            }
+        }
+    }
+
+    override fun resultAck() {
+        if (!stateMachine.canHandleEvent(MatchEvent.MATCH_SUCCESS)) {
+            Log.i(
+                TAG_CALL_MATCH_MODE,
+                "resultAck, cannot ack match result in current state: ${stateMachine.getCurrentState()}"
+            )
+            return
+        }
+        launch {
+            when (val rlt = matchHttpService.matchResultAck(CallMatchResultNotifyAck(matchId))) {
+                is Rlt.Success -> {
+                    Log.i(TAG_CALL_MATCH_MODE, "resultAck success")
+                    stateMachine.handleEvent(MatchEvent.MATCH_SUCCESS)
+                    //检查通话状态,10s后没接通电话,清除数据;男性拨打电话之后,女性是Waiting或者Accept状态
+                    delay(10_000)
+                    if(TUICallState.instance.selfUser.get().callStatus.get() == TUICallDefine.Status.None) {
+                        clearData()
+                    }
+                }
+
+                is Rlt.Failed -> {
+                    Log.i(TAG_CALL_MATCH_MODE, "resultAck failed, ${rlt.error}")
+                    stateMachine.handleEvent(MatchEvent.MATCH_FAILED)
+                }
+            }
+        }
+    }
+
+    override fun accept() {
+        stopVibrateAndRing()
+        if (!stateMachine.canHandleEvent(MatchEvent.ACCEPT_REQUEST)) {
+            Log.i(
+                TAG_CALL_MATCH_MODE,
+                "accept, cannot accept request in current state: ${stateMachine.getCurrentState()}"
+            )
+            return
+        }
+
+        launch {
+            when (val rlt = matchHttpService.matchAnswer(
+                CallMatchReqAnswer(
+                    matchId,
+                    ProfileModule.getMyUid(),
+                    AnswerType.ACCEPT.value
+                )
+            )) {
+                is Rlt.Success -> {
+                    Log.i(TAG_CALL_MATCH_MODE, "accept success")
+                    // 等待状态更新完成后再调用resultAck
+                    val eventHandled = stateMachine.handleEventAsync(MatchEvent.ACCEPT_REQUEST)
+                    if (eventHandled) {
+                        Log.i(TAG_CALL_MATCH_MODE, "State updated, calling resultAck")
+                        matchManager.resultAck()
+                    }
+                }
+
+                is Rlt.Failed -> {
+                    Log.i(TAG_CALL_MATCH_MODE, "accept failed, ${rlt.error}")
+                    stateMachine.handleEvent(MatchEvent.MATCH_TIMEOUT)
+                    clearData()
+                }
+            }
+        }
+    }
+
+    override fun reject() {
+        stopVibrateAndRing()
+        if (!stateMachine.canHandleEvent(MatchEvent.REJECT_REQUEST)) {
+            Log.w(
+                TAG_CALL_MATCH_MODE,
+                "Cannot reject request in current state: ${stateMachine.getCurrentState()}"
+            )
+            return
+        }
+        //拒绝失败时,按超时处理
+        launch {
+            when (val rlt = matchHttpService.matchAnswer(
+                CallMatchReqAnswer(
+                    matchId,
+                    ProfileModule.getMyUid(),
+                    AnswerType.REJECT.value
+                )
+            )) {
+                is Rlt.Success -> {
+                    Log.i(TAG_CALL_MATCH_MODE, "reject success")
+                    stateMachine.handleEvent(MatchEvent.REJECT_REQUEST)
+                    clearData()
+                }
+
+                is Rlt.Failed -> {
+                    Log.i(TAG_CALL_MATCH_MODE, "reject failed, ${rlt.error}")
+                    stateMachine.handleEvent(MatchEvent.MATCH_TIMEOUT)
+                    clearData()
+                }
+            }
+        }
+    }
+
+    override fun timeout() {
+        stopVibrateAndRing()
+        if (!stateMachine.canHandleEvent(MatchEvent.MATCH_TIMEOUT)) {
+            Log.i(
+                TAG_CALL_MATCH_MODE,
+                "timeout, cannot accept request in current state: ${stateMachine.getCurrentState()}"
+            )
+            return
+        }
+
+        launch {
+            when (val rlt = matchHttpService.matchAnswer(
+                CallMatchReqAnswer(
+                    matchId,
+                    ProfileModule.getMyUid(),
+                    AnswerType.TIMEOUT.value
+                )
+            )) {
+                is Rlt.Success -> {
+                    Log.i(TAG_CALL_MATCH_MODE, "timeout success")
+                    stateMachine.handleEvent(MatchEvent.MATCH_TIMEOUT)
+                    clearData()
+                }
+
+                is Rlt.Failed -> {
+                    Log.i(TAG_CALL_MATCH_MODE, "timeout failed, ${rlt.error}")
+                    stateMachine.handleEvent(MatchEvent.MATCH_TIMEOUT)
+                    clearData()
+                }
+            }
+        }
+    }
+
+    override fun getRoomId(): Long {
+        return roomId
+    }
+
+    override fun getMatchId(): Long {
+        return matchId
+    }
+
+    override fun isMatchIdValid(): Boolean {
+        return matchId != 0L
+    }
+
+    override fun clearData() {
+        matchId = 0L
+        roomId = 0L
+        callMode = CallType.Voice.type
+        stateMachine.handleEvent(MatchEvent.RESET)
+        Log.i(TAG_CALL_MATCH_MODE, "state reset")
+    }
+
+    private fun stopVibrateAndRing() {
+        stopVibrate()
+        stopRing()
+    }
+
+    private fun vibrate() {
+        if (vibrator?.hasVibrator() != true) {
+            return
+        }
+        try {
+            val pattern = longArrayOf(0, 200, 800, 200, 800) // 震动-停止-震动-停止
+            vibrator?.vibrate(pattern, 0)
+        } catch (_: Exception) {
+
+        }
+    }
+
+    private fun stopVibrate() {
+        if (vibrator?.hasVibrator() != true) {
+            return
+        }
+        vibrator?.cancel()
+    }
+
+    private fun startRing(ringtoneResId: Int) {
+        try {
+            stopRing() // 先停止之前的播放
+            mediaPlayer = MediaPlayer.create(AppUtil.appContext, ringtoneResId).apply {
+                isLooping = true
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+                    setAudioAttributes(
+                        AudioAttributes.Builder()
+                            .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
+                            .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
+                            .build()
+                    )
+                } else {
+                    @Suppress("DEPRECATION")
+                    setAudioStreamType(AudioManager.STREAM_RING)
+                }
+                start()
+            }
+        } catch (e: Exception) {
+            Log.e(TAG_CALL_MATCH_MODE, "match mode start ring fail", e)
+        }
+    }
+
+    private fun stopRing() {
+        try {
+            mediaPlayer?.let {
+                if (it.isPlaying) {
+                    it.stop()
+                }
+                it.release()
+            }
+            mediaPlayer = null
+        } catch (e: Exception) {
+            Log.e(TAG_CALL_MATCH_MODE, "match mode stop ring fail", e)
+        }
+    }
+}

+ 56 - 0
module/call/src/main/java/com/adealink/weparty/call/video/CallSettlementActivity.kt

@@ -0,0 +1,56 @@
+package com.adealink.weparty.call.video
+
+import android.content.Intent
+import android.os.Bundle
+import com.adealink.frame.mvvm.view.viewBinding
+import com.adealink.frame.router.Router
+import com.adealink.frame.router.annotation.BindExtra
+import com.adealink.frame.router.annotation.RouterUri
+import com.adealink.weparty.call.data.CallEndStatsNotify
+import com.adealink.weparty.call.databinding.ActivityCallSettlementBinding
+import com.adealink.weparty.commonui.BaseActivity
+import com.adealink.weparty.module.call.Call
+
+
+/**
+ * 通话结算页面
+ * Created by XiaoDongLin.
+ * Date: 2025/9/1
+ */
+@RouterUri(
+    path = [Call.CallSettlement.PATH]
+)
+class CallSettlementActivity : BaseActivity() {
+
+    @BindExtra(Call.CallSettlement.EXTRA_DATA)
+    var notify: CallEndStatsNotify? = null
+
+    private val binding by viewBinding(ActivityCallSettlementBinding::inflate)
+
+    override fun onBeforeCreate() {
+        super.onBeforeCreate()
+        Router.bind(this)
+    }
+
+    override fun handleNewIntent(intent: Intent?) {
+        super.handleNewIntent(intent)
+        this@CallSettlementActivity.intent = intent
+        Router.bind(this)
+        initViews()
+    }
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(binding.root)
+    }
+
+    override fun initViews() {
+        super.initViews()
+        val notify = notify ?: return
+        val fragment = CallSettlementFragment.newInstance(notify)
+        supportFragmentManager.beginTransaction()
+            .replace(binding.fragmentContainerView.id, fragment)
+            .commitAllowingStateLoss()
+    }
+
+}

+ 201 - 0
module/call/src/main/java/com/adealink/weparty/call/video/CallSettlementFragment.kt

@@ -0,0 +1,201 @@
+package com.adealink.weparty.call.video
+
+import android.content.Intent
+import android.os.Bundle
+import android.text.SpannableStringBuilder
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.view.updateLayoutParams
+import com.adealink.frame.aab.util.getCompatDrawable
+import com.adealink.frame.aab.util.getCompatString
+import com.adealink.frame.base.Rlt
+import com.adealink.frame.base.fastLazy
+import com.adealink.frame.ext.findAndSetSpan
+import com.adealink.frame.mvvm.view.viewBinding
+import com.adealink.frame.router.Router
+import com.adealink.frame.util.onClick
+import com.adealink.weparty.call.CallDialog.Companion.ICON_TAG
+import com.adealink.weparty.call.R
+import com.adealink.weparty.call.comp.BgComp
+import com.adealink.weparty.call.data.CallEndStatsNotify
+import com.adealink.weparty.call.databinding.FragmentCallSettlementBinding
+import com.adealink.weparty.call.datasource.local.CallLocalService
+import com.adealink.weparty.call.widget.BaseCallFragment
+import com.adealink.weparty.commonui.ext.dp
+import com.adealink.weparty.commonui.ext.gone
+import com.adealink.weparty.commonui.ext.onSuccess
+import com.adealink.weparty.commonui.ext.show
+import com.adealink.weparty.commonui.toast.util.showToast
+import com.adealink.weparty.commonui.widget.CenterImageSpan
+import com.adealink.weparty.module.call.Call
+import com.adealink.weparty.module.call.CallModule
+import com.adealink.weparty.module.follow.FollowModule
+import com.adealink.weparty.module.follow.data.FollowOpFrom
+import com.adealink.weparty.module.follow.viewmodel.IFollowViewModel
+import com.adealink.weparty.module.message.Message
+import com.adealink.weparty.module.message.data.EnterConversationFrom
+import com.adealink.weparty.module.profile.ProfileModule
+import com.qmuiteam.qmui.widget.util.QMUIStatusBarHelper
+import com.tencent.qcloud.tuicore.util.DateTimeUtil
+import io.rong.imlib.model.Conversation
+
+/**
+ * 通话结算页面
+ * author: PengWuliang
+ * date: 2025/8/28
+ * desc:
+ */
+class CallSettlementFragment : BaseCallFragment(R.layout.fragment_call_settlement) {
+
+    companion object {
+
+        fun newInstance(notify: CallEndStatsNotify): CallSettlementFragment {
+            return CallSettlementFragment().apply {
+                arguments = Bundle().apply {
+                    putParcelable(Call.CallSettlement.EXTRA_DATA, notify)
+                }
+            }
+        }
+    }
+
+    private val binding by viewBinding(FragmentCallSettlementBinding::bind)
+    private val followViewModel: IFollowViewModel? by fastLazy {
+        FollowModule.getFollowViewModel(this)
+    }
+    private val profileViewModel by fastLazy {
+        ProfileModule.getProfileViewModel(this)
+    }
+    private val callViewModel by fastLazy {
+        CallModule.getCallViewModel(this)
+    }
+
+    private val notify by fastLazy {
+        arguments?.getParcelable(Call.CallSettlement.EXTRA_DATA) as? CallEndStatsNotify
+    }
+
+    //对方的uid
+    private val targetUid by fastLazy {
+        val myUid = ProfileModule.getMyUid()
+        val targetUid = if (notify?.deductUid == myUid) notify?.receiveUid else notify?.deductUid
+        targetUid ?: 0
+    }
+
+    override fun initViews() {
+        super.initViews()
+        binding.topBar.updateLayoutParams<ConstraintLayout.LayoutParams> {
+            topMargin = QMUIStatusBarHelper.getStatusbarHeight(context)
+        }
+        binding.topBar.setToTransparentMode()
+
+        val notify = notify ?: return
+
+        callViewModel?.getCallMatchEntrance()
+        followViewModel?.isUserFollowed(uid = targetUid)
+
+        profileViewModel?.getUidUserInfo(uid = targetUid, cache = false)
+            ?.observe(viewLifecycleOwner) { rlt ->
+                rlt.onSuccess { userInfo ->
+                    binding.ivAvatar.setImageUrl(userInfo.url)
+                    binding.tvName.text = userInfo.name
+                    binding.ivNationalFlag.setImageUrl(userInfo.flag)
+
+                    binding.tvChat.onClick {
+                        Router.build(requireActivity(), Message.Conversation.PATH)
+                            .putExtra(Message.Common.EXTRA_TO_UID, userInfo.uid).putExtra(
+                                Message.Common.EXTRA_CONVERSATION_TYPE,
+                                Conversation.ConversationType.PRIVATE.value
+                            ).putExtra(
+                                Message.Common.EXTRA_FROM,
+                                EnterConversationFrom.VideoCall.name
+                            ).start()
+                    }
+
+                }
+            }
+
+
+        binding.tvCoinCost.text = chatCostTips()
+
+        callViewModel?.showMatchEntrance?.observe(viewLifecycleOwner) { canShow ->
+            // 控制匹配按钮显示隐藏
+            if (notify.matchId != 0L && canShow) {
+                binding.btnMatch.show()
+                binding.tvCoinCost.show()
+            } else {
+                binding.btnMatch.gone()
+                binding.tvCoinCost.gone()
+            }
+        }
+
+
+        binding.btnMatch.setOnClickListener {
+            val act = activity ?: return@setOnClickListener
+            Router.build(act, Call.CallMatch.PATH)
+                .putExtra(Call.CallMatch.EXTRA_AUTO_MATCH, true)
+                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
+                .start()
+            act.finish()
+        }
+
+        binding.tvDurationValue.text =
+            DateTimeUtil.formatSecondsTo00(notify.callDuration.toInt())
+        binding.tvEarnValue.text = notify.diamondIncome.toString()
+
+        //根据身份进行展示
+        val myUid = ProfileModule.getMyUid()
+        if (myUid == notify.receiveUid) {
+            binding.groupPayer.gone()
+            binding.groupEarn.show()
+        } else {
+            binding.groupPayer.show()
+            binding.groupEarn.gone()
+        }
+    }
+
+    override fun observeViewModel() {
+        super.observeViewModel()
+        followViewModel?.isUserFollowedLD?.observe(viewLifecycleOwner) { rlt ->
+            val isFollowed = (rlt as? Rlt.Success)?.data?.isFollowed ?: false
+            if (isFollowed) {
+                binding.tvFollow.text =
+                    getCompatString(com.adealink.weparty.R.string.common_unfollow)
+                binding.tvFollow.setOnClickListener {
+                    followViewModel?.unFollowUser(targetUid, FollowOpFrom.CALL_MATCH)
+                        ?.observe(viewLifecycleOwner) {
+                            showToast(it)
+                        }
+                }
+            } else {
+                binding.tvFollow.text =
+                    getCompatString(com.adealink.weparty.R.string.common_follow)
+                binding.tvFollow.setOnClickListener {
+                    followViewModel?.followUser(targetUid, FollowOpFrom.CALL_MATCH)
+                        ?.observe(viewLifecycleOwner) {
+                            showToast(it)
+                        }
+                }
+            }
+        }
+    }
+
+    private fun chatCostTips(): SpannableStringBuilder {
+        val chatCost = CallLocalService.chatCostPerMin.toString()
+        val text = getCompatString(R.string.call_video_calling_tips, chatCost)
+        return SpannableStringBuilder(text).apply {
+            val i = text.indexOf(ICON_TAG)
+            if (i >= 0) {
+                findAndSetSpan(
+                    CenterImageSpan(getCompatDrawable(com.adealink.weparty.R.drawable.common_coin_32_ic).apply {
+                        setBounds(0, 0, 16.dp(), 16.dp())
+                    }),
+                    ICON_TAG
+                )
+            }
+        }
+    }
+
+    override fun initComponents() {
+        super.initComponents()
+        BgComp(this, uid = targetUid, binding.vBg).attach()
+    }
+
+}

+ 21 - 1
module/call/src/main/java/com/adealink/weparty/call/video/VideoFragment.kt

@@ -1,5 +1,6 @@
 package com.adealink.weparty.call.video
 
+import android.os.Bundle
 import androidx.fragment.app.activityViewModels
 import com.adealink.frame.log.Log
 import com.adealink.frame.mvvm.view.viewBinding
@@ -7,6 +8,8 @@ import com.adealink.weparty.call.R
 import com.adealink.weparty.call.chat.viewmodel.ChatViewModel
 import com.adealink.weparty.call.constant.TAG_CALL_CHAT
 import com.adealink.weparty.call.databinding.FragmentCallVideoBinding
+import com.adealink.weparty.call.video.dialog.PayeeReceiveVideoInvitationDialog
+import com.adealink.weparty.call.video.dialog.PayerReceiveVideoInvitationDialog
 import com.adealink.weparty.call.viewmodel.CallViewModelFactory
 import com.adealink.weparty.call.widget.BaseCallFragment
 import com.tencent.cloud.tuikit.engine.call.TUICallDefine
@@ -63,6 +66,23 @@ class VideoFragment : BaseCallFragment(R.layout.fragment_call_video) {
         chatViewModel.tCallStatusLD.observe(viewLifecycleOwner) { status ->
             notifyPageChanged(status)
         }
+        chatViewModel.switchCallModeAskLD.observe(viewLifecycleOwner) { data ->
+            val self = TUICallState.instance.selfUser.get()
+            if(self.isPayer.get()) {
+                PayerReceiveVideoInvitationDialog().apply {
+                    arguments = Bundle().apply {
+                        putString("roomId", data.roomId)
+                        putInt("callMode", data.callMode)
+                    }
+                }.show(childFragmentManager)
+            } else {
+                PayeeReceiveVideoInvitationDialog().apply {
+                    arguments = Bundle().apply {
+                        putString("roomId", data.roomId)
+                        putInt("callMode", data.callMode)
+                    }
+                }.show(childFragmentManager)
+            }
+        }
     }
-
 }

+ 7 - 1
module/call/src/main/java/com/adealink/weparty/call/video/comp/VideoCallerComp.kt

@@ -1,10 +1,12 @@
 package com.adealink.weparty.call.video.comp
 
+import android.util.Log
 import android.view.ViewGroup
 import android.widget.RelativeLayout
 import androidx.lifecycle.LifecycleOwner
+import com.adealink.weparty.call.constant.TAG_CALL_MATCH_MODE
+import com.adealink.weparty.call.match.matchManager
 import com.adealink.weparty.call.widget.BaseCallViewComponent
-import com.tencent.cloud.tuikit.engine.call.TUICallDefine
 import com.tencent.qcloud.tuikit.tuicallkit.manager.EngineManager
 import com.tencent.qcloud.tuikit.tuicallkit.state.TUICallState
 import com.tencent.qcloud.tuikit.tuicallkit.view.component.videolayout.VideoView
@@ -48,6 +50,10 @@ class VideoCallerComp(
             bigVideoContainer.removeAllViews()
         }
         bigVideoContainer.addView(videoViewBig)
+        if(matchManager.isMatchIdValid() && TUICallState.instance.callUserData.get().matchId == matchManager.getMatchId()) {
+            Log.d(TAG_CALL_MATCH_MODE, "initBigRenderView, no need open in match mode begin")
+            return
+        }
         if (TUICallState.instance.isCameraOpen.get()) {
             EngineManager.instance.openCamera(viewModel.isFrontCamera.get(), videoViewBig?.getVideoView(), null)
         }

+ 46 - 2
module/call/src/main/java/com/adealink/weparty/call/video/comp/VideoRoomBottomComp.kt

@@ -1,11 +1,17 @@
 package com.adealink.weparty.call.video.comp
 
+import android.content.Intent
 import android.os.Bundle
+import android.view.View
 import androidx.lifecycle.LifecycleOwner
 import com.adealink.frame.aab.util.getCompatString
+import com.adealink.frame.base.fastLazy
 import com.adealink.frame.router.Router
 import com.adealink.weparty.call.databinding.LayoutCallVideoRoomBottomBarBinding
+import com.adealink.weparty.call.match.matchManager
 import com.adealink.weparty.call.stat.CallStatEvent
+import com.adealink.weparty.call.video.dialog.PayeeSendVideoInvitationDialog
+import com.adealink.weparty.call.video.dialog.PayerSendVideoInvitationDialog
 import com.adealink.weparty.call.widget.BaseCallViewComponent
 import com.adealink.weparty.commonui.ext.gone
 import com.adealink.weparty.commonui.ext.show
@@ -14,6 +20,8 @@ import com.adealink.weparty.commonui.widget.BottomDialogFragment
 import com.adealink.weparty.module.account.util.isUidValid
 import com.adealink.weparty.module.anchor.data.FromScene
 import com.adealink.weparty.module.call.Call
+import com.adealink.weparty.module.couple.data.IntimacyPrivilegeType
+import com.adealink.weparty.module.message.MessageModule
 import com.adealink.weparty.module.room.Room
 import com.tencent.cloud.tuikit.engine.common.TUICommonDefine
 import com.tencent.qcloud.tuicore.util.DateTimeUtil
@@ -24,9 +32,12 @@ import com.trtc.tuikit.common.livedata.Observer
 
 class VideoRoomBottomComp(
     lifecycleOwner: LifecycleOwner,
-    private val binding: LayoutCallVideoRoomBottomBarBinding
+    private val binding: LayoutCallVideoRoomBottomBarBinding,
+    private val matchNextBtn: View,
 ) : BaseCallViewComponent(lifecycleOwner) {
 
+    private val messageViewModel by fastLazy { MessageModule.getMessageViewModel(viewModelStoreOwner) }
+
     private var isMicMuteObserver = Observer<Boolean> {mute->
         binding.micBtn.isActivated = !mute
         if (mute) {
@@ -115,7 +126,30 @@ class VideoRoomBottomComp(
 
         binding.cameraBtn.isActivated = TUICallState.instance.isCameraOpen.get() == true
         binding.cameraBtn.setOnClickListener {
-            openCamera()
+            if(matchManager.isMatchIdValid() && TUICallState.instance.matchModeOpenCamera.get() == false) { //匹配中视频通话开关
+                val self = TUICallState.instance.selfUser.get()
+                if(self.isPayer.get() == true) {
+                    PayerSendVideoInvitationDialog().show(fragmentManager)
+                } else {
+                    PayeeSendVideoInvitationDialog().show(fragmentManager)
+                }
+            } else { //正常视频通话开关
+                openCamera()
+            }
+        }
+
+        if(matchManager.isMatchIdValid() && TUICallState.instance.selfUser.get().isPayer.get() == true) {
+            matchNextBtn.show()
+            matchNextBtn.setOnClickListener {
+                hangup()
+                val act = activity ?: return@setOnClickListener
+                Router.build(act, Call.CallMatch.PATH)
+                    .putExtra(Call.CallMatch.EXTRA_AUTO_MATCH, true)
+                    .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
+                    .start()
+            }
+        } else {
+            matchNextBtn.gone()
         }
 
         binding.hangupBtn.setOnClickListener {
@@ -125,6 +159,16 @@ class VideoRoomBottomComp(
 
     private fun observeViewModel() {
         addObserver()
+        messageViewModel?.getCallPrivilege(
+            TUICallState.instance.getRemoteUid() ?: 0L,
+            arrayOf(IntimacyPrivilegeType.VIDEO_CALL)
+        )?.observe(viewLifecycleOwner) {
+            if(it[IntimacyPrivilegeType.VIDEO_CALL] == true) {
+                binding.cameraBtn.show()
+            } else {
+                binding.cameraBtn.gone()
+            }
+        }
     }
 
     private fun sendGift() {

+ 31 - 7
module/call/src/main/java/com/adealink/weparty/call/video/comp/VideoRoomComp.kt

@@ -13,11 +13,17 @@ import android.widget.RelativeLayout
 import androidx.appcompat.widget.AppCompatImageView
 import androidx.core.view.updateLayoutParams
 import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.lifecycleScope
 import com.adealink.frame.aab.util.getCompatDimension
+import com.adealink.frame.coroutine.dispatcher.Dispatcher
 import com.adealink.frame.util.DisplayUtil
 import com.adealink.weparty.call.R
 import com.adealink.weparty.call.constant.TAG_CALL
+import com.adealink.weparty.call.constant.TAG_CALL_MATCH_MODE
+import com.adealink.weparty.call.match.matchManager
 import com.adealink.weparty.call.widget.BaseCallViewComponent
+import com.adealink.weparty.commonui.ext.gone
+import com.adealink.weparty.commonui.ext.show
 import com.tencent.cloud.tuikit.engine.call.TUICallDefine
 import com.tencent.cloud.tuikit.engine.common.TUICommonDefine
 import com.tencent.qcloud.tuicore.util.ScreenUtil
@@ -29,6 +35,7 @@ import com.tencent.qcloud.tuikit.tuicallkit.view.component.videolayout.VideoView
 import com.tencent.qcloud.tuikit.tuicallkit.view.component.videolayout.VideoViewFactory
 import com.tencent.qcloud.tuikit.tuicallkit.viewmodel.component.videolayout.SingleCallVideoLayoutViewModel
 import com.trtc.tuikit.common.livedata.Observer
+import kotlinx.coroutines.launch
 
 /**
  * 参考: SingleCallVideoLayout
@@ -69,6 +76,15 @@ class VideoRoomComp(
         }
     }
 
+    private val matchModeOpenCameraObserver = Observer<Boolean> {
+        if (it) {
+            lifecycleScope.launch(Dispatcher.UI) {
+                initBigRenderView(true)
+                initSmallRenderView(true)
+            }
+        }
+    }
+
     private var callStatusObserver = Observer<TUICallDefine.Status> {
         if (it == TUICallDefine.Status.Accept) {
             initSmallRenderView()
@@ -93,11 +109,13 @@ class VideoRoomComp(
         TUICallState.instance.isCameraOpen.observe(isCameraOpenObserver)
         smallVideoView?.addObserver()
         bigVideoView?.addObserver()
+        TUICallState.instance.matchModeOpenCamera.observe(matchModeOpenCameraObserver)
     }
 
     override fun removeObserver() {
         viewModel.remoteUser.callStatus.removeObserver(callStatusObserver)
         TUICallState.instance.isCameraOpen.removeObserver(isCameraOpenObserver)
+        TUICallState.instance.matchModeOpenCamera.removeObserver(matchModeOpenCameraObserver)
 //        remoteVideoView?.removeObserver()
 //        selfVideoView?.removeObserver()
     }
@@ -155,7 +173,7 @@ class VideoRoomComp(
         }
     }
 
-    private fun initSmallRenderView() {
+    private fun initSmallRenderView(forceOpen: Boolean = false) {
         smallVideoContainer ?: return
         if (smallVideoUser?.callStatus?.get() == TUICallDefine.Status.Accept) {
             smallVideoView = VideoViewFactory.instance.createVideoView(
@@ -168,11 +186,11 @@ class VideoRoomComp(
             }
             setSmallRenderViewOrientation()
             smallVideoContainer.addView(smallVideoView)
-            openVideo(smallVideoUser, smallVideoView)
+            openVideo(smallVideoUser, smallVideoView, forceOpen)
         }
     }
 
-    private fun initBigRenderView() {
+    private fun initBigRenderView(forceOpen: Boolean = false) {
         bigVideoContainer ?: return
         bigVideoView = VideoViewFactory.instance.createVideoView(
             bigVideoUser,
@@ -183,20 +201,26 @@ class VideoRoomComp(
             bigVideoContainer.removeAllViews()
         }
         bigVideoContainer.addView(bigVideoView)
-        openVideo(bigVideoUser, bigVideoView)
+        openVideo(bigVideoUser, bigVideoView, forceOpen)
     }
 
-    private fun openVideo(user: User?, videoView: VideoView?) {
+    private fun openVideo(user: User?, videoView: VideoView?, forceOpen: Boolean) {
+        if(matchManager.isMatchIdValid() && TUICallState.instance.callUserData.get().matchId == matchManager.getMatchId() && !forceOpen) {
+            switchCamera?.gone()
+            smallVideoContainer?.gone()
+            Log.i(TAG_CALL_MATCH_MODE, "openVideo, no need open in match mode begin")
+            return
+        }
+        switchCamera?.show()
+        smallVideoContainer?.show()
         if (user?.id == viewModel.remoteUser.id) {
             //对方开启视频订阅
             EngineManager.instance.startRemoteView(user?.id, videoView?.getVideoView(), null)
-
         } else if (user?.id == viewModel.selfUser.id) {
             //我方开启摄像头
             if (TUICallState.instance.isCameraOpen.get()) {
                 EngineManager.instance.openCamera(viewModel.isFrontCamera.get(), videoView?.getVideoView(), null)
             }
-
         } else {
             Log.w(TAG_CALL, "openVideo fail, for user($user) is invalid")
         }

+ 105 - 0
module/call/src/main/java/com/adealink/weparty/call/video/dialog/PayeeReceiveVideoInvitationDialog.kt

@@ -0,0 +1,105 @@
+package com.adealink.weparty.call.video.dialog
+
+import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
+import android.text.SpannableStringBuilder
+import android.view.Gravity
+import android.view.Window
+import android.view.WindowManager
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.lifecycleScope
+import com.adealink.frame.aab.util.getCompatDrawable
+import com.adealink.frame.aab.util.getCompatString
+import com.adealink.frame.base.Rlt
+import com.adealink.frame.ext.findAndSetSpan
+import com.adealink.frame.mvvm.view.viewBinding
+import com.adealink.weparty.call.CallDialog.Companion.ICON_TAG
+import com.adealink.weparty.call.R
+import com.adealink.weparty.call.databinding.DialogPayeeVideoInvitationBinding
+import com.adealink.weparty.call.datasource.local.CallLocalService
+import com.adealink.weparty.call.viewmodel.CallViewModel
+import com.adealink.weparty.call.viewmodel.CallViewModelFactory
+import com.adealink.weparty.commonui.dialogfragment.BaseDialogFragment
+import com.adealink.weparty.commonui.ext.dp
+import com.adealink.weparty.commonui.toast.util.showToast
+import com.adealink.weparty.commonui.widget.CenterImageSpan
+import com.adealink.weparty.match.AnswerType
+import com.tencent.qcloud.tuikit.tuicallkit.state.TUICallState
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import com.adealink.weparty.R as APP_R
+
+/**
+ * author: PengWuliang
+ * date: 2025/8/22
+ * desc: 收款方收到的视频邀请弹窗
+ */
+class PayeeReceiveVideoInvitationDialog: BaseDialogFragment(R.layout.dialog_payee_video_invitation) {
+
+    private val roomId: String by lazy {
+        arguments?.getString("roomId") ?: ""
+    }
+
+    private val binding by viewBinding(DialogPayeeVideoInvitationBinding::bind)
+    private val callViewModel by activityViewModels<CallViewModel>(factoryProducer = { CallViewModelFactory() })
+
+    override fun initViews() {
+        binding.tvTitle.text = getCompatString(R.string.call_video_invitation)
+        binding.tvContent.text = videoCallingTips()
+        binding.tvCancel.setOnClickListener {
+            callViewModel.switchModeAnswer(roomId, AnswerType.REJECT.value).observe(this) {
+                if(it is Rlt.Success) {
+                    dismiss()
+                }
+            }
+        }
+        binding.tvConfirm.setOnClickListener {
+            callViewModel.switchModeAnswer(roomId, AnswerType.ACCEPT.value).observe(this) {
+                when(it) {
+                    is Rlt.Success -> {
+                        dismiss()
+                    }
+                    is Rlt.Failed -> {
+                        showToast(it.error.msg)
+                        dismiss()
+                    }
+                }
+            }
+        }
+        lifecycleScope.launch {
+            delay(30_000)
+            callViewModel.switchModeAnswer(roomId, AnswerType.TIMEOUT.value)
+            dismiss()
+        }
+    }
+
+    private fun videoCallingTips(): SpannableStringBuilder {
+        val userName = TUICallState.instance.getRemoteUser()?.userInfo?.get()?.name ?: ""
+        val earnDiamond = (CallLocalService.videoEarnPerMin - CallLocalService.chatEarnPerMin).toString()
+        val text = getCompatString(R.string.call_change_video_earn_invitation, userName, earnDiamond)
+        return SpannableStringBuilder(text).apply {
+            val i = text.indexOf(ICON_TAG)
+            if (i >= 0) {
+                findAndSetSpan(
+                    CenterImageSpan(getCompatDrawable(APP_R.drawable.common_diamond_32_ic).apply {
+                        setBounds(0, 0, 16.dp(), 16.dp())
+                    }),
+                    ICON_TAG
+                )
+            }
+        }
+    }
+
+    override fun resetWindowAttributes(window: Window) {
+        super.resetWindowAttributes(window)
+        window.setLayout(
+            WindowManager.LayoutParams.MATCH_PARENT,
+            WindowManager.LayoutParams.WRAP_CONTENT
+        )
+        window.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
+        val attr = window.attributes
+        attr.gravity = Gravity.CENTER
+        attr.dimAmount = 0.7f
+        window.attributes = attr
+    }
+}

+ 89 - 0
module/call/src/main/java/com/adealink/weparty/call/video/dialog/PayeeSendVideoInvitationDialog.kt

@@ -0,0 +1,89 @@
+package com.adealink.weparty.call.video.dialog
+
+import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
+import android.text.SpannableStringBuilder
+import android.view.Gravity
+import android.view.Window
+import android.view.WindowManager
+import androidx.fragment.app.activityViewModels
+import com.adealink.frame.aab.util.getCompatDrawable
+import com.adealink.frame.aab.util.getCompatString
+import com.adealink.frame.base.Rlt
+import com.adealink.frame.ext.findAndSetSpan
+import com.adealink.frame.mvvm.view.viewBinding
+import com.adealink.weparty.call.CallDialog.Companion.ICON_TAG
+import com.adealink.weparty.call.R
+import com.adealink.weparty.call.databinding.DialogPayeeVideoInvitationBinding
+import com.adealink.weparty.call.datasource.local.CallLocalService
+import com.adealink.weparty.call.util.getRoomId
+import com.adealink.weparty.call.viewmodel.CallViewModel
+import com.adealink.weparty.call.viewmodel.CallViewModelFactory
+import com.adealink.weparty.commonui.dialogfragment.BaseDialogFragment
+import com.adealink.weparty.commonui.ext.dp
+import com.adealink.weparty.commonui.toast.util.showToast
+import com.adealink.weparty.commonui.widget.CenterImageSpan
+import com.adealink.weparty.module.call.data.CallType
+import com.tencent.qcloud.tuikit.tuicallkit.state.TUICallState
+import com.adealink.weparty.R as APP_R
+
+/**
+ * author: PengWuliang
+ * date: 2025/8/22
+ * desc: 收款方发送的视频邀请弹窗
+ */
+class PayeeSendVideoInvitationDialog: BaseDialogFragment(R.layout.dialog_payee_video_invitation) {
+    private val binding by viewBinding(DialogPayeeVideoInvitationBinding::bind)
+    private val callViewModel by activityViewModels<CallViewModel>(factoryProducer = { CallViewModelFactory() })
+
+    override fun initViews() {
+        binding.tvTitle.text = getCompatString(R.string.call_send_video_invitation)
+        binding.tvCancel.setOnClickListener {
+            dismiss()
+        }
+        binding.tvContent.text = videoCallingTips()
+        binding.tvConfirm.setOnClickListener {
+            val roomId = TUICallState.instance.roomId.get()?.getRoomId() ?: return@setOnClickListener
+            callViewModel.switchMode(roomId, CallType.Video.type).observe(this) {
+                when(it) {
+                    is Rlt.Success -> {
+                        dismiss()
+                    }
+                    is Rlt.Failed -> {
+                        showToast(it.error.msg)
+                        dismiss()
+                    }
+                }
+            }
+        }
+    }
+
+    private fun videoCallingTips(): SpannableStringBuilder {
+        val earnDiamond = (CallLocalService.videoEarnPerMin - CallLocalService.chatEarnPerMin).toString()
+        val text = getCompatString(R.string.call_change_video_earn, earnDiamond)
+        return SpannableStringBuilder(text).apply {
+            val i = text.indexOf(ICON_TAG)
+            if (i >= 0) {
+                findAndSetSpan(
+                    CenterImageSpan(getCompatDrawable(APP_R.drawable.common_diamond_32_ic).apply {
+                        setBounds(0, 0, 16.dp(), 16.dp())
+                    }),
+                    ICON_TAG
+                )
+            }
+        }
+    }
+
+    override fun resetWindowAttributes(window: Window) {
+        super.resetWindowAttributes(window)
+        window.setLayout(
+            WindowManager.LayoutParams.MATCH_PARENT,
+            WindowManager.LayoutParams.WRAP_CONTENT
+        )
+        window.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
+        val attr = window.attributes
+        attr.gravity = Gravity.CENTER
+        attr.dimAmount = 0.7f
+        window.attributes = attr
+    }
+}

+ 110 - 0
module/call/src/main/java/com/adealink/weparty/call/video/dialog/PayerReceiveVideoInvitationDialog.kt

@@ -0,0 +1,110 @@
+package com.adealink.weparty.call.video.dialog
+
+import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
+import android.text.SpannableStringBuilder
+import android.view.Gravity
+import android.view.Window
+import android.view.WindowManager
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.lifecycleScope
+import com.adealink.frame.aab.util.getCompatDrawable
+import com.adealink.frame.aab.util.getCompatString
+import com.adealink.frame.base.Rlt
+import com.adealink.frame.ext.findAndSetSpan
+import com.adealink.frame.mvvm.view.viewBinding
+import com.adealink.weparty.call.CallDialog.Companion.ICON_TAG
+import com.adealink.weparty.call.R
+import com.adealink.weparty.call.databinding.DialogPayerVideoInvitationBinding
+import com.adealink.weparty.call.datasource.local.CallLocalService
+import com.adealink.weparty.call.viewmodel.CallViewModel
+import com.adealink.weparty.call.viewmodel.CallViewModelFactory
+import com.adealink.weparty.commonui.dialogfragment.BaseDialogFragment
+import com.adealink.weparty.commonui.ext.dp
+import com.adealink.weparty.commonui.toast.util.showToast
+import com.adealink.weparty.commonui.widget.CenterImageSpan
+import com.adealink.weparty.match.AnswerType
+import com.tencent.qcloud.tuikit.tuicallkit.state.TUICallState
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import com.adealink.weparty.R as APP_R
+
+/**
+ * author: PengWuliang
+ * date: 2025/8/22
+ * desc: 消费方收到的视频邀请弹窗
+ */
+class PayerReceiveVideoInvitationDialog: BaseDialogFragment(R.layout.dialog_payer_video_invitation) {
+
+    private val roomId: String by lazy {
+        arguments?.getString("roomId") ?: ""
+    }
+
+    private val binding by viewBinding(DialogPayerVideoInvitationBinding::bind)
+    private val callViewModel by activityViewModels<CallViewModel>(factoryProducer = { CallViewModelFactory() })
+
+    override fun initViews() {
+        val remoteUser = TUICallState.instance.getRemoteUser()
+        if(remoteUser != null) {
+            binding.tvName.text = remoteUser.userInfo.get().name
+            binding.ivAvatar.setImageUrl(remoteUser.userInfo.get().url)
+        }
+        binding.tvTitle.text = getCompatString(R.string.call_receive_video_invitation)
+        binding.tvContent.text = videoCallingTips()
+        binding.tvCancel.setOnClickListener {
+            callViewModel.switchModeAnswer(roomId, AnswerType.REJECT.value).observe(this) {
+                if(it is Rlt.Success) {
+                    dismiss()
+                }
+            }
+        }
+        binding.tvConfirm.setOnClickListener {
+            callViewModel.switchModeAnswer(roomId, AnswerType.ACCEPT.value).observe(this) {
+                when(it) {
+                    is Rlt.Success -> {
+                        dismiss()
+                    }
+                    is Rlt.Failed -> {
+                        showToast(it.error.msg)
+                        dismiss()
+                    }
+                }
+            }
+        }
+        lifecycleScope.launch {
+            delay(30_000)
+            callViewModel.switchModeAnswer(roomId, AnswerType.TIMEOUT.value)
+            dismiss()
+        }
+    }
+
+    private fun videoCallingTips(): SpannableStringBuilder {
+        val userName = TUICallState.instance.getRemoteUser()?.userInfo?.get()?.name ?: ""
+        val firstMinCostDifference = (CallLocalService.videoCostPerMin - CallLocalService.chatCostPerMin).toString()
+        val text = getCompatString(R.string.call_change_video_cost_invitation, userName, firstMinCostDifference)
+        return SpannableStringBuilder(text).apply {
+            val i = text.indexOf(ICON_TAG)
+            if (i >= 0) {
+                findAndSetSpan(
+                    CenterImageSpan(getCompatDrawable(APP_R.drawable.common_coin_32_ic).apply {
+                        setBounds(0, 0, 16.dp(), 16.dp())
+                    }),
+                    ICON_TAG
+                )
+            }
+        }
+    }
+
+    override fun resetWindowAttributes(window: Window) {
+        super.resetWindowAttributes(window)
+        window.setLayout(
+            WindowManager.LayoutParams.MATCH_PARENT,
+            WindowManager.LayoutParams.WRAP_CONTENT
+        )
+        window.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
+        val attr = window.attributes
+        attr.gravity = Gravity.CENTER
+        attr.dimAmount = 0.7f
+        window.attributes = attr
+    }
+}

+ 95 - 0
module/call/src/main/java/com/adealink/weparty/call/video/dialog/PayerSendVideoInvitationDialog.kt

@@ -0,0 +1,95 @@
+package com.adealink.weparty.call.video.dialog
+
+import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
+import android.text.SpannableStringBuilder
+import android.view.Gravity
+import android.view.Window
+import android.view.WindowManager
+import androidx.fragment.app.activityViewModels
+import com.adealink.frame.aab.util.getCompatDrawable
+import com.adealink.frame.aab.util.getCompatString
+import com.adealink.frame.base.Rlt
+import com.adealink.frame.ext.findAndSetSpan
+import com.adealink.frame.mvvm.view.viewBinding
+import com.adealink.weparty.call.CallDialog.Companion.ICON_TAG
+import com.adealink.weparty.call.R
+import com.adealink.weparty.call.databinding.DialogPayerVideoInvitationBinding
+import com.adealink.weparty.call.datasource.local.CallLocalService
+import com.adealink.weparty.call.util.getRoomId
+import com.adealink.weparty.call.viewmodel.CallViewModel
+import com.adealink.weparty.call.viewmodel.CallViewModelFactory
+import com.adealink.weparty.commonui.dialogfragment.BaseDialogFragment
+import com.adealink.weparty.commonui.ext.dp
+import com.adealink.weparty.commonui.toast.util.showToast
+import com.adealink.weparty.commonui.widget.CenterImageSpan
+import com.adealink.weparty.module.call.data.CallType
+import com.tencent.qcloud.tuikit.tuicallkit.state.TUICallState
+import com.adealink.weparty.R as APP_R
+
+/**
+ * author: PengWuliang
+ * date: 2025/8/22
+ * desc: 消费方发送的视频邀请弹窗
+ */
+class PayerSendVideoInvitationDialog: BaseDialogFragment(R.layout.dialog_payer_video_invitation) {
+
+    private val binding by viewBinding(DialogPayerVideoInvitationBinding::bind)
+    private val callViewModel by activityViewModels<CallViewModel>(factoryProducer = { CallViewModelFactory() })
+
+    override fun initViews() {
+        val remoteUser = TUICallState.instance.getRemoteUser()
+        if(remoteUser != null) {
+            binding.tvName.text = remoteUser.userInfo.get().name
+            binding.ivAvatar.setImageUrl(remoteUser.userInfo.get().url)
+        }
+        binding.tvTitle.text = getCompatString(R.string.call_send_video_invitation)
+        binding.tvCancel.setOnClickListener {
+            dismiss()
+        }
+        binding.tvContent.text = videoCallingTips()
+        binding.tvConfirm.setOnClickListener {
+            val roomId = TUICallState.instance.roomId.get()?.getRoomId() ?: return@setOnClickListener
+            callViewModel.switchMode(roomId, CallType.Video.type).observe(this) {
+                when(it) {
+                    is Rlt.Success -> {
+                        dismiss()
+                    }
+                    is Rlt.Failed -> {
+                        showToast(it.error.msg)
+                        dismiss()
+                    }
+                }
+            }
+        }
+    }
+
+    private fun videoCallingTips(): SpannableStringBuilder {
+        val firstMinCostDifference = (CallLocalService.videoCostPerMin - CallLocalService.chatCostPerMin).toString()
+        val text = getCompatString(R.string.call_change_video_cost, firstMinCostDifference)
+        return SpannableStringBuilder(text).apply {
+            val i = text.indexOf(ICON_TAG)
+            if (i >= 0) {
+                findAndSetSpan(
+                    CenterImageSpan(getCompatDrawable(APP_R.drawable.common_coin_32_ic).apply {
+                        setBounds(0, 0, 16.dp(), 16.dp())
+                    }),
+                    ICON_TAG
+                )
+            }
+        }
+    }
+
+    override fun resetWindowAttributes(window: Window) {
+        super.resetWindowAttributes(window)
+        window.setLayout(
+            WindowManager.LayoutParams.MATCH_PARENT,
+            WindowManager.LayoutParams.WRAP_CONTENT
+        )
+        window.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
+        val attr = window.attributes
+        attr.gravity = Gravity.CENTER
+        attr.dimAmount = 0.7f
+        window.attributes = attr
+    }
+}

+ 24 - 3
module/call/src/main/java/com/adealink/weparty/call/video/fragment/VideoCallerFragment.kt

@@ -3,10 +3,12 @@ package com.adealink.weparty.call.video.fragment
 import android.widget.FrameLayout
 import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.core.view.updateLayoutParams
+import androidx.lifecycle.lifecycleScope
 import com.adealink.frame.mvvm.view.viewBinding
 import com.adealink.weparty.call.R
 import com.adealink.weparty.call.comp.BgComp
 import com.adealink.weparty.call.databinding.FragmentCallVideoCallerWaitingBinding
+import com.adealink.weparty.call.match.matchManager
 import com.adealink.weparty.call.util.getCallWaitingTips
 import com.adealink.weparty.call.video.comp.UserInfoComp
 import com.adealink.weparty.call.video.comp.VideoCallerComp
@@ -18,6 +20,9 @@ import com.adealink.weparty.commonui.ext.show
 import com.qmuiteam.qmui.widget.util.QMUIStatusBarHelper
 import com.tencent.qcloud.tuikit.tuicallkit.state.TUICallState
 import com.trtc.tuikit.common.livedata.Observer
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import com.adealink.weparty.call.comp.UserInfoComp as ChatUserInfoComp
 
 class VideoCallerFragment : BaseCallFragment(R.layout.fragment_call_video_caller_waiting) {
 
@@ -40,14 +45,27 @@ class VideoCallerFragment : BaseCallFragment(R.layout.fragment_call_video_caller
         binding.ivMini.setOnClickListener {
             activity?.finish()
         }
-        inflateFunctionView()
-        binding.tvCallDetail.text = getCallWaitingTips()
+        if(matchManager.isMatchIdValid()) {
+            binding.tvCallDetail.gone()
+            binding.tvCallStatus.text = getString(R.string.call_chat_connect_tips)
+            // 10s后显示功能按钮
+            lifecycleScope.launch {
+                delay(10_000)
+                inflateFunctionView(true)
+            }
+        } else {
+            inflateFunctionView()
+            binding.tvCallStatus.text = getString(R.string.call_chat_caller_waiting_tips)
+            binding.tvCallDetail.text = getCallWaitingTips()
+            binding.tvCallDetail.show()
+        }
     }
 
     override fun initComponents() {
         super.initComponents()
         BgComp(this, TUICallState.instance.getRemoteUid(), binding.vBg).attach()
         UserInfoComp(this, TUICallState.instance.getRemoteUid(), binding.vUserInfo).attach()
+        ChatUserInfoComp(this, TUICallState.instance.getRemoteUid(), binding.chatUserInfo).attach()
         VideoCallerComp(this, binding.vCallerVideo).attach()
     }
 
@@ -56,10 +74,13 @@ class VideoCallerFragment : BaseCallFragment(R.layout.fragment_call_video_caller
         TUICallState.instance.isCameraOpen.observe(isCameraOpenObserver)
     }
 
-    private fun inflateFunctionView() {
+    private fun inflateFunctionView(isHideVideoFunction: Boolean = false) {
         val context = context ?: return
         val functionView = CallerWaitingFunctionView(context)
         this@VideoCallerFragment.functionView = functionView
+        if(isHideVideoFunction) {
+            functionView.hideVideoFunctionView()
+        }
         binding.flBottomFunction.removeAllViews()
         binding.flBottomFunction.addView(
             functionView,

+ 1 - 1
module/call/src/main/java/com/adealink/weparty/call/video/fragment/VideoRoomFragment.kt

@@ -38,7 +38,7 @@ class VideoRoomFragment : BaseCallFragment(R.layout.fragment_call_video_accept)
         BgComp(this, TUICallState.instance.getRemoteUid(), binding.vBg).attach()
         VideoRoomTopComp(this, binding.idCallRoomTopBar).attach()
         UserInfoComp(this, TUICallState.instance.getRemoteUid(), binding.idCallRoomTopBar.vUserInfo).attach()
-        VideoRoomBottomComp(this, binding.idCallRoomBottomBar).attach()
+        VideoRoomBottomComp(this, binding.idCallRoomBottomBar, binding.tvMatchNext).attach()
         VideoRoomComp(this, binding.rlBigVideo, binding.rlSmallVideo, binding.ivSwitchCamera).attach()
         CallGiftComp(this, baseDynamicLayers, binding.idCallRoomBottomBar.sendGiftBtn).attach()
     }

+ 6 - 0
module/call/src/main/java/com/adealink/weparty/call/video/widget/CallerWaitingFunctionView.kt

@@ -7,6 +7,7 @@ import androidx.constraintlayout.widget.ConstraintLayout
 import com.adealink.frame.aab.util.getCompatString
 import com.adealink.weparty.call.databinding.LayoutCallVideoCallerWaitingFunctionBinding
 import com.adealink.weparty.call.widget.ICallView
+import com.adealink.weparty.commonui.ext.gone
 import com.adealink.weparty.commonui.toast.util.showToast
 import com.tencent.cloud.tuikit.engine.common.TUICommonDefine
 import com.tencent.qcloud.tuikit.tuicallkit.manager.EngineManager
@@ -58,6 +59,11 @@ class CallerWaitingFunctionView @JvmOverloads constructor(
         }
     }
 
+    fun hideVideoFunctionView() {
+        binding.cameraBtn.gone()
+        binding.switchCameraBtn.gone()
+    }
+
     /**
      * TUICallState.instance.isCameraOpen
      */

+ 20 - 10
module/call/src/main/java/com/adealink/weparty/call/view/floatview/calling/CallingView.kt

@@ -15,6 +15,7 @@ import com.adealink.frame.aab.util.getCompatString
 import com.adealink.frame.image.view.NetworkImageView
 import com.adealink.frame.util.DisplayUtil
 import com.adealink.weparty.call.R
+import com.adealink.weparty.call.match.matchManager
 import com.adealink.weparty.call.util.playDiamondAddAnim
 import com.adealink.weparty.commonui.ext.dpf
 import com.adealink.weparty.commonui.ext.gone
@@ -61,9 +62,6 @@ class CallingView(context: Context) : BaseCallView(context) {
 
     private var onCancelCallback: ICancelViewCallback? = null
 
-    private val mediaType: MediaType?
-        get() = viewModel.mediaType?.get()
-
     private val callStatus: TUICallDefine.Status?
         get() = viewModel.selfUser?.callStatus?.get()
 
@@ -73,7 +71,7 @@ class CallingView(context: Context) : BaseCallView(context) {
     private var earnDiamond = 0
 
     private val mediaTypeObserver: Observer<MediaType> = Observer {
-        updateView(it)
+        updateView(getMediaType())
     }
 
     private val timeCountObserver: Observer<Int> = Observer {
@@ -170,7 +168,7 @@ class CallingView(context: Context) : BaseCallView(context) {
             chatAvatarView?.setImageUrl(userInfo?.url)
         }
 
-        updateView(TUICallState.instance.mediaType.get())
+        updateView(getMediaType())
     }
 
     override fun onConfigurationChanged(newConfig: Configuration?) {
@@ -203,7 +201,19 @@ class CallingView(context: Context) : BaseCallView(context) {
     }
 
     fun onResume() {
-        updateView(TUICallState.instance.mediaType.get())
+        updateView(getMediaType())
+    }
+
+    private fun getMediaType(): MediaType {
+        return if(matchManager.isMatchIdValid()) {
+            if(TUICallState.instance.matchModeOpenCamera.get()) {
+                MediaType.Video
+            } else {
+                MediaType.Audio
+            }
+        } else {
+            TUICallState.instance.mediaType.get()
+        }
     }
 
     fun cancelIncomingView() {
@@ -248,7 +258,7 @@ class CallingView(context: Context) : BaseCallView(context) {
     }
 
     private fun updateCallStatus() {
-        when (mediaType) {
+        when (getMediaType()) {
             MediaType.Audio -> {
                 when (callStatus) {
                     TUICallDefine.Status.Waiting -> {
@@ -318,7 +328,7 @@ class CallingView(context: Context) : BaseCallView(context) {
     }
 
     private fun updateTimer(timer: Int) {
-        when (mediaType) {
+        when (getMediaType()) {
             MediaType.Audio -> {
                 chatTimerView?.post {
                     chatTimerView?.text = DateTimeUtil.formatSecondsTo00(timer)
@@ -338,7 +348,7 @@ class CallingView(context: Context) : BaseCallView(context) {
     }
 
     private fun updatePayer(isPayer: Boolean) {
-        when (mediaType) {
+        when (getMediaType()) {
             MediaType.Audio -> {
                 chatCurrencyImageView?.post {
                     if (isPayer) {
@@ -376,7 +386,7 @@ class CallingView(context: Context) : BaseCallView(context) {
     private fun updateDiamond(diamond: Int) {
         val changed = diamond - earnDiamond
         this.earnDiamond = diamond
-        when (mediaType) {
+        when (getMediaType()) {
             MediaType.Audio -> {
                 chatCurrencyTextView?.post {
                     chatCurrencyTextView?.text = diamond.toString()

+ 9 - 0
module/call/src/main/java/com/adealink/weparty/call/view/floatview/matchnotify/IMatchNotify.kt

@@ -0,0 +1,9 @@
+package com.adealink.weparty.call.view.floatview.matchnotify
+
+import com.adealink.weparty.module.profile.data.UserInfo
+
+interface IMatchNotify {
+    fun showNotifyView(user: UserInfo, timeout: Long)
+
+    fun cancelNotifyView()
+}

+ 11 - 0
module/call/src/main/java/com/adealink/weparty/call/view/floatview/matchnotify/MatchNotifyFloatData.kt

@@ -0,0 +1,11 @@
+package com.adealink.weparty.call.view.floatview.matchnotify
+
+import com.adealink.weparty.commonui.widget.floatview.data.FloatWindowType
+import com.adealink.weparty.commonui.widget.floatview.data.GRAVITY_TOP
+import com.adealink.weparty.commonui.widget.floatview.data.ILayoutFloatData
+
+class MatchNotifyFloatData(private val windowMode: Int) : ILayoutFloatData {
+    override fun windowType(): FloatWindowType = FloatWindowType.CALL_MATCH_NOTIFY
+    override fun windowMode(): Int = windowMode
+    override fun gravity(): Int = GRAVITY_TOP
+}

+ 86 - 0
module/call/src/main/java/com/adealink/weparty/call/view/floatview/matchnotify/MatchNotifyFloatView.kt

@@ -0,0 +1,86 @@
+package com.adealink.weparty.call.view.floatview.matchnotify
+
+import android.app.Activity
+import com.adealink.frame.aab.util.getCompatDimensionPixelSize
+import com.adealink.frame.log.Log
+import com.adealink.weparty.R
+import com.adealink.weparty.call.constant.TAG_CALL_FLOAT_WINDOW
+import com.adealink.weparty.call.constant.TAG_CALL_MATCH_MODE
+import com.adealink.weparty.call.match.matchManager
+import com.adealink.weparty.call.view.floatview.incoming.IncomingView.ICancelViewCallback
+import com.adealink.weparty.commonui.widget.floatview.WindowManagerProxy
+import com.adealink.weparty.commonui.widget.floatview.data.FloatWindowType
+import com.adealink.weparty.commonui.widget.floatview.view.BaseSlideFloatView
+import com.adealink.weparty.commonui.widget.slide.Slide
+import com.adealink.weparty.module.profile.data.UserInfo
+
+/**
+ * author: PengWuliang
+ * date: 2025/8/26
+ * desc:
+ */
+class MatchNotifyFloatView(floatData: MatchNotifyFloatData): BaseSlideFloatView(floatData),
+        IMatchNotify {
+
+    private var matchNotifyView: MatchNotifyView = MatchNotifyView(context)
+
+    override val layoutParams: LayoutParams
+        get() = LayoutParams(LayoutParams.MATCH_PARENT, getCompatDimensionPixelSize(R.dimen.top_float_view_height)).apply {
+            topToTop = LayoutParams.PARENT_ID
+            startToStart = LayoutParams.PARENT_ID
+            endToEnd = LayoutParams.PARENT_ID
+
+            marginStart = getCompatDimensionPixelSize(R.dimen.top_float_view_margin_horizontal)
+            marginEnd = getCompatDimensionPixelSize(R.dimen.top_float_view_margin_horizontal)
+        }
+
+    init {
+        id = R.id.id_float_incoming_view
+    }
+
+    override fun onCreate() {
+        super.onCreate()
+        setContentView(matchNotifyView, layoutParams)
+        matchNotifyView.setOnCancelCallback(object : ICancelViewCallback {
+            override fun onCancel() {
+                this@MatchNotifyFloatView.cancelNotifyView()
+            }
+        })
+    }
+
+    override fun showNotifyView(user: UserInfo, timeout: Long) {
+        if (WindowManagerProxy.getWindowManager().findFloatViewByType(FloatWindowType.CALL_MATCH_NOTIFY) != null) {
+            Log.d(TAG_CALL_MATCH_MODE, "MatchNotifyFloatView.showCallingView return, for floatView already added.")
+            return
+        }
+        Log.d(TAG_CALL_FLOAT_WINDOW, "MatchNotifyFloatView.showNotifyView")
+        matchNotifyView.showNotifyView(user)
+        WindowManagerProxy.getWindowManager().addView(this)
+        matchNotifyView.postDelayed({
+            matchManager.timeout()
+            cancelNotifyView()
+        }, timeout * 1000)
+    }
+
+    override fun cancelNotifyView() {
+        Log.d(TAG_CALL_FLOAT_WINDOW, "MatchNotifyFloatView.cancelNotifyView")
+        if (isAttachedToWindow) {
+            WindowManagerProxy.getWindowManager().removeView(this, reason = "MatchNotifyFloatView.cancelNotifyView")
+        }
+    }
+
+    override fun slideDirection(): Slide.SlideDirection {
+        return Slide.SlideDirection.HORIZONTAL
+    }
+
+    override fun onSlideFinish(slideToEnd: Boolean) {
+        if (slideToEnd) {
+            matchManager.timeout()
+        }
+    }
+
+    override fun onActivityChange(activity: Activity) {
+        super.onActivityChange(activity)
+        removeSelf("onActivityChange")
+    }
+}

+ 69 - 0
module/call/src/main/java/com/adealink/weparty/call/view/floatview/matchnotify/MatchNotifyView.kt

@@ -0,0 +1,69 @@
+package com.adealink.weparty.call.view.floatview.matchnotify
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import androidx.constraintlayout.widget.ConstraintLayout
+import com.adealink.frame.aab.util.getCompatColor
+import com.adealink.frame.aab.util.getCompatString
+import com.adealink.frame.base.Rlt
+import com.adealink.frame.log.Log
+import com.adealink.frame.mvvm.lifecycle.viewScope
+import com.adealink.weparty.R
+import com.adealink.weparty.call.constant.TAG_CALL_MATCH_MODE
+import com.adealink.weparty.call.databinding.CallIncomingFloatViewBinding
+import com.adealink.weparty.call.match.matchManager
+import com.adealink.weparty.call.view.floatview.incoming.IncomingView.ICancelViewCallback
+import com.adealink.weparty.module.profile.data.UserInfo
+import com.tencent.cloud.tuikit.engine.call.TUICallDefine
+import com.tencent.qcloud.tuikit.tuicallkit.utils.PermissionRequest
+import kotlinx.coroutines.launch
+
+/**
+ * author: PengWuliang
+ * date: 2025/8/26
+ * desc:
+ */
+class MatchNotifyView @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyleAttr: Int = 0,
+) : ConstraintLayout(context, attrs, defStyleAttr) {
+    private val binding = CallIncomingFloatViewBinding.inflate(LayoutInflater.from(context), this, true)
+    private var onCancelCallback: ICancelViewCallback? = null
+
+    init {
+        initView()
+    }
+
+    private fun initView() {
+        binding.rejectBtn.setOnClickListener {
+            matchManager.reject()
+            onCancelCallback?.onCancel()
+        }
+        binding.acceptBtn.setOnClickListener {
+            viewScope.launch {
+                val rlt = PermissionRequest.requestPermission(mediaType = TUICallDefine.MediaType.Video)
+                if(rlt is Rlt.Success) {
+                    matchManager.accept()
+                    onCancelCallback?.onCancel()
+                }
+            }
+        }
+    }
+
+    fun setOnCancelCallback(callback: ICancelViewCallback?) {
+        this.onCancelCallback = callback
+    }
+
+    fun showNotifyView(userInfo: UserInfo) {
+        Log.d(TAG_CALL_MATCH_MODE, "showNotifyView")
+        binding.ivAvatar.setImageUrl(userInfo.url)
+        binding.ivCountry.setImageUrl(userInfo.flag)
+        binding.tvName.text = userInfo.name
+        binding.tvTips.apply {
+            text = getCompatString(R.string.common_random_chat)
+            setTextColor(getCompatColor(R.color.color_FFC251FF))
+        }
+    }
+}

+ 85 - 0
module/call/src/main/java/com/adealink/weparty/call/viewmodel/CallViewModel.kt

@@ -8,15 +8,22 @@ import com.adealink.frame.mvvm.viewmodel.BaseViewModel
 import com.adealink.frame.network.data.Res
 import com.adealink.weparty.App
 import com.adealink.weparty.call.constant.TAG_CALL_FLOW
+import com.adealink.weparty.call.constant.TAG_CALL_MATCH_MODE
+import com.adealink.weparty.call.data.CommonRequest
 import com.adealink.weparty.call.data.MerchantFreeCallData
+import com.adealink.weparty.call.data.SwitchCallModeAnswer
+import com.adealink.weparty.call.data.SwitchCallModeAsk
+import com.adealink.weparty.call.data.SwitchCallModeRequest
 import com.adealink.weparty.call.data.toCallStatus
 import com.adealink.weparty.call.datasource.remote.CallHttpService
 import com.adealink.weparty.call.manager.ICallListener
 import com.adealink.weparty.call.manager.callManager
+import com.adealink.weparty.match.AnswerType
 import com.adealink.weparty.module.backpack.GetExperienceCardRes
 import com.adealink.weparty.module.call.data.CallStatus
 import com.adealink.weparty.module.call.viewmodel.ICallViewModel
 import com.adealink.weparty.module.profile.ProfileModule
+import com.adealink.weparty.module.profile.data.UserInfo
 import com.tencent.cloud.tuikit.engine.call.TUICallDefine
 import com.tencent.qcloud.tuikit.tuicallkit.state.TUICallState
 import com.trtc.tuikit.common.livedata.Observer
@@ -25,7 +32,9 @@ import kotlinx.coroutines.launch
 open class CallViewModel : BaseViewModel(), ICallViewModel, ICallListener {
 
     override val callStatusLD: LiveData<CallStatus> = MutableLiveData()
+    override val showMatchEntrance: LiveData<Boolean> = MutableLiveData()
     val tCallStatusLD = MutableLiveData<TUICallDefine.Status>()
+    val switchCallModeAskLD = MutableLiveData<SwitchCallModeAsk>()
     val merchantFreeCallLD: LiveData<Rlt<MerchantFreeCallData>> = MutableLiveData()
 
     init {
@@ -60,9 +69,85 @@ open class CallViewModel : BaseViewModel(), ICallViewModel, ICallListener {
         }
     }
 
+    override fun switchMode(roomId: String, callMode: Int): LiveData<Rlt<Boolean>> {
+        val liveData = MutableLiveData<Rlt<Boolean>>()
+        viewModelScope.launch {
+            when(val rlt = callHttpService.switchCallMode(SwitchCallModeRequest(roomId, callMode))) {
+                is Rlt.Success -> {
+                    liveData.send(Rlt.Success(true))
+                    Log.d(TAG_CALL_MATCH_MODE, "switchMode roomId: $roomId")
+                }
+                is Rlt.Failed -> {
+                    liveData.send(Rlt.Failed(rlt.error))
+                    Log.d(TAG_CALL_MATCH_MODE, "switchMode fail, error: ${rlt.error}")
+                }
+            }
+        }
+        return liveData
+    }
+
+    override fun switchModeAnswer(roomId: String, answerType: Int): LiveData<Rlt<Boolean>> {
+        val liveData = MutableLiveData<Rlt<Boolean>>()
+        viewModelScope.launch {
+            when(val rlt = callHttpService.answerSwitchCallMode(SwitchCallModeAnswer(roomId, answerType))) {
+                is Rlt.Success -> {
+                    liveData.send(Rlt.Success(true))
+                    if(answerType == AnswerType.ACCEPT.value) {
+                        TUICallState.instance.matchModeOpenCamera.set(true)
+                    }
+                    Log.d(TAG_CALL_MATCH_MODE, "switchModeAnswer roomId: ${rlt.data.data?.roomId}")
+                }
+                is Rlt.Failed -> {
+                    liveData.send(Rlt.Failed(rlt.error))
+                    Log.d(TAG_CALL_MATCH_MODE, "switchModeAnswer fail, error: ${rlt.error}")
+                }
+            }
+        }
+        return liveData
+    }
+
+    override fun getMatchUserList(): LiveData<Rlt<List<UserInfo>>> {
+        val liveData = MutableLiveData<Rlt<List<UserInfo>>>()
+        viewModelScope.launch {
+            when(val rlt = callHttpService.getMatchUserList(CommonRequest())) {
+                is Rlt.Success -> {
+                    val res = rlt.data.data?.userList ?: emptyList()
+                    liveData.send(Rlt.Success(res))
+                    Log.d(TAG_CALL_MATCH_MODE, "getMatchUserList -> $res")
+                }
+                is Rlt.Failed -> {
+                    liveData.send(Rlt.Failed(rlt.error))
+                    Log.d(TAG_CALL_MATCH_MODE, "getMatchUserList fail, error: ${rlt.error}")
+                }
+            }
+        }
+        return liveData
+    }
+
+    override fun getCallMatchEntrance() {
+        viewModelScope.launch {
+            when(val rlt = callHttpService.getCallMatchEntrance(CommonRequest())) {
+                is Rlt.Success -> {
+                    val res = rlt.data.data?.show ?: false
+                    showMatchEntrance.send(res)
+                    Log.d(TAG_CALL_MATCH_MODE, "getCallMatchEntrance -> $res")
+                }
+                else -> {
+                    showMatchEntrance.send(false)
+                    Log.d(TAG_CALL_MATCH_MODE, "getCallMatchEntrance -> false")
+                }
+            }
+        }
+    }
+
     override fun onExperienceCardChanged(res: Rlt.Success<Res<GetExperienceCardRes>>) {
         experienceCardLd.send(res)
     }
+
+    override fun handleVideoInvitation(data: SwitchCallModeAsk) {
+        switchCallModeAskLD.send(data)
+    }
+
     val mediaTypeLD = MutableLiveData<TUICallDefine.MediaType>()
 
     private var callStatusObserver = Observer<TUICallDefine.Status> {

+ 65 - 0
module/call/src/main/java/com/adealink/weparty/call/widget/CameraOpen.kt

@@ -0,0 +1,65 @@
+package com.adealink.weparty.call.widget
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MediatorLiveData
+import androidx.lifecycle.Observer
+import com.adealink.frame.coroutine.dispatcher.Dispatcher
+import com.adealink.frame.log.Log
+import com.adealink.frame.util.isMainThread
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.SupervisorJob
+
+/**
+ * author: PengWuliang
+ * date: 2025/8/28
+ * desc:
+ */
+val cameraOpen by lazy {
+    CameraOpen(
+        arrayListOf(
+            matchModeCameraOpen
+        )
+    )
+}
+
+open class CameraOpen(private val children: ArrayList<CameraOpen>, val tag: String = "CameraOpen") :
+        CoroutineScope by CoroutineScope(SupervisorJob() + Dispatcher.UI) {
+
+    private val _openLD = MediatorLiveData<Boolean>()
+    val openLD: LiveData<Boolean> = _openLD
+    private val open: Boolean = false
+    private val observer by lazy { Observer<Boolean> { collectChildren() } }
+
+    init {
+        children.forEach {
+            _openLD.addSource(it._openLD, observer)
+        }
+    }
+
+    fun post(open: Boolean) {
+        Log.d(tag, "post, open:$open")
+        send(open)
+    }
+
+    private fun collectChildren() {
+        if (children.isEmpty()) {
+            Log.d(tag, "collectChildren, children is empty, open:$open")
+            send(open)
+            return
+        }
+
+        var result = true
+        children.forEach {
+            result = result && it.open
+        }
+        send(result)
+    }
+
+    private fun send(value: Boolean) {
+        if (isMainThread()) {
+            _openLD.value = value
+        } else {
+            _openLD.postValue(value)
+        }
+    }
+}

+ 19 - 0
module/call/src/main/java/com/adealink/weparty/call/widget/MatchModeCameraOpen.kt

@@ -0,0 +1,19 @@
+package com.adealink.weparty.call.widget
+
+import kotlinx.coroutines.launch
+
+/**
+ * author: PengWuliang
+ * date: 2025/8/28
+ * desc:
+ */
+
+val matchModeCameraOpen by lazy { MatchModeCameraOpen() }
+
+class MatchModeCameraOpen(children: ArrayList<CameraOpen> = arrayListOf()) : CameraOpen(children, "MatchModeCameraOpen") {
+    init {
+        launch {
+            post(false)
+        }
+    }
+}

+ 8 - 0
module/call/src/main/java/com/tencent/qcloud/tuikit/tuicallkit/data/User.kt

@@ -99,4 +99,12 @@ class User {
                 + ", spendCoin=" + spendCoin.get()
                 + "}")
     }
+
+    fun copy(): User {
+        val user = User()
+        user.id = id
+        user.userInfo.set(userInfo.get())
+        user.isPayer.set(isPayer.get())
+        return user
+    }
 }

+ 1 - 0
module/call/src/main/java/com/tencent/qcloud/tuikit/tuicallkit/manager/EngineManager.kt

@@ -113,6 +113,7 @@ class EngineManager private constructor(context: Context) {
 
                         TUICallState.instance.selfUser.get().callRole.set(TUICallDefine.Role.Caller)
                         TUICallState.instance.selfUser.get().callStatus.set(TUICallDefine.Status.Waiting)
+                        TUICallState.instance.callUserData.set(callUserData)
                         initAudioPlayDevice()
                         callback?.onSuccess()
                     }

+ 13 - 0
module/call/src/main/java/com/tencent/qcloud/tuikit/tuicallkit/state/TUICallState.kt

@@ -9,9 +9,12 @@ import com.adealink.frame.log.Log
 import com.adealink.frame.util.safeToLong
 import com.adealink.weparty.call.R
 import com.adealink.weparty.call.constant.TAG_CALL_FLOW
+import com.adealink.weparty.call.constant.TAG_CALL_MATCH_MODE
+import com.adealink.weparty.call.data.CallEndStatsNotify
 import com.adealink.weparty.call.data.CallUserData
 import com.adealink.weparty.call.manager.callManager
 import com.adealink.weparty.call.manager.callPingManager
+import com.adealink.weparty.call.match.matchManager
 import com.adealink.weparty.commonui.toast.util.showToast
 import com.tencent.cloud.tuikit.engine.call.TUICallDefine
 import com.tencent.cloud.tuikit.engine.call.TUICallObserver
@@ -42,7 +45,9 @@ class TUICallState {
     var callUserData = LiveData<CallUserData>()
     //var groupId = LiveData<String?>()
 
+    var matchRemoteUser = User()
     var isCameraOpen = LiveData<Boolean>()
+    var matchModeOpenCamera = LiveData<Boolean>()
     var scene = LiveData<TUICallDefine.Scene>()
     var mediaType = LiveData<TUICallDefine.MediaType>()
     var timeCount = LiveData<Int>()
@@ -81,6 +86,7 @@ class TUICallState {
         roomId.set(null)
         //groupId.set(null)
         isCameraOpen.set(false)
+        matchModeOpenCamera.set(false)
         isFrontCamera.set(TUICommonDefine.Camera.Front)
         isMicrophoneMute.set(false)
         audioPlayoutDevice.set(AudioPlaybackDevice.Speakerphone) //固定为喇叭
@@ -178,6 +184,7 @@ class TUICallState {
                 isCameraOpen.set(false)
             }
 
+            matchRemoteUser = getRemoteUser()?.copy() ?: User()
             TUICore.notifyEvent(Constants.EVENT_TUICALLKIT_CHANGED, Constants.EVENT_START_INCOMING_VIEW, HashMap())
         }
 
@@ -226,6 +233,7 @@ class TUICallState {
                 EngineManager.instance.openMicrophone(null)
             }
 
+            matchRemoteUser = getRemoteUser()?.copy() ?: User()
             //扣费需要后端二次确认后才开始计时
             //startTimeCount()
             startForegroundService()
@@ -431,6 +439,9 @@ class TUICallState {
 
     fun clear() {
         Log.d(TAG_CALL_FLOW, "clear")
+        matchRemoteUser.clear()
+        matchManager.clearData()
+
         //reverse1v1CallRenderView = false
         isShowFloatView.set(false)
         showLargeViewUserId.set(null)
@@ -451,6 +462,7 @@ class TUICallState {
         callUserData.set(CallUserData.EMPTY_USER_DATA)
         //groupId.set(null)
         isCameraOpen.set(false)
+        matchModeOpenCamera.set(false)
         isFrontCamera.set(TUICommonDefine.Camera.Front)
         isMicrophoneMute.set(false)
         audioPlayoutDevice.set(AudioPlaybackDevice.Speakerphone)
@@ -563,6 +575,7 @@ class TUICallState {
         }
         user.callStatus.set(TUICallDefine.Status.Accept)
         if (!remoteUserList.get().contains(user) && !userId.equals(selfUser.get().id)) {
+            Log.d(TAG_CALL_MATCH_MODE, "updateUserOnEnter: $user")
             remoteUserList.add(user)
         }
         UserInfoUtils.updateUserInfo(user)

+ 37 - 1
module/call/src/main/java/com/tencent/qcloud/tuikit/tuicallkit/utils/PermissionRequest.kt

@@ -7,6 +7,9 @@ import android.content.Context
 import android.os.Build
 import androidx.fragment.app.FragmentActivity
 import com.adealink.frame.aab.util.getCompatString
+import com.adealink.frame.base.IError
+import com.adealink.frame.base.Rlt
+import com.adealink.frame.coroutine.dispatcher.Dispatcher
 import com.adealink.frame.log.Log
 import com.adealink.frame.util.AppUtil
 import com.adealink.weparty.call.R
@@ -17,9 +20,42 @@ import com.tencent.cloud.tuikit.engine.call.TUICallDefine
 import com.tencent.qcloud.tuicore.TUIConfig
 import com.tencent.qcloud.tuicore.permission.PermissionCallback
 import com.tencent.qcloud.tuicore.permission.PermissionRequester
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withContext
 
 object PermissionRequest {
-    fun requestPermissions(context: Context, type: TUICallDefine.MediaType, callback: PermissionCallback?) {
+
+    suspend fun requestPermission(
+        mediaType: TUICallDefine.MediaType
+    ): Rlt<Any> {
+        return withContext(Dispatcher.UI) {
+            suspendCancellableCoroutine { continuation ->
+                PermissionRequest.requestPermissions(
+                    AppUtil.appContext,
+                    mediaType,
+                    object : PermissionCallback() {
+                        override fun onGranted() {
+                            //主叫直接返回结果
+                            if (continuation.isActive) {
+                                continuation.resume(Rlt.Success(Any()), null)
+                            }
+                        }
+
+                        override fun onDenied() {
+                            if (continuation.isActive) {
+                                continuation.resume(Rlt.Failed(IError()), null)
+                            }
+                        }
+                    })
+            }
+        }
+    }
+
+    fun requestPermissions(
+        context: Context,
+        type: TUICallDefine.MediaType,
+        callback: PermissionCallback?
+    ) {
         val permissions = arrayListOf<String>()
         var tips = ""
         when (type) {

BIN
module/call/src/main/res/drawable-xhdpi/call_match_bg.webp


BIN
module/call/src/main/res/drawable-xhdpi/call_match_next_bg.webp


BIN
module/call/src/main/res/drawable-xhdpi/call_payee_invitation_bg.webp


BIN
module/call/src/main/res/drawable-xhdpi/call_payer_invitation_bg.webp


+ 11 - 0
module/call/src/main/res/drawable/call_match_btn_bg.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <corners android:radius="27dp" />
+    <gradient
+        android:endColor="#F051FF"
+        android:startColor="#C251FF"
+        android:type="linear" />
+    <stroke
+        android:width="1dp"
+        android:color="#33FFFFFF" />
+</shape>

+ 7 - 0
module/call/src/main/res/drawable/call_settlement_bg.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <solid android:color="@color/color_0AFF5784"/>
+    <corners android:radius="15dp"/>
+    <stroke android:width="1dp" android:color="#0AFFFFFF"/>
+</shape>

+ 12 - 0
module/call/src/main/res/layout/activity_call_settlement.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <androidx.fragment.app.FragmentContainerView
+        android:id="@+id/fragment_container_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 2 - 2
module/call/src/main/res/layout/call_incoming_float_view.xml

@@ -35,7 +35,7 @@
             app:layout_constraintTop_toTopOf="@id/iv_avatar"
             app:srcCompat="@drawable/common_online_ic" />
 
-        <com.adealink.weparty.commonui.widget.AutoMarqueeTextView
+        <androidx.appcompat.widget.AppCompatTextView
             android:id="@+id/tv_name"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
@@ -70,7 +70,7 @@
             app:layout_constraintTop_toTopOf="@id/tv_name"
             tools:visibility="visible" />
 
-        <com.adealink.weparty.commonui.widget.AutoMarqueeTextView
+        <androidx.appcompat.widget.AppCompatTextView
             android:id="@+id/tv_tips"
             android:layout_width="0dp"
             android:layout_height="wrap_content"

+ 81 - 0
module/call/src/main/res/layout/dialog_payee_video_invitation.xml

@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    tools:background="@color/color_222222">
+
+    <androidx.appcompat.widget.AppCompatImageView
+        android:id="@+id/iv_bg"
+        android:layout_width="300dp"
+        android:layout_height="205dp"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        android:scaleX="@integer/locale_mirror_scaleX"
+        app:srcCompat="@drawable/call_payee_invitation_bg" />
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/tv_title"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="16dp"
+        android:layout_marginTop="39dp"
+        android:text="@string/call_send_video_invitation"
+        android:textColor="@color/color_222222"
+        android:textSize="14sp"
+        android:textStyle="bold"
+        app:layout_constraintStart_toStartOf="@id/iv_bg"
+        app:layout_constraintTop_toTopOf="@id/iv_bg" />
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/tv_content"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="16dp"
+        android:gravity="center"
+        android:paddingHorizontal="16dp"
+        android:text="@string/call_change_video_earn"
+        android:textColor="@color/color_777777"
+        android:textSize="13sp"
+        app:layout_constraintEnd_toEndOf="@id/iv_bg"
+        app:layout_constraintStart_toStartOf="@id/iv_bg"
+        app:layout_constraintTop_toBottomOf="@id/tv_title" />
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/tv_cancel"
+        android:layout_width="0dp"
+        android:layout_height="44dp"
+        android:layout_marginHorizontal="16dp"
+        android:layout_marginBottom="15dp"
+        android:background="@drawable/common_cancel_radius_35_bg"
+        android:gravity="center"
+        android:text="@string/commonui_cancel"
+        android:textColor="@color/color_app_main"
+        android:textSize="16sp"
+        android:textStyle="bold"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toStartOf="@id/tv_confirm"
+        app:layout_constraintHorizontal_weight="0.4"
+        app:layout_constraintStart_toStartOf="@id/iv_bg"
+        app:layout_constraintWidth_default="spread" />
+
+    <com.adealink.weparty.commonui.widget.CommonButton
+        android:id="@+id/tv_confirm"
+        android:layout_width="0dp"
+        android:layout_height="44dp"
+        android:layout_marginEnd="16dp"
+        android:layout_marginBottom="15dp"
+        android:gravity="center"
+        android:text="@string/commonui_confirm"
+        android:textSize="16sp"
+        android:textStyle="bold"
+        app:button_radius="35dp"
+        app:is_strong="true"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="@id/iv_bg"
+        app:layout_constraintHorizontal_weight="0.6"
+        app:layout_constraintStart_toEndOf="@id/tv_cancel"
+        app:layout_constraintWidth_default="spread" />
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 104 - 0
module/call/src/main/res/layout/dialog_payer_video_invitation.xml

@@ -0,0 +1,104 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    tools:background="@color/color_222222">
+
+    <androidx.appcompat.widget.AppCompatImageView
+        android:id="@+id/iv_bg"
+        android:layout_width="300dp"
+        android:layout_height="252dp"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:srcCompat="@drawable/call_payer_invitation_bg" />
+
+    <com.adealink.weparty.commonui.imageview.AvatarView
+        android:id="@+id/iv_avatar"
+        android:layout_width="84dp"
+        android:layout_height="84dp"
+        android:layout_marginTop="3dp"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/tv_name"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="-8dp"
+        android:background="@drawable/common_white_radius_12_bg"
+        android:paddingHorizontal="8dp"
+        android:paddingVertical="3dp"
+        android:textColor="@color/color_222222"
+        android:textSize="13sp"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/iv_avatar"
+        tools:text="Rella" />
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/tv_title"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="12dp"
+        android:text="@string/call_send_video_invitation"
+        android:textColor="@color/color_222222"
+        android:textSize="14sp"
+        android:textStyle="bold"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/tv_name" />
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/tv_content"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginHorizontal="16dp"
+        android:layout_marginTop="8dp"
+        android:gravity="center"
+        android:text="@string/call_change_video_cost"
+        android:textColor="@color/color_777777"
+        android:textSize="13sp"
+        app:layout_constraintEnd_toEndOf="@id/iv_bg"
+        app:layout_constraintStart_toStartOf="@id/iv_bg"
+        app:layout_constraintTop_toBottomOf="@id/tv_title" />
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/tv_cancel"
+        android:layout_width="0dp"
+        android:layout_height="44dp"
+        android:layout_marginHorizontal="16dp"
+        android:layout_marginBottom="5dp"
+        android:background="@drawable/common_cancel_radius_35_bg"
+        android:gravity="center"
+        android:text="@string/commonui_cancel"
+        android:textColor="@color/color_app_main"
+        android:textSize="16sp"
+        android:textStyle="bold"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toStartOf="@id/tv_confirm"
+        app:layout_constraintHorizontal_weight="0.4"
+        app:layout_constraintStart_toStartOf="@id/iv_bg"
+        app:layout_constraintWidth_default="spread" />
+
+    <com.adealink.weparty.commonui.widget.CommonButton
+        android:id="@+id/tv_confirm"
+        android:layout_width="0dp"
+        android:layout_height="44dp"
+        android:layout_marginEnd="16dp"
+        android:layout_marginBottom="5dp"
+        android:gravity="center"
+        android:text="@string/commonui_confirm"
+        android:textSize="16sp"
+        android:textStyle="bold"
+        app:button_radius="35dp"
+        app:is_strong="true"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="@id/iv_bg"
+        app:layout_constraintHorizontal_weight="0.6"
+        app:layout_constraintStart_toEndOf="@id/tv_cancel"
+        app:layout_constraintWidth_default="spread" />
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 197 - 0
module/call/src/main/res/layout/fragment_call_settlement.xml

@@ -0,0 +1,197 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <include
+        android:id="@+id/v_bg"
+        layout="@layout/layout_call_bg"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+
+    <com.adealink.weparty.commonui.widget.CommonTopBar
+        android:id="@+id/top_bar"
+        android:layout_width="match_parent"
+        android:layout_height="44dp"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/cl_bg"
+        android:layout_width="match_parent"
+        android:layout_height="150dp"
+        android:layout_marginHorizontal="40dp"
+        android:background="@drawable/call_settlement_bg"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintVertical_bias="0.3">
+
+        <com.adealink.frame.image.view.NetworkImageView
+            android:id="@+id/iv_national_flag"
+            android:layout_width="16dp"
+            android:layout_height="16dp"
+            app:layout_constraintBottom_toBottomOf="@id/tv_name"
+            app:layout_constraintEnd_toStartOf="@id/tv_name"
+            app:layout_constraintHorizontal_chainStyle="packed"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="@id/tv_name" />
+
+        <androidx.appcompat.widget.AppCompatTextView
+            android:id="@+id/tv_name"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="5dp"
+            android:layout_marginTop="42dp"
+            android:textColor="@color/white"
+            android:textSize="16sp"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toEndOf="@id/iv_national_flag"
+            app:layout_constraintTop_toTopOf="parent" />
+
+        <com.adealink.weparty.commonui.widget.CommonButton
+            android:id="@+id/tv_follow"
+            android:layout_width="0dp"
+            android:layout_height="40dp"
+            android:layout_marginStart="26dp"
+            android:layout_marginTop="16dp"
+            android:gravity="center"
+            android:text="@string/common_follow"
+            android:textSize="16sp"
+            app:button_radius="35dp"
+            app:is_strong="true"
+            app:layout_constraintEnd_toStartOf="@id/tv_chat"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@id/tv_name" />
+
+        <androidx.appcompat.widget.AppCompatTextView
+            android:id="@+id/tv_chat"
+            android:layout_width="0dp"
+            android:layout_height="40dp"
+            android:layout_marginStart="20dp"
+            android:layout_marginTop="16dp"
+            android:layout_marginEnd="26dp"
+            android:background="@drawable/common_white_radius_35_bg"
+            android:gravity="center"
+            android:text="@string/common_chat"
+            android:textColor="@color/color_222222"
+            android:textSize="16sp"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toEndOf="@id/tv_follow"
+            app:layout_constraintTop_toBottomOf="@id/tv_name" />
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+    <com.adealink.weparty.commonui.imageview.AvatarView
+        android:id="@+id/iv_avatar"
+        android:layout_width="60dp"
+        android:layout_height="60dp"
+        android:layout_marginTop="-30dp"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="@id/cl_bg"
+        app:roundingBorderWidth="2dp"
+        app:roundingBorderColor="@color/white"/>
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/cl_settlement"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginHorizontal="40dp"
+        android:layout_marginTop="27dp"
+        android:background="@drawable/call_settlement_bg"
+        android:paddingHorizontal="16dp"
+        android:paddingVertical="20dp"
+        app:constraint_layout_round_corner="15dp"
+        app:layout_constraintTop_toBottomOf="@id/cl_bg">
+
+        <androidx.appcompat.widget.AppCompatTextView
+            android:id="@+id/tv_duration"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/call_duration"
+            android:textColor="@color/white"
+            android:textSize="13sp"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent" />
+
+        <androidx.appcompat.widget.AppCompatTextView
+            android:id="@+id/tv_duration_value"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textColor="@color/white"
+            android:textSize="13sp"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            tools:text="00:03:34" />
+
+        <androidx.appcompat.widget.AppCompatTextView
+            android:id="@+id/tv_earn"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="29dp"
+            android:text="@string/call_earn_diamonds"
+            android:textColor="@color/white"
+            android:textSize="13sp"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@id/tv_duration" />
+
+        <androidx.appcompat.widget.AppCompatTextView
+            android:id="@+id/tv_earn_value"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="29dp"
+            android:drawableEnd="@drawable/common_diamond_24_ic"
+            android:drawablePadding="2dp"
+            android:textColor="@color/white"
+            android:textSize="13sp"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toBottomOf="@id/tv_duration_value" />
+
+        <androidx.constraintlayout.widget.Group
+            android:id="@+id/group_earn"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:visibility="gone"
+            app:constraint_referenced_ids="tv_earn, tv_earn_value" />
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/btn_match"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:background="@drawable/call_match_btn_bg"
+        android:gravity="center"
+        android:minWidth="200dp"
+        android:minHeight="54dp"
+        android:text="@string/call_match_next"
+        android:textColor="@color/white"
+        android:textSize="19sp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintVertical_bias="0.76" />
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/tv_coin_cost"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="12dp"
+        tools:text="cost 4000 coin/min"
+        android:textColor="@color/color_FF848484"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/btn_match" />
+
+    <androidx.constraintlayout.widget.Group
+        android:id="@+id/group_payer"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:visibility="gone"
+        app:constraint_referenced_ids="btn_match" />
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 12 - 0
module/call/src/main/res/layout/fragment_call_video_accept.xml

@@ -52,6 +52,18 @@
         app:layout_constraintTop_toBottomOf="@id/v_small_video"
         app:srcCompat="@drawable/call_switch_camera_sel" />
 
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/tv_match_next"
+        android:layout_width="95dp"
+        android:layout_height="32dp"
+        android:background="@drawable/call_match_next_bg"
+        android:gravity="center"
+        android:text="@string/call_match_next"
+        android:textColor="@color/white"
+        android:textSize="13sp"
+        app:layout_constraintBottom_toTopOf="@id/id_call_room_bottom_bar"
+        app:layout_constraintEnd_toEndOf="parent" />
+
     <include
         android:id="@id/id_call_room_bottom_bar"
         layout="@layout/layout_call_video_room_bottom_bar"

+ 9 - 0
module/call/src/main/res/layout/fragment_call_video_caller_waiting.xml

@@ -48,6 +48,15 @@
             app:srcCompat="@drawable/call_mini_ic" />
     </androidx.constraintlayout.widget.ConstraintLayout>
 
+    <include
+        android:id="@+id/chat_user_info"
+        layout="@layout/layout_call_userinfo"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintVertical_bias="0.4" />
+
     <androidx.constraintlayout.widget.ConstraintLayout
         android:id="@+id/cl_call_info"
         android:layout_width="0dp"

+ 101 - 0
module/call/src/main/res/layout/layout_call_match_activity.xml

@@ -0,0 +1,101 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@drawable/call_match_bg">
+
+    <com.adealink.weparty.effect.WeAnimView
+        android:id="@+id/anim_before_match"
+        android:layout_width="match_parent"
+        android:layout_height="816dp"
+        app:layout_constraintTop_toTopOf="parent"/>
+
+    <com.adealink.weparty.effect.WeAnimView
+        android:id="@+id/anim_before_match_avatar"
+        android:layout_width="match_parent"
+        android:layout_height="816dp"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <com.adealink.weparty.effect.WeAnimView
+        android:id="@+id/anim_matching"
+        android:layout_width="match_parent"
+        android:layout_height="816dp"
+        app:layout_constraintTop_toTopOf="parent"/>
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/tv_title"
+        android:layout_width="wrap_content"
+        android:layout_height="44dp"
+        android:text="@string/common_random_chat"
+        android:textColor="@color/white"
+        android:textSize="20sp"
+        android:gravity="center"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:shadowColor="#9D35D6"
+        app:shadowDx="2"
+        app:shadowDy="3"
+        app:shadowRadius="3" />
+
+    <androidx.appcompat.widget.AppCompatImageView
+        android:id="@+id/iv_back"
+        android:layout_width="24dp"
+        android:layout_height="24dp"
+        android:layout_marginStart="16dp"
+        app:layout_constraintBottom_toBottomOf="@id/tv_title"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="@id/tv_title"
+        app:srcCompat="@drawable/commonui_back_white_48_ic" />
+
+    <androidx.appcompat.widget.AppCompatImageView
+        android:id="@+id/iv_minimize"
+        android:layout_width="24dp"
+        android:layout_height="24dp"
+        android:layout_marginEnd="16dp"
+        app:layout_constraintBottom_toBottomOf="@id/tv_title"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toTopOf="@id/tv_title"
+        app:srcCompat="@drawable/call_mini_ic" />
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/tv_match_tips"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginBottom="16dp"
+        android:text="@string/call_matching"
+        android:textColor="@color/color_FF901ED0"
+        android:textSize="19sp"
+        app:layout_constraintBottom_toTopOf="@id/btn_match"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent" />
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/btn_match"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:background="@drawable/call_match_btn_bg"
+        android:gravity="center"
+        android:minWidth="200dp"
+        android:minHeight="54dp"
+        android:text="@string/call_match_start"
+        android:textColor="@color/white"
+        android:textSize="19sp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintVertical_bias="0.78" />
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/tv_coin_cost"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="12dp"
+        android:text="cost 4000 coin/min"
+        android:textColor="@color/color_FF848484"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/btn_match" />
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 0 - 16
module/call/src/main/res/layout/layout_call_userinfo.xml

@@ -91,22 +91,6 @@
                 android:visibility="gone"
                 tools:visibility="visible" />
 
-<!--            <com.adealink.weparty.module.level.label.UserWealthLevelView-->
-<!--                android:id="@+id/level_view"-->
-<!--                android:layout_width="wrap_content"-->
-<!--                android:layout_height="18dp"-->
-<!--                android:layout_marginStart="4dp"-->
-<!--                android:visibility="gone"-->
-<!--                tools:visibility="visible" />-->
-
-<!--            <com.adealink.frame.image.view.NetworkImageView-->
-<!--                android:id="@+id/iv_charm_level"-->
-<!--                android:layout_width="43dp"-->
-<!--                android:layout_height="14dp"-->
-<!--                android:layout_marginStart="4dp"-->
-<!--                android:visibility="gone"-->
-<!--                tools:visibility="visible" />-->
-
             <com.adealink.weparty.module.level.label.ChatAchievementView
                 android:id="@+id/iv_chat_achievement"
                 android:layout_width="wrap_content"

+ 3 - 1
module/call/src/main/res/layout/layout_call_video_room_bottom_bar.xml

@@ -88,11 +88,13 @@
         android:layout_width="54dp"
         android:layout_height="54dp"
         android:layout_marginTop="20dp"
+        android:visibility="gone"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintEnd_toStartOf="@id/hangup_btn"
         app:layout_constraintStart_toEndOf="@id/speaker_btn"
         app:layout_constraintTop_toBottomOf="@id/barrier_bottom"
-        app:srcCompat="@drawable/call_camera_sel" />
+        app:srcCompat="@drawable/call_camera_sel"
+        tools:visibility="visible" />
 
     <androidx.appcompat.widget.AppCompatImageView
         android:id="@+id/hangup_btn"

+ 96 - 76
module/call/src/main/res/values-ar/strings.xml

@@ -1,78 +1,98 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
-   <string name="call_1v1">يتصل</string>
-   <string name="call_chat">مكالمات الدردشة</string>
-   <string name="call_video">مكالمات الفيديو</string>
-   <string name="call_target_uid_is_invalid">معرف المستخدم المتصل غير صالح</string>
-   <string name="call_target_uid_is_self">لا يمكنك الاتصال بنفسك.</string>
-   <string name="call_media_type_unknown">نوع المكالمة غير معروف</string>
-   <string name="call_answer_using_voice">الرد الصوتي</string>
-   <string name="call_request_permisson_fail">فشل طلب الإذن</string>
-   <string name="call_target_uid_is_empty">معرف المكالمة فارغ</string>
-   <string name="call_fail_for_exceeding_max_user">فشلت المكالمات، وتجاوز الحد الأقصى لعدد المستخدمين</string>
-   <string name="call_start_call_fail_room_id_invalid">فشل بدء المكالمة (معرف الغرفة غير صالح)</string>
-   <string name="call_callee_hangup">أغلق الطرف الآخر الهاتف، وانتهت المكالمة</string>
-   <string name="call_callee_reject">تم رفض المكالمة من قبل الطرف الآخر</string>
-   <string name="call_callee_no_response">ولم يرد الطرف الآخر</string>
-   <string name="call_line_busy">الخط مشغول</string>
-   <string name="call_video_call">مكالمة فيديو</string>
-   <string name="call_audio_call">مكالمة صوتية</string>
-   <string name="call_user_exceed_limit">يدعم حاليًا إجراء مكالمات مع ما يصل إلى 9 أشخاص. لإجراء مكالمات مؤتمرات أكبر، حاول استخدام TUIRoomKit</string>
-   <string name="call_wait_response">منتظر</string>
-   <string name="call_permission_camera">آلة تصوير</string>
-   <string name="call_permission_microphone">ميكروفون</string>
-   <string name="call_permission_bluetooth">بلوتوث</string>
-   <string name="call_permission_separator">،</string>
-   <string name="call_permission_tips">الرجاء تشغيل إذن %1$s أولاً</string>
-   <string name="call_permission_title">ينطبق %1$s على الوصول إلى %2$s</string>
-   <string name="call_permission_audio_tips">يجب عليك الوصول إلى الميكروفون الخاص بك ويمكن استخدامه لميزات مكالمات الصوت والفيديو.</string>
-   <string name="call_permission_video_tips">يجب عليك الوصول إلى الميكروفون والكاميرا الخاصة بك ويمكن استخدامهما لمكالمات الفيديو.</string>
-   <string name="call_permission_bluetooth_reason">لضمان إمكانية الاتصال بميزة البلوتوث للتواصل أثناء مكالمة صوتية/فيديو، نحتاج إلى طلب الإذن من الأجهزة القريبة.</string>
-   <string name="call_permission_camera_tips">يجب عليك الوصول إلى الكاميرا الخاصة بك ويمكن استخدامها لإجراء مكالمات الفيديو.</string>
-   <string name="call_have_a_new_call">لديك مكالمة جديدة</string>
-   <string name="call_have_a_new_audio_call_cost">دعوة محادثة صوتية، بتكلفة %1$s عملة/دقيقة بعد الاتصال</string>
-   <string name="call_have_a_new_audio_call_earn">دعوة محادثة صوتية، احصل على %1$s ماسة/15 ثانية بعد الاتصال</string>
-   <string name="call_have_a_new_video_call_cost">دعوة محادثة فيديو، بتكلفة %1$s عملة/دقيقة بعد الاتصال</string>
-   <string name="call_have_a_new_video_call_earn">دعوة محادثة فيديو، احصل على %1$s ماسة/15 ثانية بعد الاتصال</string>
-   <string name="call_error_in_peer_blacklist">المُعرّف مُدرج في القائمة السوداء. تعذّر إرسال هذه الرسالة!</string>
-   <string name="call_error_invalid_login">تسجيل الدخول غير صالح، يرجى تسجيل الدخول مرة أخرى</string>
-   <string name="call_error_parameter_invalid">خطأ في المعلمة</string>
-   <string name="call_error_request_refused">الحالة الحالية لا يمكن استخدام هذه الوظيفة</string>
-   <string name="call_error_request_repeated">الحالة الحالية هي انتظار/قبول، يرجى عدم الاتصال بها بشكل متكرر</string>
-   <string name="call_error_scene_not_support">المشهد الحالي لا يدعم هذه الوظيفة</string>
-   <string name="call_chat_caller_waiting_tips">نداء</string>
-   <string name="call_chat_called_waiting_tips">لقد تمت دعوتك إلى مكالمة صوتية</string>
-   <string name="call_chat_caller_cost">دعوة محادثة صوتية، التكلفة %1$s[icon]/دقيقة بعد الاتصال</string>
-   <string name="call_chat_called_earn">دعوة محادثة صوتية، احصل على %1$s[icon]/15s بعد الاتصال</string>
-   <string name="call_video_caller_cost">دعوة محادثة فيديو، التكلفة %1$s[icon]/دقيقة بعد الاتصال</string>
-   <string name="call_video_called_earn">دعوة محادثة فيديو، احصل على %1$s[icon]/15s بعد الاتصال</string>
-   <string name="call_video_invitation">دعوة فيديو</string>
-   <string name="call_video_invitation_tips">احصل على %1$s من الماس [icon]/15 ثانية</string>
-   <string name="call_video_calling">مكالمات الفيديو</string>
-   <string name="call_video_calling_tips">التكلفة %1$s عملة [icon]/دقيقة</string>
-   <string name="call_chat_invitation">دعوة للدردشة</string>
-   <string name="call_chat_invitation_tips">احصل على %1$s من الماس [icon]/15 ثانية</string>
-   <string name="call_chat_calling">مكالمات الدردشة</string>
-   <string name="call_chat_calling_tips">التكلفة %1$s عملة [icon]/دقيقة</string>
-   <string name="call_media_conflict_title">أدخل مكالمة مباشرة؟</string>
-   <string name="call_media_conflict_tips">أنت موجود في %1$s الآن، وسوف يتم تسجيل خروجك تلقائيًا عند بدء مكالمة مباشرة</string>
-   <string name="call_media_conflict_remind">لا تذكرني مرة أخرى</string>
-   <string name="call_video_camera_off">الكاميرا مغلقة</string>
-   <string name="call_fail_for_calling_now">حاليا في مكالمة</string>
-   <string name="call_permissoin_float_window">تتطلب هذه الميزة تمكين النافذة العائمة وأذونات بدء التشغيل في الخلفية</string>
-   <string name="call_permission_grant_fail">فشل منح الإذن</string>
-   <string name="call_kit_error">خطأ الخادم(%1$s)</string>
-   <string name="call_kit_error_already_in_call">لا يمكن بدء المكالمة، وهي قيد المكالمة بالفعل</string>
-   <string name="call_caller_wait">نداء…</string>
-   <string name="call_called_wait">في انتظار الجواب…</string>
-   <string name="call_invite_merchant">بدء مكالمة صوتية</string>
-   <string name="call_merchant_cost">التكلفة 0 عملة</string>
-   <string name="call_merchant_float_cost">مكالمة صوتية، مكالمة مجانية</string>
-   <string name="call_free">حر</string>
-   <string name="call_chat_free_time">%1$d ثانية مجانية</string>
-   <string name="call_merchant_free_time">يمكن الاتصال اليوم: %1$d من الوقت، %2$d من الدقائق</string>
-   <string name="call_remind_free_time">باقي اليوم: %1$d دقيقة</string>
-   <string name="call_chat_fail_tip">لا يدعم وظيفة الاتصال</string>
-   <string name="common_rtc_channel_name">اختبار قناة RTC</string>
-   <string name="common_rtc_channel_description">وصف اختبار قناة RTC</string>
-</resources>
+    <string name="call_1v1">يتصل</string>
+    <string name="call_chat">مكالمات الدردشة</string>
+    <string name="call_video">مكالمات الفيديو</string>
+    <string name="call_target_uid_is_invalid">معرف المستخدم المتصل غير صالح</string>
+    <string name="call_target_uid_is_self">لا يمكنك الاتصال بنفسك.</string>
+    <string name="call_media_type_unknown">نوع المكالمة غير معروف</string>
+    <string name="call_answer_using_voice">الرد الصوتي</string>
+    <string name="call_request_permisson_fail">فشل طلب الإذن</string>
+    <string name="call_target_uid_is_empty">معرف المكالمة فارغ</string>
+    <string name="call_fail_for_exceeding_max_user">فشلت المكالمات، وتجاوز الحد الأقصى لعدد المستخدمين</string>
+    <string name="call_start_call_fail_room_id_invalid">فشل بدء المكالمة (معرف الغرفة غير صالح)</string>
+    <string name="call_callee_hangup">أغلق الطرف الآخر الهاتف، وانتهت المكالمة</string>
+    <string name="call_callee_reject">تم رفض المكالمة من قبل الطرف الآخر</string>
+    <string name="call_callee_no_response">ولم يرد الطرف الآخر</string>
+    <string name="call_line_busy">الخط مشغول</string>
+    <string name="call_video_call">مكالمة فيديو</string>
+    <string name="call_audio_call">مكالمة صوتية</string>
+    <string name="call_user_exceed_limit">يدعم حاليًا إجراء مكالمات مع ما يصل إلى 9 أشخاص. لإجراء مكالمات مؤتمرات أكبر، حاول استخدام TUIRoomKit</string>
+    <string name="call_wait_response">منتظر</string>
+    <string name="call_permission_camera">آلة تصوير</string>
+    <string name="call_permission_microphone">ميكروفون</string>
+    <string name="call_permission_bluetooth">بلوتوث</string>
+    <string name="call_permission_separator">،</string>
+    <string name="call_permission_tips">الرجاء تشغيل إذن %1$s أولاً</string>
+    <string name="call_permission_title">ينطبق %1$s على الوصول إلى %2$s</string>
+    <string name="call_permission_audio_tips">يجب عليك الوصول إلى الميكروفون الخاص بك ويمكن استخدامه لميزات مكالمات الصوت والفيديو.</string>
+    <string name="call_permission_video_tips">يجب عليك الوصول إلى الميكروفون والكاميرا الخاصة بك ويمكن استخدامهما لمكالمات الفيديو.</string>
+    <string name="call_permission_bluetooth_reason">لضمان إمكانية الاتصال بميزة البلوتوث للتواصل أثناء مكالمة صوتية/فيديو، نحتاج إلى طلب الإذن من الأجهزة القريبة.</string>
+    <string name="call_permission_camera_tips">يجب عليك الوصول إلى الكاميرا الخاصة بك ويمكن استخدامها لإجراء مكالمات الفيديو.</string>
+    <string name="call_have_a_new_call">لديك مكالمة جديدة</string>
+    <string name="call_have_a_new_audio_call_cost">دعوة محادثة صوتية، بتكلفة %1$s عملة/دقيقة بعد الاتصال</string>
+    <string name="call_have_a_new_audio_call_earn">دعوة محادثة صوتية، احصل على %1$s ماسة/15 ثانية بعد الاتصال</string>
+    <string name="call_have_a_new_video_call_cost">دعوة محادثة فيديو، بتكلفة %1$s عملة/دقيقة بعد الاتصال</string>
+    <string name="call_have_a_new_video_call_earn">دعوة محادثة فيديو، احصل على %1$s ماسة/15 ثانية بعد الاتصال</string>
+    <string name="call_error_in_peer_blacklist">المُعرّف مُدرج في القائمة السوداء. تعذّر إرسال هذه الرسالة!</string>
+    <string name="call_error_invalid_login">تسجيل الدخول غير صالح، يرجى تسجيل الدخول مرة أخرى</string>
+    <string name="call_error_parameter_invalid">خطأ في المعلمة</string>
+    <string name="call_error_request_refused">الحالة الحالية لا يمكن استخدام هذه الوظيفة</string>
+    <string name="call_error_request_repeated">الحالة الحالية هي انتظار/قبول، يرجى عدم الاتصال بها بشكل متكرر</string>
+    <string name="call_error_scene_not_support">المشهد الحالي لا يدعم هذه الوظيفة</string>
+    <string name="call_chat_caller_waiting_tips">نداء</string>
+    <string name="call_chat_called_waiting_tips">لقد تمت دعوتك إلى مكالمة صوتية</string>
+    <string name="call_chat_caller_cost">دعوة محادثة صوتية، التكلفة %1$s[icon]/دقيقة بعد الاتصال</string>
+    <string name="call_chat_called_earn">دعوة محادثة صوتية، احصل على %1$s[icon]/15s بعد الاتصال</string>
+    <string name="call_video_caller_cost">دعوة محادثة فيديو، التكلفة %1$s[icon]/دقيقة بعد الاتصال</string>
+    <string name="call_video_called_earn">دعوة محادثة فيديو، احصل على %1$s[icon]/15s بعد الاتصال</string>
+    <string name="call_video_invitation">دعوة فيديو</string>
+    <string name="call_video_invitation_tips">احصل على %1$s من الماس [icon]/15 ثانية</string>
+    <string name="call_video_calling">مكالمات الفيديو</string>
+    <string name="call_video_calling_tips">التكلفة %1$s عملة [icon]/دقيقة</string>
+    <string name="call_chat_invitation">دعوة للدردشة</string>
+    <string name="call_chat_invitation_tips">احصل على %1$s من الماس [icon]/15 ثانية</string>
+    <string name="call_chat_calling">مكالمات الدردشة</string>
+    <string name="call_chat_calling_tips">التكلفة %1$s عملة [icon]/دقيقة</string>
+    <string name="call_media_conflict_title">أدخل مكالمة مباشرة؟</string>
+    <string name="call_media_conflict_tips">أنت موجود في %1$s الآن، وسوف يتم تسجيل خروجك تلقائيًا عند بدء مكالمة مباشرة</string>
+    <string name="call_media_conflict_remind">لا تذكرني مرة أخرى</string>
+    <string name="call_video_camera_off">الكاميرا مغلقة</string>
+    <string name="call_fail_for_calling_now">حاليا في مكالمة</string>
+    <string name="call_permissoin_float_window">تتطلب هذه الميزة تمكين النافذة العائمة وأذونات بدء التشغيل في الخلفية</string>
+    <string name="call_permission_grant_fail">فشل منح الإذن</string>
+    <string name="call_kit_error">خطأ الخادم(%1$s)</string>
+    <string name="call_kit_error_already_in_call">لا يمكن بدء المكالمة، وهي قيد المكالمة بالفعل</string>
+    <string name="call_caller_wait">نداء…</string>
+    <string name="call_called_wait">في انتظار الجواب…</string>
+    <string name="call_invite_merchant">بدء مكالمة صوتية</string>
+    <string name="call_merchant_cost">التكلفة 0 عملة</string>
+    <string name="call_merchant_float_cost">مكالمة صوتية، مكالمة مجانية</string>
+    <string name="call_free">حر</string>
+    <string name="call_chat_free_time">%1$d ثانية مجانية</string>
+    <string name="call_merchant_free_time">يمكن الاتصال اليوم: %1$d من الوقت، %2$d من الدقائق</string>
+    <string name="call_remind_free_time">باقي اليوم: %1$d دقيقة</string>
+    <string name="call_chat_fail_tip">لا يدعم وظيفة الاتصال</string>
+    <string name="common_rtc_channel_name">اختبار قناة RTC</string>
+    <string name="common_rtc_channel_description">وصف اختبار قناة RTC</string>
+    <string name="call_match_title">دردشة عشوائية</string>
+    <string name="call_match_start">ابدأ المطابقة</string>
+    <string name="call_matching">جاري المطابقة...</string>
+    <string name="call_match_next">المطابقة التالية</string>
+    <string name="call_match_timeout_tips">لم يتم العثور على مطابقة بعد. يمكنك الدردشة مع الفتيات أولاً~</string>
+    <string name="call_continue_match">متابعة المطابقة</string>
+    <string name="call_go_chat">اذهب للدردشة</string>
+    <string name="call_duration">مدة المكالمة:</string>
+    <string name="call_earn_diamonds">حصول على ألماس:</string>
+    <string name="call_fail_for_match_mode">لا يمكن بدء المكالمة عند المطابقة</string>
+    <string name="call_change_video_cost">بمجرد موافقة الطرف الآخر، يمكنك بدء مكالمة فيديو. يمكنك التبديل بعد استهلاك %1$s[icon] عملة خلال الدقيقة الأولى.</string>
+    <string name="call_change_video_earn">بمجرد موافقة الطرف الآخر، يمكنك بدء مكالمة فيديو وكسب 150 ماسة إضافية كل 15 ثانية.</string>
+    <string name="call_change_video_earn_invitation">%1$s أرسل لك دعوة مكالمة فيديو، تربح %2$s[icon] ماسة إضافية كل 15 ثانية.</string>
+    <string name="call_change_video_cost_invitation">أرسل لك %1$s دعوة فيديو. تستهلك الدقيقة الأولى %2$s[icon] عملة للإجابة.</string>
+    <string name="call_change_video_timeout">الطرف الآخر لم يرد لفترة طويلة. يرجى المحاولة مرة أخرى.</string>
+    <string name="call_change_video_decline">الطرف الآخر لم يقبل طلب مكالمتك الفيديو.</string>
+    <string name="call_send_video_invitation">أرسل دعوة مكالمة فيديو</string>
+    <string name="call_receive_video_invitation">تلقيت دعوة فيديو</string>
+    <string name="call_chat_connect_tips">جاري الاتصال...</string>
+    <string name="call_match_failed">فشلت المباراة</string>
+</resources>

+ 96 - 76
module/call/src/main/res/values-zh/strings.xml

@@ -1,78 +1,98 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
-   <string name="call_1v1">称呼</string>
-   <string name="call_chat">聊天通话</string>
-   <string name="call_video">视频通话</string>
-   <string name="call_target_uid_is_invalid">呼叫用户ID无效</string>
-   <string name="call_target_uid_is_self">不能呼叫自己</string>
-   <string name="call_media_type_unknown">未知呼叫类型</string>
-   <string name="call_answer_using_voice">语音应答</string>
-   <string name="call_request_permisson_fail">请求权限失败</string>
-   <string name="call_target_uid_is_empty">呼叫uid为空</string>
-   <string name="call_fail_for_exceeding_max_user">呼叫失败,超出最大用户数</string>
-   <string name="call_start_call_fail_room_id_invalid">开始呼叫失败(roomId 无效)</string>
-   <string name="call_callee_hangup">对方挂断电话,通话结束</string>
-   <string name="call_callee_reject">对方拒绝接听电话</string>
-   <string name="call_callee_no_response">对方没有回应</string>
-   <string name="call_line_busy">线路忙</string>
-   <string name="call_video_call">视频电话</string>
-   <string name="call_audio_call">语音通话</string>
-   <string name="call_user_exceed_limit">目前支持最多 9 人通话。对于更大规模的电话会议</string>
-   <string name="call_wait_response">等待</string>
-   <string name="call_permission_camera">相机</string>
-   <string name="call_permission_microphone">麦克风</string>
-   <string name="call_permission_bluetooth">蓝牙</string>
-   <string name="call_permission_separator">,</string>
-   <string name="call_permission_tips">请先开启%1$s权限</string>
-   <string name="call_permission_title">%1$s 申请访问 %2$s</string>
-   <string name="call_permission_audio_tips">需要访问您的麦克风,并且可以用于音频/视频通话功能。</string>
-   <string name="call_permission_video_tips">需要访问您的麦克风和相机,并且可以用于视频通话。</string>
-   <string name="call_permission_bluetooth_reason">为了确保您在音频/视频通话期间可以连接到蓝牙功能进行通信,我们需要请求附近设备的许可。</string>
-   <string name="call_permission_camera_tips">需要访问您的相机,并且可以用于视频通话。</string>
-   <string name="call_have_a_new_call">您有新来电</string>
-   <string name="call_have_a_new_audio_call_cost">语音聊天邀请,连接后每分钟花费 %1$s 币</string>
-   <string name="call_have_a_new_audio_call_earn">语音聊天邀请,连接后15秒获得%1$s钻石</string>
-   <string name="call_have_a_new_video_call_cost">视频聊天邀请,连接后每分钟花费 %1$s 币</string>
-   <string name="call_have_a_new_video_call_earn">视频聊天邀请,连接后15秒获得%1$s钻石</string>
-   <string name="call_error_in_peer_blacklist">该标识符已列入黑名单。无法发送该消息!</string>
-   <string name="call_error_invalid_login">登录无效,请重新登录</string>
-   <string name="call_error_parameter_invalid">参数错误</string>
-   <string name="call_error_request_refused">目前状态无法使用此功能</string>
-   <string name="call_error_request_repeated">当前状态为等待/接受,请不要重复调用</string>
-   <string name="call_error_scene_not_support">当前场景不支持该功能</string>
-   <string name="call_chat_caller_waiting_tips">呼唤</string>
-   <string name="call_chat_called_waiting_tips">您受邀参加语音通话</string>
-   <string name="call_chat_caller_cost">语音聊天邀请,连接后花费 %1$s[icon]/分钟</string>
-   <string name="call_chat_called_earn">语音聊天邀请,连接后 15 秒获取 %1$s[icon]</string>
-   <string name="call_video_caller_cost">视频聊天邀请,连接后花费 %1$s[icon]/分钟</string>
-   <string name="call_video_called_earn">视频聊天邀请,连接后15秒获取%1$s[icon]</string>
-   <string name="call_video_invitation">视频邀请</string>
-   <string name="call_video_invitation_tips">每 15 秒获得 %1$s 颗钻石 [icon]</string>
-   <string name="call_video_calling">视频通话</string>
-   <string name="call_video_calling_tips">花费%1$s金币[icon]/分钟</string>
-   <string name="call_chat_invitation">聊天邀请</string>
-   <string name="call_chat_invitation_tips">每 15 秒获得 %1$s 颗钻石 [icon]</string>
-   <string name="call_chat_calling">聊天通话</string>
-   <string name="call_chat_calling_tips">花费%1$s金币[icon]/分钟</string>
-   <string name="call_media_conflict_title">进入实时通话?</string>
-   <string name="call_media_conflict_tips">您现在处于 %1$s,当您发起实时通话时将自动结账</string>
-   <string name="call_media_conflict_remind">不要再提醒我</string>
-   <string name="call_video_camera_off">相机已关闭</string>
-   <string name="call_fail_for_calling_now">正在通话中</string>
-   <string name="call_permissoin_float_window">该功能需要开启悬浮窗和后台启动权限</string>
-   <string name="call_permission_grant_fail">权限授予失败</string>
-   <string name="call_kit_error">服务错误(%1$s)</string>
-   <string name="call_kit_error_already_in_call">呼叫失败,已经在呼叫中</string>
-   <string name="call_caller_wait">呼叫中…</string>
-   <string name="call_called_wait">等待接听…</string>
-   <string name="call_invite_merchant">发起语音通话</string>
-   <string name="call_merchant_cost">花费 0 枚金币</string>
-   <string name="call_merchant_float_cost">语音通话,免费通话</string>
-   <string name="call_free">免费的</string>
-   <string name="call_chat_free_time">免费 %1$d 秒</string>
-   <string name="call_merchant_free_time">今天可拨打:%1$d 次,%2$d 分钟</string>
-   <string name="call_remind_free_time">今天剩余时间:%1$d 分钟</string>
-   <string name="call_chat_fail_tip">对方不支持实时通话功能</string>
-   <string name="common_rtc_channel_name">RTC Channel Test</string>
-   <string name="common_rtc_channel_description">RTC Channel Test Desc</string>
-</resources>
+    <string name="call_1v1">称呼</string>
+    <string name="call_chat">聊天通话</string>
+    <string name="call_video">视频通话</string>
+    <string name="call_target_uid_is_invalid">呼叫用户ID无效</string>
+    <string name="call_target_uid_is_self">不能呼叫自己</string>
+    <string name="call_media_type_unknown">未知呼叫类型</string>
+    <string name="call_answer_using_voice">语音应答</string>
+    <string name="call_request_permisson_fail">请求权限失败</string>
+    <string name="call_target_uid_is_empty">呼叫uid为空</string>
+    <string name="call_fail_for_exceeding_max_user">呼叫失败,超出最大用户数</string>
+    <string name="call_start_call_fail_room_id_invalid">开始呼叫失败(roomId 无效)</string>
+    <string name="call_callee_hangup">对方挂断电话,通话结束</string>
+    <string name="call_callee_reject">对方拒绝接听电话</string>
+    <string name="call_callee_no_response">对方没有回应</string>
+    <string name="call_line_busy">线路忙</string>
+    <string name="call_video_call">视频电话</string>
+    <string name="call_audio_call">语音通话</string>
+    <string name="call_user_exceed_limit">目前支持最多 9 人通话。对于更大规模的电话会议</string>
+    <string name="call_wait_response">等待</string>
+    <string name="call_permission_camera">相机</string>
+    <string name="call_permission_microphone">麦克风</string>
+    <string name="call_permission_bluetooth">蓝牙</string>
+    <string name="call_permission_separator">,</string>
+    <string name="call_permission_tips">请先开启%1$s权限</string>
+    <string name="call_permission_title">%1$s 申请访问 %2$s</string>
+    <string name="call_permission_audio_tips">需要访问您的麦克风,并且可以用于音频/视频通话功能。</string>
+    <string name="call_permission_video_tips">需要访问您的麦克风和相机,并且可以用于视频通话。</string>
+    <string name="call_permission_bluetooth_reason">为了确保您在音频/视频通话期间可以连接到蓝牙功能进行通信,我们需要请求附近设备的许可。</string>
+    <string name="call_permission_camera_tips">需要访问您的相机,并且可以用于视频通话。</string>
+    <string name="call_have_a_new_call">您有新来电</string>
+    <string name="call_have_a_new_audio_call_cost">语音聊天邀请,连接后每分钟花费 %1$s 币</string>
+    <string name="call_have_a_new_audio_call_earn">语音聊天邀请,连接后15秒获得%1$s钻石</string>
+    <string name="call_have_a_new_video_call_cost">视频聊天邀请,连接后每分钟花费 %1$s 币</string>
+    <string name="call_have_a_new_video_call_earn">视频聊天邀请,连接后15秒获得%1$s钻石</string>
+    <string name="call_error_in_peer_blacklist">该标识符已列入黑名单。无法发送该消息!</string>
+    <string name="call_error_invalid_login">登录无效,请重新登录</string>
+    <string name="call_error_parameter_invalid">参数错误</string>
+    <string name="call_error_request_refused">目前状态无法使用此功能</string>
+    <string name="call_error_request_repeated">当前状态为等待/接受,请不要重复调用</string>
+    <string name="call_error_scene_not_support">当前场景不支持该功能</string>
+    <string name="call_chat_caller_waiting_tips">呼唤</string>
+    <string name="call_chat_called_waiting_tips">您受邀参加语音通话</string>
+    <string name="call_chat_caller_cost">语音聊天邀请,连接后花费 %1$s[icon]/分钟</string>
+    <string name="call_chat_called_earn">语音聊天邀请,连接后 15 秒获取 %1$s[icon]</string>
+    <string name="call_video_caller_cost">视频聊天邀请,连接后花费 %1$s[icon]/分钟</string>
+    <string name="call_video_called_earn">视频聊天邀请,连接后15秒获取%1$s[icon]</string>
+    <string name="call_video_invitation">视频邀请</string>
+    <string name="call_video_invitation_tips">每 15 秒获得 %1$s 颗钻石 [icon]</string>
+    <string name="call_video_calling">视频通话</string>
+    <string name="call_video_calling_tips">花费%1$s金币[icon]/分钟</string>
+    <string name="call_chat_invitation">聊天邀请</string>
+    <string name="call_chat_invitation_tips">每 15 秒获得 %1$s 颗钻石 [icon]</string>
+    <string name="call_chat_calling">聊天通话</string>
+    <string name="call_chat_calling_tips">花费%1$s金币[icon]/分钟</string>
+    <string name="call_media_conflict_title">进入实时通话?</string>
+    <string name="call_media_conflict_tips">您现在处于 %1$s,当您发起实时通话时将自动结账</string>
+    <string name="call_media_conflict_remind">不要再提醒我</string>
+    <string name="call_video_camera_off">相机已关闭</string>
+    <string name="call_fail_for_calling_now">正在通话中</string>
+    <string name="call_permissoin_float_window">该功能需要开启悬浮窗和后台启动权限</string>
+    <string name="call_permission_grant_fail">权限授予失败</string>
+    <string name="call_kit_error">服务错误(%1$s)</string>
+    <string name="call_kit_error_already_in_call">呼叫失败,已经在呼叫中</string>
+    <string name="call_caller_wait">呼叫中…</string>
+    <string name="call_called_wait">等待接听…</string>
+    <string name="call_invite_merchant">发起语音通话</string>
+    <string name="call_merchant_cost">花费 0 枚金币</string>
+    <string name="call_merchant_float_cost">语音通话,免费通话</string>
+    <string name="call_free">免费的</string>
+    <string name="call_chat_free_time">免费 %1$d 秒</string>
+    <string name="call_merchant_free_time">今天可拨打:%1$d 次,%2$d 分钟</string>
+    <string name="call_remind_free_time">今天剩余时间:%1$d 分钟</string>
+    <string name="call_chat_fail_tip">对方不支持实时通话功能</string>
+    <string name="common_rtc_channel_name">RTC Channel Test</string>
+    <string name="common_rtc_channel_description">RTC Channel Test Desc</string>
+    <string name="call_duration">通话时长:</string>
+    <string name="call_earn_diamonds">赚取钻石:</string>
+    <string name="call_match_title">随机聊天</string>
+    <string name="call_match_start">开始匹配</string>
+    <string name="call_continue_match">继续匹配</string>
+    <string name="call_match_next">匹配下一个</string>
+    <string name="call_matching">匹配中...</string>
+    <string name="call_match_timeout_tips">暂未匹配成功,可以先去找女生聊天哦~</string>
+    <string name="call_go_chat">去聊天</string>
+    <string name="call_fail_for_match_mode">匹配时无法开始通话</string>
+    <string name="call_change_video_cost">对方同意后即可开始视频聊天,首分钟消耗%1$s[icon]金币即可切换</string>
+    <string name="call_change_video_earn">对方同意后即可开始视频聊天,每15s额外赚取%1$s[icon]钻石</string>
+    <string name="call_change_video_earn_invitation">%1$s向你发来了视频邀请,每15s额外赚取%2$s[icon]钻石</string>
+    <string name="call_change_video_cost_invitation">%1$s向你发来了视频邀请,首分钟消耗%2$s[icon]金币即可接听</string>
+    <string name="call_change_video_timeout">对方长时间未同意,请重新发起</string>
+    <string name="call_change_video_decline">对方未同意你的视频通话请求</string>
+    <string name="call_send_video_invitation">发出视频邀请</string>
+    <string name="call_receive_video_invitation">收到视频邀请</string>
+    <string name="call_chat_connect_tips">正在连接...</string>
+    <string name="call_match_failed">匹配失败</string>
+</resources>

+ 96 - 76
module/call/src/main/res/values/strings.xml

@@ -1,78 +1,98 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
-   <string name="call_1v1">Call</string>
-   <string name="call_chat">Chat Calling</string>
-   <string name="call_video">Video Calling</string>
-   <string name="call_target_uid_is_invalid">Invalid calling user ID</string>
-   <string name="call_target_uid_is_self">Can\'t call yourself</string>
-   <string name="call_media_type_unknown">Unknown call type</string>
-   <string name="call_answer_using_voice">Voice Response</string>
-   <string name="call_request_permisson_fail">Request permission fail</string>
-   <string name="call_target_uid_is_empty">Call uid is empty</string>
-   <string name="call_fail_for_exceeding_max_user">calls failed, exceeding max user number</string>
-   <string name="call_start_call_fail_room_id_invalid">Start call fail(roomId is invalid)</string>
-   <string name="call_callee_hangup">Other party hung up, call ended</string>
-   <string name="call_callee_reject">Call rejected by other party</string>
-   <string name="call_callee_no_response">The other party did not respond</string>
-   <string name="call_line_busy">Line Busy</string>
-   <string name="call_video_call">Video Call</string>
-   <string name="call_audio_call">Voice Call</string>
-   <string name="call_user_exceed_limit">Currently supports call with up to 9 people.For larger conference calls,try using TUIRoomKit</string>
-   <string name="call_wait_response">Waiting</string>
-   <string name="call_permission_camera">Camera</string>
-   <string name="call_permission_microphone">Microphone</string>
-   <string name="call_permission_bluetooth">Bluetooth</string>
-   <string name="call_permission_separator">,</string>
-   <string name="call_permission_tips">Please turn on the %1$s permission first</string>
-   <string name="call_permission_title">%1$s applies for access to the %2$s</string>
-   <string name="call_permission_audio_tips">Needs access to your microphone, and it can be used for functions such as Audio/Video Call.</string>
-   <string name="call_permission_video_tips">Needs access to your microphone and camera, and it can be used for functions such as Video Call.</string>
-   <string name="call_permission_camera_tips">Needs access to your camera, and it can be used for functions such as Video Call.</string>
-   <string name="call_permission_bluetooth_reason">To ensure that you can connect to Bluetooth function for communication during Audio/Video Call, we need to request permission of nearby devices.</string>
-   <string name="call_have_a_new_call">You have a new call</string>
-   <string name="call_have_a_new_audio_call_cost">Audio chat invitation, cost %1$s coin/min after connection</string>
-   <string name="call_have_a_new_audio_call_earn">Audio chat invitation, get %1$s diamond/15s after connection</string>
-   <string name="call_have_a_new_video_call_cost">Video chat invitation, cost %1$s coin/min after connection</string>
-   <string name="call_have_a_new_video_call_earn">Video chat invitation, get %1$s diamond/15s after connection</string>
-   <string name="call_error_in_peer_blacklist">The identifier is in blacklist. Failed to send this message!</string>
-   <string name="call_error_invalid_login">Invalid login, please login again</string>
-   <string name="call_error_parameter_invalid">Parameter error</string>
-   <string name="call_error_request_refused">The current status can not use this function</string>
-   <string name="call_error_request_repeated">The current status is waiting/accept, please do not call it repeatedly</string>
-   <string name="call_error_scene_not_support">The current scene does not support this function</string>
-   <string name="call_chat_caller_waiting_tips">Calling</string>
-   <string name="call_chat_called_waiting_tips">You are invited to a voice call</string>
-   <string name="call_chat_caller_cost">Audio chat invitation, cost %1$s[icon]/min after connection</string>
-   <string name="call_chat_called_earn">Audio chat invitation, get %1$s[icon]/15s after connection</string>
-   <string name="call_video_caller_cost">Video chat invitation, cost %1$s[icon]/min after connection</string>
-   <string name="call_video_called_earn">Video chat invitation, get %1$s[icon]/15s after connection</string>
-   <string name="call_video_invitation">Video Invitation</string>
-   <string name="call_video_invitation_tips">Get %1$s diamonds [icon]/15s</string>
-   <string name="call_video_calling">Video Calling</string>
-   <string name="call_video_calling_tips">Cost %1$s coin [icon]/min</string>
-   <string name="call_chat_invitation">Chat Invitation</string>
-   <string name="call_chat_invitation_tips">Get %1$s diamonds [icon]/15s</string>
-   <string name="call_chat_calling">Chat Calling</string>
-   <string name="call_chat_calling_tips">Cost %1$s coin [icon]/min</string>
-   <string name="call_chat_free_time">Free %1$d seconds</string>
-   <string name="call_media_conflict_title">Enter live call?</string>
-   <string name="call_media_conflict_tips">You are in %1$s now, You will automatically check out when you initiate a live call</string>
-   <string name="call_media_conflict_remind">Don\'t remind me again</string>
-   <string name="call_video_camera_off">The Camera is off</string>
-   <string name="call_fail_for_calling_now">Currently on a call</string>
-   <string name="call_permissoin_float_window">This feature requires enabling floating window and background startup permissions</string>
-   <string name="call_permission_grant_fail">Permission grant failed</string>
-   <string name="call_kit_error">Server Error(%1$s)</string>
-   <string name="call_kit_error_already_in_call">Can not start call, already in call</string>
-   <string name="call_caller_wait">Calling…</string>
-   <string name="call_called_wait">Waiting for answer…</string>
-   <string name="common_rtc_channel_name">RTC Channel Test</string>
-   <string name="common_rtc_channel_description">RTC Channel Test Desc</string>
-   <string name="call_invite_merchant">Initiate a voice call</string>
-   <string name="call_free">Free</string>
-   <string name="call_merchant_free_time">Callable today: %1$d time, %2$d minutes</string>
-   <string name="call_merchant_cost">Cost 0 Coin</string>
-   <string name="call_merchant_float_cost">Voice call, Call free</string>
-   <string name="call_remind_free_time">Remainder of today: %1$d minutes</string>
-   <string name="call_chat_fail_tip">He/she does not support the call function</string>
-</resources>
+    <string name="call_1v1">Call</string>
+    <string name="call_chat">Chat Calling</string>
+    <string name="call_video">Video Calling</string>
+    <string name="call_target_uid_is_invalid">Invalid calling user ID</string>
+    <string name="call_target_uid_is_self">Can\'t call yourself</string>
+    <string name="call_media_type_unknown">Unknown call type</string>
+    <string name="call_answer_using_voice">Voice Response</string>
+    <string name="call_request_permisson_fail">Request permission fail</string>
+    <string name="call_target_uid_is_empty">Call uid is empty</string>
+    <string name="call_fail_for_exceeding_max_user">calls failed, exceeding max user number</string>
+    <string name="call_start_call_fail_room_id_invalid">Start call fail(roomId is invalid)</string>
+    <string name="call_callee_hangup">Other party hung up, call ended</string>
+    <string name="call_callee_reject">Call rejected by other party</string>
+    <string name="call_callee_no_response">The other party did not respond</string>
+    <string name="call_line_busy">Line Busy</string>
+    <string name="call_video_call">Video Call</string>
+    <string name="call_audio_call">Voice Call</string>
+    <string name="call_user_exceed_limit">Currently supports call with up to 9 people.For larger conference calls,try using TUIRoomKit</string>
+    <string name="call_wait_response">Waiting</string>
+    <string name="call_permission_camera">Camera</string>
+    <string name="call_permission_microphone">Microphone</string>
+    <string name="call_permission_bluetooth">Bluetooth</string>
+    <string name="call_permission_separator">,</string>
+    <string name="call_permission_tips">Please turn on the %1$s permission first</string>
+    <string name="call_permission_title">%1$s applies for access to the %2$s</string>
+    <string name="call_permission_audio_tips">Needs access to your microphone, and it can be used for functions such as Audio/Video Call.</string>
+    <string name="call_permission_video_tips">Needs access to your microphone and camera, and it can be used for functions such as Video Call.</string>
+    <string name="call_permission_camera_tips">Needs access to your camera, and it can be used for functions such as Video Call.</string>
+    <string name="call_permission_bluetooth_reason">To ensure that you can connect to Bluetooth function for communication during Audio/Video Call, we need to request permission of nearby devices.</string>
+    <string name="call_have_a_new_call">You have a new call</string>
+    <string name="call_have_a_new_audio_call_cost">Audio chat invitation, cost %1$s coin/min after connection</string>
+    <string name="call_have_a_new_audio_call_earn">Audio chat invitation, get %1$s diamond/15s after connection</string>
+    <string name="call_have_a_new_video_call_cost">Video chat invitation, cost %1$s coin/min after connection</string>
+    <string name="call_have_a_new_video_call_earn">Video chat invitation, get %1$s diamond/15s after connection</string>
+    <string name="call_error_in_peer_blacklist">The identifier is in blacklist. Failed to send this message!</string>
+    <string name="call_error_invalid_login">Invalid login, please login again</string>
+    <string name="call_error_parameter_invalid">Parameter error</string>
+    <string name="call_error_request_refused">The current status can not use this function</string>
+    <string name="call_error_request_repeated">The current status is waiting/accept, please do not call it repeatedly</string>
+    <string name="call_error_scene_not_support">The current scene does not support this function</string>
+    <string name="call_chat_caller_waiting_tips">Calling</string>
+    <string name="call_chat_connect_tips">Connecting...</string>
+    <string name="call_chat_called_waiting_tips">You are invited to a voice call</string>
+    <string name="call_chat_caller_cost">Audio chat invitation, cost %1$s[icon]/min after connection</string>
+    <string name="call_chat_called_earn">Audio chat invitation, get %1$s[icon]/15s after connection</string>
+    <string name="call_video_caller_cost">Video chat invitation, cost %1$s[icon]/min after connection</string>
+    <string name="call_video_called_earn">Video chat invitation, get %1$s[icon]/15s after connection</string>
+    <string name="call_video_invitation">Video Invitation</string>
+    <string name="call_send_video_invitation">Send Video Invitation</string>
+    <string name="call_receive_video_invitation">Received a video invitation</string>
+    <string name="call_video_invitation_tips">Get %1$s diamonds [icon]/15s</string>
+    <string name="call_video_calling">Video Calling</string>
+    <string name="call_video_calling_tips">Cost %1$s coin [icon]/min</string>
+    <string name="call_chat_invitation">Chat Invitation</string>
+    <string name="call_chat_invitation_tips">Get %1$s diamonds [icon]/15s</string>
+    <string name="call_chat_calling">Chat Calling</string>
+    <string name="call_chat_calling_tips">Cost %1$s coin [icon]/min</string>
+    <string name="call_chat_free_time">Free %1$d seconds</string>
+    <string name="call_media_conflict_title">Enter live call?</string>
+    <string name="call_media_conflict_tips">You are in %1$s now, You will automatically check out when you initiate a live call</string>
+    <string name="call_media_conflict_remind">Don\'t remind me again</string>
+    <string name="call_video_camera_off">The Camera is off</string>
+    <string name="call_fail_for_calling_now">Currently on a call</string>
+    <string name="call_permissoin_float_window">This feature requires enabling floating window and background startup permissions</string>
+    <string name="call_permission_grant_fail">Permission grant failed</string>
+    <string name="call_kit_error">Server Error(%1$s)</string>
+    <string name="call_kit_error_already_in_call">Can not start call, already in call</string>
+    <string name="call_caller_wait">Calling…</string>
+    <string name="call_called_wait">Waiting for answer…</string>
+    <string name="common_rtc_channel_name">RTC Channel Test</string>
+    <string name="common_rtc_channel_description">RTC Channel Test Desc</string>
+    <string name="call_invite_merchant">Initiate a voice call</string>
+    <string name="call_free">Free</string>
+    <string name="call_merchant_free_time">Callable today: %1$d time, %2$d minutes</string>
+    <string name="call_merchant_cost">Cost 0 Coin</string>
+    <string name="call_merchant_float_cost">Voice call, Call free</string>
+    <string name="call_remind_free_time">Remainder of today: %1$d minutes</string>
+    <string name="call_chat_fail_tip">He/she does not support the call function</string>
+    <string name="call_match_title">Random Chat</string>
+    <string name="call_match_start">Start Matching</string>
+    <string name="call_matching">Matching...</string>
+    <string name="call_match_next">Match Next</string>
+    <string name="call_change_video_cost">Once the other party agrees, you can start a video chat. You can switch after consuming %1$s[icon] coins in the first minute.</string>
+    <string name="call_change_video_earn">Once the other party agrees, you can start a video chat and earn an additional %1$s[icon] diamonds every 15 seconds.</string>
+    <string name="call_change_video_earn_invitation">%1$s has sent you a video invitation. Earn an extra %2$s[icon] diamonds every 15s</string>
+    <string name="call_change_video_cost_invitation">%1$s has sent you a video invitation. The first minute consumes %2$s[icon] coins to answer</string>
+    <string name="call_match_timeout_tips">Not matched yet. You can go chat with girls first~</string>
+    <string name="call_continue_match">Continue Matching</string>
+    <string name="call_go_chat">Go Chat</string>
+    <string name="call_fail_for_match_mode">Can not start call when matching</string>
+    <string name="call_change_video_timeout">The other party did not respond for a long time. Please initiate again</string>
+    <string name="call_duration">Call Duration:</string>
+    <string name="call_earn_diamonds">Earn Diamonds:</string>
+    <string name="call_change_video_decline">The other party did not accept your video call request</string>
+    <string name="call_match_failed">Match Failed</string>
+</resources>

+ 1 - 0
module/message/src/main/java/com/adealink/weparty/message/conversation/ConversationActivity.kt

@@ -85,6 +85,7 @@ class ConversationActivity: BaseActivity() {
             //未传inputMode情况,用默认模式覆盖原有模式
             intent.putExtra(Message.Common.EXTRA_INPUT_MODE, InputMode.NormalMode.mode)
         }
+        this@ConversationActivity.intent = intent
         Router.bind(this)
         val fragment = supportFragmentManager.findFragmentByTag(Message.Conversation.FRAGMENT_TAG) as? ConversationFragment
         fragment?.onNewIntent(intent)

+ 16 - 26
module/operation/src/main/java/com/adealink/weparty/operation/newuser/NewUserGreetingDialog.kt

@@ -14,7 +14,6 @@ import androidx.viewpager2.adapter.FragmentStateAdapter
 import androidx.viewpager2.widget.CompositePageTransformer
 import androidx.viewpager2.widget.ViewPager2
 import com.adealink.frame.aab.util.getCompatDrawable
-import com.adealink.frame.base.fastLazy
 import com.adealink.frame.mvvm.view.viewBinding
 import com.adealink.frame.router.Router
 import com.adealink.frame.router.annotation.BindExtra
@@ -47,10 +46,6 @@ class NewUserGreetingDialog : BaseDialogFragment(R.layout.dialog_new_user_greeti
     @BindExtra(Operation.NewUserGreetingDialog.EXTRA_DIALOG_DATA)
     var dialogData: BaseDialogData.NewUserGreetingDialogData? = null
 
-    private val dataList: MutableList<UserInfoItemData> = mutableListOf()
-    private val pageAdapter by fastLazy { UserGreetingPageAdapter() }
-
-
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         Router.bind(this)
@@ -93,6 +88,10 @@ class NewUserGreetingDialog : BaseDialogFragment(R.layout.dialog_new_user_greeti
     }
 
     private fun initViewPager() {
+
+        val userList = dialogData?.userInfoList ?: return
+        val pageAdapter = UserGreetingPageAdapter(userList)
+
         val recyclerView = binding.viewPager2.getChildAt(0) as RecyclerView
         val padding = (DisplayUtil.getScreenWidth() - 320.dp()) / 2
         recyclerView.apply {
@@ -125,26 +124,17 @@ class NewUserGreetingDialog : BaseDialogFragment(R.layout.dialog_new_user_greeti
                 }
             }
         })
-    }
 
-    override fun loadData() {
-        super.loadData()
-
-        val userList = dialogData?.userInfoList ?: return
-        dataList.clear()
-        dataList.addAll(userList)
-        pageAdapter.notifyDataSetChanged()
-
-
-        for (i in 0 until dataList.size) {
+        for (i in 0 until userList.size) {
             val indicator = ImageView(requireContext())
             indicator.setImageDrawable(getCompatDrawable(R.drawable.ic_new_user_greeting_indicator))
-            binding.indicator.addView(indicator, LinearLayout.LayoutParams(
-                LinearLayout.LayoutParams.WRAP_CONTENT,
-                LinearLayout.LayoutParams.WRAP_CONTENT
-            ).apply {
-                marginEnd = 6.dp()
-            })
+            binding.indicator.addView(
+                indicator, LinearLayout.LayoutParams(
+                    LinearLayout.LayoutParams.WRAP_CONTENT,
+                    LinearLayout.LayoutParams.WRAP_CONTENT
+                ).apply {
+                    marginEnd = 6.dp()
+                })
 
             if (i == 0) {
                 indicator.isSelected = true
@@ -152,15 +142,15 @@ class NewUserGreetingDialog : BaseDialogFragment(R.layout.dialog_new_user_greeti
         }
     }
 
-
-    inner class UserGreetingPageAdapter() : FragmentStateAdapter(this) {
+    inner class UserGreetingPageAdapter(val list: List<UserInfoItemData>) :
+        FragmentStateAdapter(this) {
 
         override fun getItemCount(): Int {
-            return dataList.size
+            return list.size
         }
 
         override fun createFragment(position: Int): Fragment {
-            return NewUserGreetingCardFragment.newInstance(dataList[position])
+            return NewUserGreetingCardFragment.newInstance(list[position])
         }
     }
 

+ 41 - 5
module/room/src/main/java/com/adealink/weparty/room/interceptor/EnterRoomUriInterceptor.kt

@@ -19,8 +19,11 @@ import com.adealink.frame.util.isActivityDestroy
 import com.adealink.weparty.App
 import com.adealink.weparty.commonui.dialogfragment.BaseDialogFragment
 import com.adealink.weparty.commonui.tip.showFailedTip
+import com.adealink.weparty.commonui.widget.CommonDialog
 import com.adealink.weparty.commonui.widget.ProgressDialog
 import com.adealink.weparty.error.CommonFunctionBlockError
+import com.adealink.weparty.match.MatchStateEnum
+import com.adealink.weparty.match.MatchStateMachine
 import com.adealink.weparty.media.MediaType
 import com.adealink.weparty.module.account.AccountModule
 import com.adealink.weparty.module.entereffect.EnterEffectModule
@@ -63,6 +66,23 @@ class EnterRoomUriInterceptor : UriInterceptor {
             return
         }
 
+        //处理跟匹配相关的冲突
+        if (MatchStateMachine.instance.getCurrentState() == MatchStateEnum.MATCHING) {
+            Log.d(TAG_ROOM_ENTER_ROOM, "EnterRoomUriInterceptor, in matching state, abort()")
+            CommonDialog.Builder()
+                .message(getCompatString(R.string.room_before_match_tips))
+                .onPositive {
+                    CoroutineScope(Dispatcher.UI).launch {
+                        enterRoom(activity, chain)
+                    }
+                }
+                .setShowDefaultCancel(true)
+                .build().show(activity.supportFragmentManager)
+            chain.abort()
+            return
+        }
+
+
         CoroutineScope(Dispatcher.UI).launch {
             enterRoom(activity, chain)
         }
@@ -116,7 +136,10 @@ class EnterRoomUriInterceptor : UriInterceptor {
         var enterRoomInfo: EnterRoomInfo?
         //1. 从跳转参数中获取进房信息
         enterRoomInfo = intent.getParcelableExtra(EXTRA_ENTER_ROOM_INFO)
-        Log.d(TAG_ROOM_ENTER_ROOM, "EnterRoomUriInterceptor, getEnterRoomInfo from enterRoomInfo, $enterRoomInfo")
+        Log.d(
+            TAG_ROOM_ENTER_ROOM,
+            "EnterRoomUriInterceptor, getEnterRoomInfo from enterRoomInfo, $enterRoomInfo"
+        )
 
         //2. 从deepLink进房参数中获取房间ID,合成进房信息
         if (enterRoomInfo == null) {
@@ -130,13 +153,19 @@ class EnterRoomUriInterceptor : UriInterceptor {
             if (roomId != null && roomId > 0L) {
                 enterRoomInfo = EnterRoomInfo(roomId = roomId, from = JoinRoomFrom.DEEPLINK.from)
             }
-            Log.d(TAG_ROOM_ENTER_ROOM, "EnterRoomUriInterceptor, getEnterRoomInfo from roomId, $enterRoomInfo")
+            Log.d(
+                TAG_ROOM_ENTER_ROOM,
+                "EnterRoomUriInterceptor, getEnterRoomInfo from roomId, $enterRoomInfo"
+            )
         }
 
         //房间信息无效,跳转房间失败
         if (enterRoomInfo == null || enterRoomInfo.roomId == 0L) {
             chain.abort()
-            Log.e(TAG_ROOM_ENTER_ROOM, "EnterRoomUriInterceptor, enterRoomInfo is null / roomId is 0, abort()")
+            Log.e(
+                TAG_ROOM_ENTER_ROOM,
+                "EnterRoomUriInterceptor, enterRoomInfo is null / roomId is 0, abort()"
+            )
             return
         }
 
@@ -198,7 +227,10 @@ class EnterRoomUriInterceptor : UriInterceptor {
             }
 
             is Rlt.Failed -> {
-                Log.e(TAG_ROOM_ENTER_ROOM, "EnterRoomUriInterceptor, join room fail(${result.error.serverCode}), abort()")
+                Log.e(
+                    TAG_ROOM_ENTER_ROOM,
+                    "EnterRoomUriInterceptor, join room fail(${result.error.serverCode}), abort()"
+                )
                 if (result.error is RoomJoinPwdError || result.error is RoomJoinNeedPwdError) {
                     showInputPwdDialog(activity, enterRoomInfo, intent.extras)
                 } else {
@@ -222,7 +254,11 @@ class EnterRoomUriInterceptor : UriInterceptor {
             }.send()
     }
 
-    private fun showInputPwdDialog(activity: FragmentActivity, enterRoomInfo: EnterRoomInfo, extra: Bundle?) {
+    private fun showInputPwdDialog(
+        activity: FragmentActivity,
+        enterRoomInfo: EnterRoomInfo,
+        extra: Bundle?
+    ) {
         Router.getRouterInstance<BaseDialogFragment>(Room.EnterRoomInputPassword.PATH)
             ?.apply {
                 arguments = extra ?: Bundle().also {

Некоторые файлы не были показаны из-за большого количества измененных файлов