Files
samplez/rippr-flutter-src/docs/v3/V3-06-notification-stats.md
uhryniuk 4c634354bd Refresh the Flutter snapshot: v3 tickets V3-04 through V3-16, plus a fresh installable APK
V3-04 through V3-07, V3-10, V3-16 shipped complete; V3-11/V3-12/V3-14 shipped code-complete pending device/account verification; V3-08/V3-09 deferred behind a new V3-17 (self-hosted OSRM investigation). 316 tests passing, up from 221.

The APK is a fresh release build (debug-signed, no release signing config exists yet) with two build fixes applied: core library desugaring enabled for flutter_local_notifications, and sentry_flutter bumped to 9.27.0 (8.14.2's bundled Kotlin plugin was incompatible with this project's Kotlin 2.4.0 toolchain).
2026-08-19 13:36:04 -05:00

5.0 KiB

V3-06 — Live stats in the notification

Phase Live map · Depends on nothing · Size S · Status Done

Goal

Distance and duration readable from the notification shade without unlocking.

Context

The ongoing notification currently says "Rippr is recording / Tracking your ride" — static text. During a pocketed ride that is a wasted surface.

Constraint that shapes this whole ticket: the notification is owned by geolocator's ForegroundNotificationConfig, which takes fixed strings at stream-subscription time and offers no update path and no actions. That is the parity gap recorded in ../port/PARITY-AUDIT.md.

Design

Two honest options:

A. Restart the position stream with new text. Cheap to write, but it tears down and re-establishes location updates every time — unacceptable during recording.

B. Take the notification back with flutter_local_notifications, and let geolocator raise a silent minimal one. More moving parts, but it also restores Pause/Resume actions — closing the one capability lost in the port.

Recommend B, precisely because it buys back the parity gap as well.

Implementation

  1. Add flutter_local_notifications
  2. Own a notification on the same channel, updated on each writer flush (~2 s), not per fix
  3. Pause/Resume actions routed back into RecordingEngine
  4. Android 13+ notification permission is already requested

Acceptance criteria

  • Distance and elapsed update while riding, without unlocking
  • Pause and Resume work from the shade
  • Location updates are not interrupted when the notification changes
  • The notification cannot be swiped away mid-ride

Tests

  • Unit: notification text formatting from a trip
  • Integration on a device: start, confirm the text advances, pause from the shade, confirm the engine actually paused (check the database, do not trust the UI)

Risks

  • Two notification sources fighting is the obvious failure. Verify only one is visible.
  • An update per fix would be a battery and jank problem; drive it from the flush.

Out of scope

iOS. There is no equivalent live notification surface; a Live Activity is a much larger piece of work and belongs in its own ticket.

Outcome

Shipped as designed (option B), with one honestly-unresolved risk carried forward.

RideNotificationController is a seam over flutter_local_notifications (FakeRideNotificationController for tests), the same shape as LocationSource and WakelockController. RideNotificationCoordinator owns the wiring: it subscribes to TripRepository.watchActiveTrip() (the same stream activeTripProvider exposes, updated once per writer flush) and calls show/cancel; it subscribes to the controller's action stream and routes pause/resume back into RecordingEngine, guarded so a Resume can't fire against a trip that isn't actually paused. rideNotificationText(Trip, {unit}) is pure and unit-tested directly — distance/elapsed formatting, the Paused · prefix, and unit-system handling.

Not screen-owned, deliberately. Unlike the live map (V3-04) and mounted mode (V3-05), which are fed from RecordScreen's widget tree, the coordinator is instantiated eagerly from main.dart's RipprApp.build via a bare ref.watch(rideNotificationCoordinatorProvider) — a pocketed ride has no visible widget tree, but the notification and Pause/Resume both still have to work.

Unresolved risk, flagged rather than papered over: the ticket's own risk section names "two notification sources fighting" as the obvious failure mode, and it is real. geolocator's ForegroundNotificationConfig is what satisfies Android's foreground-service requirement and cannot be suppressed; flutter_local_notifications raises a second, independent notification. There is no documented way to merge or guarantee only one is visible — the geolocator notification was made minimal and silent (geolocator_location_source.dart) on the assumption that an ongoing: true notification on the same-ish surface might collapse or de-prioritise it, but that assumption is unverified without a device. This is exactly the kind of claim the project's testing philosophy refuses to accept on faith — see the real-ride checklist (V3-13) and this ticket's own "Integration on a device" test, neither of which could run here.

6 new tests, all in test/ride_notification_test.dart: three for rideNotificationText (recording, paused-prefix, unit system), three for the coordinator using a real RecordingEngine + in-memory TripRepository (not a mock) so pause/resume are checked in the database per the project's standing rule, not by trusting the notification state. flutter analyze clean; full suite green (237 tests, up from 231).

Not attempted: the device-only acceptance criteria (text updates without unlocking, Pause/Resume from the shade, the notification resisting swipe-away, and the two-source visibility question above) — all require a real Android device, per the ticket's own Tests section.