diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt index daf26128e1..66c3373c28 100644 --- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt +++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt @@ -132,7 +132,9 @@ class MainActivity : AppCompatActivity() { onFailure = { lifecycleScope.launch { showToast(Res.string.contact_invalid) } }, ) } else { - Timber.d("App link data is not a channel set") + Timber.d("Unhandled app link, delegating to navigation: $it") + // Delegate other deep links (e.g., node details) to the composable NavHost + model.setDeepLinkRequested(it) } } } diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt index 18cfea79d3..6ace50e823 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -230,6 +230,18 @@ constructor( _requestChannelSet.value = null } + private val _deepLinkRequested: MutableStateFlow = MutableStateFlow(null) + val deepLinkRequested: StateFlow + get() = _deepLinkRequested.asStateFlow() + + fun setDeepLinkRequested(url: Uri) { + _deepLinkRequested.value = url + } + + fun clearDeepLinkRequested() { + _deepLinkRequested.value = null + } + override fun onCleared() { super.onCleared() Timber.d("ViewModel cleared") diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotificationsImpl.kt b/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotificationsImpl.kt index 336e463da7..b0e35fe49f 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotificationsImpl.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotificationsImpl.kt @@ -284,7 +284,7 @@ class MeshServiceNotificationsImpl @Inject constructor(@ApplicationContext priva } override fun showNewNodeSeenNotification(node: NodeEntity) { - val notification = createNewNodeSeenNotification(node.user.shortName, node.user.longName) + val notification = createNewNodeSeenNotification(node.num, node.user.shortName, node.user.longName) notificationManager.notify(node.num, notification) } @@ -374,10 +374,10 @@ class MeshServiceNotificationsImpl @Inject constructor(@ApplicationContext priva .build() } - private fun createNewNodeSeenNotification(name: String, message: String?): Notification { + private fun createNewNodeSeenNotification(destNum: Int, name: String, message: String?): Notification { val title = getString(Res.string.new_node_seen).format(name) val builder = - commonBuilder(NotificationType.NewNode) + commonBuilder(NotificationType.NewNode, createOpenNodeDetailIntent(destNum)) .setCategory(Notification.CATEGORY_STATUS) .setAutoCancel(true) .setContentTitle(title) @@ -465,6 +465,19 @@ class MeshServiceNotificationsImpl @Inject constructor(@ApplicationContext priva .build() } + private fun createOpenNodeDetailIntent(destNum: Int): PendingIntent { + val deepLinkUri = "$DEEP_LINK_BASE_URI/node/$destNum".toUri() + val deepLinkIntent = + Intent(Intent.ACTION_VIEW, deepLinkUri, context, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_SINGLE_TOP + } + + return TaskStackBuilder.create(context).run { + addNextIntentWithParentStack(deepLinkIntent) + getPendingIntent(destNum, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT) + } + } + private fun commonBuilder( type: NotificationType, contentIntent: PendingIntent? = null, diff --git a/app/src/main/java/com/geeksville/mesh/ui/Main.kt b/app/src/main/java/com/geeksville/mesh/ui/Main.kt index 62dba6474f..cb521c3da2 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/Main.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/Main.kt @@ -165,6 +165,7 @@ fun MainScreen(uIViewModel: UIViewModel = hiltViewModel(), scanModel: BTScanMode val requestChannelSet by uIViewModel.requestChannelSet.collectAsStateWithLifecycle() val sharedContactRequested by uIViewModel.sharedContactRequested.collectAsStateWithLifecycle() val unreadMessageCount by uIViewModel.unreadMessageCount.collectAsStateWithLifecycle() + val pendingDeepLink by uIViewModel.deepLinkRequested.collectAsStateWithLifecycle() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { val notificationPermissionState = rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS) @@ -248,6 +249,32 @@ fun MainScreen(uIViewModel: UIViewModel = hiltViewModel(), scanModel: BTScanMode val currentDestination = navController.currentBackStackEntryAsState().value?.destination val topLevelDestination = TopLevelDestination.fromNavDestination(currentDestination) + // Handle pending deep links (e.g., from notifications) once NavHost is ready + LaunchedEffect(pendingDeepLink) { + pendingDeepLink?.let { uri -> + // Prefer explicit route for node details to ensure correct navigation + if (uri.scheme == "meshtastic" && uri.host == "meshtastic") { + val segments = uri.pathSegments + if (segments.isNotEmpty() && segments[0] == "node") { + val destNum = segments.getOrNull(1)?.toIntOrNull() + val routed = + runCatching { navController.navigate(NodesRoutes.NodeDetailGraph(destNum)) } + .onFailure { ex -> Timber.w(ex, "Failed to navigate via NodeDetailGraph for: $uri") } + .isSuccess + if (!routed) { + runCatching { navController.navigate(uri) } + .onFailure { ex -> Timber.w(ex, "Failed to navigate to deep link: $uri") } + } + uIViewModel.clearDeepLinkRequested() + return@let + } + } + runCatching { navController.navigate(uri) } + .onFailure { ex -> Timber.w(ex, "Failed to navigate to deep link: $uri") } + uIViewModel.clearDeepLinkRequested() + } + } + // State for determining the connection type icon to display val selectedDevice by scanModel.selectedNotNullFlow.collectAsStateWithLifecycle()