From 9dfebd73dbcdfdda03f94338a39d89d0cd1fa236 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Thu, 15 Jan 2026 14:30:26 +0700 Subject: [PATCH] Refactor: Implement Hybrid Location Provider (System/Fused) (#612) * refactor: abstract LocationProvider, use FusedLocationProvider * Refactor: Sync permission state on check Replaces manual updatePermissionState calls with a unified checkAndSyncPermission method. This ensures that _permissionState flow always reflects the actual system permission status whenever it is checked (e.g. in requestOneShotLocation), preventing desync issues when permissions are revoked at runtime. * 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. * try catch --- .../android/geohash/FusedLocationProvider.kt | 142 +++++++++ .../android/geohash/LocationChannelManager.kt | 278 ++++-------------- .../android/geohash/LocationProvider.kt | 40 +++ .../android/geohash/SystemLocationProvider.kt | 238 +++++++++++++++ 4 files changed, 485 insertions(+), 213 deletions(-) create mode 100644 app/src/main/java/com/bitchat/android/geohash/FusedLocationProvider.kt create mode 100644 app/src/main/java/com/bitchat/android/geohash/LocationProvider.kt create mode 100644 app/src/main/java/com/bitchat/android/geohash/SystemLocationProvider.kt diff --git a/app/src/main/java/com/bitchat/android/geohash/FusedLocationProvider.kt b/app/src/main/java/com/bitchat/android/geohash/FusedLocationProvider.kt new file mode 100644 index 00000000..b6d29c90 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/geohash/FusedLocationProvider.kt @@ -0,0 +1,142 @@ +package com.bitchat.android.geohash + +import android.Manifest +import android.annotation.SuppressLint +import android.content.Context +import android.content.pm.PackageManager +import android.location.Location +import android.os.Looper +import android.util.Log +import androidx.core.app.ActivityCompat +import com.google.android.gms.location.* + +class FusedLocationProvider(private val context: Context) : LocationProvider { + + companion object { + private const val TAG = "FusedLocationProvider" + } + + private val fusedLocationClient: FusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context) + + // Map to keep track of callbacks to remove them later + private val activeCallbacks = mutableMapOf<(Location) -> Unit, LocationCallback>() + + private fun hasLocationPermission(): Boolean { + return ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED || + ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED + } + + @SuppressLint("MissingPermission") + override fun getLastKnownLocation(callback: (Location?) -> Unit) { + if (!hasLocationPermission()) { + callback(null) + return + } + + try { + fusedLocationClient.lastLocation + .addOnSuccessListener { location -> + callback(location) + } + .addOnFailureListener { e -> + Log.e(TAG, "Error getting last known fused location: ${e.message}") + callback(null) + } + } catch (e: Exception) { + Log.e(TAG, "Exception getting last known fused location: ${e.message}") + callback(null) + } + } + + @SuppressLint("MissingPermission") + override fun requestFreshLocation(callback: (Location?) -> Unit) { + if (!hasLocationPermission()) { + callback(null) + return + } + + try { + val request = CurrentLocationRequest.Builder() + .setPriority(Priority.PRIORITY_HIGH_ACCURACY) + .setDurationMillis(30000) + .build() + + fusedLocationClient.getCurrentLocation(request, null) + .addOnSuccessListener { location -> + callback(location) + } + .addOnFailureListener { e -> + Log.e(TAG, "Error getting fresh fused location: ${e.message}") + callback(null) + } + } catch (e: Exception) { + Log.e(TAG, "Exception getting fresh fused location: ${e.message}") + callback(null) + } + } + + @SuppressLint("MissingPermission") + override fun requestLocationUpdates( + intervalMs: Long, + minDistanceMeters: Float, + callback: (Location) -> Unit + ) { + if (!hasLocationPermission()) return + + try { + val request = LocationRequest.Builder(intervalMs) + .setMinUpdateDistanceMeters(minDistanceMeters) + .setPriority(Priority.PRIORITY_HIGH_ACCURACY) + .build() + + val locationCallback = object : LocationCallback() { + override fun onLocationResult(result: LocationResult) { + result.lastLocation?.let { callback(it) } + } + } + + synchronized(activeCallbacks) { + activeCallbacks[callback] = locationCallback + } + + fusedLocationClient.requestLocationUpdates( + request, + locationCallback, + Looper.getMainLooper() + ) + Log.d(TAG, "Registered fused updates") + + } catch (e: Exception) { + Log.e(TAG, "Error requesting fused updates: ${e.message}") + } + } + + override fun removeLocationUpdates(callback: (Location) -> Unit) { + try { + val locationCallback = synchronized(activeCallbacks) { + activeCallbacks.remove(callback) + } + + if (locationCallback != null) { + fusedLocationClient.removeLocationUpdates(locationCallback) + Log.d(TAG, "Removed fused updates") + } + } catch (e: Exception) { + 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 d6542310..222cc54e 100644 --- a/app/src/main/java/com/bitchat/android/geohash/LocationChannelManager.kt +++ b/app/src/main/java/com/bitchat/android/geohash/LocationChannelManager.kt @@ -6,11 +6,12 @@ import android.content.IntentFilter import android.content.pm.PackageManager import android.location.Geocoder import android.location.Location -import android.location.LocationListener import android.location.LocationManager import android.os.Bundle import android.util.Log import androidx.core.app.ActivityCompat +import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.GoogleApiAvailability import kotlinx.coroutines.* import java.util.* import com.google.gson.Gson @@ -47,6 +48,7 @@ class LocationChannelManager private constructor(private val context: Context) { } private val locationManager: LocationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + private val locationProvider: LocationProvider private val geocoderProvider: GeocoderProvider = GeocoderFactory.get(context) private var lastLocation: Location? = null private var geocodingJob: Job? = null @@ -72,6 +74,10 @@ class LocationChannelManager private constructor(private val context: Context) { } } + private val locationUpdateCallback: (Location) -> Unit = { location -> + onLocationUpdated(location) + } + // Published state for UI bindings (matching iOS @Published properties) private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) @@ -112,7 +118,17 @@ class LocationChannelManager private constructor(private val context: Context) { init { - updatePermissionState() + // Choose the best location provider + val availability = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) + locationProvider = if (availability == ConnectionResult.SUCCESS) { + Log.i(TAG, "Using FusedLocationProvider (Google Play Services)") + FusedLocationProvider(context) + } else { + Log.i(TAG, "Using SystemLocationProvider (Native LocationManager)") + SystemLocationProvider(context) + } + + checkAndSyncPermission() // Initialize DataManager and load persisted settings dataManager = com.bitchat.android.ui.DataManager(context) loadPersistedChannelSelection() @@ -173,35 +189,15 @@ class LocationChannelManager private constructor(private val context: Context) { return } - // Register for continuous updates from available providers - try { - if (hasLocationPermission()) { - val providers = listOf( - LocationManager.GPS_PROVIDER, - LocationManager.NETWORK_PROVIDER - ) - - providers.forEach { provider -> - if (locationManager.isProviderEnabled(provider)) { - // 2s min time, 5m min distance for responsive yet battery-aware updates - locationManager.requestLocationUpdates( - provider, - interval, - 5f, - continuousLocationListener - ) - Log.d(TAG, "Registered continuous updates for $provider") - } - } - - // Prime state immediately with last known / current location - requestOneShotLocation() - } - } catch (e: SecurityException) { - Log.e(TAG, "Missing location permission for continuous updates: ${e.message}") - } catch (e: Exception) { - Log.e(TAG, "Failed to register continuous updates: ${e.message}") - } + // Register for continuous updates from available provider + locationProvider.requestLocationUpdates( + intervalMs = interval, + minDistanceMeters = 5f, + callback = locationUpdateCallback + ) + + // Prime state immediately with last known / current location + requestOneShotLocation() } /** @@ -209,12 +205,7 @@ class LocationChannelManager private constructor(private val context: Context) { */ fun endLiveRefresh() { Log.d(TAG, "Ending live refresh") - // Unregister continuous updates listener - try { - locationManager.removeUpdates(continuousLocationListener) - } catch (_: SecurityException) { - } catch (_: Exception) { - } + locationProvider.removeLocationUpdates(locationUpdateCallback) } /** @@ -302,201 +293,66 @@ class LocationChannelManager private constructor(private val context: Context) { // MARK: - Location Operations private fun requestOneShotLocation() { - if (!hasLocationPermission()) { + if (!checkAndSyncPermission()) { Log.w(TAG, "No location permission for one-shot request") return } Log.d(TAG, "Requesting one-shot location") + // Set loading state initially + _isLoadingLocation.value = true - try { - // Try to get last known location from all available providers - var lastKnownLocation: Location? = null - - // Get all available providers and try each one - val providers = locationManager.getProviders(true) - for (provider in providers) { - val location = locationManager.getLastKnownLocation(provider) - if (location != null) { - // If we find a location, check if it's more recent than what we have - if (lastKnownLocation == null || location.time > lastKnownLocation.time) { - lastKnownLocation = location - } - } - } - - if (lastKnownLocation == null) { - lastKnownLocation = lastLocation; - } - - // Use last known location if we have one - if (lastKnownLocation != null) { - Log.d(TAG, "Using last known location: ${lastKnownLocation.latitude}, ${lastKnownLocation.longitude}") - lastLocation = lastKnownLocation - _isLoadingLocation.value = false // Make sure loading state is off - computeChannels(lastKnownLocation) - reverseGeocodeIfNeeded(lastKnownLocation) + locationProvider.getLastKnownLocation { cached -> + // If we have a cached location and it's reasonably recent (e.g. < 5 mins), use it + // For now, we just use it if it exists, similar to previous logic + if (cached != null) { + Log.d(TAG, "Using last known location: ${cached.latitude}, ${cached.longitude}") + onLocationUpdated(cached) } else { - Log.d(TAG, "No last known location available") - // Set loading state to true so UI can show a spinner - _isLoadingLocation.value = true - - // Request a fresh location only when we don't have a last known location - Log.d(TAG, "Requesting fresh location...") - requestFreshLocation() - } - } catch (e: SecurityException) { - Log.e(TAG, "Security exception requesting location: ${e.message}") - _isLoadingLocation.value = false // Turn off loading state on error - updatePermissionState() - } - } - - // Continuous location listener for real-time updates - private val continuousLocationListener = object : LocationListener { - override fun onLocationChanged(location: Location) { - Log.d(TAG, "Real-time location: ${location.latitude}, ${location.longitude} acc=${location.accuracy}m") - lastLocation = location - _isLoadingLocation.value = false - computeChannels(location) - reverseGeocodeIfNeeded(location) - } - - override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) { - // Deprecated but can still be called on older devices - Log.v(TAG, "Provider status changed: $provider -> $status") - } - - override fun onProviderEnabled(provider: String) { - Log.d(TAG, "Provider enabled: $provider") - } - - override fun onProviderDisabled(provider: String) { - Log.d(TAG, "Provider disabled: $provider") - } - } - - // One-time location listener to get a fresh location update - private val oneShotLocationListener = object : LocationListener { - override fun onLocationChanged(location: Location) { - Log.d(TAG, "One-shot location: ${location.latitude}, ${location.longitude}") - lastLocation = location - computeChannels(location) - reverseGeocodeIfNeeded(location) - - // Update loading state to indicate we have a location now - _isLoadingLocation.value = false - - // Remove this listener after getting the update - try { - locationManager.removeUpdates(this) - } catch (e: SecurityException) { - Log.e(TAG, "Error removing location listener: ${e.message}") - } - } - - override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) { - // Required for compatibility with older platform versions - } - - override fun onProviderEnabled(provider: String) { - // Required for compatibility with older platform versions - } - - override fun onProviderDisabled(provider: String) { - // Required for compatibility with older platform versions - } - } - - // Request a fresh location update using getCurrentLocation instead of continuous updates - private fun requestFreshLocation() { - if (!hasLocationPermission()) { - _isLoadingLocation.value = false // Turn off loading state if no permission - return - } - - try { - // Set loading state to true to indicate we're actively trying to get a location - _isLoadingLocation.value = true - - // Try common providers in order of preference - val providers = listOf( - LocationManager.GPS_PROVIDER, - LocationManager.NETWORK_PROVIDER, - LocationManager.PASSIVE_PROVIDER - ) - - var providerFound = false - for (provider in providers) { - if (locationManager.isProviderEnabled(provider)) { - Log.d(TAG, "Getting current location from $provider") - - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { - // For Android 11+ (API 30+), use getCurrentLocation - locationManager.getCurrentLocation( - provider, - null, // No cancellation signal - context.mainExecutor, - { location -> - if (location != null) { - Log.d(TAG, "Fresh location received: ${location.latitude}, ${location.longitude}") - lastLocation = location - computeChannels(location) - reverseGeocodeIfNeeded(location) - } else { - Log.w(TAG, "Received null location from getCurrentLocation") - } - // Update loading state to indicate we have a location now - _isLoadingLocation.value = false - } - ) + Log.d(TAG, "No last known location available, requesting fresh...") + locationProvider.requestFreshLocation { fresh -> + if (fresh != null) { + Log.d(TAG, "Fresh location received: ${fresh.latitude}, ${fresh.longitude}") + onLocationUpdated(fresh) } else { - // For older versions, fall back to one-shot requestSingleUpdate - locationManager.requestSingleUpdate( - provider, - oneShotLocationListener, - null // Looper - null uses the main thread - ) + Log.w(TAG, "Failed to get fresh location") + _isLoadingLocation.value = false } - - providerFound = true - break } } - - // If no provider was available, turn off loading state - if (!providerFound) { - Log.w(TAG, "No location providers available") - _isLoadingLocation.value = false - } - } catch (e: SecurityException) { - Log.e(TAG, "Security exception requesting location: ${e.message}") - _isLoadingLocation.value = false // Turn off loading state on error - } catch (e: Exception) { - Log.e(TAG, "Error requesting location: ${e.message}") - _isLoadingLocation.value = false // Turn off loading state on error } } + private fun onLocationUpdated(location: Location) { + lastLocation = location + _isLoadingLocation.value = false + computeChannels(location) + reverseGeocodeIfNeeded(location) + } + + // MARK: - Helpers private fun getCurrentPermissionStatus(): PermissionState { - return if (hasLocationPermission()) { + return if (checkAndSyncPermission()) { PermissionState.AUTHORIZED } else { PermissionState.DENIED } } - private fun updatePermissionState() { - val newState = getCurrentPermissionStatus() - Log.d(TAG, "Permission state updated to: $newState") - _permissionState.value = newState - } - - private fun hasLocationPermission(): Boolean { - return ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED || + private fun checkAndSyncPermission(): Boolean { + val hasPermission = ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED || ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED + + val newState = if (hasPermission) PermissionState.AUTHORIZED else PermissionState.DENIED + + if (_permissionState.value != newState) { + Log.d(TAG, "Permission state updated to: $newState") + _permissionState.value = newState + } + + return hasPermission } private fun computeChannels(location: Location) { @@ -720,16 +576,12 @@ class LocationChannelManager private constructor(private val context: Context) { fun cleanup() { Log.d(TAG, "Cleaning up LocationChannelManager") endLiveRefresh() + locationProvider.cancel() geocodingJob?.cancel() geocodingJob = null // Unregister receiver try { context.unregisterReceiver(locationStateReceiver) } catch (_: Exception) {} - - // Remove listeners to prevent memory leaks - try { locationManager.removeUpdates(oneShotLocationListener) } catch (_: Exception) {} - try { locationManager.removeUpdates(continuousLocationListener) } catch (_: Exception) {} - // For Android 11+, getCurrentLocation doesn't need explicit cleanup as it's a one-time operation } } diff --git a/app/src/main/java/com/bitchat/android/geohash/LocationProvider.kt b/app/src/main/java/com/bitchat/android/geohash/LocationProvider.kt new file mode 100644 index 00000000..0ead523d --- /dev/null +++ b/app/src/main/java/com/bitchat/android/geohash/LocationProvider.kt @@ -0,0 +1,40 @@ +package com.bitchat.android.geohash + +import android.location.Location + +/** + * Abstraction for location providers to support both + * System (LocationManager) and Google Play Services (FusedLocationProvider). + */ +interface LocationProvider { + /** + * Get the last known location from cache. + * @param callback Called with the location or null if not found/error. + */ + fun getLastKnownLocation(callback: (Location?) -> Unit) + + /** + * Request a single, fresh location update. + * @param callback Called with the location or null if failed. + */ + fun requestFreshLocation(callback: (Location?) -> Unit) + + /** + * Request continuous location updates. + * @param intervalMs Desired interval in milliseconds. + * @param minDistanceMeters Minimum distance in meters. + * @param callback Called when location updates. + */ + fun requestLocationUpdates(intervalMs: Long, minDistanceMeters: Float, callback: (Location) -> Unit) + + /** + * Stop location updates. + * @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 new file mode 100644 index 00000000..61c9d267 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/geohash/SystemLocationProvider.kt @@ -0,0 +1,238 @@ +package com.bitchat.android.geohash + +import android.Manifest +import android.annotation.SuppressLint +import android.content.Context +import android.content.pm.PackageManager +import android.location.Location +import android.location.LocationListener +import android.location.LocationManager +import android.os.Build +import android.os.Bundle +import android.util.Log +import androidx.core.app.ActivityCompat + +class SystemLocationProvider(private val context: Context) : LocationProvider { + + companion object { + private const val TAG = "SystemLocationProvider" + } + + 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 || + ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED + } + + @SuppressLint("MissingPermission") + override fun getLastKnownLocation(callback: (Location?) -> Unit) { + if (!hasLocationPermission()) { + callback(null) + return + } + + try { + var bestLocation: Location? = null + val providers = locationManager.getProviders(true) + for (provider in providers) { + val location = locationManager.getLastKnownLocation(provider) + if (location != null) { + if (bestLocation == null || location.time > bestLocation.time) { + bestLocation = location + } + } + } + callback(bestLocation) + } catch (e: Exception) { + Log.e(TAG, "Error getting last known location: ${e.message}") + callback(null) + } + } + + @SuppressLint("MissingPermission") + override fun requestFreshLocation(callback: (Location?) -> Unit) { + if (!hasLocationPermission()) { + callback(null) + return + } + + try { + val providers = listOf( + LocationManager.GPS_PROVIDER, + LocationManager.NETWORK_PROVIDER, + LocationManager.PASSIVE_PROVIDER + ) + + var providerFound = false + for (provider in providers) { + if (locationManager.isProviderEnabled(provider)) { + Log.d(TAG, "Requesting fresh location from $provider") + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + locationManager.getCurrentLocation( + provider, + null, + context.mainExecutor + ) { location -> + callback(location) + } + } else { + // 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) { + synchronized(activeOneShotListeners) { + activeOneShotListeners.remove(callback) + val runnable = activeOneShotRunnables.remove(callback) + if (runnable != null) { + handler.removeCallbacks(runnable) + } + } + try { + locationManager.removeUpdates(this) + } catch (e: Exception) { + Log.e(TAG, "Error removing updates in callback: ${e.message}") + } + 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 + } + } + + if (!providerFound) { + Log.w(TAG, "No location providers available for fresh location") + callback(null) + } + } catch (e: Exception) { + Log.e(TAG, "Error requesting fresh location: ${e.message}") + callback(null) + } + } + + @SuppressLint("MissingPermission") + override fun requestLocationUpdates( + intervalMs: Long, + minDistanceMeters: Float, + callback: (Location) -> Unit + ) { + if (!hasLocationPermission()) return + + try { + val listener = object : LocationListener { + override fun onLocationChanged(location: Location) { + callback(location) + } + override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {} + override fun onProviderEnabled(provider: String) {} + override fun onProviderDisabled(provider: String) {} + } + + // Store the listener so we can remove it later + synchronized(activeListeners) { + activeListeners[callback] = listener + } + + val providers = listOf(LocationManager.GPS_PROVIDER, LocationManager.NETWORK_PROVIDER) + var registered = false + + for (provider in providers) { + if (locationManager.isProviderEnabled(provider)) { + locationManager.requestLocationUpdates( + provider, + intervalMs, + minDistanceMeters, + listener + ) + registered = true + Log.d(TAG, "Registered updates for $provider") + } + } + + if (!registered) { + Log.w(TAG, "No providers enabled for continuous updates") + } + + } catch (e: Exception) { + Log.e(TAG, "Error requesting location updates: ${e.message}") + } + } + + override fun removeLocationUpdates(callback: (Location) -> Unit) { + try { + val listener = synchronized(activeListeners) { + activeListeners.remove(callback) + } + + if (listener != null) { + locationManager.removeUpdates(listener) + Log.d(TAG, "Removed location updates") + } + } catch (e: Exception) { + 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}") + } + } +}