Feat/android fgs survive task removal - #1234
Conversation
WPT non-regression comparisonERROR — the comparison did not produce a report; the test run itself likely failed. Workflow run · this comment is updated on every push. |
| </TabItem> | ||
| </Tabs> | ||
|
|
||
| ### Keeping the recording alive when the app is closed |
There was a problem hiding this comment.
Is this android specific? Maybe we could add a badge?
| assignOnErrorCallbackId(0); | ||
| } | ||
| void assignOnErrorCallbackId(uint64_t callbackId); | ||
| // Defined inline so AudioRecorder.cpp doesn't drag this class's whole |
There was a problem hiding this comment.
Is it possible to omit/reduce this trip to gain access to Recorder?
The recording notification's pause, resume and stop actions now act on the recorder natively, so they keep working after the app task is removed while the foreground service (stopWithTask=false) keeps the recording alive: - ActiveRecorderHandle: process-global one-slot handle to the live recorder (registered by AudioRecorderHostObject), with a consume-once stash of the file info produced by a native stop - NativeRecorderControl: static-JNI entry points callable from Kotlin without a React context; the notification receiver stops/pauses/resumes through it on an executor and still emits the matching AudioEvent so a live app can sync its UI (new event: RECORDING_NOTIFICATION_STOP) - RecordingNotification rewritten to standard NotificationCompat actions (RemoteViews layouts removed), rebuilt on every show(); adds stop action, action titles, deepLinkUri tap routing (ACTION_VIEW) and a chronometer that excludes paused spans; native pause/resume re-post the notification so the action button flips without JS - onErrorAfterClose now restores the pre-teardown state after a stream reclaim instead of force-resuming a paused recording Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
With stopWithTask=false a recording outlives the app UI, but a remounted screen (or relaunched app) had no way to learn about it — recorder state was only reachable through the instance that started it: - new JSI globals backed by ActiveRecorderHandle, surfaced as statics: AudioRecorder.isRecordingOngoing() and the consume-once AudioRecorder.takeLastRecordingResult() for files finalized by the notification stop action (mock parity + jest coverage included) - Record demo mounts directly in the live recorder's state, picks up natively stopped files, keeps the recording alive across screen exits and only enables file output when no session is ongoing (re-enabling mid-recording replaces the writer and resets the duration) - deep-link routing for the notification tap (react-navigation linking), duration displays seeded from the recorder instead of assuming a fresh session, and RecordingTime rewritten to plain state — the animated-prop binding went stale on the frozen value while paused and showed zeros Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
a16212c to
6dd83cf
Compare
michalsek
left a comment
There was a problem hiding this comment.
a bit of comments for later 👉 👈
| // A recording can outlive this screen (and, with `stopWithTask: false`, the whole | ||
| // app UI). Mounting directly in the right state lets every child initialize from | ||
| // the live recorder instead of transitioning out of a transient Idle render. |
There was a problem hiding this comment.
| // A recording can outlive this screen (and, with `stopWithTask: false`, the whole | |
| // app UI). Mounting directly in the right state lets every child initialize from | |
| // the live recorder instead of transitioning out of a transient Idle render. | |
| // Recover from "app disabled" state - recording can survive the app kill (android) |
| // The stop action already stopped the recorder natively and hid the notification; | ||
| // here we only pick up the resulting files and sync the UI. |
There was a problem hiding this comment.
| // The stop action already stopped the recorder natively and hid the notification; | |
| // here we only pick up the resulting files and sync the UI. |
| // Re-enabling file output during an ongoing recording replaces the file writer, | ||
| // which starts a new file and resets the duration — skip it when resyncing. |
There was a problem hiding this comment.
| // Re-enabling file output during an ongoing recording replaces the file writer, | |
| // which starts a new file and resets the duration — skip it when resyncing. |
There was a problem hiding this comment.
might be also worth changing on the native side to handle that and make enable a no-op in that case
| // The recording and its notification intentionally stay alive when leaving this | ||
| // screen; they can be stopped from the notification or after coming back. |
There was a problem hiding this comment.
| // The recording and its notification intentionally stay alive when leaving this | |
| // screen; they can be stopped from the notification or after coming back. |
| if (!AudioRecorder.isRecordingOngoing()) { | ||
| AudioManager.setAudioSessionActivity(false); | ||
| } |
| /** | ||
| * Shows a stop action that ends the recording natively — it works even when | ||
| * the app task has been removed and JS is unreachable. A live app is | ||
| * additionally notified through the `recordingNotificationStop` event. | ||
| * Default: false. | ||
| */ | ||
| showStopAction?: boolean; | ||
| /** Label of the pause action. Default: 'Pause'. */ | ||
| pauseActionTitle?: string; | ||
| /** Label of the resume action. Default: 'Resume'. */ | ||
| resumeActionTitle?: string; | ||
| /** Label of the stop action. Default: 'Stop'. */ | ||
| stopActionTitle?: string; | ||
| /** | ||
| * URI attached to the notification tap intent, e.g. `myapp://record`. | ||
| * Delivered through React Native's `Linking` (initial URL on cold start, | ||
| * `url` event otherwise), so it can route to a specific screen. Without it, | ||
| * tapping the notification opens the app's launcher activity. | ||
| */ | ||
| deepLinkUri?: string; | ||
| /** Shows the elapsed recording time in the notification. Default: false. */ | ||
| usesChronometer?: boolean; |
| /** | ||
| * Controls `android:stopWithTask` on the injected foreground service. When | ||
| * false, swiping the app away from recents keeps the service — and therefore | ||
| * the app process and any in-progress recording — running (Android calls | ||
| * onTaskRemoved instead of stopping the service). Defaults to true. | ||
| */ |
There was a problem hiding this comment.
| /** | |
| * Controls `android:stopWithTask` on the injected foreground service. When | |
| * false, swiping the app away from recents keeps the service — and therefore | |
| * the app process and any in-progress recording — running (Android calls | |
| * onTaskRemoved instead of stopping the service). Defaults to true. | |
| */ |
| /** | ||
| * Checks whether a recording session is ongoing (recording or paused). Native | ||
| * source of truth that needs no reference to the recorder instance, so a | ||
| * remounted screen (e.g. after navigating away and back, or reopening an app | ||
| * whose recording kept running under an Android foreground service with | ||
| * `stopWithTask: false`) can seed its UI state from it. Reflects the most | ||
| * recently created `AudioRecorder` — constructing another instance | ||
| * mid-recording displaces the probed one. | ||
| */ |
There was a problem hiding this comment.
| /** | |
| * Checks whether a recording session is ongoing (recording or paused). Native | |
| * source of truth that needs no reference to the recorder instance, so a | |
| * remounted screen (e.g. after navigating away and back, or reopening an app | |
| * whose recording kept running under an Android foreground service with | |
| * `stopWithTask: false`) can seed its UI state from it. Reflects the most | |
| * recently created `AudioRecorder` — constructing another instance | |
| * mid-recording displaces the probed one. | |
| */ |
| /** | ||
| * Returns the file info of a recording that was stopped natively (via the | ||
| * recording notification stop action, which finalizes the files even when no | ||
| * JS listener is reachable), or `null` if there is none. Consume-once: the | ||
| * result is cleared on read, so a second call returns `null`. Recordings | ||
| * stopped through {@link stop} resolve their promise with the file info | ||
| * instead and never appear here. | ||
| */ |
There was a problem hiding this comment.
| /** | |
| * Returns the file info of a recording that was stopped natively (via the | |
| * recording notification stop action, which finalizes the files even when no | |
| * JS listener is reachable), or `null` if there is none. Consume-once: the | |
| * result is cleared on read, so a second call returns `null`. Recordings | |
| * stopped through {@link stop} resolve their promise with the file info | |
| * instead and never appear here. | |
| */ |
| assignOnErrorCallbackId(0); | ||
| } | ||
| void assignOnErrorCallbackId(uint64_t callbackId); | ||
| // Defined inline so AudioRecorder.cpp doesn't drag this class's whole |
| title?: string; | ||
| contentText?: string; | ||
| paused?: boolean; // flag indicating whether to display pauseIcon or resumeIcon | ||
| paused?: boolean; // flag indicating whether to display the pause or the resume action |
There was a problem hiding this comment.
| paused?: boolean; // flag indicating whether to display the pause or the resume action | |
| paused?: boolean; |
Closes #
Introduced changes
Checklist