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

+67
View File
@@ -786,6 +786,68 @@ and headphones coming out pause. The app's capability grants
permission resolves everywhere, and on the desktop its commands answer
"not here" (the desktop has `smtc.rs`).
The glyph in the status bar is the falcon, not a note.
`ic_soulstream_note.xml` is the logo kit's monochrome path verbatim
(`branding/soulstream-logo-kit/android/adaptive/ic_launcher_monochrome.svg`),
wrapped in a `<group>` that re-frames it for a 24dp box — an Android path
element has no `transform` attribute, so the SVG's own
`translate/scale/translate` chain is folded into the group's numbers and the
falcon lands x 4.91→19.09, y 2→22. The fill stays `#FFFFFFFF` on a
transparent viewport because Android composites a notification's small icon
from its ALPHA CHANNEL and tints it itself: a coloured export renders as a
white blob. Nothing in the walk can tell those two apart — `dumpsys
notification` prints the icon's RESOURCE ID and never a pixel — so the glyph
is a screenshot, and the only thing the walk can prove is the thing that
actually breaks. `media-card` proves it: after `resumed`, the card is still
posted, on the `playback` channel, carrying the playing track's own title,
and `media-keys` a line below still gets the transport back.
The card's large icon falls back to the app's own icon. `MediaHub.art` is
cleared the moment `artKey` changes and only refilled once the frontend has
decoded a cover into bytes, and a track with no cover never refills it at
all — so without a fallback every track change has a window where the lock
screen shows a grey hole. `MediaHub.appIcon()` decodes the icon ONCE and
keeps it (`refresh()` runs on every position tick, so a per-update decode
would be a real cost), through the PackageManager rather than a resource id
because a library module's own R has no `ic_launcher`. It is drawn at
`setBounds(0, 0, 512, 512)`, and that is less obvious than it looks: an
`AdaptiveIconDrawable`'s bounds are the **72** units the launcher shows, not
the 108 its layers are drawn in — it scales its own layers up by 3/2 and
masks them inside whatever box it is handed, which is why its intrinsic width
is two thirds of the layer PNG. So the plain call already IS the launcher's
view, and a box set outside the tile to "reveal the 18% bleed" does the
opposite: it crops into the safe zone. The kit's foreground inks y 20.5→86.5
of 108, so a box 128 units oversize on each side would have shown 30→78 —
the head and the tail cut off. The same bitmap serves
`METADATA_KEY_ALBUM_ART`.
`android:appCategory="audio"` is contributed by the media-session plugin's
own library manifest. The app manifest lives in gitignored `gen/` and cannot
be edited from the repo, and it declares no category — so the merger takes
the library's, which is the one route there is.
Android 12+ draws a splash whether we ask or not, and the platform's own is a
white flash; `Theme.App.Starting` puts the brand ground (`#05080B`, the kit's
`night`) and `@mipmap/ic_launcher` there instead, and takes the window
background with it so the frame before the WebView's first paint is not white
either. It is delivered as a tracked res overlay, `src-tauri/android/res/`,
laid over the generated project by `build.sh` and `dev.sh` — the same move
`build.sh` already makes for `proguard-soulstream.pro`, and for the same
reason: `gen/` is regenerated and gitignored, so nothing of ours survives in
it. The overlay carries BOTH `values-v31/themes.xml` and
`values-night-v31/themes.xml`, and that pair is not belt and braces: the
resource qualifier order puts NIGHT MODE ahead of PLATFORM VERSION, so on a
dark-mode Android 14 phone the generated `values-night/themes.xml` beats a
lone `values-v31/` and the splash is silently ignored — on this phone, most
likely always. The style IS `Theme.app` (its parent), not a theme nothing
references, because `androidx.core:core-splashscreen` is not a dependency of
the app module: the platform attributes are the whole mechanism, there is no
`postSplashScreenTheme`, and `Theme.app` already is the post-splash theme.
Both overlay files replace `Theme.app` outright on API 31+, so anything a
later round adds to the generated `values/themes.xml` has to be added to them
too. The splash is a screenshot as well — it is gone before the walk's first
frame and `dumpsys` has no word for it.
A trap: tauri-build regenerates `gen/android/tauri.settings.gradle` and
`app/tauri.build.gradle.kts` — the lines that include every plugin's Android
module — ONLY when `TAURI_ANDROID_PROJECT_PATH` names the project, which the
@@ -1011,6 +1073,11 @@ callback returns, so `onTimeout` takes the notice down rather than being
taken down with it; the fetches are the Rust core's own and Transfers still
lists them.
The notice's glyph is the same falcon as the playback card's, diving into the
same tray bar it always had, so the two notices read as one app — a second
copy of the path on purpose, because `R.drawable.ic_soulstream_note` belongs
to the media-session module's R class and is not visible from here.
The buttons come back as the plugin event `action`
(`addPluginListener("transfer-notice", "action", …)`). **Stop all** runs
`cancelAll()` in `transfers.svelte.js`, which is the same `cancel(id)` a