mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-24 22:45:20 +00:00
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:
@@ -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.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,48 +189,23 @@ 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
|
||||
// Register for continuous updates from available provider
|
||||
locationProvider.requestLocationUpdates(
|
||||
intervalMs = interval,
|
||||
minDistanceMeters = 5f,
|
||||
callback = locationUpdateCallback
|
||||
)
|
||||
|
||||
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}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop periodic refreshes when selector UI is dismissed
|
||||
*/
|
||||
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")
|
||||
|
||||
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)
|
||||
} else {
|
||||
Log.d(TAG, "No last known location available")
|
||||
// Set loading state to true so UI can show a spinner
|
||||
// Set loading state initially
|
||||
_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)
|
||||
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.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
|
||||
)
|
||||
}
|
||||
|
||||
providerFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If no provider was available, turn off loading state
|
||||
if (!providerFound) {
|
||||
Log.w(TAG, "No location providers available")
|
||||
Log.w(TAG, "Failed to get fresh location")
|
||||
_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()
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user