- 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.
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
+72
@@ -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<String, String>(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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Location> =
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
-165
@@ -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<Location> =
|
||||
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<String, String>(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<String?> =
|
||||
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(", ")
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+92
@@ -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<Address>?) -> 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<Address>?) -> Unit,
|
||||
) {
|
||||
val locationCallback =
|
||||
object : Geocoder.GeocodeListener {
|
||||
override fun onGeocode(addresses: List<Address>) {
|
||||
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<Address>? {
|
||||
Log.d("ReverseGeoLocation", "Execute Sync $location")
|
||||
return try {
|
||||
Geocoder(context).getFromLocation(
|
||||
location.latitude,
|
||||
location.longitude,
|
||||
1,
|
||||
)
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
-3
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+73
@@ -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) }
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user