mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-09 16:14:40 +00:00
fix(location): stop spinning Around Me when Geocoder is unavailable
On AOSP / GrapheneOS without Google Play Services (or microG), Android's system Geocoder has no backing IGeocodeProvider, so `Geocoder.isPresent()` returns false. Two compounding bugs left the Around Me top-bar spinner running forever in that case: 1. `CachedReversedGeoLocations.geoLocate` only invoked `onReady` when Geocoder was present AND returned a non-null, non-blank city name. In the not-present and empty-result paths the callback never fired. 2. `LoadCityName` looped while `notReady`, but `notReady` was only flipped off when `newCityName != cityName`. With both null the loop never terminated, doubling its delay each pass. Now `geoLocate` always calls `onReady` exactly once, short-circuiting with `null` when no Geocoder backend exists. `LoadCityName` bridges the callback to a suspending call, caps retries at 5, and falls back to rendering the raw geohash so the spinner always stops. The "no geocoder backend" check short-circuits the whole retry path on devices that will never be able to resolve a name.
This commit is contained in:
+22
-21
@@ -32,6 +32,14 @@ import com.vitorpamplona.quartz.utils.Log
|
||||
object CachedReversedGeoLocations {
|
||||
val locationNames = LruCache<String, String>(20)
|
||||
|
||||
// Geocoder.isPresent() reflects whether the system has a backing
|
||||
// IGeocodeProvider registered — fixed at boot, safe to memoize. On AOSP /
|
||||
// GrapheneOS without Google Play Services or microG configured no provider
|
||||
// is registered and this returns false.
|
||||
private val geocoderAvailable: Boolean by lazy { Geocoder.isPresent() }
|
||||
|
||||
fun isGeocoderAvailable(): Boolean = geocoderAvailable
|
||||
|
||||
fun cached(geoHashStr: String): String? = locationNames[geoHashStr]
|
||||
|
||||
fun geoLocate(
|
||||
@@ -42,31 +50,24 @@ object CachedReversedGeoLocations {
|
||||
) {
|
||||
locationNames[geoHashStr]?.let {
|
||||
onReady(it)
|
||||
return
|
||||
}
|
||||
|
||||
if (Geocoder.isPresent()) {
|
||||
ReverseGeolocation.execute(location, context) { cityNames ->
|
||||
if (cityNames != null) {
|
||||
val cityName =
|
||||
cityNames.firstNotNullOfOrNull {
|
||||
val name = it.toCityCountry()
|
||||
if (!name.isBlank()) {
|
||||
name
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
if (cityName != null) {
|
||||
locationNames.put(geoHashStr, cityName)
|
||||
onReady(cityName)
|
||||
}
|
||||
} else {
|
||||
// error
|
||||
onReady(null)
|
||||
if (!geocoderAvailable) {
|
||||
Log.d("ReverseGeoLocation") { "Geocoder service not present on this device" }
|
||||
onReady(null)
|
||||
return
|
||||
}
|
||||
|
||||
ReverseGeolocation.execute(location, context) { cityNames ->
|
||||
val cityName =
|
||||
cityNames?.firstNotNullOfOrNull {
|
||||
it.toCityCountry().takeIf { name -> name.isNotBlank() }
|
||||
}
|
||||
if (cityName != null) {
|
||||
locationNames.put(geoHashStr, cityName)
|
||||
}
|
||||
} else {
|
||||
Log.d("ReverseGeoLocation", "Geocoder not present")
|
||||
onReady(cityName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+45
-24
@@ -20,6 +20,8 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.note.creators.location
|
||||
|
||||
import android.content.Context
|
||||
import android.location.Location
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -30,7 +32,10 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import com.vitorpamplona.amethyst.service.location.CachedReversedGeoLocations
|
||||
import com.vitorpamplona.amethyst.service.location.toGeoHash
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
private const val MAX_GEOLOCATE_RETRIES = 5
|
||||
|
||||
@Composable
|
||||
fun LoadCityName(
|
||||
@@ -40,34 +45,50 @@ fun LoadCityName(
|
||||
) {
|
||||
var cityName by remember(geohashStr) { mutableStateOf(CachedReversedGeoLocations.cached(geohashStr)) }
|
||||
|
||||
if (cityName == null) {
|
||||
if (onLoading != null) {
|
||||
onLoading()
|
||||
}
|
||||
val resolved = cityName
|
||||
if (resolved != null) {
|
||||
content(resolved)
|
||||
} else if (!CachedReversedGeoLocations.isGeocoderAvailable()) {
|
||||
// Devices without a system Geocoder backend (e.g. AOSP / GrapheneOS
|
||||
// without Google Play Services or microG) cannot reverse-geocode at
|
||||
// all. Show the geohash directly instead of spinning forever.
|
||||
content(geohashStr)
|
||||
} else {
|
||||
onLoading?.invoke()
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
LaunchedEffect(key1 = geohashStr, context) {
|
||||
LaunchedEffect(geohashStr, context) {
|
||||
val location = runCatching { geohashStr.toGeoHash() }.getOrNull()?.toLocation()
|
||||
if (location != null) {
|
||||
launch {
|
||||
var notReady = true
|
||||
var myStep = 1000L
|
||||
while (notReady) {
|
||||
// Retries while the Reverse Geolocation service is offline.
|
||||
CachedReversedGeoLocations.geoLocate(geohashStr, location, context) { newCityName ->
|
||||
if (newCityName != cityName) {
|
||||
notReady = false
|
||||
cityName = newCityName
|
||||
}
|
||||
}
|
||||
myStep = myStep * 2L
|
||||
delay(myStep)
|
||||
}
|
||||
}
|
||||
if (location == null) {
|
||||
cityName = geohashStr
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
var delayMs = 1000L
|
||||
repeat(MAX_GEOLOCATE_RETRIES) {
|
||||
val result = geoLocateOnce(geohashStr, location, context)
|
||||
if (result != null) {
|
||||
cityName = result
|
||||
return@LaunchedEffect
|
||||
}
|
||||
delay(delayMs)
|
||||
delayMs *= 2L
|
||||
}
|
||||
// Geocoder kept returning errors — give up and render the geohash
|
||||
// so the loading indicator stops.
|
||||
cityName = geohashStr
|
||||
}
|
||||
} else {
|
||||
cityName?.let { content(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun geoLocateOnce(
|
||||
geohashStr: String,
|
||||
location: Location,
|
||||
context: Context,
|
||||
): String? =
|
||||
suspendCancellableCoroutine { cont ->
|
||||
CachedReversedGeoLocations.geoLocate(geohashStr, location, context) { result ->
|
||||
if (cont.isActive) cont.resume(result)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user