Add the Rippr Flutter port: source, history bundle, and installable APK

The port runs on Android and iOS and is feature-complete; the native Android app
is superseded but kept, since it is still the only version that has recorded real
rides.

rippr-flutter-1.0-debug.apk is package com.rippr.port, deliberately different
from the native com.rippr so both install side by side. Recording the same ride
on both at once is the strongest available check that the port is faithful.

Added INSTALL.md covering both platforms. Android is a one-line adb install; iOS
has no APK equivalent and must be built and signed through Xcode with a free
Apple ID, which gives a 7-day profile.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-15 22:37:45 -05:00
parent 64f5dff30b
commit 0bc42b2e5a
98 changed files with 8417 additions and 24 deletions

View File

@@ -0,0 +1,111 @@
import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:rippr/src/domain/models.dart';
import 'package:rippr/src/telemetry/telemetry.dart';
/// Ported from `com.rippr.TelemetryTest`.
///
/// Tolerances are carried over as-is. Note the speed assertions: Kotlin's `Float`
/// widened to `Double` yields 88.5 only within 1e-4, which is exactly why the original
/// used that tolerance. Dart's uniform binary64 is exact here, but the tolerance is kept
/// so the two suites stay comparable line for line.
void main() {
test('converts meters per second to kmh', () {
expect(msToKmh(10), closeTo(36, 0.001));
expect(msToKmh(0), closeTo(0, 0.001));
});
test('gps jitter below the noise floor reports as zero', () {
expect(sanitizeSpeedKmh(0.9), closeTo(0, 0.001));
expect(sanitizeSpeedKmh(double.nan), closeTo(0, 0.001));
expect(sanitizeSpeedKmh(-5), closeTo(0, 0.001));
});
test('real speeds pass through untouched', () {
expect(sanitizeSpeedKmh(97.3), closeTo(97.3, 0.001));
});
test('rejects fixes worse than the accuracy budget', () {
expect(isUsableFix(12), isTrue);
expect(isUsableFix(0), isTrue,
reason: 'unknown accuracy must not be discarded');
expect(isUsableFix(120), isFalse);
});
test('formats duration as hh mm ss', () {
expect(formatDuration(0), '00:00:00');
expect(formatDuration(-1), '00:00:00');
expect(formatDuration(65000), '00:01:05');
expect(formatDuration(7384000), '02:03:04');
});
test('encodes a batch as the documented payload shape', () {
final points = [
const TrackPoint(
id: 7,
tripId: 3,
segmentId: 5,
timestamp: 1700000000000,
latitude: 51.0447,
longitude: -114.0719,
speedKmh: 88.5,
altitudeM: 1045.0,
accuracyM: 4.2,
bearingDeg: 271.5,
),
];
final json =
jsonDecode(encodeBatch('device-abc', points)) as Map<String, dynamic>;
expect(json['device_id'], 'device-abc');
final first = (json['points'] as List).first as Map<String, dynamic>;
expect(first['id'], 7);
expect(first['trip_id'], 3);
expect(first['segment_id'], 5);
expect(first['ts'], 1700000000000);
expect(first['lat'] as double, closeTo(51.0447, 1e-6));
expect(first['lon'] as double, closeTo(-114.0719, 1e-6));
expect(first['speed_kmh'] as double, closeTo(88.5, 1e-4));
});
test('a batch spanning two segments labels each point individually', () {
// A batch is drawn by id and can cross a boundary, so identity must travel with
// the point rather than the batch.
final points = [
const TrackPoint(
id: 1,
tripId: 3,
segmentId: 5,
timestamp: 1,
latitude: 51.0,
longitude: -114.0,
speedKmh: 40,
altitudeM: 1000.0),
const TrackPoint(
id: 2,
tripId: 3,
segmentId: 6,
timestamp: 2,
latitude: 51.1,
longitude: -114.0,
speedKmh: 40,
altitudeM: 1000.0),
];
final array =
(jsonDecode(encodeBatch('d', points)) as Map<String, dynamic>)['points']
as List;
expect((array[0] as Map)['segment_id'], 5);
expect((array[1] as Map)['segment_id'], 6);
expect((array[0] as Map)['trip_id'], 3);
});
test('encodes an empty batch without failing', () {
final json =
jsonDecode(encodeBatch('d', const [])) as Map<String, dynamic>;
expect((json['points'] as List).length, 0);
});
}