android: the falcon on the launcher — a real adaptive icon, with the monochrome layer
The phone has been wearing tauri's logo. `tauri android init` writes the
generated project's `res/` from its own template and never reads
`src-tauri/icons/android/`, which has carried the falcon since 50afa6a, and the
detour build runs cargo and Gradle directly — so nothing ever copied it across:
the two dominant colours in the packaged `mipmap-xxxhdpi/ic_launcher.png` are
`#24c8db` and `#ffc131`, tauri's own brand cyan and amber. `src-tauri/android/res/`
is the app module's res now, tracked, and `build.sh` and `dev.sh` copy the whole
tree into `gen/android` before Gradle, because that project is regenerated and
gitignored and an asset that only lives there is an asset that disappears.
It is a real adaptive icon: the kit's foreground and its **monochrome** layer at
five densities, the legacy mipmaps under it, and the background as one
`#FF05080B` colour resource rather than five identical flat PNGs, so the gradient
being mocked separately replaces one file. The kit's own XML could not ship — it
points every layer at `@drawable/` while shipping them as PNGs under `mipmap-*`,
and in the generated project two of those three references resolve to Android
Studio's template robot rather than failing — so the overlay's XML says
`@mipmap/` and `@color/`, and its colour file takes the same name tauri's does,
because two files declaring one colour is a hard aapt2 error.
Losing the copy would be silent — the APK still builds, still installs, and wears
the old logo with nothing in the log — so `verify-icon.sh` opens every APK both
scripts make and reads the resources back out: the two `mipmap-anydpi-v26` XMLs
present, and four PNG names at five densities byte for byte from the overlay. A
release APK keeps neither the bytes nor the names — AGP re-encodes every PNG and
shortens every resource path — so that path asks `aapt2 dump resources` what each
name points at and compares pixel dimensions instead; 72×72 at hdpi still tells
the kit's icon from tauri's 49×49. No walk step: a launcher icon is not reachable
from inside the app, and the masking and the themed layer want a minute with the
phone's home screen.
No files matched your search
@@ -51,6 +51,15 @@ mkdir -p "$DEST"
|
||||
cp "$SO" "$DEST/libapp_lib.so"
|
||||
"$BIN/llvm-strip.exe" --strip-debug "$DEST/libapp_lib.so"
|
||||
ls -la "$DEST/libapp_lib.so"
|
||||
# The launcher identity, into a project that cannot keep it: gen/android is
|
||||
# regenerated by the tauri CLI and gitignored, and the detour build never runs
|
||||
# that CLI, so the res it holds is still the one `android init` wrote — tauri's
|
||||
# own logo. Everything under src-tauri/android/res/ is copied over it on every
|
||||
# build. The whole tree, not a file list: a later round adding a resource adds
|
||||
# a file there and this line does not change.
|
||||
echo "== res overlay $(date +%T)"
|
||||
test -d src-tauri/android/res || { echo "src-tauri/android/res is gone — the APK would wear tauri's logo and say nothing"; exit 1; }
|
||||
cp -r src-tauri/android/res/. src-tauri/gen/android/app/src/main/res/
|
||||
echo "== gradle $(date +%T)"
|
||||
cd src-tauri/gen/android
|
||||
./gradlew.bat assemble$VARIANT -x rustBuild$VARIANT -q 2>&1 | grep -v "^$" | tail -20 || true
|
||||
@@ -82,4 +91,5 @@ else
|
||||
APK=src-tauri/gen/android/app/build/outputs/apk/arm64/debug/app-arm64-debug.apk
|
||||
fi
|
||||
ls -la "$APK"
|
||||
bash src-tauri/android/verify-icon.sh "$APK" $([ "$REL" = "1" ] && echo --crunched)
|
||||
echo "== done $(date +%T)"
|
||||
@@ -33,11 +33,21 @@ DEST=src-tauri/gen/android/app/src/main/jniLibs/arm64-v8a
|
||||
mkdir -p "$DEST"
|
||||
cp "$SO" "$DEST/libapp_lib.so"
|
||||
"$BIN/llvm-strip.exe" --strip-debug "$DEST/libapp_lib.so"
|
||||
# The launcher identity, into a project that cannot keep it: gen/android is
|
||||
# regenerated by the tauri CLI and gitignored, and the detour build never runs
|
||||
# that CLI, so the res it holds is still the one `android init` wrote — tauri's
|
||||
# own logo. Everything under src-tauri/android/res/ is copied over it on every
|
||||
# build. The whole tree, not a file list: a later round adding a resource adds
|
||||
# a file there and this line does not change.
|
||||
echo "== res overlay $(date +%T)"
|
||||
test -d src-tauri/android/res || { echo "src-tauri/android/res is gone — the APK would wear tauri's logo and say nothing"; exit 1; }
|
||||
cp -r src-tauri/android/res/. src-tauri/gen/android/app/src/main/res/
|
||||
echo "== gradle $(date +%T)"
|
||||
cd src-tauri/gen/android
|
||||
./gradlew.bat assembleArm64Debug -x rustBuildArm64Debug -q 2>&1 | grep -v "^$" | tail -20 || true
|
||||
cd /f/Coding/SoulStream
|
||||
APK=src-tauri/gen/android/app/build/outputs/apk/arm64/debug/app-arm64-debug.apk
|
||||
bash src-tauri/android/verify-icon.sh "$APK"
|
||||
echo "== install $(date +%T)"
|
||||
"$ADB" install -r "$APK" 2>&1 | tail -1
|
||||
"$ADB" shell pm grant com.soulstream.app android.permission.POST_NOTIFICATIONS 2>/dev/null || true
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- @mipmap, not the kit's @drawable: the layers ship as PNGs under
|
||||
mipmap-*, and @drawable/ic_launcher_foreground still resolves to the
|
||||
template robot in the generated project. The background is one colour
|
||||
resource, so the gradient replaces one file when it is approved. -->
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background" />
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@mipmap/ic_launcher_monochrome" />
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- @mipmap, not the kit's @drawable: the layers ship as PNGs under
|
||||
mipmap-*, and @drawable/ic_launcher_foreground still resolves to the
|
||||
template robot in the generated project. The background is one colour
|
||||
resource, so the gradient replaces one file when it is approved. -->
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background" />
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@mipmap/ic_launcher_monochrome" />
|
||||
</adaptive-icon>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- the brand's night; the whole adaptive background is this -->
|
||||
<color name="ic_launcher_background">#FF05080B</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env bash
|
||||
# Is the falcon actually in the APK? The way this loses is silent: gen/android
|
||||
# is regenerated and gitignored, so a project rebuilt without the overlay copy
|
||||
# still compiles, still installs, and wears tauri's logo again with nothing in
|
||||
# the log. So every APK this repo makes is opened and read before it is
|
||||
# installed. Called by build.sh and dev.sh; run it by hand on any APK.
|
||||
#
|
||||
# bash src-tauri/android/verify-icon.sh <apk> [--crunched]
|
||||
#
|
||||
# Debug and release are read DIFFERENTLY, because AGP treats their resources
|
||||
# differently and both differences were measured on 2026-09-14:
|
||||
#
|
||||
# debug packaged with --no-crunch (AGP's default for the debuggable build
|
||||
# type), so every entry is byte for byte the file on disk — checked
|
||||
# against the real 107 MB APK. Names are names: res/mipmap-hdpi-v4/…
|
||||
# release --crunched. Two things change. aapt2 re-encodes every PNG, so the
|
||||
# bytes are no longer the file; and AGP's resource optimization
|
||||
# (OptimizeResourcesTask → SHORTEN_RESOURCE_PATHS) renames the lot
|
||||
# to res/Ag.png, so the names are gone as well — an APK optimized
|
||||
# here had zero entries matching "res/mipmap". Nothing but the
|
||||
# resource table can say what is what, so that is what is read:
|
||||
# `aapt2 dump resources`, then the pixel dimensions of whatever it
|
||||
# points at. 72x72 at hdpi still tells the kit's legacy icon from
|
||||
# tauri's 49x49.
|
||||
#
|
||||
# If anyone ever sets isCrunchPngs = true on the debug build type, the debug
|
||||
# path fails on byte-equality with the file named; --crunched is the answer.
|
||||
set -uo pipefail
|
||||
APK="${1:-}"
|
||||
test -n "$APK" || { echo "usage: verify-icon.sh <apk> [--crunched]"; exit 2; }
|
||||
# resolved before the cd, so a relative path typed by hand still means what it
|
||||
# said; the repo root is where $RES is
|
||||
[ -f "$APK" ] && APK="$(cd "$(dirname "$APK")" && pwd)/$(basename "$APK")"
|
||||
# the repo this script is IN, not a drive letter. build.sh and dev.sh have
|
||||
# already cd'd to /f/Coding/SoulStream and call this as
|
||||
# src-tauri/android/verify-icon.sh, so ../.. lands on the same place they are
|
||||
# — but a checkout on another letter, or an APK checked by hand from
|
||||
# somewhere else, then compares against ITS OWN overlay rather than F:'s.
|
||||
cd "$(dirname "$0")/../.." || { echo "cannot find the repo above $0"; exit 2; }
|
||||
CRUNCH=0; [ "${2:-}" = "--crunched" ] && CRUNCH=1
|
||||
RES=src-tauri/android/res
|
||||
DPI="mdpi hdpi xhdpi xxhdpi xxxhdpi"
|
||||
NAMES="ic_launcher ic_launcher_round ic_launcher_foreground ic_launcher_monochrome"
|
||||
bad() { echo "ICON CHECK FAILED: $*"; exit 1; }
|
||||
test -f "$APK" || bad "no APK at $APK"
|
||||
test -d "$RES" || bad "$RES is missing — there is nothing to check the APK against"
|
||||
|
||||
# 1. the overlay itself, before the APK is opened. The monochrome layer is the
|
||||
# one Android 13 tints for themed icons and the one tauri's generator has
|
||||
# never made, so a later edit that drops it should not pass quietly.
|
||||
for x in ic_launcher ic_launcher_round; do
|
||||
test -f "$RES/mipmap-anydpi-v26/$x.xml" || bad "$RES/mipmap-anydpi-v26/$x.xml is missing from the overlay"
|
||||
grep -q "<monochrome" "$RES/mipmap-anydpi-v26/$x.xml" \
|
||||
|| bad "$RES/mipmap-anydpi-v26/$x.xml declares no <monochrome> layer — themed icons need one"
|
||||
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
|
||||
|
||||
# the PNG header's width and height, eight bytes at offset 16 — the one thing
|
||||
# about an image that survives being re-encoded. Printed as 72x72, so a failure
|
||||
# reads as a size rather than as hex.
|
||||
dims() {
|
||||
local h
|
||||
h="$(xxd -s 16 -l 8 -p)"
|
||||
[ "${#h}" = 16 ] || { echo "unreadable"; return; }
|
||||
printf '%dx%d\n' "$((16#${h:0:8}))" "$((16#${h:8:8}))"
|
||||
}
|
||||
|
||||
LIST="$(unzip -l "$APK")" || bad "cannot read $APK"
|
||||
|
||||
if [ "$CRUNCH" = 0 ]; then
|
||||
# 2. 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
|
||||
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"
|
||||
unzip -p "$APK" "$got" | cmp -s - "$RES/mipmap-$d/$n.png" \
|
||||
|| bad "$got in the APK is not $RES/mipmap-$d/$n.png — a stale res in gen/, the overlay lost a file, or this build crunched its PNGs (then: --crunched)"
|
||||
done; done
|
||||
echo "== icon: adaptive + monochrome, five densities, byte for byte from $RES"
|
||||
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)"
|
||||
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"
|
||||
echo " alone cannot find the icon. Unverified here, not missing."
|
||||
exit 0
|
||||
fi
|
||||
# one line per file entry: "<name> <config> <path in the APK>"
|
||||
MAP="$("$A2" dump resources "$APK" | 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 } }
|
||||
')" || bad "aapt2 could not read the resource table of $APK"
|
||||
# the other half of the same optimization pass: with resource shrinking on,
|
||||
# aapt2 also collapses the NAMES, and then even the table cannot say which
|
||||
# entry is the launcher icon
|
||||
if echo "$MAP" | grep -q "0_resource_name_obfuscated"; then
|
||||
echo "== icon: NOT CHECKED — this APK's resource names are collapsed"
|
||||
echo " (aapt2 optimize --collapse-resource-names, which comes with resource"
|
||||
echo " shrinking). Nothing left to match on. Unverified here, not missing."
|
||||
exit 0
|
||||
fi
|
||||
# the config is printed as "mdpi" or as "mdpi-v4" depending on the link's
|
||||
# min-sdk — both seen on this machine from the same resource tree, so both are
|
||||
# taken, and "anydpi" matches the "anydpi-v26" the adaptive icon lives in
|
||||
look() { echo "$MAP" | awk -v n="$1" -v c="$2" '$1 == n && ($2 == c || $2 ~ "^" c "-v[0-9]+$") { print $3; exit }'; }
|
||||
for x in ic_launcher ic_launcher_round; do
|
||||
got="$(look "$x" anydpi)"
|
||||
test -n "$got" || bad "mipmap/$x 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"
|
||||
done
|
||||
for d in $DPI; do for n in $NAMES; do
|
||||
got="$(look "$n" "$d")"
|
||||
test -n "$got" || bad "mipmap/$n has no $d entry in this APK"
|
||||
a="$(unzip -p "$APK" "$got" | dims)"
|
||||
b="$(dims < "$RES/mipmap-$d/$n.png")"
|
||||
[ "$a" = "$b" ] || bad "mipmap/$n at $d ($got) is $a, not the overlay's $b"
|
||||
done; done
|
||||
echo "== icon: adaptive + monochrome, five densities, by resource table ($(basename "$(dirname "$A2")")); release re-encodes the bytes, so dimensions only"
|
||||