Ver Fonte

feat: 实时匹配支持离线通知 (#32)

* feat: 匹配超时弹窗修改

* feat: 匹配语音个人信息展示

* feat: 后台匹配通知

* feat: 离线匹配通知

* feat: 匹配悬浮窗通知

* feat: notification channelId修改

* feat: 匹配通话跳转前台获取权限

* feat: 延长匹配通知到20s;进主页获取视频权限;悬浮窗/通知二选一展示

* feat: 离线通知进app白屏;区分悬浮窗和普通view

* feat: 实时匹配加点日志

* feat: 版本号37->38;离线通知走accept带上matchId

* feat: 实时匹配转视频金额拉取配置展示

* feat: 悬浮窗跳转

* feat: 加点日志

* feat: code review fix

* fix: 服务端发了两条匹配成功导致matchId被清除,前置摄像头打开

* fix: 匹配成功后收到通知不处理

* feat: 加锁处理callMatchResultNotify并发问题

* feat: 匹配页面头像处理
WilliumP há 6 meses atrás
pai
commit
a51329fe2c
54 ficheiros alterados com 1078 adições e 228 exclusões
  1. 22 1
      app/src/main/java/com/adealink/weparty/MainActivity.kt
  2. 2 0
      app/src/main/java/com/adealink/weparty/Routers.kt
  3. 2 1
      app/src/main/java/com/adealink/weparty/commonui/widget/floatview/FloatViewFactory.kt
  4. 5 3
      app/src/main/java/com/adealink/weparty/commonui/widget/floatview/ModeWindowManagerProxy.kt
  5. 2 2
      app/src/main/java/com/adealink/weparty/commonui/widget/floatview/data/Constants.kt
  6. 2 1
      app/src/main/java/com/adealink/weparty/commonui/widget/floatview/data/IFloatData.kt
  7. 1 1
      app/src/main/java/com/adealink/weparty/commonui/widget/floatview/mode/ApplicationModeWindowManager.kt
  8. 158 0
      app/src/main/java/com/adealink/weparty/commonui/widget/floatview/mode/SystemModeWindowManager.kt
  9. 12 19
      app/src/main/java/com/adealink/weparty/match/MatchStateMachine.kt
  10. 21 3
      app/src/main/java/com/adealink/weparty/module/call/CallModule.kt
  11. 7 1
      app/src/main/java/com/adealink/weparty/module/call/ICallService.kt
  12. 12 1
      app/src/main/java/com/adealink/weparty/module/call/data/Constant.kt
  13. 4 2
      app/src/main/java/com/adealink/weparty/module/call/match/IMatchManager.kt
  14. 9 0
      app/src/main/java/com/adealink/weparty/module/call/match/IMatchNotify.kt
  15. 9 0
      app/src/main/java/com/adealink/weparty/module/call/match/data/CallMatchData.kt
  16. 1 2
      app/src/main/java/com/adealink/weparty/module/call/viewmodel/ICallViewModel.kt
  17. 3 1
      app/src/main/java/com/adealink/weparty/notifiation/Constants.kt
  18. 9 3
      app/src/main/java/com/adealink/weparty/push/NotificationUtil.kt
  19. 1 1
      app/src/main/java/com/adealink/weparty/push/PushMessageManager.kt
  20. 2 1
      app/src/main/java/com/adealink/weparty/push/data/PushData.kt
  21. 106 0
      app/src/main/java/com/adealink/weparty/util/PermissionRequest.kt
  22. 3 0
      app/src/main/res/values-ar/strings.xml
  23. 3 0
      app/src/main/res/values-zh/strings.xml
  24. 3 0
      app/src/main/res/values/strings.xml
  25. 1 1
      gradle.properties
  26. 10 0
      module/call/src/main/AndroidManifest.xml
  27. 16 3
      module/call/src/main/java/com/adealink/weparty/call/CallServiceImpl.kt
  28. 8 1
      module/call/src/main/java/com/adealink/weparty/call/WenextUICallKitImpl.kt
  29. 4 0
      module/call/src/main/java/com/adealink/weparty/call/data/CallData.kt
  30. 0 8
      module/call/src/main/java/com/adealink/weparty/call/data/MatchData.kt
  31. 2 0
      module/call/src/main/java/com/adealink/weparty/call/datasource/local/CallLocalService.kt
  32. 6 0
      module/call/src/main/java/com/adealink/weparty/call/manager/CallLoginManager.kt
  33. 4 2
      module/call/src/main/java/com/adealink/weparty/call/manager/CallManager.kt
  34. 2 1
      module/call/src/main/java/com/adealink/weparty/call/match/CallMatchActivity.kt
  35. 101 37
      module/call/src/main/java/com/adealink/weparty/call/match/MatchManager.kt
  36. 1 0
      module/call/src/main/java/com/adealink/weparty/call/video/comp/VideoCallerComp.kt
  37. 1 0
      module/call/src/main/java/com/adealink/weparty/call/video/comp/VideoRoomComp.kt
  38. 1 1
      module/call/src/main/java/com/adealink/weparty/call/video/dialog/PayeeReceiveVideoInvitationDialog.kt
  39. 1 1
      module/call/src/main/java/com/adealink/weparty/call/video/dialog/PayeeSendVideoInvitationDialog.kt
  40. 2 2
      module/call/src/main/java/com/adealink/weparty/call/video/dialog/PayerReceiveVideoInvitationDialog.kt
  41. 2 2
      module/call/src/main/java/com/adealink/weparty/call/video/dialog/PayerSendVideoInvitationDialog.kt
  42. 0 9
      module/call/src/main/java/com/adealink/weparty/call/view/floatview/matchnotify/IMatchNotify.kt
  43. 7 1
      module/call/src/main/java/com/adealink/weparty/call/view/floatview/matchnotify/MatchFloatData.kt
  44. 21 21
      module/call/src/main/java/com/adealink/weparty/call/view/floatview/matchnotify/MatchFloatView.kt
  45. 95 0
      module/call/src/main/java/com/adealink/weparty/call/view/floatview/matchnotify/MatchFloatWindowView.kt
  46. 163 0
      module/call/src/main/java/com/adealink/weparty/call/view/floatview/matchnotify/MatchNotificationView.kt
  47. 30 0
      module/call/src/main/java/com/adealink/weparty/call/view/floatview/matchnotify/MatchNotifyReceiver.kt
  48. 31 20
      module/call/src/main/java/com/adealink/weparty/call/view/floatview/matchnotify/MatchNotifyView.kt
  49. 4 5
      module/call/src/main/java/com/adealink/weparty/call/viewmodel/CallViewModel.kt
  50. 2 1
      module/call/src/main/java/com/tencent/qcloud/tuikit/tuicallkit/state/TUICallState.kt
  51. 1 1
      module/call/src/main/java/com/tencent/qcloud/tuikit/tuicallkit/utils/PermissionRequest.kt
  52. 47 14
      module/call/src/main/java/com/tencent/qcloud/tuikit/tuicallkit/view/component/videolayout/VideoView.kt
  53. 2 2
      module/call/src/main/java/com/tencent/qcloud/tuikit/tuicallkit/view/root/BaseCallView.kt
  54. 114 52
      module/call/src/main/res/layout/call_video_view.xml

+ 22 - 1
app/src/main/java/com/adealink/weparty/MainActivity.kt

@@ -14,9 +14,12 @@ import com.adealink.frame.router.Router
 import com.adealink.frame.router.annotation.BindExtra
 import com.adealink.frame.router.annotation.RouterUri
 import com.adealink.frame.router.manager.deeplinkRouterManager
+import com.adealink.weparty.AppModule.Main.Companion.EXTRA_FROM_MATCH_NOTIFICATION
+import com.adealink.weparty.AppModule.Main.Companion.EXTRA_MATCH_ID
 import com.adealink.weparty.commonui.BaseActivity
 import com.adealink.weparty.module.account.Account
 import com.adealink.weparty.module.account.AccountModule
+import com.adealink.weparty.module.call.CallModule
 import com.adealink.weparty.push.PushStatEvent
 import com.adealink.weparty.stat.manager.serveReportManager
 import com.adealink.weparty.stat.reportAppOpenIfNeed
@@ -27,8 +30,10 @@ import com.adealink.weparty.ui.home.HomeFragment
 import com.adealink.weparty.ui.home.util.HomeLocalService
 import com.adealink.weparty.ui.splash.SplashFragment
 import com.adealink.weparty.update.updateManager
+import com.adealink.weparty.util.PermissionRequest
 import com.adealink.weparty.util.goLocalLinkPage
 import com.qmuiteam.qmui.widget.util.QMUIStatusBarHelper
+import com.tencent.qcloud.tuicore.permission.PermissionCallback
 
 @RouterUri(path = [AppModule.Main.PATH], desc = "首页")
 class MainActivity : BaseActivity() {
@@ -100,6 +105,7 @@ class MainActivity : BaseActivity() {
 
     override fun handleNewIntent(intent: Intent?) {
         super.handleNewIntent(intent)
+        setIntent(intent)
         intent?.extras?.let {
             mainTabKey = it.getString(AppModule.Main.EXTRA_MAIN_TAB)
             subTabKey = it.getString(AppModule.Main.EXTRA_MAIN_SUB_TAB)
@@ -129,6 +135,16 @@ class MainActivity : BaseActivity() {
                 finish()
                 return
             }
+
+            needHandleMatchCall(intent) -> {
+                Log.d(TAG, "needHandleMatchCall")
+                PermissionRequest.requestVideoPermission(this@MainActivity, object : PermissionCallback() {
+                    override fun onGranted() {
+                        val matchId = intent?.extras?.getLong(EXTRA_MATCH_ID, 0L) ?: 0L
+                        CallModule.getMatchManager()?.accept(matchId)
+                    }
+                })
+            }
         }
         inflateHomeFragment(from)
         intent ?: return
@@ -173,6 +189,12 @@ class MainActivity : BaseActivity() {
         return AppPref.needRegister
     }
 
+    private fun needHandleMatchCall(intent: Intent?): Boolean {
+        val fromMatchNotification =  intent?.extras?.getBoolean(EXTRA_FROM_MATCH_NOTIFICATION, false) ?: false
+        val matchId = intent?.extras?.getLong(EXTRA_MATCH_ID, 0L) ?: 0L
+        return fromMatchNotification && matchId > 0
+    }
+
     private fun inflateSplashFragment() {
         var splashFragment =
             supportFragmentManager.findFragmentByTag(SplashFragment.TAG) as? SplashFragment
@@ -203,5 +225,4 @@ class MainActivity : BaseActivity() {
 
         MainStartUpFragment.inject(this)
     }
-
 }

+ 2 - 0
app/src/main/java/com/adealink/weparty/Routers.kt

@@ -9,6 +9,8 @@ interface AppModule {
             const val EXTRA_MAIN_SUB_TAB = "subTab" //控制首页默认展示的子tab
 
             const val EXTRA_SPLASH_JUMP_URL = "splash_jump_url" //Splash页面跳转URL
+            const val EXTRA_FROM_MATCH_NOTIFICATION = "from_match_notification"
+            const val EXTRA_MATCH_ID = "match_id"
         }
     }
 }

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

@@ -49,7 +49,8 @@ class FloatViewFactory : IFloatViewFactory {
             FloatWindowType.GAME_ENTRANCE -> GameEntranceFloatView(data as GameEntranceFloatData)
             FloatWindowType.GLOBAL_HEADLINE -> HeadlineModule.getGlobalHeadlineFloatView(data)
             FloatWindowType.CALL_MATCH_ENTRANCE -> CallMatchEntranceFloatView(data as CallMatchEntranceFloatData)
-            FloatWindowType.CALL_MATCH_NOTIFY -> CallModule.getMatchNotifyFloatView(data)
+            FloatWindowType.MATCH_NOTIFY -> CallModule.getMatchFloatView(data)
+            FloatWindowType.MATCH_FLOAT_WINDOW -> CallModule.getMatchFloatView(data)
         }
         return floatView as? V
     }

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

@@ -7,8 +7,10 @@ import com.adealink.frame.util.getDeviceName
 import com.adealink.weparty.commonui.widget.floatview.data.FloatWindowType
 import com.adealink.weparty.commonui.widget.floatview.data.IFloatData
 import com.adealink.weparty.commonui.widget.floatview.data.MODE_APPLICATION
+import com.adealink.weparty.commonui.widget.floatview.data.MODE_SYSTEM
 import com.adealink.weparty.commonui.widget.floatview.data.TAG_FLOAT_VIEW
 import com.adealink.weparty.commonui.widget.floatview.mode.ApplicationModeWindowManager
+import com.adealink.weparty.commonui.widget.floatview.mode.SystemModeWindowManager
 import com.adealink.weparty.commonui.widget.floatview.mode.fix.Android10FixApplicationModeWindowManager
 import com.adealink.weparty.commonui.widget.floatview.mode.fix.Android14ModeWindowManager
 import com.adealink.weparty.commonui.widget.floatview.view.BaseFloatView
@@ -16,7 +18,7 @@ import com.adealink.weparty.commonui.widget.floatview.view.BaseFloatView
 class ModeWindowManagerProxy : BaseWindowManager() {
 
     private val windowManagerMap = LinkedHashMap<Int, BaseWindowManager>().apply {
-        //put(IBaseFloatData.MODE_SYSTEM, SystemModeWindowManager())
+        put(MODE_SYSTEM, SystemModeWindowManager())
         put(
             MODE_APPLICATION,
             if (Build.VERSION.SDK_INT == Build.VERSION_CODES.UPSIDE_DOWN_CAKE && getDeviceName().lowercase().startsWith("moto")) {
@@ -33,7 +35,7 @@ class ModeWindowManagerProxy : BaseWindowManager() {
         try {
             windowManagerMap[view.baseFloatData.windowMode()]?.addView(view)
         } catch (e: Exception) {
-            Log.e(TAG_FLOAT_VIEW, "ModeWindowManagerProxy addView ", e)
+            Log.e(TAG_FLOAT_VIEW, "ModeWindowManagerProxy addView $e")
         }
     }
 
@@ -43,7 +45,7 @@ class ModeWindowManagerProxy : BaseWindowManager() {
         try {
             windowManagerMap[view.baseFloatData.windowMode()]?.removeView(view, reason)
         } catch (e: Exception) {
-            Log.e(TAG_FLOAT_VIEW, "ModeWindowManagerProxy removeView ", e)
+            Log.e(TAG_FLOAT_VIEW, "ModeWindowManagerProxy removeView $e")
         }
     }
 

+ 2 - 2
app/src/main/java/com/adealink/weparty/commonui/widget/floatview/data/Constants.kt

@@ -4,11 +4,11 @@ import androidx.annotation.IntDef
 
 
 // ----------- 悬浮窗类型 -----------
-//const val MODE_SYSTEM = 1             // 系统级别(需要申请悬浮权限,暂不使用)
+const val MODE_SYSTEM = 1             // 系统级别(需要申请悬浮权限,暂不使用)
 const val MODE_APPLICATION = 2          // 应用级别
 //const val MODE_ACTIVITY = 3           // Activity级别,暂不使用
 
-@IntDef(MODE_APPLICATION)
+@IntDef(MODE_APPLICATION, MODE_SYSTEM)
 annotation class WindowMode
 
 

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

@@ -19,7 +19,8 @@ enum class FloatWindowType(val type: String) {
     GAME_ENTRANCE("game_entrance"),
     GLOBAL_HEADLINE("global_headline"), //全服横幅,房间内房间外
     CALL_MATCH_ENTRANCE("call_match_entrance"),
-    CALL_MATCH_NOTIFY("call_match_notify"),
+    MATCH_FLOAT_WINDOW("match_float_window"),
+    MATCH_NOTIFY("match_notify"),
 }
 
 

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

@@ -96,7 +96,7 @@ 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.CALL_MATCH_NOTIFY)
+                        && view.baseFloatData.windowType() != FloatWindowType.MATCH_NOTIFY)
             ) {
                 Log.w(
                     TAG_FLOAT_VIEW,

+ 158 - 0
app/src/main/java/com/adealink/weparty/commonui/widget/floatview/mode/SystemModeWindowManager.kt

@@ -0,0 +1,158 @@
+package com.adealink.weparty.commonui.widget.floatview.mode
+
+/**
+* author: PengWuliang
+* date: 2025/9/8
+* desc: 
+*/
+import android.app.Activity
+import android.content.Context
+import android.os.Build
+import android.provider.Settings
+import android.view.WindowManager
+import com.adealink.frame.log.Log
+import com.adealink.frame.util.AppUtil
+import com.adealink.weparty.commonui.widget.floatview.BaseWindowManager
+import com.adealink.weparty.commonui.widget.floatview.data.FloatWindowType
+import com.adealink.weparty.commonui.widget.floatview.data.IFloatData
+import com.adealink.weparty.commonui.widget.floatview.data.TAG_FLOAT_VIEW
+import com.adealink.weparty.commonui.widget.floatview.view.BaseFloatView
+import com.adealink.weparty.commonui.widget.floatview.view.BaseWindowFloatView
+import java.util.concurrent.CopyOnWriteArraySet
+
+class SystemModeWindowManager : BaseWindowManager() {
+
+    companion object {
+        private const val SUB_TAG = "SystemModeWindowManager"
+    }
+
+    private val globalWindowManager =
+        AppUtil.appContext.getSystemService(Context.WINDOW_SERVICE) as WindowManager
+    private val viewList = mutableListOf<BaseFloatView<out IFloatData>>()
+    private val viewTags = CopyOnWriteArraySet<String>()
+    private val viewTypes = CopyOnWriteArraySet<FloatWindowType>()
+
+    override fun addView(view: BaseFloatView<out IFloatData>) {
+        val mView = view as? BaseWindowFloatView ?: return
+        if ((Build.VERSION.SDK_INT < Build.VERSION_CODES.M
+                    || Settings.canDrawOverlays(AppUtil.appContext)).not()
+        ) {
+            return
+        }
+
+        if (viewTags.contains(mView.baseFloatData.windowTag())) {
+            Log.w(TAG_FLOAT_VIEW, "${mView.baseFloatData.windowTag()} is exist")
+            return
+        }
+        viewTypes.add(mView.baseFloatData.windowType())
+        viewTags.add(mView.baseFloatData.windowTag())
+        viewList.add(mView)
+        mView.setupWindowManager(this)
+        mView.windowParams.type =
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
+            else WindowManager.LayoutParams.TYPE_SYSTEM_ALERT
+        globalWindowManager.addView(mView, mView.windowParams)
+        mView.onCreate()
+        mView.onStart()
+        mView.onResume()
+        Log.i(TAG_FLOAT_VIEW, "$SUB_TAG, addView, view: $mView")
+    }
+
+    override fun removeView(
+        view: BaseFloatView<out IFloatData>, reason: String
+    ) {
+        val mView = view as? BaseWindowFloatView ?: return
+        if (!checkHasView(mView)) {
+            return
+        }
+        globalWindowManager.removeViewImmediate(mView)
+        viewTypes.remove(mView.baseFloatData.windowType())
+        viewTags.remove(mView.baseFloatData.windowTag())
+        viewList.remove(mView)
+        mView.windowParams.token = null
+        mView.onPause()
+        mView.onStop()
+        mView.onDestroy()
+        Log.i(TAG_FLOAT_VIEW, "$SUB_TAG, removeView, reason: $reason, view: $mView")
+    }
+
+    override fun findFloatViewByType(type: FloatWindowType): BaseFloatView<out IFloatData>? {
+        viewList.forEach {
+            if (it.baseFloatData.windowType() == type) {
+                return it
+            }
+        }
+        return null
+    }
+
+    override fun isFloatViewAdded(type: FloatWindowType): Boolean {
+        return findFloatViewByType(type) != null
+    }
+
+    override fun removeFloatViewByType(type: FloatWindowType, reason: String) {
+        findFloatViewByType(type)?.removeSelf(reason)
+    }
+
+    override fun setFloatViewVisibility(type: FloatWindowType, visibility: Int) {
+        findFloatViewByType(type)?.visibility = visibility
+    }
+
+    override fun removeAllFloatViews(type: FloatWindowType, reason: String) {
+        viewList.forEach {
+            if (it.baseFloatData.windowType() == type) {
+                it.removeSelf(reason)
+            }
+        }
+    }
+
+    override fun remove(predicate: (BaseFloatView<out IFloatData>) -> Boolean, reason: String) {
+        viewList.filter(predicate).forEach { view ->
+            removeView(view, reason)
+        }
+    }
+
+    private fun checkHasView(view: BaseFloatView<out IFloatData>): Boolean {
+        if (!viewList.contains(view)) {
+            Log.w(
+                TAG_FLOAT_VIEW,
+                "$SUB_TAG, checkHasView ${view.baseFloatData.windowTag()} is not exist maybe have bug"
+            )
+            return false
+        }
+        return true
+    }
+
+    override fun onScreenOn() {
+        super.onScreenOn()
+        viewList.forEach {
+            it.onResume()
+        }
+    }
+
+    override fun onScreenOff() {
+        super.onScreenOff()
+        viewList.forEach {
+            it.onPause()
+        }
+    }
+
+    override fun onActivityChange(activity: Activity) {
+        viewList.forEach { floatView ->
+            floatView.onActivityChange(activity)
+        }
+    }
+
+    override fun onEnterBackground() {
+        super.onEnterBackground()
+        viewList.forEach { floatView ->
+            floatView.onEnterBackground()
+        }
+    }
+
+    override fun onEnterForeground() {
+        super.onEnterForeground()
+        viewList.forEach { floatView ->
+            floatView.onEnterForeground()
+        }
+    }
+}

+ 12 - 19
app/src/main/java/com/adealink/weparty/match/MatchStateMachine.kt

@@ -3,11 +3,8 @@ 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
 
 /**
@@ -22,7 +19,9 @@ class MatchStateMachine {
         private const val TAG = "MatchStateMachine"
     }
 
-    private val _currentState = MutableLiveData<MatchStateEnum>(MatchStateEnum.IDLE)
+    @Volatile
+    private var state = MatchStateEnum.IDLE
+    private val _currentState = MutableLiveData(state)
     val currentState: MutableLiveData<MatchStateEnum> = _currentState
 
     private val mainHandler = Handler(Looper.getMainLooper())
@@ -32,14 +31,11 @@ class MatchStateMachine {
         callback?.onMatchTimeout()
     }
 
-    private val answerTimeout = Runnable {
-        showToast(getCompatString(R.string.common_response_timeout))
-    }
-
     private var matchStartTime: Long = 0L
 
+    @Synchronized
     fun getCurrentState(): MatchStateEnum {
-        return _currentState.value ?: MatchStateEnum.IDLE
+        return state
     }
 
     fun setTimeoutCallback(callback: MatchTimeoutCallback) {
@@ -49,14 +45,16 @@ class MatchStateMachine {
     /**
      * 检查是否可以处理某个事件
      */
+    @Synchronized
     fun canHandleEvent(event: MatchEvent): Boolean {
         val currentState = getCurrentState()
         return getNextState(currentState, event) != null
     }
 
     //todo pwl 调用的地方改成handleEventAsync?
+    @Synchronized
     fun handleEvent(event: MatchEvent): Boolean {
-        val currentState = _currentState.value ?: MatchStateEnum.IDLE
+        val currentState = state
         val nextState = getNextState(currentState, event)
 
         if (nextState == null) {
@@ -77,7 +75,7 @@ class MatchStateMachine {
      */
     suspend fun handleEventAsync(event: MatchEvent): Boolean {
         return withContext(Dispatcher.UI) {
-            val currentState = _currentState.value ?: MatchStateEnum.IDLE
+            val currentState = state
             val nextState = getNextState(currentState, event)
 
             if (nextState == null) {
@@ -100,6 +98,7 @@ class MatchStateMachine {
             MatchStateEnum.IDLE -> when (event) {
                 MatchEvent.START_MATCH -> MatchStateEnum.MATCHING
                 MatchEvent.RECEIVE_REQUEST -> MatchStateEnum.RECEIVE
+                MatchEvent.ACCEPT_REQUEST -> MatchStateEnum.MATCHING //离线通知确认,从IDLE变成MATCHING
                 MatchEvent.MATCH_FAILED,
                 MatchEvent.RESET -> MatchStateEnum.IDLE
                 else -> null
@@ -123,7 +122,6 @@ class MatchStateMachine {
             }
 
             MatchStateEnum.SUCCESS -> when (event) {
-                MatchEvent.MATCH_SUCCESS -> MatchStateEnum.SUCCESS
                 MatchEvent.RESET -> MatchStateEnum.IDLE
                 else -> null
             }
@@ -131,11 +129,12 @@ class MatchStateMachine {
     }
 
     private fun transitionTo(newState: MatchStateEnum, event: MatchEvent? = null) {
-        val oldState = _currentState.value ?: MatchStateEnum.IDLE
+        val oldState = state
         // 1、状态切换前的处理
         onStateExit(oldState)
 
         // 2、更新状态
+        state = newState
         if (Looper.myLooper() == Looper.getMainLooper()) {
             _currentState.value = newState
         } else {
@@ -153,9 +152,6 @@ class MatchStateMachine {
             MatchStateEnum.MATCHING -> {
                 mainHandler.removeCallbacks(matchTimeout)
             }
-            MatchStateEnum.RECEIVE -> {
-                mainHandler.removeCallbacks(answerTimeout)
-            }
             else -> {
                 // 其他状态无需额外处理
             }
@@ -167,9 +163,6 @@ class MatchStateMachine {
             MatchStateEnum.MATCHING -> {
                 mainHandler.postDelayed(matchTimeout, 60_000)
             }
-            MatchStateEnum.RECEIVE -> {
-                mainHandler.postDelayed(answerTimeout, 10_000)
-            }
             MatchStateEnum.IDLE -> {
                 matchStartTime = 0L
             }

+ 21 - 3
app/src/main/java/com/adealink/weparty/module/call/CallModule.kt

@@ -1,6 +1,7 @@
 package com.adealink.weparty.module.call
 
 import android.app.Application
+import android.content.Context
 import androidx.lifecycle.ViewModelStoreOwner
 import com.adealink.frame.aab.BaseDynamicModule
 import com.adealink.frame.aab.constant.AABModuleNotInitError
@@ -14,6 +15,7 @@ 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.match.IMatchNotify
 import com.adealink.weparty.module.call.viewmodel.ICallViewModel
 
 object CallModule : BaseDynamicModule<ICallService>(ICallService::class), ICallService {
@@ -38,7 +40,15 @@ object CallModule : BaseDynamicModule<ICallService>(ICallService::class), ICallS
                 return null
             }
 
-            override fun getMatchNotifyFloatView(data: IFloatData): BaseFloatView<out IFloatData>? {
+            override fun getMatchFloatView(data: IFloatData): BaseFloatView<out IFloatData>? {
+                return null
+            }
+
+            override fun getMatchFloatWindowView(data: IFloatData): BaseFloatView<out IFloatData>? {
+                return null
+            }
+
+            override fun getMatchNotificationView(context: Context): IMatchNotify? {
                 return null
             }
 
@@ -123,8 +133,16 @@ 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 getMatchFloatView(data: IFloatData): BaseFloatView<out IFloatData>? {
+        return getService().getMatchFloatView(data)
+    }
+
+    override fun getMatchFloatWindowView(data: IFloatData): BaseFloatView<out IFloatData>? {
+        return getService().getMatchFloatWindowView(data)
+    }
+
+    override fun getMatchNotificationView(context: Context): IMatchNotify? {
+        return getService().getMatchNotificationView(context)
     }
 
     override fun getCallViewModel(owner: ViewModelStoreOwner): ICallViewModel? {

+ 7 - 1
app/src/main/java/com/adealink/weparty/module/call/ICallService.kt

@@ -1,5 +1,6 @@
 package com.adealink.weparty.module.call
 
+import android.content.Context
 import androidx.lifecycle.ViewModelStoreOwner
 import com.adealink.frame.aab.IService
 import com.adealink.frame.media.IMediaOperatorGet
@@ -9,6 +10,7 @@ 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.match.IMatchNotify
 import com.adealink.weparty.module.call.viewmodel.ICallViewModel
 
 interface ICallService : IService<ICallService>, IMediaOperatorGet, IAppStartUpTask {
@@ -25,7 +27,11 @@ interface ICallService : IService<ICallService>, IMediaOperatorGet, IAppStartUpT
 
     fun getCallingFloatView(data: IFloatData): BaseFloatView<out IFloatData>?
 
-    fun getMatchNotifyFloatView(data: IFloatData): BaseFloatView<out IFloatData>?
+    fun getMatchFloatView(data: IFloatData): BaseFloatView<out IFloatData>?
+
+    fun getMatchFloatWindowView(data: IFloatData): BaseFloatView<out IFloatData>?
+
+    fun getMatchNotificationView(context: Context): IMatchNotify?
 
     fun getCallViewModel(owner: ViewModelStoreOwner): ICallViewModel?
 

+ 12 - 1
app/src/main/java/com/adealink/weparty/module/call/data/Constant.kt

@@ -1,4 +1,15 @@
 package com.adealink.weparty.module.call.data
 
 const val CALL_ERROR_UNKNOWN_MEDIA_TYPE = 0x001
-const val CALL_ERROR_START_CALL_SERVER_ERROR = 0x002
+const val CALL_ERROR_START_CALL_SERVER_ERROR = 0x002
+
+const val FROM_CALL_MANAGER = 1
+const val MATCH_PEER_NOT_SELF = 2
+const val MATCH_FAIL = 3
+const val MATCH_CALL_TIME_OUT = 4
+const val ACCEPT_FAIL = 5
+const val REJECT_SUCCESS = 6
+const val REJECT_FAIL = 7
+const val TIMEOUT_SUCCESS = 8
+const val TIMEOUT_FAIL = 9
+const val FROM_TUI_CALL = 10

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

@@ -22,7 +22,7 @@ interface IMatchManager: IBaseFrame<IMatchListener> {
 
     fun reject()
 
-    fun accept()
+    fun accept(matchId: Long? = null)
 
     fun timeout()
 
@@ -32,5 +32,7 @@ interface IMatchManager: IBaseFrame<IMatchListener> {
 
     fun getMatchId(): Long
 
-    fun clearData()
+    fun clearData(from: Int)
+
+    fun removeCallTimeout()
 }

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

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

+ 9 - 0
app/src/main/java/com/adealink/weparty/module/call/match/data/CallMatchData.kt

@@ -2,6 +2,7 @@ package com.adealink.weparty.module.call.match.data
 
 import com.adealink.frame.base.AppBaseInfo
 import com.adealink.frame.util.PackageUtil
+import com.adealink.weparty.module.profile.data.UserInfo
 import com.adealink.weparty.module.profile.tags.data.UserLabel
 import com.google.gson.annotations.Must
 import com.google.gson.annotations.SerializedName
@@ -133,4 +134,12 @@ data class CallMatchRewardGiveNotify(
     @SerializedName("currencyType") val currencyType: Int,
     @Must
     @SerializedName("currencyValue") val currencyValue: Int,
+)
+
+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
 )

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

@@ -5,7 +5,6 @@ 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>
@@ -26,5 +25,5 @@ interface ICallViewModel {
 
     fun getCallMatchEntrance()
 
-    fun getMatchUserList(): LiveData<Rlt<List<UserInfo>>>
+    fun getMatchUserList()
 }

+ 3 - 1
app/src/main/java/com/adealink/weparty/notifiation/Constants.kt

@@ -2,4 +2,6 @@ package com.adealink.weparty.notifiation
 
 const val ROOM_NOTIFICATION_CHANNEL_ID = "notification.room"
 
-const val CALL_NOTIFICATION_CHANNEL_ID = "notification.call"
+const val CALL_NOTIFICATION_CHANNEL_ID = "notification.call"
+
+const val MATCH_NOTIFICATION_CHANNEL_ID = "notification.match"

+ 9 - 3
app/src/main/java/com/adealink/weparty/push/NotificationUtil.kt

@@ -1,6 +1,5 @@
 package com.adealink.weparty.push
 
-import android.Manifest
 import android.annotation.SuppressLint
 import android.app.ActivityOptions
 import android.app.Notification
@@ -21,6 +20,7 @@ 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.coroutine.dispatcher.Dispatcher
+import com.adealink.frame.data.json.gson
 import com.adealink.frame.image.imageService
 import com.adealink.frame.image.listener.IImageLoadResultListener
 import com.adealink.frame.push.data.KEY_PUSH_DEEPLINK
@@ -35,9 +35,10 @@ import com.adealink.frame.util.PackageUtil
 import com.adealink.weparty.AppModule
 import com.adealink.weparty.R
 import com.adealink.weparty.commonui.imageview.CircleDrawable
-import com.adealink.weparty.commonui.widget.CommonDialog
+import com.adealink.weparty.module.call.CallModule
+import com.adealink.weparty.module.call.match.data.CallMatchReqAsk
 import com.adealink.weparty.module.profile.view.UserNameTextView
-import com.adealink.weparty.permission.PermissionUtils
+import com.adealink.weparty.push.data.PushMessageType
 import com.adealink.weparty.push.data.WeNextPushMessage
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
@@ -102,6 +103,11 @@ object NotificationUtil {
 
     private fun showInBroadcastNotification(pushMessage: WeNextPushMessage, bitmap: Bitmap?) {
         CoroutineScope(Dispatcher.UI).launch {
+            if(pushMessage.messageType == PushMessageType.CALL_MATCH_ASK.type) {
+                val data = gson.fromJson(pushMessage.pushContent, CallMatchReqAsk::class.java)
+                CallModule.getMatchNotificationView(AppUtil.appContext)?.showNotifyView(data.userInfo, data.matchTimeout, data.matchId)
+                return@launch
+            }
             val notifyId = pushMessage.pushId.toInt()
             val group = pushMessage.getTargetId() ?: ""
             val resultPendingIntent = if (Build.VERSION.SDK_INT == Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {

+ 1 - 1
app/src/main/java/com/adealink/weparty/push/PushMessageManager.kt

@@ -1,8 +1,8 @@
 package com.adealink.weparty.push
 
+import com.adealink.frame.aab.util.getCompatString
 import com.adealink.frame.push.data.PushMessage
 import com.adealink.frame.util.AppUtil
-import com.adealink.frame.aab.util.getCompatString
 import com.adealink.weparty.R
 import com.adealink.weparty.push.data.WeNextPushMessage
 

+ 2 - 1
app/src/main/java/com/adealink/weparty/push/data/PushData.kt

@@ -15,7 +15,8 @@ enum class PushMessageType(val type: Long) {
     ACTIVITY(5), //自建活动
     IM(40), //1v1会话提示
     SEND_GIFT(41), //送礼提示
-    INTIMACY_ONLINE(44) //亲密度上线提醒
+    INTIMACY_ONLINE(44), //亲密度上线提醒
+    CALL_MATCH_ASK(58), //匹配邀请
 }
 
 /**

+ 106 - 0
app/src/main/java/com/adealink/weparty/util/PermissionRequest.kt

@@ -0,0 +1,106 @@
+package com.adealink.weparty.util
+
+import android.Manifest
+import android.content.Context
+import android.os.Build
+import androidx.fragment.app.FragmentActivity
+import com.adealink.frame.aab.util.getCompatString
+import com.adealink.frame.util.AppUtil
+import com.adealink.weparty.R
+import com.adealink.weparty.commonui.ext.getActivity
+import com.adealink.weparty.permission.PermissionUtils
+import com.tencent.qcloud.tuicore.permission.PermissionCallback
+import com.tencent.qcloud.tuicore.permission.PermissionRequester
+
+/**
+ * author: PengWuliang
+ * date: 2025/9/8
+ * desc:
+ */
+object PermissionRequest {
+    fun requestFloatPermission(context: Context?, callback: PermissionCallback) {
+        if (PermissionRequester.newInstance(PermissionRequester.FLOAT_PERMISSION, PermissionRequester.BG_START_PERMISSION).has()) {
+            callback.onGranted()
+            return
+        }
+
+        val activity = context?.getActivity() ?: AppUtil.currentActivity as? FragmentActivity
+        if (activity == null || activity.isFinishing || activity.isDestroyed) {
+            callback.onDenied()
+            return
+        }
+
+        PermissionUtils.showRequestPermissionDialog(
+            activity,
+            getCompatString(R.string.call_permissoin_float_window),
+            onConfirm = {
+                //Please open both OverlayWindows and Background pop-ups permission.
+                PermissionRequester.newInstance(PermissionRequester.FLOAT_PERMISSION, PermissionRequester.BG_START_PERMISSION)
+                    .request()
+            },
+            onCancel = {
+                callback.onDenied()
+            }
+        )
+    }
+
+    fun requestVideoPermission(context: Context, callback: PermissionCallback) {
+        val permissions = arrayListOf<String>()
+        permissions.add(Manifest.permission.RECORD_AUDIO)
+        permissions.add(Manifest.permission.CAMERA)
+        if (PermissionRequester.newInstance(*permissions.toTypedArray()).has()) {
+            callback.onGranted()
+            return
+        }
+
+        val activity = context.getActivity() ?: AppUtil.currentActivity as? FragmentActivity
+        if (activity == null || activity.isFinishing || activity.isDestroyed) {
+            callback.onDenied()
+            return
+        }
+        PermissionUtils.requestPermissions(
+            activity,
+            permissions,
+            getCompatString(R.string.call_permission_video_tips),
+            onGranted = {
+                requestBluetoothPermission(context, object : PermissionCallback() {
+                    override fun onGranted() {
+                        callback.onGranted()
+                    }
+                })
+            },
+            onDenied = {
+                callback.onDenied()
+            }
+        )
+    }
+
+    private fun requestBluetoothPermission(context: Context, callback: PermissionCallback) {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
+            callback.onGranted()
+            return
+        }
+        if (PermissionRequester.newInstance(Manifest.permission.BLUETOOTH_CONNECT).has()) {
+            callback.onGranted()
+            return
+        }
+
+        val activity = context.getActivity() ?: AppUtil.currentActivity as? FragmentActivity
+        if (activity == null || activity.isFinishing || activity.isDestroyed) {
+            callback.onDenied()
+            return
+        }
+        PermissionUtils.requestPermissions(
+            activity,
+            listOf(Manifest.permission.BLUETOOTH_CONNECT),
+            getCompatString(R.string.call_permission_bluetooth_reason),
+            onGranted = {
+                callback.onGranted()
+            },
+            onDenied = {
+                //bluetooth is unnecessary permission, return permission granted
+                callback.onGranted()
+            }
+        )
+    }
+}

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

@@ -723,4 +723,7 @@
     <string name="common_can_not_send">غير قادر على الإرسال</string>
     <string name="common_reward_records">سجلات المكافآت</string>
     <string name="commonui_danmaku">دانماكو</string>
+    <string name="call_permissoin_float_window">تتطلب هذه الميزة تمكين النافذة العائمة وأذونات بدء التشغيل في الخلفية</string>
+    <string name="call_permission_video_tips">يجب عليك الوصول إلى الميكروفون والكاميرا الخاصة بك ويمكن استخدامهما لمكالمات الفيديو.</string>
+    <string name="call_permission_bluetooth_reason">لضمان إمكانية الاتصال بميزة البلوتوث للتواصل أثناء مكالمة صوتية/فيديو، نحتاج إلى طلب الإذن من الأجهزة القريبة.</string>
 </resources>

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

@@ -722,4 +722,7 @@
     <string name="common_can_not_send">无法发送</string>
     <string name="common_reward_records">奖励记录</string>
     <string name="commonui_danmaku">弹幕</string>
+    <string name="call_permissoin_float_window">该功能需要开启悬浮窗和后台启动权限</string>
+    <string name="call_permission_video_tips">需要访问您的麦克风和相机,并且可以用于视频通话。</string>
+    <string name="call_permission_bluetooth_reason">为了确保您在音频/视频通话期间可以连接到蓝牙功能进行通信,我们需要请求附近设备的许可。</string>
 </resources>

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

@@ -841,4 +841,7 @@
     <string name="family_role_admin">Family Admin</string>
     <string name="common_can_not_send">Cannot send</string>
     <string name="common_reward_records">Reward records</string>
+    <string name="call_permissoin_float_window">This feature requires enabling floating window and background startup permissions</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_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>
 </resources>

+ 1 - 1
gradle.properties

@@ -28,5 +28,5 @@ OFFICIAL=false
 
 IS_RELEASE=true
 
-VERSION_CODE=37
+VERSION_CODE=38
 VERSION_NAME=1.9.2

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

@@ -76,6 +76,16 @@
             </intent-filter>
         </receiver>
 
+        <receiver
+            android:name=".view.floatview.matchnotify.MatchNotifyReceiver"
+            android:enabled="true"
+            android:exported="false">
+            <intent-filter>
+                <action android:name="action_accept_match" />
+                <action android:name="action_reject_match" />
+            </intent-filter>
+        </receiver>
+
     </application>
 
 </manifest>

+ 16 - 3
module/call/src/main/java/com/adealink/weparty/call/CallServiceImpl.kt

@@ -1,6 +1,7 @@
 package com.adealink.weparty.call
 
 import android.app.Application
+import android.content.Context
 import androidx.lifecycle.ViewModelProvider
 import androidx.lifecycle.ViewModelStoreOwner
 import com.adealink.frame.aab.util.getCompatString
@@ -24,8 +25,11 @@ 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.MatchFloatView
+import com.adealink.weparty.call.view.floatview.matchnotify.MatchFloatWindowData
+import com.adealink.weparty.call.view.floatview.matchnotify.MatchFloatWindowView
+import com.adealink.weparty.call.view.floatview.matchnotify.MatchNotificationView
 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
@@ -36,6 +40,7 @@ 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.match.IMatchNotify
 import com.adealink.weparty.module.call.viewmodel.ICallViewModel
 import com.tencent.cloud.tuikit.engine.call.TUICallDefine
 import com.tencent.cloud.tuikit.engine.common.TUICommonDefine
@@ -117,8 +122,16 @@ class CallServiceImpl : ICallService {
         return CallingFloatView(data as CallingFloatData)
     }
 
-    override fun getMatchNotifyFloatView(data: IFloatData): BaseFloatView<out IFloatData> {
-        return MatchNotifyFloatView(data as MatchNotifyFloatData)
+    override fun getMatchFloatView(data: IFloatData): BaseFloatView<out IFloatData> {
+        return MatchFloatView(data as MatchNotifyFloatData)
+    }
+
+    override fun getMatchFloatWindowView(data: IFloatData): BaseFloatView<out IFloatData> {
+        return MatchFloatWindowView(data as MatchFloatWindowData)
+    }
+
+    override fun getMatchNotificationView(context: Context): IMatchNotify {
+        return MatchNotificationView(context)
     }
 
     override fun getCallViewModel(owner: ViewModelStoreOwner): ICallViewModel {

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

@@ -301,6 +301,12 @@ class WenextUICallKitImpl private constructor(context: Context) : TUICallKit(),
      * 应用后台
      */
     private fun handleNewCallWhenAppInBackground() {
+        if(matchManager.isMatchIdValid() && TUICallState.instance.callUserData.get().matchId == matchManager.getMatchId()) {
+            Log.i(TAG_CALL_MATCH_MODE, "handleNewCallWhenAppInBackground, accept call when match mode, matchId:${matchManager.getMatchId()}")
+            matchManager.removeCallTimeout()
+            callManager.accept(null)
+            return
+        }
 //        val hasFloatPermission = PermissionRequester.newInstance(PermissionRequester.FLOAT_PERMISSION).has()
 //        val hasBgPermission = PermissionRequester.newInstance(PermissionRequester.BG_START_PERMISSION).has()
         val hasNotificationPermission = PermissionRequest.isNotificationEnabled()
@@ -333,7 +339,8 @@ class WenextUICallKitImpl private constructor(context: Context) : TUICallKit(),
         //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()}")
+            Log.i(TAG_CALL_MATCH_MODE, "handleNewCallWhenAppInForeground, accept call when match mode, matchId:${matchManager.getMatchId()}")
+            matchManager.removeCallTimeout()
             callManager.accept(null)
             return
         }

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

@@ -106,6 +106,10 @@ data class CallConfigData(
     @SerializedName("videoCost") val videoCost: Long? = null,
     @GsonNullable
     @SerializedName("videoEarn") val videoEarn: Long? = null,
+    @GsonNullable
+    @SerializedName("matchCost") val matchCost: Long? = null,
+    @GsonNullable
+    @SerializedName("matchEarn") val matchEarn: Long? = null,
 )
 
 data class MerchantFreeCallData(

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

@@ -24,14 +24,6 @@ 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,

+ 2 - 0
module/call/src/main/java/com/adealink/weparty/call/datasource/local/CallLocalService.kt

@@ -17,10 +17,12 @@ object CallLocalService : TypeDelegationPrefs(
     //主叫每分钟扣费(xxx金币/min)
     var videoCostPerMin: Long by PrefUserKey("key_video_cost_per_min", 150)
     var chatCostPerMin: Long by PrefUserKey("key_chat_cost_per_min", 150)
+    var matchCostPerMin: Long by PrefUserKey("key_match_cost_per_min", 150)
 
     //被叫每分钟收益(xxx钻石/min)
     var videoEarnPerMin: Long by PrefUserKey("key_video_earn_per_min", 150)
     var chatEarnPerMin: Long by PrefUserKey("key_chat_earn_per_min", 150)
+    var matchEarnPerMin: Long by PrefUserKey("key_match_earn_per_min", 150)
 
     var callPingInterval: Long by PrefUserKey("key_call_ping_interval", 15_000) //每15秒一个ping
 

+ 6 - 0
module/call/src/main/java/com/adealink/weparty/call/manager/CallLoginManager.kt

@@ -171,6 +171,12 @@ class CallLoginManager : BaseFrame<ICallLoginListener>(),
             configData.videoEarn?.let {
                 CallLocalService.videoEarnPerMin = it
             }
+            configData.matchCost?.let {
+                CallLocalService.matchCostPerMin = it
+            }
+            configData.matchEarn?.let {
+                CallLocalService.matchEarnPerMin = it
+            }
         }
     }
 }

+ 4 - 2
module/call/src/main/java/com/adealink/weparty/call/manager/CallManager.kt

@@ -50,6 +50,7 @@ import com.adealink.weparty.module.backpack.CALL_EXPERIENCE_CARD
 import com.adealink.weparty.module.call.Call
 import com.adealink.weparty.module.call.data.CALL_ERROR_START_CALL_SERVER_ERROR
 import com.adealink.weparty.module.call.data.CallerSource
+import com.adealink.weparty.module.call.data.FROM_CALL_MANAGER
 import com.adealink.weparty.module.network.data.ServerCode
 import com.adealink.weparty.module.profile.ProfileModule
 import com.adealink.weparty.module.wallet.WalletModule
@@ -289,8 +290,9 @@ class CallManager : BaseFrame<ICallListener>(), ICallManager {
 
                 override fun onError(errCode: Int, errMsg: String?) {
                     callback?.onError(errCode, errMsg)
-                    //呼叫失败,清除匹配数据
-                    matchManager.clearData()
+                    //呼叫失败且不在呼叫状态,清除匹配数据
+                    Log.i(TAG_CALL_MATCH_MODE, "error code: $errCode")
+                    matchManager.clearData(FROM_CALL_MANAGER)
                     //处理媒体冲突不需要提示
                     if (errCode != CALL_ERROR_MEDIA_CONFLICT_CANCEL) {
                         if (errCode == ServerCode.CURRENCY_NOT_ENOUGH.code) {

+ 2 - 1
module/call/src/main/java/com/adealink/weparty/call/match/CallMatchActivity.kt

@@ -97,6 +97,7 @@ class CallMatchActivity : BaseActivity() {
                         binding.animBeforeMatch.startPlay()
                         binding.animBeforeMatchAvatar.startPlay()
                     } else {
+                        callViewModel.getMatchUserList()
                         setupBeforeMatchAnim()
                     }
                     binding.animMatching.gone()
@@ -149,7 +150,7 @@ class CallMatchActivity : BaseActivity() {
     }
 
     private fun setupBeforeMatchAnim() {
-        callViewModel.getMatchUserList().observe(this) { result ->
+        callViewModel.matchUserListLD.observe(this) { result ->
             when (result) {
                 is Rlt.Success -> {
                     binding.animBeforeMatchAvatar.setAsset(

+ 101 - 37
module/call/src/main/java/com/adealink/weparty/call/match/MatchManager.kt

@@ -5,6 +5,8 @@ import android.media.AudioAttributes
 import android.media.AudioManager
 import android.media.MediaPlayer
 import android.os.Build
+import android.os.Handler
+import android.os.Looper
 import android.os.Vibrator
 import androidx.fragment.app.FragmentActivity
 import com.adealink.frame.aab.util.getCompatString
@@ -19,34 +21,48 @@ 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.MatchFloatView
+import com.adealink.weparty.call.view.floatview.matchnotify.MatchFloatWindowData
+import com.adealink.weparty.call.view.floatview.matchnotify.MatchFloatWindowView
+import com.adealink.weparty.call.view.floatview.matchnotify.MatchNotificationView
 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.commonui.widget.floatview.data.MODE_SYSTEM
 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.ACCEPT_FAIL
 import com.adealink.weparty.module.call.data.CallType
 import com.adealink.weparty.module.call.data.CallerSource
+import com.adealink.weparty.module.call.data.MATCH_CALL_TIME_OUT
+import com.adealink.weparty.module.call.data.MATCH_FAIL
+import com.adealink.weparty.module.call.data.MATCH_PEER_NOT_SELF
+import com.adealink.weparty.module.call.data.REJECT_FAIL
+import com.adealink.weparty.module.call.data.REJECT_SUCCESS
+import com.adealink.weparty.module.call.data.TIMEOUT_FAIL
+import com.adealink.weparty.module.call.data.TIMEOUT_SUCCESS
 import com.adealink.weparty.module.call.match.IMatchListener
 import com.adealink.weparty.module.call.match.IMatchManager
+import com.adealink.weparty.module.call.match.IMatchNotify
+import com.adealink.weparty.module.call.match.data.CallMatchReqAsk
 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.tuicore.permission.PermissionRequester
 import com.tencent.qcloud.tuikit.tuicallkit.state.TUICallState
+import com.tencent.qcloud.tuikit.tuicallkit.utils.DeviceUtils
 import com.tencent.qcloud.tuikit.tuicallkit.utils.PermissionRequest
-import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
 
 
@@ -65,7 +81,14 @@ 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 var matchNotifyFloatView: IMatchNotify? = null
+    private val mainHandler = Handler(Looper.getMainLooper())
+
+    private val callTimeout = Runnable {
+        if(TUICallState.instance.selfUser.get().callStatus.get() == TUICallDefine.Status.None) {
+            clearData(MATCH_CALL_TIME_OUT)
+        }
+    }
 
     private val stateMachine = MatchStateMachine.instance
     private val matchHttpService by lazy {
@@ -87,19 +110,21 @@ class MatchManager : BaseFrame<IMatchListener>(), IMatchManager {
             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
-                    )
+                    if(stateMachine.handleEvent(MatchEvent.MATCH_SUCCESS)) {
+                        callMode = data.callMode
+                        Log.i(TAG_CALL_MATCH_MODE, "callManager call")
+                        callManager.call(
+                            data.peerUid,
+                            TUICallDefine.MediaType.Video,
+                            CallerSource.CALLER,
+                            null,
+                            true,
+                            matchId
+                        )
+                    }
                 } else if (data.peerUid != ProfileModule.getMyUid()) {
                     //matchResult为1,说明fromUid和peerUid匹配成功,但不一定是自己
-                    clearData()
+                    clearData(MATCH_PEER_NOT_SELF)
                     stopVibrateAndRing()
                     matchNotifyFloatView?.cancelNotifyView()
                     matchNotifyFloatView = null
@@ -108,7 +133,7 @@ class MatchManager : BaseFrame<IMatchListener>(), IMatchManager {
                     //走accept的回包
                 }
             } else {
-                clearData()
+                clearData(MATCH_FAIL)
                 stopVibrateAndRing()
                 matchNotifyFloatView?.cancelNotifyView()
                 matchNotifyFloatView = null
@@ -134,11 +159,17 @@ class MatchManager : BaseFrame<IMatchListener>(), IMatchManager {
             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)
-                    }
+            val isForeground = DeviceUtils.isAppRunningForeground(AppUtil.appContext)
+            Log.d(TAG_CALL_MATCH_MODE, "isAppRunningForeground: $isForeground")
+            if(isForeground) {
+                launch(Dispatcher.UI) {
+                    matchNotifyFloatView =
+                        MatchFloatView(MatchNotifyFloatData(MODE_APPLICATION)).apply {
+                            showNotifyView(data.userInfo, data.matchTimeout, data.matchId)
+                        }
+                }
+            } else {
+                handleInBackground(data)
             }
         }
 
@@ -243,10 +274,7 @@ class MatchManager : BaseFrame<IMatchListener>(), IMatchManager {
                     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()
-                    }
+                    mainHandler.postDelayed(callTimeout, 10_000)
                 }
 
                 is Rlt.Failed -> {
@@ -257,7 +285,13 @@ class MatchManager : BaseFrame<IMatchListener>(), IMatchManager {
         }
     }
 
-    override fun accept() {
+    override fun accept(matchId: Long?) {
+        if (matchId != null && matchId != 0L) {
+            this.matchId = matchId
+            Log.i(TAG_CALL_MATCH_MODE, "accept, matchId: $matchId")
+        }
+        matchNotifyFloatView?.cancelNotifyView()
+        matchNotifyFloatView = null
         stopVibrateAndRing()
         if (!stateMachine.canHandleEvent(MatchEvent.ACCEPT_REQUEST)) {
             Log.i(
@@ -270,7 +304,7 @@ class MatchManager : BaseFrame<IMatchListener>(), IMatchManager {
         launch {
             when (val rlt = matchHttpService.matchAnswer(
                 CallMatchReqAnswer(
-                    matchId,
+                    this@MatchManager.matchId,
                     ProfileModule.getMyUid(),
                     AnswerType.ACCEPT.value
                 )
@@ -278,7 +312,7 @@ class MatchManager : BaseFrame<IMatchListener>(), IMatchManager {
                 is Rlt.Success -> {
                     Log.i(TAG_CALL_MATCH_MODE, "accept success")
                     // 等待状态更新完成后再调用resultAck
-                    val eventHandled = stateMachine.handleEventAsync(MatchEvent.ACCEPT_REQUEST)
+                    val eventHandled = stateMachine.handleEvent(MatchEvent.ACCEPT_REQUEST)
                     if (eventHandled) {
                         Log.i(TAG_CALL_MATCH_MODE, "State updated, calling resultAck")
                         matchManager.resultAck()
@@ -288,13 +322,15 @@ class MatchManager : BaseFrame<IMatchListener>(), IMatchManager {
                 is Rlt.Failed -> {
                     Log.i(TAG_CALL_MATCH_MODE, "accept failed, ${rlt.error}")
                     stateMachine.handleEvent(MatchEvent.MATCH_TIMEOUT)
-                    clearData()
+                    clearData(ACCEPT_FAIL)
                 }
             }
         }
     }
 
     override fun reject() {
+        matchNotifyFloatView?.cancelNotifyView()
+        matchNotifyFloatView = null
         stopVibrateAndRing()
         if (!stateMachine.canHandleEvent(MatchEvent.REJECT_REQUEST)) {
             Log.w(
@@ -315,19 +351,21 @@ class MatchManager : BaseFrame<IMatchListener>(), IMatchManager {
                 is Rlt.Success -> {
                     Log.i(TAG_CALL_MATCH_MODE, "reject success")
                     stateMachine.handleEvent(MatchEvent.REJECT_REQUEST)
-                    clearData()
+                    clearData(REJECT_SUCCESS)
                 }
 
                 is Rlt.Failed -> {
                     Log.i(TAG_CALL_MATCH_MODE, "reject failed, ${rlt.error}")
                     stateMachine.handleEvent(MatchEvent.MATCH_TIMEOUT)
-                    clearData()
+                    clearData(REJECT_FAIL)
                 }
             }
         }
     }
 
     override fun timeout() {
+        matchNotifyFloatView?.cancelNotifyView()
+        matchNotifyFloatView = null
         stopVibrateAndRing()
         if (!stateMachine.canHandleEvent(MatchEvent.MATCH_TIMEOUT)) {
             Log.i(
@@ -348,13 +386,13 @@ class MatchManager : BaseFrame<IMatchListener>(), IMatchManager {
                 is Rlt.Success -> {
                     Log.i(TAG_CALL_MATCH_MODE, "timeout success")
                     stateMachine.handleEvent(MatchEvent.MATCH_TIMEOUT)
-                    clearData()
+                    clearData(TIMEOUT_SUCCESS)
                 }
 
                 is Rlt.Failed -> {
                     Log.i(TAG_CALL_MATCH_MODE, "timeout failed, ${rlt.error}")
                     stateMachine.handleEvent(MatchEvent.MATCH_TIMEOUT)
-                    clearData()
+                    clearData(TIMEOUT_FAIL)
                 }
             }
         }
@@ -372,12 +410,16 @@ class MatchManager : BaseFrame<IMatchListener>(), IMatchManager {
         return matchId != 0L
     }
 
-    override fun clearData() {
+    override fun clearData(from: Int) {
         matchId = 0L
         roomId = 0L
         callMode = CallType.Voice.type
         stateMachine.handleEvent(MatchEvent.RESET)
-        Log.i(TAG_CALL_MATCH_MODE, "state reset")
+        Log.i(TAG_CALL_MATCH_MODE, "state reset $from")
+    }
+
+    override fun removeCallTimeout() {
+        mainHandler.removeCallbacks(callTimeout)
     }
 
     private fun stopVibrateAndRing() {
@@ -423,7 +465,7 @@ class MatchManager : BaseFrame<IMatchListener>(), IMatchManager {
                 start()
             }
         } catch (e: Exception) {
-            Log.e(TAG_CALL_MATCH_MODE, "match mode start ring fail", e)
+            Log.e(TAG_CALL_MATCH_MODE, "match mode start ring fail $e")
         }
     }
 
@@ -437,7 +479,29 @@ class MatchManager : BaseFrame<IMatchListener>(), IMatchManager {
             }
             mediaPlayer = null
         } catch (e: Exception) {
-            Log.e(TAG_CALL_MATCH_MODE, "match mode stop ring fail", e)
+            Log.e(TAG_CALL_MATCH_MODE, "match mode stop ring fail $e")
+        }
+    }
+
+    private fun handleInBackground(data: CallMatchReqAsk) {
+        val hasFloatPermission = PermissionRequester.newInstance(PermissionRequester.FLOAT_PERMISSION).has()
+        val hasNotificationPermission = PermissionRequest.isNotificationEnabled()
+        launch(Dispatcher.UI) {
+            if(hasFloatPermission) {
+                matchNotifyFloatView = MatchFloatWindowView(MatchFloatWindowData(MODE_SYSTEM)).apply {
+                    showNotifyView(
+                        data.userInfo, data.matchTimeout, data.matchId
+                    )
+                }
+            } else if(hasNotificationPermission) {
+                matchNotifyFloatView = MatchNotificationView(AppUtil.appContext).apply {
+                    showNotifyView(
+                        data.userInfo, data.matchTimeout, data.matchId
+                    )
+                }
+            } else {
+                Log.d(TAG_CALL_MATCH_MODE, "App is in background with no permission")
+            }
         }
     }
 }

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

@@ -54,6 +54,7 @@ class VideoCallerComp(
             Log.d(TAG_CALL_MATCH_MODE, "initBigRenderView, no need open in match mode begin")
             return
         }
+        Log.i(TAG_CALL_MATCH_MODE, "initBigRenderView, matchId: ${matchManager.getMatchId()}, callUserDataMatchId: ${TUICallState.instance.callUserData.get().matchId}")
         if (TUICallState.instance.isCameraOpen.get()) {
             EngineManager.instance.openCamera(viewModel.isFrontCamera.get(), videoViewBig?.getVideoView(), null)
         }

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

@@ -211,6 +211,7 @@ class VideoRoomComp(
             Log.i(TAG_CALL_MATCH_MODE, "openVideo, no need open in match mode begin")
             return
         }
+        Log.i(TAG_CALL_MATCH_MODE, "openVideo, matchId: ${matchManager.getMatchId()}, callUserDataMatchId: ${TUICallState.instance.callUserData.get().matchId}, forceOpen: $forceOpen")
         switchCamera?.show()
         smallVideoContainer?.show()
         if (user?.id == viewModel.remoteUser.id) {

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

@@ -75,7 +75,7 @@ class PayeeReceiveVideoInvitationDialog: BaseDialogFragment(R.layout.dialog_paye
 
     private fun videoCallingTips(): SpannableStringBuilder {
         val userName = TUICallState.instance.getRemoteUser()?.userInfo?.get()?.name ?: ""
-        val earnDiamond = (CallLocalService.videoEarnPerMin - CallLocalService.chatEarnPerMin).toString()
+        val earnDiamond = (CallLocalService.matchEarnPerMin).toString()
         val text = getCompatString(R.string.call_change_video_earn_invitation, userName, earnDiamond)
         return SpannableStringBuilder(text).apply {
             val i = text.indexOf(ICON_TAG)

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

@@ -59,7 +59,7 @@ class PayeeSendVideoInvitationDialog: BaseDialogFragment(R.layout.dialog_payee_v
     }
 
     private fun videoCallingTips(): SpannableStringBuilder {
-        val earnDiamond = (CallLocalService.videoEarnPerMin - CallLocalService.chatEarnPerMin).toString()
+        val earnDiamond = (CallLocalService.matchEarnPerMin).toString()
         val text = getCompatString(R.string.call_change_video_earn, earnDiamond)
         return SpannableStringBuilder(text).apply {
             val i = text.indexOf(ICON_TAG)

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

@@ -80,8 +80,8 @@ class PayerReceiveVideoInvitationDialog: BaseDialogFragment(R.layout.dialog_paye
 
     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)
+        val cost = (CallLocalService.matchCostPerMin).toString()
+        val text = getCompatString(R.string.call_change_video_cost_invitation, userName, cost)
         return SpannableStringBuilder(text).apply {
             val i = text.indexOf(ICON_TAG)
             if (i >= 0) {

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

@@ -65,8 +65,8 @@ class PayerSendVideoInvitationDialog: BaseDialogFragment(R.layout.dialog_payer_v
     }
 
     private fun videoCallingTips(): SpannableStringBuilder {
-        val firstMinCostDifference = (CallLocalService.videoCostPerMin - CallLocalService.chatCostPerMin).toString()
-        val text = getCompatString(R.string.call_change_video_cost, firstMinCostDifference)
+        val cost = (CallLocalService.matchCostPerMin).toString()
+        val text = getCompatString(R.string.call_change_video_cost, cost)
         return SpannableStringBuilder(text).apply {
             val i = text.indexOf(ICON_TAG)
             if (i >= 0) {

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

@@ -1,9 +0,0 @@
-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()
-}

+ 7 - 1
module/call/src/main/java/com/adealink/weparty/call/view/floatview/matchnotify/MatchNotifyFloatData.kt → module/call/src/main/java/com/adealink/weparty/call/view/floatview/matchnotify/MatchFloatData.kt

@@ -3,9 +3,15 @@ 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
+import com.adealink.weparty.commonui.widget.floatview.data.IWindowFloatData
 
 class MatchNotifyFloatData(private val windowMode: Int) : ILayoutFloatData {
-    override fun windowType(): FloatWindowType = FloatWindowType.CALL_MATCH_NOTIFY
+    override fun windowType(): FloatWindowType = FloatWindowType.MATCH_NOTIFY
     override fun windowMode(): Int = windowMode
     override fun gravity(): Int = GRAVITY_TOP
+}
+
+class MatchFloatWindowData(private val windowMode: Int) : IWindowFloatData {
+    override fun windowType(): FloatWindowType = FloatWindowType.MATCH_FLOAT_WINDOW
+    override fun windowMode(): Int = windowMode
 }

+ 21 - 21
module/call/src/main/java/com/adealink/weparty/call/view/floatview/matchnotify/MatchNotifyFloatView.kt → module/call/src/main/java/com/adealink/weparty/call/view/floatview/matchnotify/MatchFloatView.kt

@@ -3,27 +3,31 @@ 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.frame.mvvm.lifecycle.viewScope
 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.call.match.IMatchNotify
 import com.adealink.weparty.module.profile.data.UserInfo
+import kotlinx.coroutines.Runnable
+import kotlinx.coroutines.launch
 
 /**
  * author: PengWuliang
  * date: 2025/8/26
- * desc:
+ * desc: 应用内匹配通知
  */
-class MatchNotifyFloatView(floatData: MatchNotifyFloatData): BaseSlideFloatView(floatData),
+class MatchFloatView(floatData: MatchNotifyFloatData): BaseSlideFloatView(floatData),
         IMatchNotify {
 
     private var matchNotifyView: MatchNotifyView = MatchNotifyView(context)
-
+    private val timeoutRunnable = Runnable {
+        matchManager.timeout()
+    }
     override val layoutParams: LayoutParams
         get() = LayoutParams(LayoutParams.MATCH_PARENT, getCompatDimensionPixelSize(R.dimen.top_float_view_height)).apply {
             topToTop = LayoutParams.PARENT_ID
@@ -41,31 +45,27 @@ class MatchNotifyFloatView(floatData: MatchNotifyFloatData): BaseSlideFloatView(
     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.")
+    override fun showNotifyView(user: UserInfo, timeout: Long, matchId: Long) {
+        if (WindowManagerProxy.getWindowManager().findFloatViewByType(FloatWindowType.MATCH_NOTIFY) != null) {
+            Log.d(TAG_CALL_MATCH_MODE, "MatchFloatView.showCallingView return, for floatView already added.")
             return
         }
-        Log.d(TAG_CALL_FLOAT_WINDOW, "MatchNotifyFloatView.showNotifyView")
+        Log.d(TAG_CALL_MATCH_MODE, "MatchFloatView.showNotifyView")
         matchNotifyView.showNotifyView(user)
-        WindowManagerProxy.getWindowManager().addView(this)
-        matchNotifyView.postDelayed({
-            matchManager.timeout()
-            cancelNotifyView()
-        }, timeout * 1000)
+        WindowManagerProxy.getWindowManager().addView(this@MatchFloatView)
+        matchNotifyView.postDelayed(timeoutRunnable, timeout * 1000)
     }
 
     override fun cancelNotifyView() {
-        Log.d(TAG_CALL_FLOAT_WINDOW, "MatchNotifyFloatView.cancelNotifyView")
+        Log.d(TAG_CALL_MATCH_MODE, "MatchFloatView.cancelNotifyView")
+        matchNotifyView.removeCallbacks(timeoutRunnable)
         if (isAttachedToWindow) {
-            WindowManagerProxy.getWindowManager().removeView(this, reason = "MatchNotifyFloatView.cancelNotifyView")
+            viewScope.launch {
+                WindowManagerProxy.getWindowManager()
+                    .removeView(this@MatchFloatView, reason = "MatchFloatView.cancelNotifyView")
+            }
         }
     }
 

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

@@ -0,0 +1,95 @@
+package com.adealink.weparty.call.view.floatview.matchnotify
+
+import android.app.Activity
+import android.view.View
+import android.view.WindowManager
+import com.adealink.frame.coroutine.dispatcher.Dispatcher
+import com.adealink.frame.log.Log
+import com.adealink.frame.mvvm.lifecycle.viewScope
+import com.adealink.frame.util.DisplayUtil
+import com.adealink.weparty.call.R
+import com.adealink.weparty.call.constant.TAG_CALL_MATCH_MODE
+import com.adealink.weparty.call.match.matchManager
+import com.adealink.weparty.commonui.ext.dp
+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.BaseDragFloatView
+import com.adealink.weparty.module.call.match.IMatchNotify
+import com.adealink.weparty.module.profile.data.UserInfo
+import kotlinx.coroutines.launch
+import com.adealink.weparty.R as APP_R
+
+/**
+ * author: PengWuliang
+ * date: 2025/8/26
+ * desc: 匹配悬浮窗(app处于后台,悬浮显示)
+ */
+class MatchFloatWindowView(floatData: MatchFloatWindowData): BaseDragFloatView(floatData),
+        IMatchNotify {
+
+    private var matchNotifyView: MatchNotifyView = MatchNotifyView(context)
+    private val timeoutRunnable = kotlinx.coroutines.Runnable {
+        matchManager.timeout()
+    }
+
+    init {
+        id = APP_R.id.id_float_incoming_view
+    }
+
+    override fun getLayoutParamWidth(): Int {
+        return 350.dp()
+    }
+
+    override fun getLayoutParamHeight(): Int {
+        return WindowManager.LayoutParams.WRAP_CONTENT
+    }
+
+    override fun getClickableViews(): List<View> {
+        val acceptBtn = matchNotifyView.findViewById<View>(R.id.accept_btn)
+        val rejectBtn = matchNotifyView.findViewById<View>(R.id.reject_btn)
+        return listOfNotNull(acceptBtn, rejectBtn)
+    }
+
+    override fun getLayoutParamX(): Int {
+        return (DisplayUtil.getScreenWidth() - getLayoutParamWidth()) / 2
+    }
+
+    override fun getLayoutParamY(): Int {
+        return 0.dp()
+    }
+
+    override fun applySnapToEdge(): Boolean {
+        return false
+    }
+
+    override fun onCreate() {
+        super.onCreate()
+        setContentView(matchNotifyView)
+    }
+
+    override fun showNotifyView(user: UserInfo, timeout: Long, matchId: Long) {
+        if (WindowManagerProxy.getWindowManager().findFloatViewByType(FloatWindowType.MATCH_FLOAT_WINDOW) != null) {
+            Log.d(TAG_CALL_MATCH_MODE, "MatchFloatWindowView.showCallingView return, for floatView already added.")
+            return
+        }
+        Log.i(TAG_CALL_MATCH_MODE, "MatchFloatWindowView.showNotifyView")
+        matchNotifyView.showNotifyView(user)
+        WindowManagerProxy.getWindowManager().addView(this@MatchFloatWindowView)
+        matchNotifyView.postDelayed(timeoutRunnable, timeout * 1000)
+    }
+
+    override fun cancelNotifyView() {
+        Log.i(TAG_CALL_MATCH_MODE, "MatchFloatWindowView.cancelNotifyView")
+        matchNotifyView.removeCallbacks(timeoutRunnable)
+        if (isAttachedToWindow) {
+            viewScope.launch(Dispatcher.UI) {
+                WindowManagerProxy.getWindowManager().removeView(this@MatchFloatWindowView, reason = "MatchFloatWindowView.cancelNotifyView")
+            }
+        }
+    }
+
+    override fun onActivityChange(activity: Activity) {
+        super.onActivityChange(activity)
+        removeSelf("onActivityChange")
+    }
+}

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

@@ -0,0 +1,163 @@
+package com.adealink.weparty.call.view.floatview.matchnotify
+
+import android.app.Notification
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.graphics.Bitmap
+import android.view.View
+import android.widget.RemoteViews
+import androidx.core.app.NotificationCompat
+import com.adealink.frame.aab.util.getCompatColor
+import com.adealink.frame.aab.util.getCompatString
+import com.adealink.frame.image.imageService
+import com.adealink.frame.image.listener.IImageLoadResultListener
+import com.adealink.frame.log.Log
+import com.adealink.frame.util.ImageUtil
+import com.adealink.weparty.AppModule.Main.Companion.EXTRA_FROM_MATCH_NOTIFICATION
+import com.adealink.weparty.AppModule.Main.Companion.EXTRA_MATCH_ID
+import com.adealink.weparty.MainActivity
+import com.adealink.weparty.call.R
+import com.adealink.weparty.call.constant.TAG_CALL_MATCH_MODE
+import com.adealink.weparty.commonui.ext.dp
+import com.adealink.weparty.module.call.match.IMatchNotify
+import com.adealink.weparty.module.profile.data.UserInfo
+import com.adealink.weparty.notifiation.MATCH_NOTIFICATION_CHANNEL_ID
+import com.adealink.weparty.push.NotificationUtil
+
+/**
+ * author: PengWuliang
+ * date: 2025/9/5
+ * desc: 匹配通知,位于通知栏
+ */
+class MatchNotificationView(context: Context): IMatchNotify {
+
+    companion object {
+        private val AVATAR_SIZE = 40.dp()
+
+        private val COUNTRY_WIDTH = 15.5f.dp()
+        private val COUNTRY_HEIGHT = 10.5f.dp()
+        private val MATCH_NOTIFICATION_ID = 9910
+    }
+
+    private val context = context.applicationContext
+    private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+
+    private var remoteViews: RemoteViews? = null
+    private var notification: Notification? = null
+
+    init {
+        NotificationUtil.createNotificationChannel(MATCH_NOTIFICATION_CHANNEL_ID)
+    }
+
+    override fun showNotifyView(user: UserInfo, timeout: Long, matchId: Long) {
+        Log.d(TAG_CALL_MATCH_MODE, "MatchNotificationView showNotifyView")
+        notification = createNotification(matchId)
+        val remoteViews = this.remoteViews ?: run {
+            Log.e(TAG_CALL_MATCH_MODE, "RemoteViews is null after createNotification")
+            return
+        }
+        remoteViews.setTextViewText(R.id.tv_incoming_title, user.name ?: "")
+        remoteViews.setTextViewText(R.id.tv_desc, getCompatString(com.adealink.weparty.R.string.common_random_chat))
+        remoteViews.setTextColor(R.id.tv_desc, getCompatColor(com.adealink.weparty.R.color.color_FFC251FF))
+        val avatarUrl = user.url
+        if (avatarUrl.isNullOrEmpty()) {
+            remoteViews.setImageViewResource(R.id.img_incoming_avatar, com.adealink.weparty.R.drawable.common_default_avatar_ic)
+            notificationManager.notify(MATCH_NOTIFICATION_ID, notification)
+        } else {
+            val avatarOptUrl = imageService.getResizeUrl(avatarUrl, AVATAR_SIZE, AVATAR_SIZE)
+            imageService.fetchImage(avatarOptUrl, object : IImageLoadResultListener {
+                override fun onSuccess(bitmap: Bitmap) {
+                    val roundBitmap = ImageUtil.getRoundedCornerBitmap(bitmap, bitmap.width / 2f)
+                    if (roundBitmap != null && !roundBitmap.isRecycled) {
+                        remoteViews?.setImageViewBitmap(R.id.img_incoming_avatar, roundBitmap)
+                        bitmap.recycle()
+                    } else {
+                        remoteViews?.setImageViewBitmap(R.id.img_incoming_avatar, bitmap)
+                    }
+                    notificationManager.notify(MATCH_NOTIFICATION_ID, notification)
+                }
+
+                override fun onFailed() {
+                    remoteViews?.setImageViewResource(R.id.img_incoming_avatar, com.adealink.weparty.R.drawable.common_default_avatar_ic)
+                    notificationManager.notify(MATCH_NOTIFICATION_ID, notification)
+                }
+            })
+        }
+
+        val flagUrl = user.flag
+        if (flagUrl.isNullOrEmpty()) {
+            remoteViews.setViewVisibility(R.id.img_incoming_country, View.GONE)
+            notificationManager.notify(MATCH_NOTIFICATION_ID, notification)
+        } else {
+            val flagOptUrl = imageService.getResizeUrl(flagUrl, COUNTRY_WIDTH, COUNTRY_HEIGHT)
+            imageService.fetchImage(flagOptUrl, object : IImageLoadResultListener {
+                override fun onSuccess(bitmap: Bitmap) {
+                    remoteViews?.setViewVisibility(R.id.img_incoming_country, View.VISIBLE)
+                    remoteViews?.setImageViewBitmap(R.id.img_incoming_country, bitmap)
+                    notificationManager.notify(MATCH_NOTIFICATION_ID, notification)
+                }
+
+                override fun onFailed() {
+                    remoteViews?.setViewVisibility(R.id.img_incoming_country, View.GONE)
+                    notificationManager.notify(MATCH_NOTIFICATION_ID, notification)
+                }
+            })
+        }
+    }
+
+    override fun cancelNotifyView() {
+        Log.d(TAG_CALL_MATCH_MODE, "MatchNotificationView cancelNotifyView")
+        notificationManager.cancel(MATCH_NOTIFICATION_ID)
+    }
+
+    private fun createNotification(matchId: Long): Notification {
+        val channelId = MATCH_NOTIFICATION_CHANNEL_ID
+        val builder = NotificationCompat.Builder(context, channelId)
+            .setOngoing(true)
+            .setWhen(System.currentTimeMillis())
+            .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+            .setChannelId(channelId)
+            .setSmallIcon(com.adealink.weparty.R.drawable.ic_launcher)
+            .setSound(null)
+            .setTimeoutAfter(20_000L)
+            .setCategory(NotificationCompat.CATEGORY_CALL)
+            .setPriority(NotificationCompat.PRIORITY_MAX)
+
+        remoteViews = RemoteViews(context.packageName, R.layout.call_incoming_notification_view)
+        remoteViews?.setOnClickPendingIntent(R.id.btn_decline, getDeclineIntent())
+        remoteViews?.setOnClickPendingIntent(R.id.btn_accept, getAcceptIntent(matchId))
+
+        builder.setContentIntent(getPendingIntent())
+        builder.setFullScreenIntent(getPendingIntent(), true)
+
+        builder.setCustomContentView(remoteViews)
+        builder.setCustomBigContentView(remoteViews)
+        return builder.build()
+    }
+
+    //使用固定的requestCode会导致系统缓存旧的PendingIntent,即使Intent内容不同
+    private fun getAcceptIntent(matchId: Long): PendingIntent {
+        val intent = Intent(context, MainActivity::class.java).apply {
+            addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
+            putExtra(EXTRA_FROM_MATCH_NOTIFICATION, true)
+            putExtra(EXTRA_MATCH_ID, matchId)
+        }
+        return PendingIntent.getActivity(context, System.currentTimeMillis().toInt(), intent, PendingIntent.FLAG_IMMUTABLE)
+    }
+
+    private fun getDeclineIntent(): PendingIntent {
+        val intent = Intent(context, MatchNotifyReceiver::class.java).apply {
+            action = MatchNotifyReceiver.ACTION_REJECT_MATCH
+        }
+        return PendingIntent.getBroadcast(context, System.currentTimeMillis().toInt(), intent, PendingIntent.FLAG_IMMUTABLE)
+    }
+
+    private fun getPendingIntent(): PendingIntent {
+        val intent = Intent(context, MainActivity::class.java).apply {
+            addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
+        }
+        return PendingIntent.getActivity(context, System.currentTimeMillis().toInt(), intent, PendingIntent.FLAG_IMMUTABLE)
+    }
+}

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

@@ -0,0 +1,30 @@
+package com.adealink.weparty.call.view.floatview.matchnotify
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import com.adealink.frame.log.Log
+import com.adealink.weparty.call.constant.TAG_CALL_MATCH_MODE
+import com.adealink.weparty.call.match.matchManager
+
+class MatchNotifyReceiver : BroadcastReceiver() {
+    companion object {
+        const val ACTION_ACCEPT_MATCH = "action_accept_match"
+        const val ACTION_REJECT_MATCH = "action_reject_match"
+    }
+
+    override fun onReceive(context: Context, intent: Intent?) {
+        if(intent == null) {
+            return
+        }
+        when(intent.action) {
+            ACTION_REJECT_MATCH -> {
+                Log.i(TAG_CALL_MATCH_MODE, "MatchNotifyReceiver, reject match")
+                matchManager.reject()
+            }
+            else -> {
+
+            }
+        }
+    }
+}

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

@@ -1,28 +1,29 @@
 package com.adealink.weparty.call.view.floatview.matchnotify
 
 import android.content.Context
+import android.content.Intent
 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.frame.util.AppUtil
+import com.adealink.weparty.AppModule.Main.Companion.EXTRA_FROM_MATCH_NOTIFICATION
+import com.adealink.weparty.MainActivity
 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
+import com.adealink.weparty.util.PermissionRequest
+import com.tencent.qcloud.tuicore.permission.PermissionCallback
+import com.tencent.qcloud.tuikit.tuicallkit.utils.DeviceUtils
 
 /**
  * author: PengWuliang
  * date: 2025/8/26
- * desc:
+ * desc: 内层view
  */
 class MatchNotifyView @JvmOverloads constructor(
     context: Context,
@@ -30,7 +31,6 @@ class MatchNotifyView @JvmOverloads constructor(
     defStyleAttr: Int = 0,
 ) : ConstraintLayout(context, attrs, defStyleAttr) {
     private val binding = CallIncomingFloatViewBinding.inflate(LayoutInflater.from(context), this, true)
-    private var onCancelCallback: ICancelViewCallback? = null
 
     init {
         initView()
@@ -39,25 +39,22 @@ class MatchNotifyView @JvmOverloads constructor(
     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()
-                }
+            if(DeviceUtils.isAppRunningForeground(AppUtil.appContext)) {
+                Log.i(TAG_CALL_MATCH_MODE, "acceptBtn requestVideoPermission")
+                PermissionRequest.requestVideoPermission(AppUtil.appContext, object : PermissionCallback() {
+                    override fun onGranted() {
+                        matchManager.accept()
+                    }
+                })
+            } else {
+                bringAppToForeground()
             }
         }
     }
 
-    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
@@ -66,4 +63,18 @@ class MatchNotifyView @JvmOverloads constructor(
             setTextColor(getCompatColor(R.color.color_FFC251FF))
         }
     }
+
+    private fun bringAppToForeground() {
+        try {
+            Log.i(TAG_CALL_MATCH_MODE, "bringAppToForeground")
+            val intent = Intent(context, MainActivity::class.java).apply {
+                addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
+                addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                putExtra(EXTRA_FROM_MATCH_NOTIFICATION, true)
+            }
+            context.startActivity(intent)
+        } catch (e: Exception) {
+            Log.e(TAG_CALL_MATCH_MODE, "Failed to bring app to foreground $e")
+        }
+    }
 }

+ 4 - 5
module/call/src/main/java/com/adealink/weparty/call/viewmodel/CallViewModel.kt

@@ -36,6 +36,7 @@ open class CallViewModel : BaseViewModel(), ICallViewModel, ICallListener {
     val tCallStatusLD = MutableLiveData<TUICallDefine.Status>()
     val switchCallModeAskLD = MutableLiveData<SwitchCallModeAsk>()
     val merchantFreeCallLD: LiveData<Rlt<MerchantFreeCallData>> = MutableLiveData()
+    val matchUserListLD = MutableLiveData<Rlt<List<UserInfo>>>()
 
     init {
         callManager.addListener(this)
@@ -106,22 +107,20 @@ open class CallViewModel : BaseViewModel(), ICallViewModel, ICallListener {
         return liveData
     }
 
-    override fun getMatchUserList(): LiveData<Rlt<List<UserInfo>>> {
-        val liveData = MutableLiveData<Rlt<List<UserInfo>>>()
+    override fun getMatchUserList() {
         viewModelScope.launch {
             when(val rlt = callHttpService.getMatchUserList(CommonRequest())) {
                 is Rlt.Success -> {
                     val res = rlt.data.data?.userList ?: emptyList()
-                    liveData.send(Rlt.Success(res))
+                    matchUserListLD.send(Rlt.Success(res))
                     Log.d(TAG_CALL_MATCH_MODE, "getMatchUserList -> $res")
                 }
                 is Rlt.Failed -> {
-                    liveData.send(Rlt.Failed(rlt.error))
+                    matchUserListLD.send(Rlt.Failed(rlt.error))
                     Log.d(TAG_CALL_MATCH_MODE, "getMatchUserList fail, error: ${rlt.error}")
                 }
             }
         }
-        return liveData
     }
 
     override fun getCallMatchEntrance() {

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

@@ -15,6 +15,7 @@ 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.adealink.weparty.module.call.data.FROM_TUI_CALL
 import com.tencent.cloud.tuikit.engine.call.TUICallDefine
 import com.tencent.cloud.tuikit.engine.call.TUICallObserver
 import com.tencent.cloud.tuikit.engine.common.TUICommonDefine
@@ -439,7 +440,7 @@ class TUICallState {
     fun clear() {
         Log.i(TAG_CALL_FLOW, "clear")
         matchRemoteUser.clear()
-        matchManager.clearData()
+        matchManager.clearData(FROM_TUI_CALL)
 
         //reverse1v1CallRenderView = false
         isShowFloatView.set(false)

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

@@ -148,7 +148,7 @@ object PermissionRequest {
         }
         PermissionUtils.requestPermissions(
             activity,
-            listOf(Manifest.permission.CAMERA),
+            listOf(Manifest.permission.BLUETOOTH_CONNECT),
             getCompatString(R.string.call_permission_bluetooth_reason),
             onGranted = {
                 callback.onGranted()

+ 47 - 14
module/call/src/main/java/com/tencent/qcloud/tuikit/tuicallkit/view/component/videolayout/VideoView.kt

@@ -4,13 +4,15 @@ import android.content.Context
 import android.view.LayoutInflater
 import android.view.MotionEvent
 import androidx.appcompat.widget.AppCompatTextView
+import androidx.constraintlayout.widget.Group
 import com.adealink.frame.aab.util.getCompatString
 import com.adealink.weparty.call.R
+import com.adealink.weparty.call.databinding.CallVideoViewBinding
 import com.adealink.weparty.call.view.CallBgView
 import com.adealink.weparty.commonui.ext.gone
 import com.adealink.weparty.commonui.ext.show
-import com.adealink.weparty.commonui.imageview.AvatarView
 import com.adealink.weparty.module.profile.data.UserInfo
+import com.adealink.weparty.module.profile.util.updateChatAchievementLabel
 import com.tencent.cloud.tuikit.engine.call.TUICallDefine
 import com.tencent.cloud.tuikit.engine.common.TUIVideoView
 import com.tencent.qcloud.tuicore.TUICore
@@ -28,13 +30,16 @@ class VideoView(context: Context) : BaseCallView(context) {
     private var bgView: CallBgView? = null
     private var tuiVideoView: TUIVideoView? = null
 
-    private var ivAvatar: AvatarView? = null
     private var tvStatus: AppCompatTextView? = null
 
     private var viewModel: VideoViewModel? = null
 
     private var isShowFloatWindow: Boolean = false
 
+    private var groupUserInfo: Group? = null
+
+    private val binding by lazy { CallVideoViewBinding.bind(this) }
+
     private val notification = ITUINotification { key, subKey, param ->
         if (key == Constants.EVENT_VIEW_STATE_CHANGED) {
             isShowFloatWindow = subKey == Constants.EVENT_SHOW_FLOAT_VIEW
@@ -103,8 +108,8 @@ class VideoView(context: Context) : BaseCallView(context) {
         LayoutInflater.from(context).inflate(R.layout.call_video_view, this, true)
         bgView = findViewById(R.id.v_bg)
         tuiVideoView = findViewById(R.id.tx_cloud_view)
-        ivAvatar = findViewById(R.id.iv_avatar)
         tvStatus = findViewById(R.id.tv_status)
+        groupUserInfo = findViewById(R.id.group_user_info)
     }
 
     private fun updateView() {
@@ -114,18 +119,18 @@ class VideoView(context: Context) : BaseCallView(context) {
             if (viewModel?.user?.isCaller() == true) {
                 tuiVideoView?.show()
                 bgView?.gone()
-                ivAvatar?.gone()
+                binding.groupUserInfo.gone()
                 tvStatus?.gone()
             } else {
                 tuiVideoView?.gone()
                 bgView?.show()
                 bgView?.setUrl(getAvatarUrl())
                 if (isShowLargeView()) {
-                    ivAvatar?.show()
-                    ivAvatar?.setImageUrl(getAvatarUrl())
+                    groupUserInfo?.show()
+                    refreshUserInfo()
                     tvStatus?.gone()
                 } else {
-                    ivAvatar?.gone()
+                    groupUserInfo?.gone()
                     tvStatus?.show()
                     tvStatus?.text = getCompatString(R.string.call_wait_response)
                 }
@@ -139,11 +144,11 @@ class VideoView(context: Context) : BaseCallView(context) {
             bgView?.show()
             bgView?.setUrl(getAvatarUrl())
             if (isShowLargeView()) {
-                ivAvatar?.show()
-                ivAvatar?.setImageUrl(getAvatarUrl())
+                groupUserInfo?.show()
+                refreshUserInfo()
                 tvStatus?.gone()
             } else {
-                ivAvatar?.gone()
+                groupUserInfo?.gone()
                 tvStatus?.show()
                 tvStatus?.text = getCompatString(R.string.call_video_camera_off)
             }
@@ -154,7 +159,7 @@ class VideoView(context: Context) : BaseCallView(context) {
         if (isVideoAvailable()) {
             tuiVideoView?.show()
             bgView?.gone()
-            ivAvatar?.gone()
+            groupUserInfo?.gone()
             tvStatus?.gone()
             if (viewModel?.user?.id != viewModel?.selfUser?.id) {
                 EngineManager.instance.startRemoteView(viewModel?.user?.id, tuiVideoView, null)
@@ -166,11 +171,11 @@ class VideoView(context: Context) : BaseCallView(context) {
         bgView?.show()
         bgView?.setUrl(getAvatarUrl())
         if (isShowLargeView()) {
-            ivAvatar?.show()
-            ivAvatar?.setImageUrl(getAvatarUrl())
+            groupUserInfo?.show()
+            refreshUserInfo()
             tvStatus?.gone()
         } else {
-            ivAvatar?.gone()
+            groupUserInfo?.gone()
             tvStatus?.show()
             tvStatus?.text = getCompatString(R.string.call_video_camera_off)
         }
@@ -214,4 +219,32 @@ class VideoView(context: Context) : BaseCallView(context) {
     fun getVideoView(): TUIVideoView? {
         return tuiVideoView
     }
+
+    private fun refreshUserInfo() {
+        val userInfo = viewModel?.user?.userInfo?.get()
+        binding.ivAvatar.setImageUrl(userInfo?.url)
+        binding.tvName.text = userInfo?.name
+        binding.vSex.setSex(userInfo?.gender, userInfo?.birthday)
+
+        binding.merchantLabel.setMerchantType(userInfo?.merchantType)
+
+        binding.userCertificationView.setCertificationStatus(userInfo?.commonConfigInfo)
+        binding.merchantLabel.setMerchantType(userInfo?.merchantType)
+
+        val sVipLevel = userInfo?.sVipLevel ?: 0
+        binding.vSVipLevel.updateLevel(sVipLevel)
+        if (sVipLevel > 0) {
+            binding.vipRechargeLabelView.show(false)
+        } else {
+            binding.vipRechargeLabelView.updateVipRechargeLevel(
+                userInfo?.getVipRechargeLevel() ?: 0
+            )
+        }
+
+        updateChatAchievementLabel(
+            userInfo?.commonConfigInfo?.getChatAchievement()?.identityType,
+            binding.ivChatAchievement
+        )
+
+    }
 }

+ 2 - 2
module/call/src/main/java/com/tencent/qcloud/tuikit/tuicallkit/view/root/BaseCallView.kt

@@ -1,10 +1,10 @@
 package com.tencent.qcloud.tuikit.tuicallkit.view.root
 
 import android.content.Context
-import android.widget.RelativeLayout
+import androidx.constraintlayout.widget.ConstraintLayout
 import com.adealink.weparty.call.widget.ICallView
 
-abstract class BaseCallView(context: Context) : RelativeLayout(context), ICallView {
+abstract class BaseCallView(context: Context) : ConstraintLayout(context), ICallView {
     override fun addObserver() {
     }
 

+ 114 - 52
module/call/src/main/res/layout/call_video_view.xml

@@ -1,6 +1,7 @@
 <?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:id="@+id/video_view"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
@@ -16,13 +17,6 @@
         android:layout_height="match_parent"
         android:visibility="gone" />
 
-    <!--    <androidx.constraintlayout.utils.widget.ImageFilterView-->
-    <!--        android:id="@+id/img_head"-->
-    <!--        android:layout_width="match_parent"-->
-    <!--        android:layout_height="match_parent"-->
-    <!--        android:scaleType="fitXY"-->
-    <!--        android:src="@drawable/common_default_avatar_ic" />-->
-
     <androidx.appcompat.widget.AppCompatTextView
         android:id="@+id/tv_status"
         android:layout_width="wrap_content"
@@ -39,6 +33,12 @@
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toTopOf="parent" />
 
+    <androidx.constraintlayout.widget.Group
+        android:id="@+id/group_user_info"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        app:constraint_referenced_ids="iv_avatar, tv_name, user_certification_view, scroll_view"/>
+
     <com.adealink.weparty.commonui.imageview.AvatarView
         android:id="@+id/iv_avatar"
         android:layout_width="80dp"
@@ -49,50 +49,112 @@
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toTopOf="parent" />
 
-    <!--    <androidx.appcompat.widget.AppCompatImageView-->
-    <!--        android:id="@+id/iv_audio_input"-->
-    <!--        android:layout_width="24dp"-->
-    <!--        android:layout_height="24dp"-->
-    <!--        android:layout_alignParentBottom="true"-->
-    <!--        android:layout_marginStart="4dp"-->
-    <!--        android:layout_marginBottom="8dp"-->
-    <!--        android:layout_toEndOf="@+id/tv_name"-->
-    <!--        android:background="@drawable/tuicallkit_ic_audio_input"-->
-    <!--        android:visibility="gone" />-->
-
-    <!--    <androidx.appcompat.widget.AppCompatImageView-->
-    <!--        android:id="@+id/iv_network"-->
-    <!--        android:layout_width="24dp"-->
-    <!--        android:layout_height="24dp"-->
-    <!--        android:layout_alignWithParentIfMissing="true"-->
-    <!--        android:layout_alignParentBottom="true"-->
-    <!--        android:layout_marginEnd="8dp"-->
-    <!--        android:layout_marginBottom="8dp"-->
-    <!--        android:layout_toStartOf="@+id/iv_switch_camera"-->
-    <!--        android:background="@drawable/tuicallkit_ic_network_bad"-->
-    <!--        android:visibility="gone" />-->
-
-    <!--    <androidx.appcompat.widget.AppCompatImageView-->
-    <!--        android:id="@+id/iv_switch_camera"-->
-    <!--        android:layout_width="24dp"-->
-    <!--        android:layout_height="24dp"-->
-    <!--        android:layout_alignWithParentIfMissing="true"-->
-    <!--        android:layout_alignParentBottom="true"-->
-    <!--        android:layout_marginEnd="8dp"-->
-    <!--        android:layout_marginBottom="8dp"-->
-    <!--        android:layout_toStartOf="@+id/iv_blur_background"-->
-    <!--        android:background="@drawable/tuicallkit_ic_switch_camera_group"-->
-    <!--        android:visibility="gone" />-->
-
-    <!--    <androidx.appcompat.widget.AppCompatImageView-->
-    <!--        android:id="@+id/iv_blur_background"-->
-    <!--        android:layout_width="24dp"-->
-    <!--        android:layout_height="24dp"-->
-    <!--        android:layout_alignParentEnd="true"-->
-    <!--        android:layout_alignParentBottom="true"-->
-    <!--        android:layout_marginEnd="8dp"-->
-    <!--        android:layout_marginBottom="8dp"-->
-    <!--        android:background="@drawable/tuicallkit_bg_blur_background"-->
-    <!--        android:visibility="gone" />-->
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/tv_name"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="16dp"
+        android:ellipsize="end"
+        android:gravity="center"
+        android:includeFontPadding="false"
+        android:maxWidth="200dp"
+        android:singleLine="true"
+        android:textColor="@color/color_FFFFFF"
+        android:textSize="16sp"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/iv_avatar"
+        tools:text="UserName UserName UserName UserName" />
+
+    <com.adealink.weparty.module.profile.view.UserCertificationView
+        android:id="@+id/user_certification_view"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        app:layout_constraintBottom_toBottomOf="@id/tv_name"
+        app:layout_constraintStart_toEndOf="@id/tv_name"
+        app:layout_constraintTop_toTopOf="@id/tv_name" />
+
+    <HorizontalScrollView
+        android:id="@+id/scroll_view"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="8dp"
+        android:fadeScrollbars="false"
+        android:fadingEdge="horizontal"
+        android:fadingEdgeLength="12dp"
+        android:scrollbars="none"
+        app:layout_constrainedWidth="true"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/tv_name">
+
+        <androidx.appcompat.widget.LinearLayoutCompat
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:gravity="center_vertical"
+            android:orientation="horizontal">
+
+            <!-- 性别 -->
+            <com.adealink.weparty.module.level.label.UserSexView
+                android:id="@+id/v_sex"
+                android:layout_width="wrap_content"
+                android:layout_height="22dp"
+                tools:layout_width="22dp" />
+
+            <com.adealink.weparty.module.profile.view.MerchantLabelView
+                android:id="@+id/merchant_label"
+                android:layout_width="wrap_content"
+                android:layout_height="22dp"
+                app:layout_constraintStart_toEndOf="@id/v_sex"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintTop_toTopOf="@id/v_sex"
+                app:layout_constraintBottom_toBottomOf="@id/v_sex"
+                android:layout_marginStart="4dp"/>
+
+            <com.adealink.weparty.module.level.label.UserSVipLevelView
+                android:id="@+id/v_s_vip_level"
+                android:layout_width="wrap_content"
+                android:layout_height="22dp"
+                android:layout_marginStart="4dp"
+                android:visibility="gone"
+                tools:visibility="visible" />
+
+            <com.adealink.weparty.module.level.label.VipRechargeLabelView
+                android:id="@+id/vip_recharge_label_view"
+                android:layout_width="wrap_content"
+                android:layout_height="22dp"
+                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"
+                android:layout_height="22dp"
+                android:layout_marginStart="4dp"
+                android:visibility="gone"
+                tools:src="@drawable/profile_chat_achievement_label_star_ic"
+                tools:visibility="visible" />
+
+            <com.adealink.weparty.module.family.view.FamilyTagView
+                android:id="@+id/family_logo_view"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginStart="4dp"
+                android:visibility="gone"
+                tools:visibility="visible" />
+
+            <androidx.appcompat.widget.AppCompatImageView
+                android:id="@+id/iv_high_potential"
+                android:layout_width="39dp"
+                android:layout_height="15dp"
+                android:layout_marginStart="4dp"
+                android:src="@drawable/common_high_potential_ic"
+                android:visibility="gone"
+                tools:visibility="visible" />
+
+        </androidx.appcompat.widget.LinearLayoutCompat>
+
+    </HorizontalScrollView>
 </androidx.constraintlayout.widget.ConstraintLayout>