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.
231 lines
14 KiB
JavaScript
231 lines
14 KiB
JavaScript
// 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) =>
|
|
'<?xml version="1.0" encoding="utf-8"?>' + NL
|
|
+ head + NL
|
|
+ '<vector xmlns:android="http://schemas.android.com/apk/res/android"' + NL
|
|
+ ' android:width="24dp"' + NL
|
|
+ ' android:height="24dp"' + NL
|
|
+ ' android:viewportWidth="24"' + NL
|
|
+ ' android:viewportHeight="24">' + NL
|
|
+ ' <group' + NL
|
|
+ ' android:translateX="' + group.tx + '"' + NL
|
|
+ ' android:translateY="' + group.ty + '"' + NL
|
|
+ ' android:scaleX="' + group.scale + '"' + NL
|
|
+ ' android:scaleY="' + group.scale + '">' + NL
|
|
+ ' <path' + NL
|
|
+ ' android:fillColor="#FFFFFFFF"' + NL
|
|
+ ' android:fillType="evenOdd"' + NL
|
|
+ ' android:pathData="' + FALCON + '" />' + NL
|
|
+ ' </group>' + NL
|
|
+ extra
|
|
+ '</vector>' + NL;
|
|
|
|
fs.writeFileSync(root + 'crates/tauri-plugin-media-session/android/src/main/res/drawable/ic_soulstream_note.xml',
|
|
glyph(
|
|
`<!-- the status-bar glyph: the falcon, white on transparent, as the shade
|
|
wants — Android composites a notification's small icon from its ALPHA
|
|
CHANNEL and tints it itself, so a coloured export renders as a white blob
|
|
and only a screenshot can tell the difference.
|
|
|
|
The path is the logo kit's monochrome layer verbatim
|
|
(branding/soulstream-logo-kit/android/adaptive/ic_launcher_monochrome.svg),
|
|
drawn in its own large source space. An Android path element has no
|
|
transform attribute, so the SVG's
|
|
translate(9.814 9.814) scale(0.0863) translate(-40.740741 20.370370)
|
|
is folded into the group below and re-framed for a 24dp box: the falcon
|
|
lands x 4.91 to 19.09, y 2 to 22. It is a narrow mark (0.709:1), so the
|
|
side margins are generous and the top and bottom are not, which is what a
|
|
status bar wants. The transfers notice carries the same falcon. -->`,
|
|
{ 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(
|
|
`<!-- the status-bar glyph: the falcon diving into a tray, white on transparent,
|
|
as the shade wants. Its own copy of the path on purpose —
|
|
R.drawable.ic_soulstream_note belongs to the media-session module's R
|
|
class and is not visible here — and deliberately the SAME falcon, so the
|
|
two notices read as one app.
|
|
|
|
Same kit source and same fold as the playback glyph
|
|
(branding/soulstream-logo-kit/android/adaptive/ic_launcher_monochrome.svg),
|
|
sized to clear the tray: the falcon lands x 7.04 to 16.96, y 2 to 16, and
|
|
the tray bar below it is unchanged at y 17.5 to 20. -->`,
|
|
{ tx: '2.4375', ty: '0.0899', scale: '0.018330' },
|
|
' <path' + NL
|
|
+ ' android:fillColor="#FFFFFFFF"' + NL
|
|
+ ' android:pathData="M5,17.5h14v2.5H5z" />' + 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(` <application>
|
|
<service`,
|
|
` <!-- The app's own manifest lives in gitignored gen/ and declares no
|
|
android:appCategory, so the merger takes this one's: an application
|
|
attribute from a library merges up when the app does not set it.
|
|
API 26+, and minSdk here is 26. -->
|
|
<application android:appCategory="audio">
|
|
<service`);
|
|
save();
|
|
|
|
// ── E. the overlay's copy step, only if round forty-four has not ──────
|
|
/* Deliberately conditional. If that round already copies the whole res tree,
|
|
this adds nothing; if it copies some narrower part of it, two overlapping
|
|
`cp -r`s are harmless. The anchor is the gradle banner, which is one line
|
|
in each script and is not something a sibling round moves. */
|
|
for (const script of ['src-tauri/android/build.sh', 'src-tauri/android/dev.sh']) {
|
|
load(script);
|
|
if (s.includes('src-tauri/android/res/.')) continue;
|
|
rep(`echo "== gradle $(date +%T)"`, ins('ins-r45-overlay-copy.txt') + NL + `echo "== gradle $(date +%T)"`);
|
|
save();
|
|
}
|
|
|
|
// ── F. the walk: the card is still posted, and still answers ──────────
|
|
load('tests/mobile/smoke.mjs');
|
|
rep(` let title = "", text = "";`, ` let title = "", text = "", card = "";`);
|
|
/* the `continue` moves BELOW the `extra` declaration it currently sits above.
|
|
It has to: `extra` is a const arrow, so today it is in the temporal dead
|
|
zone for every channel that is not downloads — nothing read it there, so
|
|
nothing broke, and nothing about the downloads branch changes here. */
|
|
rep(` if (ch) chans.push(ch);
|
|
if (ch !== "downloads") continue;`,
|
|
` if (ch) chans.push(ch);`);
|
|
rep(` title = extra("android.title");
|
|
text = extra("android.text");`,
|
|
` /* the media card's own title, for \`media-card\` in the phone
|
|
tail. This is why the \`continue\` below now sits under the
|
|
\`extra\` declaration rather than over it. */
|
|
if (ch === "playback") card = extra("android.title");
|
|
if (ch !== "downloads") continue;
|
|
title = extra("android.title");
|
|
text = extra("android.text");`);
|
|
rep(` return { shadeSeen: blocks.length, shadeChans: chans.join("|"), shadeTitle: title, shadeText: text };`,
|
|
` return { shadeSeen: blocks.length, shadeChans: chans.join("|"), shadeTitle: title, shadeText: text, shadeCard: card };`);
|
|
rep(` return { shadeSeen: 0, shadeChans: "ERR " + e.message, shadeTitle: "", shadeText: "" };`,
|
|
` return { shadeSeen: 0, shadeChans: "ERR " + e.message, shadeTitle: "", shadeText: "", shadeCard: "" };`);
|
|
/* between `media-session` and `media-keys` on purpose: the session block has
|
|
just proved the card is up, and the keys block below still proves the
|
|
transport, exactly where it was. A sibling round appending at the END of the
|
|
phone tail, or after `background`, will not meet this. */
|
|
rep(`console.log((sessOk ? " ok " : " FAIL ") + "media-session".padEnd(16) + " " + JSON.stringify(sess));`,
|
|
`console.log((sessOk ? " ok " : " FAIL ") + "media-session".padEnd(16) + " " + JSON.stringify(sess));`
|
|
+ NL + NL + ins('ins-r45-smoke-card.txt'));
|
|
save();
|
|
|
|
// ── G. the doc ────────────────────────────────────────────────────────
|
|
load('ANDROID.md');
|
|
/* the last sentence of the media-session paragraph, NOT the `## What a screen
|
|
reader says` heading below it and NOT the `## Background playback` heading
|
|
above it — round forty-four will want a heading of its own for the icon,
|
|
and this round creates none. No round count anywhere in the file is
|
|
touched. */
|
|
rep(`the desktop has \`smtc.rs\`).`,
|
|
`the desktop has \`smtc.rs\`).` + NL + NL + ins('ins-r45-doc-icon.txt'));
|
|
/* the end of the onTimeout paragraph in "The downloads in the shade" */
|
|
rep(`the fetches are the Rust core's own and Transfers still
|
|
lists them.`,
|
|
`the fetches are the Rust core's own and Transfers still
|
|
lists them.` + NL + NL + ins('ins-r45-doc-down.txt'));
|
|
save();
|
|
|
|
console.log('round 45 wired');
|