From d42645d2a0f452fa3a2d7dc8f293503abf998363 Mon Sep 17 00:00:00 2001 From: KeelrLeek <156356974+KeelrLeek@users.noreply.github.com> Date: Tue, 15 Sep 2026 07:22:01 -0500 Subject: [PATCH] =?UTF-8?q?android:=20the=20icon=20where=20it=20moves=20?= =?UTF-8?q?=E2=80=94=20the=20glyph,=20the=20lock=20screen=20and=20a=20spla?= =?UTF-8?q?sh=20that=20is=20not=20white?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- ANDROID.md | 67 +++++++++++++++++++ .../android/src/main/AndroidManifest.xml | 6 +- .../soulstream/mediasession/MediaService.kt | 47 ++++++++++++- .../main/res/drawable/ic_soulstream_note.xml | 28 ++++++-- .../main/res/drawable/ic_soulstream_down.xml | 26 +++++-- .../android/res/values-night-v31/themes.xml | 15 +++++ src-tauri/android/res/values-v31/themes.xml | 43 ++++++++++++ tests/mobile/smoke.mjs | 55 +++++++++++++-- 8 files changed, 270 insertions(+), 17 deletions(-) create mode 100644 src-tauri/android/res/values-night-v31/themes.xml create mode 100644 src-tauri/android/res/values-v31/themes.xml diff --git a/ANDROID.md b/ANDROID.md index 882427e..d3dc814 100644 --- a/ANDROID.md +++ b/ANDROID.md @@ -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 `` 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 diff --git a/crates/tauri-plugin-media-session/android/src/main/AndroidManifest.xml b/crates/tauri-plugin-media-session/android/src/main/AndroidManifest.xml index c38a29c..1d45384 100644 --- a/crates/tauri-plugin-media-session/android/src/main/AndroidManifest.xml +++ b/crates/tauri-plugin-media-session/android/src/main/AndroidManifest.xml @@ -7,7 +7,11 @@ - + + - + - + + + diff --git a/crates/tauri-plugin-transfer-notice/android/src/main/res/drawable/ic_soulstream_down.xml b/crates/tauri-plugin-transfer-notice/android/src/main/res/drawable/ic_soulstream_down.xml index 6f5681f..a882589 100644 --- a/crates/tauri-plugin-transfer-notice/android/src/main/res/drawable/ic_soulstream_down.xml +++ b/crates/tauri-plugin-transfer-notice/android/src/main/res/drawable/ic_soulstream_down.xml @@ -1,15 +1,29 @@ - + - + + + diff --git a/src-tauri/android/res/values-night-v31/themes.xml b/src-tauri/android/res/values-night-v31/themes.xml new file mode 100644 index 0000000..71e29c2 --- /dev/null +++ b/src-tauri/android/res/values-night-v31/themes.xml @@ -0,0 +1,15 @@ + + + + + +