Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion app/src/main/java/com/geeksville/mesh/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
Expand Down
12 changes: 12 additions & 0 deletions app/src/main/java/com/geeksville/mesh/model/UIState.kt
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,18 @@ constructor(
_requestChannelSet.value = null
}

private val _deepLinkRequested: MutableStateFlow<Uri?> = MutableStateFlow(null)
val deepLinkRequested: StateFlow<Uri?>
get() = _deepLinkRequested.asStateFlow()

fun setDeepLinkRequested(url: Uri) {
_deepLinkRequested.value = url
}

fun clearDeepLinkRequested() {
_deepLinkRequested.value = null
}

override fun onCleared() {
super.onCleared()
Timber.d("ViewModel cleared")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down
27 changes: 27 additions & 0 deletions app/src/main/java/com/geeksville/mesh/ui/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current message notification routes to the message screen appropriately without this, using the Navigation sdk deeplinking that's already in place. Why do we need additional handling here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I honestly wasnt able to get it working without the extra handling, just kept opening the node lsit. I can take another stab at it later today when I'm at the airport

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()

Expand Down