diff --git a/ANDROID.md b/ANDROID.md index 172fc67..2ebd768 100644 --- a/ANDROID.md +++ b/ANDROID.md @@ -30,7 +30,7 @@ implements is `research/mobile-mockup/` (gitignored; see its README). | `src-tauri/tauri.android.conf.json` | merged over `tauri.conf.json` on Android: one window at `mobile.html`, no desktop resources, `minSdkVersion` 26 | | `src-tauri/gen/android/` | the generated Gradle project (`npx tauri android init`); `minSdk = 26`. Gitignored | | `src-tauri/android/build.sh`, `deploy.sh` | the detour build (below) and build → install → launch → screenshot | -| `src-tauri/android/res/` | the app module's own Android resources, tracked: the adaptive launcher icon, its monochrome layer, the legacy mipmaps and the one colour the background is. `build.sh` and `dev.sh` copy the whole tree into `gen/android/app/src/main/res/` before Gradle; `verify-icon.sh` reads it back out of the APK | +| `src-tauri/android/res/` | the app module's own Android resources, tracked: the adaptive launcher icon, its monochrome layer, the legacy mipmaps and the gradient drawable the background is. `build.sh` and `dev.sh` copy the whole tree into `gen/android/app/src/main/res/` before Gradle; `verify-icon.sh` reads it back out of the APK | | `src-tauri/src/android_glue.rs` | `JNI_OnLoad`: hands the JavaVM and app Context to `ndk-context` (cpal's AAudio backend needs it; tao 0.35 never fills it) | | `src-tauri/src/updater_mobile.rs` | the updater's stand-in on mobile (`#[cfg(mobile)] #[path]`) | | `src-tauri/src/lyric_align_stub.rs`, `vocal_sep_stub.rs` | stand-ins off Windows for the two Windows ML modules | @@ -1613,7 +1613,10 @@ It carries: `ic_launcher_monochrome.png` (108–432 px) from the kit's `android/adaptive/res/`, and the legacy `ic_launcher.png` / `ic_launcher_round.png` (48–192 px) from its `android/legacy/`; -- `values/ic_launcher_background.xml` — `#FF05080B`, the brand's night. +- `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). Three things about it are not obvious. @@ -1626,13 +1629,45 @@ the generated project still carries Android Studio's template vectors at `drawable-v24/ic_launcher_foreground.xml`, so two of the three references resolve — to the green robot — and only `@drawable/ic_launcher_monochrome` fails the build. The overlay's XML says `@mipmap/` for the two image layers -and `@color/` for the background instead. +and a `@drawable/` of our own 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. +**The background is a vector drawable, not a colour and not five PNGs.** +`drawable/ic_launcher_ground.xml` carries the desktop icon's own ground — +concept 04's navy surface and the cyan glow off its top-left — mapped onto the +whole 108-unit layer, every number the kit's `src-tauri/icons/icon.svg` +multiplied by 108/1024. The flat `#05080B` was three quarters of what a masked +tile shows (the falcon covers about a quarter of it), and the glow survives +every mask: the visible top-left corner goes from `#05080B` to `#0E3543`. The +`rx="232"` on the kit's rects is not carried over; on the phone the launcher's +mask does that job. -**The colour file is named `ic_launcher_background.xml` on purpose.** +A vector rather than a PNG set because this artwork happens to fit one. +Android's `android:type="radial"` is a circle — centre and radius, no +transform of its own — and the kit's glow is +`translate(270 220) rotate(43) scale(660)`, a **uniform** scale, so the +ellipse *is* a circle and the rotation is a no-op. Had that scale been two +numbers, no vector could have said it and this would be five rendered PNGs. +The other cheap route, a `` of two ``s, is out on the +linear axis alone: the concept's is 51.1° off horizontal, and a shape +drawable's `android:angle` "must be a multiple of 45", with three evenly +spaced stops at most. Folding `stop-opacity` into 8-bit alpha (0.18 → `2E`, +0.05 → `0D`) costs at most one level of 255 against the SVG's float composite. +And a vector reaches sizes no PNG here does: the media card draws this same +drawable at 512 px (below) and the Android 12+ splash slot is 240 dp, about +630 px on the Pixel, both past the 432 px an xxxhdpi PNG tops out at. + +What a vector cannot do is **dither**. Both forms are 8-bit and quantise +identically — 122 colour steps down the diagonal at 432 px, longest flat run +43 px, over a total range of R 12 / G 49 / B 55 levels — so the form does not +cause banding, but only a PNG could have been dithered out of it. If the +phone's OLED contours, the answer is a dithered five-density set behind this +same `@drawable/ic_launcher_ground` name: a file per density, and no XML +moves. + +**The colour file stays, and its filename is the whole reason.** Nothing +points at `values/ic_launcher_background.xml` since the ground became a +drawable — `values-v31/themes.xml` writes the splash colour as a literal on +purpose — and deleting it would build. It is kept because `src-tauri/icons/android/values/ic_launcher_background.xml` declares the same resource name (`#fff`), and two files in one `values/` directory declaring one colour is a hard aapt2 error — `resource 'color/ic_launcher_background' @@ -1648,10 +1683,10 @@ it, and it should be brought to the kit before anyone builds through the CLI. ### Checking it `verify-icon.sh` opens the APK `build.sh` has just made and reads the -resources back out. It checks the overlay's own two XMLs first, before the APK -is opened — a `` 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: +resources back out. It checks the overlay itself first, before the APK is +opened — a `` layer that a later edit drops is a failure, and so is +a ground flattened back to one colour — and then reads the APK, debug and +release differently, because AGP hands them to aapt2 differently: - **debug** — the packaging default for a debuggable build type is `--no-crunch`, so every entry *is* the file on disk. Both @@ -1668,20 +1703,68 @@ to aapt2 differently: does not turn on), it says the icon is unchecked rather than claiming a failure it cannot see. +And in **both**, the ground. This one is not a name or a byte but a +resolution: `` is a reference, so the check asks `aapt2 dump +resources` what `mipmap/ic_launcher` is at `anydpi`, reads that file's +`` id, maps the id back through the table, and requires +`drawable/ic_launcher_ground`; then it follows the two `fillColor` references +the `aapt:attr` blocks became and requires concept 04's six stops in them. +That is the check that catches a **stale APK** as well as a lost overlay — +`build.sh` pipes Gradle through `| tail -20 || true` and then runs +`ls -la "$APK"`, so a Gradle run that failed leaves the previous build's file +exactly where the next step looks for it, with every PNG in it still the +falcon's. Without aapt2 on the machine this half says `NOT CHECKED` and the +rest of the script still runs. + 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. +back to the old artwork. It cost about a second and a half on the 107 MB debug +APK before the ground was added; the ground is one `dump resources` and three +`dump xmltree` calls on top of that, measured at 0.3 s against that same real +APK — aapt2 reads `resources.arsc` and the entries it names, never the whole +file, so the cost does not grow with the APK. The release branch reuses the +same table rather than asking aapt2 for it twice. What it cannot check is what the icon looks like. `deploy.sh`'s `px-1.png` is a screenshot of the app five seconds after launch; the launcher is not in it. The one thing that may now differ there is the Android 12+ system splash, which is drawn from the launcher icon — if a cold start ever runs past those -five seconds, that splash is the falcon on `#05080B` rather than tauri's mark. -**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. +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. +**Everything about how it looks can only be judged from the phone**, and the +gradient ground adds to that list rather than shortening it. +`verify-icon.sh` proves the adaptive icon's `` resolves to a +drawable carrying concept 04's six stops and its geometry; it cannot see a +pixel. So: the home screen under the launcher's mask, the app +drawer, recents, another app's share sheet — reading for **banding** on the +OLED above all, since that is the one thing the vector form cannot dither its +way out of, then the glow's weight under the Pixel's circle and how the falcon +sits on a lit ground rather than a flat one — and then Wallpaper & style → +Themed icons on, which swaps both layers for the monochrome one over a +background the wallpaper picks: the one slot this ground never reaches, on +the list so that a round about the background can be seen not to have moved +it. + +**One consequence of this round is reachable without the launcher**, and it +is the quickest of them to check. `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 its large icon, for every track whose +cover has not been decoded yet — so the card and the shade carry this ground +now as well. Play a track with no cover and look at the card. Nothing else in +the app reads the launcher icon, and the walk never reads the card's icon — +only its `android.title` and the session's playback state — so no step +changes meaning either way. ## Verifying diff --git a/src-tauri/android/res/drawable/ic_launcher_ground.xml b/src-tauri/android/res/drawable/ic_launcher_ground.xml new file mode 100644 index 0000000..c7499c6 --- /dev/null +++ b/src-tauri/android/res/drawable/ic_launcher_ground.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src-tauri/android/res/mipmap-anydpi-v26/ic_launcher.xml b/src-tauri/android/res/mipmap-anydpi-v26/ic_launcher.xml index ddfcdab..76ce7be 100644 --- a/src-tauri/android/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/src-tauri/android/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,10 +1,16 @@ + template robot in the generated project. + + The background is a drawable of our own — concept 04's gradient ground, + drawable/ic_launcher_ground.xml — and the name is chosen so that it + CANNOT collide with the template's drawable/ic_launcher_background.xml, + which is still sitting in gen/android as the green robot. If the overlay + ever fails to reach the generated project, this reference does not + resolve and aapt2 stops the build, instead of quietly drawing that. --> - + diff --git a/src-tauri/android/res/mipmap-anydpi-v26/ic_launcher_round.xml b/src-tauri/android/res/mipmap-anydpi-v26/ic_launcher_round.xml index ddfcdab..76ce7be 100644 --- a/src-tauri/android/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/src-tauri/android/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,10 +1,16 @@ + template robot in the generated project. + + The background is a drawable of our own — concept 04's gradient ground, + drawable/ic_launcher_ground.xml — and the name is chosen so that it + CANNOT collide with the template's drawable/ic_launcher_background.xml, + which is still sitting in gen/android as the green robot. If the overlay + ever fails to reach the generated project, this reference does not + resolve and aapt2 stops the build, instead of quietly drawing that. --> - + diff --git a/src-tauri/android/res/values/ic_launcher_background.xml b/src-tauri/android/res/values/ic_launcher_background.xml index 60d681b..72a28bb 100644 --- a/src-tauri/android/res/values/ic_launcher_background.xml +++ b/src-tauri/android/res/values/ic_launcher_background.xml @@ -1,5 +1,15 @@ - + #FF05080B diff --git a/src-tauri/android/verify-icon.sh b/src-tauri/android/verify-icon.sh index 914dded..19ca06d 100644 --- a/src-tauri/android/verify-icon.sh +++ b/src-tauri/android/verify-icon.sh @@ -7,6 +7,17 @@ # # bash src-tauri/android/verify-icon.sh [--crunched] # +# Since the background became a gradient there is a second way to lose, and it +# is quieter still: the adaptive icon's is a reference, and a +# reference resolves to whatever the APK happens to hold. A stale APK, or an +# overlay copy that did not run, and the name points at something else while +# every PNG in the file is still the falcon's. So the check does not ask +# whether drawable/ic_launcher_ground.xml is in the APK — it asks what +# RESOLVES to, through the resource table, in both build types, +# and then reads concept 04's six gradient stops back out of the thing it +# found. That half needs aapt2; without it the ground says NOT CHECKED and the +# rest of the script still runs. +# # Debug and release are read DIFFERENTLY, because AGP treats their resources # differently and both differences were measured on 2026-09-14: # @@ -53,6 +64,21 @@ for x in ic_launcher ic_launcher_round; do grep -q " layer — themed icons need one" done +# The ground is the other half of the icon and three quarters of what a +# masked tile shows, so a later edit that flattens it back to one colour +# should not pass quietly either. +GROUND="$RES/drawable/ic_launcher_ground.xml" +test -f "$GROUND" || bad "$GROUND is missing from the overlay — the adaptive background has nowhere to come from" +grep -q 'android:type="linear"' "$GROUND" || bad "$GROUND declares no linear gradient — concept 04's surface" +grep -q 'android:type="radial"' "$GROUND" || bad "$GROUND declares no radial gradient — concept 04's glow" +# quote-anchored, so the comment above them cannot satisfy the check +for c in FF0A2330 FF07141F FF040A10 2E31C0CB 0D1F8FC9 0007141F; do + grep -qi "\"#$c\"" "$GROUND" || bad "$GROUND has lost concept 04's stop #$c — it is no longer the desktop's ground" +done +for x in ic_launcher ic_launcher_round; do + grep -q '@drawable/ic_launcher_ground' "$RES/mipmap-anydpi-v26/$x.xml" \ + || bad "$RES/mipmap-anydpi-v26/$x.xml does not point its background at @drawable/ic_launcher_ground" +done 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 @@ -69,14 +95,90 @@ dims() { LIST="$(unzip -l "$APK")" || bad "cannot read $APK" +# aapt2 is beside the zipalign and apksigner build.sh has just used. Found +# here rather than in the release branch below, because both build types want +# it now: only the resource table can say what resolves to. +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)" + +# 2. the ground, in BOTH build types. Not "is the file in the APK" — the +# adaptive icon's is a REFERENCE, so what matters is what it +# resolves to, and that is the one question a stale APK answers wrongly +# while every other check still passes. (build.sh pipes Gradle through +# `| tail -20 || true` and then runs `ls -la "$APK"`, so a Gradle run that +# failed leaves the PREVIOUS build's APK exactly where the next step looks +# for it.) Everything below comes out of the resource table and never out +# of a path: a release APK has had res/mipmap-anydpi-v26/ic_launcher.xml +# renamed to res/BW.xml, and the table is the only thing that still knows. +TBL="" +[ -n "$A2" ] && { TBL="$("$A2" dump resources "$APK" 2>/dev/null)" || bad "aapt2 could not read the resource table of $APK"; } +# name [config] -> the file that resource points at. The config is not +# optional detail: mipmap/ic_launcher has SIX entries — the anydpi adaptive +# XML and five legacy PNGs — so a name-only lookup hands back +# res/mipmap-mdpi-v4/ic_launcher.png and the whole check collapses. An empty +# config means the entry with no qualifier at all, which is what a plain +# drawable has. "anydpi" matches the "anydpi-v26" too, as look() does. +path_of() { echo "$TBL" | awk -v n="$1" -v c="${2:-}" ' + $1 == "resource" && $3 == n { f = 1; next } + /^[ \t]*resource 0x/ { f = 0 } + f { cfg = $1; gsub(/[()]/, "", cfg) + if (c == "" || cfg == c || cfg ~ "^" c "-v[0-9]+$") + for (i = 1; i <= NF; i++) if ($i ~ /^res\//) { print $i; exit } }'; } +name_of() { echo "$TBL" | awk -v id="$1" '$1 == "resource" && $2 == id { print $3; exit }'; } +if [ -z "$TBL" ]; then + echo "== ground: NOT CHECKED — no aapt2 under ${SDKU:-(no SDK path)}/build-tools." + echo " What points at is a resource id; only the table reads it." +elif echo "$TBL" | grep -q "0_resource_name_obfuscated"; then + echo "== ground: NOT CHECKED — this APK's resource names are collapsed." +else + ADAPT="$(path_of mipmap/ic_launcher anydpi)" + test -n "$ADAPT" || bad "mipmap/ic_launcher has no anydpi entry in this APK — the adaptive icon is not in it, so $RES never reached src-tauri/gen/android/app/src/main/res" + BGID="$("$A2" dump xmltree "$APK" --file "$ADAPT" 2>/dev/null \ + | awk '/E: background/ { f = 1 } f && /android:drawable\(/ { sub(/.*=@?/, ""); print; exit }')" + test -n "$BGID" || bad "the adaptive icon in this APK ($ADAPT) declares no " + BGNAME="$(name_of "$BGID")" + [ "$BGNAME" = "drawable/ic_launcher_ground" ] \ + || bad "the APK's adaptive background is ${BGNAME:-$BGID}, not drawable/ic_launcher_ground — a stale APK, or the overlay did not reach src-tauri/gen/android/app/src/main/res" + # and that what it resolves to is still concept 04 rather than a flat + # fill. aapt:attr lifts each inline out into a resource of its + # own, so the two fills are references as well; follow the ids rather than + # the $name__N filenames aapt2 happens to give them this version. + GPATH="$(path_of drawable/ic_launcher_ground)" + test -n "$GPATH" || bad "drawable/ic_launcher_ground resolves to no file in this APK" + GTREE="$("$A2" dump xmltree "$APK" --file "$GPATH" 2>/dev/null)" || bad "aapt2 cannot read $GPATH out of this APK" + IDS="$(echo "$GTREE" | grep -oE 'fillColor\(0x[0-9a-f]+\)=@0x[0-9a-f]+' | grep -oE '0x[0-9a-f]+$')" + NFILL="$(echo "$IDS" | grep -c . || true)" + [ "$NFILL" = 2 ] || bad "the ground in this APK has $NFILL gradient fills, not 2 — concept 04 is a surface and the glow over it" + STOPS="" + for id in $IDS; do + g="$(path_of "$(name_of "$id")")" + test -n "$g" || bad "the ground's gradient $id has no file in the resource table" + STOPS="$STOPS $("$A2" dump xmltree "$APK" --file "$g" | grep -oE 'color\(0x[0-9a-f]+\)=#[0-9a-f]{8}' | grep -oE '[0-9a-f]{8}$' | tr '\n' ' ')" + done + for c in ff0a2330 ff07141f ff040a10 2e31c0cb 0d1f8fc9 0007141f; do + echo "$STOPS" | grep -qw "$c" || bad "the ground in this APK has lost concept 04's stop #$c" + done + echo "== ground: = $BGNAME, concept 04's six stops, read back out of the APK" +fi +# What none of this can see is a pixel. Whether the gradient BANDS on the +# phone's OLED, how the glow reads under the Pixel's mask and how the falcon +# sits on a lit ground rather than a flat one are a launcher screenshot, not a +# resource table. + if [ "$CRUNCH" = 0 ]; then - # 2. debug: the adaptive XMLs by name, then all twenty PNGs byte for byte. + # 3. debug: the adaptive XMLs by name, then all twenty PNGs byte for byte. # Nothing but the overlay makes a mipmap-anydpi-v26 file, so its absence # is exactly the failure this script exists for. 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 + # and the ground by name as well. In release the paths are shortened + # away and only the table above can find it; here it is still a file. + echo "$LIST" | grep -q " res/drawable/ic_launcher_ground\.xml$" \ + || bad "res/drawable/ic_launcher_ground.xml is not in the APK — the adaptive background never reached src-tauri/gen/android/app/src/main/res" for d in $DPI; do for n in $NAMES; do got="res/mipmap-$d-v4/$n.png" echo "$LIST" | grep -q " $got$" || bad "$got is not in the APK" @@ -87,13 +189,9 @@ if [ "$CRUNCH" = 0 ]; then exit 0 fi -# 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. -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)" +# 4. release. Ask the resource table what the names point at, since the paths +# have very likely been shortened away. +# $A2 and $SDKU come from section 2, which both build types now need. if [ -z "$A2" ]; then echo "== icon: NOT CHECKED — no aapt2 under ${SDKU:-(no SDK path)}/build-tools." echo " A release APK's resource paths are shortened (res/Ag.png), so names" @@ -101,7 +199,7 @@ if [ -z "$A2" ]; then exit 0 fi # one line per file entry: " " -MAP="$("$A2" dump resources "$APK" | awk ' +MAP="$(echo "$TBL" | awk ' /^[ \t]*resource 0x[0-9a-f]+ mipmap\// { split($3, a, "/"); cur = a[2]; next } /^[ \t]*resource 0x/ { cur = ""; next } cur != "" { for (i = 1; i <= NF; i++) if ($i ~ /^res\//) { cfg = $1; gsub(/[()]/, "", cfg); print cur, cfg, $i } }