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>
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
- Create
data/TripRepository.ktwith the interface above. - Implement each transition as a single Room
@Transactionso a crash mid-transition cannot leave a trip with two open segments. - Guard each transition on current state; log and no-op on nonsensical transitions.
- Delete
RecordingStatefromTelemetry.kt. - Relocate upload-error state so the Record screen keeps its indicator.
- Update
TelemetryUploaderandTrackingServicereferences.
Acceptance criteria
RecordingStateno longer existsobserveActiveTrip()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 TelemetryTeststill 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
observeActiveTripemits 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).