## 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`.