系统通知优化

This commit is contained in:
sansen
2026-07-24 00:18:14 +08:00
parent d2342b8829
commit 14a39381bb
8 changed files with 226 additions and 105 deletions
+2 -2
View File
@@ -13,8 +13,8 @@ android {
applicationId = "com.tsmobile.app"
minSdk = 26
targetSdk = 35
versionCode = 4
versionName = "1.0.4"
versionCode = 5
versionName = "1.0.5"
}
signingConfigs {
@@ -1,6 +1,7 @@
package com.tsmobile.app
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
@@ -17,17 +18,44 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.rememberNavController
import com.tsmobile.app.ui.navigation.AppNavGraph
import com.tsmobile.app.ui.theme.TSMobileTheme
import com.tsmobile.app.viewmodel.ServerViewModel
import com.tsmobile.app.viewmodel.VoiceViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
class MainActivity : ComponentActivity() {
companion object {
/** Intent extra: deep link target route ("chat" or "channel_list") */
const val EXTRA_DEEP_LINK = "deep_link"
const val DEEP_LINK_CHAT = "chat"
const val DEEP_LINK_CHANNEL_LIST = "channel_list"
/** Intent extra: 消息的 targetMode2=频道,1=私聊) */
const val EXTRA_TARGET_MODE = "target_mode"
/** Intent extra: 消息的 targetId(频道 ID 或客户端 ID */
const val EXTRA_TARGET_ID = "target_id"
}
/** 系统通知点击产生的导航目标 */
data class DeepLinkAction(
val target: String,
val targetMode: Int? = null,
val targetId: Long? = null,
)
private val _deepLinkAction = MutableStateFlow<DeepLinkAction?>(null)
val deepLinkAction: StateFlow<DeepLinkAction?> = _deepLinkAction.asStateFlow()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 处理冷启动时的 deep link(app 进程被杀后通过通知重新启动)
processIntent(intent)
enableEdgeToEdge()
setContent {
val serverViewModel: ServerViewModel = viewModel()
@@ -75,10 +103,44 @@ class MainActivity : ComponentActivity() {
navController = navController,
serverViewModel = serverViewModel,
voiceViewModel = voiceViewModel,
deepLinkAction = deepLinkAction,
onDeepLinkConsumed = { _deepLinkAction.value = null },
)
}
}
}
}
/**
* 用户从后台通过通知回到 app 时调用。
* 因为 [android.app.PendingIntent] 使用了 FLAG_ACTIVITY_SINGLE_TOP
* 已存在的 Activity 不会重建,而是通过此方法接收新的 Intent。
*/
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
processIntent(intent)
}
/**
* 每次 Activity 回到前台时清除消息/Poke 通知。
* 防止用户手动切回 app 后频道变化,旧通知中的 deep link 指向错误频道。
*/
override fun onResume() {
super.onResume()
try {
ViewModelProvider(this)[ServerViewModel::class.java].clearNotifications()
} catch (_: Exception) {}
}
/**
* 从 Intent 中提取 deep link 信息并更新 StateFlow
* 触发 NavGraph 的导航逻辑。
*/
private fun processIntent(intent: Intent?) {
val target = intent?.getStringExtra(EXTRA_DEEP_LINK) ?: return
val targetMode = intent.getIntExtra(EXTRA_TARGET_MODE, -1).takeIf { it >= 0 }
val targetId = intent.getLongExtra(EXTRA_TARGET_ID, -1L).takeIf { it >= 0L }
_deepLinkAction.value = DeepLinkAction(target, targetMode, targetId)
}
}
@@ -9,11 +9,14 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import com.tsmobile.app.MainActivity
import com.tsmobile.app.data.ConnectionState
import com.tsmobile.app.data.Repository
import com.tsmobile.app.ui.components.PokeNotification
@@ -26,6 +29,7 @@ import com.tsmobile.app.viewmodel.ChannelViewModel
import com.tsmobile.app.viewmodel.ChatViewModel
import com.tsmobile.app.viewmodel.ServerViewModel
import com.tsmobile.app.viewmodel.VoiceViewModel
import kotlinx.coroutines.flow.StateFlow
/**
* 导航路由定义。
@@ -49,6 +53,8 @@ fun AppNavGraph(
channelViewModel: ChannelViewModel = viewModel(),
chatViewModel: ChatViewModel = viewModel(),
voiceViewModel: VoiceViewModel = viewModel(),
deepLinkAction: StateFlow<MainActivity.DeepLinkAction?>? = null,
onDeepLinkConsumed: (() -> Unit)? = null,
) {
// 设置 ViewModel 引用,用于事件协调
LaunchedEffect(Unit) {
@@ -60,6 +66,34 @@ fun AppNavGraph(
// 观察连接状态变化,处理导航
val connectionState by serverViewModel.connectionState.collectAsState()
// ── 系统通知 deep link 导航 ──
// 当通知点击时,MainActivity 将目标写入 deepLinkAction
// 等待连接状态为 Connected 后执行导航。
val deepLink by (deepLinkAction?.collectAsState() ?: remember { mutableStateOf(null) })
LaunchedEffect(deepLink, connectionState) {
val action = deepLink ?: return@LaunchedEffect
if (connectionState !is ConnectionState.Connected) return@LaunchedEffect
when (action.target) {
MainActivity.DEEP_LINK_CHAT -> {
// 先跳到频道列表(清空回退栈),再进入聊天页
navController.navigate(Routes.CHANNEL_LIST) {
popUpTo(Routes.SERVER_CONFIG) { inclusive = true }
}
navController.navigate(Routes.CHAT) {
launchSingleTop = true
}
}
MainActivity.DEEP_LINK_CHANNEL_LIST -> {
navController.navigate(Routes.CHANNEL_LIST) {
popUpTo(Routes.SERVER_CONFIG) { inclusive = true }
}
}
}
onDeepLinkConsumed?.invoke()
}
LaunchedEffect(connectionState) {
when (val state = connectionState) {
is ConnectionState.Disconnected -> {
@@ -629,8 +629,25 @@ class ServerViewModel(application: Application) : AndroidViewModel(application)
_pokeNotification.value = null
}
/**
* 清除所有消息和 Poke 系统通知。
* 当用户主动回到 app 时调用,防止旧通知中的频道 ID 与实际频道不一致。
*/
fun clearNotifications() {
val nm = getApplication<Application>().getSystemService(NotificationManager::class.java) ?: return
synchronized(activeNotificationIds) {
for (id in activeNotificationIds) {
try { nm.cancel(id) } catch (_: Exception) {}
}
activeNotificationIds.clear()
}
}
private var pokeNotificationId = 1000
/** 追踪所有已发送的消息/Poke 通知 ID,用于 app 回到前台时批量清除 */
private val activeNotificationIds = mutableSetOf<Int>()
/**
* 发送 Poke 系统通知(app 在后台时调用)。
*/
@@ -638,9 +655,10 @@ class ServerViewModel(application: Application) : AndroidViewModel(application)
try {
val context = getApplication<Application>()
val pokeId = pokeNotificationId++
val pendingIntent = PendingIntent.getActivity(
context,
0,
pokeId,
Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
},
@@ -663,7 +681,8 @@ class ServerViewModel(application: Application) : AndroidViewModel(application)
.build()
val nm = context.getSystemService(NotificationManager::class.java)
nm.notify(pokeNotificationId++, notification)
nm.notify(pokeId, notification)
synchronized(activeNotificationIds) { activeNotificationIds.add(pokeId) }
} catch (e: Exception) {
android.util.Log.e(TAG, "Failed to send poke notification", e)
}
@@ -684,11 +703,16 @@ class ServerViewModel(application: Application) : AndroidViewModel(application)
try {
val context = getApplication<Application>()
// 使用 msgKey 的 hashCode 作为 requestCode,确保不同会话的通知有各自的 PendingIntent
val requestCode = msgKey.hashCode()
val pendingIntent = PendingIntent.getActivity(
context,
0,
requestCode,
Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
putExtra(MainActivity.EXTRA_DEEP_LINK, MainActivity.DEEP_LINK_CHAT)
putExtra(MainActivity.EXTRA_TARGET_MODE, targetMode)
putExtra(MainActivity.EXTRA_TARGET_ID, targetId)
},
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
)
@@ -709,6 +733,7 @@ class ServerViewModel(application: Application) : AndroidViewModel(application)
val notificationId = 2000 + (msgKey.hashCode() and 0xFFFF)
val nm = context.getSystemService(NotificationManager::class.java)
nm.notify(notificationId, notification)
synchronized(activeNotificationIds) { activeNotificationIds.add(notificationId) }
} catch (e: Exception) {
android.util.Log.e(TAG, "Failed to send message notification", e)
}