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.
This commit is contained in:
1 parent
867fd42972
commit
9f0601027c
400 files changed
+26401
-3
No files matched your search
@@ -0,0 +1,328 @@
|
||||
// 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');
|
||||
Reference in new issue
Block a user