From 9de938746097172d322eda212032b86d26942079 Mon Sep 17 00:00:00 2001 From: Nils Stinnesbeck Date: Wed, 28 Oct 2026 15:18:58 +0100 Subject: [PATCH] App 2.42.1: the music check connects to no other app In the car, 2.42.0's check stopped the Bluetooth music and raised a media-access prompt: binding every media browser service started the car radio and the Local Media Player. The services are now only listed; the active media sessions already carry the song of Bluetooth and CarPlay without any connection. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6 --- .claude/skills/car-logs/SKILL.md | 8 +- AGENTS.md | 2 +- SPECS.md | 15 +++ app/build.gradle | 4 +- .../java/com/evsuite/abrp/MediaProbe.java | 125 +++--------------- app/src/main/res/values/strings.xml | 2 +- 6 files changed, 44 insertions(+), 112 deletions(-) diff --git a/.claude/skills/car-logs/SKILL.md b/.claude/skills/car-logs/SKILL.md index 2c751e7..e30f9ad 100644 --- a/.claude/skills/car-logs/SKILL.md +++ b/.claude/skills/car-logs/SKILL.md @@ -65,10 +65,10 @@ drive shows the same. ## Music-sources checks `fetch-logs` also brings `media-probe-YYYYMMDD-HHMMSS.txt` (Logs tab, "Check music sources", -`MediaProbe`). Three parts: audio players (use, app, state), active media sessions, and every -media browser service with `connected` / `refused or failed` / `no answer`, then its state and -song (TITLE, ARTIST, ALBUM, ALBUM_ART as a size). CarPlay's is -`com.allgo.remoteui.mediabrowserservice/...RemoteUIMediaBrowserService`. Ask what was playing +`MediaProbe`). Three parts: audio players (use, app, state), active media sessions with state +and song (TITLE, ARTIST, ALBUM, ALBUM_ART as a size or ALBUM_ART_URI), and the media browser +services by name (2.42.1 on; 2.42.0 also connected to them, which stopped the music). CarPlay's +session is `com.allgo.remoteui.mediabrowserservice`, Bluetooth's `com.android.bluetooth`. Ask what was playing when the check ran. ## Rules diff --git a/AGENTS.md b/AGENTS.md index df65bad..744dcc4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -82,7 +82,7 @@ Repository: `ssh://git@git.stinnesbeck.com:2222/nils/mg4.git` (remote `origin`), | Live data | on/off, ABRP API key and user token, upload frequency | | Server | URL, pairing by QR, ingest token, installation ID (read-only), config import | | Settings | drive mode (Eco, Normal, Sport, Snow, Custom; the lit one is what the car reports; changes only in P at a standstill, `DriveModeWrite`; "Set at start" sets the last confirmed choice again when the service starts with the head unit), full screen, voice key, Overview style with the accent colour beside it (offered only for styles drawn in it, `OverviewStyle.usesAccent`; Retro, Terminal, Grid (and Grid with map, sharing Grid's choice) and Visor add "Original" for their own palette, `hasOriginal`), tyre unit, battery and charging price (one card on wide screens), version, update check, install from USB, About | -| Logs | signal recorder (`SignalRecorder`/`SignalRecording`: every property and vendor getter via `VendorSurfaceProbe.capture`, again and again for 3 minutes, plus change events of the driver-assistance block it subscribes to, `EVHardware.listenToProperties` with `AssistBlock`, and steering-wheel key broadcasts; lists what changed, rarest first; saved to the app, a USB stick and the server, through `LogOutbox`, which sends it again later when the car is offline; how to find where the car publishes something), music-sources check (`MediaProbe`: audio players, media sessions and every media browser service, CarPlay's among them, with their song; read only, saved and sent like a recording), update log (every install step, with the platform's words), upload log | +| Logs | signal recorder (`SignalRecorder`/`SignalRecording`: every property and vendor getter via `VendorSurfaceProbe.capture`, again and again for 3 minutes, plus change events of the driver-assistance block it subscribes to, `EVHardware.listenToProperties` with `AssistBlock`, and steering-wheel key broadcasts; lists what changed, rarest first; saved to the app, a USB stick and the server, through `LogOutbox`, which sends it again later when the car is offline; how to find where the car publishes something), music-sources check (`MediaProbe`: audio players and the active media sessions, CarPlay's and Bluetooth's among them, with their song, and the media browser services by name — never connected to: binding them stopped the music in 2.42.0; saved and sent like a recording), update log (every install step, with the platform's words), upload log | Key classes (`app/src/main/java/com/evsuite/abrp/`): `AbrpUploadService` (foreground service, 15 s tick), `TelemetryUploader` (+ `TelemetryQueue`, persisted), `TelemetryPayload` (server diff --git a/SPECS.md b/SPECS.md index 1f71ee0..dddf4cb 100644 --- a/SPECS.md +++ b/SPECS.md @@ -140,6 +140,21 @@ tyre pressures, GPS (lat, lon, elevation, heading) and the is_* flags. It does * confirmed car facts, app structure, UI conventions, build/test/ship, documents. It replaces upstream's EVABRPUploader text. The test emulator is now in the flake: `nix run .#car-emulator` (Android 9 AOSP at 1920×720, 160 dpi; the car build installs as the system user — checked). +- 2026-10-28 — App 2.42.1: **the music-sources check no longer connects to other apps' media + services.** In the car, 2.42.0's check (14:26, Bluetooth music playing) made the head unit ask + whether to allow access to media (the owner agreed) and **stopped the Bluetooth music**: it + connected to every media browser service, and among them the AOSP car radio's + (`com.android.car.radio/.RadioService`, which answered "connecting", most likely taking the + audio) and the Local Media Player (`com.android.car.media.localmediaplayer`, whose permission + request is most likely the question; it may now read media files). My claim that the check only + read was wrong: binding another app's service starts it. Now the services are only listed. + Nothing lost: the active sessions alone carried everything — **Bluetooth** + (`com.android.bluetooth`: title, artist, album, duration, track, cover as a file path, playing) + and **CarPlay** (`com.allgo.remoteui.mediabrowserservice`: title, artist, duration, paused) — + readable with MEDIA_CONTENT_CONTROL, no connection needed. Services on the car: CarPlay's, + Bluetooth A2DP, Local Media Player, the AOSP car radio, SAIC's radio (DAB, "Brillux Radio"), + SAIC's USB 1/2 and online media, and SAIC's video browser. Tests and lint pass; not run on the + emulator (stopped for low memory; the change only removes code); not tried on the car. - 2026-10-28 — App 2.42.0 published (`car-2.42.0`). - 2026-10-28 — App 2.42.0: **music-sources check** on the Logs tab (`MediaProbe`, `MediaProbeText`; owner: "is it possible to show playing music in our dashboard as well?", then diff --git a/app/build.gradle b/app/build.gradle index 82cec14..52f7827 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -34,8 +34,8 @@ android { applicationId "com.stinnesbeck.nilsmg4" minSdk 27 targetSdk 36 - versionCode 151 - versionName "2.42.0" + versionCode 152 + versionName "2.42.1" // Nothing about the server ships in the APK: its URL and ingest token are entered in // the app's Server tab (ServerConfig), and the database is only reachable by the server. diff --git a/app/src/main/java/com/evsuite/abrp/MediaProbe.java b/app/src/main/java/com/evsuite/abrp/MediaProbe.java index dc073c5..e186a63 100644 --- a/app/src/main/java/com/evsuite/abrp/MediaProbe.java +++ b/app/src/main/java/com/evsuite/abrp/MediaProbe.java @@ -9,25 +9,17 @@ import android.graphics.Bitmap; import android.media.AudioManager; import android.media.AudioPlaybackConfiguration; import android.media.MediaMetadata; -import android.media.browse.MediaBrowser; import android.media.session.MediaController; import android.media.session.MediaSessionManager; import android.media.session.PlaybackState; -import android.os.Handler; -import android.os.HandlerThread; import android.service.media.MediaBrowserService; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; -import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; -import java.util.Set; import java.util.TreeSet; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; /** * The music-sources check on the Logs tab: where the head unit tells what is playing, before @@ -37,23 +29,15 @@ import java.util.concurrent.atomic.AtomicReference; *
  • the active media sessions ({@code MediaSessionManager.getActiveSessions}, which needs * MEDIA_CONTENT_CONTROL: the car build has it as the system user), with their song and * state;
  • - *
  • every media browser service on the head unit, CarPlay's - * ({@link #CARPLAY}, where the car's own media library reads CarPlay's song) among them: - * connected to as the car's media library does, its session's song and state read, and - * disconnected.
  • + *
  • every media browser service on the head unit, listed but not connected to (2.42.1: + * a connection is not harmless, see {@link #browsers}).
  • * - * Nothing is played, paused or skipped. The report goes where a signal recording goes - * ({@link SignalRecorder#saveCopies}): the app, a USB stick, the server. + * No other app's service is bound; nothing is played, paused or skipped. The report goes where + * a signal recording goes ({@link SignalRecorder#saveCopies}): the app, a USB stick, the server. */ final class MediaProbe { static final String FILE = "media_probe.txt"; - /** CarPlay's song, as {@code com.saicmotor.sdk.media…MediaPlayControlManager} reads it. */ - static final ComponentName CARPLAY = new ComponentName("com.allgo.remoteui.mediabrowserservice", - "com.allgo.remoteui.mediabrowserservice.RemoteUIMediaBrowserService"); - static final long CONNECT_TIMEOUT_MS = 8_000L; - /** More browser services than this are listed but not connected to. */ - static final int MAX_BROWSERS = 12; private static volatile boolean running; private static long startedMs; @@ -182,98 +166,31 @@ final class MediaProbe { } } - /** Every media browser service, each connected to, read and disconnected. */ + /** + * Every media browser service, listed only. 2.42.0 connected to each, as the car's media + * library does; on the car that was not harmless: the Bluetooth music stopped (the AOSP car + * radio's service, `com.android.car.radio/.RadioService`, answered "connecting", most likely + * taking the audio) and the Local Media Player asked for access to media. The active sessions + * above already carry the song of whatever plays, CarPlay's and Bluetooth's included, so no + * service is bound any more. + */ private static void browsers(Context app, List out) { - out.add("Media browser services (" + MediaBrowserService.SERVICE_INTERFACE + "):"); - Set services = new LinkedHashSet<>(); + out.add("Media browser services (" + MediaBrowserService.SERVICE_INTERFACE + "), listed, not connected to:"); try { List found = app.getPackageManager() .queryIntentServices(new Intent(MediaBrowserService.SERVICE_INTERFACE), 0); - for (ResolveInfo ri : found == null ? new ArrayList() : found) { - if (ri.serviceInfo != null) { - services.add(new ComponentName(ri.serviceInfo.packageName, ri.serviceInfo.name)); - } + if (found == null || found.isEmpty()) { + out.add(" none"); + return; + } + for (ResolveInfo ri : found) { + if (ri.serviceInfo == null) continue; + out.add(" " + new ComponentName(ri.serviceInfo.packageName, ri.serviceInfo.name) + .flattenToShortString()); } } catch (RuntimeException e) { out.add(" list not readable: " + e.getClass().getSimpleName()); } - if (!services.contains(CARPLAY)) out.add(" CarPlay's service is not listed; tried all the same"); - services.add(CARPLAY); - HandlerThread thread = new HandlerThread("media-probe-browser"); - thread.start(); - try { - Handler handler = new Handler(thread.getLooper()); - int n = 0; - for (ComponentName service : services) { - if (++n > MAX_BROWSERS) { - out.add(" " + service.flattenToShortString() + ": not tried (limit)"); - continue; - } - browse(app, handler, service, out); - } - } finally { - thread.quitSafely(); - } - } - - /** Connects to one browser service on {@code handler}'s thread and reads its session. */ - private static void browse(Context app, Handler handler, ComponentName service, List out) { - out.add(" " + service.flattenToShortString() + ":"); - CountDownLatch answered = new CountDownLatch(1); - AtomicReference outcome = new AtomicReference<>(); - AtomicReference browser = new AtomicReference<>(); - boolean posted = handler.post(() -> { - try { - MediaBrowser b = new MediaBrowser(app, service, new MediaBrowser.ConnectionCallback() { - @Override public void onConnected() { - outcome.compareAndSet(null, "connected"); - answered.countDown(); - } - - @Override public void onConnectionFailed() { - outcome.compareAndSet(null, "refused or failed"); - answered.countDown(); - } - - @Override public void onConnectionSuspended() { - outcome.compareAndSet(null, "suspended"); - answered.countDown(); - } - }, null); - browser.set(b); - b.connect(); - } catch (Throwable t) { - outcome.compareAndSet(null, "could not try: " + t.getClass().getSimpleName()); - answered.countDown(); - } - }); - try { - if (!posted || !answered.await(CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS)) { - out.add(" no answer in " + CONNECT_TIMEOUT_MS / 1000 + " s"); - return; - } - out.add(" " + outcome.get()); - MediaBrowser b = browser.get(); - if (!"connected".equals(outcome.get()) || b == null) return; - try { - out.add(" root: " + MediaProbeText.clean(b.getRoot())); - describe(new MediaController(app, b.getSessionToken()), out, " "); - } catch (Throwable t) { - out.add(" session not readable: " + t.getClass().getSimpleName()); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - out.add(" interrupted"); - } finally { - handler.post(() -> { - try { - MediaBrowser b = browser.get(); - if (b != null) b.disconnect(); - } catch (Throwable ignored) { - // already gone - } - }); - } } /** A session's state and everything its song's metadata holds. */ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fddcd5b..64d9d2f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -229,7 +229,7 @@ Recording: %1$d:%2$02d of 3:00, %3$d captures, %4$d change events heard. Keys reported, then assistance properties heard, then values changed so far, the rarest first: Recording from %1$s: %2$d captures, %3$d change events heard, %4$s. Keys reported, then assistance properties heard, then values changed, the rarest first: Music sources - Finds where the head unit tells what is playing: the playback Android reports, the media sessions apps publish, and every media service of the head unit, CarPlay\'s among them, each with its song. Play something first (CarPlay, then the radio, Bluetooth or USB) and check once for each. Like a recording, the result goes to your server and to a USB stick. It only reads: nothing is played, paused or skipped. + Finds where the head unit tells what is playing: the playback Android reports and the media sessions apps publish (CarPlay, Bluetooth, radio, USB), each with its song, and the head unit\'s media services by name. Play something first and check once for each source. Like a recording, the result goes to your server and to a USB stick. It only reads and connects to no other app: nothing is played, paused or skipped. Check music sources Checking… (up to a minute) Check from %1$s, %2$s: