T22/T24/T26: uploader, integration tests, and release readiness
T22 -- TelemetryUploader on package:http with MockClient standing in for MockWebServer, tested against a real in-memory Drift database rather than a fake DAO. Upload runs on its own timer, injected as a callback so the engine has no opinion about HTTP and tests need no network. Config on shared_preferences with a hand-rolled UUID v4. Still no UI for the endpoint, exactly as in the native app. 7 tests. T24 -- integration_test/app_test.dart, 4 tests passing on the iOS simulator. These cover what widget tests cannot: Drift opening against real platform storage, plugin registration, go_router driving a real Navigator, cold start. T26 -- RELEASE-IOS.md. Usage strings are specific rather than generic, which is the leading Guideline 5.1.1 rejection cause; privacy-label answers decided; a pre-submission list covering the bundle-id switch back to com.rippr, the still default app icon, a release build, and a demo video for review notes. T25 written up as REAL-RIDE-CHECKLIST.md but outstanding by nature. Two items decide real things: force-stopping mid-ride on Android confirms the 111 km crash-gap bug is fixed, and parking 15 minutes mid-recording on iOS decides whether geolocator is sufficient or the paid engine is needed. Two prefer_initializing_formals lints suppressed with a reason: Dart forbids a named parameter beginning with an underscore, so the suggested fix will not compile. 178 tests passing, analyze clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,10 @@
|
||||
/// the explicit drains at pause, stop and discard.
|
||||
library;
|
||||
|
||||
// Dart does not permit a named parameter whose name begins with an underscore, so the
|
||||
// lint's suggested `required this._uploadPending` will not compile here.
|
||||
// ignore_for_file: prefer_initializing_formals
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:synchronized/synchronized.dart';
|
||||
@@ -46,6 +50,13 @@ const int flushSize = 25;
|
||||
/// Coalesce bursts without letting points sit unwritten for long.
|
||||
const Duration flushInterval = Duration(seconds: 2);
|
||||
|
||||
/// How often the backlog is offered to the server.
|
||||
///
|
||||
/// Upload runs on its own timer so a slow or dead endpoint can never interrupt
|
||||
/// recording — the same separation the native service kept between its writer loop and
|
||||
/// its upload loop.
|
||||
const Duration uploadInterval = Duration(seconds: 30);
|
||||
|
||||
/// What the recorder is doing right now, for the UI.
|
||||
enum RecorderState { idle, recording, paused }
|
||||
|
||||
@@ -55,16 +66,23 @@ class RecordingEngine {
|
||||
required LocationSource locationSource,
|
||||
LiveTelemetry? liveTelemetry,
|
||||
int Function()? clock,
|
||||
Future<void> Function()? uploadPending,
|
||||
}) : _repo = repository,
|
||||
_source = locationSource,
|
||||
_live = liveTelemetry ?? LiveTelemetry.instance,
|
||||
_now = clock ?? (() => DateTime.now().millisecondsSinceEpoch);
|
||||
_now = clock ?? (() => DateTime.now().millisecondsSinceEpoch),
|
||||
_uploadPending = uploadPending;
|
||||
|
||||
final TripRepository _repo;
|
||||
final LocationSource _source;
|
||||
final LiveTelemetry _live;
|
||||
final int Function() _now;
|
||||
|
||||
/// Injected rather than constructed here, so the engine has no opinion about HTTP and
|
||||
/// tests need no network.
|
||||
final Future<void> Function()? _uploadPending;
|
||||
Timer? _uploadTimer;
|
||||
|
||||
/// Unbounded, exactly like the Kotlin `Channel(UNLIMITED)`. Appending is the only work
|
||||
/// done on the fix path.
|
||||
final List<TrackPoint> _pending = [];
|
||||
@@ -127,10 +145,22 @@ class RecordingEngine {
|
||||
await _source.start();
|
||||
_subscription ??= _source.fixes.listen(_onFix);
|
||||
_flushTimer ??= Timer.periodic(flushInterval, (_) => _flush());
|
||||
_startUploadLoop();
|
||||
|
||||
_setState(RecorderState.recording);
|
||||
}
|
||||
|
||||
void _startUploadLoop() {
|
||||
final upload = _uploadPending;
|
||||
if (upload == null || _uploadTimer != null) return;
|
||||
_uploadTimer = Timer.periodic(uploadInterval, (_) async {
|
||||
// Swallowed on purpose. Nothing about uploading may disturb recording.
|
||||
try {
|
||||
await upload();
|
||||
} catch (_) {}
|
||||
});
|
||||
}
|
||||
|
||||
/// Stops consuming GPS but leaves the trip open, so resuming is instant.
|
||||
Future<void> pause() async {
|
||||
// Stop the source first, then drain, then close the segment. Points already queued
|
||||
@@ -320,6 +350,8 @@ class RecordingEngine {
|
||||
Future<void> _teardown() async {
|
||||
_flushTimer?.cancel();
|
||||
_flushTimer = null;
|
||||
_uploadTimer?.cancel();
|
||||
_uploadTimer = null;
|
||||
await _subscription?.cancel();
|
||||
_subscription = null;
|
||||
await _writeLock.synchronized(() async {
|
||||
|
||||
Reference in New Issue
Block a user