mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-24 23:45:19 +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.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}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user