// 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(`