From 3c801e9114866235224b8152e44b0979f168db35 Mon Sep 17 00:00:00 2001 From: Dylan Date: Sat, 15 Aug 2026 22:38:02 -0500 Subject: [PATCH] Refresh the native Rippr snapshot to match the claimed commit RIPPR.md said the native snapshot was at ba57a92, but rippr-src and the bundle were still at d418920 -- so the superseded notice and the two recorded bugs were missing from the copy. Re-exported both. Co-Authored-By: Claude Opus 5 --- rippr-full-history.bundle | Bin 502767 -> 520297 bytes rippr-src/README.md | 30 +++- rippr-src/docs/port/PLAN.md | 349 ++++++++++++++++++++++++++++++++++++ 3 files changed, 378 insertions(+), 1 deletion(-) create mode 100644 rippr-src/docs/port/PLAN.md diff --git a/rippr-full-history.bundle b/rippr-full-history.bundle index 2112fd0a19aea6c4b744b0cde3505c36f028e3ee..39ed9383c4a8254720347e7c8050189039e3fde5 100644 GIT binary patch delta 22017 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 bs|O<#$>7MLh?hlsnM36{<0Q2@7QCs$BM+AI delta 4426 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 ZGSobH?8(0=LJG2Sw3qVr&D?Qe{vVJRCVv0` diff --git a/rippr-src/README.md b/rippr-src/README.md index bd49dfd..ed656b7 100644 --- a/rippr-src/README.md +++ b/rippr-src/README.md @@ -1,4 +1,32 @@ -# Rippr +# Rippr — native Android (superseded) + +> ## ⚠ This app has been ported to Flutter +> +> Active development moved to **`~/dojo/rippr-flutter`**, which runs on Android *and* +> iOS. That port is feature-complete and better tested than this app; see its +> `docs/port/PARITY-AUDIT.md` for a row-by-row comparison. +> +> **This repository is kept as a reference and a fallback**, not archived. It is still the +> only version that has recorded real rides, and the port's validation depends on running +> both apps side by side — they use different application ids (`com.rippr` here, +> `com.rippr.port` there) specifically so they can be installed together. +> +> **Two bugs in this code were found by porting it.** Both are fixed in the port and +> still present here: +> +> 1. **`TrackingService.restoreAfterProcessDeath` measures the dead time as distance.** +> Its comment says it resumes into a new segment; it calls `resumeTrip`, which adopts +> the segment a crash left open. `RideStatistics.compute` then measures straight +> through the gap — reproduced at **111 km** of phantom distance. See +> `TripRepository.resumeIntoNewSegment` in the port. +> 2. **The elevation regression guard passes on seed luck.** On a fixture shared between +> both languages the algorithm yields ~39 m, which would fail this repo's own 35 m +> bound in `RideStatisticsTest`. It passes only because `kotlin.random.Random(42)` +> draws a benign sequence. +> +> Everything below describes this app as it stands, and remains accurate. + +--- A native Android GPS ride recorder for motorcycles. Records telemetry from an un-killable foreground service into a local SQLite database, renders the traversed path diff --git a/rippr-src/docs/port/PLAN.md b/rippr-src/docs/port/PLAN.md new file mode 100644 index 0000000..c76c7bc --- /dev/null +++ b/rippr-src/docs/port/PLAN.md @@ -0,0 +1,349 @@ +# Rippr — Flutter Port (Android + iOS) + +## Context + +Rippr is a native Android GPS ride recorder: ~3,800 lines of Kotlin across 34 files, +shipped through v1 and v2.0.1, validated on real rides. It records telemetry from a +foreground service into Room, renders the path on osmdroid, and exports GPX/GeoJSON. + +Dylan wants one codebase running on **both iOS and Android** before any further features +are built. This is a **full rewrite in Flutter**, not an incremental or add-to-app +migration — the research in `docs/PORT_RESEARCH.md` puts the cutover threshold at 10 +screens and Rippr has **six**, with no legacy debt worth preserving in Kotlin form. + +**The goal is parity, not improvement.** Every capability in the v2.0.1 feature table +works on both platforms; v3 features stay in the backlog. Deliberate scope discipline — +the port is finished when it does exactly what the Kotlin app does. + +### Three findings that shape this plan + +**The pure-logic core ports almost verbatim.** Eight files (~890 lines) carry zero Android +imports — a discipline held deliberately since v1. `Geo`, `RideStatistics`, +`RideAccumulator`, `RideExport`, `Telemetry`, `Format`, `LiveTelemetry`, `UploadStatus`. +Their ~965 lines of existing JVM tests become a **differential oracle**: the same fixtures +must produce the same numbers in Dart. This is the single biggest de-risker available and +Phase 1 exists to cash it in first. + +**Whoever owns the writes must own the database.** `TrackingService` writes to Room +directly from its writer loop. If Dart owns the schema but a native service owns the +writes, every fix crosses a platform channel that is *dead while iOS suspends Dart*. So +Dart owns both, and the background layer only has to keep the Dart isolate alive. + +**iOS suspends a stationary app; Android does not.** This is the one place where "write +once" is partly an illusion, and it gets its own task and its own real-world validation +rather than being discovered late. + +### Decisions taken + +| Decision | Choice | Rationale | +|---|---|---| +| Strategy | **Full rewrite** | 6 screens, well below the 10-screen threshold | +| Repo | **`~/dojo/rippr-flutter`**, new git history | Native app stays installable as reference and fallback | +| Existing rides | **Start fresh** | No importer; export to GPX first if any ride matters | +| Location engine | **`geolocator` + `flutter_foreground_task`** | MIT/free; avoids the ~$500/yr Transistor licence | +| Isolate model | **Single isolate** | Foreground service for process liveness only — no second isolate, no cross-isolate SQLite | +| Database | **Drift** | Type-safe, streams, real migrations, runs on the Dart VM | +| State | **Riverpod** | Maps cleanly from ViewModel + StateFlow | +| Routing | **go_router** | Three destinations, same shape as navigation-compose | +| Map | **flutter_map** | OSM raster tiles, no API key — same reasoning that chose osmdroid | +| Live map while recording | **Still no** | Parity target is v2.0.1; it is a v3 decision | + +### Non-goals + +Every v3 backlog item: live map, waypointing, activity type, accounts, cloud backup, +theming overhaul, group ride. Also no new features of any kind, and no data importer. + +--- + +## Architecture mapping + +``` +TrackingService (Kotlin) RecordingEngine (Dart, main isolate) + FusedLocationProviderClient ──► geolocator.getPositionStream + Channel(UNLIMITED) ──► StreamController (unbounded) + single writer coroutine ──► single async drain loop (batch 25 / 2s) + Mutex ──► package:synchronized Lock + Room + @Transaction ──► Drift + transaction() + Flow ──► Stream (Drift .watch) + PARTIAL_WAKE_LOCK ──► flutter_foreground_task (Android) + START_STICKY + adopt active ──► on-launch adopt of `endedAt IS NULL` + +ViewModel + StateFlow ──► Riverpod Notifier / AsyncNotifier +navigation-compose ──► go_router +osmdroid MapView ──► flutter_map +FileProvider + ACTION_SEND ──► share_plus +SharedPreferences ──► shared_preferences +OkHttp ──► package:http +``` + +**Platform-divergent, by necessity:** Android runs a foreground service with a persistent +notification to keep the process alive. iOS declares `UIBackgroundModes: location` and +sets `allowsBackgroundLocationUpdates` with `pauseLocationUpdatesAutomatically = false`. +Both keep the *same* Dart pipeline running; only the liveness mechanism differs. + +**Invariants carried over verbatim** (from `docs/v3/BACKLOG.md` §6 — each has a comment in +the Kotlin explaining why, and each must survive the port): + +1. The location callback never blocks on disk — unbounded buffer, single batched writer +2. Points stamped with `tripId`/`segmentId` **at creation**, never looked up at write time +3. Recording state derived from the database, never an in-memory flag +4. No destructive migration — Drift migrations from v1 of the Dart schema onward +5. Decimation is render-only, never reaching storage or export +6. Theme sets a default content colour (Kotlin's `Surface` lesson; Dart's is `DefaultTextStyle`) +7. The map zoom clamp — a short ride must not zoom past the tile server's max +8. Tests use in-memory databases + +--- + +## Phases and tasks + +Each task gets `docs/port/NN-slug.md` in the new repo, following the v2 template that +worked well: Goal · Context · Design · Implementation · Acceptance criteria · Tests · +Risks · Out of scope. Plus a running `docs/port/PROGRESS.md` recording what actually went +wrong — the v2 feedback loop caught real bugs and is worth repeating. + +### Phase 0 — Ground clearing *(blocking; nothing else can start)* + +| # | Task | Depends | +|---|---|---| +| T00 | Disk space + toolchain | — | +| T01 | Repo scaffold | T00 | + +**T00 — This is a genuine blocker, not a formality.** The machine has **16 GiB free at 92% +capacity** and needs, roughly: Flutter SDK + artifacts ~5 GB, an iOS simulator runtime +(**none installed**) ~9 GB, CocoaPods (**not installed**; system Ruby is 2.6.10, so install +via Homebrew, not `gem`), plus the Android emulator's non-negotiable **7.4 GB free-space +floor** and two build trees. That does not fit — v1 hit this same wall and lost real time +to it. Reclaim first (`brew cleanup -s`, Homebrew and Playwright caches, `pip cache purge`, +`go clean -cache`, old Gradle caches, stale AVDs), then install. Xcode 26.0.1 is present. +**Exit criteria:** `flutter doctor -v` clean for both toolchains, an iOS simulator *and* +the Android emulator each boot, and ≥15 GB still free afterwards. + +**T01** — `flutter create` with both platforms, bundle/application id `com.rippr`, git +init, initial commit. Add the dependency set. Write `docs/port/` scaffolding and a +`README` pointing back at the native repo's `ARCHITECTURE.md`, `TESTING.md`, and v2 +`PROGRESS.md` as the source of truth for *why* things are shaped as they are. Confirm +`flutter test` and a debug build on both platforms before a line of real code. + +### Phase 1 — Pure logic *(no platform, no UI, no database)* + +Highest value per unit risk, and it builds Dart fluency on code whose correct answers are +already known. Each task ports the Kotlin file **and its existing test suite**. + +| # | Task | Ports | Depends | +|---|---|---|---| +| T02 | Geo utilities | `geo/Geo.kt` + `GeoTest` (165 + 210 ln) | T01 | +| T03 | Telemetry + formatting | `Telemetry.kt`, `ui/Format.kt`, `UploadStatus.kt` + `TelemetryTest` | T01 | +| T04 | Ride statistics | `stats/RideStatistics.kt` + `RideStatisticsTest` (302 + 265 ln) | T02, T03 | +| T05 | Live accumulator | `RideAccumulator.kt`, `LiveTelemetry.kt` + `AccumulatorTest` | T04 | +| T06 | Export writers | `export/RideExport.kt` + `RideExportTest` (151 + 211 ln) | T02 | +| T07 | Cross-language parity harness | — | T02–T06 | + +**T04 is the hardest-won code in the project.** `ElevationAccumulator` — 15-sample moving +average, reversal hysteresis, `gainIncludingPending()`, and a `finish()` that reconciles +against `lastRaw`. A naive version once reported **1498 m of climbing over a parked bike**. +Port it structurally faithfully; do not "improve" it during translation. + +**T07** — Drive identical fixtures through both implementations and assert agreement to +six decimal places: haversine over known pairs, Douglas–Peucker output, elevation gain over +the noisy-stationary fixture, batched-vs-single-batch distance, GPX/GeoJSON byte output. +**Watch for a harness that reports suspiciously identical results** — a v2 sweep returned +four identical values because a quoting bug corrupted the source while the compile error +hid behind `/dev/null`. Never redirect a build to `/dev/null` inside a measurement loop. + +### Phase 2 — Data layer + +| # | Task | Depends | +|---|---|---| +| T08 | Drift schema | T01 | +| T09 | Trip repository | T08, T05 | + +**T08** — Mirror `app/schemas/com.rippr.data.AppDatabase/2.json`: `Trip` (with +`TripState` RECORDING/PAUSED/COMPLETED), `Segment`, `TrackPoint`; FK `CASCADE`, indices on +`tripId`/`segmentId`, WAL. Starts at Dart schema version 1 with real migrations from day +one — the destructive fallback never comes back. + +**T09** — Port `TripRepository`: every transition idempotent inside a transaction, +`startTrip` **adopts** an active trip rather than duplicating one, `mergeTrips` +re-parents segments and points without ever joining segments, then recomputes aggregates. + +**A quiet win here:** `TripRepositoryTest`, `SchemaTest`, and `MergeTest` (666 lines) are +*instrumented* tests today, needing a device. Against Drift on the Dart VM they become +plain unit tests — faster, and runnable without an emulator booted. + +### Phase 3 — Recording engine *(the risky phase)* + +| # | Task | Depends | +|---|---|---| +| T10 | Location source seam | T03 | +| T11 | Recording pipeline | T09, T10 | +| T12 | Android foreground service | T11 | +| T13 | iOS background location | T11 | +| T14 | Process-death resume | T11, T12, T13 | + +**T10** — A `LocationSource` interface with a `geolocator` implementation and a fake for +tests. This seam is what makes the engine testable without a device, and it is also the +escape hatch: if `geolocator` proves unreliable on a real iOS ride, swapping in +`flutter_background_geolocation` becomes one implementation rather than a rewrite. + +**T11** — The heart. Unbounded `StreamController`, single async drain loop batching 25 +fixes / 2 s, accumulator folded per batch, aggregates persisted per flush. Five actions: +start / pause / resume / stop / discard. Pause closes the open segment; resume opens a new +one. **Stamp `tripId`/`segmentId` at point creation** — a fix in flight during a pause must +land in the segment it actually belongs to. Stop discards a trip with zero points. + +**T12** — `flutter_foreground_task` for process liveness and the persistent notification, +`foregroundServiceType="location"`, wake lock, notification actions. Configured **without** +a separate Dart isolate — the recording loop stays on the main isolate, so there is never +cross-isolate access to one SQLite file. + +**T13 — where the platforms genuinely diverge.** `Info.plist` needs +`NSLocationWhenInUseUsageDescription`, `NSLocationAlwaysAndWhenInUseUsageDescription`, and +`UIBackgroundModes: [location]`, with **context-rich** strings — generic ones are the +leading cause of Guideline 5.1.1 rejection. Set `allowsBackgroundLocationUpdates = true` +and `pauseLocationUpdatesAutomatically = false`. Then **document and measure** what +actually happens when the bike stops at a light versus parks for ten minutes; the app must +resume cleanly rather than silently ending a ride. + +**T14** — On launch, adopt any trip with `endedAt IS NULL` and restore the accumulator from +the persisted row, matching the Kotlin restart path. Verify by force-killing mid-ride on +both platforms. + +### Phase 4 — UI + +| # | Task | Ports | Depends | +|---|---|---|---| +| T15 | Shell: router, Riverpod, theme | `RipprNavHost`, `ui/theme/` | T01 | +| T16 | Record screen | `ui/record/` | T11, T15 | +| T17 | Trips list | `ui/trips/` | T09, T15 | +| T18 | Trip detail: stats + charts | `ui/detail/`, `ui/components/Stats.kt` | T04, T15 | +| T19 | Map + path rendering | `ui/components/RideMap.kt` | T02, T18 | +| T20 | Rename / delete / merge | | T17, T18 | +| T21 | Export UI | T06 | T18 | + +**T15** — Functional dark + safety orange, matching the icon. The theme must set a default +content colour: in Compose, removing `Surface` once made a 64 sp speed figure render +black-on-black and **no test caught it — only a screenshot did**. Flutter's equivalent +exposure is `DefaultTextStyle`. + +**T16** — Headline is **live speed plus a wall-clock elapsed ticker**, not max speed. v2.0 +shipped max-speed-as-headline and it read as a frozen, broken screen on a real ride, +because on an emulator every value is zero and a number that never moves looks fine. +Keep the 72 dp glove-sized controls; confirm on Discard only. + +**T19** — Per-segment polylines so pauses leave visible gaps, speed-bucketed colouring, +Douglas–Peucker decimation **render-only**, fit-to-bounds followed by a **zoom clamp**: a +50 m ride once zoomed past OSM's max tile zoom of 19 and rendered an empty grid. Set a real +user agent before any tile fetch or OSM returns 403, and cache tiles in app-private +storage. Guard the map's own bounds so it cannot overdraw adjacent controls. + +### Phase 5 — Verification + +| # | Task | Depends | +|---|---|---| +| T22 | Uploader + config | T09 | +| T23 | Widget tests | Phase 4 | +| T24 | Integration tests, both platforms | Phase 4 | +| T25 | Real-ride validation | T24 | +| T26 | iOS release readiness | T25 | + +**T22** — `package:http` uploader with batching, retry, offline-safe backlog via the +`synced` column, carrying `trip_id`/`segment_id` in the payload. `shared_preferences` for +endpoint, `deviceId`, `mapEnabled`. Parity note: this still has **no UI**, exactly as today. + +**T23 — closes v2's largest known gap.** Zero UI tests exist across six screens today; +Flutter makes widget tests cheap enough that there is no excuse to carry that debt into the +port. Cover navigation record→trips→detail→back, empty states, chart degradation below two +points, and merge enabled only at exactly two selections. + +**T25** — Run the checklist in `docs/TESTING.md` on **both** platforms. Non-negotiable, +because **neither simulator can produce velocity** — `adb emu geo fix` teleports and the +iOS simulator's synthetic locations are no better, so max speed, average moving speed, +moving time, and **speed colouring on the map** are all unverifiable in CI. Also ride +**short and long**: a ~900 m fixture hid the short-ride zoom bug completely. + +**T26** — Privacy nutrition labels matching actual runtime behaviour, usage strings +audited, background-location justification ready for review. + +### Phase 6 — Cutover + +| # | Task | Depends | +|---|---|---| +| T27 | Parity audit and handover | all | + +Walk the v2.0.1 feature table row by row and demonstrate each on both platforms. **Do not +tick boxes in bulk** — a v2 task once had every criterion checked by a blanket regex +including items never actually verified. Then: port `ARCHITECTURE.md` with the decisions +that changed, carry `docs/v3/BACKLOG.md` forward, and mark the native repo archived with a +pointer to its replacement. + +--- + +## Dependency graph + +``` +T00 ─► T01 ─┬─► T02 ─┬─► T04 ─► T05 ─┐ + │ └─► T06 ─┐ │ + │ T03 ──► T04 │ │ + │ └─► T10 │ │ + │ │ │ + ├─► T08 ─► T09 ───┼──────┴─► T11 ─┬─► T12 ─┐ + │ │ │ ├─► T13 ─┼─► T14 + │ │ │ │ │ + └─► T15 ─┬───┼────┼───────────────┘ │ + │ │ │ │ + T02─┬─► T07 │ │ │ │ + │ │ │ │ │ + └────────┴───┴────┴─► T16/T17/T18 ─► T19 ─► T20/T21 + │ + T22 ──────────────────────► T23 ─► T24 ─► T25 ─► T26 ─► T27 +``` + +Critical path: **T00 → T01 → T08 → T09 → T11 → T12/T13 → T14 → T16 → T24 → T25 → T27**. + +Phase 1 is almost entirely parallelisable and carries near-zero risk — but per your v2 +preference, everything runs **sequentially** so dependent architecture surfaces before it +becomes expensive to change. + +--- + +## Key risks + +| Risk | Mitigation | +|---|---| +| **Disk space blocks the toolchain** | T00 gates everything; hard exit criteria before any code | +| **iOS suspends a stationary app mid-ride** | T13 measures it explicitly; T10's seam makes swapping to `flutter_background_geolocation` cheap if free tooling loses rides | +| `geolocator` proves less reliable than FusedLocation | T25 rides both a real Android and a real iOS device before the native app is retired | +| Elevation hysteresis subtly mistranslated | T07 asserts six-decimal agreement against the Kotlin implementation | +| Two isolates racing one SQLite file | Avoided by design — foreground service provides liveness only, recording stays on the main isolate | +| Decimation leaking into storage or export | Render-only, and T06's ported tests assert exact point counts | +| Simulators hide velocity-dependent bugs | Stated up front in T25; **a green suite proves nothing about speed** | +| Silent parity loss | T27 audits the feature table row by row, demonstrated not asserted | + +--- + +## Verification + +```bash +flutter analyze && flutter test # pure logic, data layer, widgets +flutter test integration_test -d +flutter test integration_test -d +flutter build apk --debug && flutter build ios --debug --no-codesign +``` + +Plus the **cross-language parity harness** (T07): identical fixtures through Kotlin and +Dart, agreement asserted to six decimals — the port's strongest single guarantee, and the +reason Phase 1 comes first. + +**Definition of done:** every row of the v2.0.1 feature table demonstrated on a real +Android phone *and* a real iPhone, the `docs/TESTING.md` checklist passed on both, and no +capability lost. + +--- + +## Open question, deferred deliberately + +The native app's known **elevation drift** (~30 m per ten stationary minutes against +synthetic noise) ports along with the algorithm — faithfully, bug included. That is correct +for a parity port: fixing it during translation would make any differential test failure +ambiguous. It stays in the v3 backlog, where it already says *do not tune this blind*.