Skip to content

fix(notifications): make action buttons work and keep IDs stable across restarts - #138

Open
renatoaraujoc wants to merge 1 commit into
sameerasw:mainfrom
renatoaraujoc:fix/notification-actions
Open

fix(notifications): make action buttons work and keep IDs stable across restarts#138
renatoaraujoc wants to merge 1 commit into
sameerasw:mainfrom
renatoaraujoc:fix/notification-actions

Conversation

@renatoaraujoc

Copy link
Copy Markdown

Fixes #136 and #137.

Both issues live on the notification action path, and neither is specific to a particular client or transport — anything speaking the protocol hits them.

Plain action buttons never fire (#136)

WebSocketMessageHandler reads the optional text field with optString(), which returns "" (not null) when the field is absent. That empty string reached performNotificationAction(), whose branch was if (replyText != null), so an absent text still selected the inline reply path: plain buttons failed the remoteInputs check and returned false. The else branch that calls pendingIntent.send() was unreachable for anything arriving over the WebSocket.

The guard now tests for a non-empty string, and the handler normalizes an absent field to null.

BleTransportBridge had a related gap: it parsed id and actionName but dropped parts[2], so inline replies never worked over BLE at all. It now forwards the reply text, treating empty as "no reply" so both transports behave the same.

Actions and dismissals break after a process restart (#137)

activeNotifications was only populated from onNotificationPosted, so once the process restarted, nothing already in the shade was actionable — clients got not found for notifications that were still on screen. onListenerConnected() now re-registers what is currently posted.

Re-registering by itself isn't sufficient, which is worth spelling out since it's not obvious: generated IDs embed postTime, and within a session the first-seen ID is preserved across updates via keyToId. Once that in-memory map is gone, regenerating from the current postTime yields a different ID than the one the client is holding, so its requests would still miss. I confirmed this on device before adding the second half of the fix.

So the key -> id mapping is persisted (sbn.key is stable across updates and restarts) and consulted when re-registering, which makes IDs obtained before a restart keep resolving. Stale entries are pruned against the currently active notifications on each listener connect, so the store doesn't grow unbounded.

Verification

Tested on a Galaxy Z Fold7 (Android 16) against the macOS client, on a build from this branch:

  • Invoking a plain button now reaches pendingIntent.send() — logcat shows Invoked action '<name>' instead of does not support reply, and the app-side effect actually happens (media notification action changed the track; "Mark as read" on a mail notification cleared it).
  • An ID the client held before a force-stop is restored identically afterwards, instead of being regenerated with a newer postTime.
  • Media controls, which share the same incoming path, were used as a control throughout and were unaffected.

I left the ID format alone on purpose. Deriving IDs from sbn.key instead of embedding postTime would make this inherently stable and drop the need for persistence, but that's protocol-visible and felt like your call rather than something to slip into a bug fix.

…ss restarts

Fixes sameerasw#136 and sameerasw#137.

Plain action buttons (sameerasw#136)

WebSocketMessageHandler read the optional "text" field with optString(),
which returns "" when the field is absent. performNotificationAction()
branched on `replyText != null`, so an absent text was still treated as an
inline reply: every plain action button went down the reply path, failed the
remoteInputs check and returned false. Any client that omits "text" for
non-reply actions could never invoke a button. The guard now tests for a
non-empty string, and the handler normalizes an absent field to null.

BleTransportBridge also dropped the optional reply text entirely, so inline
replies never worked over BLE. It now forwards it, treating empty as "no
reply" for consistency with the WebSocket path.

Actions after a process restart (sameerasw#137)

activeNotifications was only populated from onNotificationPosted, so after a
process restart nothing already in the shade was actionable — clients got
"not found" for notifications that were still visible. onListenerConnected()
now re-registers what is currently posted.

Re-registering alone is not enough: generated IDs embed postTime, and during
a session the first-seen ID is preserved across updates via keyToId. Once
that in-memory map is gone, regenerating from the current postTime produces a
different ID than the one the client holds, so its requests would still miss.
The key -> id mapping is therefore persisted (sbn.key is stable across
updates and restarts) and consulted when re-registering, so IDs a client
obtained before the restart keep resolving. Stale entries are pruned against
the currently active notifications on each listener connect.

Verified on a Galaxy Z Fold7 (Android 16) against the macOS client: invoking
a plain button now reaches pendingIntent.send() and the app-side effect
happens, and an ID held by the client before a force-stop is restored
identically afterwards.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

Status: In review

Development

Successfully merging this pull request may close these issues.

[Bug] Notification action buttons from the Mac app never fire — empty "text" is treated as an inline reply

2 participants