149 lines
9.8 KiB
Markdown
149 lines
9.8 KiB
Markdown
# Lockstep Android MVP — design
|
||
|
||
Product goals, DSP scope, and repo layout are summarized in [SPECS.md](SPECS.md). This document captures **Android shell architecture**, **decisions made so far**, **libpasada JNI + state machine**, and **open UI questions**.
|
||
|
||
## UI screens
|
||
|
||
- Onboarding: explains why notifications are necessary, implements login
|
||
- 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](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 prior **`stop()`**.
|
||
- **`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** (use **`resume()`**).
|
||
- **`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).
|
||
|
||
```mermaid
|
||
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 + `SensorManager` registration with a **`Handler` on a background `HandlerThread`** for 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 calls **`play(fd, …)`**; **`pause()`** / notification pause map to **`pause()`**; **`resume()`** when the user resumes from pause; **`stop()`** when the run ends. Sensor **`HandlerThread`** keeps calling **`feedAccel()`** whenever registered.
|
||
|
||
---
|
||
|
||
## UI / UX questions (MVP 1)
|
||
|
||
### Navigation and screens
|
||
|
||
1. **Minimum screens**: e.g. Run (now playing + pace indicator), Library (local files + jukebox side-by-side?), Settings — which are **must-have** vs deferred?
|
||
2. **Run screen layout**: Dominant **Now Playing** + subtle pace vs dominant **pace/BPM match** + compact transport?
|
||
3. **Start/stop run**: Explicit **Start run** that arms sensors + service vs auto-start on play?
|
||
|
||
### System UI integration
|
||
|
||
4. **Notification**: Full **MediaSession** (lockscreen, BT headsets, Wear) vs minimal ongoing notification only?
|
||
5. **Widgets / Wear**: Explicitly **out of scope** for MVP?
|
||
|
||
### Onboarding and permissions
|
||
|
||
6. **First-run flow**: Grant notifications, pick folder, sign-in to Spotify (if required by jukebox), battery optimization warning — **single wizard** or inline prompts?
|
||
7. **Empty states**: No MP3s mapped, no Spotify login, no BPM in DB — copy and primary actions?
|
||
|
||
### Visual design
|
||
|
||
8. **Design system**: Material 3 / dynamic color vs fixed brand palette for sports contexts (high contrast sunlight)?
|
||
9. **Accessibility**: Minimum targets for sweaty fingers, **TalkBack** labeling for pace and controls?
|
||
|
||
### Feedback and failure modes
|
||
|
||
10. **When pace detection is noisy**: Show confidence, freeze adaptation, or fallback to **nominal BPM**?
|
||
11. **Errors**: Missing file, codec failure, JNI assertion — **blocking dialog** vs toast + safe stop?
|
||
|
||
---
|
||
|
||
## Suggested order to resolve
|
||
|
||
```mermaid
|
||
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
|
||
```
|
||
|
||
1. **JNI + libpasada states** — frozen (see **libpasada state machine** section).
|
||
2. **MP3↔metadata matching** — **embedded tags first**, then **manual link** — decided.
|
||
3. Lock **screen map** + **notification depth**.
|
||
4. 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).
|