XuqmGroup-AndroidSDK/sdk-im/src/main/java/com/xuqm/sdk/im/ImSDK.kt
XuqmGroup 18f4c99b71 feat(chat): 添加聊天界面视图模型和联系人管理功能
- 实现 ChatViewModel 处理消息收发、历史记录加载和状态管理
- 添加消息搜索、草稿保存、引用回复等功能
- 实现多媒体附件发送包括图片、视频、音频和文件
- 添加群组提及用户功能和消息撤回机制
- 实现联系人管理功能包括好友搜索、添加、删除和黑名单管理
- 添加好友请求处理和实时消息监听
- 实现会话列表管理包含未读消息统计和实时更新
- 集成 IM SDK 的连接状态管理和事件监听
- 添加消息状态跟踪和超时处理机制
- 实现数据缓存机制优化用户体验
2026-04-28 22:32:20 +08:00

781 行
27 KiB
Kotlin

package com.xuqm.sdk.im
import com.xuqm.sdk.XuqmLoginSession
import com.xuqm.sdk.XuqmSDK
import com.xuqm.sdk.core.ServiceEndpointRegistry
import com.xuqm.sdk.im.api.AddMemberRequest
import com.xuqm.sdk.im.api.CreateGroupRequest
import com.xuqm.sdk.im.api.ImApi
import com.xuqm.sdk.im.model.EditMessageRequest
import com.xuqm.sdk.im.api.MuteGroupMemberRequest
import com.xuqm.sdk.im.api.SetMutedRequest
import com.xuqm.sdk.im.api.SetPinnedRequest
import com.xuqm.sdk.im.api.SetGroupRoleRequest
import com.xuqm.sdk.im.api.UpdateGroupRequest
import com.xuqm.sdk.im.listener.ImEventListener
import com.xuqm.sdk.im.model.ImConnectionState
import com.xuqm.sdk.im.model.ConversationData
import com.xuqm.sdk.im.model.GroupJoinRequest
import com.xuqm.sdk.im.model.ImGroup
import com.xuqm.sdk.im.model.ImMessage
import com.xuqm.sdk.im.model.PageResult
import com.xuqm.sdk.im.model.UserProfile
import com.xuqm.sdk.im.model.BlacklistEntry
import com.xuqm.sdk.im.model.FriendRequest
import com.xuqm.sdk.file.FileUploadResult
import com.xuqm.sdk.network.ApiClient
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.json.JSONArray
import org.json.JSONObject
import java.time.LocalDateTime
import java.util.concurrent.CopyOnWriteArraySet
import java.util.UUID
object ImSDK {
private const val TAG = "XuqmImSDK"
private var client: ImClient? = null
private val api: ImApi get() = ApiClient.create(ImApi::class.java, ServiceEndpointRegistry.imApiBaseUrl)
private const val MAX_RECONNECT_ATTEMPTS = 5
private val RECONNECT_BACKOFF_MS = longArrayOf(1_000L, 2_000L, 5_000L, 10_000L, 30_000L)
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val activeGroupSubscriptions = linkedSetOf<String>()
private val listeners = CopyOnWriteArraySet<ImEventListener>()
private var reconnectJob: Job? = null
private var reconnectAttempts = 0
@Volatile private var reconnectEnabled = false
@Volatile private var currentToken: String = ""
private val connectionListener = object : ImEventListener {
override fun onConnected() {
reconnectAttempts = 0
reconnectJob?.cancel()
reconnectJob = null
_connectionState.value = ImConnectionState.Connected
resubscribeActiveGroups()
}
override fun onDisconnected(reason: String?) {
_connectionState.value = ImConnectionState.Disconnected(reason)
scheduleReconnect(reason)
}
override fun onError(error: String) {
if (error.startsWith("Parse error", ignoreCase = true)) return
_connectionState.value = ImConnectionState.Disconnected(error)
scheduleReconnect(error)
}
}
private val _connectionState = MutableStateFlow<ImConnectionState>(ImConnectionState.Disconnected("未连接"))
val connectionState: StateFlow<ImConnectionState> = _connectionState
var currentUserId: String = ""
private set
init {
XuqmSDK.currentLoginSession?.let { onSdkLogin(it) }
}
suspend fun login(
userId: String,
userSig: String,
nickname: String? = null,
avatar: String? = null,
) = withContext(Dispatchers.IO) {
XuqmSDK.requireInit()
currentUserId = userId
connectWithToken(userSig)
}
suspend fun loginWithUserSig(userId: String, userSig: String) =
withContext(Dispatchers.IO) {
XuqmSDK.requireInit()
currentUserId = userId
connectWithToken(userSig)
}
@Deprecated("Use loginWithUserSig(userId, userSig) instead.")
suspend fun loginWithToken(userId: String, token: String) =
loginWithUserSig(userId, token)
fun sendMessage(
toId: String,
chatType: String,
msgType: String,
content: String,
mentionedUserIds: String? = null,
): ImMessage {
val message = buildOutgoingMessage(
messageId = UUID.randomUUID().toString(),
toId = toId,
chatType = chatType,
msgType = msgType,
content = content,
mentionedUserIds = mentionedUserIds,
)
val sent = client?.sendMessage(
messageId = message.id,
toId = toId,
chatType = chatType,
msgType = msgType,
content = content,
mentionedUserIds = mentionedUserIds,
) == true
Log.d(TAG, "sendMessage id=${message.id} toId=$toId chatType=$chatType msgType=$msgType contentLength=${content.length} mentioned=${mentionedUserIds.orEmpty()} sent=$sent")
return if (sent) message else message.copy(status = "FAILED")
}
fun sendTextMessage(
toId: String,
chatType: String,
content: String,
mentionedUserIds: String? = null,
): ImMessage {
return sendMessage(toId, chatType, "TEXT", content, mentionedUserIds)
}
suspend fun editMessage(messageId: String, content: String): ImMessage =
withContext(Dispatchers.IO) { api.editMessage(messageId, XuqmSDK.appId, EditMessageRequest(content)).data ?: throw IllegalStateException("edit message failed") }
suspend fun revokeMessage(messageId: String): ImMessage =
withContext(Dispatchers.IO) { api.revokeMessage(messageId, XuqmSDK.appId).data ?: throw IllegalStateException("revoke message failed") }
fun sendImageMessage(
toId: String,
chatType: String,
file: FileUploadResult,
width: Int? = null,
height: Int? = null,
): ImMessage {
return sendMessage(
toId = toId,
chatType = chatType,
msgType = "IMAGE",
content = buildMediaPayload(
url = file.url,
thumbnailUrl = file.thumbnailUrl,
name = file.originalName,
mimeType = file.mimeType,
size = file.size,
hash = file.hash,
width = width,
height = height,
),
)
}
fun sendVideoMessage(
toId: String,
chatType: String,
file: FileUploadResult,
width: Int? = null,
height: Int? = null,
durationMs: Long? = null,
): ImMessage {
return sendMessage(
toId = toId,
chatType = chatType,
msgType = "VIDEO",
content = buildMediaPayload(
url = file.url,
thumbnailUrl = file.thumbnailUrl,
name = file.originalName,
mimeType = file.mimeType,
size = file.size,
hash = file.hash,
width = width,
height = height,
durationMs = durationMs,
),
)
}
fun sendFileMessage(
toId: String,
chatType: String,
file: FileUploadResult,
): ImMessage {
return sendMessage(
toId = toId,
chatType = chatType,
msgType = "FILE",
content = buildMediaPayload(
url = file.url,
name = file.originalName,
mimeType = file.mimeType,
size = file.size,
hash = file.hash,
),
)
}
fun sendAudioMessage(
toId: String,
chatType: String,
file: FileUploadResult,
durationMs: Long? = null,
): ImMessage {
return sendMessage(
toId = toId,
chatType = chatType,
msgType = "AUDIO",
content = buildMediaPayload(
url = file.url,
name = file.originalName,
mimeType = file.mimeType,
size = file.size,
hash = file.hash,
durationMs = durationMs,
),
)
}
fun sendLocationMessage(
toId: String,
chatType: String,
latitude: Double,
longitude: Double,
title: String? = null,
address: String? = null,
): ImMessage {
return sendMessage(
toId = toId,
chatType = chatType,
msgType = "LOCATION",
content = JSONObject().apply {
put("lat", latitude)
put("lng", longitude)
title?.takeIf { it.isNotBlank() }?.let { put("title", it) }
address?.takeIf { it.isNotBlank() }?.let { put("address", it) }
}.toString(),
)
}
fun sendCustomMessage(
toId: String,
chatType: String,
data: String,
): ImMessage {
return sendMessage(
toId = toId,
chatType = chatType,
msgType = "CUSTOM",
content = JSONObject().apply {
put("data", data)
}.toString(),
)
}
fun sendRichTextMessage(
toId: String,
chatType: String,
html: String,
): ImMessage {
return sendMessage(
toId = toId,
chatType = chatType,
msgType = "RICH_TEXT",
content = JSONObject().apply {
put("html", html)
}.toString(),
)
}
fun sendForwardMessage(
toId: String,
chatType: String,
originalSender: String,
originalContent: String,
): ImMessage {
return sendMessage(
toId = toId,
chatType = chatType,
msgType = "FORWARD",
content = JSONObject().apply {
put("originalSender", originalSender)
put("originalContent", originalContent)
}.toString(),
)
}
fun sendCallAudioMessage(
toId: String,
chatType: String,
action: String,
): ImMessage {
return sendCallSignalMessage(toId, chatType, "CALL_AUDIO", action)
}
fun sendCallVideoMessage(
toId: String,
chatType: String,
action: String,
): ImMessage {
return sendCallSignalMessage(toId, chatType, "CALL_VIDEO", action)
}
fun sendNotifyMessage(
toId: String,
chatType: String,
title: String,
content: String,
): ImMessage {
return sendMessage(
toId = toId,
chatType = chatType,
msgType = "NOTIFY",
content = JSONObject().apply {
put("title", title)
put("content", content)
}.toString(),
)
}
fun sendQuoteMessage(
toId: String,
chatType: String,
quotedMsgId: String,
quotedContent: String,
text: String,
): ImMessage {
return sendMessage(
toId = toId,
chatType = chatType,
msgType = "QUOTE",
content = JSONObject().apply {
put("quotedMsgId", quotedMsgId)
put("quotedContent", quotedContent)
put("text", text)
}.toString(),
)
}
fun sendMergeMessage(
toId: String,
chatType: String,
title: String,
msgList: List<String>,
): ImMessage {
return sendMessage(
toId = toId,
chatType = chatType,
msgType = "MERGE",
content = JSONObject().apply {
put("title", title)
put("msgList", JSONArray(msgList))
}.toString(),
)
}
fun subscribeGroup(groupId: String) {
Log.d(TAG, "subscribeGroup groupId=$groupId")
synchronized(activeGroupSubscriptions) {
activeGroupSubscriptions.add(groupId)
}
client?.subscribe("/topic/group/$groupId")
}
fun unsubscribeGroup(groupId: String) {
Log.d(TAG, "unsubscribeGroup groupId=$groupId")
synchronized(activeGroupSubscriptions) {
activeGroupSubscriptions.remove(groupId)
}
client?.unsubscribe("/topic/group/$groupId")
}
suspend fun fetchHistory(toId: String, page: Int = 0, size: Int = 20): List<ImMessage> =
fetchHistoryWithFilters(toId, page, size)
suspend fun fetchHistoryWithFilters(
toId: String,
page: Int = 0,
size: Int = 20,
msgType: String? = null,
keyword: String? = null,
startTime: LocalDateTime? = null,
endTime: LocalDateTime? = null,
): List<ImMessage> =
withContext(Dispatchers.IO) {
api.fetchHistory(
toId,
XuqmSDK.appId,
msgType,
keyword,
startTime?.toString(),
endTime?.toString(),
page,
size,
).data ?: emptyList()
}
suspend fun fetchGroupHistory(groupId: String, page: Int = 0, size: Int = 20): List<ImMessage> =
fetchGroupHistoryWithFilters(groupId, page, size)
suspend fun fetchGroupHistoryWithFilters(
groupId: String,
page: Int = 0,
size: Int = 20,
msgType: String? = null,
keyword: String? = null,
startTime: LocalDateTime? = null,
endTime: LocalDateTime? = null,
): List<ImMessage> =
withContext(Dispatchers.IO) {
api.fetchGroupHistory(
groupId,
XuqmSDK.appId,
msgType,
keyword,
startTime?.toString(),
endTime?.toString(),
page,
size,
).data ?: emptyList()
}
suspend fun locateHistoryPage(
toId: String,
messageId: String,
pageSize: Int = 20,
maxPages: Int = 20,
): List<ImMessage>? = locatePage(
maxPages = maxPages,
loadPage = { page -> fetchHistory(toId, page, pageSize) },
messageId = messageId,
pageSize = pageSize,
)
suspend fun locateGroupHistoryPage(
groupId: String,
messageId: String,
pageSize: Int = 20,
maxPages: Int = 20,
): List<ImMessage>? = locatePage(
maxPages = maxPages,
loadPage = { page -> fetchGroupHistory(groupId, page, pageSize) },
messageId = messageId,
pageSize = pageSize,
)
private suspend fun locatePage(
maxPages: Int,
loadPage: suspend (Int) -> List<ImMessage>,
messageId: String,
pageSize: Int,
): List<ImMessage>? {
repeat(maxPages.coerceAtLeast(1)) { page ->
val messages = loadPage(page)
if (messages.any { it.id == messageId }) {
return messages
}
if (messages.size < pageSize) {
return null
}
}
return null
}
suspend fun listGroups(): List<ImGroup> =
withContext(Dispatchers.IO) { api.listGroups(XuqmSDK.appId).data ?: emptyList() }
suspend fun createGroup(name: String, memberIds: List<String>, groupType: String = "WORK"): ImGroup? =
withContext(Dispatchers.IO) {
api.createGroup(XuqmSDK.appId, CreateGroupRequest(name, memberIds, groupType)).data
}
suspend fun getGroupInfo(groupId: String): ImGroup? =
withContext(Dispatchers.IO) { api.getGroupInfo(groupId).data }
suspend fun listPublicGroups(keyword: String? = null): List<ImGroup> =
withContext(Dispatchers.IO) { api.listPublicGroups(XuqmSDK.appId, keyword).data ?: emptyList() }
suspend fun searchUsers(keyword: String, size: Int = 20): List<UserProfile> =
withContext(Dispatchers.IO) { api.searchUsers(XuqmSDK.appId, keyword, size).data ?: emptyList() }
suspend fun searchGroups(keyword: String, size: Int = 20): List<ImGroup> =
withContext(Dispatchers.IO) { api.searchGroups(XuqmSDK.appId, keyword, size).data ?: emptyList() }
suspend fun searchMessages(
keyword: String? = null,
chatType: String? = null,
msgType: String? = null,
startTime: LocalDateTime? = null,
endTime: LocalDateTime? = null,
page: Int = 0,
size: Int = 20,
): PageResult<ImMessage> =
withContext(Dispatchers.IO) {
api.searchMessages(
XuqmSDK.appId,
keyword,
chatType,
msgType,
startTime?.toString(),
endTime?.toString(),
page,
size,
).data ?: PageResult()
}
suspend fun updateGroupInfo(groupId: String, name: String? = null, announcement: String? = null) =
withContext(Dispatchers.IO) { api.updateGroupInfo(groupId, UpdateGroupRequest(name, announcement)) }
suspend fun addGroupMember(groupId: String, userId: String) =
withContext(Dispatchers.IO) { api.addGroupMember(groupId, AddMemberRequest(userId)) }
suspend fun removeGroupMember(groupId: String, userId: String) =
withContext(Dispatchers.IO) { api.removeGroupMember(groupId, userId) }
suspend fun leaveGroup(groupId: String) =
withContext(Dispatchers.IO) { api.removeGroupMember(groupId, currentUserId) }
suspend fun setGroupRole(groupId: String, userId: String, role: String) =
withContext(Dispatchers.IO) { api.setGroupRole(groupId, SetGroupRoleRequest(userId, role)) }
suspend fun muteGroupMember(groupId: String, userId: String, minutes: Long) =
withContext(Dispatchers.IO) { api.muteGroupMember(groupId, MuteGroupMemberRequest(userId, minutes)) }
suspend fun dismissGroup(groupId: String) =
withContext(Dispatchers.IO) { api.dismissGroup(groupId) }
suspend fun sendGroupJoinRequest(groupId: String, remark: String? = null): GroupJoinRequest? =
withContext(Dispatchers.IO) { api.sendGroupJoinRequest(groupId, XuqmSDK.appId, remark).data }
suspend fun listGroupJoinRequests(groupId: String): List<GroupJoinRequest> =
withContext(Dispatchers.IO) { api.listGroupJoinRequests(groupId, XuqmSDK.appId).data ?: emptyList() }
suspend fun acceptGroupJoinRequest(groupId: String, requestId: String): GroupJoinRequest? =
withContext(Dispatchers.IO) { api.acceptGroupJoinRequest(groupId, requestId, XuqmSDK.appId).data }
suspend fun rejectGroupJoinRequest(groupId: String, requestId: String): GroupJoinRequest? =
withContext(Dispatchers.IO) { api.rejectGroupJoinRequest(groupId, requestId, XuqmSDK.appId).data }
suspend fun listFriends(): List<String> =
withContext(Dispatchers.IO) { api.listFriends(XuqmSDK.appId).data ?: emptyList() }
suspend fun addFriend(friendId: String) =
withContext(Dispatchers.IO) { api.addFriend(XuqmSDK.appId, friendId) }
suspend fun removeFriend(friendId: String) =
withContext(Dispatchers.IO) { api.removeFriend(friendId, XuqmSDK.appId) }
suspend fun listFriendRequests(direction: String = "incoming"): List<FriendRequest> =
withContext(Dispatchers.IO) { api.listFriendRequests(XuqmSDK.appId, direction).data ?: emptyList() }
suspend fun sendFriendRequest(friendId: String, remark: String? = null): FriendRequest? =
withContext(Dispatchers.IO) { api.sendFriendRequest(XuqmSDK.appId, friendId, remark).data }
suspend fun acceptFriendRequest(requestId: String): FriendRequest? =
withContext(Dispatchers.IO) { api.acceptFriendRequest(requestId, XuqmSDK.appId).data }
suspend fun rejectFriendRequest(requestId: String): FriendRequest? =
withContext(Dispatchers.IO) { api.rejectFriendRequest(requestId, XuqmSDK.appId).data }
suspend fun listBlacklist(): List<BlacklistEntry> =
withContext(Dispatchers.IO) { api.listBlacklist(XuqmSDK.appId).data ?: emptyList() }
suspend fun addToBlacklist(blockedUserId: String): BlacklistEntry? =
withContext(Dispatchers.IO) { api.addToBlacklist(XuqmSDK.appId, blockedUserId).data }
suspend fun removeFromBlacklist(blockedUserId: String) =
withContext(Dispatchers.IO) { api.removeFromBlacklist(XuqmSDK.appId, blockedUserId) }
suspend fun getProfile(userId: String) =
withContext(Dispatchers.IO) { api.getProfile(userId, XuqmSDK.appId).data }
suspend fun updateProfile(
userId: String,
nickname: String? = null,
avatar: String? = null,
gender: String? = null,
) = withContext(Dispatchers.IO) {
api.updateProfile(userId, XuqmSDK.appId, nickname, avatar, gender).data
}
suspend fun listConversations(): List<ConversationData> =
withContext(Dispatchers.IO) { api.listConversations(XuqmSDK.appId).data ?: emptyList() }
suspend fun getTotalUnreadCount(): Int =
withContext(Dispatchers.IO) {
runCatching { listConversations().sumOf { it.unreadCount } }.getOrDefault(0)
}
suspend fun setConversationPinned(targetId: String, chatType: String, pinned: Boolean) =
withContext(Dispatchers.IO) {
api.setConversationPinned(targetId, chatType, SetPinnedRequest(pinned))
}
suspend fun setConversationMuted(targetId: String, chatType: String, muted: Boolean) =
withContext(Dispatchers.IO) {
api.setConversationMuted(targetId, chatType, SetMutedRequest(muted))
}
suspend fun markRead(targetId: String, chatType: String = "SINGLE") =
withContext(Dispatchers.IO) { api.markRead(targetId, XuqmSDK.appId, chatType) }
suspend fun setDraft(targetId: String, chatType: String, draft: String) =
withContext(Dispatchers.IO) { api.setDraft(targetId, XuqmSDK.appId, chatType, draft) }
suspend fun deleteConversation(targetId: String, chatType: String) =
withContext(Dispatchers.IO) { api.deleteConversation(targetId, XuqmSDK.appId, chatType) }
fun addListener(listener: ImEventListener) {
Log.d(TAG, "addListener listener=${listener.javaClass.name}")
listeners.add(listener)
client?.addListener(listener)
}
fun removeListener(listener: ImEventListener) {
Log.d(TAG, "removeListener listener=${listener.javaClass.name}")
listeners.remove(listener)
client?.removeListener(listener)
}
fun disconnect() {
disconnectInternal(clearTokenStore = true)
}
fun onSdkLogin(session: XuqmLoginSession) {
XuqmSDK.requireInit()
currentUserId = session.userId
connectWithToken(session.userSig)
}
fun onSdkLogout() {
disconnectInternal(clearTokenStore = false)
}
private fun connectWithToken(token: String) {
reconnectEnabled = false
reconnectJob?.cancel()
reconnectJob = null
XuqmSDK.tokenStore.saveToken(token)
client?.disconnect()
_connectionState.value = ImConnectionState.Connecting
currentToken = token
Log.d(TAG, "connectWithToken userId=$currentUserId activeGroups=${activeGroupSubscriptions.size}")
client = ImClient(ServiceEndpointRegistry.imWsUrl, token, XuqmSDK.appId)
client?.addListener(connectionListener)
listeners.forEach { client?.addListener(it) }
reconnectEnabled = true
client?.connect()
}
private fun disconnectInternal(clearTokenStore: Boolean) {
reconnectEnabled = false
reconnectJob?.cancel()
reconnectJob = null
reconnectAttempts = 0
client?.disconnect()
client = null
currentUserId = ""
currentToken = ""
synchronized(activeGroupSubscriptions) {
activeGroupSubscriptions.clear()
}
_connectionState.value = ImConnectionState.Disconnected("已断开")
if (clearTokenStore) {
XuqmSDK.tokenStore.clear()
}
}
private fun buildMediaPayload(
url: String,
thumbnailUrl: String? = null,
name: String? = null,
mimeType: String? = null,
size: Long? = null,
hash: String? = null,
width: Int? = null,
height: Int? = null,
durationMs: Long? = null,
): String = JSONObject().apply {
put("url", url)
thumbnailUrl?.takeIf { it.isNotBlank() }?.let { put("thumbnailUrl", it) }
name?.takeIf { it.isNotBlank() }?.let { put("name", it) }
mimeType?.takeIf { it.isNotBlank() }?.let { put("mimeType", it) }
size?.takeIf { it > 0 }?.let { put("size", it) }
hash?.takeIf { it.isNotBlank() }?.let { put("hash", it) }
width?.takeIf { it > 0 }?.let { put("width", it) }
height?.takeIf { it > 0 }?.let { put("height", it) }
durationMs?.takeIf { it > 0 }?.let { put("durationMs", it) }
}.toString()
private fun sendCallSignalMessage(
toId: String,
chatType: String,
msgType: String,
action: String,
): ImMessage {
return sendMessage(
toId = toId,
chatType = chatType,
msgType = msgType,
content = JSONObject().apply {
put("action", action)
}.toString(),
)
}
private fun buildOutgoingMessage(
messageId: String,
toId: String,
chatType: String,
msgType: String,
content: String,
mentionedUserIds: String? = null,
): ImMessage {
return ImMessage(
id = messageId,
appId = XuqmSDK.appId,
fromId = currentUserId,
toId = toId,
chatType = chatType,
msgType = msgType,
content = content,
status = "SENDING",
mentionedUserIds = mentionedUserIds?.takeIf { it.isNotBlank() },
createdAt = System.currentTimeMillis(),
)
}
private fun scheduleReconnect(reason: String?) {
if (!reconnectEnabled || currentToken.isBlank()) return
if (reconnectJob?.isActive == true) return
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
Log.w(TAG, "scheduleReconnect stop attempts=$reconnectAttempts reason=$reason")
return
}
val delayMs = RECONNECT_BACKOFF_MS[reconnectAttempts.coerceAtMost(RECONNECT_BACKOFF_MS.lastIndex)]
Log.w(TAG, "scheduleReconnect attempts=${reconnectAttempts + 1} delayMs=$delayMs reason=$reason")
reconnectJob = scope.launch {
delay(delayMs)
if (!reconnectEnabled || currentToken.isBlank()) return@launch
reconnectAttempts += 1
_connectionState.value = ImConnectionState.Connecting
Log.d(TAG, "reconnect attempt=$reconnectAttempts")
client = ImClient(ServiceEndpointRegistry.imWsUrl, currentToken, XuqmSDK.appId)
client?.addListener(connectionListener)
listeners.forEach { client?.addListener(it) }
client?.connect()
}
}
private fun resubscribeActiveGroups() {
val groups = synchronized(activeGroupSubscriptions) { activeGroupSubscriptions.toList() }
groups.forEach { groupId ->
client?.subscribe("/topic/group/$groupId")
}
}
}