summaryrefslogtreecommitdiff
path: root/android-app/app/src/test/kotlin/com/example/androidapp
diff options
context:
space:
mode:
authorClaude Agent <agent@example.com>2026-03-25 02:19:39 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-03-25 04:55:58 +0000
commit75ec688eb2d2754b77ff18946412bca434eb503a (patch)
tree68cb98bb7ce6469a0ae4ca1a698f974ea810d763 /android-app/app/src/test/kotlin/com/example/androidapp
parent62c27bf28de30979bc58ef7808185ac189f71197 (diff)
feat(gps): add fix-quality (accuracy) tier to GPS sensor fusion
Extend LocationService's source-selection policy with a quality-aware "marginal staleness" zone between the primary and a new extended staleness threshold (default 10 s): 1. Fresh NMEA (≤ primary threshold, 5 s) → always prefer NMEA 2. Marginally stale NMEA (5–10 s) → prefer NMEA only when GpsPosition.accuracyMeters is strictly better than Android's; fall back to Android conservatively when accuracy data is absent 3. Very stale NMEA (> 10 s) → always prefer Android 4. Only one source available → use it regardless of age Changes: - GpsPosition: add nullable accuracyMeters field (default null, no breaking change to existing callers) - LocationService: add nmeaExtendedThresholdMs constructor parameter; recomputeBestPosition() now implements three-tier logic; extract GpsPosition.hasStrictlyBetterAccuracyThan() helper - LocationServiceTest: expose nmeaExtendedThresholdMs in fusionService helper; add posWithAccuracy helper; add 4 new test cases covering accuracy-based NMEA preference, worse-accuracy fallback, no-accuracy conservative fallback, and very-stale unconditional fallback Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'android-app/app/src/test/kotlin/com/example/androidapp')
-rw-r--r--android-app/app/src/test/kotlin/com/example/androidapp/gps/LocationServiceTest.kt90
1 files changed, 90 insertions, 0 deletions
diff --git a/android-app/app/src/test/kotlin/com/example/androidapp/gps/LocationServiceTest.kt b/android-app/app/src/test/kotlin/com/example/androidapp/gps/LocationServiceTest.kt
index 237004b..4eb9898 100644
--- a/android-app/app/src/test/kotlin/com/example/androidapp/gps/LocationServiceTest.kt
+++ b/android-app/app/src/test/kotlin/com/example/androidapp/gps/LocationServiceTest.kt
@@ -119,15 +119,20 @@ class LocationServiceTest {
private fun fusionService(
nmeaStalenessThresholdMs: Long = 5_000L,
+ nmeaExtendedThresholdMs: Long = 10_000L,
clockMs: () -> Long = System::currentTimeMillis
) = LocationService(
nmeaStalenessThresholdMs = nmeaStalenessThresholdMs,
+ nmeaExtendedThresholdMs = nmeaExtendedThresholdMs,
clockMs = clockMs
)
private fun pos(lat: Double, lon: Double, timestampMs: Long) =
GpsPosition(lat, lon, sog = 0.0, cog = 0.0, timestampMs = timestampMs)
+ private fun posWithAccuracy(lat: Double, lon: Double, timestampMs: Long, accuracyMeters: Double) =
+ GpsPosition(lat, lon, sog = 0.0, cog = 0.0, timestampMs = timestampMs, accuracyMeters = accuracyMeters)
+
@Test
fun noGpsData_bestPositionNullAndSourceNone() = runBlocking {
val svc = fusionService()
@@ -203,6 +208,91 @@ class LocationServiceTest {
assertEquals(GpsSource.NMEA, svc.activeGpsSource.first())
}
+ // ── fix-quality (accuracy) tie-breaking ──────────────────────────────────
+
+ @Test
+ fun marginallyStaleNmea_betterAccuracy_preferredOverAndroid() = runBlocking {
+ // NMEA is 7 s old (> primary 5 s, ≤ extended 10 s) but has accuracy 3 m vs Android 15 m.
+ val nmeaTime = 0L
+ val now = 7_000L
+ val svc = fusionService(
+ nmeaStalenessThresholdMs = 5_000L,
+ nmeaExtendedThresholdMs = 10_000L,
+ clockMs = { now }
+ )
+
+ val nmeaFix = posWithAccuracy(41.0, -71.0, nmeaTime, accuracyMeters = 3.0)
+ val androidFix = posWithAccuracy(42.0, -72.0, now, accuracyMeters = 15.0)
+
+ svc.updateNmeaGps(nmeaFix)
+ svc.updateAndroidGps(androidFix)
+
+ assertEquals(GpsSource.NMEA, svc.activeGpsSource.first())
+ assertEquals(nmeaFix, svc.bestPosition.first())
+ }
+
+ @Test
+ fun marginallyStaleNmea_worseAccuracy_fallsBackToAndroid() = runBlocking {
+ // NMEA is 7 s old with accuracy 15 m; Android has accuracy 3 m → Android wins.
+ val nmeaTime = 0L
+ val now = 7_000L
+ val svc = fusionService(
+ nmeaStalenessThresholdMs = 5_000L,
+ nmeaExtendedThresholdMs = 10_000L,
+ clockMs = { now }
+ )
+
+ val nmeaFix = posWithAccuracy(41.0, -71.0, nmeaTime, accuracyMeters = 15.0)
+ val androidFix = posWithAccuracy(42.0, -72.0, now, accuracyMeters = 3.0)
+
+ svc.updateNmeaGps(nmeaFix)
+ svc.updateAndroidGps(androidFix)
+
+ assertEquals(GpsSource.ANDROID, svc.activeGpsSource.first())
+ assertEquals(androidFix, svc.bestPosition.first())
+ }
+
+ @Test
+ fun marginallyStaleNmea_noAccuracyData_fallsBackToAndroid() = runBlocking {
+ // Neither source has accuracy metadata — conservative: prefer Android.
+ val nmeaTime = 0L
+ val now = 7_000L
+ val svc = fusionService(
+ nmeaStalenessThresholdMs = 5_000L,
+ nmeaExtendedThresholdMs = 10_000L,
+ clockMs = { now }
+ )
+
+ val nmeaFix = pos(41.0, -71.0, nmeaTime)
+ val androidFix = pos(42.0, -72.0, now)
+
+ svc.updateNmeaGps(nmeaFix)
+ svc.updateAndroidGps(androidFix)
+
+ assertEquals(GpsSource.ANDROID, svc.activeGpsSource.first())
+ }
+
+ @Test
+ fun veryStaleNmea_beyondExtendedThreshold_androidPreferred() = runBlocking {
+ // NMEA is 15 s old (beyond extended 10 s); Android wins even if NMEA has better accuracy.
+ val nmeaTime = 0L
+ val now = 15_000L
+ val svc = fusionService(
+ nmeaStalenessThresholdMs = 5_000L,
+ nmeaExtendedThresholdMs = 10_000L,
+ clockMs = { now }
+ )
+
+ val nmeaFix = posWithAccuracy(41.0, -71.0, nmeaTime, accuracyMeters = 2.0)
+ val androidFix = posWithAccuracy(42.0, -72.0, now, accuracyMeters = 20.0)
+
+ svc.updateNmeaGps(nmeaFix)
+ svc.updateAndroidGps(androidFix)
+
+ assertEquals(GpsSource.ANDROID, svc.activeGpsSource.first())
+ assertEquals(androidFix, svc.bestPosition.first())
+ }
+
@Test
fun nmeaRecovery_switchesBackFromAndroid() = runBlocking {
var now = 0L