Skip to content

Add LocationCompat utility for graceful GMS fallback on devices without Play Services#469

Merged
torlando-tech merged 4 commits intomainfrom
claude/reduce-gps-warnings-U3LpD
Feb 16, 2026
Merged

Add LocationCompat utility for graceful GMS fallback on devices without Play Services#469
torlando-tech merged 4 commits intomainfrom
claude/reduce-gps-warnings-U3LpD

Conversation

@torlando-tech
Copy link
Copy Markdown
Owner

Summary

This PR introduces a new LocationCompat utility class that provides a graceful fallback mechanism for location services on devices without Google Play Services installed. This resolves repeated warning logs that flood the console on custom ROMs and F-Droid builds when attempting to use FusedLocationProviderClient.

Key Changes

  • New LocationCompat utility class (app/src/main/java/com/lxmf/messenger/util/LocationCompat.kt):

    • Checks Google Play Services availability once at startup and caches the result
    • Provides fallback location methods using Android's built-in LocationManager
    • Implements getLastKnownLocation(), getCurrentLocation(), requestLocationUpdates(), and removeLocationUpdates()
    • Handles API level differences (Android R+ uses getCurrentLocation(), older versions use requestSingleUpdate())
    • Logs availability status at Info level instead of generating repeated warnings
  • Updated MapScreen.kt:

    • Conditionally creates FusedLocationProviderClient only when Play Services is available
    • Falls back to LocationCompat for location updates when GMS is unavailable
    • Passes context and GMS availability flag to startLocationUpdates()
  • Updated LocationSharingManager.kt:

    • Conditionally initializes FusedLocationProviderClient based on GMS availability
    • Implements dual-path location update logic: GMS when available, platform LocationManager otherwise
    • Stores platform LocationListener reference for proper cleanup
  • Updated TelemetryCollectorManager.kt:

    • Conditionally creates FusedLocationProviderClient only when Play Services is available
    • Wraps getCurrentLocation() with GMS availability check and fallback to LocationCompat
    • Adds error handling for race conditions with cancellation tokens
  • Updated OfflineMapDownloadScreen.kt:

    • Checks GMS availability before attempting to use FusedLocationProviderClient
    • Falls back to LocationCompat.getCurrentLocation() when GMS is unavailable
  • Updated DiscoveredInterfacesScreen.kt:

    • Checks GMS availability before using FusedLocationProviderClient
    • Falls back to LocationCompat.getCurrentLocation() for location retrieval

Implementation Details

  • Uses double-checked locking pattern for thread-safe one-time GMS availability check
  • Maintains backward compatibility with existing GMS-based code paths
  • Properly handles @SuppressLint("MissingPermission") annotations where permission checks occur at higher levels
  • Ensures location listeners are properly cleaned up to prevent memory leaks
  • Gracefully handles exceptions and provides fallback behavior throughout

https://claude.ai/code/session_01Bv18BreVCSD2GeUg4bHvG1

On devices without Google Play Store (custom ROMs, F-Droid builds),
every FusedLocationProviderClient call generated repeated warnings:
  W GooglePlayServicesUtil: com.lxmf.messenger required the Google Play Store
  W GoogleApiManager: The service for ...location.k is not available

Add LocationCompat utility that checks GMS availability once and logs
at Info level. When GMS is unavailable, falls back to Android's built-in
LocationManager instead of calling GMS APIs that produce the warnings.

Updated all 5 location consumers:
- LocationSharingManager (continuous updates)
- TelemetryCollectorManager (one-shot location)
- MapScreen (continuous updates)
- DiscoveredInterfacesScreen (one-shot location)
- OfflineMapDownloadScreen (one-shot location)

Closes #456

https://claude.ai/code/session_01Bv18BreVCSD2GeUg4bHvG1
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Feb 15, 2026

Greptile Summary

This PR introduces LocationCompat, a utility that gracefully handles location services on devices without Google Play Services. The implementation checks GMS availability once at startup and caches the result using double-checked locking, then provides fallback methods using Android's built-in LocationManager.

Major changes:

  • New LocationCompat utility with thread-safe GMS availability check, last known location retrieval, single location requests (with API level-specific handling for Android R+), and continuous location updates with deduplication
  • Updated 6 files to conditionally use GMS or platform location APIs based on availability
  • Proper lifecycle management: platform LocationListener instances are stored and cleaned up in DisposableEffect (MapScreen) and service cleanup methods (LocationSharingManager)
  • Resolved log flooding issue on custom ROMs and F-Droid builds by preventing unnecessary GMS API calls

Key implementation details:

  • Double-checked locking ensures thread-safe one-time GMS check
  • Pre-Android R devices use requestSingleUpdate with 10-second timeout to prevent indefinite hangs
  • Dual provider registration (GPS + Network) includes 1-second deduplication window to prevent duplicate callbacks
  • Previous review feedback has been addressed: memory leaks fixed, @Volatile added, cancellation limitations documented

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • The implementation is well-designed with proper thread safety (double-checked locking with @Volatile), comprehensive error handling, and appropriate lifecycle management. Previous review feedback has been addressed including memory leak fixes and documentation of known limitations. The changes are backward compatible and provide graceful degradation on devices without GMS.
  • No files require special attention

Important Files Changed

Filename Overview
app/src/main/java/com/lxmf/messenger/util/LocationCompat.kt New utility providing GMS availability check and fallback location APIs. Implements double-checked locking, timeout handling, and deduplication.
app/src/main/java/com/lxmf/messenger/ui/screens/MapScreen.kt Updated to conditionally use GMS or platform location APIs. Properly stores and cleans up platform LocationListener in DisposableEffect.
app/src/main/java/com/lxmf/messenger/service/LocationSharingManager.kt Implements dual-path location updates (GMS/platform). Properly manages platform LocationListener lifecycle with cleanup in stopLocationUpdates().
app/src/main/java/com/lxmf/messenger/service/TelemetryCollectorManager.kt Added GMS availability check and platform fallback. Platform path lacks cancellation handling (acknowledged limitation in comment).

Flowchart

flowchart TD
    Start[App requests location] --> Check{LocationCompat.isPlayServicesAvailable?}
    
    Check -->|Yes - Cached| GMS[Use FusedLocationProviderClient]
    Check -->|No - Cached| Platform[Use Android LocationManager]
    Check -->|First call| DCL[Double-checked locking]
    
    DCL --> GMSCheck{GoogleApiAvailability check}
    GMSCheck -->|SUCCESS| CacheTrue[Cache: available=true]
    GMSCheck -->|Failed/Exception| CacheFalse[Cache: available=false]
    
    CacheTrue --> GMS
    CacheFalse --> Platform
    
    GMS --> GMSLast[getLastLocation]
    GMS --> GMSCurrent[getCurrentLocation with CancellationToken]
    GMS --> GMSUpdates[requestLocationUpdates with LocationCallback]
    
    Platform --> PlatformLast[LocationManager.getLastKnownLocation]
    Platform --> PlatformCurrent{Android version?}
    PlatformCurrent -->|R+| CurrentAPI[LocationManager.getCurrentLocation]
    PlatformCurrent -->|Pre-R| SingleUpdate[requestSingleUpdate + 10s timeout]
    
    Platform --> PlatformUpdates[requestLocationUpdates GPS+Network]
    PlatformUpdates --> Dedup[Deduplicate within 1s window]
    
    GMSUpdates --> Callback[Location delivered to app]
    Dedup --> Callback
    CurrentAPI --> Callback
    SingleUpdate --> Callback
Loading

Last reviewed commit: fa71687

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile


@Volatile
private var checked = false
private var available = false
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing @Volatile annotation

Suggested change
private var available = false
@Volatile
private var available = false
Prompt To Fix With AI
This is a comment left during a code review.
Path: app/src/main/java/com/lxmf/messenger/util/LocationCompat.kt
Line: 33:33

Comment:
missing `@Volatile` annotation

```suggestion
    @Volatile
    private var available = false
```

How can I resolve this? If you propose a fix, please make it concise.

torlando-tech and others added 2 commits February 15, 2026 13:51
Address review findings from Greptile and Sentry AI:

1. Add @volatile to LocationCompat.available field - the double-checked
   locking pattern requires both `checked` and `available` to be volatile,
   otherwise the fast-path read can see a stale `available` value.

2. Add 10-second timeout to LocationCompat.getCurrentLocation() on
   pre-Android 11 devices - requestSingleUpdate() may never invoke its
   callback if no GPS fix is obtained (poor signal, indoor). Without a
   timeout, callers like OfflineMapDownloadScreen get permanently stuck
   showing a loading spinner (isGettingLocation never resets to false).

3. Fix MapScreen LocationListener memory leak - the fallback path called
   LocationCompat.requestLocationUpdates() but discarded the returned
   listener, so it was never removed. Now startLocationUpdates() returns
   the listener, MapScreen stores it in a remembered state var, and a
   DisposableEffect removes it when the composable leaves composition.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use expression body `= Unit` instead of empty block body `{}` for
required LocationListener interface overrides that intentionally
do nothing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@sentry
Copy link
Copy Markdown
Contributor

sentry bot commented Feb 15, 2026

@torlando-tech
Copy link
Copy Markdown
Owner Author

@greptileai

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +752 to 761
} else {
// Fallback to platform LocationManager
if (continuation.isActive) {
LocationCompat.getCurrentLocation(context) { location ->
if (continuation.isActive) {
continuation.resume(location)
}
} catch (e: IllegalArgumentException) {
// CancellationToken was already cancelled - this can happen in race conditions
Log.w(TAG, "Location request cancelled before it could start", e)
if (continuation.isActive) {
continuation.resume(null)
}
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No cancellation handling for platform fallback

In the GMS path, invokeOnCancellation cancels the CancellationTokenSource, but the non-GMS fallback path has no cancellation support. If the coroutine is cancelled while LocationCompat.getCurrentLocation is pending (e.g., during the pre-R 10-second timeout), the platform LocationManager listener will remain registered until the timeout fires and self-cleans. This is a minor resource leak during cancellation.

Consider adding invokeOnCancellation to clean up the platform request, or at minimum adding a comment acknowledging this limitation:

Suggested change
} else {
// Fallback to platform LocationManager
if (continuation.isActive) {
LocationCompat.getCurrentLocation(context) { location ->
if (continuation.isActive) {
continuation.resume(location)
}
} catch (e: IllegalArgumentException) {
// CancellationToken was already cancelled - this can happen in race conditions
Log.w(TAG, "Location request cancelled before it could start", e)
if (continuation.isActive) {
continuation.resume(null)
}
}
}
} else {
// Fallback to platform LocationManager
// Note: cancellation is not forwarded to LocationCompat; the platform
// request will self-clean via its internal 10s timeout if cancelled.
if (continuation.isActive) {
LocationCompat.getCurrentLocation(context) { location ->
if (continuation.isActive) {
continuation.resume(location)
}
}
}
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: app/src/main/java/com/lxmf/messenger/service/TelemetryCollectorManager.kt
Line: 752:761

Comment:
**No cancellation handling for platform fallback**

In the GMS path, `invokeOnCancellation` cancels the `CancellationTokenSource`, but the non-GMS fallback path has no cancellation support. If the coroutine is cancelled while `LocationCompat.getCurrentLocation` is pending (e.g., during the pre-R 10-second timeout), the platform `LocationManager` listener will remain registered until the timeout fires and self-cleans. This is a minor resource leak during cancellation.

Consider adding `invokeOnCancellation` to clean up the platform request, or at minimum adding a comment acknowledging this limitation:

```suggestion
                } else {
                    // Fallback to platform LocationManager
                    // Note: cancellation is not forwarded to LocationCompat; the platform
                    // request will self-clean via its internal 10s timeout if cancelled.
                    if (continuation.isActive) {
                        LocationCompat.getCurrentLocation(context) { location ->
                            if (continuation.isActive) {
                                continuation.resume(location)
                            }
                        }
                    }
                }
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +222 to +244
// Request from GPS if available
if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
intervalMs,
0f,
listener,
Looper.getMainLooper(),
)
}

// Also request from network if available (provides faster initial fix)
if (locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
locationManager.requestLocationUpdates(
LocationManager.NETWORK_PROVIDER,
intervalMs,
0f,
listener,
Looper.getMainLooper(),
)
}

return listener
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate location callbacks from dual provider registration

Registering the same LocationListener with both GPS_PROVIDER and NETWORK_PROVIDER means the onLocation callback can fire twice for the same time period - once from each provider. In LocationSharingManager, this results in duplicate sendLocationToRecipients calls, and in MapScreen it causes redundant updateUserLocation calls.

Consider using a deduplication strategy, such as tracking the last delivered location timestamp and ignoring updates that arrive within a short window:

val listener =
    object : LocationListener {
        private var lastDeliveredTime = 0L

        override fun onLocationChanged(location: Location) {
            val now = SystemClock.elapsedRealtime()
            if (now - lastDeliveredTime > 1000) {
                lastDeliveredTime = now
                onLocation(location)
            }
        }
        // ...
    }
Prompt To Fix With AI
This is a comment left during a code review.
Path: app/src/main/java/com/lxmf/messenger/util/LocationCompat.kt
Line: 222:244

Comment:
**Duplicate location callbacks from dual provider registration**

Registering the same `LocationListener` with both `GPS_PROVIDER` and `NETWORK_PROVIDER` means the `onLocation` callback can fire twice for the same time period - once from each provider. In `LocationSharingManager`, this results in duplicate `sendLocationToRecipients` calls, and in `MapScreen` it causes redundant `updateUserLocation` calls.

Consider using a deduplication strategy, such as tracking the last delivered location timestamp and ignoring updates that arrive within a short window:

```kotlin
val listener =
    object : LocationListener {
        private var lastDeliveredTime = 0L

        override fun onLocationChanged(location: Location) {
            val now = SystemClock.elapsedRealtime()
            if (now - lastDeliveredTime > 1000) {
                lastDeliveredTime = now
                onLocation(location)
            }
        }
        // ...
    }
```

How can I resolve this? If you propose a fix, please make it concise.

…eedback

Wrap GoogleApiAvailability.isGooglePlayServicesAvailable() in try-catch
so it returns false instead of throwing GooglePlayServicesMissingManifestValueException
in unit test environments (fixes 25 failing TelemetryCollectorManagerTest tests).
Add time-based deduplication to requestLocationUpdates() to prevent duplicate
callbacks when both GPS and network providers are registered. Document the
platform fallback cancellation limitation in TelemetryCollectorManager.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@torlando-tech
Copy link
Copy Markdown
Owner Author

@greptileai

@torlando-tech torlando-tech merged commit 179c3c8 into main Feb 16, 2026
10 of 11 checks passed
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.

Do not flood the log with GooglePlayServicesUtil warnings when not having the closed source Google Play stuff installed

2 participants