fix: improve LocationChannelManager resource management

- Add managed CoroutineScope with SupervisorJob that is properly
  cancelled in cleanup() to prevent memory leaks from orphaned coroutines
- Add 120-second timeout for fresh location requests on Android 11+
  using CancellationSignal to prevent indefinite loading states
- Clean up timeout handler and cancellation signal in cleanup()
This commit is contained in:
callebtc
2026-01-08 18:42:19 +07:00
parent e313112d7c
commit 53d283a135
@@ -8,6 +8,9 @@ import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import android.os.Bundle
import android.os.CancellationSignal
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.core.app.ActivityCompat
import kotlinx.coroutines.*
@@ -25,6 +28,7 @@ class LocationChannelManager private constructor(private val context: Context) {
companion object {
private const val TAG = "LocationChannelManager"
private const val LOCATION_TIMEOUT_MS = 120_000L // 120 seconds
@Volatile
private var INSTANCE: LocationChannelManager? = null
@@ -51,6 +55,18 @@ class LocationChannelManager private constructor(private val context: Context) {
private var isGeocoding: Boolean = false
private val gson = Gson()
private var dataManager: com.bitchat.android.ui.DataManager? = null
// Managed coroutine scope for background operations - cancelled in cleanup()
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
// Cancellation signal for location requests with timeout
private var locationCancellationSignal: CancellationSignal? = null
private val locationTimeoutHandler = Handler(Looper.getMainLooper())
private val locationTimeoutRunnable = Runnable {
Log.w(TAG, "Location request timed out after 120 seconds")
locationCancellationSignal?.cancel()
_isLoadingLocation.value = false
}
// Published state for UI bindings (matching iOS @Published properties)
private val _permissionState = MutableStateFlow(PermissionState.NOT_DETERMINED)
@@ -402,12 +418,22 @@ class LocationChannelManager private constructor(private val context: Context) {
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
// For Android 11+ (API 30+), use getCurrentLocation with timeout
locationCancellationSignal?.cancel()
locationCancellationSignal = CancellationSignal()
// Set up timeout
locationTimeoutHandler.removeCallbacks(locationTimeoutRunnable)
locationTimeoutHandler.postDelayed(locationTimeoutRunnable, LOCATION_TIMEOUT_MS)
locationManager.getCurrentLocation(
provider,
null, // No cancellation signal
locationCancellationSignal,
context.mainExecutor,
{ location ->
// Cancel timeout since we got a response
locationTimeoutHandler.removeCallbacks(locationTimeoutRunnable)
if (location != null) {
Log.d(TAG, "Fresh location received: ${location.latitude}, ${location.longitude}")
lastLocation = location
@@ -526,7 +552,7 @@ class LocationChannelManager private constructor(private val context: Context) {
isGeocoding = true
CoroutineScope(Dispatchers.IO).launch {
coroutineScope.launch(Dispatchers.IO) {
try {
Log.d(TAG, "Starting reverse geocoding")
@@ -724,9 +750,15 @@ class LocationChannelManager private constructor(private val context: Context) {
Log.d(TAG, "Cleaning up LocationChannelManager")
endLiveRefresh()
// Cancel any pending location timeout
locationTimeoutHandler.removeCallbacks(locationTimeoutRunnable)
locationCancellationSignal?.cancel()
// Cancel all coroutines to prevent memory leaks
coroutineScope.cancel()
// 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
}
}