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).
This commit is contained in:
94
rippr-flutter-src/docs/v3/V3-06-notification-stats.md
Normal file
94
rippr-flutter-src/docs/v3/V3-06-notification-stats.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# 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](../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.
|
||||
Reference in New Issue
Block a user