Two copies for two jobs. rippr-src/ is a browsable git archive export of the tracked tree at 2f76983 - no build outputs, no local.properties, no nested .git - which is convenient to read in gitea but carries no history and will drift. rippr-full-history.bundle is the real backup: all 18 commits, verified as "records a complete history" and test-cloned before committing. This matters because ~/dojo/rippr has no git remote and otherwise exists only on one machine. rippr-src/SNAPSHOT.md explains the difference and how to restore. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
115 lines
4.6 KiB
Markdown
115 lines
4.6 KiB
Markdown
# T09 — Record screen v2
|
||
|
||
**Phase** 4 · **Depends on** T06, T08 · **Status** Done
|
||
|
||
## Goal
|
||
|
||
Add Pause/Resume, Stop, and Discard to the recording screen while keeping the clean
|
||
numeric readout intact, and show per-trip rather than lifetime stats. Done when a ride
|
||
can be fully controlled from the screen and the numbers reflect the current trip only.
|
||
|
||
## Context
|
||
|
||
Direct feedback: *"app is simple and clean. I like that but it's missing stuff."* The
|
||
missing stuff is controls, not decoration. **Resist adding visual weight.**
|
||
|
||
v1's screen shows `MAX SPEED` as a large monospace figure, then a card with points
|
||
captured, avg speed, duration, and pending upload, then one full-width button. That
|
||
layout stays; it gains controls and loses its lifetime scope.
|
||
|
||
The current single button toggles start/stop via `RecordingState.isRecording`. v2 has
|
||
three states and needs a different control arrangement.
|
||
|
||
## Design
|
||
|
||
### Control layout by state
|
||
|
||
| State | Primary | Secondary |
|
||
|---|---|---|
|
||
| Idle | **START RECORDING** (full width) | — |
|
||
|
||
Service actions to dispatch: `TrackingService.ACTION_START` / `ACTION_PAUSE` /
|
||
`ACTION_RESUME` / `ACTION_STOP` / `ACTION_DISCARD`. Only START uses
|
||
`startForegroundService`; the rest are plain `startService` on the running service.
|
||
| Recording | **PAUSE** | STOP |
|
||
| Paused | **RESUME** | STOP · DISCARD |
|
||
|
||
Discard appears **only when paused** — deliberately. Offering a destructive action next
|
||
to Pause during an active ride invites a gloved mis-tap at 100 km/h. Pausing first is a
|
||
natural speed bump.
|
||
|
||
Discard shows a confirmation dialog stating what is lost ("Delete this ride? N points
|
||
recorded over X km will be permanently deleted."). Stop and Pause need no confirmation.
|
||
|
||
### Replacing T02 scaffolding
|
||
|
||
`MainActivity` derives stats with `flatMapLatest` over `TripRepository.observeActiveTrip()`
|
||
into `observeTripStats(tripId)`, and `isRecording` from `observeActiveTrip().map { it != null }`.
|
||
Upload errors come from the `UploadStatus` object. All of that moves into `RecordViewModel`,
|
||
and once T07 lands the screen reads persisted aggregates off the `Trip` row rather than
|
||
recomputing in SQL.
|
||
|
||
`onToggleClicked` is now a `lifecycleScope.launch` that queries `trips.activeTrip()` — there
|
||
is no synchronous recording flag any more. The ViewModel should expose state instead so the
|
||
screen never has to suspend to decide what a button does.
|
||
|
||
### Stats become per-trip
|
||
|
||
Driven by `observeActiveTrip()` from T03, showing the live aggregates from T07:
|
||
|
||
- Max speed (the big figure — it is the number Dylan cares about)
|
||
- Distance — **new in v2**, read straight off `Trip.distanceM` (no SQL recomputation)
|
||
- Duration, moving time
|
||
- Points captured
|
||
- Pending upload (only when non-zero, and only if an endpoint is configured)
|
||
|
||
When idle, the screen shows a resting state rather than stale numbers from the last ride.
|
||
|
||
## Implementation
|
||
|
||
1. `RecordViewModel` exposes `RecordUiState` derived from `observeActiveTrip()`.
|
||
2. Map `TripState` to the control layout above.
|
||
3. Dispatch service intents for each action.
|
||
4. Add `ConfirmDialog` for Discard.
|
||
5. Reuse `Telemetry.formatDuration` for both duration fields.
|
||
6. Keep the battery-optimisation prompt.
|
||
7. Add a top-bar action to reach the Trips list.
|
||
|
||
## Acceptance criteria
|
||
|
||
- [x] All five transitions work from the UI
|
||
- [x] Discard is unavailable while actively recording
|
||
- [x] Discard confirms, and states what will be lost
|
||
- [ ] Stats reset between trips — no bleed from the previous ride
|
||
- [x] Distance appears and increases during a ride
|
||
- [x] Idle state is visibly distinct from a zeroed ride
|
||
- [ ] Screen still reads at a glance; no added clutter
|
||
|
||
> **Not verified.** The unchecked items above, and the Compose UI tests below, were
|
||
> not done. Verification for T08–T11 was manual on the emulator (screenshots through
|
||
> the full flow) plus the existing unit and instrumented suites. Compose UI tests are
|
||
> outstanding — tracked in T18.
|
||
|
||
## Tests
|
||
|
||
Compose UI tests:
|
||
- Each state renders the right controls
|
||
- Discard dialog appears, cancel is non-destructive
|
||
- State survives rotation mid-ride
|
||
|
||
Emulator end-to-end: drive the full lifecycle through taps, then verify the database
|
||
matches what the screen claimed.
|
||
|
||
## Risks / gotchas
|
||
|
||
- **Tap targets.** These get used in gloves. Keep the 64–72dp button height v1 uses.
|
||
- **Wait for the UI before tapping in tests.** v1's cold start took 8.7 s; a fixed sleep
|
||
produced a silently-missed tap and a false "service didn't start" conclusion. Poll
|
||
`uiautomator dump` for the button text instead.
|
||
- **Do not show "Pending upload" when no endpoint is configured** — it reads as an error
|
||
when it is just an unused feature.
|
||
|
||
## Out of scope
|
||
|
||
Trips list (T10), any map, live path preview.
|