M1-06: Foreground service, audio focus, and route hardening #6

Open
opened 2026-08-08 20:03:44 -04:00 by agent · 0 comments
Member

Record with the screen off, and stop losing recordings to route changes and interruptions.
This is the single biggest reliability complaint about v2, restated for Android.

Wes, on what this has to be: "It handles microphone grabbing beautifully. It doesn't crash like
the other one, or lock up, like the other one."

The three failure modes to design against

They were observed on iOS, and every one of them has an Android analogue that is at least as likely:

1 — Holding a stale microphone instead of probing the current default. Recorded on Bluetooth,
backgrounded for an hour, reconnected, started a new recording — and the app was still holding the
old route.

2 — Swapping to wired headphones produced a recording of nothing. The recording UI appeared,
nothing was captured, and stopping froze the app on "saving" for what turned out to be four seconds
of silence.

3 — A declined phone call ended the recording. "I didn't answer it. I declined it. But the
recording had already stopped."
On Android this arrives as a transient audio focus loss, and the
default behaviour of most implementations is exactly the bug.

Scope

  • A foreground service with FOREGROUND_SERVICE_MICROPHONE, started before capture begins.
    Android 14 requires the type and the matching permission, and a service without them is killed
    with an exception that does not explain itself.
  • POST_NOTIFICATIONS (Android 13+), because a foreground service with no visible notification is
    not a thing that exists.
  • Audio focus via AudioFocusRequest. Transient loss — the declined call — must resume, not
    terminate. Permanent loss is the only case that ends a recording, and it says so.
  • AudioDeviceCallback for route changes: follow the current input rather than caching one.
    Seamless mid-recording if achievable; if not, it must at minimum never silently record nothing.
  • Battery optimisation exemption, prompted for and explained. The Pixel is stock so doze is
    survivable, but a foreground service is the thing standing between a forty-minute walk and a
    truncated memo.
  • Never freeze on save. Finalisation is off the main thread and the UI stays responsive.

Done when — Wes validates all five on the Pixel

  • Start recording, screen off, walk for several minutes, unlock — still recording.
  • Decline an incoming call mid-recording — recording continues.
  • Swap Bluetooth for wired mid-session — audio is still captured.
  • Background the app for an hour, connect a different input, record — audio is captured.
  • A forty-minute recording with the screen off survives intact.

Milestone 1 of Stash for Android, mirroring stash-ios M1. M1 is a functioning app on the test
device with no sync at all — record voice memos reliably and get them off the device by hand.
Sync arrives in M3.

Test device is a Pixel 6 (Tensor G1) borrowed from a friend, until something better turns up.
Stock Android, so no OEM battery-killer behaviour to fight — worth remembering that a phone from
another manufacturer may not be as forgiving about background work.

Translation of Stash/stash-ios#6. Where the platform forces a different answer this issue says so; where it
does not, the iOS issue is the fuller statement of intent and should be read alongside this one.

Working agreement for every issue in this repo: feature branch, tested before the PR opens
(on-device where the label says so), then Wes reviews the PR and we walk the code together. Docs
update in the same commit. A PR that takes more than 20 minutes to review is too big — say so and
split it.

Record with the screen off, and stop losing recordings to route changes and interruptions. **This is the single biggest reliability complaint about v2, restated for Android.** Wes, on what this has to be: *"It handles microphone grabbing beautifully. It doesn't crash like the other one, or lock up, like the other one."* ## The three failure modes to design against They were observed on iOS, and every one of them has an Android analogue that is at least as likely: **1 — Holding a stale microphone instead of probing the current default.** Recorded on Bluetooth, backgrounded for an hour, reconnected, started a new recording — and the app was still holding the old route. **2 — Swapping to wired headphones produced a recording of nothing.** The recording UI appeared, nothing was captured, and stopping froze the app on "saving" for what turned out to be four seconds of silence. **3 — A declined phone call ended the recording.** *"I didn't answer it. I declined it. But the recording had already stopped."* On Android this arrives as a transient audio focus loss, and the default behaviour of most implementations is exactly the bug. ## Scope - A **foreground service** with `FOREGROUND_SERVICE_MICROPHONE`, started before capture begins. Android 14 requires the type and the matching permission, and a service without them is killed with an exception that does not explain itself. - `POST_NOTIFICATIONS` (Android 13+), because a foreground service with no visible notification is not a thing that exists. - **Audio focus** via `AudioFocusRequest`. Transient loss — the declined call — must **resume**, not terminate. Permanent loss is the only case that ends a recording, and it says so. - **`AudioDeviceCallback`** for route changes: follow the current input rather than caching one. Seamless mid-recording if achievable; if not, it must at minimum never silently record nothing. - **Battery optimisation exemption**, prompted for and explained. The Pixel is stock so doze is survivable, but a foreground service is the thing standing between a forty-minute walk and a truncated memo. - Never freeze on save. Finalisation is off the main thread and the UI stays responsive. ## Done when — Wes validates all five on the Pixel - [ ] Start recording, screen off, walk for several minutes, unlock — still recording. - [ ] **Decline an incoming call mid-recording — recording continues.** - [ ] Swap Bluetooth for wired mid-session — audio is still captured. - [ ] Background the app for an hour, connect a different input, record — audio is captured. - [ ] A forty-minute recording with the screen off survives intact. --- *Milestone 1 of Stash for Android, mirroring `stash-ios` M1. M1 is a functioning app on the test device with **no sync at all** — record voice memos reliably and get them off the device by hand. Sync arrives in M3.* *Test device is a **Pixel 6** (Tensor G1) borrowed from a friend, until something better turns up. Stock Android, so no OEM battery-killer behaviour to fight — worth remembering that a phone from another manufacturer may not be as forgiving about background work.* *Translation of `Stash/stash-ios#6`. Where the platform forces a different answer this issue says so; where it does not, the iOS issue is the fuller statement of intent and should be read alongside this one.* *Working agreement for every issue in this repo: feature branch, tested before the PR opens (on-device where the label says so), then Wes reviews the PR and we walk the code together. Docs update in the same commit. A PR that takes more than 20 minutes to review is too big — say so and split it.*
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Stash/stash-android#6
No description provided.