XuqmGroup-AndroidSDK/sdk-im/src/main/java/com/xuqm/sdk/im/ImSDK.kt
XuqmGroup 947972369c docs(testing): 添加测试文档与修复API分页问题
- 修复ImApi中历史消息接口返回类型从List改为PageResult
- 修复ImSDK中fetchHistory和fetchGroupHistory方法获取数据方式
- 重构ChatScreen中Column布局结构以解决UI问题
- 添加BUG_TRACKER.md记录已修复问题和开放问题
- 添加TEST_PLAN.md定义完整的测试策略和用例
- 添加TEST_PROGRESS.md跟踪各模块测试执行情况
- 为Python SDK添加crypto相关单元测试
- 移除CameraAction枚举未使用代码
2026-04-30 15:42:04 +08:00

787 行
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.appKey, EditMessageRequest(content)).data ?: throw IllegalStateException("edit message failed") }
suspend fun revokeMessage(messageId: String): ImMessage =
withContext(Dispatchers.IO) { api.revokeMessage(messageId, XuqmSDK.appKey).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.appKey,
msgType,
keyword,
startTime?.toString(),
endTime?.toString(),
page,
size,
).data?.content ?: 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.appKey,
msgType,
keyword,
startTime?.toString(),
endTime?.toString(),
page,
size,
).data?.content ?: 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.appKey).data ?: emptyList() }
suspend fun createGroup(name: String, memberIds: List<String>, groupType: String = "WORK"): ImGroup? =
withContext(Dispatchers.IO) {
api.createGroup(XuqmSDK.appKey, CreateGroupRequest(name, memberIds, groupType)).data
}
suspend fun getGroupInfo(groupId: String): ImGroup? =
withContext(Dispatchers.IO) { api.getGroupInfo(groupId).data }
suspend fun listGroupMembers(groupId: String): List<UserProfile> =
withContext(Dispatchers.IO) { api.listGroupMembers(groupId, XuqmSDK.appKey).data ?: emptyList() }
suspend fun searchGroupMembers(groupId: String, keyword: String, size: Int = 20): List<UserProfile> =
withContext(Dispatchers.IO) { api.searchGroupMembers(groupId, XuqmSDK.appKey, keyword, size).data ?: emptyList() }
suspend fun listPublicGroups(keyword: String? = null): List<ImGroup> =
withContext(Dispatchers.IO) { api.listPublicGroups(XuqmSDK.appKey, keyword).data ?: emptyList() }
suspend fun searchUsers(keyword: String, size: Int = 20): List<UserProfile> =
withContext(Dispatchers.IO) { api.searchUsers(XuqmSDK.appKey, keyword, size).data ?: emptyList() }
suspend fun searchGroups(keyword: String, size: Int = 20): List<ImGroup> =
withContext(Dispatchers.IO) { api.searchGroups(XuqmSDK.appKey, 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.appKey,
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.appKey, remark).data }
suspend fun listGroupJoinRequests(groupId: String): List<GroupJoinRequest> =
withContext(Dispatchers.IO) { api.listGroupJoinRequests(groupId, XuqmSDK.appKey).data ?: emptyList() }
suspend fun acceptGroupJoinRequest(groupId: String, requestId: String): GroupJoinRequest? =
withContext(Dispatchers.IO) { api.acceptGroupJoinRequest(groupId, requestId, XuqmSDK.appKey).data }
suspend fun rejectGroupJoinRequest(groupId: String, requestId: String): GroupJoinRequest? =
withContext(Dispatchers.IO) { api.rejectGroupJoinRequest(groupId, requestId, XuqmSDK.appKey).data }
suspend fun listFriends(): List<String> =
withContext(Dispatchers.IO) { api.listFriends(XuqmSDK.appKey).data ?: emptyList() }
suspend fun addFriend(friendId: String) =
withContext(Dispatchers.IO) { api.addFriend(XuqmSDK.appKey, friendId) }
suspend fun removeFriend(friendId: String) =
withContext(Dispatchers.IO) { api.removeFriend(friendId, XuqmSDK.appKey) }
suspend fun listFriendRequests(direction: String = "incoming"): List<FriendRequest> =
withContext(Dispatchers.IO) { api.listFriendRequests(XuqmSDK.appKey, direction).data ?: emptyList() }
suspend fun sendFriendRequest(friendId: String, remark: String? = null): FriendRequest? =
withContext(Dispatchers.IO) { api.sendFriendRequest(XuqmSDK.appKey, friendId, remark).data }
suspend fun acceptFriendRequest(requestId: String): FriendRequest? =
withContext(Dispatchers.IO) { api.acceptFriendRequest(requestId, XuqmSDK.appKey).data }
suspend fun rejectFriendRequest(requestId: String): FriendRequest? =
withContext(Dispatchers.IO) { api.rejectFriendRequest(requestId, XuqmSDK.appKey).data }
suspend fun listBlacklist(): List<BlacklistEntry> =
withContext(Dispatchers.IO) { api.listBlacklist(XuqmSDK.appKey).data ?: emptyList() }
suspend fun addToBlacklist(blockedUserId: String): BlacklistEntry? =
withContext(Dispatchers.IO) { api.addToBlacklist(XuqmSDK.appKey, blockedUserId).data }
suspend fun removeFromBlacklist(blockedUserId: String) =
withContext(Dispatchers.IO) { api.removeFromBlacklist(XuqmSDK.appKey, blockedUserId) }
suspend fun getProfile(userId: String) =
withContext(Dispatchers.IO) { api.getProfile(userId, XuqmSDK.appKey).data }
suspend fun updateProfile(
userId: String,
nickname: String? = null,
avatar: String? = null,
gender: String? = null,
) = withContext(Dispatchers.IO) {
api.updateProfile(userId, XuqmSDK.appKey, nickname, avatar, gender).data
}
suspend fun listConversations(): List<ConversationData> =
withContext(Dispatchers.IO) { api.listConversations(XuqmSDK.appKey).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.appKey, chatType) }
suspend fun setDraft(targetId: String, chatType: String, draft: String) =
withContext(Dispatchers.IO) { api.setDraft(targetId, XuqmSDK.appKey, chatType, draft) }
suspend fun deleteConversation(targetId: String, chatType: String) =
withContext(Dispatchers.IO) { api.deleteConversation(targetId, XuqmSDK.appKey, 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.appKey)
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.appKey,
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.appKey)
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")
}
}
}