getevent settled what the wheel can and can't tell us: och1970_holl_key advertises KEY_UP and KEY_DOWN and nothing else -- no REL_WHEEL, no resolution, because the och1970 is a Hall latch the driver quantises into clicks. There is no sub-detent position to read without a kernel driver change, so all the nuance has to come from *when* the detents arrive. So a detent is now an impulse into a velocity rather than a jump. The list coasts and eases out, and an impulse is worth up to 8x more when detents come 30 ms apart than when they come 220 ms apart -- squared, so the gain stays out of the way while you're hunting for one row. Measured on the device: one unhurried detent moves about half a row, twelve fast ones move forty-odd. Turning back the other way kills the coast first, so a correction bites instead of fighting the glide. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SKXUgsBBwe3qaHEjeV8ubP
121 lines
7.1 KiB
Markdown
121 lines
7.1 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: phases 1–3 done (2026-09-11, LinTunes rounds 50–51, andTunes
|
||
0.2.0); 0.2.1 (LinTunes round 54, 2026-09-14) fixed the grouping crash —
|
||
`Library.group()` keyed albums case-folded but artists raw-case, so one artist
|
||
spelled two ways on one album ("RJD2" / "Rjd2") took the whole library down
|
||
with `Couldn't read library.json`. Both maps fold case now, and `Artist` has a
|
||
`key` the way `Album` always did. 0.2.2 (round 55, 2026-09-14) reversed the
|
||
wheel in lists and made it scroll smoothly (`WheelScroll` — a pixel debt paid
|
||
off per frame, not a row per detent); volume on the other screens is
|
||
unchanged.** What's left is the *Parked* list, pulled in
|
||
as trav finds he needs it. Built with
|
||
`python3 andtunes/build.py` — plain Java, no Gradle, nothing downloaded (see
|
||
`README.md`, *Toolchain*). 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.
|
||
- **The wheel can only ever give us detents.** `getevent -pl` (2026-09-14):
|
||
`och1970_holl_key` advertises `KEY (0001): KEY_UP KEY_DOWN` and *nothing
|
||
else* — no `EV_REL`/`REL_WHEEL`, no `EV_ABS`, no resolution. The `och1970`
|
||
is a Hall latch, so the driver quantises the magnet into clicks and hands
|
||
userspace one key press each. There is no sub-detent position to read
|
||
without a kernel driver change, so every bit of nuance has to be derived
|
||
from the *timing* between detents (see `WheelScroll`).
|
||
- Scroll wheel = input device `och1970_holl_key` → **KEY_UP / KEY_DOWN**,
|
||
mapped by the stock `Generic.kl` (checked 2026-09-11 via `dumpsys input`)
|
||
to **`KEYCODE_DPAD_UP` / `DPAD_DOWN`** — not volume. Side/PTT button sits
|
||
on `mtk-kpd` with KEY_VOLUMEDOWN + KEY_POWER. Headset exposes
|
||
KEY_PLAYPAUSE, so a MediaSession gets media keys for free.
|
||
- USB: set to come up as **`mtp,adb`** whenever the screen is unlocked
|
||
(`adb shell svc usb setScreenUnlockedFunctions mtp`, 2026-09-11), so the
|
||
gvfs mount sync uses and the adb install uses are both there at once.
|
||
|
||
## Phase 1 — plays a song (v0.1.0) ✔
|
||
|
||
- [x] `java-21-openjdk-devel` — installed, `javac` on PATH. (Java 25 is also
|
||
present; `build.py` points at 21.)
|
||
- [x] Android SDK: platform 33 + build-tools 34.0.0 were already in
|
||
`~/Android/Sdk`. **No Gradle, no Kotlin** — `build.py` runs aapt2 →
|
||
javac → R8 → zipalign → apksigner directly (trav, 2026-09-11: don't
|
||
pull a GB over cell if it isn't needed; it isn't).
|
||
- [x] Project, package `me.teafry.andtunes`, minSdk 26 / targetSdk 33,
|
||
framework only: `Activity`, `ListView`, `MediaPlayer`, `MediaSession`,
|
||
`Notification.MediaStyle`, R8. The APK is 61 KiB.
|
||
- [x] `MANAGE_EXTERNAL_STORAGE` + a first-run screen that sends the user to
|
||
the toggle (LinTunes' installer grants it over adb, so it rarely shows).
|
||
- [x] Library loader: `android.util.JsonReader` streaming parse off the main
|
||
thread into a process singleton; artists/albums grouped in one pass;
|
||
an unknown `format` gets a readable message; reloads when
|
||
`library.json` changes (a re-sync) the next time the menu resumes.
|
||
- [x] `MenuActivity`: six tiles, 2 × 3, **loads nothing**. The sixth reads
|
||
"Now Playing" or "Shuffle All" depending on the service.
|
||
- [x] `ListActivity` in songs mode + "Shuffle all" header row.
|
||
- [x] `PlaybackService` (foreground) + `NowPlayingActivity`: white
|
||
background, square art, slim scrubber, ◀ ⏯ ▶, shuffle, repeat.
|
||
**No back button.**
|
||
- [x] Audio focus, ACTION_AUDIO_BECOMING_NOISY, MediaSession + notification.
|
||
- [x] End of queue **pauses** unless repeat is on. Repeat: off / all / one.
|
||
- [x] `adb shell am start -W`: **313–338 ms** cold to the menu (551 ms on the
|
||
very first launch after install, which includes dex verification).
|
||
|
||
## Phase 2 — the rest of the screens ✔
|
||
|
||
- [x] Playlists, artists (→ "All songs" + albums; all songs has its own
|
||
shuffle), albums, search (artists, albums and songs as you type).
|
||
- [x] Album art thumbs in album rows, full art on now playing.
|
||
- [x] The "Now playing" bar at the bottom of every list screen; the playing
|
||
song is drawn inverted in any list it appears in.
|
||
- [x] Scroll wheel → list scrolling (one row per detent), volume on the menu
|
||
and now-playing screens. Taken in `dispatchKeyEvent`, DPAD *and*
|
||
VOLUME codes, down *and* up, so the search box can't swallow it.
|
||
- [x] Replaceable button images: `Buttons/<name>.png` beats the bundled
|
||
drawable, for the six menu tiles plus ◀ ▶ ⏯. LinTunes ships the
|
||
defaults into `Buttons/` only where the file is missing.
|
||
- [x] Missing file (synced then deleted) skips with a toast, doesn't stop
|
||
the queue.
|
||
- [x] Resume last queue + track + position on launch (paused).
|
||
|
||
## Phase 3 — install from LinTunes, counts coming back
|
||
|
||
- [x] `Connections → Install andTunes on Rabbit…` (LinTunes 0.19.0):
|
||
`adb install -r` + the two permission grants; says Install / Update /
|
||
Reinstall from the version on the device; with no adb, copies the APK
|
||
to `Download/` over MTP for a manual tap. APK committed at
|
||
`lintunes/android/andTunes.apk` (+ `andTunes.json`, its version).
|
||
- [x] Format gate (LinTunes 0.20.0): sync reuses
|
||
`export/web_support.conversion_for` outright, because Android's
|
||
MediaPlayer decodes the same set a browser does. Protected AAC is
|
||
refused and reported; ALAC, AIFF and anything else unplayable becomes
|
||
**FLAC** (never a lossy re-encode). Conversions are cached per track, so
|
||
they're converted once and size-diffed after that.
|
||
- [x] Play counts back (andTunes 0.2.0): a natural finish counts a play, as
|
||
it does in LinTunes. The app keeps `plays/andtunes-<install id>.json` in
|
||
the per-machine-totals shape of `storage/play_journal.py`. Sync folds it
|
||
into `<data_dir>/plays/` with the per-track max *before* copying
|
||
anything, so the R1 is one more machine in the Round 38 model and there
|
||
are no new merge rules.
|
||
- [x] Retired `Sync Playlist to Rabbit (Auxio)` from the menu. (trav's R1
|
||
has no old `Music/<Playlist>/` folders, so no cleanup was needed.)
|
||
|
||
## 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.
|