From 6fd2c698c549573d32ab3fd4fb9175110790a6f3 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Thu, 15 Jan 2026 13:39:13 +0700 Subject: [PATCH] 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. --- .../android/geohash/FusedLocationProvider.kt | 15 +++++ .../android/geohash/LocationChannelManager.kt | 1 + .../android/geohash/LocationProvider.kt | 5 ++ .../android/geohash/SystemLocationProvider.kt | 66 ++++++++++++++++++- 4 files changed, 85 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/geohash/FusedLocationProvider.kt b/app/src/main/java/com/bitchat/android/geohash/FusedLocationProvider.kt index 86f6aaea..b6d29c90 100644 --- a/app/src/main/java/com/bitchat/android/geohash/FusedLocationProvider.kt +++ b/app/src/main/java/com/bitchat/android/geohash/FusedLocationProvider.kt @@ -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}") + } + } } diff --git a/app/src/main/java/com/bitchat/android/geohash/LocationChannelManager.kt b/app/src/main/java/com/bitchat/android/geohash/LocationChannelManager.kt index 51ec6545..222cc54e 100644 --- a/app/src/main/java/com/bitchat/android/geohash/LocationChannelManager.kt +++ b/app/src/main/java/com/bitchat/android/geohash/LocationChannelManager.kt @@ -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 diff --git a/app/src/main/java/com/bitchat/android/geohash/LocationProvider.kt b/app/src/main/java/com/bitchat/android/geohash/LocationProvider.kt index 9c9971f9..0ead523d 100644 --- a/app/src/main/java/com/bitchat/android/geohash/LocationProvider.kt +++ b/app/src/main/java/com/bitchat/android/geohash/LocationProvider.kt @@ -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() } diff --git a/app/src/main/java/com/bitchat/android/geohash/SystemLocationProvider.kt b/app/src/main/java/com/bitchat/android/geohash/SystemLocationProvider.kt index 5c00bee2..ef95ec89 100644 --- a/app/src/main/java/com/bitchat/android/geohash/SystemLocationProvider.kt +++ b/app/src/main/java/com/bitchat/android/geohash/SystemLocationProvider.kt @@ -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}") + } + } }