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.
329 lines
20 KiB
JavaScript
329 lines
20 KiB
JavaScript
// Round forty-seven: the launcher's ground. Round 44 gave the phone the real
|
|
// falcon and made the adaptive background ONE COLOUR — `#FF05080B`, the kit's
|
|
// flat night — with a note that a gradient was being mocked and would replace
|
|
// one file. The mock has settled it: the falcon covers about a quarter of a
|
|
// circle-masked tile, so the ground is three quarters of what anyone sees; the
|
|
// desktop ships concept 04 "Deep navy glow" and its glow survives every mask,
|
|
// lifting the visible top-left corner from #05080B to #0E3543; and a phone
|
|
// wearing a flatter icon than the desktop is a divergence for no gain. So the
|
|
// `@color/` becomes a `@drawable/`: one vector carrying concept 04's linear
|
|
// surface and the radial glow over it, layer-fit — the 1024 artwork mapped
|
|
// onto the whole 108 layer rather than onto the 72 window, which is
|
|
// indistinguishable under a circle or a squircle and never shows a flat edge
|
|
// when a launcher parallaxes the layers apart.
|
|
//
|
|
// A VECTOR and not five PNGs, on evidence rather than preview: the kit's glow
|
|
// is translate(270 220) rotate(43) scale(660), a UNIFORM scale, so the SVG
|
|
// ellipse is a circle and the rotate is a no-op — which is exactly what
|
|
// android:type="radial" can carry (centre + gradientRadius, no transform of
|
|
// its own). Two numbers there and no vector could have said it. The linear
|
|
// axis is 51.1 degrees off horizontal, which also rules out the other cheap
|
|
// route, a <layer-list> of <shape>s, whose android:angle "must be a multiple
|
|
// of 45". Folding stop-opacity into 8-bit alpha costs at most one level of
|
|
// 255. Banding is not a vector-vs-PNG question — both are 8-bit and quantise
|
|
// identically — but a PNG could be dithered and a vector cannot, so if Kyle's
|
|
// launcher screenshot contours, the fix is a dithered density set behind this
|
|
// same @drawable/ic_launcher_ground name.
|
|
//
|
|
// Named ic_launcher_ground and NOT ic_launcher_background: the generated
|
|
// project still carries Android Studio's template drawable of that name (the
|
|
// green robot), so a build whose overlay copy failed would silently link it.
|
|
// Under this name the same failure is a hard aapt2 link error.
|
|
//
|
|
// verify-icon.sh follows. It does not ask whether the file is in the APK — it
|
|
// asks what <background> RESOLVES to, through the resource table, in both
|
|
// build types, and then reads concept 04's six stops back out of what it
|
|
// found. That is the check that catches a stale APK as well as a lost
|
|
// overlay. What it cannot do is see a pixel: banding, the glow's weight under
|
|
// the Pixel's mask and how the falcon reads on a lit ground are a launcher
|
|
// screenshot on Kyle's phone, and nothing here claims otherwise.
|
|
//
|
|
// Round 45's notification glyphs and splash sit on the same overlay and are
|
|
// not touched; nor are the legacy mipmaps (with minSdk 26, mipmap-anydpi-v26
|
|
// always wins, and verify-icon compares them byte for byte). No build.sh, no
|
|
// dev.sh — round 44's `cp -r src-tauri/android/res/.` takes the whole tree, so
|
|
// a new drawable/ directory needs no build change. No Rust, no Kotlin, no
|
|
// frontend, no walk step.
|
|
//
|
|
// Not "unreachable from inside the app", though, and the round does not claim
|
|
// it: MediaHub.appIcon() in the media-session plugin draws whatever
|
|
// getApplicationIcon() returns into a 512 px bitmap and hands it to the
|
|
// lock-screen card as the large icon for any track whose cover has not been
|
|
// decoded yet, so the card carries this ground too. It is the ONE reader of
|
|
// the launcher icon inside the app (grepped: nothing else touches
|
|
// getApplicationIcon, R.mipmap or @mipmap at runtime), it is a fallback and
|
|
// not a fixture, it caches after one decode, and the walk asserts the card's
|
|
// android.title and the session's PLAYBACK state — never its icon — so no
|
|
// step changes meaning. ANDROID.md puts it on the list of things only the
|
|
// phone can answer, since it is the quickest of them for Kyle to look at.
|
|
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;
|
|
/* `save` does not save. Round 44's pre-flight checked every anchor against
|
|
the files as they were ON DISK and then wrote as it went, which is one step
|
|
short: an insert can make a LATER anchor ambiguous, and then the check
|
|
passes while the run still dies halfway through with two files rewritten.
|
|
(The ground block below carries its own copy of the aapt2 discovery, and
|
|
that is exactly what it did.) So every edit is computed in memory first,
|
|
`save` only queues the result, and nothing reaches the disk until all of
|
|
them are queued — the reps themselves, in the order they really run, are
|
|
the pre-flight. */
|
|
const queued = [];
|
|
const load = (file) => { p = file; s = fs.readFileSync(root + p, 'utf8'); };
|
|
const save = () => { queued.push([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);
|
|
|
|
const RES = root + 'src-tauri/android/res/';
|
|
const GROUND = RES + 'drawable/ic_launcher_ground.xml';
|
|
|
|
/* ── the re-run guard, first and before anything is read ──────────────────
|
|
Two files take `rep`s and three are rewritten wholesale; a second pass
|
|
would throw partway and leave the tree half done, which is exactly what
|
|
round 44's own first draft did. The ground drawable is what this round
|
|
creates and nothing else in the repo makes it. */
|
|
if (fs.existsSync(GROUND)) {
|
|
throw new Error('src-tauri/android/res/drawable/ic_launcher_ground.xml already exists — round 47 has already run against ' + root);
|
|
}
|
|
|
|
/* ── pre-flight: everything checked before a byte moves ───────────────── */
|
|
const holds = (file, needles) => {
|
|
let t;
|
|
try { t = fs.readFileSync(root + file, 'utf8'); }
|
|
catch (e) { throw new Error('cannot read ' + file + ' under ' + root + ' — ' + e.code); }
|
|
for (const n of needles) {
|
|
if (!t.includes(n)) throw new Error(file + ' does not hold ' + JSON.stringify(n.slice(0, 70)) + ' — this is not the tree round 47 was written against');
|
|
}
|
|
return t;
|
|
};
|
|
|
|
/* round 44 has to have run, and to still say what it said: refuse to clobber
|
|
an overlay somebody else has already moved on */
|
|
holds('src-tauri/android/res/values/ic_launcher_background.xml', [
|
|
'the brand\'s night; the whole adaptive background is this',
|
|
'<color name="ic_launcher_background">#FF05080B</color>',
|
|
]);
|
|
for (const x of ['ic_launcher', 'ic_launcher_round']) {
|
|
holds('src-tauri/android/res/mipmap-anydpi-v26/' + x + '.xml', [
|
|
'<background android:drawable="@color/ic_launcher_background" />',
|
|
'<monochrome android:drawable="@mipmap/ic_launcher_monochrome" />',
|
|
]);
|
|
}
|
|
|
|
/* ── and the SOURCE of the numbers, which is the whole premise ──────────
|
|
The phone's ground is the desktop's ground. If src-tauri/icons/icon.svg is
|
|
ever re-cut, the literals below stop describing it and this round would
|
|
ship a divergence while claiming to close one — so the round refuses
|
|
instead. Everything is derived here and compared against the insert, so a
|
|
typo in either one is caught rather than shipped. */
|
|
/* the whole stop, offset included, and not just the colour: an offset is a
|
|
number the SVG owns exactly as much as a coordinate is, and six right
|
|
colours at three wrong places is a different gradient. */
|
|
const SVG = holds('src-tauri/icons/icon.svg', [
|
|
'<linearGradient id="rounded-4-surface-gradient" x1="130" y1="50" x2="880" y2="980" gradientUnits="userSpaceOnUse">',
|
|
'gradientTransform="translate(270 220) rotate(43) scale(660)"',
|
|
'<stop offset="0" stop-color="#0A2330"/>',
|
|
'<stop offset="0.52" stop-color="#07141F"/>',
|
|
'<stop offset="1" stop-color="#040A10"/>',
|
|
'<stop offset="0" stop-color="#31C0CB" stop-opacity="0.18"/>',
|
|
'<stop offset="0.58" stop-color="#1F8FC9" stop-opacity="0.05"/>',
|
|
'<stop offset="1" stop-color="#07141F" stop-opacity="0"/>',
|
|
]);
|
|
/* layer-fit: the 1024 artwork over the whole 108 layer. 108/1024 is 27/256,
|
|
so every one of these terminates inside eight decimals and lands on an
|
|
exact float — no rounding goes into the drawable at all. */
|
|
const at = (v) => (v * 27 / 256).toFixed(8).replace(/0+$/, '').replace(/\.$/, '');
|
|
const a8 = (a) => Math.round(a * 255).toString(16).toUpperCase().padStart(2, '0');
|
|
const GEOM = ['startX="' + at(130) + '"', 'startY="' + at(50) + '"', 'endX="' + at(880) + '"', 'endY="' + at(980) + '"',
|
|
'centerX="' + at(270) + '"', 'centerY="' + at(220) + '"', 'gradientRadius="' + at(660) + '"'];
|
|
const STOPS = ['#FF0A2330', '#FF07141F', '#FF040A10',
|
|
'#' + a8(0.18) + '31C0CB', '#' + a8(0.05) + '1F8FC9', '#' + a8(0) + '07141F'];
|
|
/* and the pair, which is what the drawable actually has to spell. The bare
|
|
colours above stay because verify-icon.sh's overlay check greps for those
|
|
strings on their own; these are what stop an offset typo. */
|
|
const PAIRS = [['0', STOPS[0]], ['0.52', STOPS[1]], ['1', STOPS[2]],
|
|
['0', STOPS[3]], ['0.58', STOPS[4]], ['1', STOPS[5]]]
|
|
.map(([o, c]) => 'android:offset="' + o + '" android:color="' + c + '"');
|
|
|
|
/* every insert, before a typo in a filename lands a half-made overlay */
|
|
const INSERTS = ['ins-r47-ground-vector.txt', 'ins-r47-adaptive-icon.txt', 'ins-r47-bg-color.txt',
|
|
'ins-r47-verify-header.txt', 'ins-r47-verify-overlay.txt', 'ins-r47-verify-ground.txt',
|
|
'ins-r47-verify-debug.txt', 'ins-r47-doc-lead.txt', 'ins-r47-doc-ground.txt',
|
|
'ins-r47-doc-colour.txt', 'ins-r47-doc-checking.txt', 'ins-r47-doc-cannot.txt'];
|
|
for (const f of INSERTS) {
|
|
if (!fs.existsSync(scratch + f)) throw new Error('missing insert: ' + scratch + f);
|
|
}
|
|
/* and the ground insert has to be the artwork above, not a near miss */
|
|
{
|
|
const g = ins('ins-r47-ground-vector.txt');
|
|
for (const n of GEOM.concat(PAIRS)) {
|
|
if (!g.includes(n)) throw new Error('ins-r47-ground-vector.txt is missing ' + n + ', which src-tauri/icons/icon.svg says the ground is');
|
|
}
|
|
for (const n of ['android:type="linear"', 'android:type="radial"', 'xmlns:aapt="http://schemas.android.com/aapt"']) {
|
|
if (!g.includes(n)) throw new Error('ins-r47-ground-vector.txt is missing ' + n);
|
|
}
|
|
}
|
|
|
|
/* ── the text anchors, all of them, before a byte moves ────────────────── */
|
|
const A_VI_HEADER = '# Debug and release are read DIFFERENTLY, because AGP treats their resources';
|
|
const A_VI_OVERLAY = [
|
|
'for d in $DPI; do for n in $NAMES; do',
|
|
' test -f "$RES/mipmap-$d/$n.png" || bad "$RES/mipmap-$d/$n.png is missing from the overlay"',
|
|
'done; done',
|
|
].join(NL);
|
|
const A_VI_LIST = 'LIST="$(unzip -l "$APK")" || bad "cannot read $APK"';
|
|
const A_VI_SDKU = [
|
|
'SDKU=""',
|
|
'[ -n "${ANDROID_HOME:-}" ] && SDKU="$(cygpath -u "$ANDROID_HOME" 2>/dev/null || echo "")"',
|
|
'[ -z "$SDKU" ] && [ -n "${LOCALAPPDATA:-}" ] && SDKU="$(cygpath -u "$LOCALAPPDATA" 2>/dev/null || echo "")/Android/Sdk"',
|
|
'A2="$(ls -1 "$SDKU"/build-tools/*/aapt2.exe 2>/dev/null | sort -V | tail -1 || true)"',
|
|
].join(NL);
|
|
const A_VI_DEBUG_NUM = ' # 2. debug: the adaptive XMLs by name, then all twenty PNGs byte for byte.';
|
|
/* the backslash is real shell, so it is doubled for JS */
|
|
const A_VI_DEBUG_XMLS = [
|
|
' for x in ic_launcher ic_launcher_round; do',
|
|
' echo "$LIST" | grep -q " res/mipmap-anydpi-v26/$x\\.xml$" \\',
|
|
' || bad "res/mipmap-anydpi-v26/$x.xml is not in the APK — $RES never reached src-tauri/gen/android/app/src/main/res"',
|
|
' done',
|
|
].join(NL);
|
|
const A_VI_REL_NUM = [
|
|
'# 3. release. Ask the resource table what the names point at, since the paths',
|
|
'# have very likely been shortened away. aapt2 is beside the zipalign and',
|
|
'# apksigner build.sh has just used.',
|
|
].join(NL);
|
|
const A_VI_REL_MAP = 'MAP="$("$A2" dump resources "$APK" | awk \'';
|
|
|
|
const A_DOC_TABLE = 'the legacy mipmaps and the one colour the background is.';
|
|
const A_DOC_BULLET = '- `values/ic_launcher_background.xml` — `#FF05080B`, the brand\'s night.';
|
|
const A_DOC_FLAT = [
|
|
'and `@color/` for the background instead.',
|
|
'',
|
|
'**The background is one colour resource, not five identical flat PNGs.**',
|
|
'A richer gradient is being mocked separately; when it is approved it replaces',
|
|
'one file, and the three XML references do not move.',
|
|
].join(NL);
|
|
const A_DOC_COLOUR = [
|
|
'**The colour file is named `ic_launcher_background.xml` on purpose.**',
|
|
'`src-tauri/icons/android/values/ic_launcher_background.xml` declares the same',
|
|
].join(NL);
|
|
const A_DOC_LEAD = [
|
|
'resources back out. It checks the overlay\'s own two XMLs first, before the APK',
|
|
'is opened — a `<monochrome>` layer that a later edit drops is a failure too —',
|
|
'and then reads the APK, debug and release differently, because AGP hands them',
|
|
'to aapt2 differently:',
|
|
].join(NL);
|
|
const A_DOC_CHECKING = [
|
|
'Both `build.sh` and `dev.sh` call it, so no APK this repo makes can quietly go',
|
|
'back to the old artwork. It costs about a second and a half on the 107 MB',
|
|
'debug APK.',
|
|
].join(NL);
|
|
const A_DOC_SPLASH = 'five seconds, that splash is the falcon on `#05080B` rather than tauri\'s mark.';
|
|
const A_DOC_CANNOT = [
|
|
'**Adaptive masking and the monochrome layer can only be judged from the',
|
|
'phone\'s home screen**, which is a minute with the Pixel: the icon under the',
|
|
'launcher\'s mask, in the app drawer, in recents and in another app\'s share',
|
|
'sheet — and then Wallpaper & style → Themed icons on, to see the monochrome',
|
|
'layer take the wallpaper\'s colour.',
|
|
].join(NL);
|
|
|
|
// ── the check follows the layer (computed, not yet written) ───────────
|
|
load('src-tauri/android/verify-icon.sh');
|
|
rep(A_VI_HEADER, ins('ins-r47-verify-header.txt') + NL + '#' + NL + A_VI_HEADER);
|
|
/* section 1, straight after the adaptive XMLs and before the PNGs */
|
|
rep(A_VI_OVERLAY, ins('ins-r47-verify-overlay.txt') + NL + A_VI_OVERLAY);
|
|
/* The aapt2 discovery moves up out of the release branch, because both build
|
|
types read the table now — SDKU as well as A2, since the release branch's
|
|
"NOT CHECKED" message interpolates it. This rep goes FIRST: the ground
|
|
block below carries its own copy of those four lines, and inserting it
|
|
before this would leave two of them in the file and make this anchor
|
|
ambiguous. */
|
|
rep(A_VI_SDKU, '# $A2 and $SDKU come from section 2, which both build types now need.');
|
|
/* the shared block, after the zip listing and before the build types split,
|
|
so it runs for both */
|
|
rep(A_VI_LIST, A_VI_LIST + NL + NL + ins('ins-r47-verify-ground.txt'));
|
|
/* renumbered, since a section 2 now sits in front of them */
|
|
rep(A_VI_DEBUG_NUM, ' # 3. debug: the adaptive XMLs by name, then all twenty PNGs byte for byte.');
|
|
rep(A_VI_REL_NUM, [
|
|
'# 4. release. Ask the resource table what the names point at, since the paths',
|
|
'# have very likely been shortened away.',
|
|
].join(NL));
|
|
/* and in debug the ground is still a file with a name */
|
|
rep(A_VI_DEBUG_XMLS, A_VI_DEBUG_XMLS + NL + ins('ins-r47-verify-debug.txt'));
|
|
/* section 2 has already dumped the table; asking aapt2 for it a second time
|
|
on a 107 MB release APK is this round's own doing to undo */
|
|
rep(A_VI_REL_MAP, 'MAP="$(echo "$TBL" | awk \'');
|
|
save();
|
|
|
|
// ── the doc ───────────────────────────────────────────────────────────
|
|
load('ANDROID.md');
|
|
rep(A_DOC_TABLE, 'the legacy mipmaps and the gradient drawable the background is.');
|
|
rep(A_DOC_BULLET, [
|
|
'- `drawable/ic_launcher_ground.xml` — the adaptive **background**: concept 04\'s',
|
|
' gradient, the desktop icon\'s own ground, over the whole 108-unit layer;',
|
|
'- `values/ic_launcher_background.xml` — `#FF05080B`, the brand\'s night. Not',
|
|
' the background any more, and kept for its filename (below).',
|
|
].join(NL));
|
|
rep(A_DOC_FLAT, ins('ins-r47-doc-ground.txt'));
|
|
rep(A_DOC_COLOUR, ins('ins-r47-doc-colour.txt'));
|
|
rep(A_DOC_LEAD, ins('ins-r47-doc-lead.txt'));
|
|
rep(A_DOC_CHECKING, ins('ins-r47-doc-checking.txt'));
|
|
/* NOT "the falcon on concept 04's ground", and not the flat night
|
|
either. Whether the splash draws an adaptive icon's BACKGROUND is the
|
|
platform's decision, and developer.android.com states it as "if there is
|
|
enough contrast with the window background" — which is a simplification of
|
|
SplashscreenContentDrawer.processAdaptiveIcon, whose first and dominant
|
|
clause is !mIsBgComplex. Contrast is consulted only for a background that
|
|
is ONE colour; a multi-colour one is kept whatever the contrast is. So
|
|
round 44's flat #FF05080B — the same colour round 45 writes as
|
|
windowSplashScreenBackground, so isRgbSimilarInHsv's a == b short circuit
|
|
fires — was dropped, and this gradient one will not be. That is the
|
|
OPPOSITE of what the one-line version of the rule predicts, which is why
|
|
the doc names the real one. */
|
|
rep(A_DOC_SPLASH, [
|
|
'five seconds, that splash is the falcon rather than tauri\'s mark. Whether',
|
|
'the ground is under it there is the platform\'s call, and this round very',
|
|
'likely changes the answer. `SplashscreenContentDrawer.processAdaptiveIcon`',
|
|
'drops an adaptive icon\'s background and draws the foreground alone —',
|
|
'enlarged, since it is then filling the slot by itself — only when that',
|
|
'background is **not complex** *and* reads as the same colour as',
|
|
'`windowSplashScreenBackground`. A background with more than one colour in it',
|
|
'is kept and the whole icon is drawn, contrast or no contrast. Round 44\'s flat',
|
|
'`#FF05080B` is exactly the colour round 45\'s `values-v31/themes.xml` writes',
|
|
'there, so it was dropped and the falcon drew large on the flat night; a',
|
|
'gradient is not one colour, so the likely new answer is the whole icon —',
|
|
'this ground, under a falcon no longer enlarged to fill the slot. Likely,',
|
|
'not proven: it is a screenshot, and the docs\' own one-line version of that',
|
|
'rule would have predicted the other way.',
|
|
].join(NL));
|
|
rep(A_DOC_CANNOT, ins('ins-r47-doc-cannot.txt'));
|
|
save();
|
|
|
|
// ── and only now, the disk ────────────────────────────────────────────
|
|
/* Every anchor above matched, in the order the reps really run, and both
|
|
files are finished strings. What is left cannot fail for a reason this
|
|
round can cause. */
|
|
// the overlay: one layer swapped, nothing else moved. A new directory under
|
|
// src-tauri/android/res/ — build.sh and dev.sh copy the whole tree
|
|
// (`cp -r src-tauri/android/res/. …`), so neither script changes.
|
|
fs.mkdirSync(RES + 'drawable', { recursive: true });
|
|
fs.writeFileSync(GROUND, ins('ins-r47-ground-vector.txt') + NL);
|
|
/* both adaptive XMLs are byte-identical, as round 44 wrote them */
|
|
const ICON_XML = ins('ins-r47-adaptive-icon.txt') + NL;
|
|
fs.writeFileSync(RES + 'mipmap-anydpi-v26/ic_launcher.xml', ICON_XML);
|
|
fs.writeFileSync(RES + 'mipmap-anydpi-v26/ic_launcher_round.xml', ICON_XML);
|
|
/* the colour keeps its value and its filename; only the comment changes, and
|
|
the comment is the whole defence — a reader who saw only this file would
|
|
otherwise still think the background is flat */
|
|
fs.writeFileSync(RES + 'values/ic_launcher_background.xml', ins('ins-r47-bg-color.txt') + NL);
|
|
for (const [file, text] of queued) fs.writeFileSync(root + file, text);
|
|
|
|
console.log('round 47 wired');
|