Skip to content

fix(notifications): open node detail when tapping 'New Node Seen' notification#5752

Merged
jamesarich merged 4 commits into
meshtastic:mainfrom
LesterCheng:fix/new-node-notification-deep-link
Jun 17, 2026
Merged

fix(notifications): open node detail when tapping 'New Node Seen' notification#5752
jamesarich merged 4 commits into
meshtastic:mainfrom
LesterCheng:fix/new-node-notification-deep-link

Conversation

@LesterCheng

Copy link
Copy Markdown
Contributor

Bug

The "New Node Seen" notification does nothing when tapped.

Root cause

NodeManagerImpl.handleReceivedUser dispatches the notification through the generic KMP path:

notificationManager.dispatch(Notification(title = ..., body = ...))

AndroidNotificationManager.dispatch builds a NotificationCompat.Builder but never calls setContentIntent(...), so taps are no-ops.

A dedicated MeshServiceNotificationsImpl.showNewNodeSeenNotification(node) already wires the correct deep link via createOpenNodeDetailIntent(nodeNum)/nodes/{nodeNum}, but it is dead code — nothing in production invokes it. (Likely regressed when notifications were reworked to go through the generic NotificationManager.dispatch path.)

Fix

Smallest KMP-friendly change: enrich the generic Notification model with an optional deepLinkUri and have the Android dispatcher synthesize a PendingIntent when present.

  1. core/repository/.../Notification.kt — add val deepLinkUri: String? = null.
  2. core/data/.../NodeManagerImpl.kt — set deepLinkUri = "meshtastic://meshtastic/nodes/${next.num}" and id = next.num (so repeated emissions for the same node update the existing notification instead of duplicating, matching what showNewNodeSeenNotification did).
  3. core/service/androidMain/.../AndroidNotificationManager.kt — when deepLinkUri != null, build a TaskStackBuilder PendingIntent with ACTION_VIEW targeting MainActivity and attach via setContentIntent(...). Uses Class.forName("org.meshtastic.app.MainActivity") (mirroring MeshServiceNotificationsImpl) to avoid a :core:service:androidApp dependency.

The rest works because the deep-link infrastructure already exists end-to-end:

notification tap
  → MainActivity.handleIntent(intent)
  → model.handleDeepLink(uri)
  → DeepLinkRouter.route("/nodes/{n}") → [NodesRoute.Nodes, NodesRoute.NodeDetail(n)]
  → MultiBackstack.handleDeepLink(navKeys)  // switches tab + replaces stack

Because MultiBackstack.handleDeepLink replaces the active stack with [Nodes, NodeDetail], pressing back from NodeDetail naturally returns to the Nodes list — exactly the requested behavior.

The manifest already declares the URI scheme/host on MainActivity:

<data android:scheme="meshtastic" android:host="meshtastic" />

Tests

AndroidNotificationManagerTest — 3 new cases:

  • dispatch attaches deep-link PendingIntent when deepLinkUri is set — asserts posted.contentIntent non-null and the wrapped Intent is ACTION_VIEW with the correct URI + MainActivity component.
  • dispatch leaves contentIntent unset when deepLinkUri is null — guards against false positives.
  • dispatch uses provided notification id as system id — verifies explicit-id cancellation works.

Test infrastructure:

  • New test-only stub core/service/src/androidHostTest/.../org/meshtastic/app/MainActivity.kt extending Activity so Class.forName("org.meshtastic.app.MainActivity") resolves under Robolectric without pulling :androidApp into :core:service's test classpath.
  • registerStubMainActivity() helper using shadowOf(packageManager).addOrUpdateActivity(ActivityInfo) so TaskStackBuilder.addNextIntentWithParentStack doesn't throw NameNotFoundException.

Why not just revive showNewNodeSeenNotification?

The KMP-friendly fix benefits Desktop too — DesktopNotificationManager currently ignores deepLinkUri but can honor it in a follow-up. Reviving the dead Android-specific path would have re-coupled NodeManagerImpl to the MeshServiceNotifications interface and left Desktop with the same broken behavior.

Why hardcode the URI string in NodeManagerImpl?

DEEP_LINK_BASE_URI lives in core/navigation/Routes.kt, which depends on Compose Navigation 3. Pulling that into core/data (a pure data layer) would be a heavy cross-cutting change for one string constant. The literal is documented with a comment pointing at DEEP_LINK_BASE_URI so they stay in sync.

Verification

  • ./gradlew :core:repository:allTests
  • ./gradlew :core:data:allTests
  • ./gradlew :core:service:allTests :core:service:testAndroidHostTest :core:service:detekt
  • ./gradlew spotlessApply spotlessCheck detekt assembleDebug
  • ./gradlew test allTests -x :desktopApp:test ✓ (full repo)

Note: LinuxNotificationSenderTest (3 cases) fails in my agent's container because no libnotify daemon is installed. The same 3 tests fail on main in the same environment — unrelated to this change.

Follow-up (out of scope)

MeshServiceNotifications.showNewNodeSeenNotification + its Android implementation can be removed in a cleanup PR now that nothing calls it.

@CLAassistant

CLAassistant commented Jun 8, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@github-actions github-actions Bot added the bugfix PR tag label Jun 8, 2026
@LesterCheng

Copy link
Copy Markdown
Contributor Author

Bug report to come, will update here when report has been appropriately filed.

@jamesarich

Copy link
Copy Markdown
Collaborator

@LesterCheng please sign CLA

…ification

The 'New Node Seen' notification dispatched from NodeManagerImpl had no
content intent attached, so tapping it did nothing. AndroidNotificationManager.dispatch
built a NotificationCompat.Builder without ever calling setContentIntent(...).

Enrich the generic KMP Notification model with an optional deepLinkUri and
have the Android dispatcher synthesize a TaskStackBuilder PendingIntent when
one is provided. NodeManagerImpl now sets deepLinkUri to meshtastic://meshtastic/nodes/{nodeNum}
and id to the node num (so repeated emissions update the existing notification
instead of duplicating).

The existing deep-link plumbing in MainActivity -> DeepLinkRouter ->
MultiBackstack handles the rest: /nodes/{n} routes to [NodesRoute.Nodes,
NodesRoute.NodeDetail(n)] and MultiBackstack.handleDeepLink switches to the
Nodes tab and replaces the stack, so back from NodeDetail returns naturally
to the Nodes list.

Tests:
- AndroidNotificationManagerTest: 3 new cases covering PendingIntent is
  attached with correct ACTION_VIEW + URI + component, contentIntent stays
  null when deepLinkUri is null, and explicit notification id is honored.
- Test-only stub MainActivity at org.meshtastic.app so Class.forName resolves
  under Robolectric without pulling :androidApp into the :core:service test
  classpath. Activity registered dynamically via shadowOf(packageManager)
  so TaskStackBuilder.addNextIntentWithParentStack succeeds.

Note: MeshServiceNotificationsImpl.showNewNodeSeenNotification already wires
this deep link correctly but is dead code (never invoked from production).
Left as-is; can be cleaned up in a follow-up PR.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jamesarich jamesarich enabled auto-merge June 16, 2026 23:24
@jamesarich jamesarich added this pull request to the merge queue Jun 16, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jun 16, 2026
@jamesarich jamesarich added this to the 2.8.0 milestone Jun 17, 2026
@jamesarich jamesarich enabled auto-merge June 17, 2026 02:02
@jamesarich jamesarich added this pull request to the merge queue Jun 17, 2026
Merged via the queue into meshtastic:main with commit ae3e3d2 Jun 17, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix PR tag

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants