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>
113 lines
4.7 KiB
Markdown
113 lines
4.7 KiB
Markdown
# T03 — Trip repository + reactive state
|
|
|
|
**Phase** 1 · **Depends on** T02 · **Status** Done
|
|
|
|
## Goal
|
|
|
|
Own trip lifecycle transitions in one place and make recording state a function of the
|
|
database rather than an in-memory flag. Done when `RecordingState` is deleted, the UI
|
|
observes trip state through a Flow, and killing the app process no longer loses track of
|
|
whether a ride is in progress.
|
|
|
|
## State as of T02
|
|
|
|
`TripDao`/`SegmentDao` already expose every query this task needs (`observeActive`, `getActive`, `openSegment`, `close`, `setState`, `deleteById`).
|
|
|
|
T02 left **temporary scaffolding** that this task and T06 replace:
|
|
`TrackingService.openTripAndSegment()` opens a trip and segment inline via the DAOs, and
|
|
`@Volatile currentTripId/currentSegmentId` are read by the location callback. The callback
|
|
drops fixes while those are 0 so a point can never violate the FK — **preserve that guard**.
|
|
|
|
## Context
|
|
|
|
v1 keeps state in a process-wide singleton in
|
|
`app/src/main/java/com/rippr/Telemetry.kt`:
|
|
|
|
```kotlin
|
|
object RecordingState {
|
|
private val _isRecording = MutableStateFlow(false)
|
|
…
|
|
}
|
|
```
|
|
|
|
This was already an improvement over the Activity-local `isRecording` the original spec
|
|
used, but it still lies after process death: `START_STICKY` restarts the service with a
|
|
null intent and the flag resets to `false` while a ride is genuinely underway.
|
|
|
|
The fix is that the database already knows. A row in `trips` with `endedAt IS NULL` *is*
|
|
the fact of an in-progress ride, and it survives anything short of uninstall.
|
|
|
|
**Keep the rest of `Telemetry.kt` exactly as it is.** `msToKmh`, `sanitizeSpeedKmh`,
|
|
`isUsableFix`, `formatDuration`, and `encodeBatch` are pure, already unit-tested by
|
|
`TelemetryTest`, and have no reason to change. Only `RecordingState` goes.
|
|
|
|
`RecordingState.lastUploadError` is also used by `TelemetryUploader` and surfaced on the
|
|
Record screen — that one is genuinely ephemeral UI state and can stay as a small
|
|
`UploadStatus` object, or move onto the repository. Do not silently drop it.
|
|
|
|
## Design
|
|
|
|
```kotlin
|
|
class TripRepository(
|
|
private val tripDao: TripDao,
|
|
private val segmentDao: SegmentDao,
|
|
private val pointDao: TrackPointDao,
|
|
) {
|
|
fun observeActiveTrip(): Flow<Trip?>
|
|
fun observeTrip(id: Long): Flow<Trip?>
|
|
fun observeCompletedTrips(): Flow<List<Trip>>
|
|
|
|
suspend fun startTrip(now: Long): Trip // creates trip + first segment
|
|
suspend fun pauseTrip(now: Long) // closes open segment, state=PAUSED
|
|
suspend fun resumeTrip(now: Long) // opens new segment, state=RECORDING
|
|
suspend fun completeTrip(now: Long) // closes segment, endedAt, COMPLETED
|
|
suspend fun discardTrip() // deletes active trip (CASCADE)
|
|
}
|
|
```
|
|
|
|
Every transition is idempotent and safe to call from an unexpected state — the service
|
|
can be restarted by the OS at any moment, so `resumeTrip` on an already-recording trip
|
|
must be a no-op rather than opening a duplicate segment.
|
|
|
|
**Single instance.** v1 uses a `@Volatile` double-checked singleton for `AppDatabase`;
|
|
follow the same pattern for the repository rather than introducing a DI framework. The
|
|
app is small and Hilt would be more ceremony than it earns.
|
|
|
|
## Implementation
|
|
|
|
1. Create `data/TripRepository.kt` with the interface above.
|
|
2. Implement each transition as a single Room `@Transaction` so a crash mid-transition
|
|
cannot leave a trip with two open segments.
|
|
3. Guard each transition on current state; log and no-op on nonsensical transitions.
|
|
4. Delete `RecordingState` from `Telemetry.kt`.
|
|
5. Relocate upload-error state so the Record screen keeps its indicator.
|
|
6. Update `TelemetryUploader` and `TrackingService` references.
|
|
|
|
## Acceptance criteria
|
|
|
|
- [x] `RecordingState` no longer exists
|
|
- [x] `observeActiveTrip()` reflects reality after a simulated process death
|
|
- [x] Double `startTrip()` adopts rather than duplicating
|
|
- [x] `resumeTrip()` while already RECORDING is a no-op
|
|
- [x] Upload-error surfacing still works, now via `UploadStatus`
|
|
- [x] `TelemetryTest` still green (the pure functions are untouched)
|
|
|
|
## Tests
|
|
|
|
Instrumented, against a real in-memory database:
|
|
- start → pause → resume → complete produces exactly two segments
|
|
- discard removes the trip and all its points
|
|
- transitions from wrong states are no-ops
|
|
- `observeActiveTrip` emits null after complete
|
|
|
|
## Risks / gotchas
|
|
|
|
- **Transitions race the writer loop.** The service writes points continuously; pausing
|
|
mid-flush must not orphan points into a closed segment. T06 handles ordering — pause
|
|
drains the channel *before* closing the segment.
|
|
- **Do not delete the pure Telemetry functions.** They are load-bearing and tested.
|
|
|
|
## Out of scope
|
|
|
|
Wiring transitions to service actions (T06) and aggregate maths (T07).
|