Everything written this session that has not been through the device: rounds 43 to 48 (each patch adversarially verified, 43's transient-notice expectation fixed), a partial round 49, the pipeline scripts, the Android Auto design and the icon comparison mock. Also the two harness changes that were never A/B'd — adaptive waits, which may only shorten a declared wait and never lengthen it, and a ship that runs the mock walk and the deploy at the same time. The A/B and its 255-step baseline are in handoff/tools; main stays at what the phone has actually walked.
62 lines
3.7 KiB
Plaintext
62 lines
3.7 KiB
Plaintext
## The downloads in the shade
|
|
|
|
Transfers is a screen inside the app; the desktop says what a Save is doing
|
|
with the ring in its title bar, the phone with a badge on the nav. Neither
|
|
is anywhere to be seen once the app is in the background, which is exactly
|
|
when a Save is left to run on its own. So a running Save now shows in the
|
|
shade: how many files are still in flight, the one it is on, a progress
|
|
bar, and two buttons — **Stop all** and **Open**.
|
|
|
|
Its own channel ("Downloads", `IMPORTANCE_LOW`, no badge, private on a
|
|
locked screen, since a filename is nobody else's business), separate from
|
|
Playback so one can be silenced without the other, and created when the app
|
|
starts rather than when the first download does — a user who wants the
|
|
downloads quiet should not have to wait for one to be noisy first.
|
|
|
|
Its own service too, and that is the part worth spelling out.
|
|
`foregroundServiceType` is per-service: a download is `dataSync`, playback
|
|
is `mediaPlayback`. `MediaService` stops itself the moment the bar empties,
|
|
so folding the notice into it would take the card down mid-Save; keeping it
|
|
alive for a download instead would leave a `mediaPlayback` service running
|
|
with no session, which Android 14+ treats as a violation. And nothing else
|
|
keeps the process alive for a Save with nothing playing — which is exactly
|
|
how the first real Save died on 2026-09-13, four tracks in. A `dataSync`
|
|
foreground service that exists while downloads are in flight is the
|
|
structural fix, and it can only be its own service. The same promote-first
|
|
rule applies: `onStartCommand` calls `startForeground()` before it looks at
|
|
anything, because Android kills the app about ten seconds after a
|
|
foreground start that never did. Android 15 caps a `dataSync` service at
|
|
six cumulative hours a day and kills the app if it is still up when the
|
|
callback returns, so `onTimeout` takes the notice down rather than being
|
|
taken down with it; the fetches are the Rust core's own and Transfers still
|
|
lists them.
|
|
|
|
The buttons come back as the plugin event `action`
|
|
(`addPluginListener("transfer-notice", "action", …)`). **Stop all** runs
|
|
`cancelAll()` in `transfers.svelte.js`, which is the same `cancel(id)` a
|
|
row's tap calls, once per live row — not a second path to the same thing.
|
|
**Open** brings the activity forward and puts the You tab on Transfers, and
|
|
the notice's body does the same. Transfers grew the matching button: Cancel
|
|
all while anything is in flight, beside Clear finished.
|
|
|
|
`src/mobile/xfernote.svelte.js` is the mirror, and it is driven by a Svelte
|
|
effect over the transfer store rather than off the `soulseek:download`
|
|
event. That is deliberate twice over: the effect tracks progress as well as
|
|
arrivals, and it means a row written straight into the store drives the
|
|
notice exactly as a fetched one does — which is how the walk exercises this
|
|
without fetching anything. Posts are throttled to one per 700 ms of changed
|
|
content; download progress arrives far faster than anyone reads it, and the
|
|
system rate-limits (and drops) notifications posted in a flood.
|
|
|
|
`crates/tauri-plugin-transfer-notice` is shaped like the media-session
|
|
plugin: `build.rs` hands its `android/` Gradle module to tauri-build,
|
|
`src/lib.rs` registers the Kotlin class and exposes `update` / `clear`, and
|
|
the Kotlin side is two files — the plugin (arguments in, the `action` event
|
|
out) and `TransferService`. The app's capability grants
|
|
`transfer-notice:default`; the plugin is registered on every platform so
|
|
the permission resolves everywhere, and on the desktop its commands answer
|
|
"not here". The tauri-build trap above applies to this plugin as well: the
|
|
Android module only reaches the app project when
|
|
`TAURI_ANDROID_PROJECT_PATH` is set, which is why adding it also touched
|
|
`src-tauri/build.rs`.
|