From 5ac73bbeba49b769caca244dd891cec949294483 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 29 Mar 2025 11:00:37 -0400 Subject: [PATCH] - Retries to reverse geolocate if fails due to the services long startup time. - Adds support for the new and old ways to use reverse geolocation services. - Hits all existing providers for location updates instead of just the network ones - Uses past last known locations to kick start the cache. - Improves logging to check potential failures in production. --- .../amethyst/service/location/AddressExt.kt | 39 ++++ .../location/CachedReversedGeoLocations.kt | 72 +++++++ .../amethyst/service/location/LocationFlow.kt | 78 ++++++++ .../service/location/LocationState.kt | 185 ++---------------- .../service/location/ReverseGeolocation.kt | 92 +++++++++ .../ui/navigation/FeedFilterSpinner.kt | 14 +- .../vitorpamplona/amethyst/ui/note/Loaders.kt | 38 ---- .../location/DisplayLocationObserver.kt | 2 +- .../ui/note/creators/location/LoadCityName.kt | 73 +++++++ .../ui/note/elements/DisplayLocation.kt | 2 +- .../screen/loggedIn/geohash/GeoHashScreen.kt | 2 +- 11 files changed, 388 insertions(+), 209 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/location/AddressExt.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/location/CachedReversedGeoLocations.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/location/LocationFlow.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/location/ReverseGeolocation.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/LoadCityName.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/location/AddressExt.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/location/AddressExt.kt new file mode 100644 index 000000000..89c5b836e --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/location/AddressExt.kt @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.location + +import android.location.Address + +fun Address.toCityCountry(): String = + buildString { + var includeComma = false + + val city = locality ?: subAdminArea + if (city != null && city.isNotBlank()) { + append(city) + includeComma = true + } + + if (countryCode != null && countryCode.isNotBlank()) { + if (includeComma) append(", ") + append(countryCode) + } + } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/location/CachedReversedGeoLocations.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/location/CachedReversedGeoLocations.kt new file mode 100644 index 000000000..34e62f979 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/location/CachedReversedGeoLocations.kt @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.location + +import android.content.Context +import android.location.Geocoder +import android.location.Location +import android.util.Log +import android.util.LruCache + +/** + * Maintains a cache of geohashes and city,country pairs. + */ +object CachedReversedGeoLocations { + val locationNames = LruCache(20) + + fun cached(geoHashStr: String): String? = locationNames[geoHashStr] + + fun geoLocate( + geoHashStr: String, + location: Location, + context: Context, + onReady: (String?) -> Unit, + ) { + locationNames[geoHashStr]?.let { + onReady(it) + } + + 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) + } + } + } else { + Log.d("ReverseGeoLocation", "Geocoder not present") + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/location/LocationFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/location/LocationFlow.kt new file mode 100644 index 000000000..22aa275ae --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/location/LocationFlow.kt @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.location + +import android.annotation.SuppressLint +import android.content.Context +import android.location.Location +import android.location.LocationListener +import android.location.LocationManager +import android.os.Looper +import android.util.Log +import com.vitorpamplona.amethyst.service.location.LocationState.Companion.MIN_DISTANCE +import com.vitorpamplona.amethyst.service.location.LocationState.Companion.MIN_TIME +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.launch + +class LocationFlow( + private val context: Context, +) { + @SuppressLint("MissingPermission") + fun get( + minTimeMs: Long = MIN_TIME, + minDistanceM: Float = MIN_DISTANCE, + ): Flow = + callbackFlow { + Log.i("LocationFlow", "Start") + val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + + val locationCallback = + object : LocationListener { + override fun onLocationChanged(location: Location) { + Log.d("LocationFlow", "onLocationChanged $location") + launch { send(location) } + } + } + + locationManager.allProviders.forEach { + val location = locationManager.getLastKnownLocation(it) + Log.d("LocationFlow", "Last Known location is $location") + if (location != null) { + send(location) + } + Log.d("LocationFlow", "Requesting Updates") + locationManager.requestLocationUpdates( + it, + minTimeMs, + minDistanceM, + locationCallback, + Looper.getMainLooper(), + ) + } + + awaitClose { + Log.i("LocationFlow", "Stop") + locationManager.removeUpdates(locationCallback) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/location/LocationState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/location/LocationState.kt index 5f1f625de..18573b4f1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/location/LocationState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/location/LocationState.kt @@ -20,74 +20,21 @@ */ package com.vitorpamplona.amethyst.service.location -import android.annotation.SuppressLint import android.content.Context -import android.location.Geocoder -import android.location.Location -import android.location.LocationListener -import android.location.LocationManager -import android.os.Build -import android.os.Looper -import android.util.Log -import android.util.LruCache +import coil3.util.CoilUtils.result import com.fonfon.kgeohash.GeoHash import com.fonfon.kgeohash.toGeoHash -import com.vitorpamplona.amethyst.service.location.LocationState.Companion.MIN_DISTANCE -import com.vitorpamplona.amethyst.service.location.LocationState.Companion.MIN_TIME import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeohashPrecision -import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.transformLatest -import kotlinx.coroutines.launch - -class LocationFlow( - private val context: Context, -) { - @SuppressLint("MissingPermission") - fun get( - minTimeMs: Long = MIN_TIME, - minDistanceM: Float = MIN_DISTANCE, - ): Flow = - callbackFlow { - val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager - - val locationCallback = - object : LocationListener { - override fun onLocationChanged(location: Location) { - launch { send(location) } - } - - override fun onProviderEnabled(provider: String) {} - - override fun onProviderDisabled(provider: String) {} - } - - Log.d("Location Service", "LocationState Start") - locationManager.requestLocationUpdates( - LocationManager.NETWORK_PROVIDER, - minTimeMs, - minDistanceM, - locationCallback, - Looper.getMainLooper(), - ) - - awaitClose { - locationManager.removeUpdates(locationCallback) - Log.d("Location Service", "LocationState Stop") - } - } -} class LocationState( context: Context, @@ -121,120 +68,28 @@ class LocationState( val geohashStateFlow = hasLocationPermission .transformLatest { - emitAll( - LocationFlow - (context) - .get(MIN_TIME, MIN_DISTANCE) - .map { - LocationResult.Success(it.toGeoHash(GeohashPrecision.KM_5_X_5.digits)) as LocationResult - }.onEach { - latestLocation = it - }.catch { e -> - e.printStackTrace() - latestLocation = LocationResult.LackPermission - emit(LocationResult.LackPermission) - }, - ) + if (it) { + emit(LocationResult.Loading) + val result = + LocationFlow(context) + .get(MIN_TIME, MIN_DISTANCE) + .map { + LocationResult.Success(it.toGeoHash(GeohashPrecision.KM_5_X_5.digits)) as LocationResult + }.onEach { + latestLocation = it + }.catch { e -> + e.printStackTrace() + latestLocation = LocationResult.LackPermission + emit(LocationResult.LackPermission) + } + + emitAll(result) + } else { + emit(LocationResult.LackPermission) + } }.stateIn( scope, SharingStarted.WhileSubscribed(5000), latestLocation, ) } - -object CachedGeoLocations { - val locationNames = LruCache(20) - - fun cached(geoHashStr: String): String? = locationNames[geoHashStr] - - suspend fun geoLocate( - geoHashStr: String, - location: Location, - context: Context, - ): String? { - locationNames[geoHashStr]?.let { - return it - } - - val name = ReverseGeoLocationUtil().execute(location, context)?.ifBlank { null } - - if (name != null) { - locationNames.put(geoHashStr, name) - } - - return name - } -} - -private class ReverseGeoLocationUtil { - suspend fun execute( - location: Location, - context: Context, - ): String? { - return try { - Geocoder(context) - .getFromLocation( - location.latitude, - location.longitude, - 1, - )?.firstOrNull() - ?.let { address -> - listOfNotNull(address.locality ?: address.subAdminArea, address.countryCode) - .joinToString(", ") - } - } catch (e: Exception) { - if (e is CancellationException) throw e - e.printStackTrace() - return null - } - } -} - -class ReverseGeoLocationFlow( - private val context: Context, -) { - @SuppressLint("MissingPermission") - fun get(location: Location): Flow = - callbackFlow { - val locationManager = Geocoder(context) - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - val locationCallback = - ( - Geocoder.GeocodeListener { addresses -> - launch { - send( - addresses.firstOrNull()?.let { - listOfNotNull(it.locality ?: it.subAdminArea, it.countryCode).joinToString(", ") - }, - ) - } - } - ) - Log.d("GeoLocation Service", "LocationState Start") - - locationManager - .getFromLocation( - location.latitude, - location.longitude, - 1, - locationCallback, - ) - } else { - launch { - send( - Geocoder(context) - .getFromLocation( - location.latitude, - location.longitude, - 1, - )?.firstOrNull() - ?.let { address -> - listOfNotNull(address.locality ?: address.subAdminArea, address.countryCode) - .joinToString(", ") - }, - ) - } - } - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/location/ReverseGeolocation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/location/ReverseGeolocation.kt new file mode 100644 index 000000000..a5ee5d5ff --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/location/ReverseGeolocation.kt @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.location + +import android.content.Context +import android.location.Address +import android.location.Geocoder +import android.location.Location +import android.os.Build +import android.util.Log +import androidx.annotation.RequiresApi +import java.io.IOException + +class ReverseGeolocation { + companion object { + fun execute( + location: Location, + context: Context, + onReady: (List
?) -> Unit, + ) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + executeAsync(location, context, onReady) + } else { + onReady(executeSync(location, context)) + } + } + + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + fun executeAsync( + location: Location, + context: Context, + onReady: (List
?) -> Unit, + ) { + val locationCallback = + object : Geocoder.GeocodeListener { + override fun onGeocode(addresses: List
) { + Log.d("ReverseGeoLocation", "Found ${addresses.size} new addresses") + onReady(addresses) + } + + override fun onError(errorMessage: String?) { + super.onError(errorMessage) + Log.w("ReverseGeoLocation", "Failure $errorMessage") + onReady(null) + } + } + + Log.d("ReverseGeoLocation", "Execute Async $location") + Geocoder(context).getFromLocation( + location.latitude, + location.longitude, + 1, + locationCallback, + ) + } + + fun executeSync( + location: Location, + context: Context, + ): List
? { + Log.d("ReverseGeoLocation", "Execute Sync $location") + return try { + Geocoder(context).getFromLocation( + location.latitude, + location.longitude, + 1, + ) + } catch (e: IOException) { + e.printStackTrace() + return null + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/FeedFilterSpinner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/FeedFilterSpinner.kt index fc37d9514..bae4c9263 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/FeedFilterSpinner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/FeedFilterSpinner.kt @@ -45,6 +45,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.map @@ -56,7 +57,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.ui.components.LoadingAnimation -import com.vitorpamplona.amethyst.ui.note.LoadCityName +import com.vitorpamplona.amethyst.ui.note.creators.location.LoadCityName import com.vitorpamplona.amethyst.ui.screen.AroundMeFeedDefinition import com.vitorpamplona.amethyst.ui.screen.CommunityName import com.vitorpamplona.amethyst.ui.screen.FeedDefinition @@ -138,8 +139,15 @@ fun FeedFilterSpinner( LoadCityName( geohashStr = myLocation.geoHash.toString(), onLoading = { - Spacer(modifier = StdHorzSpacer) - LoadingAnimation() + Row { + Text( + text = "(${myLocation.geoHash})", + fontSize = 12.sp, + lineHeight = 12.sp, + ) + Spacer(modifier = StdHorzSpacer) + LoadingAnimation(indicatorSize = 12.dp, circleWidth = 2.dp) + } }, ) { cityName -> Text( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt index 6c6edbaa6..6c9b65488 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt @@ -27,14 +27,11 @@ import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.fonfon.kgeohash.toGeoHash import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.location.CachedGeoLocations import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists @@ -191,41 +188,6 @@ fun LoadOts( } } -@Composable -fun LoadCityName( - geohashStr: String, - onLoading: (@Composable () -> Unit)? = null, - content: @Composable (String) -> Unit, -) { - var cityName by remember(geohashStr) { mutableStateOf(CachedGeoLocations.cached(geohashStr)) } - - if (cityName == null) { - if (onLoading != null) { - onLoading() - } - - val context = LocalContext.current - - LaunchedEffect(key1 = geohashStr, context) { - launch(Dispatchers.IO) { - val geoHash = runCatching { geohashStr.toGeoHash() }.getOrNull() - if (geoHash != null) { - val newCityName = - CachedGeoLocations - .geoLocate(geohashStr, geoHash.toLocation(), context) - ?.ifBlank { null } - - if (newCityName != null && newCityName != cityName) { - cityName = newCityName - } - } - } - } - } else { - cityName?.let { content(it) } - } -} - @Composable fun LoadChannel( baseChannelHex: String, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/DisplayLocationObserver.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/DisplayLocationObserver.kt index 2dbe7e6f5..d6fc2f8a2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/DisplayLocationObserver.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/DisplayLocationObserver.kt @@ -25,6 +25,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp @@ -32,7 +33,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.ui.components.LoadingAnimation -import com.vitorpamplona.amethyst.ui.note.LoadCityName import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Size5dp import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/LoadCityName.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/LoadCityName.kt new file mode 100644 index 000000000..d60af4207 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/LoadCityName.kt @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.note.creators.location + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext +import com.fonfon.kgeohash.toGeoHash +import com.vitorpamplona.amethyst.service.location.CachedReversedGeoLocations +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +@Composable +fun LoadCityName( + geohashStr: String, + onLoading: (@Composable () -> Unit)? = null, + content: @Composable (String) -> Unit, +) { + var cityName by remember(geohashStr) { mutableStateOf(CachedReversedGeoLocations.cached(geohashStr)) } + + if (cityName == null) { + if (onLoading != null) { + onLoading() + } + + val context = LocalContext.current + + LaunchedEffect(key1 = 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) + } + } + } + } + } else { + cityName?.let { content(it) } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayLocation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayLocation.kt index dcea711ca..ebae43765 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayLocation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayLocation.kt @@ -27,7 +27,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.font.FontWeight import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.note.LoadCityName +import com.vitorpamplona.amethyst.ui.note.creators.location.LoadCityName import com.vitorpamplona.amethyst.ui.theme.Font14SP @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashScreen.kt index 0bde5c607..2bcd6fa01 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashScreen.kt @@ -45,7 +45,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.NostrGeohashDataSource import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.TopBarExtensibleWithBackButton -import com.vitorpamplona.amethyst.ui.note.LoadCityName +import com.vitorpamplona.amethyst.ui.note.creators.location.LoadCityName import com.vitorpamplona.amethyst.ui.screen.NostrGeoHashFeedViewModel import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel