# T06 — Trip lifecycle in TrackingService **Phase** 3 · **Depends on** T03 · **Status** Done ## Goal Teach the foreground service the full ride lifecycle — start, pause, resume, stop, discard — with segments opening and closing correctly and the notification reflecting actual state. Done when a paused ride keeps its notification, stops consuming GPS, and resumes into a *new* segment. ## Context `app/src/main/java/com/rippr/TrackingService.kt` (265 lines) already gets the hard parts right, and **none of this architecture should be disturbed**: - Location callbacks `trySend` into an **unbounded `Channel`**; a single writer coroutine drains it in batches of 25 every 2 s. This is why slow disk I/O can never block the GPS thread, and why no fix is dropped under back pressure. - `onDestroy` drains whatever remains on a scope that *outlives* `serviceScope`, so the final points still land. Verified on the emulator: 81 points, ids 1..81 contiguous. - A `PARTIAL_WAKE_LOCK` keeps the writer running with the screen off — a foreground service alone does not guarantee this on all OEMs. - `startForegroundCompat()` runs before anything else, because Android allows roughly five seconds from `startForegroundService()` before killing the process. What changes is lifecycle, not plumbing. ### State after T03 The repository is already wired in: the service holds `TripRepository`, start calls `trips.startTrip()`, and `stopRecording()` calls `trips.completeTrip()` on a survivor scope. `@Volatile currentTripId`/`currentSegmentId` remain (the location callback runs on another thread) and are now populated from the returned `TripHandle`. Still to do here: - The five actions; today there is only start and `ACTION_STOP` - `drainChannel()` and drain-before-close ordering - Pause/resume, wake-lock release on pause, per-state notification - The null-intent restart path (`trips.activeTrip()` already provides what it needs) - The `tripId == 0L` guard in the location callback — **keep it** **Note on force-stop.** A user-initiated `am force-stop` is more aggressive than an OS kill: Android will not honour START_STICKY afterwards. The database state stays correct and the UI reads it correctly, but recording does not auto-resume. The restart path here covers OS-initiated kills, which is the case that matters on a long ride. ## Design ### Actions | Action | Behaviour | |---|---| | `ACTION_START` | `repo.startTrip()`, acquire wake lock, request location updates | | `ACTION_PAUSE` | **Drain channel first**, close segment, `removeLocationUpdates`, release wake lock, keep service alive | | `ACTION_RESUME` | `repo.resumeTrip()` (new segment), re-acquire wake lock, re-request updates | | `ACTION_STOP` | Drain, close segment, `completeTrip()`, recompute aggregates (T07), stop service | | `ACTION_DISCARD` | Drain and discard buffered points, `discardTrip()` (CASCADE), stop service | **Correction to the original reasoning.** This doc previously claimed that closing a segment before draining would write points into the *wrong* segment. That is wrong: `segmentId` is stamped onto each `TrackPoint` at creation in the location callback, so a fix already queued always lands in the segment it was recorded during, whatever order the writes happen in. Draining before closing still matters, just for different reasons: points should not sit unwritten while a ride idles at a pause, and stop/discard must not lose the tail. The order used is stop-the-source → drain → close. ### Pause keeps the service, drops the wake lock The foreground service stays alive so the notification persists and resuming is instant. But with no location updates arriving there is nothing for the writer to do, so the wake lock is released — a paused ride at a long lunch stop should not hold the CPU awake. ### Notification states | State | Title | Text | Actions | |---|---|---|---| | Recording | Rippr Active | Recording ride telemetry… | Pause, Stop | | Paused | Rippr Paused | Ride paused — not recording | Resume, Stop | `NotificationCompat.Builder` is rebuilt and re-notified on transition; the channel and id (101) stay as they are. The small icon stays `R.drawable.ic_stat_rippr`. ### Restart after process death `START_STICKY` redelivers a null intent. On a null intent the service asks the repository for the active trip: - **RECORDING** → resume recording into a new segment (the gap is real; the phone was off) - **PAUSED** → restore the paused notification, do not request location - **none** → `stopSelf()` This is only possible because T03 moved state into the database. ## Implementation 1. Replace the single `ACTION_STOP` constant with the five actions. 2. Inject `TripRepository`; drop all `RecordingState` references. 3. Add `drainChannel()` — a suspend function that empties the channel into the DB and returns once flushed; reuse it from pause, stop, discard, and `onDestroy`. 4. Split wake lock acquire/release out of `onStartCommand` so pause/resume can call them. 5. Rebuild the notification per state; add `buildNotification(state: TripState)`. 6. Handle the null-intent restart path. 7. `stopWithTask="false"` in the manifest already survives task swipe — keep it. ## Acceptance criteria - [x] Start → pause → resume → stop produces exactly two segments - [x] No point is written into a closed segment - [x] Paused: notification persists, no location updates, wake lock released - [x] Discard deletes the trip and every one of its points - [x] Force-stop then restart with an active RECORDING trip resumes into a new segment - [x] Contiguous point ids across a pause (no gaps, no duplicates) ## Tests Instrumented plus emulator end-to-end: - Feed fixes, pause, feed more, resume, feed more, stop → assert two segments with the right point counts either side - Assert no point has a `segmentId` whose segment closed before its timestamp - Discard leaves zero rows Emulator harness (proven in v1): ``` adb shell am start-foreground-service -n com.rippr/.TrackingService # blocked: not exported ``` Drive through the UI instead — `adb shell input tap`, after waiting for the UI to actually exist. A fixed `sleep` is not enough; the activity took 8.7 s to display on a cold start during v1 testing and a premature tap silently does nothing. ## Risks / gotchas - **Do not replace the Channel with a direct write.** It is the reason the GPS callback never blocks. - **Drain ordering** as described above; get this wrong and points land in the wrong segment, which only shows up later as a rendering artefact. - **Wake lock double-release** throws. `setReferenceCounted(false)` is already set; still guard with `isHeld`. - **`onDestroy` already spawns a survivor scope** for the final flush — do not remove it while refactoring. ## Out of scope Aggregate maths (T07) and any UI (T09).