**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.
The Android Auto prober
A throwaway app that binds SoulStream's MediaBrowserService the way Android
Auto does, walks whatever tree comes back, and prints one JSON object per
event under the logcat tag SoulAutoProbe.
bash tools/auto-prober/probe.sh # depth 2, the ordinary root
bash tools/auto-prober/probe.sh 3 true # depth 3, the RECENT root
adb uninstall com.soulstream.autoprober # and it is gone
Why it exists
Android Auto reaches a media app through MediaBrowserService, with the
app's process cold — no Activity, no WebView, and therefore no DevTools
socket. tests/mobile/smoke.mjs drives the phone over CDP, so it cannot see
any of this: with SoulStream force-stopped there is nothing for it to attach
to. Without a head unit or the Desktop Head Unit, this is the only instrument
the project has.
It is framework-only on purpose — android.media.browse.MediaBrowser, not
androidx, not media3. That is the path Auto itself takes into a media3 app
(media3 serves the legacy android.media.browse.MediaBrowserService action
through MediaSessionService.onBind), and a prober built out of the same
library as the thing it probes would not prove that path works.
Where it lives, and why not in the generated project
src-tauri/gen/android/ is regenerated by npx tauri android init and is
gitignored; anything put in there is deleted the next time someone runs it.
So the prober is its own Gradle build, tracked, outside it — and it borrows
the generated project's wrapper rather than shipping one, which pins Gradle
8.14.3, AGP 8.11.0, Kotlin 1.9.25 and JDK 17: exactly what is already in the
Gradle cache, so a probe run downloads nothing.
There is deliberately no gradle-wrapper.jar in this directory.
What it prints
One line per event, each a JSON object, all under SoulAutoProbe:I:
ev |
what it says |
|---|---|
start |
the package, component, depth and whether the RECENT root was asked for |
desc |
the com.google.android.gms.car.application meta-data resolved to a resource id, that resource's name, and the <uses name=…> inside it |
filter |
what queryIntentServices resolves for the legacy browse action and for media3's own, scoped to SoulStream |
root |
the number this exists for: milliseconds from connect() to onConnected() with the process cold, the root media id, and whether a session token came with it |
children |
one per subscription: the parent, how many rows, and each row's id, title, browsable and playable |
fail |
the bind failed, was suspended, or the 8 s watchdog fired |
done |
the walk finished: elapsed, nodes, leaves |
desc is the important one for the manifest, because adb shell pm dump
prints resolver tables and package state and no application meta-data at
all — the phone walk's auto-browse and auto-library checks can prove
the intent filters and nothing else. This resolves the meta-data int through
PackageManager.GET_META_DATA and then opens the resource out of the app's
own Resources, which is the only way to tell a real declaration from a
dangling id without unpacking the APK.
probe.sh also prints two lines of SoulStream's own log: auto root …, the
service's view of the same connection (including which root hints survived
the trip), and auto service created n=…, which is the androidx/media #3158
counter — see below.
androidx/media #3158
Open since 2026-04-03, still open, reproduced on media3 1.11.0: a host that
binds a MediaLibraryService while nothing is playing can put the service
into an onCreate → onBind → onGetSession → onConnect → onDestroy loop at
about thirty cycles a second. It is 100% reproducible after a force-stop with
no existing binding — which is precisely what this prober does, so expect to
see it here first.
MediaHub holds the session, the player, the notification provider, the
audio-focus request and the becoming-noisy receiver for the life of the
process, so a cycle allocates and registers nothing and the loop is survivable.
The loop itself is Android Auto's to fix and media3 has said it will not act
first. auto service created n=… is how you find out: one or two is normal, a
number climbing while a probe or a car is connected is the bug.
What it is not
It does not prove the car screen. Nothing here can: the Desktop Head Unit or a real head unit is the only thing that shows what Auto actually draws, how it groups a tree, or whether a tap plays. This proves the app answers.
Safety
No permissions beyond <queries>, no network, no storage, no transport
command. Every exit path disconnects the browser and onDestroy disconnects
again, so nothing is left bound to SoulStream's service. The only force-stop
probe.sh issues is of com.soulstream.app, which tests/mobile/smoke.mjs
already does on every phone run.