From 29bbfd04c24227a8c0deb4277ce1795859f07dc3 Mon Sep 17 00:00:00 2001 From: Taksh Date: Mon, 5 Oct 2026 16:10:58 +0530 Subject: [PATCH 1/3] Preserve granted location privacy state when enabling Split the existing PR change into a smaller reviewable piece. Original change: fix: do not revoke live location when enabling it --- .../com/bitchat/android/geohash/LiveLocationPrivacyGate.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/bitchat/android/geohash/LiveLocationPrivacyGate.kt b/app/src/main/java/com/bitchat/android/geohash/LiveLocationPrivacyGate.kt index 5a4f30fb2..ebcdfd704 100644 --- a/app/src/main/java/com/bitchat/android/geohash/LiveLocationPrivacyGate.kt +++ b/app/src/main/java/com/bitchat/android/geohash/LiveLocationPrivacyGate.kt @@ -89,7 +89,12 @@ internal object LiveLocationPrivacyGate { fun update(enabled: Boolean) { policy.update(enabled) - notifyRevoked() + // Revocation listeners cancel in-flight location work. Turning location + // *on* must not fire them — that used to kill the one-shot/refresh that + // Enable Location just started (#577). + if (!enabled) { + notifyRevoked() + } } fun invalidate() { From 40e741221a69bc16213445c13da0e9159b8c3d75 Mon Sep 17 00:00:00 2001 From: Taksh Date: Mon, 5 Oct 2026 16:10:58 +0530 Subject: [PATCH 2/3] Refresh live location controls without revoking permission Split the existing PR change into a smaller reviewable piece. Original change: fix: do not revoke live location when enabling it --- .../android/ui/LocationNotesSheetPresenter.kt | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/bitchat/android/ui/LocationNotesSheetPresenter.kt b/app/src/main/java/com/bitchat/android/ui/LocationNotesSheetPresenter.kt index 9a8cf9e72..69e45a709 100644 --- a/app/src/main/java/com/bitchat/android/ui/LocationNotesSheetPresenter.kt +++ b/app/src/main/java/com/bitchat/android/ui/LocationNotesSheetPresenter.kt @@ -3,6 +3,7 @@ package com.bitchat.android.ui import androidx.compose.foundation.layout.* import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -17,6 +18,7 @@ import com.bitchat.android.core.ui.component.sheet.BitchatSheetTitle import com.bitchat.android.geohash.GeohashChannelLevel import com.bitchat.android.geohash.LocationChannelManager import com.bitchat.android.R +import com.bitchat.android.geohash.LiveLocationPrivacyGate /** * Presenter component for LocationNotesSheet @@ -34,6 +36,8 @@ fun LocationNotesSheetPresenter( val availableChannels by locationManager.availableChannels.collectAsStateWithLifecycle() val permissionState by locationManager.permissionState.collectAsStateWithLifecycle() val isLoadingLocation by locationManager.isLoadingLocation.collectAsStateWithLifecycle() + val appLocationEnabled by LiveLocationPrivacyGate.enabled.collectAsStateWithLifecycle() + val systemLocationEnabled by locationManager.systemLocationEnabled.collectAsStateWithLifecycle() val nickname by viewModel.nickname.collectAsStateWithLifecycle() // iOS pattern: notesGeohash ?? LocationChannelManager.shared.availableChannels.first(where: { $0.level == .building })?.geohash @@ -51,7 +55,17 @@ fun LocationNotesSheetPresenter( nickname = nickname, onDismiss = onDismiss ) - } else if (permissionState == LocationChannelManager.PermissionState.AUTHORIZED && isLoadingLocation) { + } else if (permissionState == LocationChannelManager.PermissionState.AUTHORIZED && + (isLoadingLocation || (appLocationEnabled && systemLocationEnabled)) + ) { + // Authorized with services on but no building channel yet — first + // install often has no cached last-fix, so treat this as acquiring + // rather than "unavailable" (#577). Kick a live refresh once. + LaunchedEffect(appLocationEnabled, systemLocationEnabled) { + if (appLocationEnabled && systemLocationEnabled) { + locationManager.beginLiveRefresh() + } + } LocationNotesAcquiringSheet(onDismiss = onDismiss) } else { // No building geohash available - show error state (matches iOS) @@ -138,6 +152,7 @@ private fun LocationNotesErrorSheet( // Then request location channels (which will also request permission if needed) locationManager.enableLocationChannels() locationManager.refreshChannels() + locationManager.beginLiveRefresh() }) { Text("Enable Location") } From 577f8f6260bb37aecf25619d17e1f468efd437d1 Mon Sep 17 00:00:00 2001 From: Taksh Date: Mon, 5 Oct 2026 16:10:58 +0530 Subject: [PATCH 3/3] Test live location permission preservation Split the existing PR change into a smaller reviewable piece. Original change: fix: do not revoke live location when enabling it --- .../geohash/LiveLocationPrivacyGateTest.kt | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 app/src/test/java/com/bitchat/android/geohash/LiveLocationPrivacyGateTest.kt diff --git a/app/src/test/java/com/bitchat/android/geohash/LiveLocationPrivacyGateTest.kt b/app/src/test/java/com/bitchat/android/geohash/LiveLocationPrivacyGateTest.kt new file mode 100644 index 000000000..ee4caa900 --- /dev/null +++ b/app/src/test/java/com/bitchat/android/geohash/LiveLocationPrivacyGateTest.kt @@ -0,0 +1,32 @@ +package com.bitchat.android.geohash + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class LiveLocationPrivacyGateTest { + @Test + fun `enabling location does not fire revocation listeners`() { + var revoked = 0 + val listener = { revoked += 1 } + LiveLocationPrivacyGate.addRevocationListener(listener) + try { + LiveLocationPrivacyGate.update(false) + val afterDisable = revoked + assertTrue("disable should revoke", afterDisable >= 1) + + LiveLocationPrivacyGate.update(true) + assertEquals( + "enable must not cancel in-flight location work", + afterDisable, + revoked + ) + + LiveLocationPrivacyGate.update(false) + assertTrue(revoked > afterDisable) + } finally { + LiveLocationPrivacyGate.removeRevocationListener(listener) + LiveLocationPrivacyGate.update(false) + } + } +}