Two build-blocking issues fixed to get a release APK building at all:
- flutter_local_notifications (V3-06) requires core library desugaring below API 33; enabled it and added the desugar_jdk_libs dependency.
- sentry_flutter 8.14.2's bundled Kotlin plugin declares languageVersion 1.6, incompatible with this project's Kotlin 2.4.0 toolchain. Bumped to sentry_flutter ^9.0.0 (resolved 9.27.0), which builds clean; left three copyWith deprecation infos from the v9 API as a follow-up, not blocking.
Signed with the debug keystore (release signingConfig still points at debug -- no release signing config exists yet, matches the existing TODO in build.gradle.kts). app-release.apk committed directly (68.7MB, force-added past .gitignore's build/ exclusion) per request, so it can be pushed once a remote exists.
shouldInitializeCrashReporting() and scrubExtra() are pure, fully unit-tested decision/scrubbing functions wired into sentry_flutter via beforeSend/beforeBreadcrumb. Config.crashReportingEnabled defaults off; the DSN is a compile-time --dart-define, not a preference. RecordingEngine gained an injected onUnexpectedStop callback, firing once when restoreAfterProcessDeath finds a trip still 'recording' at launch -- the process died without a user stop.
Marked partially done: the ticket's real acceptance criteria (a forced crash landing in a real Sentry project from a release build, inspecting a real payload) need a Sentry account and a release build this environment can't produce.
RideNotificationController seam over flutter_local_notifications; RideNotificationCoordinator wires TripRepository.watchActiveTrip() to it and routes Pause/Resume actions back into RecordingEngine. Instantiated eagerly at app root rather than screen-owned, since a pocketed ride has no visible widget tree. Documents an unresolved risk: geolocator's own foreground-service notification cannot be suppressed, so two notifications may be visible until verified on a device.
Wake lock via a WakelockController seam (mirrors LocationSource), gated strictly to actual recording and released on stop/discard/navigate-away/dispose. High-contrast ripprMountedTheme() applied only to the record screen. BigStat gained a scale param for the headline figure rather than a blanket TextTheme.apply, which crashes on Material 3's default theme.
Adds Activity (motorcycle/bicycle/scooter/skateboard/running/walking/other) as
a column on Trip, and threads it through as real behaviour rather than a label:
ActivityProfile (domain/activity_profile.dart) carries a noise floor, accuracy
gate, histogram bucket width, and elevation smoothing window/threshold per
activity, consumed by sanitizeSpeedKmh, isUsableFix, computeSummary, Accumulator
and speedHistogram. The motorcycle profile reproduces the exact constants the
app shipped with before this existed, and a test asserts they never drift apart.
This is the port's first real migration: schemaVersion 1 -> 2,
m.addColumn(trips, trips.activity) with a motorcycle default so every existing
row survives unmodified. Proven with a hand-built v1 SQLite file (raw sqlite3,
not drift_dev's schema tooling) that a real database with real rides upgrades
and keeps every trip, segment and point.
No picker in front of Start: a new trip defaults to whichever activity was most
recently used, derived live from the trips table rather than duplicated into a
Config field. Editable afterwards on trip detail, which recomputes aggregates
under the new profile immediately -- a walking pace that reads as noise under a
motorcycle's floor reads as real movement once the activity is corrected, and
there's a test proving exactly that transition.
The widget-test pass for the activity-picker sheet caught a real overflow bug:
seven options overflowed a Column-based bottom sheet the same way the record
screen once did. Fixed with a scrollable ListView + isScrollControlled, same
shape as that earlier fix.
188 tests (171 -> 188): 2 migration, 5 ActivityProfile, 3 profile-threading
proofs in computeSummary, 5 repository, 2 widget. Analyze clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
geolocator_android's ForegroundNotificationConfig raises a service with
foregroundServiceType=location and holds a wake lock for as long as the position
stream is subscribed, and the plugin declares that service in its own manifest.
That covers everything the native TrackingService used a foreground service and
a PARTIAL_WAKE_LOCK for, so flutter_foreground_task is gone -- and with it both
deprecation warnings that were recorded under T01.
Known parity gap for T27: geolocator's notification cannot carry actions, so the
native Pause/Resume buttons in the shade are not reproduced.
iOS: UIBackgroundModes plus allowBackgroundLocationUpdates keep the isolate and
the writer timer alive while updates flow. Two settings matter more than they
look -- pauseLocationUpdatesAutomatically is false because CoreLocation does not
reliably restart, and activityType is otherNavigation rather than
automotiveNavigation, which would snap fixes to the road network and silently
falsify the recorded path. Usage strings are specific, not generic.
Android deliberately does not request ACCESS_BACKGROUND_LOCATION: a foreground
service with a visible notification is exactly the supported case, and asking
would trigger the harder "Allow all the time" flow for no benefit.
T14 process-death resume is implemented and called at startup, covered by four
tests including the 111 km crash-gap guard.
Verified by building both platforms and launching on the iOS simulator: Drift
opened, Riverpod resolved, restore found no active trip, controls correct.
lib/main.dart is a harness so the pipeline can be exercised on hardware before
Phase 4 builds screens; T15 replaces it.
Phase 3 done: 145 tests passing, analyze clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
T05 — ride_accumulator.dart with the cross-batch anchor intact (12 tests).
T06 — ride_export.dart, GPX 1.1 and GeoJSON (15 tests), parsed with real
parsers rather than substring matching.
T07 — parity harness extended to cover export byte output.
GPX and GeoJSON are byte-identical across Kotlin and Dart: same length, same
FNV hash, including the escaped hostile name and all %.7f/%.1f formatting.
Two harness bugs found and fixed while building it. String.hashCode is not
comparable across Java and Dart, so text comparison used FNV-1a instead. And a
missing jar let a failed Kotlin compile pass as a green run -- run.sh now checks
for the artifact and exits non-zero, the same failure mode as the v2 sweep that
hid a compile error behind /dev/null.
Phase 1 done: 79 tests passing, analyze clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
T03 — telemetry.dart, format.dart, live_telemetry.dart, plus pure domain models
(Trip/Segment/TrackPoint/RideStats) with no persistence dependency, so Drift can
map to them in T08 rather than the domain depending on the database.
T04 — ride_statistics.dart including ElevationAccumulator, ported structurally
faithfully: moving average, reversal hysteresis, gainIncludingPending, and the
finish() reconciliation against lastRaw.
T07 (early, because T04 forced it) — tool/parity/ drives identical fixtures
through the real Kotlin files and the Dart port, then diffs. Result: every value
byte-identical, including noisy_gain=38.959594555022136 to the last digit. The
sole difference is run_avg_speed, where Kotlin's 32-bit Float widens to double
with artefacts Dart's binary64 does not reproduce. Documented, not papered over.
That harness settled a real question. The ported elevation test failed at 50.9m
against Kotlin's 35m bound, which looked like a porting bug. It was not: Kotlin's
and Dart's Random(42) are different streams. On a shared LCG fixture both produce
39.0m -- which would also fail Kotlin's own bound. The native guard passes on seed
luck rather than on a property of the algorithm. The Dart test now uses the shared
LCG, asserts bit-equality with Kotlin, and sets its bound from measured behaviour
(25 seeds spanned 24.7-46.7m).
52 tests passing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
T00 — toolchain green on both platforms. flutter doctor reports no issues.
Pinned Flutter to Temurin 21 (AGP rejects the default JDK 25, same constraint
as the native build) and installed Android cmdline-tools with an explicit
--sdk_root, the trap already documented in the native DEVELOPMENT.md.
No caches were deleted: the 16 GiB disk reading that drove the cleanup plan
re-measured at 36 GiB before anything was removed.
T01 — flutter create for android+ios. applicationId is com.rippr.port, not
com.rippr, so the native app stays installable alongside it during the port;
T27 switches it at cutover.
T02 — geo/geo.dart ported from com.rippr.geo.Geo with all 23 tests, tolerances
and comments carried over unchanged. Kotlin's object namespace became top-level
functions; LatLon stays our own type so the pure layer never depends on
flutter_map.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>