Files
SoulStream/handoff/rounds/patch-round44.cjs
T
KeelrLeek 9f0601027c 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.
2026-09-14 07:57:41 -05:00

170 lines
9.9 KiB
JavaScript

// Round forty-four: the phone has been wearing tauri's logo. `tauri android
// init` writes the generated project's res from its own template and never
// reads src-tauri/icons/android/ — which has carried the falcon since 50afa6a
// — and the detour build runs cargo and Gradle directly, so nothing ever
// copied it across: the two dominant colours in the packaged
// mipmap-xxxhdpi/ic_launcher.png are #24c8db and #ffc131, tauri's own brand
// cyan and amber. So: src-tauri/android/res/, tracked, carrying a real
// adaptive icon with a MONOCHROME layer (Android 13's themed icons; tauri's
// generator makes none), the legacy mipmaps under it, and the background as
// one #FF05080B colour resource rather than five identical flat PNGs; a copy
// of the whole tree from build.sh and dev.sh before Gradle, because
// gen/android is regenerated and gitignored; and verify-icon.sh, which opens
// the APK afterwards and reads the resources back out, because the way this
// loses is silent — a project rebuilt without the copy still compiles, still
// installs, and wears the old logo with nothing in the log. A debug APK is
// read by name and by byte (AGP packages that build type --no-crunch, so the
// entries ARE the files); a release APK keeps neither — it re-encodes the PNGs
// and shortens every resource path away — so that path asks the resource table
// through aapt2 and compares pixel dimensions. No frontend, no Rust, no
// Kotlin, no walk step: a launcher icon is not reachable from inside the app.
const fs = require('fs');
const root = process.env.SS_ROOT || 'F:/Coding/SoulStream/';
/* branding/ is gitignored, so the kit is never inside a snapshot of the repo;
it lives on the real tree and is what the overlay is copied FROM. */
const kit = process.env.SS_KIT || 'F:/Coding/SoulStream/branding/soulstream-logo-kit/android/';
const scratch = 'C:/Users/Kyle/AppData/Local/Temp/claude/F--Coding-SoulStream/5488514a-11e8-482a-b0a0-20cb511ee110/scratchpad/';
const ins = (name) => fs.readFileSync(scratch + name, 'utf8').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);
/* Two shell files and a doc take `rep`s that would throw halfway through on a
second pass and leave a half-written tree. Refuse one here, before a byte
moves: the overlay directory is what this round creates and nothing else in
the repo makes it. */
if (fs.existsSync(root + 'src-tauri/android/res')) {
throw new Error('src-tauri/android/res already exists — round 44 has already run against ' + root);
}
/* and the kit is not optional: a half-copied overlay is the one failure
verify-icon.sh cannot tell apart from a missing one */
const DPI = ['mdpi', 'hdpi', 'xhdpi', 'xxhdpi', 'xxxhdpi'];
const sources = [];
for (const d of DPI) {
sources.push(['legacy/mipmap-' + d + '/ic_launcher.png', 'mipmap-' + d + '/ic_launcher.png']);
sources.push(['legacy/mipmap-' + d + '/ic_launcher.png', 'mipmap-' + d + '/ic_launcher_round.png']);
sources.push(['adaptive/res/mipmap-' + d + '/ic_launcher_foreground.png', 'mipmap-' + d + '/ic_launcher_foreground.png']);
sources.push(['adaptive/res/mipmap-' + d + '/ic_launcher_monochrome.png', 'mipmap-' + d + '/ic_launcher_monochrome.png']);
}
for (const pair of sources) {
if (!fs.existsSync(kit + pair[0])) {
throw new Error('the logo kit is not at ' + kit + ' (missing ' + pair[0] + ') — set SS_KIT');
}
}
/* And every text anchor, before a byte moves. Two shell files and a doc take
`rep`s; a missing anchor in the second or third would otherwise throw with
the overlay and verify-icon.sh already written, and the re-run guard then
refuses to finish what it started. Checked here, the round is all or
nothing. */
const A_COPY = 'echo "== gradle $(date +%T)"' + NL + 'cd src-tauri/gen/android';
const A_BUILD_DONE = 'ls -la "$APK"' + NL + 'echo "== done $(date +%T)"';
const A_DEV_APK = 'APK=src-tauri/gen/android/app/build/outputs/apk/arm64/debug/app-arm64-debug.apk' + NL + 'echo "== install $(date +%T)"';
const A_DOC_ROW = '| `src-tauri/android/build.sh`, `deploy.sh` | the detour build (below) and build → install → launch → screenshot |';
const A_DOC_BYHAND = '# 2. package with Gradle, skipping the task that would redo the symlink';
const A_DOC_SECTION = '`adb logcat --pid=$(adb shell pidof com.soulstream.app)`.' + NL + NL + '## Verifying';
for (const [file, anchors] of [
['src-tauri/android/build.sh', [A_COPY, A_BUILD_DONE]],
['src-tauri/android/dev.sh', [A_COPY, A_DEV_APK]],
['ANDROID.md', [A_DOC_ROW, A_DOC_BYHAND, A_DOC_SECTION]],
]) {
load(file);
for (const a of anchors) once(a);
}
/* the five inserts too: a typo in a filename should not land a half-made
overlay either */
for (const f of ['ins-r44-verify-icon.txt', 'ins-r44-overlay-copy.txt', 'ins-r44-doc-row.txt', 'ins-r44-doc-byhand.txt', 'ins-r44-doc-section.txt']) {
if (!fs.existsSync(scratch + f)) throw new Error('missing insert: ' + scratch + f);
}
// ── the overlay: the app module's res, where gen/ cannot lose it ──────
/* The adaptive XML is NOT the kit's. The kit points every layer at
`@drawable/...` while shipping them as PNGs under `mipmap-*` with no
`drawable-*` directory at all — and the generated project still carries
Android Studio's template vectors under those exact drawable names, so two
of the three would resolve to the green robot and only the monochrome one
would fail the build. `@mipmap/` for the two image layers, `@color/` for
the background. Verified by an aapt2 compile+link over the real merged res
tree, 2026-09-14. */
const ICON_XML = [
'<?xml version="1.0" encoding="utf-8"?>',
'<!-- @mipmap, not the kit\'s @drawable: the layers ship as PNGs under',
' mipmap-*, and @drawable/ic_launcher_foreground still resolves to the',
' template robot in the generated project. The background is one colour',
' resource, so the gradient replaces one file when it is approved. -->',
'<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">',
' <background android:drawable="@color/ic_launcher_background" />',
' <foreground android:drawable="@mipmap/ic_launcher_foreground" />',
' <monochrome android:drawable="@mipmap/ic_launcher_monochrome" />',
'</adaptive-icon>',
'',
].join(NL);
/* Named after tauri's own file on purpose: src-tauri/icons/android/values/
ic_launcher_background.xml declares this same colour as #fff, and two files
in one values/ directory declaring one colour is a hard aapt2 error. Same
name, so a copy overwrites rather than collides. */
const BG_XML = [
'<?xml version="1.0" encoding="utf-8"?>',
'<resources>',
' <!-- the brand\'s night; the whole adaptive background is this -->',
' <color name="ic_launcher_background">#FF05080B</color>',
'</resources>',
'',
].join(NL);
const RES = root + 'src-tauri/android/res/';
fs.mkdirSync(RES + 'mipmap-anydpi-v26', { recursive: true });
fs.mkdirSync(RES + 'values', { recursive: true });
for (const d of DPI) fs.mkdirSync(RES + 'mipmap-' + d, { recursive: true });
fs.writeFileSync(RES + 'mipmap-anydpi-v26/ic_launcher.xml', ICON_XML);
fs.writeFileSync(RES + 'mipmap-anydpi-v26/ic_launcher_round.xml', ICON_XML);
fs.writeFileSync(RES + 'values/ic_launcher_background.xml', BG_XML);
for (const pair of sources) fs.copyFileSync(kit + pair[0], RES + pair[1]);
// ── the check, written before the scripts that call it ────────────────
fs.writeFileSync(root + 'src-tauri/android/verify-icon.sh', ins('ins-r44-verify-icon.txt') + NL);
// ── build.sh: copy before Gradle, read the APK back after ─────────────
/* The anchor is the gradle echo and the cd under it, which both scripts
share. A sibling round adding a resource changes nothing here: the copy
takes the whole tree, not a file list. */
load('src-tauri/android/build.sh');
rep(A_COPY, ins('ins-r44-overlay-copy.txt') + NL + A_COPY);
/* after the APK is named and listed, before "done" — so both build types
reach it, and the release one is told its bytes are crunched */
rep(A_BUILD_DONE,
'ls -la "$APK"' + NL +
'bash src-tauri/android/verify-icon.sh "$APK" $([ "$REL" = "1" ] && echo --crunched)' + NL +
'echo "== done $(date +%T)"');
save();
// ── dev.sh: the same two, the debug APK only ──────────────────────────
load('src-tauri/android/dev.sh');
rep(A_COPY, ins('ins-r44-overlay-copy.txt') + NL + A_COPY);
rep(A_DEV_APK,
'APK=src-tauri/gen/android/app/build/outputs/apk/arm64/debug/app-arm64-debug.apk' + NL +
'bash src-tauri/android/verify-icon.sh "$APK"' + NL +
'echo "== install $(date +%T)"');
save();
// ── the doc ───────────────────────────────────────────────────────────
load('ANDROID.md');
rep(A_DOC_ROW, A_DOC_ROW + NL + ins('ins-r44-doc-row.txt'));
/* the by-hand block claims to be what the scripts do; it now misses a step */
rep(A_DOC_BYHAND, ins('ins-r44-doc-byhand.txt') + NL + A_DOC_BYHAND);
/* a section of its own, between the release build and Verifying. The anchor
is the last line of the release section plus the heading under it, so it
cannot match the identical logcat line up in "Build and run". */
rep(A_DOC_SECTION,
'`adb logcat --pid=$(adb shell pidof com.soulstream.app)`.' + NL + NL + ins('ins-r44-doc-section.txt') + NL + NL + '## Verifying');
save();
console.log('round 44 wired');