From 416dc989e8b8bca1d490ec2904b567eefe13c8a6 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 27 Jul 2026 01:51:44 +0200 Subject: [PATCH] Stop nearby notes while app is backgrounded --- .../android/nostr/NearbyNotesController.kt | 10 +++++- .../java/com/bitchat/android/ui/ChatScreen.kt | 28 ++++++++++++++++ .../nostr/NearbyNotesControllerTest.kt | 32 ++++++++++++++++--- 3 files changed, 65 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/nostr/NearbyNotesController.kt b/app/src/main/java/com/bitchat/android/nostr/NearbyNotesController.kt index 187c0e48..a5f1a8ec 100644 --- a/app/src/main/java/com/bitchat/android/nostr/NearbyNotesController.kt +++ b/app/src/main/java/com/bitchat/android/nostr/NearbyNotesController.kt @@ -12,7 +12,7 @@ import kotlinx.coroutines.flow.asStateFlow * * Merely rendering the mesh timeline must not open a building-precision Nostr * subscription. A subscription is eligible only after an explicit reveal and - * while at least one nearby-notes surface is active. + * while the app is foregrounded and at least one nearby-notes surface is active. */ @MainThread class NearbyNotesController internal constructor( @@ -25,6 +25,7 @@ class NearbyNotesController internal constructor( private var activeHolders = 0 private var locationEnabled = false private var locationAuthorized = false + private var appForeground = false private var buildingGeohash: String? = null private var subscribedGeohash: String? = null @@ -50,6 +51,12 @@ class NearbyNotesController internal constructor( reconcileSubscription() } + /** Closes the live subscription whenever the process leaves the foreground. */ + fun updateAppForeground(isForeground: Boolean) { + appForeground = isForeground + reconcileSubscription() + } + /** * Updates the privacy-sensitive inputs independently of view activation. * Permission revocation, location disable, or loss of the building cell @@ -78,6 +85,7 @@ class NearbyNotesController internal constructor( private fun reconcileSubscription() { val target = buildingGeohash.takeIf { activeHolders > 0 && + appForeground && _revealed.value && locationEnabled && locationAuthorized diff --git a/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt b/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt index 39bf8887..0514217d 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt @@ -29,6 +29,10 @@ import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.zIndex +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.bitchat.android.R import com.bitchat.android.geohash.ChannelID @@ -117,6 +121,30 @@ fun ChatScreen(viewModel: ChatViewModel) { selectedPrivatePeer == null && privateChatSheetPeer == null + val processLifecycleOwner = remember { ProcessLifecycleOwner.get() } + DisposableEffect(processLifecycleOwner, nearbyNotesController) { + val lifecycle = processLifecycleOwner.lifecycle + val observer = object : DefaultLifecycleObserver { + override fun onStart(owner: LifecycleOwner) { + nearbyNotesController.updateAppForeground(true) + } + + override fun onStop(owner: LifecycleOwner) { + nearbyNotesController.updateAppForeground(false) + } + } + + lifecycle.addObserver(observer) + nearbyNotesController.updateAppForeground( + lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED), + ) + + onDispose { + lifecycle.removeObserver(observer) + nearbyNotesController.updateAppForeground(false) + } + } + DisposableEffect( isMeshTimeline, locationEnabled, diff --git a/app/src/test/kotlin/com/bitchat/android/nostr/NearbyNotesControllerTest.kt b/app/src/test/kotlin/com/bitchat/android/nostr/NearbyNotesControllerTest.kt index f4252eeb..6bd86042 100644 --- a/app/src/test/kotlin/com/bitchat/android/nostr/NearbyNotesControllerTest.kt +++ b/app/src/test/kotlin/com/bitchat/android/nostr/NearbyNotesControllerTest.kt @@ -16,9 +16,13 @@ class NearbyNotesControllerTest { unsubscribe = { unsubscribeCount += 1 }, ) + private fun foregroundController() = controller().also { + it.updateAppForeground(true) + } + @Test fun `active mesh timeline does not subscribe before explicit reveal`() { - val controller = controller() + val controller = foregroundController() controller.updateAvailability( locationEnabled = true, @@ -38,7 +42,7 @@ class NearbyNotesControllerTest { @Test fun `reveal remains dormant until a nearby notes surface is active`() { - val controller = controller() + val controller = foregroundController() controller.updateAvailability(true, true, "u4pruydq") controller.reveal() @@ -52,7 +56,7 @@ class NearbyNotesControllerTest { @Test fun `last deactivate unsubscribes exactly once`() { - val controller = controller() + val controller = foregroundController() controller.updateAvailability(true, true, "u4pruydq") controller.reveal() controller.activate() @@ -67,9 +71,28 @@ class NearbyNotesControllerTest { assertEquals(1, unsubscribeCount) } + @Test + fun `backgrounding closes the subscription and foregrounding restores it`() { + val controller = foregroundController() + controller.updateAvailability(true, true, "u4pruydq") + controller.activate() + controller.reveal() + + controller.updateAppForeground(false) + + assertEquals(1, unsubscribeCount) + assertTrue(controller.revealed.value) + + controller.updateAppForeground(false) + assertEquals(1, unsubscribeCount) + + controller.updateAppForeground(true) + assertEquals(listOf("u4pruydq", "u4pruydq"), subscriptions) + } + @Test fun `disable and permission revocation close the live subscription`() { - val controller = controller() + val controller = foregroundController() controller.updateAvailability(true, true, "u4pruydq") controller.activate() controller.reveal() @@ -91,6 +114,7 @@ class NearbyNotesControllerTest { subscribe = { events += "subscribe:$it" }, unsubscribe = { events += "unsubscribe" }, ) + controller.updateAppForeground(true) controller.updateAvailability(true, true, "u4pruydq") controller.activate() controller.reveal()