android: the launcher's ground is the desktop's, not a flat colour under it

The icon round left the adaptive background as one `#FF05080B` and a note that
a gradient was being mocked. The mock settles it: the falcon covers about a
quarter of a circle-masked tile, so the ground is three quarters of what
anyone sees, and concept 04's glow survives every mask — the visible top-left
corner goes from `#05080B` to `#0E3543`. So `@color/ic_launcher_background`
becomes `@drawable/ic_launcher_ground`, a vector carrying the desktop icon's
own linear surface and the radial glow over it, every number
`src-tauri/icons/icon.svg`'s multiplied by 108/1024 and nothing else. The
colour resource stays where it is, unreferenced, because its filename is the
only thing stopping tauri's own `#fff` from colliding with it.

A vector and not five PNGs on evidence. The kit's glow has a uniform scale, so
the SVG ellipse is a circle and its rotation is a no-op, which is exactly what
`android:type="radial"` can carry; two numbers there and no vector could have
said it. The linear axis is 51.1° off horizontal, which rules out a
`<layer-list>` of shapes as well, whose angle must be a multiple of 45. It was
compiled and linked with the SDK's own aapt2 over the real merged res tree and
every stop read back out of the APK. The name is `ic_launcher_ground` and not
`ic_launcher_background` on purpose: the generated project still carries
Android Studio's template drawable of that name, so under the other name an
overlay copy that failed would silently link the green robot — under this one
it is a hard link error, which was tested too.

`verify-icon.sh` follows the layer. It does not ask whether the file is in the
APK; it asks what the adaptive icon's `<background>` resolves to, through the
resource table, in both build types, and then reads concept 04's six stops back
out of what it found — which catches a stale APK as well as a lost overlay.
Passing on a debug APK and on one put through `--shorten-resource-paths`,
failing on a stale one, and saying NOT CHECKED rather than guessing when aapt2
is absent. What none of it can see is a pixel: banding on the OLED, the glow's
weight under the Pixel's mask and how the falcon reads on a lit ground are a
launcher screenshot on the phone, and ANDROID.md says so instead of implying
the check covers it.

One slot the ground reaches is not the launcher at all. `MediaHub.appIcon()`
draws whatever `getApplicationIcon()` returns into a 512 px bitmap and hands
it to the lock-screen card as the large icon for any track whose cover has
not been decoded yet, so the card and the shade carry this ground as well —
the quickest place for Kyle to look at it, and the only reader of the
launcher icon inside the app. The walk reads that card's title and the
session's playback state and never its icon, so no step changes meaning. The
Android 12+ splash very likely changes as well, and the other way from what
the docs' one-line version of the rule suggests: the platform drops an
adaptive background only when it is a single colour close to
`windowSplashScreenBackground`, which round 44's flat `#05080B` was, so the
falcon has been drawing there alone and enlarged; a gradient is not one
colour, so it is kept and the whole icon is drawn. Likely, not proven — that
one is a screenshot too.
This commit is contained in:
KeelrLeek committed 2026-09-15 07:22:02 -05:00
1 parent afcd3cc7f3
commit da4abeb8c9
6 files changed
+314 -35

No files matched your search

+102 -19
View File
@@ -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/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/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/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/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/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 | | `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 `ic_launcher_monochrome.png` (108–432 px) from the kit's
`android/adaptive/res/`, and the legacy `ic_launcher.png` / `android/adaptive/res/`, and the legacy `ic_launcher.png` /
`ic_launcher_round.png` (48–192 px) from its `android/legacy/`; `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. 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 `drawable-v24/ic_launcher_foreground.xml`, so two of the three references
resolve — to the green robot — and only `@drawable/ic_launcher_monochrome` 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 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.** **The background is a vector drawable, not a colour and not five PNGs.**
A richer gradient is being mocked separately; when it is approved it replaces `drawable/ic_launcher_ground.xml` carries the desktop icon's own ground —
one file, and the three XML references do not move. 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 `<layer-list>` of two `<shape>`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 `src-tauri/icons/android/values/ic_launcher_background.xml` declares the same
resource name (`#fff`), and two files in one `values/` directory declaring resource name (`#fff`), and two files in one `values/` directory declaring
one colour is a hard aapt2 error — `resource 'color/ic_launcher_background' 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 ### Checking it
`verify-icon.sh` opens the APK `build.sh` has just made and reads the `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 resources back out. It checks the overlay itself first, before the APK is
is opened — a `<monochrome>` layer that a later edit drops is a failure too — opened — a `<monochrome>` layer that a later edit drops is a failure, and so is
and then reads the APK, debug and release differently, because AGP hands them a ground flattened back to one colour — and then reads the APK, debug and
to aapt2 differently: release differently, because AGP hands them to aapt2 differently:
- **debug** — the packaging default for a debuggable build type is - **debug** — the packaging default for a debuggable build type is
`--no-crunch`, so every entry *is* the file on disk. Both `--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 does not turn on), it says the icon is unchecked rather than claiming a
failure it cannot see. failure it cannot see.
And in **both**, the ground. This one is not a name or a byte but a
resolution: `<background>` is a reference, so the check asks `aapt2 dump
resources` what `mipmap/ic_launcher` is at `anydpi`, reads that file's
`<background>` 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 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 back to the old artwork. It cost about a second and a half on the 107 MB debug
debug APK. 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 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. 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, 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 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. five seconds, that splash is the falcon rather than tauri's mark. Whether
**Adaptive masking and the monochrome layer can only be judged from the the ground is under it there is the platform's call, and this round very
phone's home screen**, which is a minute with the Pixel: the icon under the likely changes the answer. `SplashscreenContentDrawer.processAdaptiveIcon`
launcher's mask, in the app drawer, in recents and in another app's share drops an adaptive icon's background and draws the foreground alone —
sheet — and then Wallpaper & style → Themed icons on, to see the monochrome enlarged, since it is then filling the slot by itself — only when that
layer take the wallpaper's colour. 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 `<background>` 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 ## Verifying
@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- The adaptive background: concept 04's ground, not a flat colour.
The desktop ships concept 04 "Deep navy glow" (src-tauri/icons/icon.svg,
byte-identical to the kit's soulstream-04-deep-navy-glow.svg) and the
phone shipped #05080B under the same falcon. The falcon covers about a
quarter of a circle-masked tile, so the ground is three quarters of what
anyone sees; the glow survives every mask, lifting the visible top-left
corner from #05080B to #0E3543; and a phone wearing a flatter icon than
the desktop is a divergence for no gain.
LAYER-FIT, not mask-fit: the 1024 artwork is mapped onto the whole 108
layer, not onto the 72 window inside it. Under a circle or a squircle the
two are indistinguishable, and this one never shows a flat edge when a
launcher parallaxes the layers apart.
Every number below is the kit's, multiplied by 108/1024 and nothing else:
surface <linearGradient x1=130 y1=50 x2=880 y2=980>
glow <radialGradient gradientTransform=
"translate(270 220) rotate(43) scale(660)">
108/1024 is 27/256, so each of those terminates inside eight decimals and
is an exact float — nothing here is a rounded coordinate.
The glow's transform has a UNIFORM scale, so the ellipse is a circle and
the rotate is a no-op — which is the whole reason a VectorDrawable can
carry this at all: android:type="radial" is centre plus radius, circular,
with no transform of its own. Had that scale been two numbers, no vector
could have expressed it and this would be five PNGs. stop-opacity folds
into the 8-bit alpha of android:color (0.18 -> 2E, 0.05 -> 0D, 0 -> 00);
the worst error that introduces, measured against the SVG's own float
composite at the layer corners, the visible corners, the circle's rim and
the centre, is one level of 255.
The rx="232" on the kit's two rects is deliberately NOT here. That is the
desktop window's corner; on the phone the launcher's mask does that job,
and an adaptive background must fill its layer.
Two full-bleed paths, surface then glow, because an adaptive background
must be opaque: the first is #FF everywhere and the second only lightens
it. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M0,0h108v108h-108z">
<aapt:attr name="android:fillColor">
<gradient
android:type="linear"
android:startX="13.7109375"
android:startY="5.2734375"
android:endX="92.8125"
android:endY="103.359375">
<item android:offset="0" android:color="#FF0A2330" />
<item android:offset="0.52" android:color="#FF07141F" />
<item android:offset="1" android:color="#FF040A10" />
</gradient>
</aapt:attr>
</path>
<path android:pathData="M0,0h108v108h-108z">
<aapt:attr name="android:fillColor">
<gradient
android:type="radial"
android:centerX="28.4765625"
android:centerY="23.203125"
android:gradientRadius="69.609375">
<item android:offset="0" android:color="#2E31C0CB" />
<item android:offset="0.58" android:color="#0D1F8FC9" />
<item android:offset="1" android:color="#0007141F" />
</gradient>
</aapt:attr>
</path>
</vector>
@@ -1,10 +1,16 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- @mipmap, not the kit's @drawable: the layers ship as PNGs under <!-- @mipmap, not the kit's @drawable: the layers ship as PNGs under
mipmap-*, and @drawable/ic_launcher_foreground still resolves to the mipmap-*, and @drawable/ic_launcher_foreground still resolves to the
template robot in the generated project. The background is one colour template robot in the generated project.
resource, so the gradient replaces one file when it is approved. -->
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. -->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" /> <background android:drawable="@drawable/ic_launcher_ground" />
<foreground android:drawable="@mipmap/ic_launcher_foreground" /> <foreground android:drawable="@mipmap/ic_launcher_foreground" />
<monochrome android:drawable="@mipmap/ic_launcher_monochrome" /> <monochrome android:drawable="@mipmap/ic_launcher_monochrome" />
</adaptive-icon> </adaptive-icon>
@@ -1,10 +1,16 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- @mipmap, not the kit's @drawable: the layers ship as PNGs under <!-- @mipmap, not the kit's @drawable: the layers ship as PNGs under
mipmap-*, and @drawable/ic_launcher_foreground still resolves to the mipmap-*, and @drawable/ic_launcher_foreground still resolves to the
template robot in the generated project. The background is one colour template robot in the generated project.
resource, so the gradient replaces one file when it is approved. -->
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. -->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" /> <background android:drawable="@drawable/ic_launcher_ground" />
<foreground android:drawable="@mipmap/ic_launcher_foreground" /> <foreground android:drawable="@mipmap/ic_launcher_foreground" />
<monochrome android:drawable="@mipmap/ic_launcher_monochrome" /> <monochrome android:drawable="@mipmap/ic_launcher_monochrome" />
</adaptive-icon> </adaptive-icon>
@@ -1,5 +1,15 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<!-- the brand's night; the whole adaptive background is this --> <!-- The brand's night. It WAS the whole adaptive background until the
ground became a gradient, and nothing points at it now:
drawable/ic_launcher_ground.xml is the background, and
values-v31/themes.xml writes this same colour as a literal on
purpose. It is kept for its FILENAME rather than for its value.
tauri's own src-tauri/icons/android/values/ic_launcher_background.xml
declares this same resource name as #fff, and two files in one
values/ directory declaring one colour is a hard aapt2 error — while
this file exists, a copy of that one overwrites it instead of
colliding with it. Deleting it would build today, since nothing reads
the colour; it would just hand that trap back for nothing saved. -->
<color name="ic_launcher_background">#FF05080B</color> <color name="ic_launcher_background">#FF05080B</color>
</resources> </resources>
+107 -9
View File
@@ -7,6 +7,17 @@
# #
# bash src-tauri/android/verify-icon.sh <apk> [--crunched] # bash src-tauri/android/verify-icon.sh <apk> [--crunched]
# #
# Since the background became a gradient there is a second way to lose, and it
# is quieter still: the adaptive icon's <background> 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
# <background> 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 # Debug and release are read DIFFERENTLY, because AGP treats their resources
# differently and both differences were measured on 2026-09-14: # 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 "<monochrome" "$RES/mipmap-anydpi-v26/$x.xml" \ grep -q "<monochrome" "$RES/mipmap-anydpi-v26/$x.xml" \
|| bad "$RES/mipmap-anydpi-v26/$x.xml declares no <monochrome> layer — themed icons need one" || bad "$RES/mipmap-anydpi-v26/$x.xml declares no <monochrome> layer — themed icons need one"
done 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 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" test -f "$RES/mipmap-$d/$n.png" || bad "$RES/mipmap-$d/$n.png is missing from the overlay"
done; done done; done
@@ -69,14 +95,90 @@ dims() {
LIST="$(unzip -l "$APK")" || bad "cannot read $APK" 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 <background> 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 <background> 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 <background> 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 <background>"
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 <gradient> 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: <background> = $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 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 # Nothing but the overlay makes a mipmap-anydpi-v26 file, so its absence
# is exactly the failure this script exists for. # is exactly the failure this script exists for.
for x in ic_launcher ic_launcher_round; do for x in ic_launcher ic_launcher_round; do
echo "$LIST" | grep -q " res/mipmap-anydpi-v26/$x\.xml$" \ 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" || bad "res/mipmap-anydpi-v26/$x.xml is not in the APK — $RES never reached src-tauri/gen/android/app/src/main/res"
done 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 for d in $DPI; do for n in $NAMES; do
got="res/mipmap-$d-v4/$n.png" got="res/mipmap-$d-v4/$n.png"
echo "$LIST" | grep -q " $got$" || bad "$got is not in the APK" echo "$LIST" | grep -q " $got$" || bad "$got is not in the APK"
@@ -87,13 +189,9 @@ if [ "$CRUNCH" = 0 ]; then
exit 0 exit 0
fi fi
# 3. release. Ask the resource table what the names point at, since the paths # 4. 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 # have very likely been shortened away.
# apksigner build.sh has just used. # $A2 and $SDKU come from section 2, which both build types now need.
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)"
if [ -z "$A2" ]; then if [ -z "$A2" ]; then
echo "== icon: NOT CHECKED — no aapt2 under ${SDKU:-(no SDK path)}/build-tools." 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" echo " A release APK's resource paths are shortened (res/Ag.png), so names"
@@ -101,7 +199,7 @@ if [ -z "$A2" ]; then
exit 0 exit 0
fi fi
# one line per file entry: "<name> <config> <path in the APK>" # one line per file entry: "<name> <config> <path in the APK>"
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[0-9a-f]+ mipmap\// { split($3, a, "/"); cur = a[2]; next }
/^[ \t]*resource 0x/ { cur = ""; next } /^[ \t]*resource 0x/ { cur = ""; next }
cur != "" { for (i = 1; i <= NF; i++) if ($i ~ /^res\//) { cfg = $1; gsub(/[()]/, "", cfg); print cur, cfg, $i } } cur != "" { for (i = 1; i <= NF; i++) if ($i ~ /^res\//) { cfg = $1; gsub(/[()]/, "", cfg); print cur, cfg, $i } }