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