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
This commit is contained in:
callebtc
2026-01-15 14:30:26 +07:00
committed by GitHub
parent c2f60d6ab2
commit 9dfebd73db
4 changed files with 485 additions and 213 deletions
@@ -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}")
}
}
}
@@ -6,11 +6,12 @@ import android.content.IntentFilter
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.location.Geocoder import android.location.Geocoder
import android.location.Location import android.location.Location
import android.location.LocationListener
import android.location.LocationManager import android.location.LocationManager
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.util.* import java.util.*
import com.google.gson.Gson 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 locationManager: LocationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
private val locationProvider: LocationProvider
private val geocoderProvider: GeocoderProvider = GeocoderFactory.get(context) private val geocoderProvider: GeocoderProvider = GeocoderFactory.get(context)
private var lastLocation: Location? = null private var lastLocation: Location? = null
private var geocodingJob: Job? = 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) // Published state for UI bindings (matching iOS @Published properties)
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
@@ -112,7 +118,17 @@ class LocationChannelManager private constructor(private val context: Context) {
init { 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 // Initialize DataManager and load persisted settings
dataManager = com.bitchat.android.ui.DataManager(context) dataManager = com.bitchat.android.ui.DataManager(context)
loadPersistedChannelSelection() loadPersistedChannelSelection()
@@ -173,35 +189,15 @@ class LocationChannelManager private constructor(private val context: Context) {
return return
} }
// Register for continuous updates from available providers // Register for continuous updates from available provider
try { locationProvider.requestLocationUpdates(
if (hasLocationPermission()) { intervalMs = interval,
val providers = listOf( minDistanceMeters = 5f,
LocationManager.GPS_PROVIDER, callback = locationUpdateCallback
LocationManager.NETWORK_PROVIDER )
)
// Prime state immediately with last known / current location
providers.forEach { provider -> requestOneShotLocation()
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}")
}
} }
/** /**
@@ -209,12 +205,7 @@ class LocationChannelManager private constructor(private val context: Context) {
*/ */
fun endLiveRefresh() { fun endLiveRefresh() {
Log.d(TAG, "Ending live refresh") Log.d(TAG, "Ending live refresh")
// Unregister continuous updates listener locationProvider.removeLocationUpdates(locationUpdateCallback)
try {
locationManager.removeUpdates(continuousLocationListener)
} catch (_: SecurityException) {
} catch (_: Exception) {
}
} }
/** /**
@@ -302,201 +293,66 @@ class LocationChannelManager private constructor(private val context: Context) {
// MARK: - Location Operations // MARK: - Location Operations
private fun requestOneShotLocation() { private fun requestOneShotLocation() {
if (!hasLocationPermission()) { if (!checkAndSyncPermission()) {
Log.w(TAG, "No location permission for one-shot request") Log.w(TAG, "No location permission for one-shot request")
return return
} }
Log.d(TAG, "Requesting one-shot location") Log.d(TAG, "Requesting one-shot location")
// Set loading state initially
_isLoadingLocation.value = true
try { locationProvider.getLastKnownLocation { cached ->
// Try to get last known location from all available providers // If we have a cached location and it's reasonably recent (e.g. < 5 mins), use it
var lastKnownLocation: Location? = null // For now, we just use it if it exists, similar to previous logic
if (cached != null) {
// Get all available providers and try each one Log.d(TAG, "Using last known location: ${cached.latitude}, ${cached.longitude}")
val providers = locationManager.getProviders(true) onLocationUpdated(cached)
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)
} else { } else {
Log.d(TAG, "No last known location available") Log.d(TAG, "No last known location available, requesting fresh...")
// Set loading state to true so UI can show a spinner locationProvider.requestFreshLocation { fresh ->
_isLoadingLocation.value = true if (fresh != null) {
Log.d(TAG, "Fresh location received: ${fresh.latitude}, ${fresh.longitude}")
// Request a fresh location only when we don't have a last known location onLocationUpdated(fresh)
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
}
)
} else { } else {
// For older versions, fall back to one-shot requestSingleUpdate Log.w(TAG, "Failed to get fresh location")
locationManager.requestSingleUpdate( _isLoadingLocation.value = false
provider,
oneShotLocationListener,
null // Looper - null uses the main thread
)
} }
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 // MARK: - Helpers
private fun getCurrentPermissionStatus(): PermissionState { private fun getCurrentPermissionStatus(): PermissionState {
return if (hasLocationPermission()) { return if (checkAndSyncPermission()) {
PermissionState.AUTHORIZED PermissionState.AUTHORIZED
} else { } else {
PermissionState.DENIED PermissionState.DENIED
} }
} }
private fun updatePermissionState() { private fun checkAndSyncPermission(): Boolean {
val newState = getCurrentPermissionStatus() val hasPermission = ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED ||
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 ||
ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_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) { private fun computeChannels(location: Location) {
@@ -720,16 +576,12 @@ class LocationChannelManager private constructor(private val context: Context) {
fun cleanup() { fun cleanup() {
Log.d(TAG, "Cleaning up LocationChannelManager") Log.d(TAG, "Cleaning up LocationChannelManager")
endLiveRefresh() endLiveRefresh()
locationProvider.cancel()
geocodingJob?.cancel() geocodingJob?.cancel()
geocodingJob = null geocodingJob = null
// Unregister receiver // Unregister receiver
try { context.unregisterReceiver(locationStateReceiver) } catch (_: Exception) {} 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
} }
} }
@@ -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()
}
@@ -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}")
}
}
}