9.7 KiB
9.7 KiB
Lockstep Android MVP — design
Product goals, DSP scope, and repo layout are summarized in SPECS.md. This document captures Android shell architecture, decisions made so far, libpasada JNI + state machine, and open UI questions.
UI screens
- Library: shows the playlists of the user
- Annotation: user presses a button to annotate the beat
- Now Playing: player showing an individual track, with pause/previous/next buttons
- Collection mode: app records sensor data during a run
- Settings: allows to enable modes above, logout button
Already captured from SPECS.md
- Product: Pace-aware playlist ordering + real-time playback adaptation via accelerometer, libpasada (C++/JNI) + Oboe, user-supplied MP3 via file descriptors, feedback loop for sensor-driven playback.
- Metadata: jukebox submodule — Spotify-backed metadata in SQLite, separate from DSP.
- Native: libpasada independently buildable/testable; Android side may stub JNI until native is ready.
Your decisions (this session)
| Topic | Choice |
|---|---|
| Session context | Background / screen off — needs foreground service, ongoing notification, and media-style controls expectation |
| Playlist assembly | Manual local MP3 pick + align/display with jukebox metadata where possible |
| Playback ownership | Run mode owned by a foreground service — Oboe, libpasada JNI, and accelerometer feeding live in the service while a run is active; UI binds for display/control only |
| Accel → native path | SensorManager callbacks on a dedicated HandlerThread (not main) — onSensorChanged may call JNI directly; no intermediate Java queue for MVP. libpasada must be safe relative to concurrent Oboe/audio-thread work (locking or lock-free handoff inside native). |
| Jukebox scope (MVP) | Playlist identity + BPM/metadata in SQLite; album/track artwork deferred — artwork is intended to live in jukebox later, not in the app long-term. |
| Spotify credentials | Refresh is triggered and handled by the host app (lifecycle, WorkManager, error handling, user re-login). Jukebox remains a library: expose/sync APIs that accept valid credentials or sessions the app supplies after refresh; avoid owning background refresh loops unless you later consolidate that into jukebox by explicit choice. |
| MP3 ↔ jukebox matching | 1) Match primarily using embedded MP3 tags (ID3 / common frames—title, artist, album; plus ISRC when present on both file and jukebox row). 2) If no confident unique match, prompt the user to link manually (pick jukebox track ↔ local file). Persist manual links in app or jukebox-backed mapping store. Ambiguous multi-match should fall through to manual/disambiguation (exact field weights deferred at implementation). |
| libpasada JNI + states | State machine (below); JNI entry points init (per run start), feedAccel, play (MP3 FD + begin adaptive playback), pause, resume, stop, getDiagnostics. |
libpasada state machine (native)
Oboe behavior is summarized per state:
| State | Meaning |
|---|---|
| LOADED | JNI .so loaded (System.loadLibrary); no Oboe callbacks registered yet. |
| INITIALIZED | Oboe stream active, outputting silence; callbacks registered; ready for play(). |
| PLAYING | Oboe outputting decoded/adapted music (pace-driven processing active). |
| PAUSED | User paused: Oboe still runs but outputs silence (distinct from STOPPED). |
| FINISHED | Current track ended: Oboe outputs silence until the app starts another play() (or you later add explicit advance rules). |
| STOPPED | Oboe callbacks unregistered / stream torn down for this run segment; lowest-power idle after stop(). |
JNI calls (frozen names for stubs)
init()— Called each time a run is started (not once per app launch): transition into INITIALIZED (register Oboe, run silent audio, allocate buffers, reset pace pipeline as needed). Preconditions: typically LOADED or returning from STOPPED after priorstop().feedAccel()— Submit accelerometer samples to the pace estimator (unchanged intent).play(...)— Accept open MP3 file descriptor (and any offset/length/index the native decoder needs), begin decoding and adaptive playback → PLAYING. Used when starting or switching tracks from INITIALIZED or FINISHED (new FD); not used to leave PAUSED (useresume()).pause()— PLAYING → PAUSED (silent output; keep graph alive).resume()— PAUSED → PLAYING: continue the same track/decode position and adaptive processing without re-opening the FD.stop()— Tear down to STOPPED (unregister Oboe callbacks / release stream for this run).getDiagnostics()— Runtime metrics / last error for logging and UI (unchanged intent).
stateDiagram-v2
direction LR
[*] --> Loaded
Loaded --> Initialized: init on run start
Initialized --> Playing: play fd
Playing --> Paused: pause
Paused --> Playing: resume
Playing --> Finished: track ends native
Finished --> Playing: play next fd
Paused --> Stopped: stop
Playing --> Stopped: stop
Finished --> Stopped: stop
Initialized --> Stopped: stop
Stopped --> Initialized: init next run
Architecture implications (non-negotiable follow-through)
- Foreground service + notification channel (Android 8+) with media controls (Compact/MediaStyle or MediaSession) so runs survive screen-off and fit user expectations.
- Lifecycle split: ForegroundService is the single owner of the real-time pipeline during a run (Oboe + JNI +
SensorManagerregistration with aHandleron a backgroundHandlerThreadfor listener delivery). Activity/Compose is for setup, library mapping, and reflecting service state—not for owning playback threads. Accelerometer-driven JNI runs on that sensor HandlerThread, concurrent with Oboe’s audio callback path unless libpasada merges them internally. (Separate process for the service remains optional if OEM kills warrant it—defer unless needed.) - Permissions & battery:
POST_NOTIFICATIONS(13+), possibly exact alarm if you schedule anything (likely N/A for MVP); document ignore battery optimizations only if you discover OEM kills sensor/audio — question below. - Jukebox vs app: Treat jukebox as metadata persistence + Spotify fetch helpers; the app owns OAuth/token refresh scheduling and passes renewed tokens (or session handles) into jukebox when syncing. Reads should stay useful offline from SQLite; network sync requires valid credentials (exact failure UX still covered under onboarding/errors below).
- Library ingestion: Reading tags runs off main thread (e.g. during folder scan or when user adds files); store resolved local URI/path ↔ jukebox track id so the foreground service can open FDs without redoing fuzzy matching.
- ForegroundService ↔ JNI: Starting a run calls
init(); handing off each local track callsplay(fd, …);pause()/ notification pause map topause();resume()when the user resumes from pause;stop()when the run ends. SensorHandlerThreadkeeps callingfeedAccel()whenever registered.
UI / UX questions (MVP 1)
Navigation and screens
- Minimum screens: e.g. Run (now playing + pace indicator), Library (local files + jukebox side-by-side?), Settings — which are must-have vs deferred?
- Run screen layout: Dominant Now Playing + subtle pace vs dominant pace/BPM match + compact transport?
- Start/stop run: Explicit Start run that arms sensors + service vs auto-start on play?
System UI integration
- Notification: Full MediaSession (lockscreen, BT headsets, Wear) vs minimal ongoing notification only?
- Widgets / Wear: Explicitly out of scope for MVP?
Onboarding and permissions
- First-run flow: Grant notifications, pick folder, sign-in to Spotify (if required by jukebox), battery optimization warning — single wizard or inline prompts?
- Empty states: No MP3s mapped, no Spotify login, no BPM in DB — copy and primary actions?
Visual design
- Design system: Material 3 / dynamic color vs fixed brand palette for sports contexts (high contrast sunlight)?
- Accessibility: Minimum targets for sweaty fingers, TalkBack labeling for pace and controls?
Feedback and failure modes
- When pace detection is noisy: Show confidence, freeze adaptation, or fallback to nominal BPM?
- Errors: Missing file, codec failure, JNI assertion — blocking dialog vs toast + safe stop?
Suggested order to resolve
flowchart LR
subgraph core [Core commitments]
FS[ForegroundService]
MS[MediaSession optional depth]
JNI[libpasada JNI states]
end
subgraph data [Data layer]
Match[Tags then manual map]
JB[Jukebox SQLite plus app auth]
end
subgraph ui [UI shell]
Nav[Screen map and navigation]
RunUX[Run lifecycle UX]
end
FS --> MS
FS --> JNI
JNI --> Match
Match --> JB
FS --> RunUX
Nav --> RunUX
- JNI + libpasada states — frozen (see libpasada state machine section).
- MP3↔metadata matching — embedded tags first, then manual link — decided.
- Lock screen map + notification depth.
- Fill edge-case UX (permissions, empty states, failures).
Optional codebase alignment
Quick pass over existing Gradle modules and any placeholder Activity will turn this questionnaire into file-specific tasks (app module layout, jukebox submodule APIs).