Files
DarkRoom/docker/android/package.sh
T
dtourolleandClaude Opus 5 d49b4b41de Add thumbnail size classes and grid zoom; fix the scrub ordinal
The grid now zooms, which needs thumbnails at two resolutions rather than
one, and exposed a scrub that landed in the wrong place.

**Two thumbnail size classes.** `ThumbSize::Grid` (256px, ~10 KB) and
`Large` (1024px, ~45 KB), with the class part of the store key so both
coexist. Storing everything large would take the reference library from
~200 MB to ~860 MB, and shards sync, so that is transfer cost on every
device rather than only disk. A store written before the class existed
migrates in place: its entries are all grid-sized, which is what the
column defaults to, so nothing already fetched is discarded.

`forget` now drops every size for an image. Reading a single row left the
other class's bytes on the shard's tally for good, sealing it early on
space nothing occupied.

**Grid zoom.** Ctrl+wheel and pinch resize cells between 90px and 420px in
geometric steps, so the gesture feels the same at either end where a fixed
pixel step would be imperceptible at 400px and violent at 90px. Crossing
256px switches to the large class, so a zoomed cell is sharp rather than
upscaled. Columns and window capacity already derived from cell size, so
the grid reflows for free.

**The scrub landed about half a library too high.** It counted only dated
images while the grid shows all of them — 10,733 dated against 19,841
rows — and ignored `shadowed_by`. Verified against the live catalog: the
old formula gave 10,887, the new one 10,732, the true grid position
10,732. The scrub's count and the grid's window must use identical
predicates and ordering; a test now fails if they diverge.

**Timeline gestures are continuous.** Scrub and pan were quantised to
whole buckets, so a slow drag did nothing until it crossed a boundary and
then jumped a month. Both work in fractions of the visible span now, and
pinch-to-zoom arrives for tablet, where there is no wheel to reach the
axis with.

The pinch accumulator was wrong on first writing: it took at most one step
per update, so an 8x spread — three doublings — yielded one zoom level.
`log2().trunc()` now extracts every whole doubling and carries the
remainder. The original test asserted the wrong number and defended it in
a comment, which is worth remembering: a test can entrench a bug as
readily as catch one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 21:58:32 +02:00

131 lines
5.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# Build DarkRoom into an installable APK, without Gradle.
#
# ./docker/android/package.sh # build + package, debug-signed
# ./docker/android/package.sh --install # ...and adb install to a device
#
# Gradle would add a second build system, a second dependency tree, and a
# second place for the toolchain versions to drift out of step with the
# Dockerfile. The four tools it would have driven — aapt2, d8, zipalign,
# apksigner — are in build-tools already and are enough on their own, because
# the app has no Java of its own: android-activity's glue calls android_main
# directly, and the only classes in the APK are the ones Slint's build script
# compiles for its own helper.
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO="$(cd "${HERE}/../.." && pwd)"
ABI="arm64-v8a"
RUST_TARGET="aarch64-linux-android"
PKG_DIR="${REPO}/apps/darkroom-android/android"
# The container writes here (see build.sh); the APK is assembled in the same
# place so both halves of the build agree on one output directory.
CACHE="${XDG_CACHE_HOME:-${HOME}/.cache}/darkroom-android"
# Under the cache's target/ rather than beside it: the container writes to
# /work/target-android/apk, and that is the directory bind-mounted here.
OUT="${CACHE}/target/apk"
APK="${OUT}/darkroom.apk"
INSTALL=0
[[ "${1:-}" == "--install" ]] && INSTALL=1
# ---------------------------------------------------------------------------
# 1. Cross-compile the shared library.
# ---------------------------------------------------------------------------
echo "==> building libdarkroom.so (${ABI})"
"${HERE}/build.sh" cargo ndk -t "${ABI}" -o /work/target-android/jniLibs \
build --release -p darkroom-android
SO="${CACHE}/target/jniLibs/${ABI}/libdarkroom.so"
[[ -f "${SO}" ]] || { echo "error: ${SO} not built" >&2; exit 1; }
# ---------------------------------------------------------------------------
# 2. Assemble the APK inside the container, where the SDK lives.
#
# Everything below runs in one container invocation: aapt2 link produces a base
# APK with the manifest, then the .so and Slint's dex are added as stored
# entries, then zipalign and apksigner finish it. The .so is stored rather than
# deflated so Android can mmap it directly (extractNativeLibs=false territory);
# for a 37 MB library that also keeps install times sane.
# ---------------------------------------------------------------------------
echo "==> packaging APK"
"${HERE}/build.sh" bash -euo pipefail -c '
SDK=/opt/android-sdk
BT="${SDK}/build-tools/36.0.0"
ABI="'"${ABI}"'"
RT="'"${RUST_TARGET}"'"
OUT=/work/target-android/apk
rm -rf "${OUT}" && mkdir -p "${OUT}/staging/lib/${ABI}"
# Slint compiles a Java helper (SlintAndroidJavaHelper) in its build script
# and dexes it. The build-dir hash changes whenever its inputs change, so
# find it rather than hard-coding a path; the newest wins if stale
# directories from earlier builds are still around.
DEX="$(find "/work/target-android/${RT}/release/build" \
-path "*i-slint-backend-android-activity*/out/classes.dex" \
-printf "%T@ %p\n" 2>/dev/null | sort -rn | head -1 | cut -d" " -f2-)"
if [[ -z "${DEX}" ]]; then
echo "error: Slint classes.dex not found — did the backend build?" >&2
exit 1
fi
echo " dex: ${DEX}"
# A debug keystore, created once and kept in the cache. Debug-signed only:
# this exists to get the app onto a test device, not to release it.
KS=/work/target-android/debug.keystore
if [[ ! -f "${KS}" ]]; then
echo " generating debug keystore"
keytool -genkeypair -keystore "${KS}" -alias androiddebugkey \
-storepass android -keypass android \
-keyalg RSA -keysize 2048 -validity 10950 \
-dname "CN=Android Debug,O=Android,C=US" >/dev/null 2>&1
fi
# aapt2 link needs the compile SDK to resolve android: attributes, and
# --min-sdk-version is what ends up in the manifest the device reads.
"${BT}/aapt2" link \
-I "${SDK}/platforms/android-36/android.jar" \
--manifest /work/apps/darkroom-android/android/AndroidManifest.xml \
--min-sdk-version 28 \
--target-sdk-version 36 \
-o "${OUT}/base.apk" \
--auto-add-overlay
cp "/work/target-android/jniLibs/${ABI}/libdarkroom.so" \
"${OUT}/staging/lib/${ABI}/libdarkroom.so"
cp "${DEX}" "${OUT}/staging/classes.dex"
# -0 "" stores without compression; see the note above about mmap.
cd "${OUT}/staging"
cp "${OUT}/base.apk" "${OUT}/unaligned.apk"
zip -q -0 -X "${OUT}/unaligned.apk" "lib/${ABI}/libdarkroom.so"
zip -q -X "${OUT}/unaligned.apk" classes.dex
# zipalign before signing: apksigner preserves alignment, the reverse order
# invalidates the signature.
"${BT}/zipalign" -p -f 4 "${OUT}/unaligned.apk" "${OUT}/darkroom.apk"
"${BT}/apksigner" sign \
--ks "${KS}" --ks-pass pass:android --key-pass pass:android \
--min-sdk-version 28 \
"${OUT}/darkroom.apk"
"${BT}/apksigner" verify --print-certs "${OUT}/darkroom.apk" | head -2
'
echo "==> ${APK}"
ls -la "${APK}"
# ---------------------------------------------------------------------------
# 3. Install from the host.
#
# The container has adb but no device: build.sh mounts no USB and shares no
# network, so the host's already-authorised adb server is the shorter path.
# ---------------------------------------------------------------------------
if [[ "${INSTALL}" == "1" ]]; then
command -v adb >/dev/null || { echo "error: adb not on PATH" >&2; exit 1; }
echo "==> installing"
adb install -r "${APK}"
echo "==> launching"
adb shell am start -n paris.tourolle.darkroom/android.app.NativeActivity
fi