Refactor: Add timeout and tracking to SystemLocationProvider

Implements robust cleanup and timeout logic for location requests.
- SystemLocationProvider: Adds 30s timeout and listener tracking for legacy one-shot requests to prevent memory leaks on pre-Android 11 devices.
- FusedLocationProvider: Adds 30s duration to requests.
- LocationProvider: Adds cancel() method for full resource cleanup.
- LocationChannelManager: Ensures cancel() is called during cleanup.
This commit is contained in:
callebtc
2026-01-15 13:39:13 +07:00
parent e96adb6250
commit 6fd2c698c5
4 changed files with 85 additions and 2 deletions
@@ -58,6 +58,7 @@ class FusedLocationProvider(private val context: Context) : LocationProvider {
try {
val request = CurrentLocationRequest.Builder()
.setPriority(Priority.PRIORITY_HIGH_ACCURACY)
.setDurationMillis(30000)
.build()
fusedLocationClient.getCurrentLocation(request, null)
@@ -124,4 +125,18 @@ class FusedLocationProvider(private val context: Context) : LocationProvider {
Log.e(TAG, "Error removing fused updates: ${e.message}")
}
}
override fun cancel() {
try {
synchronized(activeCallbacks) {
for ((callback, locationCallback) in activeCallbacks) {
fusedLocationClient.removeLocationUpdates(locationCallback)
}
activeCallbacks.clear()
}
Log.d(TAG, "Cancelled all fused updates")
} catch (e: Exception) {
Log.e(TAG, "Error cancelling fused provider: ${e.message}")
}
}
}
@@ -576,6 +576,7 @@ class LocationChannelManager private constructor(private val context: Context) {
fun cleanup() {
Log.d(TAG, "Cleaning up LocationChannelManager")
endLiveRefresh()
locationProvider.cancel()
geocodingJob?.cancel()
geocodingJob = null
@@ -32,4 +32,9 @@ interface LocationProvider {
* @param callback The same callback instance passed to requestLocationUpdates.
*/
fun removeLocationUpdates(callback: (Location) -> Unit)
/**
* Cancel any pending one-shot location requests and cleanup resources.
*/
fun cancel()
}
@@ -19,8 +19,12 @@ class SystemLocationProvider(private val context: Context) : LocationProvider {
}
private val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
private val handler = android.os.Handler(android.os.Looper.getMainLooper())
// Map to keep track of listeners to unregister them later
private val activeListeners = mutableMapOf<(Location) -> Unit, LocationListener>()
private val activeOneShotListeners = mutableMapOf<(Location?) -> Unit, LocationListener>()
private val activeOneShotRunnables = mutableMapOf<(Location?) -> Unit, Runnable>()
private fun hasLocationPermission(): Boolean {
return ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED ||
@@ -80,17 +84,47 @@ class SystemLocationProvider(private val context: Context) : LocationProvider {
callback(location)
}
} else {
// For older versions, use requestSingleUpdate
// For older versions, use requestSingleUpdate with timeout mechanism
val timeoutRunnable = Runnable {
Log.w(TAG, "Location request timed out")
synchronized(activeOneShotListeners) {
val listener = activeOneShotListeners.remove(callback)
activeOneShotRunnables.remove(callback)
if (listener != null) {
try {
locationManager.removeUpdates(listener)
} catch (e: Exception) {
Log.e(TAG, "Error removing timed out listener: ${e.message}")
}
}
}
callback(null)
}
val listener = object : LocationListener {
override fun onLocationChanged(location: Location) {
callback(location)
synchronized(activeOneShotListeners) {
activeOneShotListeners.remove(callback)
val runnable = activeOneShotRunnables.remove(callback)
if (runnable != null) {
handler.removeCallbacks(runnable)
}
}
locationManager.removeUpdates(this)
callback(location)
}
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {}
override fun onProviderEnabled(provider: String) {}
override fun onProviderDisabled(provider: String) {}
}
synchronized(activeOneShotListeners) {
activeOneShotListeners[callback] = listener
activeOneShotRunnables[callback] = timeoutRunnable
}
locationManager.requestSingleUpdate(provider, listener, null)
handler.postDelayed(timeoutRunnable, 30000L) // 30s timeout
}
providerFound = true
break
@@ -169,4 +203,32 @@ class SystemLocationProvider(private val context: Context) : LocationProvider {
Log.e(TAG, "Error removing updates: ${e.message}")
}
}
override fun cancel() {
try {
// Cancel continuous updates
synchronized(activeListeners) {
for ((_, listener) in activeListeners) {
try { locationManager.removeUpdates(listener) } catch (_: Exception) {}
}
activeListeners.clear()
}
// Cancel one-shot requests
synchronized(activeOneShotListeners) {
for ((_, listener) in activeOneShotListeners) {
try { locationManager.removeUpdates(listener) } catch (_: Exception) {}
}
activeOneShotListeners.clear()
for ((_, runnable) in activeOneShotRunnables) {
handler.removeCallbacks(runnable)
}
activeOneShotRunnables.clear()
}
Log.d(TAG, "Cancelled all system location requests")
} catch (e: Exception) {
Log.e(TAG, "Error cancelling system provider: ${e.message}")
}
}
}