Files
samplez/rippr-src/docs/v2/03-repository.md
uhryniuk 280fd7f988 Add Rippr source snapshot and full-history bundle
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>
2026-08-11 08:30:25 -05:00

4.7 KiB

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:

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

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

  • RecordingState no longer exists
  • observeActiveTrip() reflects reality after a simulated process death
  • Double startTrip() adopts rather than duplicating
  • resumeTrip() while already RECORDING is a no-op
  • Upload-error surfacing still works, now via UploadStatus
  • 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).