Skip to content

Implement LocationManagerLocationProvider with multi-provider registration and guardrails - #587

Open
temcguir wants to merge 1 commit into
temcguir/location-permissions-and-settings-uifrom
temcguir/location-locationmanager-provider
Open

temcguir wants to merge 1 commit into
temcguir/location-permissions-and-settings-uifrom
temcguir/location-locationmanager-provider

Conversation

@temcguir

Copy link
Copy Markdown
Collaborator

Summary

Implements a platform LocationProvider backed by LocationManagerCompat in :core:location:location-manager.

This is Part 2 of the location series, stacked on #586.

Key Changes

  • :core:location:location-manager:
    • Implements LocationManagerLocationProvider:
      • Concurrently registers available platform providers (GPS_PROVIDER, NETWORK_PROVIDER, and FUSED_PROVIDER on API 31+) to ensure fast acquisition indoors and outdoors.
      • Enforces runtime permission checks and device master location state (isLocationAvailable()), returning null when location services are turned off even if permissions remain granted.
      • Restricts coarse-only permission queries to avoid attempting fine-only platform queries (GPS_PROVIDER and PASSIVE_PROVIDER).
      • Implements time-aware isBetterLocation() heuristics:
        • Fixes >2 minutes newer supersede older fixes regardless of accuracy, preventing coordinate anchoring while traveling.
        • Within the 2-minute window, fixes must be more accurate, newer and at least as accurate, or newer from the same provider and no more than 20 meters less accurate.
      • Evicts stale fixes older than 30 minutes from the cache and incoming updates.
      • Applies a 60-second hardware timeout to conserve battery when a fix <= 50 meters is not achieved, and automatically stops updates early once a fix <= 50 meters is accepted into the cache.
      • Periodically re-executes a warmup cycle every 5 minutes during extended preview sessions to refresh coordinates.
      • Filters out NaN, infinite, and (0, 0) coordinates.
  • Testing:
    • Adds 28 Robolectric unit tests in LocationManagerLocationProviderTest covering multi-provider registration, anchor breaking, coarse-only filtering, permission revocation with cached fixes, out-of-order rejection, and periodic refresh.

…ation and guardrails

Introduces a platform LocationProvider implementation backed by LocationManagerCompat in :core:location:location-manager.

Key features:
- Concurrently registers available platform providers (GPS_PROVIDER, NETWORK_PROVIDER, and FUSED_PROVIDER on API 31+) to ensure fast location acquisition indoors and outdoors.
- Enforces runtime permission checks and device master location state (isLocationAvailable()), returning null when location services are turned off even if permissions are granted.
- Restricts coarse-only permission queries to avoid attempting fine-only platform queries (GPS_PROVIDER and PASSIVE_PROVIDER).
- Implements time-aware isBetterLocation() heuristics:
  - Fixes >2 minutes newer supersede older fixes regardless of accuracy, preventing coordinate anchoring while traveling.
  - Within the 2-minute window, fixes must be more accurate, newer and at least as accurate, or newer from the same provider and no more than 20 meters less accurate.
- Evicts stale fixes older than 30 minutes from the cache and incoming updates.
- Applies a 60-second hardware timeout to conserve battery when a fix <= 50 meters is not achieved, and automatically stops updates early once a fix <= 50 meters is accepted into the cache.
- Periodically re-executes a warmup cycle every 5 minutes during long camera preview sessions to refresh coordinates.
- Filters out NaN, infinite, and (0, 0) coordinates.
- Adds 28 Robolectric unit tests covering multi-provider registration, anchor-breaking, permission revocation with cached fixes, out-of-order rejection, and periodic refresh.

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request introduces the :core:location:location-manager module, implementing LocationManagerLocationProvider to provide location updates using the platform's LocationManager via LocationManagerCompat, along with comprehensive Robolectric tests. Feedback on the implementation includes safely retrieving the LocationManager to prevent potential null pointer exceptions on unsupported devices, extracting magic numbers into constants, checking provider availability before querying last known locations to avoid exception overhead, and removing high-frequency debug logs to prevent log spam.

Comment on lines +65 to +66
private val locationManager = context.getSystemService(Context.LOCATION_SERVICE)
as LocationManager

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.

high

Using context.getSystemService(Context.LOCATION_SERVICE) as LocationManager is unsafe because getSystemService can return null on devices or form factors (such as some Wear OS, Android TV, or custom embedded devices) that do not support location services. This unsafe cast will cause a NullPointerException or TypeCastException during class initialization.

Consider retrieving the service safely using ContextCompat.getSystemService and treating locationManager as nullable (LocationManager?), guarding its usage throughout the class with safe calls (locationManager?.).

Suggested change
private val locationManager = context.getSystemService(Context.LOCATION_SERVICE)
as LocationManager
private val locationManager = ContextCompat.getSystemService(context, LocationManager::class.java)
References
  1. Look for potential null-safety issues, improper error handling, or resource leaks. (link)

Comment on lines +123 to +127
val request = LocationRequestCompat.Builder(1000L)
.setMinUpdateIntervalMillis(1000L)
.setMinUpdateDistanceMeters(0f)
.setQuality(LocationRequestCompat.QUALITY_HIGH_ACCURACY)
.build()

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.

medium

Avoid using magic numbers like 1000L and 0f directly in the location request builder. Consider extracting these values into private constants at the top of the file (e.g., LOCATION_UPDATE_INTERVAL_MS and LOCATION_UPDATE_DISTANCE_METERS) to improve readability and maintainability, consistent with other constants defined in this file.

Suggested change
val request = LocationRequestCompat.Builder(1000L)
.setMinUpdateIntervalMillis(1000L)
.setMinUpdateDistanceMeters(0f)
.setQuality(LocationRequestCompat.QUALITY_HIGH_ACCURACY)
.build()
val request = LocationRequestCompat.Builder(LOCATION_UPDATE_INTERVAL_MS)
.setMinUpdateIntervalMillis(LOCATION_UPDATE_INTERVAL_MS)
.setMinUpdateDistanceMeters(LOCATION_UPDATE_DISTANCE_METERS)
.setQuality(LocationRequestCompat.QUALITY_HIGH_ACCURACY)
.build()
References
  1. Avoid Magic Numbers: Avoid scattering literal dimension values or scales directly in the layout code. Instead, group them into a private object Tokens at the top of the file if file-scoped, or in a separate Dimensions.kt or Tokens.kt file if shared across features. (link)

Comment on lines +180 to +184
val providers = buildList {
// GPS and PASSIVE providers require ACCESS_FINE_LOCATION.
if (hasFine) add(LocationManager.GPS_PROVIDER)
add(LocationManager.NETWORK_PROVIDER)
if (hasFine) add(LocationManager.PASSIVE_PROVIDER)

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.

medium

Querying getLastKnownLocation on unsupported providers will throw an IllegalArgumentException. Although these exceptions are caught and logged as warnings on lines 203-207, throwing and catching exceptions in Kotlin/Java carries performance overhead. Consider checking if the provider is supported using LocationManagerCompat.hasProvider before adding it to the query list.

        val providers = buildList {
            // GPS and PASSIVE providers require ACCESS_FINE_LOCATION.
            if (hasFine && LocationManagerCompat.hasProvider(locationManager, LocationManager.GPS_PROVIDER)) {
                add(LocationManager.GPS_PROVIDER)
            }
            if (LocationManagerCompat.hasProvider(locationManager, LocationManager.NETWORK_PROVIDER)) {
                add(LocationManager.NETWORK_PROVIDER)
            }
            if (hasFine && LocationManagerCompat.hasProvider(locationManager, LocationManager.PASSIVE_PROVIDER)) {
                add(LocationManager.PASSIVE_PROVIDER)
            }

Comment on lines +220 to +226
accepted = true
Log.d(
TAG,
"Updated cached location: provider=${location.provider}, " +
"acc=${location.accuracy}m"
)
break

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.

medium

This debug log is triggered on every single location update. During active tracking, location updates can be highly frequent (e.g., every second), which will flood the logcat, cluttering the logs and potentially impacting performance. Consider removing this debug log or restricting it to avoid log spam.

                accepted = true
                break
References
  1. Scrutinize Debug Logs: Question the use of Log.d, Log.v, and especially println(). These are often remnants of debugging and should be removed before merging unless they provide essential, long-term value. (link)

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