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
101 lines
5.6 KiB
Markdown
101 lines
5.6 KiB
Markdown
# 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 on `mtk-kpd` with
|
||
KEY_VOLUMEDOWN + KEY_POWER. Headset exposes KEY_PLAYPAUSE, so a MediaSession
|
||
gets media keys for free.
|
||
|
||
## Phase 1 — plays a song (v0.1.0)
|
||
|
||
- [x] `java-21-openjdk-devel` — installed, `javac` on 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 assembleDebug` needs no connection. Exact commands
|
||
in `README.md`.
|
||
- [ ] Gradle project, package `me.teafry.andtunes`, minSdk 26 / targetSdk 33.
|
||
**No Compose, no androidx, no Media3** — framework `Activity`,
|
||
`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_AUDIO` grants the audio files but **not**
|
||
`library.json`, which isn't media — that's the whole reason.
|
||
- [ ] Library loader: parse `library.json` off the main thread into a
|
||
singleton that survives between activities. Refuse an unknown `format`
|
||
with 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.
|
||
- [ ] `ListActivity` in 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/DOWN` **and** `DPAD_UP/DOWN` in both
|
||
`onKeyDown` and `onKeyUp` — 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; see `README.md`.
|
||
- [ ] Replaceable button images: load `Buttons/<name>.png` from the device
|
||
folder when present, else the bundled drawable. LinTunes ships defaults
|
||
into `Buttons/` 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")`
|
||
(the `ffmpeg_available()` runtime-tool pattern — never a pip dep), then
|
||
`adb install -r`; fall back to copying the APK over MTP for a manual
|
||
tap. APK committed at `lintunes/android/andTunes.apk` with
|
||
`package_data`, read via `importlib.resources` like `export/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>.json` in
|
||
the per-machine-totals shape of `storage/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 old `Music/<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.
|