Skip to content

Fixing Notification Action Reliability on Android#8

Merged
ScottMorris merged 1 commit intomainfrom
blog/notification-action-reliability
Feb 24, 2026
Merged

Fixing Notification Action Reliability on Android#8
ScottMorris merged 1 commit intomainfrom
blog/notification-action-reliability

Conversation

@ScottMorris
Copy link
Copy Markdown
Contributor

Adds a new blog post documenting the investigation and resolution of three separate issues in Tauri's Android notification plugin, surfaced while building Threshold and addressed as upstream contributions to the plugin.

Context

Tauri's notification plugin allows apps to define action buttons on notifications (dismiss, snooze, etc.). On Android, these actions were being silently dropped across all apps using the plugin. The work spans two repositories: ScottMorris/tauri-plugins-workspace (notification-actions-fix branch) and liminal-hq/threshold.

Discovery 1: Action-Group Storage Keying

NotificationStorage.kt was writing action groups using the action type's string ID as a key suffix (e.g. "idalarm-actions") but reading them back using numeric indices ("id0", "id1"). The keys never matched, so every action group lookup returned nothing and actionId was always empty on delivery, causing the app-side handler to silently discard the event.

This issue was first surfaced by @Innominus in June 2025 (tauri-apps/plugins-workspace#2805), who also extended the fix to support registering action types from Rust. The PR sat unreviewed for eight months. The post documents the collaboration to get it across the finish line.

Discovery 2: Payload Shape Mismatch

When a notification is reconstructed after a channel reset, or when extras round-trip through org.json.JSONObject.toString() and back, a nameValuePairs wrapper leaks into the serialised output. The Tauri bridge was passing this through verbatim, causing actionId to be undefined in JavaScript. A recursive normalisation pass was added to the guest JS layer to unwrap the artefact and rebuild a clean ActionPerformedNotification.

Discovery 3: Cold-Start Timing Gap

Android fires the BroadcastReceiver the moment a user taps a notification action. On a cold boot, Threshold's Rust core takes ~400ms to initialise before the event bridge is open. Any action tapped in that window was lost. The fix introduces a listener-ready handshake: the Kotlin plugin buffers events in a keyed, persistent queue (SharedPreferences) until the JS layer calls register_action_listener_ready, then drains through the registered callback. Persisted events survive reloads, are TTL-filtered (24h), and are deduplicated on restore. The new command is wired into Tauri v2's permission system.

Threshold Architecture

Alongside the plugin work, Threshold's notification architecture was refactored to match the cleaner model the fixed plugin enables. Action type ownership was moved to context owners via a provider-based model. AlarmNotificationService was extracted to own the full ringing alarm lifecycle. The plugin fork is vendored as a git submodule with CI updated to build it before dependency resolution.

@ScottMorris ScottMorris added blog Blog posts, rendering, or content pipeline content Site copy, wording, and non-code content updates labels Feb 24, 2026
@ScottMorris ScottMorris changed the title feat(blog): Fixing Notification Action Reliability on Android Fixing Notification Action Reliability on Android Feb 24, 2026
@ScottMorris ScottMorris force-pushed the blog/notification-action-reliability branch 5 times, most recently from 1547e4f to c6278a0 Compare February 24, 2026 21:04
Adds a new blog post documenting the investigation and resolution of three
separate issues in Tauri's Android notification plugin, surfaced while building
Threshold and addressed as upstream contributions to the plugin itself.

**Context**

Tauri's notification plugin allows apps to define action buttons on notifications
(dismiss, snooze, etc.). On Android, these actions were being silently dropped
across all apps using the plugin. The work described spans two repositories:
ScottMorris/tauri-plugins-workspace (notification-actions-fix branch) and
liminal-hq/threshold.

**Discovery 1: Action-Group Storage Keying**

NotificationStorage.kt was writing action groups using the action type's string
ID as a key suffix (e.g. "idalarm-actions") but reading them back using numeric
indices ("id0", "id1"). The keys never matched, so every action group lookup
returned nothing. This meant actionId was always empty when a notification action
arrived, causing the app-side handler to silently discard the event.

This issue was first surfaced by @Innominus in June 2025 (tauri-apps PR #2805),
who also extended the fix to support registering action types from Rust. The PR
sat unreviewed for eight months before being independently rediscovered while
working on Threshold. The post documents the collaboration to get the fix across
the finish line.

**Discovery 2: Payload Shape Mismatch**

Android can serialise notification action extras in two shapes. When a
notification is reconstructed after a channel reset, or when extras round-trip
through org.json.JSONObject.toString() and back, a "nameValuePairs" wrapper
leaks into the output. The Tauri bridge was passing this through to JavaScript
verbatim, causing actionId to be undefined. A normalisation pass was added to
the guest JS layer to recursively unwrap the artefact and rebuild a clean
ActionPerformedNotification. The normaliser also deduplicates events and handles
array-like bridge objects.

**Discovery 3: Cold-Start Timing Gap**

Android fires the BroadcastReceiver the moment a user taps a notification action.
On a cold boot, Threshold's Rust core takes ~400ms to initialise before the
event bridge is open. Any action tapped in that window was lost. The same gap
exists after a WebView reload.

The fix introduces a listener-ready handshake: the Kotlin plugin buffers incoming
action events in a keyed, persistent queue (SharedPreferences) until the
JavaScript layer calls register_action_listener_ready. The plugin then drains the
queue through the registered callback. Persisted events survive reloads and are
TTL-filtered (24h) and deduplicated on restore. The new command is wired into
Tauri v2's permission system via an autogenerated allow/deny TOML pair, included
in the plugin's default permission set.

**Threshold Architecture**

Alongside the plugin work, Threshold's notification architecture was refactored
to match the cleaner model the fixed plugin enables. Action type ownership was
moved from a central startup registry to context owners (provider-based
registration). AlarmNotificationService was extracted to own the full ringing
alarm lifecycle. The plugin fork is vendored as a git submodule with CI updated
to build it before dependency resolution.

**Post details**

- File: content/blog/2026-02-24-notification-action-reliability.md
- Slug: notification-action-reliability-android
- Tags: Threshold, Tauri, Android, Notifications, Rust, Kotlin, Open Source
- Canadian spelling throughout
- Links to tauri-apps PR #2805 and Tauri notification plugin docs
@ScottMorris ScottMorris force-pushed the blog/notification-action-reliability branch from c6278a0 to 51a6239 Compare February 24, 2026 21:29
@ScottMorris ScottMorris merged commit e7e4d69 into main Feb 24, 2026
@ScottMorris ScottMorris deleted the blog/notification-action-reliability branch February 24, 2026 21:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

blog Blog posts, rendering, or content pipeline content Site copy, wording, and non-code content updates

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant