android: the downloads say so in the shade
A Save still running with the app in the background now posts a progress notice — how many files, the one it is on, a bar, Stop all and Open — and takes it down when the queue empties. It sits on its own low-importance channel so it can be silenced without silencing the player's card, and rides its own dataSync foreground service rather than the media one: a mediaPlayback service stops itself when the bar empties, a download is not playback, and nothing else was keeping the process alive for a Save with nothing playing — which is how the first real Save died, four tracks in. Stop all goes through the same cancel(id) a row's tap takes, and Transfers grew the matching button beside Clear finished. The walk pushes two rows straight into the transfer store and reads the real notification back out of `dumpsys notification`, without fetching anything.
This commit is contained in:
1 parent
4fc6e9c630
commit
5738c2d7bf
31 files changed
+1358
-6
No files matched your search
+72
-2
@@ -18,9 +18,11 @@ implements is `research/mobile-mockup/` (gitignored; see its README).
|
||||
| `src/mobile/lib.svelte.js` | albums, recents, follows, playlists; artists derived from the albums |
|
||||
| `src/mobile/catalog.svelte.js` | the catalog lookups, cached per session: an artist by name (the desktop's most-followed-exact-match rule), a catalog record by id |
|
||||
| `src/mobile/media.svelte.js` | the lock-screen card's wiring: mirrors the bar into the media session plugin on every transport change (plus a top-up every 30 s), carries out the card's, the headset's and audio focus's actions |
|
||||
| `src/mobile/xfernote.svelte.js` | the shade's download notice: mirrors what is still in flight into the transfers plugin (throttled, one post per 700 ms of changed content) and carries out its Stop all and Open |
|
||||
| `src/mobile/slsk.svelte.js` | the Soulseek hub: the connection, the rooms, the chats, the buddies — the desktop's `slsk` store and events |
|
||||
| `src/mobile/contacts.svelte.js` | the local view of a person on the network: a petname, a group, a note and the two marks (`contacts_meta_list` / `contact_meta_set`) — this phone only, published nowhere |
|
||||
| `crates/tauri-plugin-media-session/` | a Tauri plugin with a Kotlin foreground service (`MediaService`) that owns the `MediaSession`, the MediaStyle notification, audio focus and the becoming-noisy pause. The audio never passes through it; it is what keeps the process alive in the background and what the lock screen shows |
|
||||
| `crates/tauri-plugin-transfer-notice/` | a second Tauri plugin with a second Kotlin foreground service (`TransferService`, `dataSync`) — the downloads' own progress notification on its own low-importance channel, with Stop all and Open on it, and the reason a Save outlives the app going to the background with nothing playing |
|
||||
| `src/mobile/Sheet.svelte`, `HalfSheet.svelte`, `SyncRoom.svelte`, `Lane.svelte`, `Cover.svelte`, `Release.svelte` | the player (art / lyrics / queue faces, container transform from the mini bar, drags), the bottom sheets (track, record, sort, the page overflow, the sleep timer), the tap-to-sync room, the waveform lane, the two card pieces (a long press on a card is its sheet) |
|
||||
| `src/mobile/screens/` | Home, Search (the library half: records and artists here, tracks through `library_track_search`), Library (albums / artists / tracks / playlists), Album, Artist (the library's shell at once, the catalog dissolving in: portrait, Popular, discography with what is here marked), CatalogAlbum (a catalog record, rows the library has play), Playlist, Community (sign in; rooms, chat, buddies, taste — the Soulseek side; the private layer waits), Room, Thread, You (Appearance switches, real) |
|
||||
| `src/mobile/prefs.svelte.js` | Appearance, kept in localStorage: ambience, accent from artwork, motion |
|
||||
@@ -584,6 +586,68 @@ it means splitting `.trk` (one flex row with a whole-row `:active`) and
|
||||
rewriting the walk's `.trk .r` selectors, for no gain a screen reader
|
||||
would notice.
|
||||
|
||||
## 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`.
|
||||
|
||||
## Performance
|
||||
|
||||
`tests/mobile/perf.mjs` logs the frames the WebView produces through
|
||||
@@ -718,7 +782,9 @@ waveform growing; backend events go over the mock's own bus, which
|
||||
press → Search → Library and its tabs → a record → its sheets → the player's
|
||||
three faces → the back gesture → an artist page → Community → a room → a
|
||||
catalog search → a song through the resolver → the lane rising in the sheet →
|
||||
a catalog record, its Save and the Transfers burst → Gaps → the playlist
|
||||
a catalog record, its Save and the Transfers burst (and the shade's notice:
|
||||
posted with a count while rows are in flight, gone when they are not) →
|
||||
Gaps → the playlist
|
||||
shelf and a page with every row kind (a local row, the stream row through
|
||||
the resolver, the filter, a removal and its undo, unpin, rename, a new
|
||||
playlist, Add to playlist from a record's sheet, delete) → the import doors
|
||||
@@ -747,7 +813,11 @@ harness's to set.
|
||||
|
||||
The phone walk is the same driver on the real thing (plus a step kind of
|
||||
its own: `{ adb: [...] }` runs an adb command — the share-sheet intent, a
|
||||
key; every CDP call times out after 45 s and the reload step re-attaches
|
||||
key — and a step may carry a `probe` as well, a function whose reading of
|
||||
the system is merged into that step's own expectation, which is how the
|
||||
shade's notice is asserted out of `dumpsys notification`; the shade steps
|
||||
push two rows straight into the transfer store rather than fetching
|
||||
anything, on purpose; every CDP call times out after 45 s and the reload step re-attaches
|
||||
to the page, since a WebView that stops answering once hung a walk for ten
|
||||
minutes; its audio check counts SoulStream's OWN tracks' underruns from the
|
||||
mixer dump — the thread-wide counters move with any app's notification
|
||||
|
||||
Reference in new issue
Block a user