v0.19.0: andTunes plays music on the Rabbit

The Android half of andTunes had been paused on a 1 GB toolchain download
(Gradle wrapper, Android Gradle Plugin, Kotlin). None of that was needed:
platform 33 and build-tools 34 were already in ~/Android/Sdk, and an app that
links no libraries needs five SDK steps, not a build system. andtunes/build.py
runs aapt2 -> javac -> R8 -> zipalign -> apksigner and produces a 61 KiB APK.

andTunes 0.1.0 is framework-only Java: a six-tile menu that loads nothing, a
streaming library.json parse grouped into artists and albums in one pass, one
ListActivity for every list (songs and artist songs open with Shuffle all,
albums carry art thumbs, search as you type), a Now playing bar under every
list, and a white Now Playing screen with square art, a slim scrubber,
prev/play/next, shuffle and repeat, and no back button. A foreground
PlaybackService owns the queue: audio focus, becoming-noisy, MediaSession and
a MediaStyle notification, end of queue pauses, missing files skip, and the
last queue and position come back on launch. Black on white, and every size
doubled for the R1's override density. Cold start to the menu: 313-338 ms.

The scroll wheel turned out to be DPAD_UP/DOWN (stock Generic.kl), not
volume. The app takes both in dispatchKeyEvent: lists scroll a row per
detent, and the menu and Now Playing get volume.

LinTunes side: Connections > Install andTunes on Rabbit... installs the
committed APK over adb and grants all-files access, or copies it to Download/
over MTP when there's no adb. Sync fills Buttons/ with default artwork only
where a file is missing, so trav's own art is never overwritten.

Verified against the dev library only, synced to the R1 and driven with
adb input + screencap.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KSsYtRn4WZx6xj4GEdVPt9
This commit is contained in:
2026-09-11 14:25:55 -05:00
co-authored by Claude Opus 5
parent 8188a5b389
commit 9483d8951b
51 changed files with 2963 additions and 98 deletions
+39 -34
View File
@@ -15,10 +15,12 @@ track number of every file it just copied, so it writes them into
`Music/andTunes/library.json` at sync time and the app parses one file
instead of indexing a filesystem. That is the entire architectural bet.
**Status (2026-09-10): the desktop half is done and shipped** in LinTunes
v0.17.0 (`lintunes/andtunes/`, round 47). The Android app has not been
started. Work is paused on a slow connection — see *Toolchain* below for the
one blocking download.
**Status (2026-09-11): andTunes 0.1.0 plays music on the Rabbit.** The
desktop half shipped in LinTunes v0.17.0 (round 47); the app itself landed in
round 50 (LinTunes v0.19.0), built in plain Java with no Gradle and no
download — see *Toolchain*. Cold start to the menu measures 313–338 ms on the
R1 (target < 400). Play counts coming back and the format gate are next
(`TASKS.md`, phase 3).
---
@@ -89,49 +91,51 @@ screen where the stock behaviour is the useful one.
## Toolchain
Already done on this machine:
Everything is already on this machine, and **nothing else gets downloaded**:
- `java-21-openjdk-devel` — installed, `javac` on PATH. (Java 25 is also
installed; AGP won't run on it, so the Gradle build must be pointed at 21.)
- `java-21-openjdk-devel` — `build.py` uses `/usr/lib/jvm/java-21-openjdk`
(Java 25 is the system default; R8 and apksigner want 21).
- `~/Android/Sdk/platforms/android-33` + `build-tools/34.0.0` (aapt2, d8/R8,
zipalign, apksigner). No `cmdline-tools` / `sdkmanager` needed.
- `adb` / `fastboot` — in `/usr/local/sbin`, platform-tools 31.0.3.
**The one thing left, and the reason this is paused: the Android SDK is a
~1–1.5 GB download** and trav is on a very weak connection.
**No Gradle and no Kotlin** (decided 2026-09-11). The original plan was
Kotlin + Gradle, which is another ~1 GB of wrapper, Android Gradle Plugin and
Kotlin compiler — tools that produce the same APK. An app that links no
libraries needs exactly five SDK steps, so `build.py` runs them directly:
aapt2 compile/link → javac (`--release 11`, against `android.jar`) → R8
(`--release`, keep rules from aapt2) → zipalign → apksigner. Plain Java also
means no Kotlin runtime inside the APK: it's 61 KiB. The source tree is the
standard Gradle shape, so Gradle could be dropped in later with no moves.
```sh
# 1. commandline-tools (~150 MB) from developer.android.com/studio
# (bottom of the page, "Command line tools only", linux zip)
mkdir -p ~/Android/Sdk/cmdline-tools
unzip commandlinetools-linux-*_latest.zip -d ~/Android/Sdk/cmdline-tools
mv ~/Android/Sdk/cmdline-tools/cmdline-tools ~/Android/Sdk/cmdline-tools/latest
To build on a second machine: install `java-21-openjdk-devel`, and copy
`~/Android/Sdk/platforms/android-33` and `build-tools/34.0.0` across (≈ 280 MB,
no internet needed). `ANDROID_HOME` / `JAVA_HOME` override the default paths.
export ANDROID_HOME="$HOME/Android/Sdk"
export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$PATH"
export JAVA_HOME=/usr/lib/jvm/java-21-openjdk
`andtunes.keystore` is committed on purpose: both machines must sign the same
way, or `adb install -r` refuses an update built on the other one. A sideloaded
app with no store listing has nothing for the key to protect.
# 2. the platform we build against (~150 MB total)
sdkmanager --licenses
sdkmanager "platforms;android-33" "build-tools;34.0.0"
```
Then the **first** Gradle build pulls the wrapper (~130 MB) and the Android
Gradle Plugin's dependencies (several hundred MB). After that it's all cached
and `./gradlew --offline assembleDebug` works with no connection at all — so
the download is a one-time cost, but it is not a small one. Do it on a decent
connection.
R8 gotcha: `-keepattributes SourceFile,LineNumberTable` makes R8 8.2 die with
an internal NPE on javac 21's debug info for an anonymous class, so
`proguard.pro` keeps none. Stack traces still carry class and method names
(`-dontobfuscate`).
Fedora note: don't `dnf install android-tools` over the existing
`/usr/local/sbin/adb`; two adb versions on PATH fight over the server port.
---
## Building and installing (once the SDK is there)
## Building and installing
```sh
cd andtunes
./gradlew assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apk
adb shell am start -n me.teafry.andtunes/.MenuActivity
python3 andtunes/build.py # -> andtunes/build/andTunes.apk
python3 andtunes/build.py --install # ... + adb install -r and launch
python3 andtunes/build.py --ship # ... + copy into lintunes/android/
# after a fresh install by hand, the grants LinTunes' installer does for you:
adb shell appops set --uid me.teafry.andtunes MANAGE_EXTERNAL_STORAGE allow
adb shell pm grant me.teafry.andtunes android.permission.POST_NOTIFICATIONS
# the number the whole project is judged on:
adb shell am start -W -n me.teafry.andtunes/.MenuActivity | grep TotalTime
@@ -143,7 +147,8 @@ Target: menu visible in **under 400 ms**.
## Design decisions already locked
- **Kotlin, no Compose, no androidx, no Media3.** Framework `Activity`,
- **Java, no Compose, no androidx, no Media3** (Kotlin was the first plan;
see *Toolchain* for why it went). Framework `Activity`,
`ListView`, `MediaPlayer`, `MediaSession`, `Notification.MediaStyle`, R8
minify. Cold start is the feature; there is nothing to initialize if nothing
is linked in. Same instinct as `trash.py` and `sync_identity.py` on the