// Round forty-five: the icon where it moves. The launcher tile is the round // before this one; these are the four places the mark has to appear that a // mipmap does not reach. The status-bar glyph on BOTH notices — the playback // card's and the transfers notice's — becomes the kit's own monochrome falcon, // because a note beside an arrow does not read as one app, and because the // shade composites a small icon from its ALPHA CHANNEL alone: the fill stays // white on transparent, and a coloured export would render as a white blob. // The lock screen gets an app-icon fallback for its large icon, decoded ONCE, // because `MediaHub.art` is null on every track change until a cover finishes // decoding and is null forever for a track that has none. The app manifest // lives in gitignored gen/ and declares no `android:appCategory`, so the // media-session plugin's library manifest contributes `audio` through the // merger. And Android 12+ draws a splash whatever we do — a white flash today // — so a `Theme.App.Starting` on the brand ground replaces it, in values-v31 // AND values-night-v31, because the qualifier order puts night mode ahead of // platform version and a splash written only into values-v31 is silently // ignored on a dark-mode phone. // // No Rust. The walk gains one assertion, `media-card`, and it asserts the only // thing here that can actually break: a drawable the shade cannot inflate, or // a large icon that throws, and the card is simply never posted. The glyph // itself and the splash are SCREENSHOTS — `dumpsys notification` prints the // icon's resource id and never a pixel, and the splash is gone before the // walk's first frame. 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/'; 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); /* Four of the writes below are whole files, which succeed as happily the second time as the first, while the Kotlin, manifest, build-script and smoke.mjs `rep`s would throw somewhere in the middle and leave half a tree. Refuse a second run here, before a byte is touched: `appIcon` is the name this round puts in MediaService.kt and nothing else in the repo makes it. */ const MEDIA_KT = 'crates/tauri-plugin-media-session/android/src/main/java/com/soulstream/mediasession/MediaService.kt'; if (fs.readFileSync(root + MEDIA_KT, 'utf8').includes('fun appIcon(')) { throw new Error('round 45 is already applied in ' + root + ' — reset the tree rather than re-running'); } /* The glyph source: the `d` of the kit's monochrome layer, byte for byte, no edit. The kit's README is explicit that the foreground's optical offset sits deliberately above the geometric safe-zone line and must not be "fixed" — it is inside this contour's own placement chain, and the group numbers below only re-frame the whole thing for a 24dp box, which preserves it. */ const FALCON = ins('ins-r45-falcon-path.txt'); if (FALCON.length !== 5161 || !/^M [\d.]/.test(FALCON) || !FALCON.endsWith('Z') || /[^MCLZ0-9.\- ]/.test(FALCON)) { throw new Error('ins-r45-falcon-path.txt is not the kit path (len ' + FALCON.length + ')'); } // ── A. the two glyphs, the same falcon in both modules ──────────────── /* Whole-file writes: there is nothing in either drawable worth anchoring on, and the transfers one keeps its tray bar byte-identical so the pair still reads as one family — the falcon dives into the tray, which is the download. Two copies of the path on purpose: the comment already in the down glyph says why (R is per-module and the media-session module's is not visible from the transfers one). */ const glyph = (head, group, extra) => '' + NL + head + NL + '' + NL + ' ' + NL + ' ' + NL + ' ' + NL + extra + '' + NL; fs.writeFileSync(root + 'crates/tauri-plugin-media-session/android/src/main/res/drawable/ic_soulstream_note.xml', glyph( ``, { tx: '-1.6606', ty: '-0.7287', scale: '0.026185' }, '')); fs.writeFileSync(root + 'crates/tauri-plugin-transfer-notice/android/src/main/res/drawable/ic_soulstream_down.xml', glyph( ``, { tx: '2.4375', ty: '0.0899', scale: '0.018330' }, ' ' + NL)); // ── B. the app-icon fallback, so the lock screen is never a grey hole ─ load(MEDIA_KT); /* alphabetically where they belong, and BitmapFactory is the last of the graphics imports today. Canvas is the only one this round needs: AdaptiveIconDrawable is deliberately NOT special-cased — see appIcon(). */ rep(`import android.graphics.BitmapFactory`, `import android.graphics.BitmapFactory import android.graphics.Canvas`); /* the last field of MediaHub, so a sibling adding one of its own to the top of the object does not meet this */ rep(` @Volatile var service: MediaService? = null`, ` @Volatile var service: MediaService? = null /** the app's own icon, decoded once — see appIcon() */ @Volatile private var icon: Bitmap? = null`); rep(` fun act(action: String, position: Double = -1.0) {`, ins('ins-r45-appicon.txt') + NL + NL + ` fun act(action: String, position: Double = -1.0) {`); /* the session's metadata (the lock-screen background) and the card's own large icon. `this` is the service, which is the Context appIcon wants. */ rep(` MediaHub.art?.let {`, ` (MediaHub.art ?: MediaHub.appIcon(this))?.let {`); rep(` .setLargeIcon(MediaHub.art)`, ` .setLargeIcon(MediaHub.art ?: MediaHub.appIcon(this))`); save(); // ── C. the splash, as a tracked res overlay ─────────────────────────── /* Round forty-four's overlay root, assumed to be src-tauri/android/res/ mirroring app/src/main/res/ — the only shape consistent with what is here: gen/ is gitignored, and build.sh already copies proguard-soulstream.pro in exactly this way. If that round chose a different root, MOVE these two files: they would otherwise land where nothing copies them and the splash would do nothing at all, with no build error to say so. */ fs.mkdirSync(root + 'src-tauri/android/res/values-v31', { recursive: true }); fs.mkdirSync(root + 'src-tauri/android/res/values-night-v31', { recursive: true }); fs.writeFileSync(root + 'src-tauri/android/res/values-v31/themes.xml', ins('ins-r45-themes-v31.txt') + NL); fs.writeFileSync(root + 'src-tauri/android/res/values-night-v31/themes.xml', ins('ins-r45-themes-night-v31.txt') + NL); // ── D. appCategory, the only way in from a repo that cannot edit gen/ ─ load('crates/tauri-plugin-media-session/android/src/main/AndroidManifest.xml'); rep(`