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**.
└── Beats/ ← canonical beats per playlist (TYPE_BEATS)
└── Running Mix/
├── 001.json
└── 002.json
```
Each type is recorded in Room `file_metadata` (`FileMetadataEntity`) with `fileUri`, `trackId`, `type`, `version`, `synced`, and `lastSyncedAt` (epoch millis).
| Type | Constant | Written by | Path pattern | Server sync |
| **Annotation session** | `TYPE_ANNOTATION` | Annotation screen (`BeatAnnotationStorage`) | `Documents/Lockstep/{sessionFolder}/{playlist}_{NNN}.json` — one timestamped folder per annotation session | Yes — uploaded via `POST /metadata` when signed in |
| **Run collection** | `TYPE_COLLECTION` | Now Playing with collect-run-data enabled (`RunDataStorage`) | `Documents/Lockstep/{sessionFolder}/{playlist}_{NNN}.json` — one timestamped folder per run session | Yes |
| **Canonical beats** | `TYPE_BEATS` | Annotation screen (`BeatAnnotationStorage`) — written alongside each session annotation | `Documents/Lockstep/Beats/{playlist_name}/{NNN}.json` — one folder per playlist; **overwrites** on re-annotation | **No** — tracked locally only; `synced = 1` and `lastSyncedAt` set at write time |
During an **annotation session**, leaving a track writes **both**`TYPE_ANNOTATION` (append-only session archive) and `TYPE_BEATS` (latest beats for that playlist slot).
### Beat JSON schema (`TYPE_ANNOTATION` and `TYPE_BEATS`)
Same schema for session annotations and canonical beats:
```json
{
"contentId": "primary:Documents/Music/track.mp3",
"title": "Track Title",
"artist": "Artist Name",
"beatTimesSec": [1.23, 2.45, 3.67]
}
```
- **`contentId`** — SAF document id of the paired local MP3 when available; otherwise Spotify track id.
- **`beatTimesSec`** — user-tapped beat times in seconds (playback position).
| 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`**. |
- **`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()`**).
- **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**?
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).