# 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 fun observeTrip(id: Long): Flow fun observeCompletedTrips(): Flow> 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).