android: the handoff — the pending rounds, the pipeline, and what is unverified
Everything written this session that has not been through the device: rounds 43 to 48 (each patch adversarially verified, 43's transient-notice expectation fixed), a partial round 49, the pipeline scripts, the Android Auto design and the icon comparison mock. Also the two harness changes that were never A/B'd — adaptive waits, which may only shorten a declared wait and never lengthen it, and a ship that runs the mock walk and the deploy at the same time. The A/B and its 255-step baseline are in handoff/tools; main stays at what the phone has actually walked.
This commit is contained in:
1 parent
867fd42972
commit
9f0601027c
400 files changed
+26401
-3
No files matched your search
@@ -0,0 +1,419 @@
|
||||
// Round forty-nine: Android Auto can see the app.
|
||||
//
|
||||
// Auto reaches a media app through MediaBrowserService, so `MediaService`
|
||||
// becomes a `MediaLibraryService`, its filter grows the two actions Auto and
|
||||
// media3 actually look for, an `automotive_app_desc.xml` and its
|
||||
// `<meta-data>` declare the app to the car, and `onGetLibraryRoot` answers
|
||||
// with a browsable root — immediately, unconditionally, out of
|
||||
// `Futures.immediateFuture`, because `MediaLibraryServiceLegacyStub.onGetRoot`
|
||||
// blocks the binder thread serving the host on that future with NO timeout
|
||||
// and runs the callback on the main handler. Nothing is under the root yet:
|
||||
// `onGetChildren` is left un-overridden, its default answers
|
||||
// ERROR_NOT_SUPPORTED at once, and round fifty fills the tree.
|
||||
//
|
||||
// THE ROUND ALSO CARRIES A MITIGATION IT CANNOT BE SPLIT FROM. androidx/media
|
||||
// #3158 — open since 2026-04-03, confirmed again 2026-09-04, reproduced on
|
||||
// media3 1.11.0 — has a host binding a MediaLibraryService with nothing
|
||||
// playing drive the service round onCreate → onBind → onGetSession →
|
||||
// onConnect → onDestroy at ~30 Hz, deterministically after a force-stop with
|
||||
// no existing binding. That is exactly how Android Auto connects and exactly
|
||||
// what tools/auto-prober/ does. media3 will not act before an Auto-side fix
|
||||
// lands, so the loop cannot be stopped from here; it can only be made to cost
|
||||
// nothing. So the session, the player, the notification provider, the audio
|
||||
// focus and the becoming-noisy receiver move into `MediaHub` for the life of
|
||||
// the PROCESS, `onCreate` allocates nothing, and `onDestroy` calls
|
||||
// `removeSession` rather than `release()` — round forty-eight's version would
|
||||
// otherwise have abandoned audio focus thirty times a second while music was
|
||||
// playing and latched `SoulStreamPlayer.released`, which is a card that never
|
||||
// updates again.
|
||||
//
|
||||
// And the instrument: tools/auto-prober/, a standalone Kotlin/Gradle app
|
||||
// OUTSIDE src-tauri/gen/android (which `tauri android init` regenerates) that
|
||||
// binds the same way Auto does, with Auto's own root hints, and logs the tree
|
||||
// as JSON. With the app force-stopped there is no WebView, so no CDP, so the
|
||||
// phone walk is blind to all of this; the prober is the only eye the project
|
||||
// will have on Auto without a head unit.
|
||||
//
|
||||
// Two corrections to the plan, argued in the files: `adb shell pm dump` is
|
||||
// `dumpsys package` and prints NO application meta-data, so the Auto
|
||||
// declaration cannot be a hard walk assertion — the two that ARE hard are the
|
||||
// intent filters (`auto-browse`, `auto-library`), which catch the
|
||||
// TAURI_ANDROID_PROJECT_PATH trap completely because the filters and the
|
||||
// meta-data live in one library manifest and are lost together; and the
|
||||
// prober module compiles at jvmTarget 1.8 like every other module here, not
|
||||
// 17 (JDK 17 is what RUNS Gradle).
|
||||
//
|
||||
// No Rust, no frontend, no cargo. Kotlin and Gradle cannot be compiled on
|
||||
// this machine: the Kotlin is read against the media3 1.10.1 sources and the
|
||||
// first `bash src-tauri/android/dev.sh` is where a typo shows.
|
||||
const fs = require('fs');
|
||||
const root = process.env.SS_ROOT || 'F:/Coding/SoulStream/';
|
||||
const scratch = 'C:/Users/Kyle/AppData/Local/Temp/claude/F--Coding-SoulStream/5488514a-11e8-482a-b0a0-20cb511ee110/scratchpad/';
|
||||
/* LF, always. ANDROID.md is mixed — two of round forty-eight's ten inserts
|
||||
were written CRLF and its `rep`s carried that into the file — and the rest
|
||||
of the repo is LF, so everything this round writes is normalised to LF and
|
||||
the two anchors that land in those CRLF paragraphs are converted instead
|
||||
(see `crlf` below). A normalisation that ever changed an anchor read back
|
||||
out of an ins-r48 file would fail in the pre-flight, before a byte moves,
|
||||
which is the safe direction for it to fail in. */
|
||||
const ins = (name) =>
|
||||
fs.readFileSync(scratch + name, 'utf8').replace(/\r\n/g, String.fromCharCode(10)).replace(/\s+$/, '');
|
||||
let p, s;
|
||||
const load = (file) => { p = file; s = fs.readFileSync(root + p, 'utf8'); };
|
||||
const save = () => fs.writeFileSync(root + p, s);
|
||||
const once = (a) => {
|
||||
const i = s.indexOf(a);
|
||||
if (i < 0) throw new Error('missing in ' + p + ': ' + a.slice(0, 90));
|
||||
if (s.indexOf(a, i + 1) >= 0) throw new Error('ambiguous in ' + p + ': ' + a.slice(0, 90));
|
||||
return i;
|
||||
};
|
||||
const rep = (a, b) => { once(a); s = s.replace(a, () => b); };
|
||||
const NL = String.fromCharCode(10);
|
||||
/* An anchor as it sits in one of ANDROID.md's two CRLF paragraphs — the ones
|
||||
ins-r48-doc-bg.txt and ins-r48-doc-card-proves.txt wrote. Everything else
|
||||
in the file, and every byte this round writes, is LF. */
|
||||
const crlf = (t) => t.split(NL).join(String.fromCharCode(13) + NL);
|
||||
|
||||
const PLUGIN = 'crates/tauri-plugin-media-session/';
|
||||
const KOTLIN = PLUGIN + 'android/src/main/java/com/soulstream/mediasession/';
|
||||
const XML = PLUGIN + 'android/src/main/res/xml/';
|
||||
const TOOLS = 'tools/auto-prober/';
|
||||
const PROBER = TOOLS + 'prober/src/main/';
|
||||
|
||||
/* ── the re-run guard, before a byte moves ────────────────────────────
|
||||
This round `rep`s across four files, rewrites a fifth and creates ten.
|
||||
A second pass would throw on the first anchor it had already consumed and
|
||||
leave a tree with a MediaLibraryService whose manifest still declares one
|
||||
action, or a doc half rewritten. `res/xml/automotive_app_desc.xml` is what
|
||||
this round creates and nothing else in the repo makes. */
|
||||
if (fs.existsSync(root + XML + 'automotive_app_desc.xml')) {
|
||||
throw new Error(XML + 'automotive_app_desc.xml already exists — round 49 has already run against ' + root);
|
||||
}
|
||||
|
||||
/* ── every anchor, and every insert, before the first write ───────────
|
||||
Round forty-four's lesson, which is now the house rule: check it ALL
|
||||
here, so the round is all or nothing. A patch that wrote first left a
|
||||
half-applied tree its own re-run guard then refused to finish. */
|
||||
|
||||
// AndroidManifest.xml — the comment and the filter, as ONE span. Replaced
|
||||
// whole on purpose: round forty-eight's reason ("media3 finds its own
|
||||
// service through this") survives a careless edit and would then be a wrong
|
||||
// reason on a load-bearing comment, which is how the next round breaks it.
|
||||
const A_FILTER = ' <!-- media3 finds its own service through this, and while the' + NL +
|
||||
' process is alive it is what routes a headset or a card' + NL +
|
||||
' button here. There is no MediaButtonReceiver any more, and' + NL +
|
||||
" media3's own is deliberately NOT put in its place: its" + NL +
|
||||
' docs make that receiver conditional on' + NL +
|
||||
' MediaSession.Callback.onPlaybackResumption(), which this' + NL +
|
||||
' app cannot implement — playback lives in Rust behind a' + NL +
|
||||
' WebView round trip. Two receivers would only have fought' + NL +
|
||||
' over the buttons. -->' + NL +
|
||||
' <intent-filter>' + NL +
|
||||
' <action android:name="androidx.media3.session.MediaSessionService" />' + NL +
|
||||
' </intent-filter>';
|
||||
const A_APPEND = ' </service>' + NL + ' </application>';
|
||||
|
||||
// consumer-rules.pro
|
||||
const A_RULES = "# MediaService is declared in this module's AndroidManifest.xml, so AGP's" + NL +
|
||||
'# own merged-manifest keep rules hold it by name, and SoulStreamPlayer needs' + NL +
|
||||
'# no rule of its own — the only thing that reaches it is a direct reference' + NL +
|
||||
"# from MediaService. androidx's MediaButtonReceiver went with the move to" + NL +
|
||||
"# media3: the service's own SERVICE_INTERFACE filter routes media buttons" + NL +
|
||||
'# now, and two receivers would have fought over them.';
|
||||
|
||||
// tests/mobile/smoke.mjs
|
||||
const A_PLUMB_FN = '/* ── the service, the focus stack and the noisy receiver ──────────────';
|
||||
const A_PLUMB_LOG = ' console.log(" -- " + "media-plumbing".padEnd(16) + " " + JSON.stringify(mediaPlumbing()) + " (advisory: prints, never fails)");';
|
||||
|
||||
// ANDROID.md
|
||||
const A_DOC_TABLE = '| `tests/mobile/smoke.mjs`, `perf.mjs`, `cdp.mjs` | the phone UI walked in headless Chrome or on the phone, the frame-time probe, and the DevTools client they share (below) |';
|
||||
/* CRLF: this sentence came from ins-r48-doc-bg.txt */
|
||||
const A_DOC_PLUGIN = crlf('`MediaService` is a `MediaSessionService` holding' + NL +
|
||||
'one `MediaSession`, and `SoulStreamPlayer` is a `SimpleBasePlayer`');
|
||||
const A_DOC_UNCHANGED = '`src/mobile/media.svelte.js`, which did not change.';
|
||||
/* CRLF: this paragraph came from ins-r48-doc-card-proves.txt */
|
||||
const A_DOC_CARDPROVES = crlf('queue window arriving across the bridge, field for field (`queue-window`).' + NL +
|
||||
'Printed but never failed, because their `dumpsys` formats have never been read' + NL +
|
||||
'back from this phone: whether the service is foreground, whether our audio' + NL +
|
||||
'focus is in the system\'s stack, and whether the becoming-noisy receiver is' + NL +
|
||||
'registered (`media-plumbing`).');
|
||||
const A_DOC_PROVES = 'Then, after the step loop, ten things the mock cannot do at all: a force-stop' + NL +
|
||||
'and the paused restore that follows it (waited out past twenty-five seconds,' + NL +
|
||||
'which is well past the ten-second FGS-kill window), the position restored,' + NL +
|
||||
'playback resumed, the system media session and its ownership, media keys,' + NL +
|
||||
'exactly one card in the shade and exactly one session in the system\'s list —' + NL +
|
||||
'the two checks that prove the media3 migration landed rather than doubled —' + NL +
|
||||
'ten seconds in the background with the position still advancing, SoulStream\'s' + NL +
|
||||
'own mixer underruns, and the queue window that rides along with every card' + NL +
|
||||
'update and that the lock-screen card is now built out of. One more line' + NL +
|
||||
'prints and never fails: whether the service is foreground, whether our audio' + NL +
|
||||
'focus is in the system\'s stack, and whether the becoming-noisy receiver is' + NL +
|
||||
'registered. What none of it touches is in "What the walk proves about the' + NL +
|
||||
'card, and what it does not", above.';
|
||||
/* the heading the prober section is inserted BEFORE — the last subsection of
|
||||
"## Verifying" ends where this begins. Anchored on the heading that FOLLOWS
|
||||
rather than on "## Verifying" itself, so a sibling round inserting earlier
|
||||
in that section cannot collide with this one. */
|
||||
const A_DOC_NOTDONE = NL + '## Not done yet' + NL;
|
||||
/* round forty-eight's whole "Android Auto, half-built" bullet, byte for byte
|
||||
from the insert that wrote it, replaced whole */
|
||||
const A_DOC_AUTO = ins('ins-r48-doc-auto.txt');
|
||||
|
||||
for (const [file, anchors] of [
|
||||
[PLUGIN + 'android/src/main/AndroidManifest.xml', [A_FILTER, A_APPEND]],
|
||||
[PLUGIN + 'android/consumer-rules.pro', [A_RULES]],
|
||||
['tests/mobile/smoke.mjs', [A_PLUMB_FN, A_PLUMB_LOG]],
|
||||
['ANDROID.md', [A_DOC_TABLE, A_DOC_PLUGIN, A_DOC_UNCHANGED, A_DOC_CARDPROVES,
|
||||
A_DOC_PROVES, A_DOC_NOTDONE, A_DOC_AUTO]],
|
||||
]) {
|
||||
load(file);
|
||||
for (const a of anchors) once(a);
|
||||
}
|
||||
/* the file this round rewrites whole, and two the new Kotlin leans on: named
|
||||
here so a tree pointed at the wrong place fails now rather than after four
|
||||
writes. MediaService.kt has no text anchor at all — it is replaced from
|
||||
ins-r49-service.txt, round forty-eight's own idiom for this file — so its
|
||||
existence is the only check there can be. */
|
||||
for (const f of [
|
||||
KOTLIN + 'MediaService.kt',
|
||||
KOTLIN + 'SoulStreamPlayer.kt',
|
||||
PLUGIN + 'android/src/main/res/values/strings.xml',
|
||||
PLUGIN + 'android/src/main/res/drawable/ic_soulstream_note.xml',
|
||||
]) {
|
||||
if (!fs.existsSync(root + f)) throw new Error('not the tree this round expects: missing ' + f);
|
||||
}
|
||||
/* the generated project is where probe.sh borrows its Gradle wrapper from,
|
||||
and it is gitignored — so its absence is a note, not a failure */
|
||||
if (!fs.existsSync(root + 'src-tauri/gen/android/gradlew.bat')) {
|
||||
console.log('note: no src-tauri/gen/android/gradlew.bat — tools/auto-prober/probe.sh borrows that wrapper, so it wants a normal Android build first');
|
||||
}
|
||||
/* and every insert, because a typo in a filename should not land half an
|
||||
Auto surface either */
|
||||
for (const f of ['ins-r49-service.txt', 'ins-r49-walk-helper.txt', 'ins-r49-walk-checks.txt',
|
||||
'ins-r49-probe-activity.txt', 'ins-r49-probe-manifest.txt', 'ins-r49-probe-sh.txt',
|
||||
'ins-r49-probe-readme.txt', 'ins-r49-doc-prober.txt', 'ins-r49-doc-auto.txt',
|
||||
'ins-r49-doc-proves.txt', 'ins-r49-doc-cardproves.txt', 'ins-r49-doc-singleton.txt']) {
|
||||
if (!fs.existsSync(scratch + f)) throw new Error('missing insert: ' + scratch + f);
|
||||
}
|
||||
|
||||
// ── the resource that declares the app to a car ──────────────────────────
|
||||
/* Compiled with this project's own aapt2 (build-tools 36.0.0) before it was
|
||||
written here. It is referenced from the manifest as
|
||||
`@xml/automotive_app_desc`, which is a COMPILE-TIME reference: a missing
|
||||
resource fails the link rather than shipping a dangling meta-data id — and
|
||||
that is the only hard proof of this file there is, because `pm dump` prints
|
||||
no application meta-data at all. tools/auto-prober/ reads it back off the
|
||||
device through PackageManager.GET_META_DATA. */
|
||||
fs.mkdirSync(root + XML, { recursive: true });
|
||||
fs.writeFileSync(root + XML + 'automotive_app_desc.xml', [
|
||||
'<?xml version="1.0" encoding="utf-8"?>',
|
||||
'<!-- What Android Auto reads to decide this is a media app it can host.',
|
||||
' Named by the <meta-data android:name="com.google.android.gms.car.',
|
||||
' application"> in this module\'s AndroidManifest.xml; "media" is the',
|
||||
' whole declaration (the other value, "template", is for a car app',
|
||||
' built on the Car App Library, which this is not). -->',
|
||||
'<automotiveApp>',
|
||||
' <uses name="media" />',
|
||||
'</automotiveApp>',
|
||||
'',
|
||||
].join(NL));
|
||||
|
||||
// ── the manifest: three actions on one filter, and the Auto declaration ──
|
||||
load(PLUGIN + 'android/src/main/AndroidManifest.xml');
|
||||
rep(A_FILTER,
|
||||
' <!-- Three actions, one component, and all three are safe.' + NL +
|
||||
' Android Auto binds the LEGACY one' + NL +
|
||||
' (android.media.browse.MediaBrowserService), which media3' + NL +
|
||||
" still serves: MediaSessionService.onBind answers it with" + NL +
|
||||
' the session\'s legacy browser binder. media3\'s own two are' + NL +
|
||||
' how a MediaController and a SessionToken find this' + NL +
|
||||
' service; SessionToken(Context, ComponentName) tests' + NL +
|
||||
' MediaLibraryService FIRST, so the component is one' + NL +
|
||||
' TYPE_LIBRARY_SERVICE token however many actions it wears,' + NL +
|
||||
' and getAllServiceTokens collects into a Set.' + NL +
|
||||
NL +
|
||||
' The MediaSessionService action is KEPT rather than' + NL +
|
||||
' replaced because the media-button path already verified on' + NL +
|
||||
' this phone hangs off it: MediaSessionLegacyStub looks for' + NL +
|
||||
' an ACTION_MEDIA_BUTTON receiver, finds none (there is no' + NL +
|
||||
' MediaButtonReceiver any more, and media3\'s own is' + NL +
|
||||
' deliberately not in its place — its docs make it' + NL +
|
||||
' conditional on MediaSession.Callback.onPlaybackResumption(),' + NL +
|
||||
' which this app cannot implement because playback lives in' + NL +
|
||||
' Rust behind a WebView round trip), and falls to a service' + NL +
|
||||
' declared under MediaLibraryService and then' + NL +
|
||||
' MediaSessionService. Either way it lands here.' + NL +
|
||||
NL +
|
||||
' android:exported is true above for the same reason the' + NL +
|
||||
' filters are here: a browse service Auto cannot bind is a' + NL +
|
||||
' browse service that does not exist. -->' + NL +
|
||||
' <intent-filter>' + NL +
|
||||
' <action android:name="androidx.media3.session.MediaLibraryService" />' + NL +
|
||||
' <action android:name="androidx.media3.session.MediaSessionService" />' + NL +
|
||||
' <action android:name="android.media.browse.MediaBrowserService" />' + NL +
|
||||
' </intent-filter>');
|
||||
/* beside android:appCategory="audio", which is already this manifest's — a
|
||||
library <application> attribute merges up when the app does not set one,
|
||||
and the app's own manifest lives in gitignored gen/ */
|
||||
rep(A_APPEND,
|
||||
' </service>' + NL +
|
||||
NL +
|
||||
' <!-- What puts SoulStream in Android Auto\'s app list at all.' + NL +
|
||||
' The resource is res/xml/automotive_app_desc.xml and the' + NL +
|
||||
' reference is resolved at LINK time, so a missing file fails' + NL +
|
||||
' the build rather than shipping a car-invisible app. -->' + NL +
|
||||
' <meta-data' + NL +
|
||||
' android:name="com.google.android.gms.car.application"' + NL +
|
||||
' android:resource="@xml/automotive_app_desc" />' + NL +
|
||||
' </application>');
|
||||
save();
|
||||
|
||||
// ── the service, rewritten whole ─────────────────────────────────────────
|
||||
fs.writeFileSync(root + KOTLIN + 'MediaService.kt', ins('ins-r49-service.txt') + NL);
|
||||
|
||||
// ── the keep rules: three objects now, and a different filter story ──────
|
||||
load(PLUGIN + 'android/consumer-rules.pro');
|
||||
rep(A_RULES,
|
||||
"# MediaService is declared in this module's AndroidManifest.xml, so AGP's" + NL +
|
||||
'# own merged-manifest keep rules hold it by name, and SoulStreamPlayer,' + NL +
|
||||
'# MediaHub and LibraryTree need no rule of their own — the only thing that' + NL +
|
||||
'# reaches any of them is a direct reference from MediaService.' + NL +
|
||||
"# androidx's MediaButtonReceiver went with the move to media3: the" + NL +
|
||||
"# service's own filter routes media buttons now — through the" + NL +
|
||||
'# MediaLibraryService action, which MediaSessionLegacyStub queries first —' + NL +
|
||||
'# and two receivers would have fought over them.');
|
||||
save();
|
||||
|
||||
// ── the prober: its own Gradle build, outside gen/ ───────────────────────
|
||||
/* gen/android is regenerated by `npx tauri android init` and gitignored, so
|
||||
anything put in there is deleted the next time someone runs it. This is a
|
||||
separate build that borrows that project's WRAPPER — which pins Gradle
|
||||
8.14.3, AGP 8.11.0, Kotlin 1.9.25 and JDK 17, all already in the cache, so
|
||||
a probe run downloads nothing. There is deliberately no
|
||||
gradle-wrapper.jar here: a Node patch cannot write a binary, and a second
|
||||
copy of one would be a second thing to keep in step. */
|
||||
fs.mkdirSync(root + PROBER + 'java/com/soulstream/autoprober', { recursive: true });
|
||||
|
||||
fs.writeFileSync(root + TOOLS + 'settings.gradle.kts', [
|
||||
'/* The prober is its own build. No pluginManagement block: the root',
|
||||
' build.gradle.kts puts AGP and the Kotlin plugin on the buildscript',
|
||||
' classpath the way src-tauri/gen/android does, so nothing here is',
|
||||
' resolved from the Gradle Plugin Portal. */',
|
||||
'rootProject.name = "auto-prober"',
|
||||
'include(":prober")',
|
||||
'',
|
||||
].join(NL));
|
||||
|
||||
fs.writeFileSync(root + TOOLS + 'build.gradle.kts', [
|
||||
'/* Copied in shape from src-tauri/gen/android/build.gradle.kts, versions',
|
||||
' included: AGP 8.11.0 and Kotlin 1.9.25 are what the app is built with',
|
||||
' and what is already in Kyle\'s Gradle cache. buildscript{} rather than',
|
||||
' plugins{} for the same reason it is that way over there. */',
|
||||
'buildscript {',
|
||||
' repositories {',
|
||||
' google()',
|
||||
' mavenCentral()',
|
||||
' }',
|
||||
' dependencies {',
|
||||
' classpath("com.android.tools.build:gradle:8.11.0")',
|
||||
' classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25")',
|
||||
' }',
|
||||
'}',
|
||||
'',
|
||||
'allprojects {',
|
||||
' repositories {',
|
||||
' google()',
|
||||
' mavenCentral()',
|
||||
' }',
|
||||
'}',
|
||||
'',
|
||||
].join(NL));
|
||||
|
||||
fs.writeFileSync(root + TOOLS + 'gradle.properties', [
|
||||
'org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8',
|
||||
'android.useAndroidX=true',
|
||||
'android.nonTransitiveRClass=true',
|
||||
'',
|
||||
].join(NL));
|
||||
|
||||
fs.writeFileSync(root + TOOLS + '.gitignore', [
|
||||
'# the prober is source; its build output and its Gradle state are not',
|
||||
'build/',
|
||||
'.gradle/',
|
||||
'local.properties',
|
||||
'',
|
||||
].join(NL));
|
||||
|
||||
fs.writeFileSync(root + TOOLS + 'prober/build.gradle.kts', [
|
||||
'plugins {',
|
||||
' id("com.android.application")',
|
||||
' id("org.jetbrains.kotlin.android")',
|
||||
'}',
|
||||
'',
|
||||
'android {',
|
||||
' namespace = "com.soulstream.autoprober"',
|
||||
' compileSdk = 36',
|
||||
'',
|
||||
' defaultConfig {',
|
||||
' applicationId = "com.soulstream.autoprober"',
|
||||
' // 26 and 36 to match the app: package visibility (the <queries>',
|
||||
' // block in the manifest) only behaves like the real thing at a',
|
||||
' // targetSdk that has it, and a prober that can see more than',
|
||||
' // Android Auto can would prove the wrong thing.',
|
||||
' minSdk = 26',
|
||||
' targetSdk = 36',
|
||||
' versionCode = 1',
|
||||
' versionName = "1.0"',
|
||||
' }',
|
||||
'',
|
||||
' compileOptions {',
|
||||
' sourceCompatibility = JavaVersion.VERSION_1_8',
|
||||
' targetCompatibility = JavaVersion.VERSION_1_8',
|
||||
' }',
|
||||
' kotlinOptions {',
|
||||
' jvmTarget = "1.8"',
|
||||
' }',
|
||||
'}',
|
||||
'',
|
||||
'// NO dependencies, deliberately. android.media.browse.MediaBrowser is the',
|
||||
'// framework class Android Auto itself uses to reach a media3 app, and a',
|
||||
'// prober built out of media3 would not prove that path works. The Kotlin',
|
||||
'// plugin adds its own stdlib and nothing else comes along.',
|
||||
'',
|
||||
].join(NL));
|
||||
|
||||
fs.writeFileSync(root + PROBER + 'AndroidManifest.xml', ins('ins-r49-probe-manifest.txt') + NL);
|
||||
fs.writeFileSync(root + PROBER + 'java/com/soulstream/autoprober/ProbeActivity.kt',
|
||||
ins('ins-r49-probe-activity.txt') + NL);
|
||||
fs.writeFileSync(root + TOOLS + 'probe.sh', ins('ins-r49-probe-sh.txt') + NL);
|
||||
fs.writeFileSync(root + TOOLS + 'README.md', ins('ins-r49-probe-readme.txt') + NL);
|
||||
|
||||
// ── the walk: one reader, two hard checks and an advisory ────────────────
|
||||
load('tests/mobile/smoke.mjs');
|
||||
/* the helper goes beside the other dumpsys readers, before mediaPlumbing() */
|
||||
rep(A_PLUMB_FN, ins('ins-r49-walk-helper.txt') + NL + NL + A_PLUMB_FN);
|
||||
/* and the checks right after the plumbing line, while the app is still in
|
||||
front and something is playing — the same block one-card and one-session
|
||||
live in */
|
||||
rep(A_PLUMB_LOG, A_PLUMB_LOG + NL + ins('ins-r49-walk-checks.txt'));
|
||||
save();
|
||||
|
||||
// ── the doc ──────────────────────────────────────────────────────────────
|
||||
load('ANDROID.md');
|
||||
rep(A_DOC_TABLE, A_DOC_TABLE + NL +
|
||||
'| `tools/auto-prober/` | a throwaway app that binds SoulStream\'s `MediaBrowserService` the way Android Auto does and logs the tree as JSON — the only eye on Auto without a head unit, because a force-stopped app has no WebView and so no DevTools socket for `smoke.mjs`. Its own Gradle build, outside `gen/android/` so `tauri android init` cannot delete it (below) |');
|
||||
rep(A_DOC_PLUGIN,
|
||||
'`MediaService` is a `MediaLibraryService` holding' + NL +
|
||||
'one process-lifetime `MediaLibrarySession`, which is what lets Android Auto' + NL +
|
||||
'list and open the app at all, and `SoulStreamPlayer` is a' + NL +
|
||||
'`SimpleBasePlayer`');
|
||||
rep(A_DOC_UNCHANGED, A_DOC_UNCHANGED + NL + NL + ins('ins-r49-doc-singleton.txt'));
|
||||
rep(A_DOC_CARDPROVES, ins('ins-r49-doc-cardproves.txt'));
|
||||
rep(A_DOC_PROVES, ins('ins-r49-doc-proves.txt'));
|
||||
/* a section of its own, as the last thing under "## Verifying" */
|
||||
rep(A_DOC_NOTDONE, NL + ins('ins-r49-doc-prober.txt') + NL + NL + '## Not done yet' + NL);
|
||||
/* and what is now true, and what is still not */
|
||||
rep(A_DOC_AUTO, ins('ins-r49-doc-auto.txt'));
|
||||
save();
|
||||
|
||||
console.log('round 49 wired');
|
||||
Reference in new issue
Block a user