Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 104 additions & 29 deletions app/src/main/java/com/bitchat/android/geohash/LocationChannelManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -126,36 +126,55 @@ class LocationChannelManager private constructor(private val context: Context) {
}

/**
* Begin periodic one-shot location refreshes while a selector UI is visible
* Begin real-time location updates while a selector UI is visible
* Uses requestLocationUpdates for continuous updates, plus a one-shot to prime state immediately
*/
fun beginLiveRefresh(interval: Long = 5000L) {
Log.d(TAG, "Beginning live refresh with interval ${interval}ms")
fun beginLiveRefresh(interval: Long = 5000L) { // interval unused; kept for API compatibility
Log.d(TAG, "Beginning live refresh (continuous updates)")

if (_permissionState.value != PermissionState.AUTHORIZED) {
Log.w(TAG, "Cannot start live refresh - permission not authorized")
return
}

if (!isLocationServicesEnabled()) {
Log.w(TAG, "Cannot start live refresh - location services disabled by user")
return
}

// Cancel existing timer
// Cancel any existing timer-based refreshers
refreshTimer?.cancel()

// Start new timer with coroutines
refreshTimer = CoroutineScope(Dispatchers.IO).launch {
while (isActive) {
if (isLocationServicesEnabled()) {
requestOneShotLocation()
refreshTimer = null

// Register for continuous updates from available providers
try {
if (hasLocationPermission()) {
val providers = listOf(
LocationManager.GPS_PROVIDER,
LocationManager.NETWORK_PROVIDER
)

providers.forEach { provider ->
if (locationManager.isProviderEnabled(provider)) {
// 2s min time, 5m min distance for responsive yet battery-aware updates
locationManager.requestLocationUpdates(
provider,
2000L,
5f,
continuousLocationListener
Comment on lines +160 to +164

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Repeated beginLiveRefresh piles up location listeners

The new real-time refresh registers continuous updates every time beginLiveRefresh is called, but unlike the previous timer-based implementation it never removes an existing listener before calling requestLocationUpdates. In LocationChannelsSheet the LaunchedEffect keyed on availableChannels and bookmarks re-invokes beginLiveRefresh on each channel update while the sheet stays open, so every location result triggers another registration to the same continuousLocationListener. This stacks multiple active listeners, causing duplicate callbacks and unnecessary background location polling (battery and compute overhead) until the sheet is dismissed.

Useful? React with 👍 / 👎.

)
Log.d(TAG, "Registered continuous updates for $provider")
}
}
delay(interval)

// 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}")
}

// Kick off immediately
requestOneShotLocation()
}

/**
Expand All @@ -165,6 +184,12 @@ class LocationChannelManager private constructor(private val context: Context) {
Log.d(TAG, "Ending live refresh")
refreshTimer?.cancel()
refreshTimer = null
// Unregister continuous updates listener
try {
locationManager.removeUpdates(continuousLocationListener)
} catch (_: SecurityException) {
} catch (_: Exception) {
}
}

/**
Expand Down Expand Up @@ -193,6 +218,27 @@ class LocationChannelManager private constructor(private val context: Context) {
Log.d(TAG, "Teleported (immediate recompute): $isTeleportedNow (current: $currentGeohash, selected: ${channel.channel.geohash})")
}
}

/**
* Start background location updates for other features that need live location
* (e.g., Location Notes auto-geohash selection in future). Currently unused but available.
*/
fun startBackgroundLocationUpdates(minTimeMs: Long = 5000L, minDistanceM: Float = 10f) {
if (!hasLocationPermission() || !isLocationServicesEnabled()) return
try {
val providers = listOf(LocationManager.GPS_PROVIDER, LocationManager.NETWORK_PROVIDER)
providers.forEach { provider ->
if (locationManager.isProviderEnabled(provider)) {
locationManager.requestLocationUpdates(provider, minTimeMs, minDistanceM, continuousLocationListener)
}
}
} catch (_: Exception) { }
}

fun stopBackgroundLocationUpdates() {
try { locationManager.removeUpdates(continuousLocationListener) } catch (_: Exception) {}
}

}
}

Expand Down Expand Up @@ -299,24 +345,60 @@ class LocationChannelManager private constructor(private val context: Context) {
}
}

// 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.postValue(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, "Fresh location received: ${location.latitude}, ${location.longitude}")
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.postValue(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
Expand Down Expand Up @@ -668,16 +750,9 @@ class LocationChannelManager private constructor(private val context: Context) {
Log.d(TAG, "Cleaning up LocationChannelManager")
endLiveRefresh()

// For older Android versions, remove any remaining location listener to prevent memory leaks
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.R) {
try {
locationManager.removeUpdates(oneShotLocationListener)
} catch (e: SecurityException) {
Log.e(TAG, "Error removing location listener during cleanup: ${e.message}")
} catch (e: Exception) {
Log.e(TAG, "Error during cleanup: ${e.message}")
}
}
// 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
}
}
Loading