Improve system location provider fallback

This commit is contained in:
liuyunming521520-cell 2026-09-09 21:09:08 +08:00
parent ae489a2439
commit e49daae804

View File

@ -10,6 +10,7 @@ import android.location.LocationManager
import android.os.Build
import android.os.Bundle
import android.os.CancellationSignal
import android.os.SystemClock
import android.util.Log
import androidx.core.app.ActivityCompat
@ -17,19 +18,41 @@ internal class SystemLocationProvider(private val context: Context) : LocationPr
companion object {
private const val TAG = "SystemLocationProvider"
private const val FRESH_LOCATION_TIMEOUT_MS = 30_000L
}
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
// 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 val activeOneShotCancellationSignals = mutableMapOf<(Location?) -> Unit, CancellationSignal>()
private val activeOneShotRequests = mutableSetOf<PendingOneShot>()
private class PendingOneShot(
val callback: (Location?) -> Unit,
val providers: List<String>,
val deadlineElapsedRealtime: Long
) {
var nextProviderIndex = 0
var cancellationSignal: CancellationSignal? = null
var listener: LocationListener? = null
var timeoutRunnable: Runnable? = null
var completed = false
}
private data class ProviderAttempt(
val provider: String,
val timeoutMs: Long
)
private data class OneShotResources(
val cancellationSignal: CancellationSignal?,
val listener: LocationListener?,
val timeoutRunnable: Runnable?
)
private fun hasLocationPermission(): Boolean {
return LiveLocationPrivacyGate.isEnabled &&
return LiveLocationPrivacyGate.captureToken() != null &&
(ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED ||
ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED
)
@ -53,7 +76,7 @@ internal class SystemLocationProvider(private val context: Context) : LocationPr
}
}
}
callback(bestLocation.takeIf { LiveLocationPrivacyGate.isEnabled })
callback(bestLocation.takeIf { hasLocationPermission() })
} catch (e: Exception) {
Log.e(TAG, "Error getting last-known location")
callback(null)
@ -67,100 +90,279 @@ internal class SystemLocationProvider(private val context: Context) : LocationPr
return
}
try {
val providers = listOf(
val request = PendingOneShot(
callback = callback,
providers = listOf(
LocationManager.GPS_PROVIDER,
LocationManager.NETWORK_PROVIDER,
LocationManager.PASSIVE_PROVIDER
)
LocationManager.NETWORK_PROVIDER
),
deadlineElapsedRealtime = SystemClock.elapsedRealtime() + FRESH_LOCATION_TIMEOUT_MS
)
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) {
val cancellationSignal = CancellationSignal()
synchronized(activeOneShotCancellationSignals) {
activeOneShotCancellationSignals[callback] = cancellationSignal
}
try {
locationManager.getCurrentLocation(
provider,
cancellationSignal,
context.mainExecutor
) { location ->
synchronized(activeOneShotCancellationSignals) {
activeOneShotCancellationSignals.remove(callback)
}
callback(location.takeIf { LiveLocationPrivacyGate.isEnabled })
}
} catch (e: Exception) {
synchronized(activeOneShotCancellationSignals) {
activeOneShotCancellationSignals.remove(callback)
}
cancellationSignal.cancel()
throw e
}
} 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")
}
}
}
callback(null)
}
synchronized(activeOneShotRequests) {
activeOneShotRequests += request
}
startNextOneShotProvider(request)
}
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")
}
callback(location.takeIf { LiveLocationPrivacyGate.isEnabled })
}
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {}
override fun onProviderEnabled(provider: String) {}
override fun onProviderDisabled(provider: String) {}
private fun startNextOneShotProvider(request: PendingOneShot) {
var shouldFinish = false
val attempt = synchronized(activeOneShotRequests) {
if (!isOneShotActiveLocked(request)) {
null
} else if (!hasLocationPermission()) {
shouldFinish = true
null
} else {
val remainingMs = request.deadlineElapsedRealtime - SystemClock.elapsedRealtime()
if (remainingMs <= 0L) {
shouldFinish = true
null
} else {
var nextAttempt: ProviderAttempt? = null
while (request.nextProviderIndex < request.providers.size && nextAttempt == null) {
val provider = request.providers[request.nextProviderIndex++]
if (isProviderEnabled(provider)) {
val remainingEnabledProviders = 1 + request.providers
.drop(request.nextProviderIndex)
.count(::isProviderEnabled)
nextAttempt = ProviderAttempt(
provider = provider,
timeoutMs = maxOf(1L, remainingMs / remainingEnabledProviders)
)
}
synchronized(activeOneShotListeners) {
activeOneShotListeners[callback] = listener
activeOneShotRunnables[callback] = timeoutRunnable
}
locationManager.requestSingleUpdate(provider, listener, null)
handler.postDelayed(timeoutRunnable, 30000L) // 30s timeout
}
providerFound = true
break
if (nextAttempt == null) {
shouldFinish = true
}
nextAttempt
}
}
}
when {
attempt != null -> requestOneShotFromProvider(request, attempt)
shouldFinish -> finishOneShot(request, null)
}
}
@SuppressLint("MissingPermission")
private fun requestOneShotFromProvider(
request: PendingOneShot,
attempt: ProviderAttempt
) {
Log.d(TAG, "Requesting fresh location from ${attempt.provider}")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
requestCurrentLocation(request, attempt)
} else {
requestSingleLocationUpdate(request, attempt)
}
}
@SuppressLint("MissingPermission")
private fun requestCurrentLocation(
request: PendingOneShot,
attempt: ProviderAttempt
) {
val cancellationSignal = CancellationSignal()
val timeout = Runnable {
val resources = detachCurrentProvider(
request = request,
expectedCancellationSignal = cancellationSignal
) ?: return@Runnable
Log.w(TAG, "Location request timed out for ${attempt.provider}")
releaseOneShotResources(resources)
startNextOneShotProvider(request)
}
if (!attachCurrentProvider(request, cancellationSignal, null, timeout)) return
handler.postDelayed(timeout, attempt.timeoutMs)
try {
locationManager.getCurrentLocation(
attempt.provider,
cancellationSignal,
context.mainExecutor
) { location ->
val resources = detachCurrentProvider(
request = request,
expectedCancellationSignal = cancellationSignal
) ?: return@getCurrentLocation
releaseOneShotResources(resources)
if (location != null && hasLocationPermission()) {
finishOneShot(request, location)
} else {
startNextOneShotProvider(request)
}
}
} catch (e: Exception) {
val resources = detachCurrentProvider(
request = request,
expectedCancellationSignal = cancellationSignal
)
if (resources != null) {
Log.w(TAG, "Unable to request ${attempt.provider} location", e)
releaseOneShotResources(resources)
startNextOneShotProvider(request)
}
}
}
@SuppressLint("MissingPermission")
private fun requestSingleLocationUpdate(
request: PendingOneShot,
attempt: ProviderAttempt
) {
val listener = object : LocationListener {
override fun onLocationChanged(location: Location) {
val resources = detachCurrentProvider(
request = request,
expectedListener = this
) ?: return
releaseOneShotResources(resources)
if (hasLocationPermission()) {
finishOneShot(request, location)
} else {
startNextOneShotProvider(request)
}
}
if (!providerFound) {
Log.w(TAG, "No location providers available for fresh location")
callback(null)
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) = Unit
override fun onProviderEnabled(provider: String) = Unit
override fun onProviderDisabled(provider: String) = Unit
}
val timeout = Runnable {
val resources = detachCurrentProvider(
request = request,
expectedListener = listener
) ?: return@Runnable
Log.w(TAG, "Location request timed out for ${attempt.provider}")
releaseOneShotResources(resources)
startNextOneShotProvider(request)
}
if (!attachCurrentProvider(request, null, listener, timeout)) return
handler.postDelayed(timeout, attempt.timeoutMs)
try {
locationManager.requestSingleUpdate(attempt.provider, listener, null)
if (!isCurrentProviderRequest(request, expectedListener = listener)) {
runCatching { locationManager.removeUpdates(listener) }
}
} catch (e: Exception) {
Log.e(TAG, "Error requesting fresh location")
callback(null)
val resources = detachCurrentProvider(
request = request,
expectedListener = listener
)
if (resources != null) {
Log.w(TAG, "Unable to request ${attempt.provider} location", e)
releaseOneShotResources(resources)
startNextOneShotProvider(request)
}
}
}
private fun isProviderEnabled(provider: String): Boolean {
return try {
locationManager.isProviderEnabled(provider)
} catch (e: Exception) {
Log.w(TAG, "Unable to inspect $provider provider", e)
false
}
}
private fun attachCurrentProvider(
request: PendingOneShot,
cancellationSignal: CancellationSignal?,
listener: LocationListener?,
timeoutRunnable: Runnable
): Boolean = synchronized(activeOneShotRequests) {
if (!isOneShotActiveLocked(request) || !hasLocationPermission()) {
false
} else {
request.cancellationSignal = cancellationSignal
request.listener = listener
request.timeoutRunnable = timeoutRunnable
true
}
}
private fun isCurrentProviderRequest(
request: PendingOneShot,
expectedCancellationSignal: CancellationSignal? = null,
expectedListener: LocationListener? = null
): Boolean = synchronized(activeOneShotRequests) {
if (!isOneShotActiveLocked(request)) {
false
} else {
(expectedCancellationSignal == null || request.cancellationSignal === expectedCancellationSignal) &&
(expectedListener == null || request.listener === expectedListener)
}
}
private fun detachCurrentProvider(
request: PendingOneShot,
expectedCancellationSignal: CancellationSignal? = null,
expectedListener: LocationListener? = null
): OneShotResources? = synchronized(activeOneShotRequests) {
if (!isCurrentProviderRequestLocked(request, expectedCancellationSignal, expectedListener)) {
null
} else {
OneShotResources(
cancellationSignal = request.cancellationSignal,
listener = request.listener,
timeoutRunnable = request.timeoutRunnable
).also {
request.cancellationSignal = null
request.listener = null
request.timeoutRunnable = null
}
}
}
private fun isOneShotActiveLocked(request: PendingOneShot): Boolean =
!request.completed && activeOneShotRequests.contains(request)
private fun isCurrentProviderRequestLocked(
request: PendingOneShot,
expectedCancellationSignal: CancellationSignal?,
expectedListener: LocationListener?
): Boolean =
isOneShotActiveLocked(request) &&
(expectedCancellationSignal == null || request.cancellationSignal === expectedCancellationSignal) &&
(expectedListener == null || request.listener === expectedListener)
private fun finishOneShot(request: PendingOneShot, location: Location?) {
val resources = synchronized(activeOneShotRequests) {
if (!isOneShotActiveLocked(request)) {
null
} else {
request.completed = true
activeOneShotRequests.remove(request)
OneShotResources(
cancellationSignal = request.cancellationSignal,
listener = request.listener,
timeoutRunnable = request.timeoutRunnable
).also {
request.cancellationSignal = null
request.listener = null
request.timeoutRunnable = null
}
}
} ?: return
releaseOneShotResources(resources)
request.callback(location.takeIf { hasLocationPermission() })
}
private fun releaseOneShotResources(resources: OneShotResources) {
resources.timeoutRunnable?.let(handler::removeCallbacks)
resources.cancellationSignal?.cancel()
resources.listener?.let { listener ->
runCatching { locationManager.removeUpdates(listener) }
.onFailure { Log.w(TAG, "Unable to remove one-shot location listener", it) }
}
}
@ -172,26 +374,25 @@ internal class SystemLocationProvider(private val context: Context) : LocationPr
) {
if (!hasLocationPermission()) return
try {
val listener = object : LocationListener {
override fun onLocationChanged(location: Location) {
if (LiveLocationPrivacyGate.isEnabled) callback(location)
}
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {}
override fun onProviderEnabled(provider: String) {}
override fun onProviderDisabled(provider: String) {}
removeLocationUpdates(callback)
val listener = object : LocationListener {
override fun onLocationChanged(location: Location) {
if (hasLocationPermission()) callback(location)
}
// Store the listener so we can remove it later
synchronized(activeListeners) {
activeListeners[callback] = listener
}
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) = Unit
override fun onProviderEnabled(provider: String) = Unit
override fun onProviderDisabled(provider: String) = Unit
}
val providers = listOf(LocationManager.GPS_PROVIDER, LocationManager.NETWORK_PROVIDER)
var registered = false
for (provider in providers) {
if (locationManager.isProviderEnabled(provider)) {
var registered = false
var shouldCleanUp = false
synchronized(activeListeners) {
activeListeners[callback] = listener
for (provider in listOf(LocationManager.GPS_PROVIDER, LocationManager.NETWORK_PROVIDER)) {
if (!isProviderEnabled(provider)) continue
try {
locationManager.requestLocationUpdates(
provider,
intervalMs,
@ -200,62 +401,57 @@ internal class SystemLocationProvider(private val context: Context) : LocationPr
)
registered = true
Log.d(TAG, "Registered updates for $provider")
} catch (e: Exception) {
Log.w(TAG, "Unable to register updates for $provider", e)
}
}
if (!registered) {
Log.w(TAG, "No providers enabled for continuous updates")
}
} catch (e: Exception) {
Log.e(TAG, "Error requesting location updates")
shouldCleanUp = !registered || activeListeners[callback] !== listener
if (shouldCleanUp && activeListeners[callback] === listener) {
activeListeners.remove(callback)
}
}
if (shouldCleanUp) {
runCatching { locationManager.removeUpdates(listener) }
Log.w(TAG, "No system providers accepted continuous location updates")
}
}
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")
val listener = synchronized(activeListeners) {
activeListeners.remove(callback)
}
if (listener != null) {
runCatching { locationManager.removeUpdates(listener) }
.onFailure { Log.w(TAG, "Unable to remove location updates", it) }
}
}
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()
}
synchronized(activeOneShotCancellationSignals) {
activeOneShotCancellationSignals.values.forEach { it.cancel() }
activeOneShotCancellationSignals.clear()
}
Log.d(TAG, "Cancelled all system location requests")
} catch (e: Exception) {
Log.e(TAG, "Error cancelling system provider")
val listeners = synchronized(activeListeners) {
activeListeners.values.toList().also { activeListeners.clear() }
}
listeners.forEach { listener ->
runCatching { locationManager.removeUpdates(listener) }
.onFailure { Log.w(TAG, "Unable to remove location updates", it) }
}
val oneShotResources = synchronized(activeOneShotRequests) {
activeOneShotRequests.map { request ->
request.completed = true
OneShotResources(
cancellationSignal = request.cancellationSignal,
listener = request.listener,
timeoutRunnable = request.timeoutRunnable
)
}.also {
activeOneShotRequests.clear()
}
}
oneShotResources.forEach(::releaseOneShotResources)
Log.d(TAG, "Cancelled all system location requests")
}
}