feat: RNode disconnect notification#628
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Greptile SummaryThis PR adds an RNode disconnect notification feature: a heads-up alert ( Key implementation highlights:
Confidence Score: 4/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant PY as reticulum_wrapper.py
participant KRB as KotlinRNodeBridge
participant RSB as ReticulumServiceBinder
participant SNM as ServiceNotificationManager
participant NM as NotificationManager (Android)
Note over PY: RNode hardware disconnects
PY->>KRB: notifyOnlineStatusChanged(false, interfaceName)
KRB->>RSB: onRNodeOnlineStatusChanged(false, interfaceName)
RSB->>SNM: updateRNodeStatus(false, interfaceName)
Note over SNM: mainHandler.post {}
SNM->>SNM: disconnectedRNodeInterfaces.add(interfaceName)
SNM->>NM: activeNotifications.any { id == NOTIFICATION_ID_RNODE }
alt Notification already visible
SNM->>NM: notify(NOTIFICATION_ID_RNODE, alert) [silent content update]
else Not visible AND outside debounce window
SNM->>NM: notify(NOTIFICATION_ID_RNODE, alert) [heads-up]
end
SNM->>NM: notify(NOTIFICATION_ID, foreground) [appends "(RNode disconnected)"]
Note over PY: RNode hardware reconnects
PY->>KRB: notifyOnlineStatusChanged(true, interfaceName)
KRB->>RSB: onRNodeOnlineStatusChanged(true, interfaceName)
RSB->>SNM: updateRNodeStatus(true, interfaceName)
Note over SNM: mainHandler.post {}
SNM->>SNM: disconnectedRNodeInterfaces.remove(interfaceName)
alt All interfaces back online
SNM->>NM: cancel(NOTIFICATION_ID_RNODE)
SNM->>SNM: lastDisconnectNotifyMs = 0L
SNM->>NM: notify(NOTIFICATION_ID, foreground) [removes "(RNode disconnected)"]
else Some interfaces still disconnected
SNM->>NM: notify(NOTIFICATION_ID_RNODE, alert) [update names]
end
|
app/src/main/java/com/lxmf/messenger/service/manager/ServiceNotificationManager.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/com/lxmf/messenger/service/manager/ServiceNotificationManager.kt
Outdated
Show resolved
Hide resolved
Thread interface name through Python→KotlinRNodeBridge→ServiceNotificationManager so users get a visible heads-up alert when their RNode radio loses connection, plus a "(RNode disconnected)" suffix on the foreground service notification. Closes #625 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
5db2fd5 to
51f523d
Compare
app/src/main/java/com/lxmf/messenger/service/manager/ServiceNotificationManager.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/com/lxmf/messenger/service/manager/ServiceNotificationManager.kt
Outdated
Show resolved
Hide resolved
Address Greptile review feedback: - Replace single rnodeOnline boolean with per-interface MutableSet (ConcurrentHashMap.newKeySet) so one interface reconnecting doesn't dismiss the alert for another still-disconnected interface - Add 10s debounce on heads-up disconnect notifications to prevent spam on flaky BLE connections - Add ServiceNotificationManagerRNodeTest (7 tests) covering disconnect/reconnect, per-interface tracking, foreground text, and debounce behaviour Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
app/src/main/java/com/lxmf/messenger/service/manager/ServiceNotificationManager.kt
Show resolved
Hide resolved
Reset lastDisconnectNotifyMs to 0 when all interfaces reconnect and the alert is cancelled, so a fresh disconnect always fires a heads-up notification even if it occurs within 10s of a previous one. Update tests to verify reconnect-disconnect cycles re-post the alert, and add test for debounce suppression when a second interface disconnects while the alert is already visible. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
0f81b4f to
3b8875e
Compare
app/src/main/java/com/lxmf/messenger/service/manager/ServiceNotificationManager.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/com/lxmf/messenger/service/manager/ServiceNotificationManager.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/com/lxmf/messenger/service/manager/ServiceNotificationManager.kt
Outdated
Show resolved
Hide resolved
Separate content updates from heads-up gating: notify() is always called to keep the notification body accurate (showing only currently disconnected interfaces), while the debounce timer only controls whether the heads-up overlay (sound + vibration) re-triggers. Track rnodeAlertVisible so a fresh disconnect always fires an alert after the user taps to dismiss (autoCancel) or after a reconnect. Add tests for partial-reconnect content updates and multi-interface notification body accuracy. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
app/src/main/java/com/lxmf/messenger/service/manager/ServiceNotificationManager.kt
Outdated
Show resolved
Hide resolved
The debounce condition was updating lastDisconnectNotifyMs but notify() was called unconditionally — making the debounce dead code. Now notify() is only called inside the debounce branches: - Fresh heads-up: when alert is not visible or cooldown expired - Silent content update: when alert is already in the shade Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| rnodeAlertVisible = true | ||
| } else if (rnodeAlertVisible) { | ||
| // Silent content-only update while alert is still in the shade | ||
| notificationManager.notify(NOTIFICATION_ID_RNODE, alert) | ||
| } | ||
| } else { | ||
| // All RNode interfaces are online — dismiss the alert | ||
| notificationManager.cancel(NOTIFICATION_ID_RNODE) | ||
| rnodeAlertVisible = false | ||
| lastDisconnectNotifyMs = 0L | ||
| } |
There was a problem hiding this comment.
Bug: If a user dismisses the disconnect notification, rnodeAlertVisible is not reset. A subsequent disconnect within the cooldown period incorrectly re-posts a new heads-up notification, bypassing the debounce logic.
Severity: MEDIUM
Suggested Fix
Use setDeleteIntent on the notification. This allows you to provide a PendingIntent that will be sent when the user dismisses the notification, which can then be used to reset the rnodeAlertVisible flag to false and keep the state synchronized.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.
Location:
app/src/main/java/com/lxmf/messenger/service/manager/ServiceNotificationManager.kt#L249-L266
Potential issue: The `rnodeAlertVisible` flag is not reset to false when a user
dismisses the RNode disconnect notification. If another disconnect event occurs within
the 10-second cooldown period, the code enters a branch intended for silent updates.
However, since the notification was already dismissed, calling
`notificationManager.notify()` creates a new notification. This new notification, being
on a high-importance channel, incorrectly triggers a new heads-up alert with sound and
vibration, defeating the purpose of the debounce logic which is to prevent notification
spam.
app/src/test/java/com/lxmf/messenger/service/manager/ServiceNotificationManagerRNodeTest.kt
Show resolved
Hide resolved
Verify that updateRNodeStatus does not refresh the foreground notification while a propagation sync is in progress, covering the documented but previously untested behaviour. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
rnodeAlertVisible was never cleared when the user tapped the notification (autoCancel), leaving the debounce in a stale state. Replace the manual flag with notificationManager.activeNotifications query — this gives ground truth about whether the notification is currently in the shade, regardless of how it was dismissed (user tap, swipe, or programmatic cancel). The debounce now has two clear branches: - Visible: silent content-only update (always allowed) - Not visible: fresh heads-up gated by 10s cooldown Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| } else { | ||
| // Notification not visible — debounce fresh heads-up postings | ||
| val now = System.currentTimeMillis() | ||
| if (now - lastDisconnectNotifyMs >= disconnectNotifyCooldownMs) { | ||
| lastDisconnectNotifyMs = now | ||
| notificationManager.notify(NOTIFICATION_ID_RNODE, alert) | ||
| } | ||
| } |
There was a problem hiding this comment.
Bug: If a user dismisses a disconnect notification, a subsequent disconnect within the 10-second debounce window will not trigger a new notification, leaving the user uninformed.
Severity: MEDIUM
Suggested Fix
Modify the logic to ensure notify() is always called when the notification is not visible (isCurrentlyVisible is false), regardless of the debounce timer. The debounce logic should only control whether the notification is posted with high priority (triggering a heads-up display and sound) versus standard priority (a silent update to the notification shade).
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.
Location:
app/src/main/java/com/lxmf/messenger/service/manager/ServiceNotificationManager.kt#L256-L263
Potential issue: The logic to show RNode disconnect notifications fails to re-post a
notification under specific conditions. If a user dismisses the initial disconnect
notification, and a second interface disconnects within the 10-second debounce window,
the `notify()` function is not called. This happens because the code checks if the
notification `isCurrentlyVisible` (which is false after dismissal) and then checks if
the debounce period has expired. When the debounce is still active, neither condition is
met, and the notification is not re-displayed, leaving the user unaware of the second
disconnection.
Summary
IMPORTANCE_HIGH) when an RNode radio disconnects, with auto-cancel on tapinterfaceNamethrough the full Python → KotlinRNodeBridge → ReticulumServiceBinder → ServiceNotificationManager chainCloses #625
Test plan
KotlinRNodeBridgeOnlineStatusTest,test_wrapper_ble.py)🤖 Generated with Claude Code