### The Auto prober

```bash
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
```

Android Auto reaches a media app through `MediaBrowserService`, with the
app's process **cold** — no Activity, no WebView, and therefore no DevTools
socket. `smoke.mjs` drives the phone over CDP, so with SoulStream
force-stopped it has nothing to attach to and is blind to every bit of this.
`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 prints one
JSON object per event under the logcat tag `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/` deliberately — `npx tauri android
init` would delete it — and borrows the generated project's Gradle wrapper
rather than shipping one, which pins Gradle 8.14.3, AGP 8.11.0, Kotlin 1.9.25
and JDK 17: what is already in the cache, so a run downloads nothing.
`probe.sh` builds, installs, clears logcat, force-stops **SoulStream** (only;
the walk already does the same), starts the prober by name, waits, and prints
the log. No permissions beyond `<queries>`, no network, no storage, no
transport command; every exit path disconnects and `onDestroy` disconnects
again.

The events are `start`, `desc`, `filter`, `root`, `children`, `fail`, `done`.
The number it exists for is `root`: **milliseconds from `connect()` to
`onConnected()` with the process cold**, next to the root media id and
whether a session token came with it. Auto gives a media app a few seconds
and then drops it, which is why `onGetLibraryRoot` answers with
`Futures.immediateFuture` and nothing else — `MediaLibraryServiceLegacyStub`
blocks the binder thread on that future with no timeout.

`desc` is the half the walk cannot do. `adb shell pm dump` is `dumpsys
package`: resolver tables, permissions, package state, and **no application
meta-data at all** — so `auto-browse` and `auto-library` can prove the intent
filters and nothing else. The prober resolves
`com.google.android.gms.car.application` through
`PackageManager.GET_META_DATA` and then opens that resource out of the app's
own `Resources` and reads the `<uses name="media"/>` inside it, which is the
only way to tell a real declaration from a dangling id short of unpacking the
APK. The walk echoes the last such line back as the advisory `auto-desc`.

**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 exactly
what this prober does — so it shows up here before it ever shows up in a car.
`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. The counter is the instrument:

```bash
adb logcat -d -s SoulStream:I | grep "auto service created" | tail -1
```

One or two is normal. A number climbing while a probe or a car is connected
is the bug, and it is worth watching across a LONG bind rather than a short
one — the failure mode this guards against is an OOM hours in, not a crash at
connect.

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