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.
86 lines
4.8 KiB
Plaintext
86 lines
4.8 KiB
Plaintext
## The tree in the car
|
|
|
|
Android Auto browses through a `MediaLibraryService`, and the system starts
|
|
that service in a process the app has never been in: no Activity, no Tauri,
|
|
no WebView, and therefore no frontend, no `#[tauri::command]` and no
|
|
resolver. Everything on a car screen has to come from somewhere else. It
|
|
comes from `src-tauri/src/auto.rs`, which opens `library.db` **read-only**
|
|
and answers with its own SQL, and from
|
|
`crates/tauri-plugin-media-session/…/LibraryBridge.kt`, which loads the
|
|
native library, hands the JSON across and turns it into `MediaItem`s.
|
|
|
|
Cold start works because of three things that were already true.
|
|
`JNI_OnLoad` (`android_glue.rs`) runs at `System.loadLibrary("app_lib")`,
|
|
before any component exists, and publishes the JavaVM and the app Context;
|
|
`crates/library` is plain rusqlite with no Tauri in it; `crates/player` is
|
|
plain rodio with no Tauri in it. So a service-started process can load the
|
|
`.so`, open the database and — in a later round — drive the engine.
|
|
`LibraryBridge` is what calls `System.loadLibrary` there, because the app's
|
|
only other loader (`com.soulstream.app.Rust`) needs an Activity to exist.
|
|
|
|
**The connection is read-only and that is not a detail.** `Library::open`
|
|
runs `PRAGMA journal_mode = WAL` and eleven `CREATE TABLE IF NOT EXISTS`
|
|
before it returns, which are writes into a database whose real owner may be
|
|
running in another process at that moment. `auto.rs` uses
|
|
`Connection::open_with_flags(…, SQLITE_OPEN_READ_ONLY)` with a 1.5-second
|
|
busy timeout, and its statements are copied from `crates/library` rather
|
|
than invented — including `album_tracks`'s disc ordering and
|
|
`recent_albums`'s `link` CTE.
|
|
|
|
**Artists are grouped in Rust the way the phone groups them in JS.** There
|
|
is no artists table; `src/mobile/lib.svelte.js` derives artists by folding
|
|
`list_albums()` in its own order, keeping the FIRST casing it sees. A SQL
|
|
`GROUP BY artist` would pick a different display name and the car would
|
|
disagree with the phone in the same hand.
|
|
|
|
The node grammar, which is the contract the rounds after this one read:
|
|
|
|
| node | |
|
|
| --- | --- |
|
|
| `ss:root[?limit=N]` | the tabs; `N` is the browser's own root-children hint |
|
|
| `ss:tab:recent` / `albums` / `artists` / `playlists` | browsable |
|
|
| `ss:album:<albums.id>` | browsable → tracks |
|
|
| `ss:artist:<pct-encoded lowercase name>` | browsable → albums |
|
|
| `ss:playlist:<playlists.id>` | browsable → rows |
|
|
| `ss:track:<tracks.id>` | playable; `tracks.path` |
|
|
| `ss:plitem:<playlist_items.id>` | playable; `playlist_items.path`, **which may be NULL** |
|
|
|
|
A playlist row of kind `stream` or `unresolved` has no file and is listed
|
|
and marked playable anyway, because the phone lists it: the round that plays
|
|
these owes them `STATE_ERROR` and "open SoulStream on your phone" rather
|
|
than a hole in the list. Catalog and Soulseek content cannot play from cold
|
|
at all — the resolver is 872 lines of shared JS and Soulseek needs a live
|
|
session — and that is the same answer.
|
|
|
|
**Every failure is an empty but valid tree.** No config, no database, a
|
|
storage grant revoked since the last launch, a corrupt page: each of them
|
|
answers an empty list with the reason in the envelope's `note`. `ss:root` is
|
|
answered without opening anything at all, so the four tabs are there on a
|
|
phone that has never had a library. A head unit that spins forever is worse
|
|
than one that says the library is empty, and a driver cannot dismiss a
|
|
dialog.
|
|
|
|
**Covers go out as `content://`, never as bytes.** Auto supports that scheme
|
|
and `android.resource://` and no other, and `MediaLibraryServiceLegacyStub`
|
|
will decode `MediaMetadata.artworkData` into a Bitmap for every item it
|
|
converts — a few hundred of those is over Binder's 1 MB limit and the whole
|
|
browse fails. So `auto.rs` picks the file (the warmed 360 px thumbnail from
|
|
`library_art_prewarm`'s cache when it is there, the master otherwise) and
|
|
`LibraryBridge` mints the URI through the app's FileProvider. That provider
|
|
needed new roots: see `src-tauri/android/res/xml/file_paths.xml`, and note
|
|
that `files-path` is the wrong tag — it maps `getFilesDir()`, and the
|
|
library lives one level up under `getDataDir()`.
|
|
|
|
**And media3 grants nothing.** There is no `grantUriPermission` anywhere in
|
|
`MediaSessionImpl`, `MediaSessionLegacyStub`, `MediaLibraryServiceLegacyStub`
|
|
or `LegacyConversions` at 1.10.1, and the app's provider is
|
|
`android:exported="false"` — so `LibraryBridge` grants each URI to the
|
|
browser's own package by hand. Forget that line and the car browses
|
|
perfectly and shows a grid of grey squares, with nothing in any log.
|
|
|
|
The phone walk's six `auto-*` checks run **with the app force-stopped**,
|
|
which is the entire claim, and they read `LibraryBridge`'s own
|
|
`auto node=… n=… … note=…` line out of logcat — the only place on a device
|
|
with no screen where any of this can be read back.
|
|
|