|
|
|
@ -5,9 +5,12 @@ import android.app.Application
|
|
|
|
import android.app.NotificationChannel
|
|
|
|
import android.app.NotificationChannel
|
|
|
|
import android.app.NotificationManager
|
|
|
|
import android.app.NotificationManager
|
|
|
|
import android.app.PendingIntent
|
|
|
|
import android.app.PendingIntent
|
|
|
|
|
|
|
|
import android.content.ContentResolver
|
|
|
|
import android.content.Context
|
|
|
|
import android.content.Context
|
|
|
|
import android.content.Intent
|
|
|
|
import android.content.Intent
|
|
|
|
import android.content.pm.PackageManager
|
|
|
|
import android.content.pm.PackageManager
|
|
|
|
|
|
|
|
import android.media.AudioAttributes
|
|
|
|
|
|
|
|
import android.net.Uri
|
|
|
|
import android.os.Build
|
|
|
|
import android.os.Build
|
|
|
|
import androidx.core.app.NotificationCompat
|
|
|
|
import androidx.core.app.NotificationCompat
|
|
|
|
import androidx.core.app.NotificationManagerCompat
|
|
|
|
import androidx.core.app.NotificationManagerCompat
|
|
|
|
@ -52,7 +55,6 @@ import com.onlinemsg.client.util.LanguageManager
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* 单例管理类,负责整个聊天会话的生命周期、网络连接、消息收发、状态维护和持久化。
|
|
|
|
* 单例管理类,负责整个聊天会话的生命周期、网络连接、消息收发、状态维护和持久化。
|
|
|
|
* 所有公开方法均通过 ViewModel 代理调用,内部使用协程处理异步操作。
|
|
|
|
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
object ChatSessionManager {
|
|
|
|
object ChatSessionManager {
|
|
|
|
|
|
|
|
|
|
|
|
@ -81,18 +83,18 @@ object ChatSessionManager {
|
|
|
|
private var identity: RsaCryptoManager.Identity? = null
|
|
|
|
private var identity: RsaCryptoManager.Identity? = null
|
|
|
|
|
|
|
|
|
|
|
|
// 连接相关内部状态
|
|
|
|
// 连接相关内部状态
|
|
|
|
private var manualClose = false // 是否为手动断开
|
|
|
|
private var manualClose = false
|
|
|
|
private var fallbackTried = false // 是否已尝试切换 ws/wss
|
|
|
|
private var fallbackTried = false
|
|
|
|
private var connectedUrl = "" // 当前连接的服务器地址
|
|
|
|
private var connectedUrl = ""
|
|
|
|
private var serverPublicKey = "" // 服务端公钥(握手后获得)
|
|
|
|
private var serverPublicKey = ""
|
|
|
|
private var helloTimeoutJob: Job? = null // 握手超时任务
|
|
|
|
private var helloTimeoutJob: Job? = null
|
|
|
|
private var authTimeoutJob: Job? = null // 认证超时任务
|
|
|
|
private var authTimeoutJob: Job? = null
|
|
|
|
private var reconnectJob: Job? = null // 自动重连任务
|
|
|
|
private var reconnectJob: Job? = null
|
|
|
|
private var reconnectAttempt: Int = 0 // 当前重连尝试次数
|
|
|
|
private var reconnectAttempt: Int = 0
|
|
|
|
private val systemMessageExpiryJobs: MutableMap<String, Job> = mutableMapOf() // 系统消息自动过期任务
|
|
|
|
private val systemMessageExpiryJobs: MutableMap<String, Job> = mutableMapOf()
|
|
|
|
private var autoReconnectTriggered = false
|
|
|
|
private var autoReconnectTriggered = false
|
|
|
|
@Volatile
|
|
|
|
@Volatile
|
|
|
|
private var keepAliveRequested = false // 是否应保活(前台服务标志)
|
|
|
|
private var keepAliveRequested = false
|
|
|
|
private var notificationIdSeed = 2000
|
|
|
|
private var notificationIdSeed = 2000
|
|
|
|
|
|
|
|
|
|
|
|
// WebSocket 事件监听器
|
|
|
|
// WebSocket 事件监听器
|
|
|
|
@ -167,10 +169,6 @@ object ChatSessionManager {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 初始化管理器,必须在应用启动时调用一次。
|
|
|
|
|
|
|
|
* @param application Application 实例
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
@Synchronized
|
|
|
|
@Synchronized
|
|
|
|
fun initialize(application: Application) {
|
|
|
|
fun initialize(application: Application) {
|
|
|
|
if (initialized) return
|
|
|
|
if (initialized) return
|
|
|
|
@ -179,7 +177,6 @@ object ChatSessionManager {
|
|
|
|
preferencesRepository = UserPreferencesRepository(application, json)
|
|
|
|
preferencesRepository = UserPreferencesRepository(application, json)
|
|
|
|
cryptoManager = RsaCryptoManager(application)
|
|
|
|
cryptoManager = RsaCryptoManager(application)
|
|
|
|
historyRepository = ChatHistoryRepository(ChatDatabase.getInstance(application).chatMessageDao())
|
|
|
|
historyRepository = ChatHistoryRepository(ChatDatabase.getInstance(application).chatMessageDao())
|
|
|
|
ensureMessageNotificationChannel()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scope.launch {
|
|
|
|
scope.launch {
|
|
|
|
val pref = preferencesRepository.preferencesFlow.first()
|
|
|
|
val pref = preferencesRepository.preferencesFlow.first()
|
|
|
|
@ -197,11 +194,14 @@ object ChatSessionManager {
|
|
|
|
themeId = pref.themeId,
|
|
|
|
themeId = pref.themeId,
|
|
|
|
useDynamicColor = pref.useDynamicColor,
|
|
|
|
useDynamicColor = pref.useDynamicColor,
|
|
|
|
language = pref.language,
|
|
|
|
language = pref.language,
|
|
|
|
|
|
|
|
notificationSound = pref.notificationSound,
|
|
|
|
messages = historyMessages,
|
|
|
|
messages = historyMessages,
|
|
|
|
statusHint = LanguageManager.getString("status_hint.click_to_connect", pref.language)
|
|
|
|
statusHint = LanguageManager.getString("status_hint.click_to_connect", pref.language)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// 如果上次会话启用了自动重连,则自动恢复连接
|
|
|
|
|
|
|
|
|
|
|
|
ensureMessageNotificationChannel(pref.notificationSound)
|
|
|
|
|
|
|
|
|
|
|
|
if (pref.shouldAutoReconnect && !autoReconnectTriggered) {
|
|
|
|
if (pref.shouldAutoReconnect && !autoReconnectTriggered) {
|
|
|
|
autoReconnectTriggered = true
|
|
|
|
autoReconnectTriggered = true
|
|
|
|
ChatForegroundService.start(application)
|
|
|
|
ChatForegroundService.start(application)
|
|
|
|
@ -210,10 +210,14 @@ object ChatSessionManager {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
fun updateNotificationSound(sound: String) {
|
|
|
|
* 更新主题
|
|
|
|
_uiState.update { it.copy(notificationSound = sound) }
|
|
|
|
* @param themeId 主题名
|
|
|
|
scope.launch {
|
|
|
|
*/
|
|
|
|
preferencesRepository.setNotificationSound(sound)
|
|
|
|
|
|
|
|
ensureMessageNotificationChannel(sound)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fun updateTheme(themeId: String) {
|
|
|
|
fun updateTheme(themeId: String) {
|
|
|
|
_uiState.update { it.copy(themeId = themeId) }
|
|
|
|
_uiState.update { it.copy(themeId = themeId) }
|
|
|
|
scope.launch {
|
|
|
|
scope.launch {
|
|
|
|
@ -221,10 +225,6 @@ object ChatSessionManager {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 更新语言
|
|
|
|
|
|
|
|
* @param language 语言代码
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
fun updateLanguage(language: String) {
|
|
|
|
fun updateLanguage(language: String) {
|
|
|
|
_uiState.update { it.copy(language = language) }
|
|
|
|
_uiState.update { it.copy(language = language) }
|
|
|
|
scope.launch {
|
|
|
|
scope.launch {
|
|
|
|
@ -232,10 +232,6 @@ object ChatSessionManager {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 更改使用动态颜色
|
|
|
|
|
|
|
|
* @param enabled 主题名
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
fun updateUseDynamicColor(enabled: Boolean) {
|
|
|
|
fun updateUseDynamicColor(enabled: Boolean) {
|
|
|
|
_uiState.update { it.copy(useDynamicColor = enabled) }
|
|
|
|
_uiState.update { it.copy(useDynamicColor = enabled) }
|
|
|
|
scope.launch {
|
|
|
|
scope.launch {
|
|
|
|
@ -243,10 +239,6 @@ object ChatSessionManager {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 更新显示名称并持久化。
|
|
|
|
|
|
|
|
* @param value 新名称(自动截断至 64 字符)
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
fun updateDisplayName(value: String) {
|
|
|
|
fun updateDisplayName(value: String) {
|
|
|
|
val displayName = value.take(64)
|
|
|
|
val displayName = value.take(64)
|
|
|
|
_uiState.update { it.copy(displayName = displayName) }
|
|
|
|
_uiState.update { it.copy(displayName = displayName) }
|
|
|
|
@ -255,34 +247,18 @@ object ChatSessionManager {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 更新当前输入的服务器地址(不持久化)。
|
|
|
|
|
|
|
|
* @param value 新地址
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
fun updateServerUrl(value: String) {
|
|
|
|
fun updateServerUrl(value: String) {
|
|
|
|
_uiState.update { it.copy(serverUrl = value) }
|
|
|
|
_uiState.update { it.copy(serverUrl = value) }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 更新私聊目标公钥。
|
|
|
|
|
|
|
|
* @param value 公钥字符串
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
fun updateTargetKey(value: String) {
|
|
|
|
fun updateTargetKey(value: String) {
|
|
|
|
_uiState.update { it.copy(targetKey = value) }
|
|
|
|
_uiState.update { it.copy(targetKey = value) }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 更新消息草稿。
|
|
|
|
|
|
|
|
* @param value 草稿内容
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
fun updateDraft(value: String) {
|
|
|
|
fun updateDraft(value: String) {
|
|
|
|
_uiState.update { it.copy(draft = value) }
|
|
|
|
_uiState.update { it.copy(draft = value) }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 切换广播/私聊模式并持久化。
|
|
|
|
|
|
|
|
* @param enabled true 为私聊模式
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
fun toggleDirectMode(enabled: Boolean) {
|
|
|
|
fun toggleDirectMode(enabled: Boolean) {
|
|
|
|
_uiState.update { it.copy(directMode = enabled) }
|
|
|
|
_uiState.update { it.copy(directMode = enabled) }
|
|
|
|
scope.launch {
|
|
|
|
scope.launch {
|
|
|
|
@ -290,10 +266,6 @@ object ChatSessionManager {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 切换是否显示系统消息并持久化。
|
|
|
|
|
|
|
|
* @param show true 显示
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
fun toggleShowSystemMessages(show: Boolean) {
|
|
|
|
fun toggleShowSystemMessages(show: Boolean) {
|
|
|
|
_uiState.update { it.copy(showSystemMessages = show) }
|
|
|
|
_uiState.update { it.copy(showSystemMessages = show) }
|
|
|
|
scope.launch {
|
|
|
|
scope.launch {
|
|
|
|
@ -301,9 +273,6 @@ object ChatSessionManager {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 清空所有消息,并取消系统消息的过期任务。
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
fun clearMessages() {
|
|
|
|
fun clearMessages() {
|
|
|
|
cancelSystemMessageExpiryJobs()
|
|
|
|
cancelSystemMessageExpiryJobs()
|
|
|
|
_uiState.update { it.copy(messages = emptyList()) }
|
|
|
|
_uiState.update { it.copy(messages = emptyList()) }
|
|
|
|
@ -314,9 +283,6 @@ object ChatSessionManager {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 保存当前服务器地址到历史列表并持久化。
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
fun saveCurrentServerUrl() {
|
|
|
|
fun saveCurrentServerUrl() {
|
|
|
|
val normalized = ServerUrlFormatter.normalize(_uiState.value.serverUrl)
|
|
|
|
val normalized = ServerUrlFormatter.normalize(_uiState.value.serverUrl)
|
|
|
|
if (normalized.isBlank()) {
|
|
|
|
if (normalized.isBlank()) {
|
|
|
|
@ -342,10 +308,6 @@ object ChatSessionManager {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 从历史列表中移除当前服务器地址。
|
|
|
|
|
|
|
|
* 如果列表清空则恢复默认地址。
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
fun removeCurrentServerUrl() {
|
|
|
|
fun removeCurrentServerUrl() {
|
|
|
|
val normalized = ServerUrlFormatter.normalize(_uiState.value.serverUrl)
|
|
|
|
val normalized = ServerUrlFormatter.normalize(_uiState.value.serverUrl)
|
|
|
|
if (normalized.isBlank()) return
|
|
|
|
if (normalized.isBlank()) return
|
|
|
|
@ -376,9 +338,6 @@ object ChatSessionManager {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 加载或生成本地身份密钥对,并将公钥显示到 UI。
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
fun revealMyPublicKey() {
|
|
|
|
fun revealMyPublicKey() {
|
|
|
|
scope.launch {
|
|
|
|
scope.launch {
|
|
|
|
_uiState.update { it.copy(loadingPublicKey = true) }
|
|
|
|
_uiState.update { it.copy(loadingPublicKey = true) }
|
|
|
|
@ -398,17 +357,10 @@ object ChatSessionManager {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 主动连接服务器(由用户点击连接触发)。
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
fun connect() {
|
|
|
|
fun connect() {
|
|
|
|
connectInternal(isAutoRestore = false)
|
|
|
|
connectInternal(isAutoRestore = false)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 内部连接逻辑,区分自动恢复和手动连接。
|
|
|
|
|
|
|
|
* @param isAutoRestore 是否为应用启动时的自动恢复连接
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
private fun connectInternal(isAutoRestore: Boolean) {
|
|
|
|
private fun connectInternal(isAutoRestore: Boolean) {
|
|
|
|
if (!initialized) return
|
|
|
|
if (!initialized) return
|
|
|
|
val state = _uiState.value
|
|
|
|
val state = _uiState.value
|
|
|
|
@ -459,10 +411,6 @@ object ChatSessionManager {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 主动断开连接。
|
|
|
|
|
|
|
|
* @param stopService 是否同时停止前台服务(默认 true)
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
fun disconnect(stopService: Boolean = true) {
|
|
|
|
fun disconnect(stopService: Boolean = true) {
|
|
|
|
manualClose = true
|
|
|
|
manualClose = true
|
|
|
|
cancelReconnect()
|
|
|
|
cancelReconnect()
|
|
|
|
@ -487,10 +435,6 @@ object ChatSessionManager {
|
|
|
|
addSystemMessage("已断开连接")
|
|
|
|
addSystemMessage("已断开连接")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 发送消息(广播或私聊)。
|
|
|
|
|
|
|
|
* 执行签名、加密并发送。
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
fun sendMessage() {
|
|
|
|
fun sendMessage() {
|
|
|
|
val current = _uiState.value
|
|
|
|
val current = _uiState.value
|
|
|
|
if (!current.canSend) return
|
|
|
|
if (!current.canSend) return
|
|
|
|
@ -549,19 +493,12 @@ object ChatSessionManager {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 消息复制成功后的回调,显示“已复制”提示。
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
fun onMessageCopied() {
|
|
|
|
fun onMessageCopied() {
|
|
|
|
scope.launch {
|
|
|
|
scope.launch {
|
|
|
|
_events.emit(UiEvent.ShowSnackbar("已复制"))
|
|
|
|
_events.emit(UiEvent.ShowSnackbar("已复制"))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 确保本地身份已加载 or 创建。
|
|
|
|
|
|
|
|
* @return 本地身份对象
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
private suspend fun ensureIdentity(): RsaCryptoManager.Identity {
|
|
|
|
private suspend fun ensureIdentity(): RsaCryptoManager.Identity {
|
|
|
|
return identityMutex.withLock {
|
|
|
|
return identityMutex.withLock {
|
|
|
|
identity ?: withContext(Dispatchers.Default) {
|
|
|
|
identity ?: withContext(Dispatchers.Default) {
|
|
|
|
@ -572,10 +509,6 @@ object ChatSessionManager {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 处理收到的原始文本消息(可能是握手包 or 加密消息)。
|
|
|
|
|
|
|
|
* @param rawText 原始文本
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
private suspend fun handleIncomingMessage(rawText: String) {
|
|
|
|
private suspend fun handleIncomingMessage(rawText: String) {
|
|
|
|
if (_uiState.value.status == ConnectionStatus.HANDSHAKING) {
|
|
|
|
if (_uiState.value.status == ConnectionStatus.HANDSHAKING) {
|
|
|
|
val lang = _uiState.value.language
|
|
|
|
val lang = _uiState.value.language
|
|
|
|
@ -587,7 +520,6 @@ object ChatSessionManager {
|
|
|
|
json.decodeFromString<JsonElement>(normalizedText) as? JsonObject
|
|
|
|
json.decodeFromString<JsonElement>(normalizedText) as? JsonObject
|
|
|
|
}.getOrNull()
|
|
|
|
}.getOrNull()
|
|
|
|
|
|
|
|
|
|
|
|
// 尝试直接解析为 HelloDataDto(某些服务器可能直接发送,不带外层)
|
|
|
|
|
|
|
|
val directHello = rootObject?.let { obj ->
|
|
|
|
val directHello = rootObject?.let { obj ->
|
|
|
|
val hasPublicKey = obj["publicKey"] != null
|
|
|
|
val hasPublicKey = obj["publicKey"] != null
|
|
|
|
val hasChallenge = obj["authChallenge"] != null
|
|
|
|
val hasChallenge = obj["authChallenge"] != null
|
|
|
|
@ -603,7 +535,6 @@ object ChatSessionManager {
|
|
|
|
return
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 尝试解析为带外层的 EnvelopeDto
|
|
|
|
|
|
|
|
val plain = runCatching { json.decodeFromString<EnvelopeDto>(normalizedText) }.getOrNull()
|
|
|
|
val plain = runCatching { json.decodeFromString<EnvelopeDto>(normalizedText) }.getOrNull()
|
|
|
|
if (plain?.type == "publickey") {
|
|
|
|
if (plain?.type == "publickey") {
|
|
|
|
cancelHelloTimeout()
|
|
|
|
cancelHelloTimeout()
|
|
|
|
@ -624,7 +555,6 @@ object ChatSessionManager {
|
|
|
|
return
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 握手阶段收到非预期消息则报错
|
|
|
|
|
|
|
|
if (_uiState.value.status == ConnectionStatus.HANDSHAKING && plain != null) {
|
|
|
|
if (_uiState.value.status == ConnectionStatus.HANDSHAKING && plain != null) {
|
|
|
|
val lang = _uiState.value.language
|
|
|
|
val lang = _uiState.value.language
|
|
|
|
_uiState.update { it.copy(statusHint = LanguageManager.getString("status_hint.handshake_failed_unexpected", lang)) }
|
|
|
|
_uiState.update { it.copy(statusHint = LanguageManager.getString("status_hint.handshake_failed_unexpected", lang)) }
|
|
|
|
@ -636,7 +566,6 @@ object ChatSessionManager {
|
|
|
|
addSystemMessage("握手包解析失败:$preview")
|
|
|
|
addSystemMessage("握手包解析失败:$preview")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 尝试解密(若已握手完成,收到的应是加密消息)
|
|
|
|
|
|
|
|
val id = ensureIdentity()
|
|
|
|
val id = ensureIdentity()
|
|
|
|
val decrypted = runCatching {
|
|
|
|
val decrypted = runCatching {
|
|
|
|
withContext(Dispatchers.Default) {
|
|
|
|
withContext(Dispatchers.Default) {
|
|
|
|
@ -654,10 +583,6 @@ object ChatSessionManager {
|
|
|
|
handleSecureMessage(secure)
|
|
|
|
handleSecureMessage(secure)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 处理服务端发来的握手 Hello 数据。
|
|
|
|
|
|
|
|
* @param hello 服务端公钥和挑战
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
private suspend fun handleServerHello(hello: HelloDataDto) {
|
|
|
|
private suspend fun handleServerHello(hello: HelloDataDto) {
|
|
|
|
cancelHelloTimeout()
|
|
|
|
cancelHelloTimeout()
|
|
|
|
serverPublicKey = hello.publicKey
|
|
|
|
serverPublicKey = hello.publicKey
|
|
|
|
@ -704,10 +629,6 @@ object ChatSessionManager {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 发送认证消息(包含签名后的身份信息)。
|
|
|
|
|
|
|
|
* @param challenge 服务端提供的挑战值
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
private suspend fun sendAuth(challenge: String) {
|
|
|
|
private suspend fun sendAuth(challenge: String) {
|
|
|
|
val id = ensureIdentity()
|
|
|
|
val id = ensureIdentity()
|
|
|
|
val displayName = _uiState.value.displayName.trim().ifBlank { createGuestName() }
|
|
|
|
val displayName = _uiState.value.displayName.trim().ifBlank { createGuestName() }
|
|
|
|
@ -751,10 +672,6 @@ object ChatSessionManager {
|
|
|
|
check(socketClient.send(cipher)) { "连接不可用" }
|
|
|
|
check(socketClient.send(cipher)) { "连接不可用" }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 处理安全通道建立后的业务消息(广播、私聊、认证结果等)。
|
|
|
|
|
|
|
|
* @param message 解密后的 EnvelopeDto
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
private fun handleSecureMessage(message: EnvelopeDto) {
|
|
|
|
private fun handleSecureMessage(message: EnvelopeDto) {
|
|
|
|
when (message.type) {
|
|
|
|
when (message.type) {
|
|
|
|
"auth_ok" -> {
|
|
|
|
"auth_ok" -> {
|
|
|
|
@ -795,30 +712,15 @@ object ChatSessionManager {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 处理 WebSocket 连接关闭事件。
|
|
|
|
|
|
|
|
* @param code 关闭状态码
|
|
|
|
|
|
|
|
* @param reason 关闭原因
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
private fun handleSocketClosed(code: Int, reason: String) {
|
|
|
|
private fun handleSocketClosed(code: Int, reason: String) {
|
|
|
|
cancelHelloTimeout()
|
|
|
|
cancelHelloTimeout()
|
|
|
|
cancelAuthTimeout()
|
|
|
|
cancelAuthTimeout()
|
|
|
|
|
|
|
|
|
|
|
|
if (manualClose) {
|
|
|
|
if (manualClose || reason == "reconnect" || reconnectJob?.isActive == true) return
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (reason == "reconnect") {
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (reconnectJob?.isActive == true) {
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
val currentStatus = _uiState.value.status
|
|
|
|
val currentStatus = _uiState.value.status
|
|
|
|
|
|
|
|
|
|
|
|
val allowFallback = !fallbackTried && currentStatus != ConnectionStatus.READY
|
|
|
|
val allowFallback = !fallbackTried && currentStatus != ConnectionStatus.READY
|
|
|
|
|
|
|
|
|
|
|
|
// 尝试切换 ws/wss 协议重试(仅限非就绪状态)
|
|
|
|
|
|
|
|
if (allowFallback) {
|
|
|
|
if (allowFallback) {
|
|
|
|
val fallbackUrl = ServerUrlFormatter.toggleWsProtocol(connectedUrl)
|
|
|
|
val fallbackUrl = ServerUrlFormatter.toggleWsProtocol(connectedUrl)
|
|
|
|
if (fallbackUrl.isNotBlank()) {
|
|
|
|
if (fallbackUrl.isNotBlank()) {
|
|
|
|
@ -847,13 +749,8 @@ object ChatSessionManager {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
addSystemMessage("连接关闭 ($code):${reason.ifBlank { "连接中断" }}")
|
|
|
|
addSystemMessage("连接关闭 ($code):${reason.ifBlank { "连接中断" }}")
|
|
|
|
scheduleReconnect("连接已中断")
|
|
|
|
scheduleReconnect("连接已中断")
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 添加一条系统消息(自动按 TTL 过期)。
|
|
|
|
|
|
|
|
* @param content 消息内容
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
private fun addSystemMessage(content: String) {
|
|
|
|
private fun addSystemMessage(content: String) {
|
|
|
|
val message = UiMessage(
|
|
|
|
val message = UiMessage(
|
|
|
|
role = MessageRole.SYSTEM,
|
|
|
|
role = MessageRole.SYSTEM,
|
|
|
|
@ -866,13 +763,6 @@ object ChatSessionManager {
|
|
|
|
scheduleSystemMessageExpiry(message.id)
|
|
|
|
scheduleSystemMessageExpiry(message.id)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 添加一条接收到的用户消息。
|
|
|
|
|
|
|
|
* @param sender 发送者名称
|
|
|
|
|
|
|
|
* @param subtitle 附加说明(如私聊来源)
|
|
|
|
|
|
|
|
* @param content 消息内容
|
|
|
|
|
|
|
|
* @param channel 消息通道(广播/私聊)
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
private fun addIncomingMessage(
|
|
|
|
private fun addIncomingMessage(
|
|
|
|
sender: String,
|
|
|
|
sender: String,
|
|
|
|
subtitle: String,
|
|
|
|
subtitle: String,
|
|
|
|
@ -894,12 +784,6 @@ object ChatSessionManager {
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 添加一条发出的消息。
|
|
|
|
|
|
|
|
* @param content 消息内容
|
|
|
|
|
|
|
|
* @param subtitle 附加说明(如私聊目标)
|
|
|
|
|
|
|
|
* @param channel 消息通道
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
private fun addOutgoingMessage(
|
|
|
|
private fun addOutgoingMessage(
|
|
|
|
content: String,
|
|
|
|
content: String,
|
|
|
|
subtitle: String,
|
|
|
|
subtitle: String,
|
|
|
|
@ -916,15 +800,11 @@ object ChatSessionManager {
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 将消息追加到列表尾部,并清理超出数量限制的消息。
|
|
|
|
|
|
|
|
* @param message 要追加的消息
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
private fun appendMessage(message: UiMessage) {
|
|
|
|
private fun appendMessage(message: UiMessage) {
|
|
|
|
_uiState.update { current ->
|
|
|
|
_uiState.update { current ->
|
|
|
|
val next = (current.messages + message).takeLast(MAX_MESSAGES)
|
|
|
|
val next = (current.messages + message).takeLast(MAX_MESSAGES)
|
|
|
|
val aliveIds = next.asSequence().map { it.id }.toSet()
|
|
|
|
val aliveIds = next.asSequence().map { it.id }.toSet()
|
|
|
|
val removedIds = systemMessageExpiryJobs.keys.filterNot { it in aliveIds }
|
|
|
|
val removedIds = systemMessageExpiryJobs.keys.filterNot { id -> id in aliveIds }
|
|
|
|
removedIds.forEach { id ->
|
|
|
|
removedIds.forEach { id ->
|
|
|
|
systemMessageExpiryJobs.remove(id)?.cancel()
|
|
|
|
systemMessageExpiryJobs.remove(id)?.cancel()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -938,21 +818,13 @@ object ChatSessionManager {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 取消认证超时任务。
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
private fun cancelAuthTimeout() {
|
|
|
|
private fun cancelAuthTimeout() {
|
|
|
|
authTimeoutJob?.cancel()
|
|
|
|
authTimeoutJob?.cancel()
|
|
|
|
authTimeoutJob = null
|
|
|
|
authTimeoutJob = null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 安排自动重连(指数退避)。
|
|
|
|
|
|
|
|
* @param reason 触发重连的原因
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
private fun scheduleReconnect(reason: String) {
|
|
|
|
private fun scheduleReconnect(reason: String) {
|
|
|
|
if (manualClose) return
|
|
|
|
if (manualClose || reconnectJob?.isActive == true) return
|
|
|
|
if (reconnectJob?.isActive == true) return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
reconnectAttempt += 1
|
|
|
|
reconnectAttempt += 1
|
|
|
|
val exponential = 1 shl minOf(reconnectAttempt - 1, 5)
|
|
|
|
val exponential = 1 shl minOf(reconnectAttempt - 1, 5)
|
|
|
|
@ -1004,18 +876,11 @@ object ChatSessionManager {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 取消自动重连任务。
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
private fun cancelReconnect() {
|
|
|
|
private fun cancelReconnect() {
|
|
|
|
reconnectJob?.cancel()
|
|
|
|
reconnectJob?.cancel()
|
|
|
|
reconnectJob = null
|
|
|
|
reconnectJob = null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 为系统消息安排过期自动删除。
|
|
|
|
|
|
|
|
* @param messageId 消息唯一 ID
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
private fun scheduleSystemMessageExpiry(messageId: String) {
|
|
|
|
private fun scheduleSystemMessageExpiry(messageId: String) {
|
|
|
|
systemMessageExpiryJobs.remove(messageId)?.cancel()
|
|
|
|
systemMessageExpiryJobs.remove(messageId)?.cancel()
|
|
|
|
systemMessageExpiryJobs[messageId] = scope.launch {
|
|
|
|
systemMessageExpiryJobs[messageId] = scope.launch {
|
|
|
|
@ -1028,17 +893,11 @@ object ChatSessionManager {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 取消所有系统消息的过期任务。
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
private fun cancelSystemMessageExpiryJobs() {
|
|
|
|
private fun cancelSystemMessageExpiryJobs() {
|
|
|
|
systemMessageExpiryJobs.values.forEach { it.cancel() }
|
|
|
|
systemMessageExpiryJobs.values.forEach { it.cancel() }
|
|
|
|
systemMessageExpiryJobs.clear()
|
|
|
|
systemMessageExpiryJobs.clear()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 启动握手超时计时器。
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
private fun startHelloTimeout() {
|
|
|
|
private fun startHelloTimeout() {
|
|
|
|
cancelHelloTimeout()
|
|
|
|
cancelHelloTimeout()
|
|
|
|
helloTimeoutJob = scope.launch {
|
|
|
|
helloTimeoutJob = scope.launch {
|
|
|
|
@ -1058,56 +917,29 @@ object ChatSessionManager {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 取消握手超时任务。
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
private fun cancelHelloTimeout() {
|
|
|
|
private fun cancelHelloTimeout() {
|
|
|
|
helloTimeoutJob?.cancel()
|
|
|
|
helloTimeoutJob?.cancel()
|
|
|
|
helloTimeoutJob = null
|
|
|
|
helloTimeoutJob = null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 缩写显示公钥(取前后各8字符)。
|
|
|
|
|
|
|
|
* @param key 完整公钥
|
|
|
|
|
|
|
|
* @return 缩写字符串
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
private fun summarizeKey(key: String): String {
|
|
|
|
private fun summarizeKey(key: String): String {
|
|
|
|
if (key.length <= 16) return key
|
|
|
|
if (key.length <= 16) return key
|
|
|
|
return "${key.take(8)}...${key.takeLast(8)}"
|
|
|
|
return "${key.take(8)}...${key.takeLast(8)}"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 生成访客名称(如 guest-123456)。
|
|
|
|
|
|
|
|
* @return 随机名称
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
private fun createGuestName(): String {
|
|
|
|
private fun createGuestName(): String {
|
|
|
|
val rand = (100000..999999).random()
|
|
|
|
val rand = (100000..999999).random()
|
|
|
|
return "guest-$rand"
|
|
|
|
return "guest-$rand"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 从可能包含前缀的原始文本中提取 JSON 对象部分。
|
|
|
|
|
|
|
|
* @param rawText 原始文本
|
|
|
|
|
|
|
|
* @return 最外层的 JSON 字符串
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
private fun extractJsonCandidate(rawText: String): String {
|
|
|
|
private fun extractJsonCandidate(rawText: String): String {
|
|
|
|
val trimmed = rawText.trim()
|
|
|
|
val trimmed = rawText.trim()
|
|
|
|
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
|
|
if (trimmed.startsWith("{") && trimmed.endsWith("}")) return trimmed
|
|
|
|
return trimmed
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
val start = rawText.indexOf('{')
|
|
|
|
val start = rawText.indexOf('{')
|
|
|
|
val end = rawText.lastIndexOf('}')
|
|
|
|
val end = rawText.lastIndexOf('}')
|
|
|
|
return if (start in 0 until end) {
|
|
|
|
return if (start in 0 until end) rawText.substring(start, end + 1) else rawText
|
|
|
|
rawText.substring(start, end + 1)
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
rawText
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 关闭所有资源(用于应用退出时)。
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
fun shutdownAll() {
|
|
|
|
fun shutdownAll() {
|
|
|
|
cancelSystemMessageExpiryJobs()
|
|
|
|
cancelSystemMessageExpiryJobs()
|
|
|
|
cancelReconnect()
|
|
|
|
cancelReconnect()
|
|
|
|
@ -1116,9 +948,6 @@ object ChatSessionManager {
|
|
|
|
socketClient.shutdown()
|
|
|
|
socketClient.shutdown()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 前台服务停止时的回调。
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
fun onForegroundServiceStopped() {
|
|
|
|
fun onForegroundServiceStopped() {
|
|
|
|
keepAliveRequested = false
|
|
|
|
keepAliveRequested = false
|
|
|
|
if (_uiState.value.status != ConnectionStatus.IDLE) {
|
|
|
|
if (_uiState.value.status != ConnectionStatus.IDLE) {
|
|
|
|
@ -1130,52 +959,57 @@ object ChatSessionManager {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 判断前台服务是否应该运行。
|
|
|
|
|
|
|
|
* @return true 表示应保持服务运行
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
fun shouldForegroundServiceRun(): Boolean = keepAliveRequested
|
|
|
|
fun shouldForegroundServiceRun(): Boolean = keepAliveRequested
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
private fun ensureMessageNotificationChannel(soundCode: String = "default") {
|
|
|
|
* 创建消息通知渠道(Android O+)。
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
private fun ensureMessageNotificationChannel() {
|
|
|
|
|
|
|
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
|
|
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
|
|
|
val manager = app.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
|
|
val manager = app.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
|
|
|
|
|
|
val channelId = "${MESSAGE_CHANNEL_ID}_$soundCode"
|
|
|
|
|
|
|
|
if (manager.getNotificationChannel(channelId) != null) return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
val soundUri = getSoundUri(soundCode)
|
|
|
|
val channel = NotificationChannel(
|
|
|
|
val channel = NotificationChannel(
|
|
|
|
MESSAGE_CHANNEL_ID,
|
|
|
|
channelId,
|
|
|
|
"OnlineMsg 消息提醒",
|
|
|
|
"OnlineMsg 消息提醒",
|
|
|
|
NotificationManager.IMPORTANCE_DEFAULT
|
|
|
|
NotificationManager.IMPORTANCE_DEFAULT
|
|
|
|
).apply {
|
|
|
|
).apply {
|
|
|
|
description = "收到服务器新消息时提醒"
|
|
|
|
description = "收到服务器新消息时提醒"
|
|
|
|
|
|
|
|
if (soundUri != null) {
|
|
|
|
|
|
|
|
setSound(soundUri, AudioAttributes.Builder()
|
|
|
|
|
|
|
|
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
|
|
|
|
|
|
|
|
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
|
|
|
|
|
|
|
.build())
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
manager.createNotificationChannel(channel)
|
|
|
|
manager.createNotificationChannel(channel)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
private fun getSoundUri(code: String): Uri? {
|
|
|
|
* 显示新消息到达的通知。
|
|
|
|
val resId = when(code) {
|
|
|
|
* @param title 通知标题
|
|
|
|
"ding" -> R.raw.load
|
|
|
|
* @param body 通知正文
|
|
|
|
"nameit5" -> R.raw.nameit5
|
|
|
|
*/
|
|
|
|
"wind_chime" -> R.raw.notification_sound_effects
|
|
|
|
|
|
|
|
"default" -> R.raw.default_sound
|
|
|
|
|
|
|
|
else -> return null
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + app.packageName + "/" + resId)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private fun showIncomingNotification(title: String, body: String) {
|
|
|
|
private fun showIncomingNotification(title: String, body: String) {
|
|
|
|
if (!initialized) return
|
|
|
|
if (!initialized) return
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
|
|
|
|
ContextCompat.checkSelfPermission(app, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED
|
|
|
|
ContextCompat.checkSelfPermission(app, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED
|
|
|
|
) {
|
|
|
|
) return
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
val launchIntent = Intent(app, MainActivity::class.java).apply {
|
|
|
|
val launchIntent = Intent(app, MainActivity::class.java).apply {
|
|
|
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
|
|
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
|
|
|
}
|
|
|
|
}
|
|
|
|
val pendingIntent = PendingIntent.getActivity(
|
|
|
|
val pendingIntent = PendingIntent.getActivity(app, 0, launchIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
|
|
|
app,
|
|
|
|
|
|
|
|
0,
|
|
|
|
|
|
|
|
launchIntent,
|
|
|
|
|
|
|
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
val notification = NotificationCompat.Builder(app, MESSAGE_CHANNEL_ID)
|
|
|
|
val soundCode = _uiState.value.notificationSound
|
|
|
|
|
|
|
|
val channelId = "${MESSAGE_CHANNEL_ID}_$soundCode"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
val notification = NotificationCompat.Builder(app, channelId)
|
|
|
|
.setSmallIcon(android.R.drawable.stat_notify_chat)
|
|
|
|
.setSmallIcon(android.R.drawable.stat_notify_chat)
|
|
|
|
.setContentTitle(title.ifBlank { "OnlineMsg" })
|
|
|
|
.setContentTitle(title.ifBlank { "OnlineMsg" })
|
|
|
|
.setContentText(body.take(120))
|
|
|
|
.setContentText(body.take(120))
|
|
|
|
@ -1188,17 +1022,12 @@ object ChatSessionManager {
|
|
|
|
NotificationManagerCompat.from(app).notify(nextMessageNotificationId(), notification)
|
|
|
|
NotificationManagerCompat.from(app).notify(nextMessageNotificationId(), notification)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 生成下一个通知 ID(线程安全递增)。
|
|
|
|
|
|
|
|
* @return 新的通知 ID
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
@Synchronized
|
|
|
|
@Synchronized
|
|
|
|
private fun nextMessageNotificationId(): Int {
|
|
|
|
private fun nextMessageNotificationId(): Int {
|
|
|
|
notificationIdSeed += 1
|
|
|
|
notificationIdSeed += 1
|
|
|
|
return notificationIdSeed
|
|
|
|
return notificationIdSeed
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 常量定义
|
|
|
|
|
|
|
|
private const val HELLO_TIMEOUT_MS = 12_000L
|
|
|
|
private const val HELLO_TIMEOUT_MS = 12_000L
|
|
|
|
private const val AUTH_TIMEOUT_MS = 20_000L
|
|
|
|
private const val AUTH_TIMEOUT_MS = 20_000L
|
|
|
|
private const val MAX_MESSAGES = 500
|
|
|
|
private const val MAX_MESSAGES = 500
|
|
|
|
|