# andTunes A music player for the Rabbit R1 that **never scans anything**. Every other Android player rebuilds its library on the device. Auxio reads Android's MediaStore on each launch, which on a Helio P35 is the "your songs will show up here" hang — and Musicolet, AIMP, Vinyl, Retro, Gramophone and Fossify are all MediaStore-backed too. Symfonium and VLC keep their own database but still scan the device to fill it, and both assume a phone-sized screen. (trav sideloaded Musicolet in September 2026 to check: "it was… fine?" — nothing off the shelf is built around already knowing the answer.) LinTunes *does* already know the answer. It has the artist, album, year and 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. --- ## The device Measured on trav's R1, not taken from the internet (which says 240×282 and is wrong): | | | |---|---| | Panel | **480 × 640 px**, physical density 320, **override density 160** | | Build | Android **13**, API **33**, `gsi_r1-userdebug` (r1_escape AOSP flash) | | CPU | MediaTek Helio P35, arm64-v8a, 4 GB RAM | | Storage | 128 GB, ~65 GB free | | Scroll wheel | input device `och1970_holl_key` → **KEY_UP / KEY_DOWN** | | Side/PTT button | `mtk-kpd`: KEY_VOLUMEDOWN + KEY_POWER | | Headset | `mt63xx-accdet`: KEY_PLAYPAUSE → a MediaSession gets media keys free | ### The dp trap Density is overridden to 160, so an app sees a 480 × 640 **dp** canvas — but the panel is 2.88", about 278 real ppi. **One dp is roughly half its usual physical size here.** A stock 48 dp list row is 4.4 mm tall and unusable. > **Rule: double every stock dp value.** Body text 28–32 sp, list rows ≥ 88 dp, > menu tiles ~213 dp. A 2 × 3 grid gives six 240 × 213 dp buttons, ≈ 22 × 19 mm > each. Do **not** "fix" this by changing the device's density. trav has it where he wants it and asked for it to be left alone. ### The scroll wheel, and whether we can have it trav reports the wheel acts as **volume** in every app, even though the kernel device emits KEY_UP/KEY_DOWN — so the ROM ships a key layout mapping those to `KEYCODE_VOLUME_UP`/`DOWN`. Confirm which one actually arrives when the device is next plugged in: ```sh adb shell 'ls /system/usr/keylayout/ | grep -i holl' adb shell 'cat /system/usr/keylayout/och1970_holl_key.kl' # if it exists ``` Either way **an app can absolutely claim it for itself**, without touching a system file or affecting any other app. A foreground Activity sees key events before the system's volume handling, so: ```kotlin override fun onKeyDown(code: Int, e: KeyEvent): Boolean = when (code) { KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_DPAD_UP -> { wheelUp(); true } KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_DPAD_DOWN -> { wheelDown(); true } else -> super.onKeyDown(code, e) } ``` Handle **both** mappings so it works whichever the ROM sends, and consume `onKeyUp` for the same codes too — returning true only from `onKeyDown` can still let the system act on the release. Returning true is what stops the volume UI appearing. This is ordinary Android (camera apps do it for the shutter); it applies only while an andTunes activity is focused, so volume behaves normally everywhere else. Worth deciding once it's testable: the wheel scrolls lists everywhere, and on the now-playing screen it probably *should* stay volume — that's the one screen where the stock behaviour is the useful one. --- ## Toolchain Already done on this machine: - `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.) - `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. ```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 export ANDROID_HOME="$HOME/Android/Sdk" export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$PATH" export JAVA_HOME=/usr/lib/jvm/java-21-openjdk # 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. 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) ```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 # the number the whole project is judged on: adb shell am start -W -n me.teafry.andtunes/.MenuActivity | grep TotalTime ``` Target: menu visible in **under 400 ms**. --- ## Design decisions already locked - **Kotlin, no Compose, no androidx, no Media3.** 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 LinTunes side. - **Package** `me.teafry.andtunes`, minSdk 26, targetSdk 33, arm64-v8a. - **`MANAGE_EXTERNAL_STORAGE`** with a first-run screen sending the user to the toggle. `READ_MEDIA_AUDIO` grants the audio files but *not* `library.json`, which isn't media — that's the whole reason. Fallback if it turns out to be blocked: a one-time SAF folder pick. - **Screens are activities**, so "the last screen we were at" is Android's own back stack with no bookkeeping. `MenuActivity` (six tiles, loads nothing), one generic `ListActivity` parameterised for every list in the spec, `NowPlayingActivity`, and a foreground `PlaybackService`. - **No back button on the now-playing screen** — Android's own is always on screen and the panel is tiny (trav's call). - **End of a queue pauses.** It does not roll into shuffle-all. Repeat is off / all / one. - **Replaceable button artwork**: the app loads `Buttons/.png` from the device folder when present, else a bundled drawable. LinTunes fills in defaults only where the file is *missing*, so trav's own art is never overwritten. ## What the app reads LinTunes writes all of this; see `lintunes/andtunes/layout.py` for the authority and `manifest.py` for the exact rows. ``` /sdcard/Music/andTunes/ library.json format 1; refuse an unknown format loudly Media///04 Song.mp3 Art/<8-hex album key>.jpg 480 px, one per album Playlists/.m3u entries are ../Media/… relative paths Buttons/*.png the user's own menu artwork — app reads only plays/andtunes-.json the app writes counts back here (phase 3) ``` ```json {"format":1,"generated":"…","generator":"lintunes 0.17.0", "tracks":[{"id":123,"title":"Alhambra","path":"Media/…/04 Alhambra.mp3", "artist":"Ahmad Jamal","album":"…","year":1961,"track":4, "secs":214,"art":"Art/3f2a1b9c.jpg"}], "playlists":[{"id":"A1B2C3D4","name":"Roadtrip","tracks":[123,456]}]} ``` Rows are **sparse** — empty and zero fields are omitted, so use `optString`/`optInt`. Artists and albums are deliberately *not* shipped: group the flat list in one pass at load rather than parsing three redundant lists. Titles are stored in full even when the filename was truncated, so always display `title`, never the filename. ## Testing without a Rabbit `python3 scripts/make_dev_library.py --out dev-library` builds a synthetic LinTunes library, and syncing it produces a real `Music/andTunes/` tree you can push onto any Android device or emulator with `adb push`. That tree includes the awkward cases on purpose — an emoji album name, a 208-character title, two tracks that differ only by id, a track with no artist. ## Board `TASKS.md` in this directory.