Improves the speed of Location services.

This commit is contained in:
Vitor Pamplona
2024-02-29 14:19:50 -05:00
parent c686e775bf
commit 0084c2b532
6 changed files with 103 additions and 109 deletions
@@ -27,6 +27,7 @@ import android.location.Location
import android.location.LocationListener import android.location.LocationListener
import android.location.LocationManager import android.location.LocationManager
import android.os.HandlerThread import android.os.HandlerThread
import android.util.LruCache
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
@@ -92,7 +93,33 @@ class LocationUtil(context: Context) {
} }
} }
class ReverseGeoLocationUtil { object CachedGeoLocations {
val locationNames = LruCache<String, String>(20)
fun cached(geoHashStr: String): String? {
return 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( suspend fun execute(
location: Location, location: Location,
context: Context, context: Context,
@@ -124,7 +124,6 @@ import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.fonfon.kgeohash.toGeoHash
import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.permissions.rememberPermissionState
@@ -134,7 +133,6 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.Nip96MediaServers import com.vitorpamplona.amethyst.service.Nip96MediaServers
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
import com.vitorpamplona.amethyst.service.ReverseGeoLocationUtil
import com.vitorpamplona.amethyst.ui.components.BechLink import com.vitorpamplona.amethyst.ui.components.BechLink
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.InvoiceRequest import com.vitorpamplona.amethyst.ui.components.InvoiceRequest
@@ -144,6 +142,7 @@ import com.vitorpamplona.amethyst.ui.components.ZapRaiserRequest
import com.vitorpamplona.amethyst.ui.note.BaseUserPicture import com.vitorpamplona.amethyst.ui.note.BaseUserPicture
import com.vitorpamplona.amethyst.ui.note.CancelIcon import com.vitorpamplona.amethyst.ui.note.CancelIcon
import com.vitorpamplona.amethyst.ui.note.CloseIcon import com.vitorpamplona.amethyst.ui.note.CloseIcon
import com.vitorpamplona.amethyst.ui.note.LoadCityName
import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.note.PollIcon import com.vitorpamplona.amethyst.ui.note.PollIcon
import com.vitorpamplona.amethyst.ui.note.RegularPostIcon import com.vitorpamplona.amethyst.ui.note.RegularPostIcon
@@ -1172,23 +1171,12 @@ fun FowardZapTo(
@OptIn(ExperimentalPermissionsApi::class) @OptIn(ExperimentalPermissionsApi::class)
@Composable @Composable
fun LocationAsHash(postViewModel: NewPostViewModel) { fun LocationAsHash(postViewModel: NewPostViewModel) {
val context = LocalContext.current
val locationPermissionState = val locationPermissionState =
rememberPermissionState( rememberPermissionState(
Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION,
) )
if (locationPermissionState.status.isGranted) { if (locationPermissionState.status.isGranted) {
var locationDescriptionFlow by remember(postViewModel) { mutableStateOf<Flow<String>?>(null) }
DisposableEffect(key1 = Unit) {
postViewModel.startLocation(context = context)
locationDescriptionFlow = postViewModel.location
onDispose { postViewModel.stopLocation() }
}
Column( Column(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) { ) {
@@ -1219,7 +1207,7 @@ fun LocationAsHash(postViewModel: NewPostViewModel) {
modifier = Modifier.padding(start = 10.dp), modifier = Modifier.padding(start = 10.dp),
) )
locationDescriptionFlow?.let { geoLocation -> DisplayLocationObserver(geoLocation) } DisplayLocationObserver(postViewModel)
} }
Divider() Divider()
@@ -1236,41 +1224,39 @@ fun LocationAsHash(postViewModel: NewPostViewModel) {
} }
@Composable @Composable
fun DisplayLocationObserver(geoLocation: Flow<String>) { fun DisplayLocationObserver(postViewModel: NewPostViewModel) {
val location by geoLocation.collectAsStateWithLifecycle(null) val context = LocalContext.current
var locationDescriptionFlow by remember(postViewModel) { mutableStateOf<Flow<String>?>(null) }
location?.let { DisplayLocationInTitle(geohash = it) } DisposableEffect(key1 = context) {
postViewModel.startLocation(context = context)
locationDescriptionFlow = postViewModel.location
onDispose { postViewModel.stopLocation() }
}
locationDescriptionFlow?.let {
val location by it.collectAsStateWithLifecycle(null)
location?.let { DisplayLocationInTitle(geohash = it) }
}
} }
@Composable @Composable
fun DisplayLocationInTitle(geohash: String) { fun DisplayLocationInTitle(geohash: String) {
val context = LocalContext.current LoadCityName(
geohashStr = geohash,
var cityName by remember(geohash) { mutableStateOf<String>(geohash) } onLoading = {
Spacer(modifier = StdHorzSpacer)
LaunchedEffect(key1 = geohash) { LoadingAnimation()
launch(Dispatchers.IO) { },
val newCityName = ) { cityName ->
ReverseGeoLocationUtil().execute(geohash.toGeoHash().toLocation(), context)?.ifBlank {
null
}
if (newCityName != null && newCityName != cityName) {
cityName = newCityName
}
}
}
if (geohash != "s0000") {
Text( Text(
text = cityName, text = cityName,
fontSize = 20.sp, fontSize = 20.sp,
fontWeight = FontWeight.W500, fontWeight = FontWeight.W500,
modifier = Modifier.padding(start = Size5dp), modifier = Modifier.padding(start = Size5dp),
) )
} else {
Spacer(modifier = StdHorzSpacer)
LoadingAnimation()
} }
} }
@@ -74,7 +74,6 @@ import androidx.lifecycle.map
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.navigation.NavBackStackEntry import androidx.navigation.NavBackStackEntry
import coil.Coil import coil.Coil
import com.fonfon.kgeohash.toGeoHash
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.AddressableNote
@@ -817,15 +816,12 @@ fun SimpleTextSpinner(
fun RenderOption(option: Name) { fun RenderOption(option: Name) {
when (option) { when (option) {
is GeoHashName -> { is GeoHashName -> {
val geohash = runCatching { option.geoHashTag.toGeoHash() }.getOrNull() LoadCityName(option.geoHashTag) {
if (geohash != null) { Row(
LoadCityName(geohash) { horizontalArrangement = Arrangement.Center,
Row( modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center, ) {
modifier = Modifier.fillMaxWidth(), Text(text = "/g/$it", color = MaterialTheme.colorScheme.onSurface)
) {
Text(text = "/g/$it", color = MaterialTheme.colorScheme.onSurface)
}
} }
} }
} }
@@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.ui.navigation
import android.os.Bundle import android.os.Bundle
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -32,7 +31,6 @@ import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavDestination import androidx.navigation.NavDestination
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.NavType import androidx.navigation.NavType
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.navArgument import androidx.navigation.navArgument
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
@@ -101,7 +101,6 @@ import coil.compose.AsyncImagePainter
import coil.request.SuccessResult import coil.request.SuccessResult
import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fonfon.kgeohash.GeoHash
import com.fonfon.kgeohash.toGeoHash import com.fonfon.kgeohash.toGeoHash
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.BaseMediaContent import com.vitorpamplona.amethyst.commons.BaseMediaContent
@@ -115,7 +114,7 @@ import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.RelayBriefInfoCache import com.vitorpamplona.amethyst.model.RelayBriefInfoCache
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.ReverseGeoLocationUtil import com.vitorpamplona.amethyst.service.CachedGeoLocations
import com.vitorpamplona.amethyst.ui.actions.NewRelayListView import com.vitorpamplona.amethyst.ui.actions.NewRelayListView
import com.vitorpamplona.amethyst.ui.components.ClickableUrl import com.vitorpamplona.amethyst.ui.components.ClickableUrl
import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji
@@ -2768,23 +2767,34 @@ fun LoadOts(
@Composable @Composable
fun LoadCityName( fun LoadCityName(
geohash: GeoHash, geohashStr: String,
onLoading: (@Composable () -> Unit)? = null,
content: @Composable (String) -> Unit, content: @Composable (String) -> Unit,
) { ) {
val context = LocalContext.current var cityName by remember(geohashStr) { mutableStateOf(CachedGeoLocations.cached(geohashStr)) }
var cityName by remember(geohash) { mutableStateOf<String>(geohash.toString()) }
LaunchedEffect(key1 = geohash) { if (cityName == null) {
launch(Dispatchers.IO) { if (onLoading != null) {
val newCityName = onLoading()
ReverseGeoLocationUtil().execute(geohash.toLocation(), context)?.ifBlank { null } }
if (newCityName != null && newCityName != cityName) {
cityName = newCityName 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) }
} }
content(cityName)
} }
@Composable @Composable
@@ -2792,24 +2802,21 @@ fun DisplayLocation(
geohashStr: String, geohashStr: String,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
val geoHash = runCatching { geohashStr.toGeoHash() }.getOrNull() LoadCityName(geohashStr) { cityName ->
if (geoHash != null) { ClickableText(
LoadCityName(geoHash) { cityName -> text = AnnotatedString(cityName),
ClickableText( onClick = { nav("Geohash/$geohashStr") },
text = AnnotatedString(cityName), style =
onClick = { nav("Geohash/$geoHash") }, LocalTextStyle.current.copy(
style = color =
LocalTextStyle.current.copy( MaterialTheme.colorScheme.primary.copy(
color = alpha = 0.52f,
MaterialTheme.colorScheme.primary.copy( ),
alpha = 0.52f, fontSize = Font14SP,
), fontWeight = FontWeight.Bold,
fontSize = Font14SP, ),
fontWeight = FontWeight.Bold, maxLines = 1,
), )
maxLines = 1,
)
}
} }
} }
@@ -31,32 +31,26 @@ import androidx.compose.material3.Divider
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.fonfon.kgeohash.toGeoHash
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.NostrGeohashDataSource import com.vitorpamplona.amethyst.service.NostrGeohashDataSource
import com.vitorpamplona.amethyst.service.ReverseGeoLocationUtil import com.vitorpamplona.amethyst.ui.note.LoadCityName
import com.vitorpamplona.amethyst.ui.screen.NostrGeoHashFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrGeoHashFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView
import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.StdPadding import com.vitorpamplona.amethyst.ui.theme.StdPadding
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable @Composable
fun GeoHashScreen( fun GeoHashScreen(
@@ -174,27 +168,13 @@ fun DislayGeoTagHeader(
geohash: String, geohash: String,
modifier: Modifier, modifier: Modifier,
) { ) {
val context = LocalContext.current LoadCityName(geohashStr = geohash) { cityName ->
Text(
var cityName by remember(geohash) { mutableStateOf<String>(geohash) } cityName,
fontWeight = FontWeight.Bold,
LaunchedEffect(key1 = geohash) { modifier = modifier,
launch(Dispatchers.IO) { )
val newCityName =
ReverseGeoLocationUtil().execute(geohash.toGeoHash().toLocation(), context)?.ifBlank {
null
}
if (newCityName != null && newCityName != cityName) {
cityName = "$newCityName ($geohash)"
}
}
} }
Text(
cityName,
fontWeight = FontWeight.Bold,
modifier = modifier,
)
} }
@Composable @Composable