# 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/.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-.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//` 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.