Implement LocationManagerLocationProvider with multi-provider registration and guardrails - #587
Conversation
…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.
There was a problem hiding this comment.
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.
| private val locationManager = context.getSystemService(Context.LOCATION_SERVICE) | ||
| as LocationManager |
There was a problem hiding this comment.
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?.).
| private val locationManager = context.getSystemService(Context.LOCATION_SERVICE) | |
| as LocationManager | |
| private val locationManager = ContextCompat.getSystemService(context, LocationManager::class.java) |
References
- Look for potential null-safety issues, improper error handling, or resource leaks. (link)
| val request = LocationRequestCompat.Builder(1000L) | ||
| .setMinUpdateIntervalMillis(1000L) | ||
| .setMinUpdateDistanceMeters(0f) | ||
| .setQuality(LocationRequestCompat.QUALITY_HIGH_ACCURACY) | ||
| .build() |
There was a problem hiding this comment.
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.
| 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
- 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)
| 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) |
There was a problem hiding this comment.
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)
}| accepted = true | ||
| Log.d( | ||
| TAG, | ||
| "Updated cached location: provider=${location.provider}, " + | ||
| "acc=${location.accuracy}m" | ||
| ) | ||
| break |
There was a problem hiding this comment.
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
breakReferences
- 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)
Summary
Implements a platform
LocationProviderbacked byLocationManagerCompatin:core:location:location-manager.This is Part 2 of the location series, stacked on #586.
Key Changes
:core:location:location-manager:LocationManagerLocationProvider:GPS_PROVIDER,NETWORK_PROVIDER, andFUSED_PROVIDERon API 31+) to ensure fast acquisition indoors and outdoors.isLocationAvailable()), returningnullwhen location services are turned off even if permissions remain granted.GPS_PROVIDERandPASSIVE_PROVIDER).isBetterLocation()heuristics:LocationManagerLocationProviderTestcovering multi-provider registration, anchor breaking, coarse-only filtering, permission revocation with cached fixes, out-of-order rejection, and periodic refresh.