android: the icon where it moves — the glyph, the lock screen and a splash that is not white

Both notification glyphs are the same falcon now, the logo kit's monochrome
path verbatim inside a group that re-frames it for a 24dp box — the playback
card's and the transfers notice's, because a note beside an arrow does not
read as one app, and the transfers one keeps the tray bar it always had so
the falcon dives into it. The fill stays white on transparent: the shade
composites a small icon from its alpha channel alone and tints it itself, so
a coloured export would arrive as a white blob.

The lock screen falls back to the app's own icon instead of a grey hole.
`MediaHub.art` is null from the moment a track changes until the frontend has
finished decoding its cover, and null forever for a track with none, so
`appIcon()` decodes the launcher icon ONCE — through the PackageManager,
because a library module's R has no `ic_launcher` — and hands it to both the
card and `METADATA_KEY_ALBUM_ART`. The app manifest lives in gitignored
`gen/` and declares no category, so the media-session plugin's own library
manifest contributes `android:appCategory="audio"` through the merger, and a
`Theme.App.Starting` on the brand ground replaces Android 12+'s white flash —
in `values-v31` and `values-night-v31` both, 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.

The walk gains `media-card`: after the session block, the card is still
posted on the `playback` channel with the playing track's title on it, and
`media-keys` below it still gets the transport back — which is what a
drawable the shade cannot inflate would actually break. The glyph itself and
the splash are screenshots; `dumpsys notification` prints a resource id and
never a pixel, and the splash is gone before the walk's first frame.
This commit is contained in:
KeelrLeek committed 2026-09-15 07:22:01 -05:00
1 parent 4ebc38d5f3
commit d42645d2a0
8 files changed
+270 -17

No files matched your search

+51 -4
View File
@@ -2439,7 +2439,7 @@ function shadeNotice() {
const dump = adb("shell", "dumpsys", "notification", "--noredact").toString();
const blocks = dump.split("NotificationRecord(").slice(1);
const chans = [];
let title = "", text = "";
let title = "", text = "", card = "";
for (const block of blocks) {
const head = block.slice(0, 6000);
if (!/pkg=com\.soulstream\.app\b/.test(head)) continue;
@@ -2448,7 +2448,6 @@ function shadeNotice() {
let ch = (head.match(/channel=([\w.\-]+)/) || [])[1] || "";
if (!ch || ch === "NotificationChannel") ch = (head.match(/mId='([^']*)'/) || [])[1] || ch;
if (ch) chans.push(ch);
if (ch !== "downloads") continue;
/* the extras print as `<key>=<class> (<value>)`, and the class is
whatever CharSequence the builder kept — String on this one,
but a Spannable on a device that styles it, so the class is
@@ -2457,12 +2456,17 @@ function shadeNotice() {
const m = head.match(new RegExp(key.replace(/\./g, "\\.") + "=[A-Za-z]+ \\(([^\\n]*?)\\)\\s*\\n"));
return m ? m[1].replace(/^"|"$/g, "") : "";
};
/* 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");
}
return { shadeSeen: blocks.length, shadeChans: chans.join("|"), shadeTitle: title, shadeText: text };
return { shadeSeen: blocks.length, shadeChans: chans.join("|"), shadeTitle: title, shadeText: text, shadeCard: card };
} catch (e) {
return { shadeSeen: 0, shadeChans: "ERR " + e.message, shadeTitle: "", shadeText: "" };
return { shadeSeen: 0, shadeChans: "ERR " + e.message, shadeTitle: "", shadeText: "", shadeCard: "" };
}
}
@@ -2946,6 +2950,49 @@ async function main() {
if (!sessOk) problems.push("EXPECT media-session " + JSON.stringify(sess));
console.log((sessOk ? " ok " : " FAIL ") + "media-session".padEnd(16) + " " + JSON.stringify(sess));
/* ── the card is still in the shade ───────────────────────
What a small icon the shade cannot inflate, or a large icon
that throws while the notification is built, actually does:
the card is never posted at all. So read it back out of the
same dump the downloads notice uses — our package, on the
`playback` channel, with the playing track's own title on it.
The title is taken from the player on BOTH sides of the dump
because a track may cross while it runs, and either answer is
a correct one; both sides come from the live app at the same
moment, so a real core answering faster than the mock cannot
make this disagree with itself.
It runs after `resumed` has asserted status === "playing", so
the ongoing card is settled state rather than a race, and it
sits above `media-keys`, which proves the transport still
answers through the session.
What this canNOT prove: the GLYPH. `dumpsys notification`
prints the icon's resource id and never a pixel, so a correct
falcon and a white rectangle read identically here. That one
is a screenshot. */
let cardBefore = await cdp.evaluate(PLAYER);
let cardNote = shadeNotice();
let cardAfter = await cdp.evaluate(PLAYER);
const cardAgrees = () => /playback/.test(cardNote.shadeChans) && !!cardNote.shadeCard
&& (cardNote.shadeCard === cardBefore?.title || cardNote.shadeCard === cardAfter?.title);
if (!cardAgrees()) {
/* the one race the two reads do not cover: `push()` in
media.svelte.js is debounced, so the card trails the
player by a moment, and a track crossing exactly here
shows the player two NEW titles and the shade the old
one. Let it settle and look once more. A card that is
not posted at all fails this the second time too. */
await sleep(2000);
cardBefore = await cdp.evaluate(PLAYER);
cardNote = shadeNotice();
cardAfter = await cdp.evaluate(PLAYER);
}
const cardOk = cardAgrees();
const cardSeen = { chans: cardNote.shadeChans, card: cardNote.shadeCard, title: cardAfter?.title ?? null };
if (!cardOk) problems.push("EXPECT media-card " + JSON.stringify(cardSeen));
console.log((cardOk ? " ok " : " FAIL ") + "media-card".padEnd(16) + " " + JSON.stringify(cardSeen));
adb("shell", "input", "keyevent", "KEYCODE_MEDIA_PAUSE");
await sleep(1500);
const keyPaused = await cdp.evaluate(PLAYER);