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.
71 lines
3.2 KiB
Bash
71 lines
3.2 KiB
Bash
#!/usr/bin/env bash
|
|
# Build, install and run the Android Auto prober, then print what it saw.
|
|
#
|
|
# bash tools/auto-prober/probe.sh # depth 2, the ordinary root
|
|
# bash tools/auto-prober/probe.sh 3 true # depth 3, the RECENT root
|
|
#
|
|
# This installs a SECOND app on the phone — com.soulstream.autoprober, about
|
|
# 50 KB, no permissions, no network, no storage, no UI. `adb uninstall
|
|
# com.soulstream.autoprober` removes it completely. It never signs in,
|
|
# downloads, joins a room, writes the config or touches the library; the only
|
|
# thing it force-stops is SoulStream, which the phone walk already does.
|
|
#
|
|
# Gradle comes from the GENERATED project's wrapper on purpose: that pins
|
|
# Gradle 8.14.3, AGP 8.11.0, Kotlin 1.9.25 and JDK 17, all of which are
|
|
# already in Kyle's Gradle cache, so this downloads nothing new. There is no
|
|
# gradle-wrapper.jar in this directory and there should not be one.
|
|
set -euo pipefail
|
|
ROOT=/f/Coding/SoulStream
|
|
cd "$ROOT"
|
|
source src-tauri/android/env.sh
|
|
|
|
DEPTH="${1:-2}"
|
|
RECENT="${2:-false}"
|
|
# A media id here plays it instead of walking the tree — the only way to prove
|
|
# a car's tap without a car. The run takes ~25s because the claim being tested
|
|
# is that the SECOND track starts on its own.
|
|
PLAY="${3:-}"
|
|
APK=tools/auto-prober/prober/build/outputs/apk/debug/prober-debug.apk
|
|
GRADLEW="$ROOT/src-tauri/gen/android/gradlew.bat"
|
|
|
|
test -f "$GRADLEW" || { echo "no generated Android project — run a normal build first"; exit 1; }
|
|
|
|
echo "== this installs com.soulstream.autoprober on the phone (adb uninstall com.soulstream.autoprober removes it)"
|
|
echo "== gradle $(date +%T)"
|
|
(
|
|
cd tools/auto-prober
|
|
MSYS_NO_PATHCONV=1 "$GRADLEW" :prober:assembleDebug -q 2>&1 | grep -v "^$" | tail -20 || true
|
|
)
|
|
test -f "$APK" || { echo "no prober APK — gradle's tail is above"; exit 1; }
|
|
ls -la "$APK"
|
|
|
|
echo "== install $(date +%T)"
|
|
"$ADB" install -r "$APK" > /dev/null
|
|
|
|
echo "== cold: force-stopping SoulStream $(date +%T)"
|
|
"$ADB" logcat -c
|
|
# SS_KEEP=1 leaves SoulStream running — the only way to test the WARM path,
|
|
# where the car hands a catalog row to an app that is up and has the resolver.
|
|
if [ -z "${SS_KEEP:-}" ]; then "$ADB" shell am force-stop com.soulstream.app; fi
|
|
"$ADB" shell am start -W -n com.soulstream.autoprober/.ProbeActivity \
|
|
--ei depth "$DEPTH" --ez recent "$RECENT" ${PLAY:+--es play "$PLAY"} > /dev/null
|
|
if [ -n "$PLAY" ]; then sleep 25; else sleep 7; fi
|
|
|
|
echo
|
|
echo "== the probe (one JSON object per event)"
|
|
"$ADB" logcat -d -s SoulAutoProbe:I
|
|
|
|
echo
|
|
echo "== the root, as the service saw it"
|
|
"$ADB" logcat -d -s SoulStream:I | grep "auto root" | tail -3 || echo " (no root line — the callback was never reached)"
|
|
|
|
# androidx/media #3158: a host that binds with nothing playing can put the
|
|
# service in a create/destroy loop at ~30 Hz. The singleton in MediaHub means
|
|
# nothing is allocated per cycle, but the loop itself is Android Auto's bug
|
|
# to fix — so this number is the thing to watch, and it is why those two
|
|
# lifecycle log lines exist at all. One or two is normal.
|
|
echo
|
|
echo "== androidx/media #3158: how many times the service was built"
|
|
"$ADB" logcat -d -s SoulStream:I | grep "auto service created" | tail -1 \
|
|
|| echo " (no creation line — the service was never built)"
|