mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-24 22:45:20 +00:00
Fix and Improve LocationManager logic (#599)
* location manager improvements * fix: resolve compilation errors and improve location state handling - Fix missing imports and stray braces in LocationChannelManager - Implement checkSystemLocationEnabled and BroadcastReceiver for location state - Order class members for correct initialization - Consolidate UI refresh logic in LocationChannelsSheet * fix: guard against stale geocoding results on legacy devices - Add isActive check after blocking Geocoder.getFromLocation call - Prevent stale results from overwriting state after job cancellation
This commit is contained in:
@@ -36,7 +36,18 @@ data class GeohashChannel(
|
|||||||
*/
|
*/
|
||||||
sealed class ChannelID {
|
sealed class ChannelID {
|
||||||
object Mesh : ChannelID()
|
object Mesh : ChannelID()
|
||||||
data class Location(val channel: GeohashChannel) : ChannelID()
|
data class Location(val channel: GeohashChannel) : ChannelID() {
|
||||||
|
companion object {
|
||||||
|
fun fromPersisted(levelName: String, geohash: String): Location? {
|
||||||
|
return try {
|
||||||
|
val level = GeohashChannelLevel.valueOf(levelName)
|
||||||
|
Location(GeohashChannel(level, geohash))
|
||||||
|
} catch (_: IllegalArgumentException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Human readable name for UI.
|
* Human readable name for UI.
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.bitchat.android.geohash
|
|||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
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
|
||||||
@@ -15,7 +16,10 @@ import java.util.*
|
|||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.google.gson.JsonSyntaxException
|
import com.google.gson.JsonSyntaxException
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages location permissions, one-shot location retrieval, and computing geohash channels.
|
* Manages location permissions, one-shot location retrieval, and computing geohash channels.
|
||||||
@@ -38,22 +42,40 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
|
|
||||||
// State enum matching iOS
|
// State enum matching iOS
|
||||||
enum class PermissionState {
|
enum class PermissionState {
|
||||||
NOT_DETERMINED,
|
|
||||||
DENIED,
|
DENIED,
|
||||||
RESTRICTED,
|
|
||||||
AUTHORIZED
|
AUTHORIZED
|
||||||
}
|
}
|
||||||
|
|
||||||
private val locationManager: LocationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
private val locationManager: LocationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||||
private val geocoder: Geocoder = Geocoder(context, Locale.getDefault())
|
private val geocoder: Geocoder = Geocoder(context, Locale.getDefault())
|
||||||
private var lastLocation: Location? = null
|
private var lastLocation: Location? = null
|
||||||
private var refreshTimer: Job? = null
|
private var geocodingJob: Job? = null
|
||||||
private var isGeocoding: Boolean = false
|
|
||||||
private val gson = Gson()
|
private val gson = Gson()
|
||||||
private var dataManager: com.bitchat.android.ui.DataManager? = null
|
private var dataManager: com.bitchat.android.ui.DataManager? = null
|
||||||
|
|
||||||
|
private fun checkSystemLocationEnabled(): Boolean {
|
||||||
|
return try {
|
||||||
|
locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) ||
|
||||||
|
locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val locationStateReceiver = object : android.content.BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context?, intent: android.content.Intent?) {
|
||||||
|
if (intent?.action == LocationManager.PROVIDERS_CHANGED_ACTION) {
|
||||||
|
val isEnabled = checkSystemLocationEnabled()
|
||||||
|
Log.d(TAG, "System location state changed: $isEnabled")
|
||||||
|
_systemLocationEnabled.value = isEnabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Published state for UI bindings (matching iOS @Published properties)
|
// Published state for UI bindings (matching iOS @Published properties)
|
||||||
private val _permissionState = MutableStateFlow(PermissionState.NOT_DETERMINED)
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
||||||
|
|
||||||
|
private val _permissionState = MutableStateFlow(PermissionState.DENIED)
|
||||||
val permissionState: StateFlow<PermissionState> = _permissionState
|
val permissionState: StateFlow<PermissionState> = _permissionState
|
||||||
|
|
||||||
private val _availableChannels = MutableStateFlow<List<GeohashChannel>>(emptyList())
|
private val _availableChannels = MutableStateFlow<List<GeohashChannel>>(emptyList())
|
||||||
@@ -74,12 +96,30 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
private val _locationServicesEnabled = MutableStateFlow(false)
|
private val _locationServicesEnabled = MutableStateFlow(false)
|
||||||
val locationServicesEnabled: StateFlow<Boolean> = _locationServicesEnabled
|
val locationServicesEnabled: StateFlow<Boolean> = _locationServicesEnabled
|
||||||
|
|
||||||
|
private val _systemLocationEnabled = MutableStateFlow(checkSystemLocationEnabled())
|
||||||
|
val systemLocationEnabled: StateFlow<Boolean> = _systemLocationEnabled
|
||||||
|
|
||||||
|
val effectiveLocationEnabled: StateFlow<Boolean> = combine(
|
||||||
|
locationServicesEnabled,
|
||||||
|
systemLocationEnabled
|
||||||
|
) { appToggle, systemToggle ->
|
||||||
|
appToggle && systemToggle
|
||||||
|
}.stateIn(
|
||||||
|
scope,
|
||||||
|
SharingStarted.Eagerly,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
updatePermissionState()
|
updatePermissionState()
|
||||||
// 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()
|
||||||
loadLocationServicesState()
|
loadLocationServicesState()
|
||||||
|
|
||||||
|
// Register for system location changes
|
||||||
|
context.registerReceiver(locationStateReceiver, IntentFilter(LocationManager.PROVIDERS_CHANGED_ACTION))
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Public API (matching iOS interface)
|
// MARK: - Public API (matching iOS interface)
|
||||||
@@ -91,26 +131,19 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
fun enableLocationChannels() {
|
fun enableLocationChannels() {
|
||||||
Log.d(TAG, "enableLocationChannels() called")
|
Log.d(TAG, "enableLocationChannels() called")
|
||||||
|
|
||||||
// UNIFIED FIX: Check if location services are enabled by user
|
if (!_locationServicesEnabled.value || !_systemLocationEnabled.value) {
|
||||||
if (!isLocationServicesEnabled()) {
|
Log.w(TAG, "Location services disabled (app or system) - not requesting location")
|
||||||
Log.w(TAG, "Location services disabled by user - not requesting location")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
when (getCurrentPermissionStatus()) {
|
|
||||||
PermissionState.NOT_DETERMINED -> {
|
if (getCurrentPermissionStatus() == PermissionState.AUTHORIZED) {
|
||||||
Log.d(TAG, "Permission not determined - user needs to grant in app settings")
|
Log.d(TAG, "Permission authorized - requesting location")
|
||||||
_permissionState.value = PermissionState.NOT_DETERMINED
|
_permissionState.value = PermissionState.AUTHORIZED
|
||||||
}
|
requestOneShotLocation()
|
||||||
PermissionState.DENIED, PermissionState.RESTRICTED -> {
|
} else {
|
||||||
Log.d(TAG, "Permission denied or restricted")
|
Log.d(TAG, "Permission not granted")
|
||||||
_permissionState.value = PermissionState.DENIED
|
_permissionState.value = PermissionState.DENIED
|
||||||
}
|
|
||||||
PermissionState.AUTHORIZED -> {
|
|
||||||
Log.d(TAG, "Permission authorized - requesting location")
|
|
||||||
_permissionState.value = PermissionState.AUTHORIZED
|
|
||||||
requestOneShotLocation()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,14 +169,10 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!isLocationServicesEnabled()) {
|
if (!isLocationServicesEnabled()) {
|
||||||
Log.w(TAG, "Cannot start live refresh - location services disabled by user")
|
Log.w(TAG, "Cannot start live refresh - location services disabled")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cancel any existing timer-based refreshers
|
|
||||||
refreshTimer?.cancel()
|
|
||||||
refreshTimer = null
|
|
||||||
|
|
||||||
// Register for continuous updates from available providers
|
// Register for continuous updates from available providers
|
||||||
try {
|
try {
|
||||||
if (hasLocationPermission()) {
|
if (hasLocationPermission()) {
|
||||||
@@ -180,8 +209,6 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
*/
|
*/
|
||||||
fun endLiveRefresh() {
|
fun endLiveRefresh() {
|
||||||
Log.d(TAG, "Ending live refresh")
|
Log.d(TAG, "Ending live refresh")
|
||||||
refreshTimer?.cancel()
|
|
||||||
refreshTimer = null
|
|
||||||
// Unregister continuous updates listener
|
// Unregister continuous updates listener
|
||||||
try {
|
try {
|
||||||
locationManager.removeUpdates(continuousLocationListener)
|
locationManager.removeUpdates(continuousLocationListener)
|
||||||
@@ -235,8 +262,8 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
_locationServicesEnabled.value = true
|
_locationServicesEnabled.value = true
|
||||||
saveLocationServicesState(true)
|
saveLocationServicesState(true)
|
||||||
|
|
||||||
// If we have permission, start location operations
|
// If we have permission and system location is on, start location operations
|
||||||
if (_permissionState.value == PermissionState.AUTHORIZED) {
|
if (_permissionState.value == PermissionState.AUTHORIZED && systemLocationEnabled.value) {
|
||||||
requestOneShotLocation()
|
requestOneShotLocation()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -265,8 +292,11 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
/**
|
/**
|
||||||
* Check if location services are enabled by the user
|
* Check if location services are enabled by the user
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Check if both the app toggle and system location are enabled
|
||||||
|
*/
|
||||||
fun isLocationServicesEnabled(): Boolean {
|
fun isLocationServicesEnabled(): Boolean {
|
||||||
return _locationServicesEnabled.value
|
return _locationServicesEnabled.value && _systemLocationEnabled.value
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Location Operations
|
// MARK: - Location Operations
|
||||||
@@ -451,16 +481,10 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
// MARK: - Helpers
|
// MARK: - Helpers
|
||||||
|
|
||||||
private fun getCurrentPermissionStatus(): PermissionState {
|
private fun getCurrentPermissionStatus(): PermissionState {
|
||||||
return when {
|
return if (hasLocationPermission()) {
|
||||||
ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED -> {
|
PermissionState.AUTHORIZED
|
||||||
PermissionState.AUTHORIZED
|
} else {
|
||||||
}
|
PermissionState.DENIED
|
||||||
ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED -> {
|
|
||||||
PermissionState.AUTHORIZED
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
PermissionState.DENIED // In Android, we can't distinguish between denied and not determined after first ask
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -519,33 +543,70 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isGeocoding) {
|
// Cancel any pending geocoding job to avoid race conditions
|
||||||
Log.d(TAG, "Already geocoding, skipping")
|
geocodingJob?.cancel()
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
isGeocoding = true
|
geocodingJob = scope.launch(Dispatchers.IO) {
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
try {
|
try {
|
||||||
Log.d(TAG, "Starting reverse geocoding")
|
Log.d(TAG, "Starting reverse geocoding")
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
|
||||||
val addresses = geocoder.getFromLocation(location.latitude, location.longitude, 1)
|
// Use new listener-based API for Android 13+
|
||||||
|
suspendCancellableCoroutine<Unit> { cont ->
|
||||||
if (!addresses.isNullOrEmpty()) {
|
try {
|
||||||
val address = addresses[0]
|
geocoder.getFromLocation(
|
||||||
val names = namesByLevel(address)
|
location.latitude,
|
||||||
|
location.longitude,
|
||||||
Log.d(TAG, "Reverse geocoding result: $names")
|
1,
|
||||||
_locationNames.value = names
|
object : Geocoder.GeocodeListener {
|
||||||
|
override fun onGeocode(addresses: MutableList<android.location.Address>) {
|
||||||
|
if (!cont.isActive) return
|
||||||
|
if (addresses.isNotEmpty()) {
|
||||||
|
val address = addresses[0]
|
||||||
|
val names = namesByLevel(address)
|
||||||
|
Log.d(TAG, "Reverse geocoding result: $names")
|
||||||
|
_locationNames.value = names
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "No reverse geocoding results")
|
||||||
|
}
|
||||||
|
cont.resume(Unit) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(errorMessage: String?) {
|
||||||
|
if (!cont.isActive) return
|
||||||
|
Log.e(TAG, "Reverse geocoding failed: $errorMessage")
|
||||||
|
cont.resume(Unit) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (!cont.isActive) return@suspendCancellableCoroutine
|
||||||
|
Log.e(TAG, "Error initiating geocoding listener: ${e.message}")
|
||||||
|
cont.resume(Unit) {}
|
||||||
|
}
|
||||||
|
cont.invokeOnCancellation {
|
||||||
|
// Listener-based API has no explicit unregister, so we just ignore callbacks
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "No reverse geocoding results")
|
@Suppress("DEPRECATION")
|
||||||
|
val addresses = geocoder.getFromLocation(location.latitude, location.longitude, 1)
|
||||||
|
|
||||||
|
if (!isActive) return@launch
|
||||||
|
|
||||||
|
if (!addresses.isNullOrEmpty()) {
|
||||||
|
val address = addresses[0]
|
||||||
|
val names = namesByLevel(address)
|
||||||
|
Log.d(TAG, "Reverse geocoding result: $names")
|
||||||
|
_locationNames.value = names
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "No reverse geocoding results")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Reverse geocoding failed: ${e.message}")
|
if (e !is CancellationException) {
|
||||||
} finally {
|
Log.e(TAG, "Reverse geocoding failed: ${e.message}")
|
||||||
isGeocoding = false
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -599,18 +660,14 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
private fun saveChannelSelection(channel: ChannelID) {
|
private fun saveChannelSelection(channel: ChannelID) {
|
||||||
try {
|
try {
|
||||||
val channelData = when (channel) {
|
val channelData = when (channel) {
|
||||||
is ChannelID.Mesh -> {
|
is ChannelID.Mesh -> gson.toJson(PersistedChannel(mesh = true))
|
||||||
gson.toJson(mapOf("type" to "mesh"))
|
is ChannelID.Location -> gson.toJson(
|
||||||
}
|
PersistedChannel(
|
||||||
is ChannelID.Location -> {
|
mesh = false,
|
||||||
gson.toJson(mapOf(
|
level = channel.channel.level.name,
|
||||||
"type" to "location",
|
geohash = channel.channel.geohash
|
||||||
"level" to channel.channel.level.name,
|
)
|
||||||
"precision" to channel.channel.level.precision,
|
)
|
||||||
"geohash" to channel.channel.geohash,
|
|
||||||
"displayName" to channel.channel.level.displayName
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
dataManager?.saveLastGeohashChannel(channelData)
|
dataManager?.saveLastGeohashChannel(channelData)
|
||||||
Log.d(TAG, "Saved channel selection: ${channel.displayName}")
|
Log.d(TAG, "Saved channel selection: ${channel.displayName}")
|
||||||
@@ -625,46 +682,14 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
private fun loadPersistedChannelSelection() {
|
private fun loadPersistedChannelSelection() {
|
||||||
try {
|
try {
|
||||||
val channelData = dataManager?.loadLastGeohashChannel()
|
val channelData = dataManager?.loadLastGeohashChannel()
|
||||||
if (channelData != null) {
|
if (!channelData.isNullOrBlank()) {
|
||||||
val channelMap = gson.fromJson(channelData, Map::class.java) as? Map<String, Any>
|
val persisted = gson.fromJson(channelData, PersistedChannel::class.java)
|
||||||
if (channelMap != null) {
|
val channel = persisted?.toChannel()
|
||||||
val channel = when (channelMap["type"] as? String) {
|
if (channel != null) {
|
||||||
"mesh" -> ChannelID.Mesh
|
_selectedChannel.value = channel
|
||||||
"location" -> {
|
Log.d(TAG, "Restored persisted channel: ${channel.displayName}")
|
||||||
val levelName = channelMap["level"] as? String
|
|
||||||
val precision = (channelMap["precision"] as? Double)?.toInt()
|
|
||||||
val geohash = channelMap["geohash"] as? String
|
|
||||||
val displayName = channelMap["displayName"] as? String
|
|
||||||
|
|
||||||
if (levelName != null && precision != null && geohash != null && displayName != null) {
|
|
||||||
try {
|
|
||||||
val level = GeohashChannelLevel.valueOf(levelName)
|
|
||||||
val geohashChannel = GeohashChannel(level, geohash)
|
|
||||||
ChannelID.Location(geohashChannel)
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
Log.w(TAG, "Invalid geohash level in persisted data: $levelName")
|
|
||||||
null
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.w(TAG, "Incomplete location channel data in persistence")
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
Log.w(TAG, "Unknown channel type in persisted data: ${channelMap["type"]}")
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (channel != null) {
|
|
||||||
_selectedChannel.value = channel
|
|
||||||
Log.d(TAG, "Restored persisted channel: ${channel.displayName}")
|
|
||||||
} else {
|
|
||||||
Log.d(TAG, "Could not restore persisted channel, defaulting to Mesh")
|
|
||||||
_selectedChannel.value = ChannelID.Mesh
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "Invalid channel data format in persistence")
|
Log.d(TAG, "Could not restore persisted channel, defaulting to Mesh")
|
||||||
_selectedChannel.value = ChannelID.Mesh
|
_selectedChannel.value = ChannelID.Mesh
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -679,7 +704,23 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
_selectedChannel.value = ChannelID.Mesh
|
_selectedChannel.value = ChannelID.Mesh
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class PersistedChannel(
|
||||||
|
val mesh: Boolean,
|
||||||
|
val level: String? = null,
|
||||||
|
val geohash: String? = null
|
||||||
|
) {
|
||||||
|
fun toChannel(): ChannelID? {
|
||||||
|
return if (mesh) {
|
||||||
|
ChannelID.Mesh
|
||||||
|
} else {
|
||||||
|
val levelName = level ?: return null
|
||||||
|
val gh = geohash ?: return null
|
||||||
|
ChannelID.Location.fromPersisted(levelName, gh)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear persisted channel selection (useful for testing or reset)
|
* Clear persisted channel selection (useful for testing or reset)
|
||||||
*/
|
*/
|
||||||
@@ -724,6 +765,12 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
Log.d(TAG, "Cleaning up LocationChannelManager")
|
Log.d(TAG, "Cleaning up LocationChannelManager")
|
||||||
endLiveRefresh()
|
endLiveRefresh()
|
||||||
|
|
||||||
|
geocodingJob?.cancel()
|
||||||
|
geocodingJob = null
|
||||||
|
|
||||||
|
// Unregister receiver
|
||||||
|
try { context.unregisterReceiver(locationStateReceiver) } catch (_: Exception) {}
|
||||||
|
|
||||||
// Remove listeners to prevent memory leaks
|
// Remove listeners to prevent memory leaks
|
||||||
try { locationManager.removeUpdates(oneShotLocationListener) } catch (_: Exception) {}
|
try { locationManager.removeUpdates(oneShotLocationListener) } catch (_: Exception) {}
|
||||||
try { locationManager.removeUpdates(continuousLocationListener) } catch (_: Exception) {}
|
try { locationManager.removeUpdates(continuousLocationListener) } catch (_: Exception) {}
|
||||||
|
|||||||
@@ -63,7 +63,9 @@ fun LocationChannelsSheet(
|
|||||||
val availableChannels by locationManager.availableChannels.collectAsStateWithLifecycle()
|
val availableChannels by locationManager.availableChannels.collectAsStateWithLifecycle()
|
||||||
val selectedChannel by locationManager.selectedChannel.collectAsStateWithLifecycle()
|
val selectedChannel by locationManager.selectedChannel.collectAsStateWithLifecycle()
|
||||||
val locationNames by locationManager.locationNames.collectAsStateWithLifecycle()
|
val locationNames by locationManager.locationNames.collectAsStateWithLifecycle()
|
||||||
val locationServicesEnabled by locationManager.locationServicesEnabled.collectAsStateWithLifecycle()
|
val appLocationEnabled by locationManager.locationServicesEnabled.collectAsStateWithLifecycle()
|
||||||
|
val systemLocationEnabled by locationManager.systemLocationEnabled.collectAsStateWithLifecycle()
|
||||||
|
val locationServicesEnabled by locationManager.effectiveLocationEnabled.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
// Observe bookmarks state
|
// Observe bookmarks state
|
||||||
val bookmarks by bookmarksStore.bookmarks.collectAsStateWithLifecycle()
|
val bookmarks by bookmarksStore.bookmarks.collectAsStateWithLifecycle()
|
||||||
@@ -162,24 +164,7 @@ fun LocationChannelsSheet(
|
|||||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
) {
|
) {
|
||||||
when (permissionState) {
|
when (permissionState) {
|
||||||
LocationChannelManager.PermissionState.NOT_DETERMINED -> {
|
LocationChannelManager.PermissionState.DENIED -> {
|
||||||
Button(
|
|
||||||
onClick = { locationManager.enableLocationChannels() },
|
|
||||||
colors = ButtonDefaults.buttonColors(
|
|
||||||
containerColor = standardGreen.copy(alpha = 0.12f),
|
|
||||||
contentColor = standardGreen
|
|
||||||
),
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.grant_location_permission),
|
|
||||||
fontSize = 12.sp,
|
|
||||||
fontFamily = FontFamily.Monospace
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LocationChannelManager.PermissionState.DENIED,
|
|
||||||
LocationChannelManager.PermissionState.RESTRICTED -> {
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.location_permission_denied),
|
text = stringResource(R.string.location_permission_denied),
|
||||||
@@ -211,20 +196,6 @@ fun LocationChannelsSheet(
|
|||||||
color = standardGreen
|
color = standardGreen
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
null -> {
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
CircularProgressIndicator(modifier = Modifier.size(12.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.checking_permissions),
|
|
||||||
fontSize = 11.sp,
|
|
||||||
fontFamily = FontFamily.Monospace,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -562,34 +533,27 @@ fun LocationChannelsSheet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lifecycle management: when presented, sample both nearby and bookmarked geohashes
|
// Lifecycle management: when presented, manage location updates
|
||||||
|
DisposableEffect(isPresented, permissionState, locationServicesEnabled) {
|
||||||
|
if (isPresented && permissionState == LocationChannelManager.PermissionState.AUTHORIZED && locationServicesEnabled) {
|
||||||
|
locationManager.refreshChannels()
|
||||||
|
locationManager.beginLiveRefresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
onDispose {
|
||||||
|
locationManager.endLiveRefresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sampling management: update sampling when channels/bookmarks change
|
||||||
LaunchedEffect(isPresented, availableChannels, bookmarks) {
|
LaunchedEffect(isPresented, availableChannels, bookmarks) {
|
||||||
if (isPresented) {
|
if (isPresented) {
|
||||||
if (permissionState == LocationChannelManager.PermissionState.AUTHORIZED && locationServicesEnabled) {
|
|
||||||
locationManager.refreshChannels()
|
|
||||||
locationManager.beginLiveRefresh()
|
|
||||||
}
|
|
||||||
val geohashes = (availableChannels.map { it.geohash } + bookmarks).toSet().toList()
|
val geohashes = (availableChannels.map { it.geohash } + bookmarks).toSet().toList()
|
||||||
viewModel.beginGeohashSampling(geohashes)
|
viewModel.beginGeohashSampling(geohashes)
|
||||||
} else {
|
} else {
|
||||||
locationManager.endLiveRefresh()
|
|
||||||
viewModel.endGeohashSampling()
|
viewModel.endGeohashSampling()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// React to permission changes
|
|
||||||
LaunchedEffect(permissionState) {
|
|
||||||
if (permissionState == LocationChannelManager.PermissionState.AUTHORIZED && locationServicesEnabled) {
|
|
||||||
locationManager.refreshChannels()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// React to location services enable/disable
|
|
||||||
LaunchedEffect(locationServicesEnabled) {
|
|
||||||
if (locationServicesEnabled && permissionState == LocationChannelManager.PermissionState.AUTHORIZED) {
|
|
||||||
locationManager.refreshChannels()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ fun LocationNotesButton(
|
|||||||
val selectedLocationChannel by viewModel.selectedLocationChannel.collectAsStateWithLifecycle()
|
val selectedLocationChannel by viewModel.selectedLocationChannel.collectAsStateWithLifecycle()
|
||||||
val locationManager = remember { LocationChannelManager.getInstance(context) }
|
val locationManager = remember { LocationChannelManager.getInstance(context) }
|
||||||
val permissionState by locationManager.permissionState.collectAsStateWithLifecycle()
|
val permissionState by locationManager.permissionState.collectAsStateWithLifecycle()
|
||||||
val locationServicesEnabled by locationManager.locationServicesEnabled.collectAsStateWithLifecycle(false)
|
val locationServicesEnabled by locationManager.effectiveLocationEnabled.collectAsStateWithLifecycle(false)
|
||||||
|
|
||||||
// Check both permission AND location services enabled
|
// Check both permission AND location services enabled
|
||||||
val locationPermissionGranted = permissionState == LocationChannelManager.PermissionState.AUTHORIZED
|
val locationPermissionGranted = permissionState == LocationChannelManager.PermissionState.AUTHORIZED
|
||||||
|
|||||||
Reference in New Issue
Block a user