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

