Skip to content

fix: replace continuous GPS with on-demand one-shot in Group Tracker#746

Open
MatthieuTexier wants to merge 10 commits intotorlando-tech:mainfrom
MatthieuTexier:fix/telemetry-gps-oneshot
Open

fix: replace continuous GPS with on-demand one-shot in Group Tracker#746
MatthieuTexier wants to merge 10 commits intotorlando-tech:mainfrom
MatthieuTexier:fix/telemetry-gps-oneshot

Conversation

@MatthieuTexier
Copy link
Copy Markdown
Contributor

@MatthieuTexier MatthieuTexier commented Apr 2, 2026

Summary

  • TelemetryLocationTracker maintained a continuous FusedLocationProvider subscription (30s/15s) to cache positions, but TelemetryCollectorManager only reads it every 5+ minutes. This kept GPS hardware active ~100% of the time for no benefit.
  • Replace with on-demand one-shot fixes that activate GPS only when a send needs a position (~10-20s per fix). Reduce cache TTL from 5 min to 30s to ensure fresh positions.

Measured impact (5-min benchmark, Android 16)

Metric Before After
GNSS energy 2.43 mAh 0.009 mAh (÷270)
GPS active time 5m01s (100%) 1.1s (0.4%)
Hourly drain (GNSS) ~29 mAh/h (~5.9%/h) ~0.11 mAh/h (~0.02%/h)

Feature behavior unchanged — Group Tracker sends positions at the same interval with the same accuracy.

Test plan

  • Enable Group Tracker ("Share with group"), configure 5-min interval
  • Verify positions are received by group host at expected interval
  • Verify position accuracy is unchanged (HIGH_ACCURACY one-shot)
  • Run adb shell dumpsys batterystats — confirm no continuous GPS wakelock from Columba
  • Test on device without GMS (LocationCompat fallback path)

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 2, 2026

Greptile Summary

After several fixup commits, the implementation lands on a hybrid model: continuous BALANCED_POWER_ACCURACY tracking at a derived interval keeps the cache warm, and a HIGH_ACCURACY one-shot fills in when the cache is stale. Previous review concerns (the locationTrackingActive guard, stale OS-cached fix rejection, and AtomicBoolean for the pre-R cancellation race) are all addressed. Two minor P2 points remain: the PR description still describes the abandoned pure one-shot approach, and getTrackedLocationAgeMs reads an instance-level timestamp that isn't coupled to the specific location being tested, which can cause a stale hit in a narrow concurrent-update window.

Confidence Score: 5/5

Safe 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

Filename Overview
app/src/main/java/network/columba/app/service/TelemetryLocationTracker.kt Implements hybrid continuous BALANCED_POWER tracking with HIGH_ACCURACY one-shot fallback; locationTrackingActive guard, stale-fix rejection, and cache TTL all look correct; minor race in getTrackedLocationAgeMs reading an instance-level timestamp not coupled to the passed location object.
app/src/main/java/network/columba/app/service/TelemetryCollectorManager.kt Wires locationTracker.update() correctly in all settings-change paths; sendTelemetryToCollector delegates to the tracker and handles NoLocationAvailable cleanly; no new issues.
app/src/main/java/network/columba/app/util/LocationCompat.kt AtomicBoolean fix correctly guards against cancellation-from-background-thread racing the main-looper location callback; pre-R timeout and cancel-listener logic look sound.
app/src/test/java/network/columba/app/service/TelemetryCollectorManagerTest.kt Tests activate the tracker via ensureLocationTrackerActive() and mock LocationCompat.getCurrentLocation to exercise the one-shot path; coverage is adequate for the changed logic.

Sequence Diagram

sequenceDiagram
    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)
Loading
Prompt To Fix All With AI
This 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

@sentry
Copy link
Copy Markdown
Contributor

sentry bot commented Apr 2, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@MatthieuTexier MatthieuTexier marked this pull request as draft April 3, 2026 19:52
@MatthieuTexier MatthieuTexier marked this pull request as ready for review April 4, 2026 09:13
@MatthieuTexier MatthieuTexier mentioned this pull request Apr 9, 2026
MatthieuTexier and others added 10 commits April 16, 2026 08:49
…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>
@MatthieuTexier MatthieuTexier force-pushed the fix/telemetry-gps-oneshot branch from 8231af0 to f966e33 Compare April 16, 2026 07:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant