android/handoff-2026-09-14
12
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8f46f9c663 |
android auto: a catalog row is handed to the app, not failed at
Every entry in a streamed playlist is `kind='stream'` with no file on this phone — only a catalog id. A service-only process cannot turn that into audio: the resolver is hundreds of lines of shared JS that exist while a WebView does and not otherwise. So the car tapped those rows and nothing happened, which is the worst of the three possible answers. Now the process asks whether the APP can do it instead. Rust, finding nothing local, reports WHICH playlist and entry were meant rather than just failing — a catalog row is not a broken row, it is one this process cannot do alone, and that difference is what decides between an error and a hand-off. Kotlin carries it over `MediaHub.onRemote`, which is non-null only while the plugin is loaded, which is only while a WebView exists: the same fact is both the channel and the test for "is the app warm". And the JS plays it with `playFrom(playlist, items, item)` — the exact call the half-sheet makes when the same row is tapped in the app, so the queue, the provenance and the failure message are the app's own. Nothing about the car is special once it arrives. Cold, it says so instead of pretending: the focus is handed back rather than held for silence, and the log names the row. A phone in a pocket genuinely cannot play a Deezer id, and a tap that quietly does nothing is worse than one that stops. Two things the same change fixed on the way past. A streamed entry has no cover FILE, only a remote URL, so `COALESCE(art_path, art_url)` gives those rows their sleeves and a remote uri now goes to the host untouched instead of through a FileProvider whose root check it could never pass. And Recent, playlists and an artist's albums join Albums as GRIDS — all four are things known by the picture, which is what the grid is for. `probe.sh` gained `SS_KEEP=1`, without which the warm path cannot be tested at all: the script force-stops SoulStream, which is precisely the app the hand-off needs alive. **Proven:** cold answers `auto cold, cannot resolve` and releases focus; warm answers `auto hand-off to the app` and the event reaches the JS with no console error. **Not proven:** that the audio then starts. Those Deezer ids have to go through the resolver exactly as they do when tapped in the app, and if the app cannot play them the car will not either — which is correct behaviour and indistinguishable, from here, from a bridge that silently failed. It wants checking with the app in front of you. |
||
|
|
54245a33b5 |
android auto: Recent means albums and playlists, and is never empty over a full library
"Recent" answered with TRACKS out of the `plays` table, and on a phone whose
history had just been wiped that was "No items" sitting over a full library —
the first thing a driver sees, saying the app has nothing.
Three changes, each about what the word means in a car.
**Albums, not tracks.** "Recent" in a car is "put that back on", and what
anyone means by that is the record. A list of single songs plays one and stops.
`plays` records what was played by NAME rather than by track id, so getting
back to an album means matching the pair again; anything played that has since
left the library drops out of the join, which is right for a row whose promise
is that tapping it works.
**Playlists, which were absent entirely.** They are in the database, they are
local, and they play from cold exactly as albums do — they were missing only
because the four root slots were spent, and Recent is somewhere they can live
without taking one. Counted out of `playlist_items` rather than
`playlist_tracks`: it is the richer table and the one that knows whether an
entry is a local file or something that needs the resolver. A playlist that is
part catalog says so on the row — "Playlist · 4 of 9 here" — so the gap is
declared before the tap rather than discovered after it, and tapping plays the
local entries in order. Entries that need the phone are shown rather than
hidden, so the car's list matches what is on the phone, and marked unplayable
rather than pretending.
**An unplayed library is not an empty shelf.** With no history at all, Recent
falls back to the newest albums, which is a defensible reading of the word and
a great deal better than an empty tab on a fresh install.
`ss:playlist:<id>` and `ss:pitem:<id>` join the tree and `resolve_playable`, so
a playlist or one of its tracks plays with the rest behind it.
Two fixes to the instrument that found this: `probe.sh` was passing
`--es play ""` when no query was given, which `am start` rejects outright
("Argument expected after play") — it is conditional now. And a splice left
`fn track_node(fn track_node(` in the source, which rustc reports as an
unclosed delimiter at the END of the file, pointing 400 lines away from the
mistake.
|
||
|
|
062e1845e0 |
android auto: one engine per process, actually — init was still spawning its own
`headless.rs` was written around a guarantee its own doc comment made and the code did not keep: `playback::init` still called `Player::spawn_with_device` directly. So a car could start a cold process, play a record into it, and the moment the app's Activity came up the process built a SECOND player — two AAudio streams out of one app, playing over each other. That is precisely the failure the module exists to prevent, and nothing would have reported it but Kyle's ears. On Android `init` now takes the process's engine instead. The event receiver is single-consumer and the headless pump already owns it, so the UI SUBSCRIBES to the pump rather than reading the engine directly; and the fallback queue is cleared on the way in, because from that point the JS decides what plays next and two things advancing one engine would skip a track each time. The desktop path is untouched — it still spawns its own, because there is nothing else in that process to share with. Also here: a log line on the transport's routing decision (`auto playWhenReady=… headless=…` and `auto transport …`), added while chasing a pause that turned out to be working. It is kept because the question "did this button go to Rust or to a WebView that is not there" has now come up three times and is invisible without it. The pause was real, and the measurement was not. "One active AAudio track after a pause" proves nothing — a paused sink still shows as active. The honest test is that the session reports PAUSED(2) and the position stops moving, which it does: 24873 ms, unchanged five seconds later. |
||
|
|
f59a2d773e |
android auto: the position the engine is actually at, and search by voice
**Pausing looked like the track restarting, and it was not only a look.** The card carried the position it was BUILT with — zero, on the headless path, because nothing wrote it again. media3 extrapolates a position while playing, so the bar moved and seemed right, but every state rebuild snapped back to that stale zero. Worse than cosmetic: media3 then BELIEVED the player was at zero, and reconciled on resume by seeking there, so the track really did start again. The same staleness made a seek look like it had done nothing — the engine moved and the next rebuild reported zero. `getState()` asks the engine now, on every rebuild, through `positionMs()` and `sounding()`. One JNI call and always true, instead of a number remembered from the moment playback began. A seek moves the reported position immediately rather than waiting for the engine's next event, so the scrubber does not lag a beat behind and read as snapping back. **Search, which is the half of round fifty-two that needed no decisions.** `onSearch` answers the count and `onGetSearchResult` the rows; both run the same couple of LIKEs over the library, because there is no search index in this schema and a few hundred albums do not need one. Albums rank before tracks: "play Discovery" almost always means the record, and whoever wanted the song would have said more — which matters because a voice command takes the first result. Voice comes in by a different door and it is implemented too. "Play X" arrives at `onAddMediaItems` as an item with NO media id and a `searchQuery` instead; Google's own samples disagree about whether Auto uses that or `onSearch`, so both are answered. An empty query is "play some music" and gets Recent rather than an error. The prober gained a `search:` prefix for it — `probe.sh 2 false "search:discovery"` issues `playFromSearch` through the session token, which is the path a spoken sentence actually takes. It answers `auto voice 'discovery' n=1`, `auto play ss:album:2 queued=14`, and the controller reports "One More Time" playing: a sentence turned into a record, in a process with no Activity. Still library-only, and that is now the whole remaining question. Playlists are local and would play from cold exactly as albums do, blocked only on which four root slots to spend. Catalog and Soulseek rows cannot resolve without the WebView, but can whenever the app is warm — browsable always, an honest error only when cold. |
||
|
|
4d75395411 |
android auto: the car's own controls, the covers, and four tabs with faces
Everything here came from putting the thing on a real head unit and looking at it, which is the only reason any of it was found. **The transport did nothing.** Pause, seek, next and previous all routed through `MediaHub.act`, which is a round trip out to the WebView — and a car that started a cold process has no WebView, so every button drew, moved, and sent a message to nobody. `AutoNative.driving()` reports whether the headless queue is what is playing, and when it is, the transport goes straight to the engine instead: pause/resume, a real seek, and next/previous that move the fallback queue and push a card update after them. A normal phone session is untouched and still goes through the JS, which owns shuffle, repeat, `alts` failover and the real queue. That is the THIRD instance of one bug: the headless path silently skipping something the WebView path does for free. Audio focus was the first (a live AAudio track, muted, audible to nobody), the card was the second (STATE_NONE over playing music), the transport is this one. Anything the JS does on the way to making a sound has to have an answer here too, and the failure is always silence rather than an error. **The covers, and the colour.** The playback screen was showing the app icon, because nothing filled the card's art. Android Auto washes that screen with a colour it extracts from whatever artwork it is handed — so every record was tinted the falcon's blue. The engine already knows the album's cover path, so it now travels with the track and the card is built from it. Random Access Memories comes up gold. The app derives an accent per cover and the car cannot be told to, but it turns out the car does it anyway, for free, given art. **Four tabs with icons**, which is what every other media app in the car has: a root browsable item's artwork is drawn as its tab icon, and ours had none, so they rendered as bare words. `android.resource://` URIs rather than the FileProvider — these are our own resources, identical on every install, and granting a per-connection URI for an icon that never changes would be wrong. **The grid, in the right place.** The hint was set on each node's own `LibraryParams` when returning its children; Auto reads it from the ROOT as a default and from the PARENT ITEM as an override. So the albums drew as a list with the grid hint set correctly and delivered nowhere. The root declares `CONTENT_STYLE_SUPPORTED` now and the `ss:albums` tab carries the override, along with each artist, whose children are also sleeves. **Shuffle** is a `CommandButton` on `setMediaButtonPreferences` — both spellings exist in 1.10.1 and `setCustomLayout` is the older one. It uses the PREDEFINED `ICON_SHUFFLE_ON` rather than a drawable of ours: a custom resource the host does not recognise is dropped silently, which is what happened the first time, and `ICON_SHUFFLE_OFF` names the state and is drawn as a circular arrow that reads as repeat. It reorders only what is AHEAD, so the current song does not restart under the driver's hands and "previous" still goes back to what was actually heard. The accent is `#31C0CB` in a car theme named by `com.google.android.gms.car.application.theme`. Its parent is a FRAMEWORK theme because the style is resolved inside the car's process, where AppCompat and Material are not guaranteed to exist. Proven on the Pixel 6a through a real Android Auto session, driven from the DHU: the four tabs render with their icons, Albums is a grid of full covers, the playback screen shows the album art with the colour pulled from it, and a media `next` moves the queue and lands the card on "The Game of Love". **What this does not touch, and it is the next question.** Everything in the tree is a local file. Playlists are in the database, are local, and would play from cold exactly as albums do — they are absent only because Auto gives four root slots and four are taken, which is a trade to make rather than a limit. Catalog and Soulseek rows genuinely cannot play from a cold process, but they CAN whenever the app is warm, through the same `MediaHub.act` round trip the transport just stopped depending on — browsable always, honest error only when there is no WebView. That is a better answer than the design's "never" and it is where this goes next. |
||
|
|
f50cf2018a |
android auto: a tap in the car plays, with no Activity anywhere in the process
Android Auto starts this app by binding a service. There is no Activity, no WebView, no `tauri::AppHandle` — and until now no way to make a sound, because the engine lived in Tauri's managed state. `src-tauri/src/headless.rs` moves it into a `OnceLock` owned by the PROCESS: whoever needs audio first builds it, `playback::init` on a normal launch or a car tapping a row into a process with no UI, and everyone after that gets the same one. **That singleton is the whole safety story.** `Player::spawn` opens an output stream, so two of them in one process is two AAudio streams playing over each other — the failure this module exists to make impossible. The event receiver is single-consumer, so the pump lives there too and offers a subscription slot for the WebView to claim when it finally appears, rather than a second engine being stood up beside the first. The queue here is deliberately not the app's queue. The JS one keeps shuffle, repeat, `alts` failover, provenance and the three-ahead preload; this is a vector and an index whose only job is that the album keeps playing when there is no JS alive to advance it. Its metadata is read ONCE, when the queue is built, so advancing never touches the database again — which matters when the thing advancing is a background thread in a process with no UI. Resolution happens in Rust, against the library, read-only. A tapped `ss:track:<id>` brings its whole album at the right index, because selecting one song and getting silence after it is not what a record does. Nothing trusts a URI from the controller: the tree only ever publishes `ss:` ids, so there is no file path for anyone to hand back, and an id that resolves to nothing returns a count of zero rather than a guess. Kotlin's half is three things that each fail invisibly on their own. `COMMAND_SET_MEDIA_ITEM` is advertised now — without it a browse row is drawn as playable and does nothing when tapped. `onAddMediaItems` passes ids through instead of media3's default `UnsupportedOperationException`, which kills the tap before it reaches the player. And `handleSetMediaItems` does the work on its own thread, because the first tap in a cold process opens a database AND starts an audio engine, and it is called on the application thread where ten seconds is an ANR. **Two things the device taught, both of which looked like silence.** The headless path has to request audio focus itself: the first run showed a live AAudio track on our uid, decoding perfectly, with `PortMuted=true` and the gain at `-inf` — audible to nobody. On the WebView path the JS asks for focus; a car tapping a cold process has no JS to ask. And it has to write the card itself: `getState()` mirrors `MediaHub.state`, which only the WebView ever fills, so the second run played audio while the session still reported `STATE_NONE` with no title — a head unit showing a blank player over music. The engine knows what it started, and that now goes into `MediaHub.update` in the shape the card already reads. Proven by the prober, which gained a `play` mode for it: bind cold, issue `playFromMediaId` through the session token the browser handed back — Auto's own path, not an interface of ours — and read the state back. `ss:track:1` answers `queued=13` (the album behind it), and the controller reports `state=3` with the title and a position moving 3.8 s → 11.8 s → 19.8 s across the run. `probe.sh <depth> <recent> <mediaId>` is the invocation. **Not done, and it is the display rather than the audio.** Nothing tells Kotlin when the fallback queue advances, so the card will keep showing track one while track two plays. The position moves on its own — `SimpleBasePlayer` extrapolates it while playing — but a track change and a seek both need the pump to push, and the pump currently only speaks to a subscriber that exists when there is a WebView. The handover the design asks for (a UI arriving over live headless playback, and exactly one queue driver surviving) is untested for the same reason. |
||
|
|
3f56f274c5 |
android auto: the covers, and the grid a record is owed
Round fifty put the tree in the car with every row a bare line of text. This
gives the rows their sleeves and asks for albums to be drawn as a grid — the
two things the design picked that a tree alone cannot express, and the only
parts of a car screen the app actually controls.
**Artwork goes out as a `content://`, not as bytes.** media3 will carry a cover
as data in the metadata and the legacy stub Android Auto binds turns that into
a bitmap per row — inside a single Binder reply with a one-megabyte ceiling for
the whole list. Two hundred rows of even thumbnailed JPEG is a
`TransactionTooLargeException` on a list that worked yesterday. A URI is a few
dozen bytes and the host fetches only what it draws. The provider stays
`exported="false"`; each browse grants read on its own URIs to the package that
asked, which `onGetChildren` already knows.
Reaching the library needed a new `file_paths.xml` in the res overlay, and it
needs BOTH spellings of one directory. Covers live under the app's DATA dir,
which `files-path` cannot reach — that tag means `getFilesDir()`, the `files/`
subdirectory. `root-path` can, but FileProvider resolves a file through
`getCanonicalPath()` before matching, and for the primary user
`/data/user/0/<pkg>` canonicalises to `/data/data/<pkg>`, so a root scoped to
`/data/user/` never matches on a phone. `/data/user/` still earns its place:
a car's driver profile is user 10 and does not canonicalise away.
The grid is `CONTENT_STYLE_BROWSABLE_HINT` on the parent's `LibraryParams` —
grid for `ss:albums` and for an artist's albums, list for artists and tracks.
Those keys belong to the host rather than to media3, which is why the same
constants serve a phone-projected car and a head unit alike.
**Three failures, one cause: a catch with nothing in it.** `artUri` returned
null on any throw and said nothing, and every row came back coverless with the
database, the res overlay and the APK all checking out — the binary XML was
dumped out of the APK to prove the root was in there. It cost an afternoon and
two wrong diagnoses. One `Log.w` in the catch named both real bugs in a single
run: `JSONObject.optString` returns the four-character string `"null"` for a
JSON null rather than `""`, so the tabs were handing `File("null")` to the
provider (`isNull` is checked first now); and the canonical-path substitution
above, which is visible ONLY in that exception's text — `readlink -f` on the
device does not show it, and believing it cost the first wrong answer. The
warning latches to one line per process, because two hundred identical traces
would bury the tree logging it sits beside.
The prober reports each row's `iconUri` now. A tree walk could say a row
arrived but never whether it would be drawn with anything, which is the one
thing this round changes, and `art:""` against a real `content://` is the whole
difference.
Verified on the Pixel 6a through Android Auto's own binder with the app cold:
both albums come back carrying
`content://com.soulstream.app.fileprovider/ss_library_canonical/...cover.jpg`.
The car screen itself is still the Desktop Head Unit's to show.
|
||
|
|
515b3da71f |
android auto: the tree under the root — four tabs read cold out of library.db
Round forty-nine put SoulStream in the car with an empty root. This fills it.
`onGetChildren` answers the four tabs Kyle picked — Recent · Albums · Artists ·
Saved — and everything under them: albums with their artist and track count,
artists grouped the way the app groups them, an album's tracks in disc and
track order, and every local file under Saved. `src-tauri/src/auto_browse.rs`
is one JNI export over `library.db`, and `AutoNative` in the plugin is the
Kotlin half.
**It reads read-only and it never calls `Library::open`.** That constructor
runs `PRAGMA journal_mode = WAL` and a `CREATE TABLE IF NOT EXISTS` batch —
writes, both of them — and a service that takes a write lock on the library to
draw a list is a bug waiting for a long drive. The path is derived the way
`config::download_dir` derives it, but without it: that function takes a
`tauri::AppHandle` and there is no Tauri in this process, so `config.json` is
parsed read-only for `download_dir` and the default is `<dataDir>/Library`,
with the database under `.soulstream/` as the portability rule requires.
**`AutoNative` calls `System.loadLibrary("app_lib")` itself, and that is the
round's other half.** When Auto binds a cold app there is no Activity, so
nothing has loaded the Rust side and none of it exists. Loading it here also
runs `JNI_OnLoad`, which publishes the JavaVM and the app Context to
`ndk-context` — the same hand-off the audio backend needs, arriving before
there is any audio, which is what round fifty-one stands on. `loadLibrary` is
idempotent, so it costs nothing when the app is already up.
Two things a head unit taught that no unit test would have. **The root must not
depend on the database.** The four tabs are a constant, and gating the whole
dispatch on a connection meant one unreadable library cost the car its entire
structure — it read as "Media isn't available for this list", which names
neither the tabs nor the reason. A library that cannot be read should cost the
ROWS, never the shape. And **`log::warn!` goes nowhere in a cold service
process**: nothing has run Tauri, so no `android_logger` is installed, and the
first failure here was completely silent. It writes to the platform log
directly now, which is the only reason the second failure had a cause attached.
`open_read_only` falls back to a read-write handle it immediately sets
`query_only` on. A read-only connection to a WAL-mode database needs the `-shm`
beside it and cannot create one, and the mode lives in the header, so it does
not go away when the sidecars do. This has NOT been observed to bite — the one
failure that looked like it was SELinux, below — but the fallback is cheap and
the connection still cannot change a row. `CREATE` is in neither flag set, so a
missing database stays an error rather than becoming an empty one.
Verified twice. The prober walks it cold on the phone AVD: 12 nodes, 139
leaves, 2.26 s, with the album tracklists in the right order and both artists
carrying the right albums. And it is on an Android Automotive head unit —
`ss_car`, API 34 — showing the four tabs, eleven Recent rows and five albums
with their counts, which is the first time any of this work has been seen
rendered by a car rather than described.
**Not done, and deliberately not guessed at.** There is no artwork: the plan
called for cover art through the existing FileProvider via a `files-path`
entry, but the library lives under the app's DATA dir and `files-path` maps to
`getFilesDir()`, which does not reach it — so the mechanism needs deciding, not
patching. And the rows are all lists; the grid Kyle chose for albums is a
content-style hint that is not set yet. Both are presentation over a tree that
is now correct, and both want the car in front of them.
|
||
|
|
e1bf456743 |
android auto: the car can see the app, and the root it opens is empty
Android Auto reaches a media app through `MediaBrowserService`, so `MediaService` becomes a `MediaLibraryService`, one intent filter carries three actions — media3's `MediaLibraryService` and `MediaSessionService`, plus the legacy `android.media.browse.MediaBrowserService` that Auto actually binds — and `res/xml/automotive_app_desc.xml` with the `com.google.android.gms.car.application` meta-data declares the app to the car. Auto lists SoulStream, opens it, and gets a browsable root. There is nothing under it: `onGetChildren` is left un-overridden so the default answers `ERROR_NOT_SUPPORTED` at once, and round fifty fills the tree. `onGetLibraryRoot` answers out of `Futures.immediateFuture` — no database, no I/O, nothing deferred — because `MediaLibraryServiceLegacyStub.onGetRoot` blocks the binder thread serving the host on that future with NO timeout and runs the callback on the main handler. A root that waits on anything is a head unit that gives up. The round carries a mitigation it cannot be split from. androidx/media #3158 — open since 2026-04-03, confirmed again 2026-09-04, reproduced on media3 1.11.0 — has a host that binds a `MediaLibraryService` with nothing playing drive the service round `onCreate` → `onBind` → `onGetSession` → `onConnect` → `onDestroy` at about thirty cycles a second, deterministically after a force-stop with no existing binding. That is exactly how Auto connects and exactly what the prober does. media3 will not act before an Auto-side fix lands, so the loop cannot be stopped from here; it can only be made to cost nothing. The session, the player, the notification provider, the audio-focus request and the becoming-noisy receiver move into `MediaHub` for the life of the PROCESS and `onCreate` allocates and registers nothing — round forty-eight's version would have built a platform session and a legacy browser stub thirty times a second, registered and unregistered a receiver sixty times a second, and abandoned audio focus thirty times a second while music was playing. Three consequences, each a trap for the next round. `onDestroy` calls `removeSession` and never `release()`: `removeSession` drops the session from that instance's map and clears its listener, which is what stops a destroyed service being called back, while `markReleased()` would latch `released` and leave a process-lifetime player that never updates the card again. The session is built with `MediaLibrarySession.Builder(applicationContext, …)` rather than the `(MediaLibraryService, …)` overload, because `MediaSessionServiceLegacyStub.initialize()` does `attachToBaseContext(sessionImpl.getContext())` — a session built against the Service pins a destroyed Service as the base context of a live `MediaBrowserServiceCompat`. And nothing releases the session any more, so it and its platform `MediaSessionCompat` live until the process dies; the walk's `one-session` check is what keeps that honest. `onCreate` logs `auto service created n=…` and `onDestroy` logs `auto service destroyed`, which are the only way to count the loop on a real phone. And the instrument. `tools/auto-prober/` is a throwaway app that binds the same way Auto does, with Auto's own root hints, walks whatever tree comes back, and logs one JSON object per event under `SoulAutoProbe`. It is framework-only (`android.media.browse.MediaBrowser`, not androidx, not media3) because that is the path Auto itself takes into a media3 app, and a prober built out of the library it is probing would not prove that path works. It lives outside `src-tauri/gen/android/` because `tauri android init` would delete it, and borrows the generated project's Gradle wrapper rather than shipping one, so a run downloads nothing. With SoulStream force-stopped there is no WebView, so no CDP, so `smoke.mjs` is blind to every bit of this — the prober is the only eye the project has on Auto without a head unit. The number it exists for is `root`: milliseconds from `connect()` to `onConnected()` with the process cold. Two corrections to the plan, argued in the files. `adb shell pm dump` is `dumpsys package` and prints NO application meta-data, so the Auto declaration cannot be a hard walk assertion — the two that ARE hard are the intent filters (`auto-browse`, `auto-library`), which catch the `TAURI_ANDROID_PROJECT_PATH` trap completely because the filters and the meta-data live in one library manifest and are lost together; the declaration is read instead by the prober, through `PackageManager.GET_META_DATA` and the app's own `Resources`, and echoed back by the walk as the advisory `auto-desc`. And the prober module compiles at jvmTarget 1.8 like every other module here, not 17 — JDK 17 is what RUNS Gradle. No Rust, no frontend, no cargo. The round was authored where none of it could be compiled and it has now been built and run: the Kotlin compiles, the APK installs, and the prober binds the app cold. `desc` resolves the meta-data to a real `xml/automotive_app_desc`; `filter` shows BOTH the legacy `android.media.browse.MediaBrowserService` and media3's own resolving to `MediaService`; `root` comes back at ~2.2 s with `ss:root` and a session token, of which the callback itself is about nine milliseconds and the rest is the cold process starting; `children` answers `n=-1` with nothing in it, which is the un-overridden default doing its job; and `auto service created n=1 reused=false` — #3158 does not loop here. One fix the first run earned. The prober read the descriptor's `name` out of the android namespace, but Google's `<uses name="media"/>` is a BARE attribute with no namespace, so it came back `[""]` — a real declaration and a dangling resource id reported identically, which is the single thing the event exists to tell apart. It reads the null namespace now and keeps ANDROID_NS as a fallback; the second run says `uses:["media"]`. None of this proves the car screen: the Desktop Head Unit or a real head unit is the only thing that shows what Auto draws, how it groups a tree, or whether a tap plays. This proves the app answers. |
||
|
|
fbc6a0268b |
android auto: the UI is four tabs and a colour, not a screen — the design, decided
An earlier pass in this repo mocked a car UI from imagination: a tab strip, a header with search and queue icons, a bottom mini-player, a big-art playback view with a linear progress bar. None of those exist. Google's line is flat — "You don't draw your user interface directly on Android Auto; the framework has two well-defined UIs that are created automatically" — and the reference frames confirm it. The bottom bar is the SYSTEM's (home, notifications, Assistant). Progress is a RING around play/pause. The artwork sits small at the left behind a blur of itself. About THREE rows fit on screen, not eight. So the artifact that was needed was never a layout. It is the short list of slots the app actually fills, and this commit decides them: tabs Recent · Albums · Artists · Saved style albums grid, artists and tracks list actions Shuffle · Save, 2 of the 6 allowed reach amber dot on rows that need the app; Save shows a tick when down accent brand teal #31C0CB Playlists loses its tab because the phone holds exactly one, and an empty tab is a dead quarter of the only four we get. Saved takes it because it is the set that plays with the app shut, which is the one thing genuinely different about this app in a car. Grid-vs-list is not density — the car fits three either way — it is what identifies the row: a record by its sleeve, an artist by name, a track only by text. Save is the highest-value button on the screen: hear it, save it, it plays cold on the way home. The dot marks the RISKY row rather than the safe one, and thins out on its own as more music is saved. Auto allows one accent pair, set at build time, and it cannot follow the artwork the way the app does everywhere else — so it has to be the constant, the falcon's teal. None of this can ship before round 48. The card is MediaSessionCompat today, Auto needs a MediaLibraryService, and two live sessions is not a safe intermediate. Order is unchanged: 44 → 45 → 46 → 47 → 48, soak, then 49–52 build the table above. The mockup that settles it is research/auto-mockup (gitignored): both real libraries — the handset's three albums and the desktop's 418 — inside the real template, at the three DHU resolutions, with an overlay that outlines which pixels are the car's and which are ours. |
||
|
|
07dad65ca5 |
android: the keep rules, and the release build that proves them
Every APK this port has produced is `assembleArm64Debug`, and the generated project turns R8 on for `release` and only for `release` — while tauri finds a plugin class by string, files every `@Command` under `commands[method.name]`, and lets Jackson fill every `@InvokeArg` field by field. Minification breaks exactly that, and it breaks it without a word: the app compiles, installs, launches, shows Home, and then a tap on Library folder answers "No command pickFolder found for plugin ..." in logcat and nothing on screen. So each plugin module now carries its own `consumer-rules.pro` naming its own reflected surface — the shape tauri-android and tauri-plugin-dialog already use in this build, and one that travels with the plugin into a regenerated `gen/android`, which a rule written into that gitignored tree would not. What belongs to no single plugin is in `src-tauri/android/proguard-soulstream.pro`, copied into the generated project by the build script because the app's own Gradle file is regenerated: the runtime annotation attributes, a `-dontwarn` pair for the two `java.beans` classes jackson-databind references and Android does not have (R8 turns a missing class into a build error, so without them there may be no APK at all), and a keep for the four `@Command` methods declared on `app.tauri.plugin.Plugin` itself. Tauri's own rule matches classes annotated `@TauriPlugin` and the members they declare; the base class carries no such annotation, so nothing names `registerListener` — which is what the lock-screen card's buttons and the download notice's Cancel and Open ride on. `build.sh` grows `--release`: the release profile, `--features tauri/devtools` so the walk can still attach at all, `assembleArm64Release`, and a zipalign and sign with the same debug key the phone already trusts, so the APK installs over the app rather than forcing an uninstall that would take Kyle's settings with it. With no argument the script is what it has always been, cargo line for cargo line, so deploy.sh and ship.sh keep the path they run every twenty minutes. And it has been built. Not on the phone and not as arm64 — as **x86_64**, on an emulator, because that is what this machine can run — but the flavour is the only difference: the same Gradle, the same R8, the same rules, the same zipalign and apksigner. `minifyX86_64ReleaseWithR8` succeeded and wrote **no** `missing_rules.txt` at all, which is the `-dontwarn` pair doing its job; `lintVitalX86_64Release` passed, so the `-x lintVital` advice is for a failure that has not happened. The signed APK went over the debug app with `adb install -r` and left its data alone, exactly as the same-key argument above says it must. Then the minified app was walked: Home, the record, playback, the queue, the sheets, the lyrics, the drags, the export — **131 of 131, no exceptions, no console errors, every step as expected**, and nothing in logcat about a missing command or class. That is the failure this round exists to prevent, looked for and not found. What it does not cover is the hands-only list in ANDROID.md — the pickers, the notification buttons, the share sheet — which no walk reaches, and the four `@Command` methods on `Plugin` itself remain a precaution rather than a diagnosed bug. The document says so, command by command. |
||
|
|
9f0601027c |
android: the handoff — the pending rounds, the pipeline, and what is unverified
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. |