Round 47 was verified by loading trav's real 21,531-track library. Nothing was written and nothing was at risk, but it's the wrong habit and he said so. The alternative is better for development anyway: reproducible, in-repo, and full of the awkward cases on purpose rather than by luck. scripts/make_dev_library.py builds a complete synthetic library — data dir and music tree — from ffmpeg sine waves in six containers, with art on some albums and not others, every playlist type (regular, folder, live/non-live/ unsupported/limited+nested smart, system, empty, duplicates), a play journal and a tombstone. ~4 MB, gitignored; the generator is the artifact worth keeping, not the sine waves. The edge cases are the point. A collision pair identical in artist/album/title/ number, slashes and colons in every name, a track with no artist, a multi-disc release, a compilation whose album_artist differs, a dangling location, a 208-character title, and non-ASCII plus an emoji all the way out to the m3u filename. CLAUDE.md now carries the rule: never develop against the real library, and when a feature needs a shape the fixture lacks, add it here. Using it found two bugs in it — a relative --out made every location resolve against the wrong root, and four-minute uncompressed clips made it 62 MB. andTunes is paused on a weak connection (the Android SDK is a ~1-1.5 GB one-time download; after that --offline builds need nothing). andtunes/ README.md now carries everything needed to resume cold: measured device facts, the dp trap, exact toolchain commands, locked design decisions, and the library.json contract. JDK 21 is installed and ticked off. Also answers the scroll wheel question: it reads as volume because the ROM's key layout maps the wheel's KEY_UP/KEY_DOWN to KEYCODE_VOLUME_UP/DOWN, but a focused activity sees key events first — so andTunes can claim it by consuming both those and DPAD_UP/DOWN, in onKeyDown and onKeyUp. No system file touched, no other app affected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015wrTys5U1fLb4pBD2LKWzV
5.6 KiB
5.6 KiB
andTunes — task board
A music player for the Rabbit R1 that never scans anything: LinTunes writes
Music/andTunes/library.json during sync and the app just reads it. See
../andTunes spec.txt for the original brief.
Legend: [ ] todo · [~] in progress · [x] done.
Status: paused 2026-09-10 on a weak connection — the Android SDK is a
~1–1.5 GB download. Everything needed to resume cold is in README.md:
device facts, toolchain commands, locked design decisions, and the exact
shape of the library.json LinTunes already writes. Phases are andTunes'
own; which LinTunes round each lands in is decided when it starts.
Device facts (measured 2026-09-06, adb shell wm size etc.)
- Panel 480 × 640 px, physical density 320, override density 160 — so the app sees a 480 × 640 dp canvas on a 2.88" screen (~278 real ppi).
- One dp is about half its usual physical size here. Double every stock value: body text 28–32 sp, list rows ≥ 88 dp, menu tiles ~213 dp. A 48 dp row would be 4.4 mm tall. Do not "fix" this by changing the device density — trav likes it where it is.
- Android 13 / API 33,
gsi_r1-userdebug, arm64-v8a, 65 GB free. - Scroll wheel = input device
och1970_holl_key→ KEY_UP / KEY_DOWN (KEYCODE_DPAD_UP/DPAD_DOWN). Side/PTT button sits onmtk-kpdwith KEY_VOLUMEDOWN + KEY_POWER. Headset exposes KEY_PLAYPAUSE, so a MediaSession gets media keys for free.
Phase 1 — plays a song (v0.1.0)
java-21-openjdk-devel— installed,javacon PATH. (Java 25 is also present; AGP won't run on it, so point the build at 21.)- Android SDK — the one blocker. commandline-tools + platform 33 +
build-tools into
~/Android/Sdk, then a first Gradle build to warm the wrapper and AGP caches. ~1–1.5 GB all told; after that./gradlew --offline assembleDebugneeds no connection. Exact commands inREADME.md. - Gradle project, package
me.teafry.andtunes, minSdk 26 / targetSdk 33. No Compose, no androidx, no Media3 — frameworkActivity,ListView,MediaPlayer,MediaSession,Notification.MediaStyle, R8 minify. Cold start is the whole feature; there is nothing to initialize if nothing is linked in. MANAGE_EXTERNAL_STORAGE+ a first-run screen that sends the user to the toggle.READ_MEDIA_AUDIOgrants the audio files but notlibrary.json, which isn't media — that's the whole reason.- Library loader: parse
library.jsonoff the main thread into a singleton that survives between activities. Refuse an unknownformatwith a readable message rather than half-reading it. MenuActivity: six tiles, 2 × 3, loads nothing. The sixth reads "Now Playing" or "Shuffle All" depending on the service.ListActivityin songs mode + "Shuffle all" header row.PlaybackService(foreground) +NowPlayingActivity: white background, square art, slim scrubber, ◀ ⏯ ▶, shuffle. No back button — Android's own is always on screen.- Audio focus, ACTION_AUDIO_BECOMING_NOISY, MediaSession + notification.
- End of queue pauses unless repeat is on. Repeat: off / all / one.
- Verify with
adb shell am start -W— target: menu visible < 400 ms.
Phase 2 — the rest of the screens
- Playlists, artists (→ albums + "All songs" + shuffle for that artist), albums, search.
- Album art thumbs in album rows, full art on now playing.
- The "Now playing" bar at the bottom of every list screen.
- Scroll wheel → list scrolling, and volume on the now-playing screen.
Consume
KEYCODE_VOLUME_UP/DOWNandDPAD_UP/DOWNin bothonKeyDownandonKeyUp— the ROM's key layout decides which arrives, and claiming them in a focused activity affects no other app. First check/system/usr/keylayout/on the device; seeREADME.md. - Replaceable button images: load
Buttons/<name>.pngfrom the device folder when present, else the bundled drawable. LinTunes ships defaults intoButtons/only when the file is missing, so the user's own artwork is never overwritten. - Missing file (synced then deleted) skips with a toast, doesn't crash the queue.
- Resume last track + position on launch.
Phase 3 — install from LinTunes, counts coming back
Connections → Install andTunes on Rabbit…:shutil.which("adb")(theffmpeg_available()runtime-tool pattern — never a pip dep), thenadb install -r; fall back to copying the APK over MTP for a manual tap. APK committed atlintunes/android/andTunes.apkwithpackage_data, read viaimportlib.resourceslikeexport/templates.- Format gate shaped like
export/web_support.py: deny-by-default on suffix, AIFF and protected AAC refused, anything else unplayable converted to FLAC (never a lossy re-encode). - Play counts back: the app writes
plays/andtunes-<installid>.jsonin the per-machine-totals shape ofstorage/play_journal.py; sync copies it back and folds it in. The R1 becomes one more machine in the Round 38 model — no new merge rules. - Retire
Sync Playlist to Rabbit (Auxio)once andTunes covers it, and offer a one-off cleanup of the oldMusic/<Playlist>/folders.
Parked
- Gapless, crossfade, EQ, ratings, folder browsing, sleep timer, Android Auto.
- Deleting the old per-playlist folders automatically. They're outside the andTunes root and the containment guard makes them unreachable on purpose; removing them should be an explicit, separately-confirmed action.