From b94303a77a5ad5d3d064e91b3a4f706e4667f507 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Jul 2026 02:11:59 +0000 Subject: [PATCH] =?UTF-8?q?fix(location):=20audit=20fixes=20=E2=80=94=20st?= =?UTF-8?q?uck=20GPS=20spinner,=20main-thread=20geocode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "Use my location" could spin forever after a permission denial: the reset was in an isGranted-keyed effect that never re-ran on false→false. Use rememberPermissionState's result callback, which fires on grant AND denial, so the spinner always clears. - Forward-geocode search ran the blocking Geocoder on the UI thread on Android < 13 (ANR risk). Run the search on Dispatchers.IO. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01YU8YLcjH9ALr4PgdAkGPZh --- .../location/GeohashLocationPickerDialog.kt | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/GeohashLocationPickerDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/GeohashLocationPickerDialog.kt index b700f3e822..3a103691d8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/GeohashLocationPickerDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/location/GeohashLocationPickerDialog.kt @@ -65,6 +65,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -92,8 +93,10 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.geohashChat.label import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.experimental.bitchat.geohash.GeohashChannelLevel import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeoHash +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeoutOrNull import org.osmdroid.util.BoundingBox import org.osmdroid.util.GeoPoint @@ -192,6 +195,7 @@ fun GeohashLocationPickerContent( ) { val context = LocalContext.current val keyboard = LocalSoftwareKeyboardController.current + val scope = rememberCoroutineScope() val locationManager = Amethyst.instance.locationManager val seed = remember(initialGeohash) { initialGeohash?.takeIf { it.isNotBlank() }?.let { GeoHash.decode(it) } } @@ -246,19 +250,18 @@ fun GeohashLocationPickerContent( } // "Use my location": tapping either fires the fetch (permission already granted) or - // asks for it; [awaitingPermission] carries the intent across the system dialog so a - // grant auto-starts the fetch, while a denial simply drops the request (no stuck spinner). - val permission = rememberPermissionState(Manifest.permission.ACCESS_COARSE_LOCATION) + // asks for it; [awaitingPermission] tracks the in-flight prompt so the button shows a + // spinner. The result callback fires on BOTH grant and denial — so a denial always + // clears the spinner (an isGranted-keyed effect wouldn't re-run on the false→false case). var wantsMyLocation by remember { mutableStateOf(false) } var awaitingPermission by remember { mutableStateOf(false) } + val permission = + rememberPermissionState(Manifest.permission.ACCESS_COARSE_LOCATION) { granted -> + awaitingPermission = false + if (granted) wantsMyLocation = true + } LaunchedEffect(permission.status.isGranted) { locationManager.setLocationPermission(permission.status.isGranted) - if (permission.status.isGranted && awaitingPermission) { - awaitingPermission = false - wantsMyLocation = true - } else if (!permission.status.isGranted) { - awaitingPermission = false - } } LaunchedEffect(wantsMyLocation) { if (wantsMyLocation) { @@ -309,11 +312,15 @@ fun GeohashLocationPickerContent( searching = true searchMissed = false results = emptyList() - ForwardGeolocation.execute(q, context) { addresses -> - searching = false - val hits = addresses.orEmpty().filter { it.hasLatitude() && it.hasLongitude() } - results = hits - searchMissed = hits.isEmpty() + // Off the main thread: on Android < 13 ForwardGeolocation.execute() calls the + // blocking Geocoder synchronously, which would freeze/ANR the UI thread. + scope.launch(Dispatchers.IO) { + ForwardGeolocation.execute(q, context) { addresses -> + searching = false + val hits = addresses.orEmpty().filter { it.hasLatitude() && it.hasLongitude() } + results = hits + searchMissed = hits.isEmpty() + } } } }