|
|
|
|
@ -47,6 +47,10 @@ import kotlinx.serialization.json.JsonObject
|
|
|
|
|
import kotlinx.serialization.json.decodeFromJsonElement
|
|
|
|
|
import kotlinx.serialization.json.encodeToJsonElement
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 单例管理类,负责整个聊天会话的生命周期、网络连接、消息收发、状态维护和持久化。
|
|
|
|
|
* 所有公开方法均通过 ViewModel 代理调用,内部使用协程处理异步操作。
|
|
|
|
|
*/
|
|
|
|
|
object ChatSessionManager {
|
|
|
|
|
|
|
|
|
|
private val json = Json {
|
|
|
|
|
@ -60,29 +64,34 @@ object ChatSessionManager {
|
|
|
|
|
private val socketClient = OnlineMsgSocketClient()
|
|
|
|
|
private var initialized = false
|
|
|
|
|
|
|
|
|
|
// 状态流,供 UI 层订阅
|
|
|
|
|
private val _uiState = MutableStateFlow(ChatUiState())
|
|
|
|
|
val uiState = _uiState.asStateFlow()
|
|
|
|
|
|
|
|
|
|
// 事件流(如 Snackbar 消息)
|
|
|
|
|
private val _events = MutableSharedFlow<UiEvent>()
|
|
|
|
|
val events = _events.asSharedFlow()
|
|
|
|
|
|
|
|
|
|
// 用于线程安全地访问本地身份
|
|
|
|
|
private val identityMutex = Mutex()
|
|
|
|
|
private var identity: RsaCryptoManager.Identity? = null
|
|
|
|
|
|
|
|
|
|
private var manualClose = false
|
|
|
|
|
private var fallbackTried = false
|
|
|
|
|
private var connectedUrl = ""
|
|
|
|
|
private var serverPublicKey = ""
|
|
|
|
|
private var helloTimeoutJob: Job? = null
|
|
|
|
|
private var authTimeoutJob: Job? = null
|
|
|
|
|
private var reconnectJob: Job? = null
|
|
|
|
|
private var reconnectAttempt: Int = 0
|
|
|
|
|
private val systemMessageExpiryJobs: MutableMap<String, Job> = mutableMapOf()
|
|
|
|
|
// 连接相关内部状态
|
|
|
|
|
private var manualClose = false // 是否为手动断开
|
|
|
|
|
private var fallbackTried = false // 是否已尝试切换 ws/wss
|
|
|
|
|
private var connectedUrl = "" // 当前连接的服务器地址
|
|
|
|
|
private var serverPublicKey = "" // 服务端公钥(握手后获得)
|
|
|
|
|
private var helloTimeoutJob: Job? = null // 握手超时任务
|
|
|
|
|
private var authTimeoutJob: Job? = null // 认证超时任务
|
|
|
|
|
private var reconnectJob: Job? = null // 自动重连任务
|
|
|
|
|
private var reconnectAttempt: Int = 0 // 当前重连尝试次数
|
|
|
|
|
private val systemMessageExpiryJobs: MutableMap<String, Job> = mutableMapOf() // 系统消息自动过期任务
|
|
|
|
|
private var autoReconnectTriggered = false
|
|
|
|
|
@Volatile
|
|
|
|
|
private var keepAliveRequested = false
|
|
|
|
|
private var keepAliveRequested = false // 是否应保活(前台服务标志)
|
|
|
|
|
private var notificationIdSeed = 2000
|
|
|
|
|
|
|
|
|
|
// WebSocket 事件监听器
|
|
|
|
|
private val socketListener = object : OnlineMsgSocketClient.Listener {
|
|
|
|
|
override fun onOpen() {
|
|
|
|
|
scope.launch {
|
|
|
|
|
@ -151,6 +160,10 @@ object ChatSessionManager {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 初始化管理器,必须在应用启动时调用一次。
|
|
|
|
|
* @param application Application 实例
|
|
|
|
|
*/
|
|
|
|
|
@Synchronized
|
|
|
|
|
fun initialize(application: Application) {
|
|
|
|
|
if (initialized) return
|
|
|
|
|
@ -169,9 +182,12 @@ object ChatSessionManager {
|
|
|
|
|
serverUrls = pref.serverUrls,
|
|
|
|
|
serverUrl = pref.currentServerUrl,
|
|
|
|
|
directMode = pref.directMode,
|
|
|
|
|
showSystemMessages = pref.showSystemMessages
|
|
|
|
|
showSystemMessages = pref.showSystemMessages,
|
|
|
|
|
themeId = pref.themeId, // 假设 preferences 中有 themeId
|
|
|
|
|
useDynamicColor = pref.useDynamicColor
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
// 如果上次会话启用了自动重连,则自动恢复连接
|
|
|
|
|
if (pref.shouldAutoReconnect && !autoReconnectTriggered) {
|
|
|
|
|
autoReconnectTriggered = true
|
|
|
|
|
ChatForegroundService.start(application)
|
|
|
|
|
@ -180,6 +196,32 @@ object ChatSessionManager {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 更新主题
|
|
|
|
|
* @param themeId 主题名
|
|
|
|
|
*/
|
|
|
|
|
fun updateTheme(themeId: String) {
|
|
|
|
|
_uiState.update { it.copy(themeId = themeId) }
|
|
|
|
|
scope.launch {
|
|
|
|
|
preferencesRepository.setThemeId(themeId)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 更改使用动态颜色
|
|
|
|
|
* @param enabled 主题名
|
|
|
|
|
*/
|
|
|
|
|
fun updateUseDynamicColor(enabled: Boolean) {
|
|
|
|
|
_uiState.update { it.copy(useDynamicColor = enabled) }
|
|
|
|
|
scope.launch {
|
|
|
|
|
preferencesRepository.setUseDynamicColor(enabled)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 更新显示名称并持久化。
|
|
|
|
|
* @param value 新名称(自动截断至 64 字符)
|
|
|
|
|
*/
|
|
|
|
|
fun updateDisplayName(value: String) {
|
|
|
|
|
val displayName = value.take(64)
|
|
|
|
|
_uiState.update { it.copy(displayName = displayName) }
|
|
|
|
|
@ -188,18 +230,34 @@ object ChatSessionManager {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 更新当前输入的服务器地址(不持久化)。
|
|
|
|
|
* @param value 新地址
|
|
|
|
|
*/
|
|
|
|
|
fun updateServerUrl(value: String) {
|
|
|
|
|
_uiState.update { it.copy(serverUrl = value) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 更新私聊目标公钥。
|
|
|
|
|
* @param value 公钥字符串
|
|
|
|
|
*/
|
|
|
|
|
fun updateTargetKey(value: String) {
|
|
|
|
|
_uiState.update { it.copy(targetKey = value) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 更新消息草稿。
|
|
|
|
|
* @param value 草稿内容
|
|
|
|
|
*/
|
|
|
|
|
fun updateDraft(value: String) {
|
|
|
|
|
_uiState.update { it.copy(draft = value) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 切换广播/私聊模式并持久化。
|
|
|
|
|
* @param enabled true 为私聊模式
|
|
|
|
|
*/
|
|
|
|
|
fun toggleDirectMode(enabled: Boolean) {
|
|
|
|
|
_uiState.update { it.copy(directMode = enabled) }
|
|
|
|
|
scope.launch {
|
|
|
|
|
@ -207,6 +265,10 @@ object ChatSessionManager {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 切换是否显示系统消息并持久化。
|
|
|
|
|
* @param show true 显示
|
|
|
|
|
*/
|
|
|
|
|
fun toggleShowSystemMessages(show: Boolean) {
|
|
|
|
|
_uiState.update { it.copy(showSystemMessages = show) }
|
|
|
|
|
scope.launch {
|
|
|
|
|
@ -214,11 +276,17 @@ object ChatSessionManager {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 清空所有消息,并取消系统消息的过期任务。
|
|
|
|
|
*/
|
|
|
|
|
fun clearMessages() {
|
|
|
|
|
cancelSystemMessageExpiryJobs()
|
|
|
|
|
_uiState.update { it.copy(messages = emptyList()) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 保存当前服务器地址到历史列表并持久化。
|
|
|
|
|
*/
|
|
|
|
|
fun saveCurrentServerUrl() {
|
|
|
|
|
val normalized = ServerUrlFormatter.normalize(_uiState.value.serverUrl)
|
|
|
|
|
if (normalized.isBlank()) {
|
|
|
|
|
@ -243,6 +311,10 @@ object ChatSessionManager {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 从历史列表中移除当前服务器地址。
|
|
|
|
|
* 如果列表清空则恢复默认地址。
|
|
|
|
|
*/
|
|
|
|
|
fun removeCurrentServerUrl() {
|
|
|
|
|
val normalized = ServerUrlFormatter.normalize(_uiState.value.serverUrl)
|
|
|
|
|
if (normalized.isBlank()) return
|
|
|
|
|
@ -268,6 +340,9 @@ object ChatSessionManager {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 加载或生成本地身份密钥对,并将公钥显示到 UI。
|
|
|
|
|
*/
|
|
|
|
|
fun revealMyPublicKey() {
|
|
|
|
|
scope.launch {
|
|
|
|
|
_uiState.update { it.copy(loadingPublicKey = true) }
|
|
|
|
|
@ -287,10 +362,17 @@ object ChatSessionManager {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 主动连接服务器(由用户点击连接触发)。
|
|
|
|
|
*/
|
|
|
|
|
fun connect() {
|
|
|
|
|
connectInternal(isAutoRestore = false)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 内部连接逻辑,区分自动恢复和手动连接。
|
|
|
|
|
* @param isAutoRestore 是否为应用启动时的自动恢复连接
|
|
|
|
|
*/
|
|
|
|
|
private fun connectInternal(isAutoRestore: Boolean) {
|
|
|
|
|
if (!initialized) return
|
|
|
|
|
val state = _uiState.value
|
|
|
|
|
@ -339,6 +421,10 @@ object ChatSessionManager {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 主动断开连接。
|
|
|
|
|
* @param stopService 是否同时停止前台服务(默认 true)
|
|
|
|
|
*/
|
|
|
|
|
fun disconnect(stopService: Boolean = true) {
|
|
|
|
|
manualClose = true
|
|
|
|
|
cancelReconnect()
|
|
|
|
|
@ -362,6 +448,10 @@ object ChatSessionManager {
|
|
|
|
|
addSystemMessage("已断开连接")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 发送消息(广播或私聊)。
|
|
|
|
|
* 执行签名、加密并发送。
|
|
|
|
|
*/
|
|
|
|
|
fun sendMessage() {
|
|
|
|
|
val current = _uiState.value
|
|
|
|
|
if (!current.canSend) return
|
|
|
|
|
@ -419,12 +509,19 @@ object ChatSessionManager {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 消息复制成功后的回调,显示“已复制”提示。
|
|
|
|
|
*/
|
|
|
|
|
fun onMessageCopied() {
|
|
|
|
|
scope.launch {
|
|
|
|
|
_events.emit(UiEvent.ShowSnackbar("已复制"))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 确保本地身份已加载或创建。
|
|
|
|
|
* @return 本地身份对象
|
|
|
|
|
*/
|
|
|
|
|
private suspend fun ensureIdentity(): RsaCryptoManager.Identity {
|
|
|
|
|
return identityMutex.withLock {
|
|
|
|
|
identity ?: withContext(Dispatchers.Default) {
|
|
|
|
|
@ -435,6 +532,10 @@ object ChatSessionManager {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 处理收到的原始文本消息(可能是握手包或加密消息)。
|
|
|
|
|
* @param rawText 原始文本
|
|
|
|
|
*/
|
|
|
|
|
private suspend fun handleIncomingMessage(rawText: String) {
|
|
|
|
|
if (_uiState.value.status == ConnectionStatus.HANDSHAKING) {
|
|
|
|
|
_uiState.update { it.copy(statusHint = "已收到握手数据,正在解析...") }
|
|
|
|
|
@ -445,7 +546,7 @@ object ChatSessionManager {
|
|
|
|
|
json.decodeFromString<JsonElement>(normalizedText) as? JsonObject
|
|
|
|
|
}.getOrNull()
|
|
|
|
|
|
|
|
|
|
// 兼容某些代理/中间层直接转发 hello data 对象(没有 envelope 外层)
|
|
|
|
|
// 尝试直接解析为 HelloDataDto(某些服务器可能直接发送,不带外层)
|
|
|
|
|
val directHello = rootObject?.let { obj ->
|
|
|
|
|
val hasPublicKey = obj["publicKey"] != null
|
|
|
|
|
val hasChallenge = obj["authChallenge"] != null
|
|
|
|
|
@ -461,6 +562,7 @@ object ChatSessionManager {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 尝试解析为带外层的 EnvelopeDto
|
|
|
|
|
val plain = runCatching { json.decodeFromString<EnvelopeDto>(normalizedText) }.getOrNull()
|
|
|
|
|
if (plain?.type == "publickey") {
|
|
|
|
|
cancelHelloTimeout()
|
|
|
|
|
@ -480,6 +582,7 @@ object ChatSessionManager {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 握手阶段收到非预期消息则报错
|
|
|
|
|
if (_uiState.value.status == ConnectionStatus.HANDSHAKING && plain != null) {
|
|
|
|
|
_uiState.update { it.copy(statusHint = "握手失败:收到非预期消息") }
|
|
|
|
|
addSystemMessage("握手阶段收到非预期消息类型:${plain.type}")
|
|
|
|
|
@ -492,6 +595,7 @@ object ChatSessionManager {
|
|
|
|
|
addSystemMessage("握手包解析失败:$preview")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 尝试解密(若已握手完成,收到的应是加密消息)
|
|
|
|
|
val id = ensureIdentity()
|
|
|
|
|
val decrypted = runCatching {
|
|
|
|
|
withContext(Dispatchers.Default) {
|
|
|
|
|
@ -509,6 +613,10 @@ object ChatSessionManager {
|
|
|
|
|
handleSecureMessage(secure)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 处理服务端发来的握手 Hello 数据。
|
|
|
|
|
* @param hello 服务端公钥和挑战
|
|
|
|
|
*/
|
|
|
|
|
private suspend fun handleServerHello(hello: HelloDataDto) {
|
|
|
|
|
cancelHelloTimeout()
|
|
|
|
|
serverPublicKey = hello.publicKey
|
|
|
|
|
@ -552,6 +660,10 @@ object ChatSessionManager {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 发送认证消息(包含签名后的身份信息)。
|
|
|
|
|
* @param challenge 服务端提供的挑战值
|
|
|
|
|
*/
|
|
|
|
|
private suspend fun sendAuth(challenge: String) {
|
|
|
|
|
val id = ensureIdentity()
|
|
|
|
|
val displayName = _uiState.value.displayName.trim().ifBlank { createGuestName() }
|
|
|
|
|
@ -595,6 +707,10 @@ object ChatSessionManager {
|
|
|
|
|
check(socketClient.send(cipher)) { "连接不可用" }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 处理安全通道建立后的业务消息(广播、私聊、认证结果等)。
|
|
|
|
|
* @param message 解密后的 EnvelopeDto
|
|
|
|
|
*/
|
|
|
|
|
private fun handleSecureMessage(message: EnvelopeDto) {
|
|
|
|
|
when (message.type) {
|
|
|
|
|
"auth_ok" -> {
|
|
|
|
|
@ -634,6 +750,11 @@ object ChatSessionManager {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 处理 WebSocket 连接关闭事件。
|
|
|
|
|
* @param code 关闭状态码
|
|
|
|
|
* @param reason 关闭原因
|
|
|
|
|
*/
|
|
|
|
|
private fun handleSocketClosed(code: Int, reason: String) {
|
|
|
|
|
cancelHelloTimeout()
|
|
|
|
|
cancelAuthTimeout()
|
|
|
|
|
@ -652,6 +773,7 @@ object ChatSessionManager {
|
|
|
|
|
|
|
|
|
|
val allowFallback = !fallbackTried && currentStatus != ConnectionStatus.READY
|
|
|
|
|
|
|
|
|
|
// 尝试切换 ws/wss 协议重试(仅限非就绪状态)
|
|
|
|
|
if (allowFallback) {
|
|
|
|
|
val fallbackUrl = ServerUrlFormatter.toggleWsProtocol(connectedUrl)
|
|
|
|
|
if (fallbackUrl.isNotBlank()) {
|
|
|
|
|
@ -680,6 +802,10 @@ object ChatSessionManager {
|
|
|
|
|
scheduleReconnect("连接已中断")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 添加一条系统消息(自动按 TTL 过期)。
|
|
|
|
|
* @param content 消息内容
|
|
|
|
|
*/
|
|
|
|
|
private fun addSystemMessage(content: String) {
|
|
|
|
|
val message = UiMessage(
|
|
|
|
|
role = MessageRole.SYSTEM,
|
|
|
|
|
@ -692,6 +818,13 @@ object ChatSessionManager {
|
|
|
|
|
scheduleSystemMessageExpiry(message.id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 添加一条接收到的用户消息。
|
|
|
|
|
* @param sender 发送者名称
|
|
|
|
|
* @param subtitle 附加说明(如私聊来源)
|
|
|
|
|
* @param content 消息内容
|
|
|
|
|
* @param channel 消息通道(广播/私聊)
|
|
|
|
|
*/
|
|
|
|
|
private fun addIncomingMessage(
|
|
|
|
|
sender: String,
|
|
|
|
|
subtitle: String,
|
|
|
|
|
@ -713,6 +846,12 @@ object ChatSessionManager {
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 添加一条发出的消息。
|
|
|
|
|
* @param content 消息内容
|
|
|
|
|
* @param subtitle 附加说明(如私聊目标)
|
|
|
|
|
* @param channel 消息通道
|
|
|
|
|
*/
|
|
|
|
|
private fun addOutgoingMessage(
|
|
|
|
|
content: String,
|
|
|
|
|
subtitle: String,
|
|
|
|
|
@ -729,6 +868,10 @@ object ChatSessionManager {
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 将消息追加到列表尾部,并清理超出数量限制的消息。
|
|
|
|
|
* @param message 要追加的消息
|
|
|
|
|
*/
|
|
|
|
|
private fun appendMessage(message: UiMessage) {
|
|
|
|
|
_uiState.update { current ->
|
|
|
|
|
val next = (current.messages + message).takeLast(MAX_MESSAGES)
|
|
|
|
|
@ -741,11 +884,18 @@ object ChatSessionManager {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 取消认证超时任务。
|
|
|
|
|
*/
|
|
|
|
|
private fun cancelAuthTimeout() {
|
|
|
|
|
authTimeoutJob?.cancel()
|
|
|
|
|
authTimeoutJob = null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 安排自动重连(指数退避)。
|
|
|
|
|
* @param reason 触发重连的原因
|
|
|
|
|
*/
|
|
|
|
|
private fun scheduleReconnect(reason: String) {
|
|
|
|
|
if (manualClose) return
|
|
|
|
|
if (reconnectJob?.isActive == true) return
|
|
|
|
|
@ -793,11 +943,18 @@ object ChatSessionManager {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 取消自动重连任务。
|
|
|
|
|
*/
|
|
|
|
|
private fun cancelReconnect() {
|
|
|
|
|
reconnectJob?.cancel()
|
|
|
|
|
reconnectJob = null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 为系统消息安排过期自动删除。
|
|
|
|
|
* @param messageId 消息唯一 ID
|
|
|
|
|
*/
|
|
|
|
|
private fun scheduleSystemMessageExpiry(messageId: String) {
|
|
|
|
|
systemMessageExpiryJobs.remove(messageId)?.cancel()
|
|
|
|
|
systemMessageExpiryJobs[messageId] = scope.launch {
|
|
|
|
|
@ -810,11 +967,17 @@ object ChatSessionManager {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 取消所有系统消息的过期任务。
|
|
|
|
|
*/
|
|
|
|
|
private fun cancelSystemMessageExpiryJobs() {
|
|
|
|
|
systemMessageExpiryJobs.values.forEach { it.cancel() }
|
|
|
|
|
systemMessageExpiryJobs.clear()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 启动握手超时计时器。
|
|
|
|
|
*/
|
|
|
|
|
private fun startHelloTimeout() {
|
|
|
|
|
cancelHelloTimeout()
|
|
|
|
|
helloTimeoutJob = scope.launch {
|
|
|
|
|
@ -833,21 +996,38 @@ object ChatSessionManager {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 取消握手超时任务。
|
|
|
|
|
*/
|
|
|
|
|
private fun cancelHelloTimeout() {
|
|
|
|
|
helloTimeoutJob?.cancel()
|
|
|
|
|
helloTimeoutJob = null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 缩写显示公钥(取前后各8字符)。
|
|
|
|
|
* @param key 完整公钥
|
|
|
|
|
* @return 缩写字符串
|
|
|
|
|
*/
|
|
|
|
|
private fun summarizeKey(key: String): String {
|
|
|
|
|
if (key.length <= 16) return key
|
|
|
|
|
return "${key.take(8)}...${key.takeLast(8)}"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 生成访客名称(如 guest-123456)。
|
|
|
|
|
* @return 随机名称
|
|
|
|
|
*/
|
|
|
|
|
private fun createGuestName(): String {
|
|
|
|
|
val rand = (100000..999999).random()
|
|
|
|
|
return "guest-$rand"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 从可能包含前缀的原始文本中提取 JSON 对象部分。
|
|
|
|
|
* @param rawText 原始文本
|
|
|
|
|
* @return 最外层的 JSON 字符串
|
|
|
|
|
*/
|
|
|
|
|
private fun extractJsonCandidate(rawText: String): String {
|
|
|
|
|
val trimmed = rawText.trim()
|
|
|
|
|
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
|
|
|
@ -863,6 +1043,9 @@ object ChatSessionManager {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 关闭所有资源(用于应用退出时)。
|
|
|
|
|
*/
|
|
|
|
|
fun shutdownAll() {
|
|
|
|
|
cancelSystemMessageExpiryJobs()
|
|
|
|
|
cancelReconnect()
|
|
|
|
|
@ -871,6 +1054,9 @@ object ChatSessionManager {
|
|
|
|
|
socketClient.shutdown()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 前台服务停止时的回调。
|
|
|
|
|
*/
|
|
|
|
|
fun onForegroundServiceStopped() {
|
|
|
|
|
keepAliveRequested = false
|
|
|
|
|
if (_uiState.value.status != ConnectionStatus.IDLE) {
|
|
|
|
|
@ -882,8 +1068,15 @@ object ChatSessionManager {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 判断前台服务是否应该运行。
|
|
|
|
|
* @return true 表示应保持服务运行
|
|
|
|
|
*/
|
|
|
|
|
fun shouldForegroundServiceRun(): Boolean = keepAliveRequested
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 创建消息通知渠道(Android O+)。
|
|
|
|
|
*/
|
|
|
|
|
private fun ensureMessageNotificationChannel() {
|
|
|
|
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
|
|
|
|
val manager = app.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
|
|
|
@ -897,6 +1090,11 @@ object ChatSessionManager {
|
|
|
|
|
manager.createNotificationChannel(channel)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 显示新消息到达的通知。
|
|
|
|
|
* @param title 通知标题
|
|
|
|
|
* @param body 通知正文
|
|
|
|
|
*/
|
|
|
|
|
private fun showIncomingNotification(title: String, body: String) {
|
|
|
|
|
if (!initialized) return
|
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
|
|
|
|
|
@ -928,16 +1126,21 @@ object ChatSessionManager {
|
|
|
|
|
NotificationManagerCompat.from(app).notify(nextMessageNotificationId(), notification)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 生成下一个通知 ID(线程安全递增)。
|
|
|
|
|
* @return 新的通知 ID
|
|
|
|
|
*/
|
|
|
|
|
@Synchronized
|
|
|
|
|
private fun nextMessageNotificationId(): Int {
|
|
|
|
|
notificationIdSeed += 1
|
|
|
|
|
return notificationIdSeed
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 常量定义
|
|
|
|
|
private const val HELLO_TIMEOUT_MS = 12_000L
|
|
|
|
|
private const val AUTH_TIMEOUT_MS = 20_000L
|
|
|
|
|
private const val MAX_MESSAGES = 500
|
|
|
|
|
private const val MAX_RECONNECT_DELAY_SECONDS = 30
|
|
|
|
|
private const val SYSTEM_MESSAGE_TTL_MS = 1_000L
|
|
|
|
|
private const val MESSAGE_CHANNEL_ID = "onlinemsg_messages"
|
|
|
|
|
}
|
|
|
|
|
}
|