fix: replace continuous GPS with on-demand one-shot in Group Tracker#746
fix: replace continuous GPS with on-demand one-shot in Group Tracker#746MatthieuTexier wants to merge 10 commits intotorlando-tech:mainfrom
Conversation
Greptile SummaryAfter several fixup commits, the implementation lands on a hybrid model: continuous Confidence Score: 5/5Safe to merge — all previously-raised P1 concerns are resolved; remaining findings are minor P2 items that don't affect correctness in practice. The three P1/P0 issues from prior review rounds (stop()-race guard, stale OS-fix rejection, AtomicBoolean for pre-R cancellation) are all addressed. The two remaining comments are P2: a documentation mismatch in the PR description, and a narrow concurrent-update race in getTrackedLocationAgeMs whose practical impact is negligible. Neither blocks a merge. TelemetryLocationTracker.kt — the uncoupled latestTrackedLocationRecordedAtMs field warrants a follow-up refactor if location accuracy SLAs tighten. Important Files Changed
Sequence DiagramsequenceDiagram
participant CM as TelemetryCollectorManager
participant LT as TelemetryLocationTracker
participant Cache as latestTrackedLocation
participant FLP as FusedLocationClient (BALANCED)
participant OSS as One-shot (HIGH_ACCURACY)
Note over FLP,Cache: Continuous background subscription
FLP-->>Cache: cacheTrackedLocation(fix)
CM->>LT: sendTelemetryToCollector()
LT->>LT: getTelemetryLocation()
LT->>Cache: read latestTrackedLocation
alt Cache hit (isLocationRecent)
Cache-->>LT: fresh location
else Cache miss or stale
LT->>OSS: getCurrentLocation() with 20s timeout
alt Fix received and isFixFresh
OSS-->>LT: location
LT->>Cache: cacheTrackedLocation(fix)
else Timed out or stale OS fix
OSS-->>LT: null
end
end
LT-->>CM: Location? (null → NoLocationAvailable)
Prompt To Fix All With AIThis is a comment left during a code review.
Path: app/src/main/java/network/columba/app/service/TelemetryLocationTracker.kt
Line: 187-202
Comment:
**Instance-level timestamp not coupled to the passed location**
`getTrackedLocationAgeMs` reads `latestTrackedLocationRecordedAtMs` from instance state rather than a per-location value. If a background tracking callback fires between the two lines in `getTelemetryLocation`:
```kotlin
val tracked = latestTrackedLocation // reads old location
if (tracked != null && isLocationRecent(tracked)) { // now latestTrackedLocationRecordedAtMs == "just now"
```
…then `maxOf(old_fix_time, just_now)` yields an age of ~0 ms, making a stale cached location appear fresh. The window is narrow but the root cause is that the "recorded at" timestamp is instance-global rather than stored alongside the location it belongs to.
Pairing them in a small data class would eliminate the race entirely:
```kotlin
private data class CachedLocation(val location: Location, val recordedAtMs: Long)
@Volatile private var cachedLocation: CachedLocation? = null
```
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: app/src/main/java/network/columba/app/service/TelemetryLocationTracker.kt
Line: 18-27
Comment:
**KDoc accurately describes implementation; PR description does not**
The class KDoc correctly describes the final approach — continuous `BALANCED_POWER_ACCURACY` tracking with a `HIGH_ACCURACY` one-shot fallback — but the PR title and summary still say "replace continuous GPS with on-demand one-shot." The git history shows an intermediate commit (`bdcb0d5b`) reverted the pure one-shot approach to this hybrid, which is the right call for Doze reliability, but the PR description was never updated. Worth amending the PR description so the reviewed intent matches what was actually shipped.
How can I resolve this? If you propose a fix, please make it concise.Reviews (11): Last reviewed commit: "fix: replace resultDelivered boolean wit..." | Re-trigger Greptile |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…etryLocationTracker The TelemetryLocationTracker maintained a continuous FusedLocationProvider subscription (30s/15s intervals) to keep a cache fresh, but the TelemetryCollectorManager only reads that cache once every 5+ minutes. This kept the GPS hardware active ~100% of the time for no benefit. Replace with on-demand one-shot fixes that only activate GPS when a telemetry send actually needs a position (~10-20s per fix). The existing cache (5 min TTL) prevents redundant fixes if called again quickly. Measured impact (5-min benchmark, BraX3 Android 16): - GNSS: 2.43 mAh → 0.009 mAh (÷270) - GPS active time: 5m01s (100%) → 1.1s (0.4%) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
With continuous tracking removed, the 5-minute cache TTL could cause stale positions to be sent when the send interval also equals 5 minutes. Reduce to 30s so each periodic send always triggers a fresh GPS fix, while still deduplicating rapid back-to-back calls. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add locationTrackingActive check at top of getTelemetryLocation() to prevent a one-shot GPS fix firing after stop() is called - Remove unreachable post-fix staleness check: cacheTrackedLocation() sets recordedAtMs to now, so isLocationRecent() always returns true immediately after — simplify to just return the fix directly Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Merge the null/non-null paths into a single return to stay within the 3-return-statement limit. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The guard prevented manual sends (sendTelemetryNow) from obtaining a position when the tracker had not yet been started by settings flows. The tracker is a stateless position provider — lifecycle control belongs in TelemetryCollectorManager. The race window (stop during one-shot) is harmless: the fix completes, gets cached, and stop() clears the cache. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
FusedLocationProviderClient.getCurrentLocation() can return a previously cached OS-level fix in GPS-denied or power-saving conditions. Add isFixFresh() check using location.time to reject fixes older than 5 min. Fixes are accepted if location.time is 0 (provider didn't set timestamp, common with some location providers — treat as fresh since just received). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Prevents a 20-second GPS fix from firing after stop() is called during the race window between periodic send wakeup and user disabling the tracker. Tests updated to explicitly activate the tracker before manual sends, since enabling via settings flows starts periodic loops incompatible with runTest. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…t helper - LocationCompat.getCurrentLocation: accept optional CancellationSignal, pass it to LocationManager.getCurrentLocation (API 30+) and wire setOnCancelListener for pre-R fallback to stop GPS on cancellation - TelemetryLocationTracker: pass CancellationSignal in non-GMS path so withTimeoutOrNull cancellation actually stops the GPS hardware - TelemetryCollectorManager: annotate ensureLocationTrackerActive with @VisibleForTesting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…mic interval Pure one-shot GPS failed on non-GMS devices in Doze (cold GPS can't acquire satellites within the 20s timeout). Instead, use low-power continuous tracking (WiFi/cell via BALANCED_POWER_ACCURACY) with the tracking interval derived from the send interval (sendInterval - 30s, min 60s). This keeps a cached position available while avoiding the ~29 mAh/h cost of continuous HIGH_ACCURACY GPS. Examples: - Send every 5 min → track every 4min30 - Send every 1h → track every 59min30 One-shot HIGH_ACCURACY remains as fallback when the cache is stale. Stale OS-cached fixes (>5 min) are rejected via isFixFresh(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…mpat The pre-API 30 location path uses resultDelivered to guard against duplicate callbacks. With cancellation support, setOnCancelListener runs on the cancelling thread while the other callbacks run on the main looper — creating a cross-thread data race on a plain boolean. Switch to AtomicBoolean.compareAndSet for lock-free thread safety. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
8231af0 to
f966e33
Compare
Summary
Measured impact (5-min benchmark, Android 16)
Feature behavior unchanged — Group Tracker sends positions at the same interval with the same accuracy.
Test plan
adb shell dumpsys batterystats— confirm no continuous GPS wakelock from Columba