Compare commits
2
Commits
car-2.42.0
...
car-2.42.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9de9387460 | ||
|
|
f9154c051f |
@@ -65,10 +65,10 @@ drive shows the same.
|
|||||||
## Music-sources checks
|
## Music-sources checks
|
||||||
|
|
||||||
`fetch-logs` also brings `media-probe-YYYYMMDD-HHMMSS.txt` (Logs tab, "Check music sources",
|
`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
|
`MediaProbe`). Three parts: audio players (use, app, state), active media sessions with state
|
||||||
media browser service with `connected` / `refused or failed` / `no answer`, then its state and
|
and song (TITLE, ARTIST, ALBUM, ALBUM_ART as a size or ALBUM_ART_URI), and the media browser
|
||||||
song (TITLE, ARTIST, ALBUM, ALBUM_ART as a size). CarPlay's is
|
services by name (2.42.1 on; 2.42.0 also connected to them, which stopped the music). CarPlay's
|
||||||
`com.allgo.remoteui.mediabrowserservice/...RemoteUIMediaBrowserService`. Ask what was playing
|
session is `com.allgo.remoteui.mediabrowserservice`, Bluetooth's `com.android.bluetooth`. Ask what was playing
|
||||||
when the check ran.
|
when the check ran.
|
||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
|
|||||||
@@ -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 |
|
| 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 |
|
| 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 |
|
| 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,
|
Key classes (`app/src/main/java/com/evsuite/abrp/`): `AbrpUploadService` (foreground service,
|
||||||
15 s tick), `TelemetryUploader` (+ `TelemetryQueue`, persisted), `TelemetryPayload` (server
|
15 s tick), `TelemetryUploader` (+ `TelemetryQueue`, persisted), `TelemetryPayload` (server
|
||||||
|
|||||||
@@ -140,6 +140,22 @@ 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
|
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`
|
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).
|
(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`,
|
- 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
|
`MediaProbeText`; owner: "is it possible to show playing music in our dashboard as well?", then
|
||||||
"yes start the probe"). Before a now-playing display, it finds where the head unit tells what is
|
"yes start the probe"). Before a now-playing display, it finds where the head unit tells what is
|
||||||
|
|||||||
+2
-2
@@ -34,8 +34,8 @@ android {
|
|||||||
applicationId "com.stinnesbeck.nilsmg4"
|
applicationId "com.stinnesbeck.nilsmg4"
|
||||||
minSdk 27
|
minSdk 27
|
||||||
targetSdk 36
|
targetSdk 36
|
||||||
versionCode 151
|
versionCode 152
|
||||||
versionName "2.42.0"
|
versionName "2.42.1"
|
||||||
|
|
||||||
// Nothing about the server ships in the APK: its URL and ingest token are entered in
|
// 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.
|
// the app's Server tab (ServerConfig), and the database is only reachable by the server.
|
||||||
|
|||||||
@@ -9,25 +9,17 @@ import android.graphics.Bitmap;
|
|||||||
import android.media.AudioManager;
|
import android.media.AudioManager;
|
||||||
import android.media.AudioPlaybackConfiguration;
|
import android.media.AudioPlaybackConfiguration;
|
||||||
import android.media.MediaMetadata;
|
import android.media.MediaMetadata;
|
||||||
import android.media.browse.MediaBrowser;
|
|
||||||
import android.media.session.MediaController;
|
import android.media.session.MediaController;
|
||||||
import android.media.session.MediaSessionManager;
|
import android.media.session.MediaSessionManager;
|
||||||
import android.media.session.PlaybackState;
|
import android.media.session.PlaybackState;
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.HandlerThread;
|
|
||||||
import android.service.media.MediaBrowserService;
|
import android.service.media.MediaBrowserService;
|
||||||
|
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.TreeSet;
|
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
|
* 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
|
* <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
|
* MEDIA_CONTENT_CONTROL: the car build has it as the system user), with their song and
|
||||||
* state;</li>
|
* state;</li>
|
||||||
* <li>every media browser service on the head unit, CarPlay's
|
* <li>every media browser service on the head unit, listed but not connected to (2.42.1:
|
||||||
* ({@link #CARPLAY}, where the car's own media library reads CarPlay's song) among them:
|
* a connection is not harmless, see {@link #browsers}).</li>
|
||||||
* connected to as the car's media library does, its session's song and state read, and
|
|
||||||
* disconnected.</li>
|
|
||||||
* </ol>
|
* </ol>
|
||||||
* Nothing is played, paused or skipped. The report goes where a signal recording goes
|
* No other app's service is bound; nothing is played, paused or skipped. The report goes where
|
||||||
* ({@link SignalRecorder#saveCopies}): the app, a USB stick, the server.
|
* a signal recording goes ({@link SignalRecorder#saveCopies}): the app, a USB stick, the server.
|
||||||
*/
|
*/
|
||||||
final class MediaProbe {
|
final class MediaProbe {
|
||||||
|
|
||||||
static final String FILE = "media_probe.txt";
|
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 volatile boolean running;
|
||||||
private static long startedMs;
|
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) {
|
private static void browsers(Context app, List<String> out) {
|
||||||
out.add("Media browser services (" + MediaBrowserService.SERVICE_INTERFACE + "):");
|
out.add("Media browser services (" + MediaBrowserService.SERVICE_INTERFACE + "), listed, not connected to:");
|
||||||
Set<ComponentName> services = new LinkedHashSet<>();
|
|
||||||
try {
|
try {
|
||||||
List<ResolveInfo> found = app.getPackageManager()
|
List<ResolveInfo> found = app.getPackageManager()
|
||||||
.queryIntentServices(new Intent(MediaBrowserService.SERVICE_INTERFACE), 0);
|
.queryIntentServices(new Intent(MediaBrowserService.SERVICE_INTERFACE), 0);
|
||||||
for (ResolveInfo ri : found == null ? new ArrayList<ResolveInfo>() : found) {
|
if (found == null || found.isEmpty()) {
|
||||||
if (ri.serviceInfo != null) {
|
out.add(" none");
|
||||||
services.add(new ComponentName(ri.serviceInfo.packageName, ri.serviceInfo.name));
|
return;
|
||||||
}
|
}
|
||||||
|
for (ResolveInfo ri : found) {
|
||||||
|
if (ri.serviceInfo == null) continue;
|
||||||
|
out.add(" " + new ComponentName(ri.serviceInfo.packageName, ri.serviceInfo.name)
|
||||||
|
.flattenToShortString());
|
||||||
}
|
}
|
||||||
} catch (RuntimeException e) {
|
} catch (RuntimeException e) {
|
||||||
out.add(" list not readable: " + e.getClass().getSimpleName());
|
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. */
|
/** A session's state and everything its song's metadata holds. */
|
||||||
|
|||||||
@@ -229,7 +229,7 @@
|
|||||||
<string name="signal_recorder_running">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:</string>
|
<string name="signal_recorder_running">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:</string>
|
||||||
<string name="signal_recorder_done">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:</string>
|
<string name="signal_recorder_done">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:</string>
|
||||||
<string name="label_media_probe">Music sources</string>
|
<string name="label_media_probe">Music sources</string>
|
||||||
<string name="media_probe_hint">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.</string>
|
<string name="media_probe_hint">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.</string>
|
||||||
<string name="btn_media_probe">Check music sources</string>
|
<string name="btn_media_probe">Check music sources</string>
|
||||||
<string name="media_probe_running">Checking… (up to a minute)</string>
|
<string name="media_probe_running">Checking… (up to a minute)</string>
|
||||||
<string name="media_probe_done">Check from %1$s, %2$s:</string>
|
<string name="media_probe_done">Check from %1$s, %2$s:</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user