# 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**. ## 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).