// 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 = [ '', '', '', ' ', ' ', ' ', '', '', ].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 = [ '', '', ' ', ' #FF05080B', '', '', ].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');