Repository navigation
Conversation
Android gains a persisted event queue that delivers native events to `addVoltraListener` (ADR 0014): `interaction`, `notificationDismissed` and `notificationTapped`, replayed in order to the first listener for their type, at most once. An `interaction` that finds no running app starts the app's JavaScript through a headless task. This is the channel callstackincubator#314 and callstackincubator#316 depend on; the receivers that produce notification events follow separately.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What is this?
Android has no way to tell an app's JavaScript that something happened while the app was not watching.
addVoltraListenerwarns "only supported on iOS" for every event and returns a no-op. #354 removed the Kotlin event bus because it could never hold an event, and the one native-to-JavaScript signal that exists,onOngoingNotificationEnded(ADR 0012), is internal, not queued, and only reaches a running app.This adds the event channel that #314 (Dynamic Widget taps) and #316 (ongoing notification actions, replies, taps and dismissals) depend on. On Android,
addVoltraListenernow returns a working subscription forinteraction,notificationDismissedandnotificationTapped. A newregisterInteractionHandlerregisters aninteractionhandler at module scope. Nothing produces events yet: the notification receivers come in the follow-up PR for #316. The decision is recorded in ADR 0014.How does it work?
commit()beforesendreturns, so an event survives the process being reclaimed right after a broadcast receiver finishes. Events are ordered by a persistedseqand carry a randomeventId. The queue follows Android Dynamic Widgets: deliver interaction events to the React Native app (app-level half of #276) #314's rules: at most 200 events (oldest dropped with a warning), entries older than 7 days pruned, and unreadable entries skipped without losing the rest. All event types share one queue.setEventListenerActive(type, true)) and off when its last one is removed. Turning a type on drains that type's queued events, oldest first, through a codegenonVoltraEventemitter, markeddelivery: 'replay'. An event sent while a listener is active is emitted at once as'live'. Events of types nobody listens to keep their place, so anotificationDismissedlistener cannot consume interactions meant for a listener that registers a moment later. Sending and draining share one lock, so an event sent during a replay is emitted after the replayed ones. Each event is removed from the queue just before it is emitted: delivery is at most once.HeadlessJsTaskServicewhen aninteractionfinds no React instance in the process and the app is not in the foreground. The receiver takes the wake lock and callsstartForegroundServicebefore it returns. The service runs as ashortServiceforeground service with a silent, low-importance notification. Dismissals and taps never wake JavaScript; they wait for the next listener. The headless task, registered by@use-voltra/android-clienton import, does no work itself: loading the bundle runs the app's module-scope listeners, which drain the queue, and the task keeps the process alive until their promises settle.enableNotifications, the Expo plugin declares the service and addsFOREGROUND_SERVICEandWAKE_LOCK.VoltraViewon Android stops subscribing tointeraction. Android views never emitted interaction events, and a view's listener would now drain notification actions meant for the app's own listener. ItsonInteractionprop still warns, and its callback is now typed withVoltraViewInteractionEvent, which has the same fields as before.ADR 0014 explains why the channel differs from #314 in places: one queue instead of one per event kind, and headless delivery instead of only a seam for it.
Why is this useful?
Native code on Android can now report events that JavaScript receives reliably: in order, once, and even when they happen while the app is not running. #316 builds on this so that a press on "Pause" or "Add tip" in an ongoing notification runs app code without opening the app. #314 can add widget taps by producing new
VoltraEventsubclasses, without changing the queue, the policy or the JavaScript API.