ソースを参照

feat: 语聊房基础代码

DoggyZhang 3 週間 前
コミット
07c3658890
67 ファイル変更2663 行追加3 行削除
  1. 1 0
      app/build.gradle
  2. 11 0
      app/src/main/java/com/adealink/weparty/debug/DebugActivity.kt
  3. 7 0
      app/src/main/java/com/adealink/weparty/module/room/IRoomService.kt
  4. 10 0
      app/src/main/java/com/adealink/weparty/module/room/RoomAction.kt
  5. 66 0
      app/src/main/java/com/adealink/weparty/module/room/RoomModule.kt
  6. 24 0
      app/src/main/java/com/adealink/weparty/module/room/Router.kt
  7. 75 0
      app/src/main/java/com/adealink/weparty/module/room/base/BaseRoomComp.kt
  8. 23 0
      app/src/main/java/com/adealink/weparty/module/room/constant/Seat.kt
  9. 10 0
      app/src/main/java/com/adealink/weparty/module/room/data/RoomAttrData.kt
  10. 28 0
      app/src/main/java/com/adealink/weparty/module/room/data/RoomFlowData.kt
  11. 8 0
      app/src/main/java/com/adealink/weparty/module/room/data/Tags.kt
  12. 19 0
      app/src/main/java/com/adealink/weparty/module/room/datasource/local/RoomLocalService.kt
  13. 16 0
      app/src/main/java/com/adealink/weparty/module/room/listener/IMicSeatListener.kt
  14. 12 0
      app/src/main/java/com/adealink/weparty/module/room/listener/IRoomBottomListener.kt
  15. 16 0
      app/src/main/java/com/adealink/weparty/module/room/listener/IRoomListener.kt
  16. BIN
      app/src/main/res/drawable-xhdpi/room_default_bg.webp
  17. 32 0
      app/src/main/res/layout/activity_debug.xml
  18. 1 0
      app/src/main/res/values/no_translate_strings.xml
  19. 5 0
      app/src/main/res/values/themes.xml
  20. 1 2
      frame/room/src/main/java/com/adealink/frame/room/supplier/IAppSupplier.kt
  21. 1 0
      module/room/.gitignore
  22. 52 0
      module/room/build.gradle
  23. 24 0
      module/room/src/androidTest/java/com/adealink/weparty/room/ExampleInstrumentedTest.kt
  24. 52 0
      module/room/src/main/AndroidManifest.xml
  25. 25 0
      module/room/src/main/java/com/adealink/weparty/room/BaseRoomFragment.kt
  26. 150 0
      module/room/src/main/java/com/adealink/weparty/room/RoomActivity.kt
  27. 92 0
      module/room/src/main/java/com/adealink/weparty/room/RoomServiceImpl.kt
  28. 12 0
      module/room/src/main/java/com/adealink/weparty/room/constant/Constants.kt
  29. 49 0
      module/room/src/main/java/com/adealink/weparty/room/constant/Tags.kt
  30. 216 0
      module/room/src/main/java/com/adealink/weparty/room/interceptor/EnterRoomUriInterceptor.kt
  31. 13 0
      module/room/src/main/java/com/adealink/weparty/room/listener/IRoomOpListener.kt
  32. 16 0
      module/room/src/main/java/com/adealink/weparty/room/sdk/context/IRoomContext.kt
  33. 106 0
      module/room/src/main/java/com/adealink/weparty/room/sdk/controller/BaseController.kt
  34. 30 0
      module/room/src/main/java/com/adealink/weparty/room/sdk/controller/IController.kt
  35. 23 0
      module/room/src/main/java/com/adealink/weparty/room/sdk/controller/IWPAttrController.kt
  36. 7 0
      module/room/src/main/java/com/adealink/weparty/room/sdk/controller/IWPDeviceController.kt
  37. 16 0
      module/room/src/main/java/com/adealink/weparty/room/sdk/controller/IWPJoinController.kt
  38. 8 0
      module/room/src/main/java/com/adealink/weparty/room/sdk/controller/IWPMemberController.kt
  39. 7 0
      module/room/src/main/java/com/adealink/weparty/room/sdk/controller/IWPSeatController.kt
  40. 158 0
      module/room/src/main/java/com/adealink/weparty/room/sdk/controller/impl/WPAttrController.kt
  41. 225 0
      module/room/src/main/java/com/adealink/weparty/room/sdk/controller/impl/WPDeviceController.kt
  42. 52 0
      module/room/src/main/java/com/adealink/weparty/room/sdk/controller/impl/WPJoinController.kt
  43. 353 0
      module/room/src/main/java/com/adealink/weparty/room/sdk/controller/impl/WPMemberController.kt
  44. 15 0
      module/room/src/main/java/com/adealink/weparty/room/sdk/controller/impl/WPSeatController.kt
  45. 9 0
      module/room/src/main/java/com/adealink/weparty/room/sdk/listener/IWPAttrListener.kt
  46. 13 0
      module/room/src/main/java/com/adealink/weparty/room/sdk/listener/IWPDeviceListener.kt
  47. 27 0
      module/room/src/main/java/com/adealink/weparty/room/sdk/listener/IWPJoinListener.kt
  48. 19 0
      module/room/src/main/java/com/adealink/weparty/room/sdk/listener/IWPMemberListener.kt
  49. 46 0
      module/room/src/main/java/com/adealink/weparty/room/sdk/listener/IWPSeatListener.kt
  50. 39 0
      module/room/src/main/java/com/adealink/weparty/room/sdk/service/IRoomService.kt
  51. 110 0
      module/room/src/main/java/com/adealink/weparty/room/sdk/service/WPRoomService.kt
  52. 19 0
      module/room/src/main/java/com/adealink/weparty/room/sdk/supplier/AppSupplier.kt
  53. 150 0
      module/room/src/main/java/com/adealink/weparty/room/service/KeepForegroundService.kt
  54. 12 0
      module/room/src/main/res/layout/activity_room.xml
  55. 3 0
      module/room/src/main/res/values-in/strings.xml
  56. 3 0
      module/room/src/main/res/values-zh/strings.xml
  57. 4 0
      module/room/src/main/res/values/attrs.xml
  58. 3 0
      module/room/src/main/res/values/colors.xml
  59. 3 0
      module/room/src/main/res/values/dimens.xml
  60. 102 0
      module/room/src/main/res/values/ids.xml
  61. 8 0
      module/room/src/main/res/values/strings.xml
  62. 4 0
      module/room/src/main/res/values/styles.xml
  63. 3 0
      module/room/src/main/res/values/tags.xml
  64. 1 0
      module/room/src/main/resources/META-INF/services/com.adealink.weparty.module.room.IRoomService
  65. 17 0
      module/room/src/test/java/com/adealink/weparty/room/ExampleUnitTest.kt
  66. 0 1
      patch/xcrash/native-dump-trace.patch
  67. 1 0
      settings.gradle

+ 1 - 0
app/build.gradle

@@ -231,6 +231,7 @@ android {
             ':module:image',
             ':module:joinus',
             ':module:call',
+            ':module:room',
     ]
     buildFeatures {
         viewBinding true

+ 11 - 0
app/src/main/java/com/adealink/weparty/debug/DebugActivity.kt

@@ -30,6 +30,7 @@ import com.adealink.weparty.module.account.AccountLocalService
 import com.adealink.weparty.module.account.AccountModule
 import com.adealink.weparty.module.im.IM
 import com.adealink.weparty.module.im.IMModule
+import com.adealink.weparty.module.room.Room
 import com.adealink.weparty.storage.AppPref
 import com.adealink.weparty.util.goLocalLinkPage
 import com.adjust.sdk.Adjust
@@ -196,6 +197,16 @@ class DebugActivity : BaseActivity(), OnReturnValue {
         binding.saveDeviceidBtn.onClick {
             saveDeviceId()
         }
+
+        binding.roomEnterBtn.onClick {
+            val roomId = binding.roomIdEd.text?.trim()?.toString()
+            if (roomId.isNullOrEmpty()) {
+                return@onClick
+            }
+            Router.build(this@DebugActivity, Room.Room.PATH)
+                .putExtra(Room.Room.EXTRA_ENTER_ROOM_ID, roomId)
+                .start()
+        }
     }
 
     private fun saveWebUrlEd() {

+ 7 - 0
app/src/main/java/com/adealink/weparty/module/room/IRoomService.kt

@@ -0,0 +1,7 @@
+package com.adealink.weparty.module.room
+
+import com.adealink.frame.aab.IService
+import com.adealink.frame.media.IMediaOperatorGet
+
+interface IRoomService : IService<IRoomService>, IMediaOperatorGet {
+}

+ 10 - 0
app/src/main/java/com/adealink/weparty/module/room/RoomAction.kt

@@ -0,0 +1,10 @@
+package com.adealink.weparty.module.room
+
+import com.adealink.frame.mvvm.livedata.ExtMutableLiveData
+
+/**
+ * Created by XiaoDongLin.
+ * Date: 2025/7/11
+ */
+
+val openGamePlayPanel = ExtMutableLiveData<Unit>()

+ 66 - 0
app/src/main/java/com/adealink/weparty/module/room/RoomModule.kt

@@ -0,0 +1,66 @@
+package com.adealink.weparty.module.room
+
+import com.adealink.frame.aab.BaseDynamicModule
+import com.adealink.frame.aab.constant.AABModuleNotInitError
+import com.adealink.frame.base.Rlt
+import com.adealink.frame.media.IMediaOperator
+import com.adealink.frame.media.MediaConflictConfig
+import com.adealink.frame.media.MediaInfo
+import com.adealink.weparty.R
+
+object RoomModule : BaseDynamicModule<IRoomService>(IRoomService::class), IRoomService {
+
+    override val featureName: String
+        get() = "room"
+
+    override val moduleNameResId: Int
+        get() = R.string.module_room
+
+    override fun emptyService(): IRoomService {
+        return object : IRoomService {
+            override fun getService(): IRoomService? {
+                return null
+            }
+
+            override fun getMediaOperator(): IMediaOperator {
+                return object : IMediaOperator {
+                    override suspend fun confirmExitConflictMedia(exitMediaInfo: MediaInfo, enterMediaInfo: MediaInfo): Rlt<Any> {
+                        return Rlt.Failed(AABModuleNotInitError())
+                    }
+
+                    override fun getConflictConfig(): MediaConflictConfig {
+                        return MediaConflictConfig(
+                            MediaInfo(
+                                com.adealink.weparty.media.MediaType.UNKNOWN.type,
+                                ""
+                            ), false
+                        )
+                    }
+
+                    override fun getJoinedRoomId(): Long? {
+                        return null
+                    }
+
+                    override suspend fun isMediaIn(): Boolean {
+                        return false
+                    }
+
+                    override suspend fun leaveMedia(): Rlt<Any> {
+                        return Rlt.Failed(AABModuleNotInitError())
+                    }
+
+                    override suspend fun rejoinRoom(): Rlt<Any> {
+                        return Rlt.Failed(AABModuleNotInitError())
+                    }
+
+                }
+            }
+
+        }
+    }
+
+    override fun getMediaOperator(): IMediaOperator {
+        return getService().getMediaOperator()
+    }
+
+}

+ 24 - 0
app/src/main/java/com/adealink/weparty/module/room/Router.kt

@@ -0,0 +1,24 @@
+package com.adealink.weparty.module.room
+
+
+interface Room {
+
+    interface Common {
+
+        companion object {
+        }
+
+    }
+
+    interface Room {
+
+        companion object {
+            const val PATH = "/room"
+
+            const val EXTRA_ENTER_ROOM_INFO = "extra_enter_room_info"
+
+            const val EXTRA_ENTER_ROOM_ID = "room_id"
+        }
+    }
+
+}

+ 75 - 0
app/src/main/java/com/adealink/weparty/module/room/base/BaseRoomComp.kt

@@ -0,0 +1,75 @@
+package com.adealink.weparty.module.room.base
+
+import android.content.Intent
+import androidx.lifecycle.LifecycleOwner
+import com.adealink.frame.base.fastLazy
+import com.adealink.frame.util.removeUiCallbacks
+import com.adealink.frame.util.runOnUiThread
+import com.adealink.weparty.mvvm.view.FragmentViewComponent
+
+/**
+ * 房间基础组件
+ * 1. 延时loadData, observeViewModel()
+ */
+abstract class BaseRoomComp(
+    lifecycleOwner: LifecycleOwner,
+) : FragmentViewComponent(lifecycleOwner) {
+
+    private val immediatelyLoad by fastLazy {
+        Runnable {
+            importantLoad()
+        }
+    }
+
+    private val delayLoad by fastLazy {
+        Runnable {
+            delayLoad()
+        }
+    }
+
+
+    override fun onCreate() {
+        super.onCreate()
+        initViews()
+        observeViewModel()
+        runOnUiThread(immediatelyLoad)
+        runOnUiThread(delayLoad, 500L)
+    }
+
+    /**
+     * initViews 直接操作控件会破坏组件懒加载机制,导致控件懒加载失效
+     * 需要封装[com.adealink.weparty.commonui.layout.IDynamicView]
+     */
+    open fun initViews() {
+    }
+
+    private fun importantLoad() {
+        loadData()
+    }
+
+    open fun loadData() {
+    }
+
+    open fun observeViewModel() {
+    }
+
+    private fun delayLoad() {
+        delayLoadData()
+    }
+
+    open fun delayLoadData() {
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        removeUiCallbacks(immediatelyLoad)
+        removeUiCallbacks(delayLoad)
+    }
+
+    override fun onNewIntent(intent: Intent?) {
+        super.onNewIntent(intent)
+        runOnUiThread(immediatelyLoad)
+        runOnUiThread(delayLoad, 500L)
+    }
+
+}

+ 23 - 0
app/src/main/java/com/adealink/weparty/module/room/constant/Seat.kt

@@ -0,0 +1,23 @@
+package com.adealink.weparty.module.room.constant
+
+import com.adealink.weparty.commonui.ext.dp
+
+//麦位头像最小尺寸
+val SEAT_MIN_AVATAR_SIZE = 45.dp()
+
+//麦位头像最大尺寸
+val SEAT_MAX_AVATAR_SIZE = 80.dp()
+
+//麦位头像框默认尺寸
+private val DEFAULT_AVATAR_SIZE = 55.dp()
+
+//头像相对于屏幕宽度的比率
+const val SEAT_AVATAR_SIZE_RATIO_TO_SCREEN_WIDTH = 55f / 375
+
+const val GAME_MATCHING_SEAT_AVATAR_SIZE_RATIO_TO_SCREEN_WIDTH = 65f / 375
+
+//麦位头像最大尺寸
+val GAME_MATCHING_SEAT_MAX_AVATAR_SIZE = 58.dp()
+
+//麦位最小高度比率
+const val SEAT_MIN_HEIGHT_RATIO = 1.68f

+ 10 - 0
app/src/main/java/com/adealink/weparty/module/room/data/RoomAttrData.kt

@@ -0,0 +1,10 @@
+package com.adealink.weparty.module.room.data
+
+import com.adealink.weparty.commonui.recycleview.diffutil.BaseListItemData
+
+
+interface RoomData : BaseListItemData {
+
+}
+
+

+ 28 - 0
app/src/main/java/com/adealink/weparty/module/room/data/RoomFlowData.kt

@@ -0,0 +1,28 @@
+package com.adealink.weparty.module.room.data
+
+import android.os.Parcelable
+import com.google.gson.annotations.Must
+import com.google.gson.annotations.SerializedName
+import kotlinx.parcelize.Parcelize
+
+
+@Parcelize
+data class RoomInfo(
+    @Must
+    @SerializedName("roomId") val roomId: Long = 0,
+) : Parcelable, RoomData {
+
+    override fun areItemsTheSame(newItem: Any): Boolean {
+        val newData = newItem as? RoomInfo ?: return false
+        return roomId == newData.roomId
+    }
+}
+
+infix fun RoomInfo.contentsTheSame(other: RoomInfo): Boolean =
+    roomId == other.roomId
+
+@Parcelize
+data class EnterRoomInfo(
+    val roomId: Long,
+    var password: String? = null,
+) : Parcelable

+ 8 - 0
app/src/main/java/com/adealink/weparty/module/room/data/Tags.kt

@@ -0,0 +1,8 @@
+package com.adealink.weparty.module.room.data
+
+import com.adealink.frame.room.data.TAG_ROOM
+
+//tag_room_bell_invite
+const val TAG_BELL_INVITED = "${TAG_ROOM}_bell_invite"
+const val TAG_ROOM_SEND_GIFT_NOTICE = "${TAG_ROOM}_send_gift_notice"
+const val TAG_ROOM_MEMBER_INFO_CARD = "${TAG_ROOM}_member_info_card"

+ 19 - 0
app/src/main/java/com/adealink/weparty/module/room/datasource/local/RoomLocalService.kt

@@ -0,0 +1,19 @@
+package com.adealink.weparty.module.room.datasource.local
+
+import android.content.Context
+import com.adealink.frame.storage.sp.TypeDelegationPrefs
+import com.adealink.frame.util.AppUtil
+import com.adealink.weparty.module.profile.ProfileModule
+
+object RoomLocalService : TypeDelegationPrefs(
+    prefs = {
+        AppUtil.appContext.getSharedPreferences("pref_room", Context.MODE_PRIVATE)
+    },
+    userId = {
+        ProfileModule.getMyUid()
+    }
+) {
+
+    var myRoomInfo: String by PrefUserKey("key_my_room_info", "")
+
+}

+ 16 - 0
app/src/main/java/com/adealink/weparty/module/room/listener/IMicSeatListener.kt

@@ -0,0 +1,16 @@
+package com.adealink.weparty.module.room.listener
+
+import android.view.View
+
+
+interface IMicSeatViewListener {
+    fun getSeatViewByUid(uid: Long): View?
+}
+
+interface IMicSeatListener : IMicSeatViewListener {
+
+}
+
+interface IMicSeatPKListener {
+    fun isMVP(uid: Long): Boolean
+}

+ 12 - 0
app/src/main/java/com/adealink/weparty/module/room/listener/IRoomBottomListener.kt

@@ -0,0 +1,12 @@
+package com.adealink.weparty.module.room.listener
+
+import android.view.View
+
+
+interface IRoomOperateItemListener {
+    fun getOperateItemById(itemId: Int): View?
+}
+
+interface IRoomBottomListener : IRoomOperateItemListener {
+
+}

+ 16 - 0
app/src/main/java/com/adealink/weparty/module/room/listener/IRoomListener.kt

@@ -0,0 +1,16 @@
+package com.adealink.weparty.module.room.listener
+
+import com.adealink.frame.room.data.FlowStateInfo
+
+
+interface IRoomListener {
+
+    fun onRoomIn(roomId: Long, flowStateInfo: FlowStateInfo) {}
+
+    fun onRoomLeaved(roomId: Long, flowStateInfo: FlowStateInfo) {}
+
+    fun onChannelIn(roomId: Long) {}
+
+    fun onChannelLeave(roomId: Long) {}
+
+}

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


+ 32 - 0
app/src/main/res/layout/activity_debug.xml

@@ -495,6 +495,38 @@
 
                 </androidx.appcompat.widget.LinearLayoutCompat>
 
+                <View
+                    android:layout_width="match_parent"
+                    android:layout_height="1dp"
+                    android:background="@color/color_FFE1E3E6" />
+
+                <androidx.appcompat.widget.LinearLayoutCompat
+                    android:id="@+id/room_test_cl"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="horizontal">
+
+                    <androidx.appcompat.widget.AppCompatEditText
+                        android:id="@+id/room_id_ed"
+                        android:layout_width="0dp"
+                        android:layout_height="wrap_content"
+                        android:layout_gravity="center_vertical"
+                        android:layout_weight="1"
+                        android:hint="房间ID"
+                        android:textColor="@color/black"
+                        android:textColorHint="@color/color_333333"
+                        android:textSize="14sp" />
+
+                    <androidx.appcompat.widget.AppCompatButton
+                        android:id="@+id/room_enter_btn"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:layout_gravity="center_vertical"
+                        android:gravity="center"
+                        android:text="进入房间" />
+
+                </androidx.appcompat.widget.LinearLayoutCompat>
+
             </androidx.appcompat.widget.LinearLayoutCompat>
 
         </androidx.appcompat.widget.LinearLayoutCompat>

+ 1 - 0
app/src/main/res/values/no_translate_strings.xml

@@ -14,5 +14,6 @@
     <string name="module_image" translatable="false">image</string>
     <string name="module_joinus" translatable="false">activity</string>
     <string name="module_call" translatable="false">call</string>
+    <string name="module_room" translatable="false">room</string>
 
 </resources>

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

@@ -41,4 +41,9 @@
         <item name="android:windowContentOverlay">@null</item>
         <item name="android:windowBackground">@color/white</item>
     </style>
+
+    <style name="RoomTheme" parent="AppTheme">
+        <item name="android:windowBackground">@drawable/room_default_bg</item>
+        <item name="default_dot_style">@style/SmallRedDot</item>
+    </style>
 </resources>

+ 1 - 2
frame/room/src/main/java/com/adealink/frame/room/supplier/IAppSupplier.kt

@@ -8,10 +8,9 @@ interface IAppSupplier {
 
     val mediaService: IMediaRtcService
 
-    val selfUid: Long
+    val selfUid: String
 
     val appContext: Context
 
-    val roomStat: IRoomStat
 
 }

+ 1 - 0
module/room/.gitignore

@@ -0,0 +1 @@
+/build

+ 52 - 0
module/room/build.gradle

@@ -0,0 +1,52 @@
+plugins {
+    id 'com.android.dynamic-feature'
+    id 'org.jetbrains.kotlin.android'
+    id 'org.jetbrains.kotlin.kapt'
+    id 'kotlin-parcelize'
+}
+android {
+    namespace 'com.adealink.weparty.room'
+    compileSdk libs.versions.compileSdk.get().toInteger()
+
+    defaultConfig {
+        minSdk libs.versions.minSdk.get().toInteger()
+        resConfigs "zh", "en", "in"
+
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+        ndk {
+            abiFilters "armeabi-v7a"
+            abiFilters "arm64-v8a"
+        }
+    }
+
+
+
+    buildTypes {
+        release {
+            debuggable false
+        }
+    }
+    viewBinding {
+        enabled = true
+    }
+
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_17
+        targetCompatibility JavaVersion.VERSION_17
+    }
+
+    kotlinOptions {
+        jvmTarget = JavaVersion.VERSION_17.majorVersion
+    }
+}
+
+dependencies {
+    implementation project(":app")
+    //frame
+    kapt libs.frame.router.compiler
+
+    //test
+    testImplementation libs.junit
+    androidTestImplementation libs.androidx.junit
+    androidTestImplementation libs.androidx.espresso.core
+}

+ 24 - 0
module/room/src/androidTest/java/com/adealink/weparty/room/ExampleInstrumentedTest.kt

@@ -0,0 +1,24 @@
+package com.adealink.weparty.room
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+    @Test
+    fun useAppContext() {
+        // Context of the app under test.
+        val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+        assertEquals("com.adealink.weparty.room", appContext.packageName)
+    }
+}

+ 52 - 0
module/room/src/main/AndroidManifest.xml

@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:dist="http://schemas.android.com/apk/distribution"
+    xmlns:tools="http://schemas.android.com/tools"
+    tools:ignore="LockedOrientationActivity">
+
+    <dist:module
+        dist:instant="false"
+        dist:title="@string/module_room">
+        <dist:fusing dist:include="true" />
+        <dist:delivery>
+            <dist:install-time>
+                <dist:removable dist:value="true" />
+            </dist:install-time>
+        </dist:delivery>
+    </dist:module>
+
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.RECORD_AUDIO" />
+    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
+    <uses-permission android:name="android.permission.VIBRATE" />
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
+    <uses-permission
+        android:name="android.permission.WRITE_EXTERNAL_STORAGE"
+        tools:ignore="ScopedStorage" />
+
+    <application android:requestLegacyExternalStorage="true">
+
+        <activity
+            android:name=".RoomActivity"
+            android:configChanges="orientation|screenSize"
+            android:exported="true"
+            android:launchMode="singleTask"
+            android:screenOrientation="portrait"
+            android:theme="@style/RoomTheme"
+            android:windowSoftInputMode="stateAlwaysHidden|adjustNothing" />
+
+        <activity
+            android:name=".liveover.LiveOverActivity"
+            android:screenOrientation="portrait"
+            android:theme="@style/AppTheme" />
+
+        <service
+            android:name=".service.KeepForegroundService"
+            android:enabled="true"
+            android:exported="false"
+            android:foregroundServiceType="microphone"
+            android:stopWithTask="true" />
+
+    </application>
+
+</manifest>

+ 25 - 0
module/room/src/main/java/com/adealink/weparty/room/BaseRoomFragment.kt

@@ -0,0 +1,25 @@
+package com.adealink.weparty.room
+
+import android.content.Intent
+import androidx.annotation.LayoutRes
+import com.adealink.frame.room.data.LeaveRoomReason
+import com.adealink.weparty.commonui.BaseFragment
+import com.adealink.weparty.room.listener.IRoomCloseCallback
+import com.adealink.weparty.room.listener.IRoomOpListener
+
+abstract class BaseRoomFragment(@LayoutRes contentLayoutId: Int) :
+    BaseFragment(contentLayoutId), IRoomCloseCallback {
+
+    /**
+     * 可能fragment未add就会调用,需要判断当前Fragment是否valid
+     */
+    abstract fun handleNewIntent(intent: Intent?)
+
+    fun exitRoom(reason: LeaveRoomReason, showCloseTip: Boolean = true) {
+        if (showCloseTip) {
+            (activity as? IRoomOpListener)?.showRoomCloseTipDialog(reason)
+        } else {
+            (activity as? IRoomOpListener)?.exitRoom(reason)
+        }
+    }
+}

+ 150 - 0
module/room/src/main/java/com/adealink/weparty/room/RoomActivity.kt

@@ -0,0 +1,150 @@
+package com.adealink.weparty.room
+
+import android.content.Intent
+import android.view.KeyEvent
+import com.adealink.frame.log.Log
+import com.adealink.frame.mvvm.view.viewBinding
+import com.adealink.frame.room.data.LeaveRoomReason
+import com.adealink.frame.router.Router
+import com.adealink.frame.router.annotation.RouterUri
+import com.adealink.frame.share.shareManager
+import com.adealink.weparty.commonui.BaseActivity
+import com.adealink.weparty.module.room.Room
+import com.adealink.weparty.room.constant.logRoomTime
+import com.adealink.weparty.room.databinding.ActivityRoomBinding
+import com.adealink.weparty.room.interceptor.EnterRoomUriInterceptor
+import com.adealink.weparty.room.listener.IRoomOpListener
+import java.util.LinkedList
+
+@RouterUri(
+    path = [Room.Room.PATH],
+    interceptors = [EnterRoomUriInterceptor::class],
+    desc = "语音房间"
+)
+class RoomActivity : BaseActivity(), IRoomOpListener {
+
+    companion object {
+        private const val TAG = "RoomActivity"
+
+        private const val CHAT_FRAGMENT = "room_fragment"
+
+        private const val REJOIN_TIP_DIALOG = "RejoinTipDialog"
+    }
+
+    private val binding by viewBinding(ActivityRoomBinding::inflate)
+
+    override val forceFitNavigationBar: Boolean
+        get() = true
+
+    //默认聊天房间
+    private var roomFragment: BaseRoomFragment? = null
+
+    // 权限申请队列, RxPermissions 申请权限时, 必须等前面那个完成才能申请下一个, 不然连续调用后面的会无效
+    private val requestPermissionPending = LinkedList<(onFinished: () -> Unit) -> Unit>()
+    private var requestingPermission = false
+
+    override fun onBeforeCreate() {
+        super.onBeforeCreate()
+        Router.bind(this)
+    }
+
+    override fun initViews() {
+        logRoomTime("RoomActivity initViews")
+        setContentView(binding.root)
+        setNavigationBarColor(com.adealink.weparty.R.color.black)
+    }
+
+    override fun handleNewIntent(intent: Intent?) {
+        super.handleNewIntent(intent)
+        roomFragment?.handleNewIntent(intent)
+    }
+
+    override fun onStart() {
+        super.onStart()
+        //检查当前房间信息是否丢失
+//        val roomId = roomService.joinController.getJoinedRoomId()
+//        if (roomId == null || roomId == 0L) {
+//            Log.i(TAG_ROOM, "RoomActivity onStart, roomId is 0, finish()")
+//            //当前房间已经销毁,finish
+//            finish()
+//        }
+    }
+
+
+    internal fun addRequestPermissionPending(block: (onFinished: () -> Unit) -> Unit) {
+        requestPermissionPending.add(block)
+        requestNextPermission()
+    }
+
+    private fun requestNextPermission() {
+        if (requestingPermission) {
+            return
+        }
+        val poll = requestPermissionPending.poll() ?: return
+        Log.d(TAG, "requestNextPermission: ")
+        requestingPermission = true
+        poll.invoke {
+            requestingPermission = false
+            requestNextPermission()
+        }
+    }
+
+    override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
+        if(isFinishing || isDestroyed) return true
+        return if (event?.keyCode == KeyEvent.KEYCODE_BACK) {
+            showRoomCloseTipDialog()
+            true
+        } else {
+            super.onKeyDown(keyCode, event)
+        }
+    }
+
+    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+        super.onActivityResult(requestCode, resultCode, data)
+        shareManager.onActivityResult(requestCode, resultCode, data)
+    }
+
+    override fun showRoomCloseTipDialog(reason: LeaveRoomReason) {
+//        RoomCloseTipDialog.newInstance(
+//            object : ICloseRoomActionListener {
+//                override fun onRoomMinimizeClick() {
+//                    roomFragment?.onRoomMinimize {
+//                        if (it) {
+//                            doRoomMinimize()
+//                        }
+//                    } ?: doRoomMinimize()
+//                }
+//
+//                private fun doRoomMinimize() {
+//                    RoomBaseStatEvent.reportBtnClick(
+//                        RoomBaseStatEvent.Btn.CLOSE_ROOM,
+//                        RoomBaseStatEvent.Result.OPEN
+//                    )
+//                    finish()
+//                }
+//
+//                override fun onRoomCloseClick() {
+//                    if (roomService.seatController.showCancelOnMicApplyDialog(null) {
+//                            handleExitRoom(LeaveRoomReason.INITIATIVE)
+//                        }) {
+//                        return
+//                    }
+//                    roomFragment?.onRoomClose {
+//                        if (it) {
+//                            exitRoom(reason)
+//                        }
+//                    } ?: exitRoom(reason)
+//                }
+//            }
+//        ).show(supportFragmentManager)
+    }
+
+    override fun exitRoom(reason: LeaveRoomReason) {
+        handleExitRoom(reason)
+    }
+
+    private fun handleExitRoom(reason: LeaveRoomReason) {
+        finish()
+    }
+
+}

+ 92 - 0
module/room/src/main/java/com/adealink/weparty/room/RoomServiceImpl.kt

@@ -0,0 +1,92 @@
+package com.adealink.weparty.room
+
+import com.adealink.frame.aab.util.getCompatString
+import com.adealink.frame.base.Rlt
+import com.adealink.frame.data.collections.ConcurrentList
+import com.adealink.frame.media.IMediaOperator
+import com.adealink.frame.media.MediaConflictConfig
+import com.adealink.frame.media.MediaInfo
+import com.adealink.frame.room.data.ChannelState
+import com.adealink.frame.room.data.FlowStateInfo
+import com.adealink.frame.room.data.RoomState
+import com.adealink.frame.spi.RegisterService
+import com.adealink.weparty.media.MediaType
+import com.adealink.weparty.module.room.IRoomService
+import com.adealink.weparty.module.room.listener.IRoomListener
+
+@RegisterService(IRoomService::class)
+class RoomServiceImpl : IRoomService   {
+
+    private val listeners = ConcurrentList<IRoomListener>()
+
+
+    override fun getService(): IRoomService {
+        return this
+    }
+
+    override fun getMediaOperator(): IMediaOperator {
+        return object : IMediaOperator {
+
+            override suspend fun isMediaIn(): Boolean {
+                // TODO: zhangfei
+                return false
+//                return roomService.joinController.isRoomJoiningOrJoined()
+            }
+
+            override suspend fun leaveMedia(): Rlt<Any> {
+//                roomService.joinController.tryLeaveRoom(LeaveRoomReason.ENTER_OTHER_MEDIA, true)
+                return Rlt.Success(Any())
+            }
+
+            override suspend fun confirmExitConflictMedia(exitMediaInfo: MediaInfo, enterMediaInfo: MediaInfo): Rlt<Any> {
+                return Rlt.Success(Unit)
+//                val currentActivity =
+//                    AppUtil.currentActivity as? BaseActivity ?: return Rlt.Success(Any())
+//                return withContext(Dispatcher.UI) {
+//                    suspendCancellableCoroutine { continuation ->
+//                        var confirm = false
+//                        RoomMediaConflictDialog.show(
+//                            exitMediaInfo,
+//                            enterMediaInfo,
+//                            currentActivity.supportFragmentManager,
+//                            onPositive = {
+//                                confirm = true
+//                            },
+//                            onNegative = null,
+//                            onDismiss = {
+//                                if (continuation.isActive) {
+//                                    if (confirm) {
+//                                        continuation.resume(Rlt.Success(Any()), null)
+//                                    } else {
+//                                        continuation.resume(Rlt.Failed(IError()), null)
+//                                    }
+//                                }
+//                            }
+//                        )
+//                    }
+//                }
+            }
+
+            override fun getConflictConfig(): MediaConflictConfig {
+                return MediaConflictConfig(
+                    MediaInfo(
+                        MediaType.CHAT_ROOM.type,
+                        getCompatString(R.string.room_chat)
+                    ), false
+                )
+            }
+
+            override fun getJoinedRoomId(): Long? {
+                return null
+//                return roomService.joinController.getJoinedRoomId()
+            }
+
+            override suspend fun rejoinRoom(): Rlt<Any> {
+                return Rlt.Success(Unit)
+//                return roomService.joinController.rejoinRoom()
+            }
+
+        }
+    }
+
+}

+ 12 - 0
module/room/src/main/java/com/adealink/weparty/room/constant/Constants.kt

@@ -0,0 +1,12 @@
+package com.adealink.weparty.room.constant
+
+const val ROOM_COVER_IMAGE_MAX_WIDTH = 360
+const val ROOM_COVER_IMAGE_MAX_HEIGHT = 360
+const val ROOM_COVER_IMAGE_MAX_SIZE_KB = 100
+const val ROOM_COVER_IMAGE_MIN_QUALITY = 50
+
+const val KEY_ROOM_TYPE = "key_room_type"
+const val KEY_MIC_MODE_INFO = "key_mic_mode_info"
+const val KEY_MIC_SEAT_INFO = "key_mic_seat_info"
+
+const val ROOM_MIC_MODE_SEAT_20= 21

+ 49 - 0
module/room/src/main/java/com/adealink/weparty/room/constant/Tags.kt

@@ -0,0 +1,49 @@
+package com.adealink.weparty.room.constant
+
+import android.os.SystemClock
+import com.adealink.frame.base.AppBase
+import com.adealink.frame.log.Log
+import com.adealink.frame.room.data.TAG_ROOM
+import com.adealink.weparty.commonui.toast.util.showToast
+
+const val TAG_ROOM_CREATE = "${TAG_ROOM}_create"
+const val TAG_ROOM_ENTER_ROOM = "${TAG_ROOM}_enter_room"
+const val TAG_ROOM_THEME = "${TAG_ROOM}_theme"
+
+const val TAG_ROOM_TIME = "tag_room_time"
+
+var enterRoomTime: Long = 0
+
+var enterRoomJoinReqTime: Long = 0 //发起进房请求的时间
+var enterRoomJoinResTime: Long = 0
+
+var enterRoomUILoadTime: Long = 0 //房间UI加载时间(路由时间 -> RoomActivity.onCreate)
+
+fun logRoomTime(msg: String) {
+    Log.d(TAG_ROOM_TIME, "$msg, time: ${SystemClock.elapsedRealtime() - enterRoomTime}")
+}
+
+fun markEnterRoomTime(time: Long) {
+    enterRoomTime = time
+    enterRoomJoinReqTime = 0
+    enterRoomJoinResTime = 0
+    enterRoomUILoadTime = 0
+}
+
+fun toastEnterRoomTime() {
+    if (AppBase.isRelease) {
+        return
+    }
+    val nowTs = SystemClock.elapsedRealtime()
+    val toastContent = StringBuilder()
+    toastContent.append("进房总耗时:").append(nowTs - enterRoomTime).append("\n")
+
+    val netTime = enterRoomJoinResTime - enterRoomJoinReqTime
+    if (netTime > 0) {
+        toastContent.append("网络耗时:").append(netTime).append("\n")
+    }
+
+    toastContent.append("UI加载时间:").append(nowTs - enterRoomUILoadTime)
+
+    showToast(toastContent)
+}

+ 216 - 0
module/room/src/main/java/com/adealink/weparty/room/interceptor/EnterRoomUriInterceptor.kt

@@ -0,0 +1,216 @@
+package com.adealink.weparty.room.interceptor
+
+import android.os.SystemClock
+import androidx.fragment.app.FragmentActivity
+import com.adealink.frame.aab.util.getCompatString
+import com.adealink.frame.base.Rlt
+import com.adealink.frame.coroutine.dispatcher.Dispatcher
+import com.adealink.frame.log.Log
+import com.adealink.frame.media.MediaInfo
+import com.adealink.frame.room.data.JoinRoomReq
+import com.adealink.frame.router.RouterDeepLinkActivity
+import com.adealink.frame.router.interceptor.UriInterceptor
+import com.adealink.frame.router.request.UriRequest
+import com.adealink.frame.statistics.CommonEventValue
+import com.adealink.frame.util.AppUtil
+import com.adealink.frame.util.isActivityDestroy
+import com.adealink.weparty.App
+import com.adealink.weparty.commonui.tip.showFailedTip
+import com.adealink.weparty.commonui.widget.ProgressDialog
+import com.adealink.weparty.error.CommonFunctionBlockError
+import com.adealink.weparty.media.MediaType
+import com.adealink.weparty.module.account.AccountModule
+import com.adealink.weparty.module.room.Room
+import com.adealink.weparty.module.room.Room.Room.Companion.EXTRA_ENTER_ROOM_INFO
+import com.adealink.weparty.module.room.data.EnterRoomInfo
+import com.adealink.weparty.room.R
+import com.adealink.weparty.room.constant.TAG_ROOM_ENTER_ROOM
+import com.adealink.weparty.room.constant.enterRoomJoinReqTime
+import com.adealink.weparty.room.constant.enterRoomJoinResTime
+import com.adealink.weparty.room.constant.enterRoomUILoadTime
+import com.adealink.weparty.room.constant.markEnterRoomTime
+import com.adealink.weparty.util.parcelableExtra
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+class EnterRoomUriInterceptor : UriInterceptor {
+
+    override fun intercept(chain: UriInterceptor.Chain) {
+        val request = chain.request()
+        val activity = request.ctx as? FragmentActivity
+        if (activity == null || isActivityDestroy(activity)) {
+            Log.e(TAG_ROOM_ENTER_ROOM, "EnterRoomUriInterceptor, activity is null")
+            chain.abort()
+            return
+        }
+
+        CoroutineScope(Dispatcher.UI).launch {
+            enterRoom(activity, chain)
+        }
+    }
+
+    private suspend fun enterRoom(activity: FragmentActivity, chain: UriInterceptor.Chain) {
+        markEnterRoomTime(SystemClock.elapsedRealtime())
+        val request = chain.request()
+        val intent = request.intent
+        //获取activity finish参数
+        val finishActivity = intent.getBooleanExtra("extra_success_finish_activity", false)
+        val finishDeeplinkActivity = intent.getBooleanExtra("extra_finish_deeplink_activity", false)
+                && activity is RouterDeepLinkActivity
+
+        enterRoomInner(activity, object : UriInterceptor.Chain {
+            override fun request(): UriRequest {
+                return chain.request()
+            }
+
+            override fun proceed(innerRequest: UriRequest) {
+                if (finishActivity && isActivityDestroy(activity).not()) {
+                    activity.finish()
+                }
+                if (finishDeeplinkActivity && isActivityDestroy(activity).not()) {
+                    activity.finish()
+                }
+                val activityDestroy = isActivityDestroy(activity)
+                if (activityDestroy) {
+                    request.ctx = AppUtil.currentActivity
+                }
+                enterRoomUILoadTime = SystemClock.elapsedRealtime()
+                chain.proceed(request)
+            }
+
+            override fun abort() {
+                if (finishActivity && isActivityDestroy(activity).not()) {
+                    activity.finish()
+                }
+                if (finishDeeplinkActivity && isActivityDestroy(activity).not()) {
+                    activity.finish()
+                }
+                chain.abort()
+            }
+        })
+    }
+
+    private suspend fun enterRoomInner(activity: FragmentActivity, chain: UriInterceptor.Chain) {
+        val request = chain.request()
+        val intent = request.intent
+
+
+        var enterRoomInfo: EnterRoomInfo?
+        //1. 从跳转参数中获取进房信息
+        enterRoomInfo = intent.parcelableExtra(EXTRA_ENTER_ROOM_INFO, EnterRoomInfo::class.java)
+
+        Log.d(
+            TAG_ROOM_ENTER_ROOM,
+            "EnterRoomUriInterceptor, getEnterRoomInfo from enterRoomInfo, $enterRoomInfo"
+        )
+
+        //2. 从deepLink进房参数中获取房间ID,合成进房信息
+        if (enterRoomInfo == null) {
+            //deepLink进房
+            val roomId = intent.getStringExtra(Room.Room.EXTRA_ENTER_ROOM_ID)?.toLongOrNull()
+            if (roomId != null && roomId > 0L) {
+                enterRoomInfo = EnterRoomInfo(roomId = roomId)
+            }
+            Log.d(
+                TAG_ROOM_ENTER_ROOM,
+                "EnterRoomUriInterceptor, getEnterRoomInfo from roomId, $enterRoomInfo"
+            )
+        }
+
+        //房间信息无效,跳转房间失败
+        if (enterRoomInfo == null || enterRoomInfo.roomId == 0L) {
+            chain.abort()
+            Log.e(
+                TAG_ROOM_ENTER_ROOM,
+                "EnterRoomUriInterceptor, enterRoomInfo is null / roomId is 0, abort()"
+            )
+            return
+        }
+
+        //跳转参数补充房间信息
+        intent.putExtra(EXTRA_ENTER_ROOM_INFO, enterRoomInfo)
+
+        //当前已经是进房状态,直接进入房间
+        val joinedRoomId = roomService.joinController.getJoinedRoomId()
+        if (joinedRoomId == enterRoomInfo.roomId) {
+            Log.d(TAG_ROOM_ENTER_ROOM, "EnterRoomUriInterceptor, already joined room, proceed()")
+            chain.proceed(request)
+            return
+        }
+
+        //房间媒体冲突处理失败,进房失败
+        val conflictRlt = App.instance.mediaManager.conflictHandle(
+            MediaInfo(MediaType.CHAT_ROOM.type, getCompatString(R.string.room_chat))
+        )
+        if (conflictRlt is Rlt.Failed) {
+            Log.e(TAG_ROOM_ENTER_ROOM, "EnterRoomUriInterceptor, media conflict fail, abort()")
+            chain.abort()
+            return
+        }
+
+        if (joinedRoomId != null && joinedRoomId > 0) {
+            if (roomService.seatController.showCancelOnMicApplyDialog(enterRoomInfo)) {
+                return
+            }
+        }
+
+        enterRoomJoinReqTime = SystemClock.elapsedRealtime()
+        val progressDialog = ProgressDialog.Builder()
+            .text(getCompatString(R.string.room_entering_room))
+            .build()
+        progressDialog.show(activity.supportFragmentManager, ProgressDialog.TAG)
+        EnterEffectModule.subscribeNotify()
+        onlineAudienceManager.subscribeNotify()
+        val startTime = SystemClock.elapsedRealtime()
+        val result = roomService.joinController.joinRoom(
+            JoinRoomReq(
+                enterRoomInfo.roomId,
+                enterRoomInfo.from,
+                enterRoomInfo.password,
+                invisibleJoin = RoomLocalService.isOpenInvisible,
+            )
+        )
+        progressDialog.dismiss()
+        enterRoomJoinResTime = SystemClock.elapsedRealtime()
+        when (result) {
+            is Rlt.Success -> {
+                Log.i(TAG_ROOM_ENTER_ROOM, "EnterRoomUriInterceptor, join room success, proceed()")
+                //记录最近进房信息
+                recentRoomListManager.enterRoom(enterRoomInfo.roomId)
+                GiftModule.getGifts(true)
+                GiftModule.getCustomGiftConfig()
+                GiftModule.pullTimeLimitGiftConfig(enterRoomInfo.roomId)
+                EnterRoomStatEvent().send()
+                chain.proceed(request)
+            }
+
+            is Rlt.Failed -> {
+                Log.e(
+                    TAG_ROOM_ENTER_ROOM,
+                    "EnterRoomUriInterceptor, join room fail(${result.error.serverCode}), abort()"
+                )
+                if (result.error is RoomJoinPwdError || result.error is RoomJoinNeedPwdError) {
+                    showInputPwdDialog(activity, enterRoomInfo, intent.extras)
+                } else {
+                    showFailedTip(activity, result)
+                    if (result.error is CommonFunctionBlockError) {
+                        val res = result.error.data as? com.adealink.weparty.room.data.JoinRoomRes
+                        AccountModule.onFunctionBlock(res?.reason, res?.expire)
+                    }
+                    chain.abort()
+                }
+            }
+        }
+        createRoomQualityStatEvent(RoomQualityStatEvent.Action.JOIN_ROOM)
+            .apply {
+                this.result to (if (result is Rlt.Success) CommonEventValue.Result.SUCCESS else CommonEventValue.Result.FAILED)
+                if (result is Rlt.Failed) {
+                    error to result.error.getStatError()
+                }
+                source to enterRoomInfo.from
+                duration to (SystemClock.elapsedRealtime() - startTime)
+            }.send()
+    }
+
+
+}

+ 13 - 0
module/room/src/main/java/com/adealink/weparty/room/listener/IRoomOpListener.kt

@@ -0,0 +1,13 @@
+package com.adealink.weparty.room.listener
+
+import com.adealink.frame.room.data.LeaveRoomReason
+
+interface IRoomOpListener {
+    fun exitRoom(reason: LeaveRoomReason)
+    fun showRoomCloseTipDialog(reason: LeaveRoomReason = LeaveRoomReason.INITIATIVE)
+}
+
+interface IRoomCloseCallback {
+    fun onRoomMinimize(minimize: (Boolean) -> Unit)
+    fun onRoomClose(close: (Boolean) -> Unit)
+}

+ 16 - 0
module/room/src/main/java/com/adealink/weparty/room/sdk/context/IRoomContext.kt

@@ -0,0 +1,16 @@
+package com.adealink.weparty.room.sdk.context
+
+import com.adealink.frame.media.IMediaRtcService
+import com.adealink.frame.room.supplier.IAppSupplier
+import com.adealink.weparty.room.sdk.service.IRoomService
+
+
+interface IRoomContext {
+
+    val mediaService: IMediaRtcService
+
+    val roomService: IRoomService
+
+    val appSupplier: IAppSupplier
+
+}

+ 106 - 0
module/room/src/main/java/com/adealink/weparty/room/sdk/controller/BaseController.kt

@@ -0,0 +1,106 @@
+package com.adealink.weparty.room.sdk.controller
+
+import android.os.Handler
+import androidx.annotation.CallSuper
+import com.adealink.frame.coroutine.dispatcher.Dispatcher.remove
+import com.adealink.frame.coroutine.dispatcher.Dispatcher.submit
+import com.adealink.frame.data.collections.ConcurrentList
+import com.adealink.frame.room.data.ChannelState
+import com.adealink.frame.room.data.FlowStateInfo
+import com.adealink.frame.room.data.RoomState
+import com.adealink.frame.room.listener.IListener
+import com.adealink.weparty.room.sdk.context.IRoomContext
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.android.asCoroutineDispatcher
+import java.util.concurrent.atomic.AtomicBoolean
+
+abstract class BaseController<L : IListener>(
+    open val ctx: IRoomContext,
+    val serialHandler: Handler
+) : IController<L>, CoroutineScope {
+    override val coroutineContext = SupervisorJob() + serialHandler.asCoroutineDispatcher()
+    var dispatch: AtomicBoolean = AtomicBoolean(false)
+
+    val listeners = ConcurrentList<L>()
+
+    override fun addListener(l: L) {
+        listeners.add(l)
+    }
+
+    override fun removeListener(l: L) {
+        listeners.remove(l)
+    }
+
+    override fun clearListener() {
+        listeners.clear()
+    }
+
+    @CallSuper
+    open fun onCreate() {
+    }
+
+    @CallSuper
+    open fun onResume() {
+        dispatch = AtomicBoolean(true)
+    }
+
+    @CallSuper
+    open fun onPause() {
+        dispatch = AtomicBoolean(false)
+    }
+
+    @CallSuper
+    open fun onDestroy() {
+    }
+
+    @CallSuper
+    open fun onClear() {
+    }
+
+    @CallSuper
+    override fun onRoomStateChanged(
+        fromState: RoomState,
+        toState: RoomState,
+        flowStateInfo: FlowStateInfo,
+    ) {
+        when (toState) {
+            RoomState.ROOM_IN -> {
+                onRoomIn(flowStateInfo)
+            }
+            RoomState.ROOM_LEAVE -> {
+                onRoomLeaved(flowStateInfo)
+            }
+            else -> {}
+        }
+    }
+
+    @CallSuper
+    override fun onChannelStateChanged(fromState: ChannelState, toState: ChannelState, flowStateInfo: FlowStateInfo) {
+        when (toState) {
+            ChannelState.CHANNEL_IN -> {
+                onChannelIn(flowStateInfo)
+            }
+            ChannelState.CHANNEL_LEAVE -> {
+                onChannelLeave(flowStateInfo)
+            }
+        }
+    }
+
+    open fun onRoomIn(flowStateInfo: FlowStateInfo) {}
+
+    open fun onChannelIn(flowStateInfo: FlowStateInfo) {}
+
+    open fun onChannelLeave(flowStateInfo: FlowStateInfo) {}
+
+    open fun onRoomLeaved(flowStateInfo: FlowStateInfo) {}
+
+    fun runOnSerialHandler(runnable: Runnable, delay: Long = 0L) {
+        serialHandler.submit(runnable, delay)
+    }
+
+    fun removeFromSerialHandler(runnable: Runnable) {
+        serialHandler.remove(runnable)
+    }
+
+}

+ 30 - 0
module/room/src/main/java/com/adealink/weparty/room/sdk/controller/IController.kt

@@ -0,0 +1,30 @@
+package com.adealink.weparty.room.sdk.controller
+
+import com.adealink.frame.room.data.ChannelState
+import com.adealink.frame.room.data.FlowStateInfo
+import com.adealink.frame.room.data.RoomState
+import com.adealink.frame.room.listener.IListener
+
+interface IController<L : IListener> {
+
+    fun addListener(l: L)
+
+    fun removeListener(l: L)
+
+    fun clearListener()
+
+    fun onRoomStateChanged(
+        fromState: RoomState,
+        toState: RoomState,
+        flowStateInfo: FlowStateInfo
+    ) {
+    }
+
+    fun onChannelStateChanged(
+        fromState: ChannelState,
+        toState: ChannelState,
+        flowStateInfo: FlowStateInfo
+    ) {
+    }
+
+}

+ 23 - 0
module/room/src/main/java/com/adealink/weparty/room/sdk/controller/IWPAttrController.kt

@@ -0,0 +1,23 @@
+package com.adealink.weparty.room.sdk.controller
+
+import com.adealink.frame.base.Rlt
+import com.adealink.weparty.module.room.data.RoomInfo
+import com.adealink.weparty.room.sdk.listener.IWPAttrListener
+
+interface IWPAttrController<L : IWPAttrListener> : IController<L> {
+
+    suspend fun getRoomInfo(roomId: Long): Rlt<RoomInfo>
+
+    suspend fun getMyRoomInfo(forceNet: Boolean): Rlt<RoomInfo>
+
+    fun getNecessaryRoomConfig(roomId: Long)
+
+    suspend fun getRoomInfo(
+        roomIds: List<Long>,
+        from: Long? = null,
+        needRoomOnlineInfo: Boolean = false,
+        getRoomOwnerFamily: Boolean = false
+    ): Rlt<List<RoomInfo>>
+
+
+}

+ 7 - 0
module/room/src/main/java/com/adealink/weparty/room/sdk/controller/IWPDeviceController.kt

@@ -0,0 +1,7 @@
+package com.adealink.weparty.room.sdk.controller
+
+import com.adealink.weparty.room.sdk.listener.IWPDeviceListener
+
+interface IWPDeviceController<L : IWPDeviceListener> : IController<L> {
+
+}

+ 16 - 0
module/room/src/main/java/com/adealink/weparty/room/sdk/controller/IWPJoinController.kt

@@ -0,0 +1,16 @@
+package com.adealink.weparty.room.sdk.controller
+
+import com.adealink.frame.base.IError
+import com.adealink.weparty.room.sdk.listener.IWPJoinListener
+
+interface IWPJoinController<L : IWPJoinListener> : IController<L> {
+
+}
+
+interface JoinChannelCallback {
+
+    fun onSuccess(channel: String)
+
+    fun onFailed(channel: String, error: IError)
+
+}

+ 8 - 0
module/room/src/main/java/com/adealink/weparty/room/sdk/controller/IWPMemberController.kt

@@ -0,0 +1,8 @@
+package com.adealink.weparty.room.sdk.controller
+
+import com.adealink.weparty.room.sdk.listener.IWPMemberListener
+
+interface IWPMemberController<L : IWPMemberListener> : IController<L> {
+
+
+}

+ 7 - 0
module/room/src/main/java/com/adealink/weparty/room/sdk/controller/IWPSeatController.kt

@@ -0,0 +1,7 @@
+package com.adealink.weparty.room.sdk.controller
+
+import com.adealink.weparty.room.sdk.listener.IWPSeatListener
+
+interface IWPSeatController<L : IWPSeatListener> : IController<L> {
+
+}

+ 158 - 0
module/room/src/main/java/com/adealink/weparty/room/sdk/controller/impl/WPAttrController.kt

@@ -0,0 +1,158 @@
+package com.adealink.weparty.room.sdk.controller.impl
+
+import android.os.Handler
+import com.adealink.frame.base.CommonDataNullError
+import com.adealink.frame.base.Rlt
+import com.adealink.frame.data.json.froJsonErrorNull
+import com.adealink.frame.data.json.toJsonErrorNull
+import com.adealink.frame.log.Log
+import com.adealink.frame.room.data.FlowStateInfo
+import com.adealink.frame.room.data.RoomInfoSizeError
+import com.adealink.frame.room.data.TAG_ROOM_ATTR
+import com.adealink.weparty.module.room.data.RoomInfo
+import com.adealink.weparty.module.room.datasource.local.RoomLocalService
+import com.adealink.weparty.room.sdk.context.IRoomContext
+import com.adealink.weparty.room.sdk.controller.BaseController
+import com.adealink.weparty.room.sdk.controller.IWPAttrController
+import com.adealink.weparty.room.sdk.listener.IWPAttrListener
+import com.adealink.weparty.room.sdk.listener.IWPSeatListener
+import com.adealink.weparty.room.sdk.service.roomService
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+class WPAttrController(override val ctx: IRoomContext, serialHandler: Handler) :
+    BaseController<IWPAttrListener>(ctx, serialHandler), IWPAttrController<IWPAttrListener>,
+    IWPSeatListener {
+
+    private var myRoomInfo: RoomInfo? = null
+    private suspend fun updateMyRoomInfo(roomInfo: RoomInfo?) {
+        withContext(this.coroutineContext) {
+            myRoomInfo = roomInfo
+        }
+    }
+
+    override fun onRoomIn(flowStateInfo: FlowStateInfo) {
+        roomService.seatController.addListener(this)
+    }
+
+    override fun onRoomLeaved(flowStateInfo: FlowStateInfo) {
+        roomService.seatController.removeListener(this)
+    }
+
+    override suspend fun getRoomInfo(
+        roomIds: List<Long>,
+        from: Long?,
+        needRoomOnlineInfo: Boolean,
+        getRoomOwnerFamily: Boolean
+    ): Rlt<List<RoomInfo>> {
+        return Rlt.Success(emptyList())
+//        return when (val result =
+//            roomHttpService.getRoomInfo(
+//                GetRoomsInfoReq(
+//                    roomIds,
+//                    needRoomOnlineInfo = needRoomOnlineInfo,
+//                    getRoomOwnerFamily = getRoomOwnerFamily
+//                )
+//            )) {
+//            is Rlt.Success -> {
+//                Rlt.Success(result.data.data?.roomInfoList ?: emptyList())
+//            }
+//
+//            is Rlt.Failed -> result
+//        }
+    }
+
+    override suspend fun getRoomInfo(roomId: Long): Rlt<RoomInfo> {
+        return when (val result = getRoomInfo(listOf(roomId), getRoomOwnerFamily = true)) {
+            is Rlt.Success -> {
+                val roomInfoList = result.data
+                if (roomInfoList.isEmpty().not()) {
+                    Rlt.Success(roomInfoList[0])
+                } else {
+                    Rlt.Failed(RoomInfoSizeError())
+                }
+            }
+
+            is Rlt.Failed -> {
+                result.apply { error.data = roomId }
+            }
+        }.apply {
+            Log.logRltD(TAG_ROOM_ATTR, "getRoomInfo, roomId:${roomId}", this)
+        }
+    }
+
+    private suspend fun getMyRoomInfoFromNet(): Rlt<RoomInfo> {
+//        return when (val result = roomHttpService.getMyRoomInfo()) {
+//            is Rlt.Success -> {
+//                if (result.data.data == null) {
+//                    return Rlt.Failed(CommonDataNullError())
+//                }
+//                Rlt.Success(result.data.data!!.roomInfo)
+//            }
+//
+//            is Rlt.Failed -> {
+//                return if (result.error.serverCode == ServerCode.USER_NO_ROOM.code) {
+//                    Rlt.Failed(RoomNoExistError())
+//                } else {
+//                    result
+//                }
+//            }
+//        }.apply {
+//            Log.logRltD(TAG_ROOM_ATTR, "getMyRoomInfoFromNet", this)
+//        }
+        return Rlt.Failed(CommonDataNullError())
+    }
+
+    override suspend fun getMyRoomInfo(forceNet: Boolean): Rlt<RoomInfo> {
+        return withContext(this.coroutineContext) {
+            if (forceNet) {
+                val rlt = getMyRoomInfoFromNet()
+                return@withContext if (rlt is Rlt.Success) {
+                    updateMyRoomInfo(rlt.data)
+                    saveMyRoomInfo(rlt.data)
+                    Rlt.Success(rlt.data)
+                } else {
+                    rlt
+                }
+            }
+
+            if (myRoomInfo != null) {
+                return@withContext Rlt.Success(myRoomInfo!!)
+            }
+
+            updateMyRoomInfo(loadMyRoomInfo())
+            if (myRoomInfo != null) {
+                return@withContext Rlt.Success(myRoomInfo!!)
+            }
+
+            val rlt = getMyRoomInfoFromNet()
+            return@withContext if (rlt is Rlt.Success) {
+                updateMyRoomInfo(rlt.data)
+                saveMyRoomInfo(rlt.data)
+                Rlt.Success(rlt.data)
+            } else {
+                rlt
+            }
+        }
+    }
+
+    private fun saveMyRoomInfo(roomInfo: RoomInfo) {
+        RoomLocalService.myRoomInfo = toJsonErrorNull(roomInfo) ?: ""
+    }
+
+    private fun loadMyRoomInfo(): RoomInfo? {
+        return froJsonErrorNull(RoomLocalService.myRoomInfo)
+    }
+
+    override fun getNecessaryRoomConfig(roomId: Long) {
+        launch {
+        }
+    }
+
+    override fun onClear() {
+        super.onClear()
+        launch {
+            updateMyRoomInfo(null)
+        }
+    }
+}

+ 225 - 0
module/room/src/main/java/com/adealink/weparty/room/sdk/controller/impl/WPDeviceController.kt

@@ -0,0 +1,225 @@
+package com.adealink.weparty.room.sdk.controller.impl
+
+import android.content.Context
+import android.net.wifi.WifiManager
+import android.os.Handler
+import android.os.PowerManager
+import androidx.annotation.CallSuper
+import com.adealink.frame.base.CommonSwitchStateSameError
+import com.adealink.frame.base.Rlt
+import com.adealink.frame.log.Log
+import com.adealink.weparty.room.sdk.context.IRoomContext
+import com.adealink.weparty.room.sdk.controller.BaseController
+import com.adealink.frame.room.data.TAG_ROOM_DEVICE
+import com.adealink.frame.util.ActivityLifecycleCallbacksExt
+import com.adealink.frame.util.AppUtil
+import com.adealink.frame.util.runOnUiThread
+import com.adealink.weparty.room.sdk.controller.IWPDeviceController
+import com.adealink.frame.room.data.FlowStateInfo
+import com.adealink.weparty.room.sdk.listener.IWPDeviceListener
+
+class WPDeviceController(override val ctx: IRoomContext, serialHandler: Handler) :
+    BaseController<IWPDeviceListener>(ctx, serialHandler), IWPDeviceController<IWPDeviceListener> {
+
+    companion object {
+        const val TAG_WAKE_LOCK = "room-sdk:WAKE_LOCK"
+        const val TAG_WIFI_LOCK = "room-sdk:WIFI_LOCK"
+
+        const val BACKGROUND_LOCK = PowerManager.PARTIAL_WAKE_LOCK
+        const val FOREGROUND_WAKE_FLAG = PowerManager.SCREEN_BRIGHT_WAKE_LOCK
+    }
+
+    private var wifiLock: WifiManager.WifiLock? = null
+    private var wakeLock: PowerManager.WakeLock? = null
+    private var currLevelAndFlags = -1
+
+    private val activityLifecycleCallback by lazy { ActivityLifecycleCallback() }
+
+    private var localAudioMute: Boolean = false
+    private var localAudioInitiativeMute: Boolean = false //用户主动mute
+    private var audioPlayerMute: Boolean = false
+    private var recordingSignalVolume = 100 //这个值要根据rtc不同调整,目前声网和腾讯云都是100
+
+    override fun enableLocalAudio(enabled: Boolean) {
+        Log.i(TAG_ROOM_DEVICE, "enableLocalAudio, enabled:$enabled")
+        ctx.mediaService.enableLocalAudio(enabled)
+    }
+
+    override fun userMuteMic(mute: Boolean, reason: MicMuteReason): Rlt<Any> {
+        Log.d(TAG_ROOM_DEVICE, "userMuteMic, mute:$mute, reason:$reason")
+        if (localAudioInitiativeMute != mute) {
+            localAudioInitiativeMute = mute
+            notifyUserMicMuteChanged()
+        }
+        return muteLocalAudio(mute, reason)
+    }
+
+    override fun ownerMuteMic(mute: Boolean) {
+        Log.d(TAG_ROOM_DEVICE, "ownerMuteMic, mute:$mute")
+        if (!mute && localAudioInitiativeMute) {
+            return
+        }
+
+        muteLocalAudio(mute, MicMuteReason.OWNER_SETTING)
+    }
+
+    private fun isSelfMicSeatMute(): Boolean {
+        return ctx.roomService.seatController.isMicSeatMute(ctx.appSupplier.selfUid)
+    }
+
+    override fun muteLocalAudio(mute: Boolean, reason: MicMuteReason): Rlt<Any> {
+        if (localAudioMute == mute) {
+            return Rlt.Failed(CommonSwitchStateSameError())
+        }
+
+        Log.i(TAG_ROOM_DEVICE, "muteLocalAudio, mute:$mute, reason:$reason")
+        localAudioMute = mute
+//        //禁麦(背景音乐同时静音)
+//        ctx.mediaService.muteLocalAudioStream(mute)
+        // 禁麦不禁背景音乐播放处理
+        ctx.mediaService.adjustRecordingSignalVolume(if (mute) 0 else recordingSignalVolume)
+        notifyMicMuteChanged()
+
+        return Rlt.Success(Any())
+    }
+
+    override fun isLocalAudioMute(): Boolean {
+        return localAudioMute || localAudioInitiativeMute
+    }
+
+    override fun isLocalAudioInitiativeMute(): Boolean {
+        return localAudioInitiativeMute
+    }
+
+    private fun notifyUserMicMuteChanged() {
+        listeners.dispatch {
+            it.onUserMicMuteChanged(localAudioInitiativeMute)
+        }
+    }
+
+    private fun notifyMicMuteChanged() {
+        listeners.dispatch {
+            it.onMicMuteChanged(isLocalAudioMute())
+        }
+    }
+
+    override fun muteAudioPlayer(mute: Boolean) {
+        if (audioPlayerMute == mute) {
+            return
+        }
+
+        Log.i(TAG_ROOM_DEVICE, "muteAudioPlayer, mute:$mute")
+        audioPlayerMute = mute
+        notifyAudioPlayerMuteChanged(mute)
+        ctx.mediaService.muteAllRemoteAudioStreams(mute)
+    }
+
+    override fun isAudioPlayerMute(): Boolean {
+        return audioPlayerMute
+    }
+
+    private fun notifyAudioPlayerMuteChanged(mute: Boolean) {
+        listeners.dispatch {
+            it.onAudioPlayerMute(mute)
+        }
+    }
+
+    @CallSuper
+    override fun onRoomIn(flowStateInfo: FlowStateInfo) {
+        runOnUiThread {
+            acquireLock(AppUtil.background)
+        }
+        AppUtil.registerActivityLifecycleCallbacks(activityLifecycleCallback)
+    }
+
+    @CallSuper
+    override fun onRoomLeaved(flowStateInfo: FlowStateInfo) {
+        localAudioMute = true
+        localAudioInitiativeMute = false
+        audioPlayerMute = false
+        runOnUiThread {
+            releaseLock()
+        }
+        AppUtil.unregisterActivityLifecycleCallbacks(activityLifecycleCallback)
+    }
+
+    @CallSuper
+    override fun onChannelIn(flowStateInfo: FlowStateInfo) {
+        super.onChannelIn(flowStateInfo)
+        ctx.mediaService.setEnableSpeakerphone(true)
+        if (isLocalAudioMute()) {
+            ctx.mediaService.adjustRecordingSignalVolume(0)
+        }
+        if (audioPlayerMute) {
+            ctx.mediaService.muteAllRemoteAudioStreams(audioPlayerMute)
+        }
+    }
+
+    override fun acquireLock(background: Boolean) {
+        acquireWifiLock()
+        acquireWakeLock(if (background) BACKGROUND_LOCK else FOREGROUND_WAKE_FLAG)
+    }
+
+    private fun acquireWifiLock() {
+        if (wifiLock == null) {
+            val wm =
+                ctx.appSupplier.appContext.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
+            wifiLock = wm.createWifiLock(TAG_WIFI_LOCK)
+            wifiLock?.setReferenceCounted(false)
+        }
+        if (wifiLock?.isHeld == false) {
+            wifiLock?.acquire()
+        }
+    }
+
+    private fun acquireWakeLock(levelAndFlags: Int) {
+        if (wakeLock == null) {
+            val pm =
+                ctx.appSupplier.appContext.applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager
+            wakeLock = pm.newWakeLock(
+                levelAndFlags
+                        or PowerManager.ACQUIRE_CAUSES_WAKEUP
+                        or PowerManager.ON_AFTER_RELEASE, TAG_WAKE_LOCK
+            )
+            wakeLock?.setReferenceCounted(false)
+            currLevelAndFlags = levelAndFlags
+        }
+        if (wakeLock?.isHeld == false) {
+            wakeLock?.acquire(10 * 60 * 1000L)
+        }
+    }
+
+    override fun releaseLock() {
+        releaseWakeLock()
+        releaseWifiLock()
+    }
+
+    private fun releaseWakeLock() {
+        if (wakeLock?.isHeld == true) {
+            wakeLock?.release()
+            wakeLock = null
+            currLevelAndFlags = -1
+        }
+    }
+
+    private fun releaseWifiLock() {
+        if (wifiLock?.isHeld == true) {
+            wifiLock?.release()
+        }
+    }
+
+    inner class ActivityLifecycleCallback : ActivityLifecycleCallbacksExt {
+
+        override fun onEnterBackGround() {
+            super.onEnterBackGround()
+            acquireLock(true)
+        }
+
+        override fun onEnterForeGround() {
+            super.onEnterForeGround()
+            acquireLock(false)
+        }
+
+    }
+
+}

+ 52 - 0
module/room/src/main/java/com/adealink/weparty/room/sdk/controller/impl/WPJoinController.kt

@@ -0,0 +1,52 @@
+package com.adealink.weparty.room.sdk.controller.impl
+
+import android.os.Handler
+import com.adealink.frame.log.Log
+import com.adealink.frame.room.data.ChannelState
+import com.adealink.frame.room.data.FlowStateInfo
+import com.adealink.frame.room.data.RoomState
+import com.adealink.frame.room.data.TAG_ROOM_FLOW
+import com.adealink.frame.room.listener.IRoomStateChangeListener
+import com.adealink.weparty.room.sdk.context.IRoomContext
+import com.adealink.weparty.room.sdk.controller.BaseController
+import com.adealink.weparty.room.sdk.controller.IWPJoinController
+import com.adealink.weparty.room.sdk.listener.IWPJoinListener
+
+open class WPJoinController(override val ctx: IRoomContext, serialHandler: Handler) :
+    BaseController<IWPJoinListener>(ctx, serialHandler), IWPJoinController<IWPJoinListener>,
+    IRoomStateChangeListener {
+    override fun notifyRoomStateChanged(
+        fromState: RoomState,
+        toState: RoomState,
+        flowStateInfo: FlowStateInfo
+    ) {
+        Log.i(
+            TAG_ROOM_FLOW,
+            "notifyRoomStateChanged, fromState:$fromState, toState:$toState, flowStateInfo:$flowStateInfo"
+        )
+        ctx.roomService.controllers.forEach {
+            it.onRoomStateChanged(fromState, toState, flowStateInfo)
+        }
+    }
+
+    override fun notifyChannelStateChanged(
+        fromState: ChannelState,
+        toState: ChannelState,
+        flowStateInfo: FlowStateInfo
+    ) {
+        Log.i(
+            TAG_ROOM_FLOW,
+            "notifyChannelStateChanged, fromState:$fromState, toState:$toState, flowStateInfo:$flowStateInfo"
+        )
+        ctx.roomService.controllers.forEach {
+            it.onChannelStateChanged(fromState, toState, flowStateInfo)
+        }
+    }
+
+
+    companion object {
+        private const val REJOIN_CHANNEL_DELAY = 10_000L
+        private const val REJOINING_INTERVAL_MS = 15 * 1000
+    }
+
+}

+ 353 - 0
module/room/src/main/java/com/adealink/weparty/room/sdk/controller/impl/WPMemberController.kt

@@ -0,0 +1,353 @@
+package com.adealink.weparty.room.sdk.controller.impl
+
+import android.os.Handler
+import com.adealink.frame.base.Rlt
+import com.adealink.frame.log.Log
+import com.adealink.frame.network.ISocketNotify
+import com.adealink.frame.network.data.Res
+import com.adealink.frame.room.data.FlowStateInfo
+import com.adealink.frame.room.data.JoinChannelReason
+import com.adealink.frame.room.data.LeaveChannelReason
+import com.adealink.frame.room.data.LeaveRoomReason
+import com.adealink.frame.room.data.RoomMemberOnlineInfo
+import com.adealink.frame.room.data.TAG_ROOM_MEMBER
+import com.adealink.weparty.App
+import com.adealink.weparty.module.profile.ProfileModule
+import com.adealink.weparty.module.profile.data.UserInfo
+import com.adealink.weparty.module.room.data.MemberRoomRole
+import com.adealink.weparty.module.room.data.RoomUserInfo
+import com.adealink.weparty.room.constant.getRoomErrorByError
+import com.adealink.weparty.room.data.AddRoomAdminReq
+import com.adealink.weparty.room.data.DelRoomAdminReq
+import com.adealink.weparty.room.data.KickOutRoomNotify
+import com.adealink.weparty.room.data.KickOutRoomReq
+import com.adealink.weparty.room.data.KickOutRoomRes
+import com.adealink.weparty.room.data.RoomMemberOnlineInfoNotify
+import com.adealink.weparty.room.datasource.remote.RoomHttpService
+import com.adealink.weparty.room.sdk.context.IRoomContext
+import com.adealink.weparty.room.sdk.controller.BaseController
+import com.adealink.weparty.room.sdk.controller.IWPMemberController
+import com.adealink.weparty.room.sdk.listener.IWPMemberListener
+import com.adealink.weparty.room.stat.RoomDurationReporter
+import kotlinx.coroutines.launch
+
+/**
+ * Created by sunxiaodong on 2021/5/3.
+ */
+class WPMemberController(override val ctx: IRoomContext, serialHandler: Handler) :
+    BaseController<IWPMemberListener>(ctx, serialHandler), IWPMemberController<IWPMemberListener> {
+
+    private val roomHttpService by lazy { App.instance.networkService.getHttpService(RoomHttpService::class.java) }
+    private var onlineInfoVersion = 0L
+    private var onlineMemberCount = 0L //在线成员数
+
+    private val kickOutRoomNotify = object : ISocketNotify<KickOutRoomNotify> {
+
+        override val uri: String = "USER_KICK_NOTIFY"
+
+        override fun needHandle(data: KickOutRoomNotify?): Boolean {
+            return data != null && ctx.roomService.joinController.getJoinedRoomId() == data.roomId
+        }
+
+        override fun onNotify(data: KickOutRoomNotify) {
+            launch {
+                ctx.roomService.joinController.tryLeaveRoom(LeaveRoomReason.KICK_OUT, false)
+                listeners.dispatch {
+                    it.onKickOutRoom(data.kickReason)
+                }
+            }
+        }
+
+    }
+
+    private val roomMemberOnlineInfoNotify = object : ISocketNotify<RoomMemberOnlineInfoNotify> {
+
+        override val uri: String = "ROOM_ONLINE_NOTIFY"
+
+        override fun needHandle(data: RoomMemberOnlineInfoNotify?): Boolean {
+            return data != null && ctx.roomService.joinController.getJoinedRoomId() == data.roomId
+        }
+
+        override fun onNotify(data: RoomMemberOnlineInfoNotify) {
+            handleMemberOnlineInfo(data.memberOnlineInfo, data.ownerInfo)
+        }
+
+    }
+
+    override fun isJoinedRoomOwner(uid: Long): Boolean {
+        return ctx.roomService.joinController.joinedRoomInfo?.ownerUid == uid
+    }
+
+    override fun getJoinedRoomOwnerUid(): Long? {
+        return ctx.roomService.joinController.joinedRoomInfo?.ownerUid
+    }
+
+    override fun isJoinedRoomAdmin(uid: Long): Boolean {
+        return ctx.roomService.joinController.joinedRoomInfo?.adminList?.contains(uid) == true
+    }
+
+    override fun getJoinedRoomMemberRole(uid: Long): MemberRoomRole {
+        return when {
+            isJoinedRoomOwner(uid) -> MemberRoomRole.OWNER
+            isJoinedRoomAdmin(uid) -> MemberRoomRole.ADMIN
+            ctx.roomService.joinController.isRoomJoined() -> MemberRoomRole.AUDIENCE
+            else -> MemberRoomRole.UNKNOWN
+        }
+    }
+
+    override fun isJoinedRoomOwnerOrAdmin(uid: Long): Boolean {
+        return getJoinedRoomMemberRole(uid).isRoomOwnerOrAdmin()
+    }
+
+    private fun handleMemberOnlineInfo(
+        memberOnlineInfo: RoomMemberOnlineInfo,
+        ownerInfo: RoomUserInfo? = null,
+        token: String? = null
+    ) {
+
+        launch {
+            if (memberOnlineInfo.version <= onlineInfoVersion) {
+                Log.d(
+                    TAG_ROOM_MEMBER,
+                    "version return, onlineInfoVersion:$onlineInfoVersion,memberOnlineInfo.version:${memberOnlineInfo.version},"
+                )
+                return@launch
+            }
+            ownerInfo?.userInfo?.let { memberInfo ->
+                updateCacheMemberInfo(memberInfo)
+                ownerInfo.configInfo?.let {
+                    ProfileModule.handleUserConfigUpdate(memberInfo.uid, it)
+                }
+            }
+            onlineInfoVersion = memberOnlineInfo.version
+            onlineMemberCount = memberOnlineInfo.onlineMemberCount
+            Log.d(
+                TAG_ROOM_MEMBER,
+                "version return, onlineMemberCount:$onlineMemberCount,isMediaUsing:${ctx.mediaService.isMediaUsing()},"
+            )
+            //根据房间成员数执行媒体进出,节约成本
+            if (onlineMemberCount > 1) {
+                ctx.roomService.joinController.joinChannel(
+                    JoinChannelReason.ON_LINE_MEMBER_GT_1,
+                    token
+                )
+            } else if (!ctx.mediaService.isMediaUsing() && ctx.mediaService.isChannelJoined()) {
+                ctx.roomService.joinController.leaveChannel(LeaveChannelReason.ON_LINE_MEMBER_LE_1)
+            }
+            notifyMemberCountChanged(onlineMemberCount)
+            if (isJoinedRoomOwner(ctx.appSupplier.selfUid)) {
+                RoomDurationReporter.memberChange(onlineMemberCount)
+            }
+        }
+    }
+
+    override fun getOnlineMemberCount(): Long {
+        return onlineMemberCount
+    }
+
+    private fun notifyMemberCountChanged(count: Long) {
+        listeners.dispatch {
+            it.onMemberCountChanged(count)
+        }
+    }
+
+    override fun onRoomIn(flowStateInfo: FlowStateInfo) {
+        App.instance.networkService.subscribeNotify(kickOutRoomNotify)
+        App.instance.networkService.subscribeNotify(roomMemberOnlineInfoNotify)
+        val onlineInfo = flowStateInfo.onlineInfo ?: return
+        handleMemberOnlineInfo(onlineInfo, null, flowStateInfo.token)
+    }
+
+    override fun onRoomLeaved(flowStateInfo: FlowStateInfo) {
+        reset()
+        App.instance.networkService.unSubscribeNotify(kickOutRoomNotify)
+        App.instance.networkService.unSubscribeNotify(roomMemberOnlineInfoNotify)
+    }
+
+    private fun reset() {
+        onlineInfoVersion = 0
+        onlineMemberCount = 0
+    }
+
+    override suspend fun kickOutRoom(uid: Long): Rlt<Res<KickOutRoomRes>> {
+        val joinedRoomIdRlt = ctx.roomService.joinController.getJoinedRoomIdRlt()
+        if (joinedRoomIdRlt is Rlt.Failed) {
+            return joinedRoomIdRlt
+        }
+
+        val roomId = (joinedRoomIdRlt as Rlt.Success).data
+        return when (val rlt = roomHttpService.kickOutRoom(KickOutRoomReq(roomId, uid))
+            .apply {
+                Log.logRltD(TAG_ROOM_MEMBER, "kickOutRoom, roomId:${roomId}, uid:${uid}", this)
+            }) {
+            is Rlt.Success -> {
+                rlt
+            }
+
+            is Rlt.Failed -> {
+                Rlt.Failed(getRoomErrorByError(rlt.error))
+            }
+        }
+    }
+
+    override fun onCreate() {
+        super.onCreate()
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+    }
+
+    override suspend fun getMembers(): Rlt<MutableList<UserInfo>> {
+        val joinedRoomIdRlt = ctx.roomService.joinController.getJoinedRoomIdRlt()
+        if (joinedRoomIdRlt is Rlt.Failed) {
+            return joinedRoomIdRlt
+        }
+
+        val roomId = (joinedRoomIdRlt as Rlt.Success).data
+        return when (val result = roomHttpService.getRoomMembers(roomId)) {
+            is Rlt.Success -> {
+                val members = result.data.data?.members?.toSet() ?: emptySet()
+                val userInfos = (ProfileModule.getUsersInfoByUid(members) as? Rlt.Success)?.data
+                Rlt.Success(
+                    members.map {
+                        userInfos?.get(it) ?: UserInfo.emptyUserInfo(it, "")
+                    }.toMutableList()
+                )
+            }
+
+            is Rlt.Failed -> result
+        }.apply {
+            Log.logRltD(TAG_ROOM_MEMBER, "getMembers, roomId:${roomId}", this)
+        }
+    }
+
+    override suspend fun getMembers(page: Int, size: Int): Rlt<MutableList<UserInfo>> {
+        val joinedRoomIdRlt = ctx.roomService.joinController.getJoinedRoomIdRlt()
+        if (joinedRoomIdRlt is Rlt.Failed) {
+            return joinedRoomIdRlt
+        }
+
+        val roomId = (joinedRoomIdRlt as Rlt.Success).data
+        return when (val result = roomHttpService.getRoomMembers(roomId)) {
+            is Rlt.Success -> {
+                val members = result.data.data?.members?.toSet() ?: emptySet()
+                val userInfos = (ProfileModule.getUsersInfoByUid(members) as? Rlt.Success)?.data
+                Rlt.Success(
+                    members.map {
+                        userInfos?.get(it) ?: UserInfo.emptyUserInfo(it, "")
+                    }.toMutableList()
+                )
+            }
+
+            is Rlt.Failed -> result
+        }.apply {
+            Log.logRltD(TAG_ROOM_MEMBER, "getMembers, roomId:${roomId}", this)
+        }
+    }
+
+    override suspend fun getMemberInfoWithRetry(uid: Long): Rlt<UserInfo> {
+        var retry = 0
+        var result: Rlt<UserInfo>
+        do {
+            result = getMemberInfo(uid)
+            retry++
+        } while (result is Rlt.Failed && retry < 3)
+        return result
+    }
+
+    override suspend fun getMemberInfo(uid: Long): Rlt<UserInfo> {
+        val roomMember = ProfileModule.getCacheUserInfo(uid)
+        if (roomMember != null) {
+            return Rlt.Success(roomMember)
+        }
+
+        return when (val result = ProfileModule.getUserInfoByUid(uid)) {
+            is Rlt.Success -> {
+                Rlt.Success(result.data)
+            }
+
+            is Rlt.Failed -> {
+                result
+            }
+        }.apply {
+            Log.logRltD(TAG_ROOM_MEMBER, "getMemberInfo, uid:${uid}", this)
+        }
+    }
+
+    override suspend fun getMembersInfo(uidSet: Set<Long>): Rlt<Map<Long, UserInfo>> {
+        val uidToMemberMap = hashMapOf<Long, UserInfo>()
+        val noCacheMemberUidSet = hashSetOf<Long>()
+        uidSet.forEach {
+            val roomMember = ProfileModule.getCacheUserInfo(it)
+            if (roomMember == null) {
+                noCacheMemberUidSet.add(it)
+            } else {
+                uidToMemberMap[it] = roomMember
+            }
+        }
+        if (noCacheMemberUidSet.isEmpty()) {
+            return Rlt.Success(uidToMemberMap)
+        }
+
+        return when (val result = ProfileModule.getUsersInfoByUid(noCacheMemberUidSet)) {
+            is Rlt.Success -> {
+                result.data.forEach {
+                    uidToMemberMap[it.key] = it.value
+                }
+                return Rlt.Success(uidToMemberMap)
+
+            }
+
+            is Rlt.Failed -> result
+        }.apply {
+            Log.logRltD(TAG_ROOM_MEMBER, "getMembersInfo, uidSet:${uidSet}", this)
+        }
+    }
+
+    override suspend fun addRoomAdmin(uid: Long): Rlt<Any> {
+        return roomHttpService.addRoomAdmin(AddRoomAdminReq(uid))
+    }
+
+    override suspend fun delRoomAdmin(uid: Long): Rlt<Any> {
+        return roomHttpService.delRoomAdmin(DelRoomAdminReq(adminId = uid))
+    }
+
+    override suspend fun getRankBoardMembers(): Rlt<MutableList<UserInfo>> {
+        val joinedRoomIdRlt = ctx.roomService.joinController.getJoinedRoomIdRlt()
+        if (joinedRoomIdRlt is Rlt.Failed) {
+            return joinedRoomIdRlt
+        }
+        val roomId = (joinedRoomIdRlt as Rlt.Success).data
+        return when (val result = roomHttpService.getRankBoardRoomMembers(roomId)) {
+            is Rlt.Success -> {
+                val roomMembersRes = result.data.data!!
+                val members = roomMembersRes.members
+                val memberInfos = mutableListOf<UserInfo>()
+                val userInfoMap =
+                    (ProfileModule.getUsersInfoByUid(members.toSet()) as? Rlt.Success)?.data
+                members.onEach { uid ->
+                    userInfoMap?.get(uid)?.let { roomMember ->
+                        memberInfos.add(roomMember)
+                    }
+                }
+                Rlt.Success(memberInfos)
+            }
+
+            is Rlt.Failed -> result
+        }.apply {
+            Log.logRltD(TAG_ROOM_MEMBER, "getRankBoardMembers, roomId:${roomId}", this)
+        }
+    }
+
+    override fun updateCacheMemberInfo(member: UserInfo) {
+        ProfileModule.updateCacheUserInfo(member)
+    }
+
+    override fun notifySelfRoleChanged(before: MemberRoomRole, after: MemberRoomRole) {
+        listeners.dispatch {
+            it.onSelfRoleChanged(before, after)
+        }
+    }
+
+}

+ 15 - 0
module/room/src/main/java/com/adealink/weparty/room/sdk/controller/impl/WPSeatController.kt

@@ -0,0 +1,15 @@
+package com.adealink.weparty.room.sdk.controller.impl
+
+import android.os.Handler
+import com.adealink.frame.media.listener.IMediaRtcListener
+import com.adealink.weparty.room.sdk.context.IRoomContext
+import com.adealink.weparty.room.sdk.controller.BaseController
+import com.adealink.weparty.room.sdk.controller.IWPSeatController
+import com.adealink.weparty.room.sdk.listener.IWPDeviceListener
+import com.adealink.weparty.room.sdk.listener.IWPSeatListener
+
+class WPSeatController(override val ctx: IRoomContext, serialHandler: Handler) :
+    BaseController<IWPSeatListener>(ctx, serialHandler), IWPSeatController<IWPSeatListener>,
+    IWPDeviceListener, IMediaRtcListener {
+
+}

+ 9 - 0
module/room/src/main/java/com/adealink/weparty/room/sdk/listener/IWPAttrListener.kt

@@ -0,0 +1,9 @@
+package com.adealink.weparty.room.sdk.listener
+
+import com.adealink.frame.room.listener.IListener
+
+interface IWPAttrListener : IListener {
+//    fun onRoomTypeChanged(roomType: RoomType)
+//    fun onRoomConfigChanged(configType: Int, configValue: String)
+//    fun onRoomOnMicModeChanged(onMicMode: RoomOnMicMode)
+}

+ 13 - 0
module/room/src/main/java/com/adealink/weparty/room/sdk/listener/IWPDeviceListener.kt

@@ -0,0 +1,13 @@
+package com.adealink.weparty.room.sdk.listener
+
+import com.adealink.frame.room.listener.IListener
+
+interface IWPDeviceListener : IListener {
+
+    fun onAudioPlayerMute(mute: Boolean) {}
+
+    fun onUserMicMuteChanged(userMute: Boolean) {}
+
+    fun onMicMuteChanged(mute: Boolean) {}
+
+}

+ 27 - 0
module/room/src/main/java/com/adealink/weparty/room/sdk/listener/IWPJoinListener.kt

@@ -0,0 +1,27 @@
+package com.adealink.weparty.room.sdk.listener
+
+import com.adealink.frame.room.data.ChannelState
+import com.adealink.frame.room.data.FlowStateInfo
+import com.adealink.frame.room.data.RoomState
+import com.adealink.frame.room.listener.IListener
+import com.adealink.weparty.module.room.data.RoomInfo
+
+interface IWPJoinListener : IListener {
+
+    fun onRoomStateChanged(
+        fromState: RoomState,
+        toState: RoomState,
+        flowStateInfo: FlowStateInfo,
+    ) {
+    } //Dispatcher.SINGLE_BG上回调
+
+    fun onChannelStateChanged(
+        fromState: ChannelState,
+        toState: ChannelState,
+        flowStateInfo: FlowStateInfo,
+    ) {
+    }
+
+    fun onJoinedRoomInfoUpdate(roomInfo: RoomInfo) {}
+
+}

+ 19 - 0
module/room/src/main/java/com/adealink/weparty/room/sdk/listener/IWPMemberListener.kt

@@ -0,0 +1,19 @@
+package com.adealink.weparty.room.sdk.listener
+
+import com.adealink.frame.room.listener.IListener
+
+interface IWPMemberListener : IListener {
+
+    fun onKickOutRoom(kickReason: Long) {}
+
+    /**
+     * 在线成员数变化
+     */
+    fun onMemberCountChanged(count: Long)
+
+    /**
+     * 角色变化回调
+     */
+//    fun onSelfRoleChanged(before: MemberRoomRole, after: MemberRoomRole) {}
+
+}

+ 46 - 0
module/room/src/main/java/com/adealink/weparty/room/sdk/listener/IWPSeatListener.kt

@@ -0,0 +1,46 @@
+package com.adealink.weparty.room.sdk.listener
+
+import com.adealink.frame.room.listener.IListener
+
+interface IWPSeatListener : IListener {
+
+//    fun onMicSeatsChanged(micSeatsInfo: Map<Int, MicSeatInfo>) {}
+//
+//    /**
+//     * 自己麦位状态发生变化
+//     * @param micSeatInfo: null: 下麦
+//     */
+//    fun onSelfMicSeatChanged(micSeatInfo: MicSeatInfo?) {}
+//
+//    fun onMicSeatOpResult(result: MicSeatOpResult) {}
+//
+//    fun onMemberSpeakingChanged(changed: List<MemberSpeaking>) {}
+//
+//    /**
+//     * 被设置为旁听
+//     */
+//    fun onMicSeatListened(reason: MicKickReason?) {}
+//
+//    /**
+//     * 邀请上麦
+//     */
+//    fun onMicSeatInvite(micIndex: MicIndex) {}
+//
+//    /**
+//     * 超级麦位开关变化
+//     */
+//    fun onSuperMicSwitchChanged(open: Boolean, isDeluxeMic: Boolean) {}
+//
+//    fun onMicModeChanged(mode: RoomMicMode, layout: RoomMicMode) {}
+//
+//    /**
+//     * 上麦申请队列变化
+//     */
+//    fun onUpMicQueueChanged(notify: UpMicRequestQueueChangedNotify) {}
+//
+//    /**
+//     * 申请上麦状态变化
+//     */
+//    fun applyOnMicStatusChanged(status: ApplyOnMicStatus) {}
+
+}

+ 39 - 0
module/room/src/main/java/com/adealink/weparty/room/sdk/service/IRoomService.kt

@@ -0,0 +1,39 @@
+package com.adealink.weparty.room.sdk.service
+
+import com.adealink.frame.room.listener.IListener
+import com.adealink.weparty.room.sdk.context.IRoomContext
+import com.adealink.weparty.room.sdk.controller.IController
+import com.adealink.weparty.room.sdk.controller.IWPAttrController
+import com.adealink.weparty.room.sdk.controller.IWPDeviceController
+import com.adealink.weparty.room.sdk.controller.IWPJoinController
+import com.adealink.weparty.room.sdk.controller.IWPMemberController
+import com.adealink.weparty.room.sdk.controller.IWPSeatController
+import com.adealink.weparty.room.sdk.listener.IWPAttrListener
+import com.adealink.weparty.room.sdk.listener.IWPDeviceListener
+import com.adealink.weparty.room.sdk.listener.IWPJoinListener
+import com.adealink.weparty.room.sdk.listener.IWPMemberListener
+import com.adealink.weparty.room.sdk.listener.IWPSeatListener
+
+/**
+ * Created by sunxiaodong on 2021/3/1.
+ */
+interface IRoomService : IRoomContext {
+
+    val joinController: IWPJoinController<IWPJoinListener>
+    val attrController: IWPAttrController<IWPAttrListener>
+    val memberController: IWPMemberController<IWPMemberListener>
+    val seatController: IWPSeatController<IWPSeatListener>
+    val deviceController: IWPDeviceController<IWPDeviceListener>
+    val controllers: MutableList<out IController<out IListener>>
+
+    fun create() //用于监听注册,初始化数据
+
+    fun resume() //恢复事件分发
+
+    fun pause() //暂停事件分发
+
+    fun destroy() //用于监听移除,清理数据
+
+    fun clear()
+
+}

+ 110 - 0
module/room/src/main/java/com/adealink/weparty/room/sdk/service/WPRoomService.kt

@@ -0,0 +1,110 @@
+package com.adealink.weparty.room.sdk.service
+
+import com.adealink.frame.coroutine.dispatcher.Dispatcher
+import com.adealink.frame.media.IMediaRtcService
+import com.adealink.frame.room.listener.IListener
+import com.adealink.frame.room.supplier.IAppSupplier
+import com.adealink.weparty.room.sdk.controller.BaseController
+import com.adealink.weparty.room.sdk.controller.IController
+import com.adealink.weparty.room.sdk.controller.IWPAttrController
+import com.adealink.weparty.room.sdk.controller.IWPDeviceController
+import com.adealink.weparty.room.sdk.controller.IWPJoinController
+import com.adealink.weparty.room.sdk.controller.IWPMemberController
+import com.adealink.weparty.room.sdk.controller.IWPSeatController
+import com.adealink.weparty.room.sdk.controller.impl.WPAttrController
+import com.adealink.weparty.room.sdk.controller.impl.WPDeviceController
+import com.adealink.weparty.room.sdk.controller.impl.WPJoinController
+import com.adealink.weparty.room.sdk.controller.impl.WPMemberController
+import com.adealink.weparty.room.sdk.controller.impl.WPSeatController
+import com.adealink.weparty.room.sdk.listener.IWPAttrListener
+import com.adealink.weparty.room.sdk.listener.IWPDeviceListener
+import com.adealink.weparty.room.sdk.listener.IWPJoinListener
+import com.adealink.weparty.room.sdk.listener.IWPMemberListener
+import com.adealink.weparty.room.sdk.listener.IWPSeatListener
+import com.adealink.weparty.room.sdk.supplier.AppSupplier
+
+/**
+ * Created by sunxiaodong on 2021/3/1.
+ */
+
+val roomService by lazy { WPRoomService(AppSupplier()).apply { create() } }
+
+class WPRoomService(override val appSupplier: IAppSupplier) : IRoomService {
+
+    private val serialHandler = Dispatcher.getSerialHandler()
+
+    override val joinController: IWPJoinController<IWPJoinListener> by lazy { WPJoinController(this, serialHandler) }
+    override val attrController: IWPAttrController<IWPAttrListener> by lazy { WPAttrController(this, serialHandler) }
+    override val memberController: IWPMemberController<IWPMemberListener> by lazy { WPMemberController(this, serialHandler) }
+    override val seatController: IWPSeatController<IWPSeatListener> by lazy { WPSeatController(this, serialHandler) }
+    override val deviceController: IWPDeviceController<IWPDeviceListener> by lazy { WPDeviceController(this, serialHandler) }
+    override val controllers: MutableList<BaseController<out IListener>> = arrayListOf()
+
+    override val mediaService: IMediaRtcService
+        get() = appSupplier.mediaService
+
+    override val roomService: IRoomService
+        get() = this
+
+    private var lifecycleState: LifecycleState = LifecycleState.DESTROY
+
+    private fun addController(controller: IController<out IListener>) {
+        (controller as? BaseController<out IListener>)?.let { controllers.add(it) }
+    }
+
+    override fun create() {
+        synchronized(lifecycleState) {
+            if (lifecycleState == LifecycleState.DESTROY) {
+                lifecycleState = LifecycleState.CREATE
+                addController(joinController)
+                addController(seatController)
+                addController(attrController)
+                addController(memberController)
+                addController(deviceController)
+                controllers.onEach { it.onCreate() }
+            }
+        }
+    }
+
+    override fun resume() {
+        synchronized(lifecycleState) {
+            if (lifecycleState == LifecycleState.CREATE || lifecycleState == LifecycleState.PAUSE) {
+                lifecycleState = LifecycleState.RESUME
+                controllers.onEach { it.onResume() }
+            }
+        }
+    }
+
+    override fun pause() {
+        synchronized(lifecycleState) {
+            if (lifecycleState == LifecycleState.RESUME) {
+                lifecycleState = LifecycleState.PAUSE
+                controllers.onEach { it.onPause() }
+            }
+        }
+    }
+
+    override fun destroy() {
+        synchronized(lifecycleState) {
+            if (lifecycleState != LifecycleState.DESTROY) {
+                lifecycleState = LifecycleState.DESTROY
+                controllers.onEach { it.onDestroy() }
+                controllers.clear()
+            }
+        }
+    }
+
+    override fun clear() {
+        synchronized(lifecycleState) {
+            controllers.onEach { it.onClear() }
+        }
+    }
+
+}
+
+private enum class LifecycleState {
+    CREATE,
+    RESUME,
+    PAUSE,
+    DESTROY
+}

+ 19 - 0
module/room/src/main/java/com/adealink/weparty/room/sdk/supplier/AppSupplier.kt

@@ -0,0 +1,19 @@
+package com.adealink.weparty.room.sdk.supplier
+
+import android.content.Context
+import com.adealink.frame.media.IMediaRtcService
+import com.adealink.frame.room.supplier.IAppSupplier
+import com.adealink.weparty.App
+import com.adealink.weparty.module.profile.ProfileModule
+
+class AppSupplier : IAppSupplier {
+
+    override val mediaService: IMediaRtcService by lazy { App.instance.mediaService }
+
+    override val selfUid: String
+        get() = ProfileModule.getMyUid()
+
+    override val appContext: Context
+        get() = App.instance
+
+}

+ 150 - 0
module/room/src/main/java/com/adealink/weparty/room/service/KeepForegroundService.kt

@@ -0,0 +1,150 @@
+package com.adealink.weparty.room.service
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
+import android.os.Build
+import android.os.IBinder
+import androidx.core.app.NotificationCompat
+import androidx.core.app.ServiceCompat
+import com.adealink.frame.aab.util.getCompatString
+import com.adealink.frame.log.Log
+import com.adealink.frame.util.AppUtil
+import com.adealink.weparty.notifiation.ROOM_NOTIFICATION_CHANNEL_ID
+import com.adealink.weparty.room.BuildConfig
+import com.adealink.weparty.room.R
+import com.adealink.weparty.room.RoomActivity
+import com.adealink.weparty.R as APP_R
+
+class KeepForegroundService : Service() {
+
+    override fun onBind(intent: Intent): IBinder? {
+        return null
+    }
+
+    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+        handleKeepForegroundAction(intent)
+        return START_STICKY
+    }
+
+    private fun handleKeepForegroundAction(intent: Intent?): Boolean {
+        // TODO: zhangfei
+//        if (roomService.joinController.getJoinedRoomId() == null) {
+//            stopSelf()
+//            return false
+//        }
+
+        if (intent == null) {
+            return false
+        }
+
+        val action = intent.action
+        if (ACTION_START_KEEP_FOREGROUND != action) {
+            return false
+        }
+
+        startForegroundForKeepAlive()
+        return true
+    }
+
+    private fun startForegroundForKeepAlive() {
+        // TODO: zhangfei
+//        val roomId = roomService.joinController.getJoinedRoomId()
+//        if (roomId == null) {
+//            Log.i(TAG, "startForegroundForKeepAlive, not in room")
+//            return
+//        }
+
+        try {
+            ServiceCompat.startForeground(
+                this,
+                ROOM_BG_NOTIFICATION_ID,
+                crateRoomBGNotification(),
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+                    FOREGROUND_SERVICE_TYPE_MICROPHONE
+                } else {
+                    0
+                }
+            )
+        } catch (e: Exception) {
+            Log.e(TAG, "startForegroundForKeepAlive: ${e.message}")
+        }
+    }
+
+    companion object {
+
+        private const val TAG = "KeepForegroundService"
+        private const val ACTION_START_KEEP_FOREGROUND =
+            "${BuildConfig.APPLICATION_ID}.KeepForeground"
+        private val ROOM_BG_NOTIFICATION_ID: Int = "notification.room.bg".hashCode()
+
+        fun startKeepRoomForeground() {
+            Log.i(TAG, "startKeepRoomForeground, keep room alive")
+
+            try {
+                val context = AppUtil.appContext
+                val intent = Intent(context, KeepForegroundService::class.java)
+                intent.action = ACTION_START_KEEP_FOREGROUND
+                context.startService(intent)
+            } catch (e: Exception) {
+                Log.e(TAG, "startKeepRoomForeground, e:", e)
+            }
+        }
+
+        fun stopKeepRoomForeground() {
+            Log.i(TAG, "stopKeepRoomForeground")
+
+            try {
+                val context = AppUtil.appContext
+                val intent = Intent(context, KeepForegroundService::class.java)
+                context.stopService(intent)
+            } catch (e: Exception) {
+                Log.e(TAG, "stopKeepRoomForeground, e:", e)
+            }
+        }
+
+        private fun createNotificationChannel() {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+                val name = getCompatString(R.string.room_channel_name)
+                val channel = NotificationChannel(
+                    ROOM_NOTIFICATION_CHANNEL_ID,
+                    name,
+                    NotificationManager.IMPORTANCE_LOW
+                ).apply {
+                    description = name
+                }
+                val notificationManager: NotificationManager? =
+                    AppUtil.getSystemService<NotificationManager>(Context.NOTIFICATION_SERVICE)
+                notificationManager?.createNotificationChannel(channel)
+            }
+        }
+
+        private fun crateRoomBGNotification(): Notification {
+            createNotificationChannel()
+            val context = AppUtil.appContext
+            val pendingIntent: PendingIntent = Intent(context, RoomActivity::class.java)
+                .let {
+                    PendingIntent.getActivity(context, 0, it, PendingIntent.FLAG_IMMUTABLE)
+                }
+
+            return NotificationCompat.Builder(context, ROOM_NOTIFICATION_CHANNEL_ID)
+                .setContentTitle(
+                    getCompatString(
+                        R.string.room_notification_running,
+                        getCompatString(APP_R.string.app_name)
+                    )
+                )
+                .setContentText(getCompatString(R.string.room_notification_back))
+                .setSmallIcon(APP_R.drawable.notification_ic)
+                .setContentIntent(pendingIntent)
+                .setTicker(getCompatString(R.string.room_notification_running))
+                .build()
+        }
+    }
+
+}

+ 12 - 0
module/room/src/main/res/layout/activity_room.xml

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

+ 3 - 0
module/room/src/main/res/values-in/strings.xml

@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+</resources>

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

@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+</resources>

+ 4 - 0
module/room/src/main/res/values/attrs.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+</resources>

+ 3 - 0
module/room/src/main/res/values/colors.xml

@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+</resources>

+ 3 - 0
module/room/src/main/res/values/dimens.xml

@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+</resources>

+ 102 - 0
module/room/src/main/res/values/ids.xml

@@ -0,0 +1,102 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+    <!--自定义id统一在前面增加id_前缀-->
+    <item name="id_mic_seat_avatar_decor" type="id" />
+    <item name="id_mic_seat_avatar_frame_decor" type="id" />
+    <item name="id_mic_seat_mic_anim" type="id" />
+    <item name="id_mic_seat_mute_decor" type="id" />
+    <item name="id_mic_seat_name_decor" type="id" />
+    <item name="id_mic_seat_speaking_ripple_decor" type="id" />
+    <item name="id_mic_seat_emotion" type="id" />
+    <item name="id_mic_seat_pk_halo_decor" type="id" />
+    <item name="id_mic_seat_pk_mvp_halo_decor" type="id" />
+    <item name="id_mic_seat_pk_mvp_label_decor" type="id" />
+    <item name="id_room_unlock" type="id" />
+    <item name="id_room_change_password" type="id" />
+    <item name="id_mic_seat_diamonds_decor" type="id" />
+    <item name="id_mic_seat_gift_wish_decor" type="id" />
+    <item name="id_mic_seat_bg_decor" type="id" />
+    <item name="id_mic_seat_wedding_identity_decor" type="id" />
+    <item name="id_mic_seat_speaking_normal_ripple_view" type="id" />
+    <item name="id_mic_seat_speaking_svip_ripple_view" type="id" />
+    <item name="id_mic_seat_cp_effect_decor" type="id" />
+    <item name="id_mic_seat_intimacy_effect_decor" type="id" />
+    <item name="id_mic_seat_decoration_decor" type="id" />
+    <item name="id_mic_seat_avatar_decoration_decor" type="id" />
+
+    <item name="id_betting_pk_entrance_view" type="id" />
+    <item name="id_mic_seat_mic_grab_prepare" type="id" />
+    <item name="id_mic_seat_mic_grab_index" type="id" />
+    <item name="id_mic_seat_mic_grab_avatar" type="id" />
+    <item name="id_mic_seat_mic_grab_rank" type="id" />
+    <item name="id_mic_seat_index_decor" type="id" />
+
+    <item name="id_mic_seat_mic_sofa_decor" type="id" />
+
+    <item name="id_mic_seat_game_remove_user_decor" type="id" />
+    <item name="id_mic_seat_game_role_decor" type="id" />
+    <item name="id_mic_seat_game_cloak_decor" type="id" />
+    <item name="id_mic_seat_game_avatar_frame_decor" type="id" />
+
+    <item name="edge_flash_effect_view_image" type="id" />
+    <item name="edge_flash_effect_view_svga" type="id" />
+    <item name="edge_flash_effect_view_vap" type="id" />
+    <item name="tag_msg_bubble_bg" type="id" />
+
+    <item name="id_room_bg_type_img" type="id" />
+    <item name="id_room_bg_type_svga" type="id" />
+    <item name="id_room_bg_type_mp4" type="id" />
+
+    <!-- 语聊房控件id -->
+    <item name="id_chat_room_gift_effect_view" type="id" />
+    <item name="id_chat_room_gift_flow_light_effect_view" type="id" />
+    <item name="id_chat_room_gift_notice_view" type="id" />
+    <item name="id_chat_room_gift_falling_down_view" type="id" />
+    <item name="id_chat_room_gift_banner_view" type="id" />
+    <item name="id_chat_room_gift_static_effect_view" type="id" />
+    <item name="id_chat_room_gift_combo_view" type="id" />
+
+    <item name="id_chat_room_car_effect_view" type="id" />
+    <item name="id_chat_room_car_edge_flash_effect_view" type="id" />
+
+    <item name="id_chat_room_task_view" type="id" />
+    <item name="id_chat_room_recharge_daily_view" type="id" />
+
+    <item name="id_chat_room_activity_view" type="id" />
+    <item name="id_chat_room_activity_timer_view" type="id" />
+
+    <item name="id_chat_room_game_view" type="id" />
+    <item name="id_chat_room_game_state_view" type="id" />
+
+    <item name="id_chat_room_music_view" type="id" />
+    <item name="id_chat_room_red_packet_view" type="id" />
+
+    <item name="id_chat_room_room_pk_view" type="id" />
+    <item name="id_chat_room_room_pk_anim_view" type="id" />
+
+    <item name="id_chat_room_mic_charm_pk_view" type="id" />
+    <item name="id_chat_room_rank_entrance_view" type="id" />
+    <item name="id_chat_room_reward_view" type="id" />
+
+    <item name="id_chat_room_danmaku_view" type="id" />
+    <item name="id_chat_room_treasure_gift_view" type="id" />
+
+    <item name="id_chat_room_headline_view" type="id" />
+
+    <item name="id_chat_room_enter_effect_view" type="id" />
+
+    <item name="id_chat_room_online_member_view" type="id" />
+
+
+    <!-- 房间底部操作栏 -->
+    <item name="id_bottom_operate_speaker" type="id" />
+    <item name="id_bottom_operate_mic" type="id" />
+    <item name="id_bottom_operate_emoji" type="id" />
+    <item name="id_bottom_operate_gift" type="id" />
+    <item name="id_bottom_operate_message" type="id" />
+    <item name="id_bottom_operate_play_center" type="id" />
+    <item name="id_bottom_operate_game_entrance" type="id" />
+
+    <item name="id_chat_image_preview_save" type="id"/>
+</resources>

+ 8 - 0
module/room/src/main/res/values/strings.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="room_chat">Chat Room</string>
+
+    <string name="room_notification_running">%s room is running now</string>
+    <string name="room_notification_back">Click back to the room</string>
+    <string name="room_channel_name">Room</string>
+</resources>

+ 4 - 0
module/room/src/main/res/values/styles.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+</resources>

+ 3 - 0
module/room/src/main/res/values/tags.xml

@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+</resources>

+ 1 - 0
module/room/src/main/resources/META-INF/services/com.adealink.weparty.module.room.IRoomService

@@ -0,0 +1 @@
+com.adealink.weparty.room.RoomServiceImpl

+ 17 - 0
module/room/src/test/java/com/adealink/weparty/room/ExampleUnitTest.kt

@@ -0,0 +1,17 @@
+package com.adealink.weparty.room
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+    @Test
+    fun addition_isCorrect() {
+        assertEquals(4, 2 + 2)
+    }
+}

+ 0 - 1
patch/xcrash/native-dump-trace.patch

@@ -1,5 +1,4 @@
 From 0ddaddce2d4a7a61f3bf5adf9fb3d2a7a51e28e1 Mon Sep 17 00:00:00 2001
-From: sunxiaodong <sunxiaodongme@gmail.com>
 Date: Mon, 21 Nov 2022 16:13:12 +0800
 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0nativeDumpTrace?=
 MIME-Version: 1.0

+ 1 - 0
settings.gradle

@@ -92,6 +92,7 @@ include ':module:share'
 include ':module:image'
 include ':module:joinus'
 include ':module:call'
+include ':module:room'
 
 //调试frame框架
 //include ':frame:network'