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
7.1 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: 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_keyadvertisesKEY (0001): KEY_UP KEY_DOWNand nothing else — noEV_REL/REL_WHEEL, noEV_ABS, no resolution. Theoch1970is 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 (seeWheelScroll). - Scroll wheel = input device
och1970_holl_key→ KEY_UP / KEY_DOWN, mapped by the stockGeneric.kl(checked 2026-09-11 viadumpsys input) toKEYCODE_DPAD_UP/DPAD_DOWN— not volume. Side/PTT button sits onmtk-kpdwith KEY_VOLUMEDOWN + KEY_POWER. Headset exposes KEY_PLAYPAUSE, so a MediaSession gets media keys for free. - USB: set to come up as
mtp,adbwhenever 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) ✔
java-21-openjdk-devel— installed,javacon PATH. (Java 25 is also present;build.pypoints at 21.)- Android SDK: platform 33 + build-tools 34.0.0 were already in
~/Android/Sdk. No Gradle, no Kotlin —build.pyruns 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). - Project, package
me.teafry.andtunes, minSdk 26 / targetSdk 33, framework only:Activity,ListView,MediaPlayer,MediaSession,Notification.MediaStyle, R8. The APK is 61 KiB. MANAGE_EXTERNAL_STORAGE+ a first-run screen that sends the user to the toggle (LinTunes' installer grants it over adb, so it rarely shows).- Library loader:
android.util.JsonReaderstreaming parse off the main thread into a process singleton; artists/albums grouped in one pass; an unknownformatgets a readable message; reloads whenlibrary.jsonchanges (a re-sync) the next time the menu resumes. 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, repeat. No back button.- Audio focus, ACTION_AUDIO_BECOMING_NOISY, MediaSession + notification.
- End of queue pauses unless repeat is on. Repeat: off / all / one.
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 ✔
- Playlists, artists (→ "All songs" + albums; all songs has its own shuffle), albums, search (artists, albums and songs as you type).
- Album art thumbs in album rows, full art on now playing.
- The "Now playing" bar at the bottom of every list screen; the playing song is drawn inverted in any list it appears in.
- 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. - Replaceable button images:
Buttons/<name>.pngbeats the bundled drawable, for the six menu tiles plus ◀ ▶ ⏯. LinTunes ships the defaults intoButtons/only where the file is missing. - Missing file (synced then deleted) skips with a toast, doesn't stop the queue.
- Resume last queue + track + position on launch (paused).
Phase 3 — install from LinTunes, counts coming back
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 toDownload/over MTP for a manual tap. APK committed atlintunes/android/andTunes.apk(+andTunes.json, its version).- Format gate (LinTunes 0.20.0): sync reuses
export/web_support.conversion_foroutright, 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. - 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>.jsonin the per-machine-totals shape ofstorage/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. - Retired
Sync Playlist to Rabbit (Auxio)from the menu. (trav's R1 has no oldMusic/<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.