系统通知优化
This commit is contained in:
@@ -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: 消息的 targetMode(2=频道,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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user