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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
This commit is contained in:
2026-10-28 15:18:58 +01:00
co-authored by Claude Opus 5.5
parent f9154c051f
commit 9de9387460
6 changed files with 44 additions and 112 deletions
@@ -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;
* <li>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;</li>
* <li>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.</li>
* <li>every media browser service on the head unit, listed but not connected to (2.42.1:
* a connection is not harmless, see {@link #browsers}).</li>
* </ol>
* 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<String> out) {
out.add("Media browser services (" + MediaBrowserService.SERVICE_INTERFACE + "):");
Set<ComponentName> services = new LinkedHashSet<>();
out.add("Media browser services (" + MediaBrowserService.SERVICE_INTERFACE + "), listed, not connected to:");
try {
List<ResolveInfo> found = app.getPackageManager()
.queryIntentServices(new Intent(MediaBrowserService.SERVICE_INTERFACE), 0);
for (ResolveInfo ri : found == null ? new ArrayList<ResolveInfo>() : 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<String> out) {
out.add(" " + service.flattenToShortString() + ":");
CountDownLatch answered = new CountDownLatch(1);
AtomicReference<String> outcome = new AtomicReference<>();
AtomicReference<MediaBrowser> 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. */