From 90806a1c85b4ce4b9bc41269fdb8296bd2df5c4c Mon Sep 17 00:00:00 2001 From: Bardi Harborow Date: Tue, 6 Oct 2026 09:39:18 +1100 Subject: [PATCH] Hide full screen intent banner as soon as the permission is granted The permission is granted from the system settings, outside of the app, so nothing triggered a recomposition of FullScreenIntentPermissionsPresenter when the user came back. The "Enhance your call experience" banner kept being displayed until something else caused a recomposition. Check the permission again with LifecycleResumeEffect, as done in BatteryOptimizationPresenter, and add a test for this case. --- .../FullScreenIntentPermissionsPresenter.kt | 12 ++++- ...ullScreenIntentPermissionsPresenterTest.kt | 48 ++++++++++++------- 2 files changed, 41 insertions(+), 19 deletions(-) diff --git a/libraries/fullscreenintent/impl/src/main/kotlin/io/element/android/libraries/fullscreenintent/impl/FullScreenIntentPermissionsPresenter.kt b/libraries/fullscreenintent/impl/src/main/kotlin/io/element/android/libraries/fullscreenintent/impl/FullScreenIntentPermissionsPresenter.kt index 7d726ccdcc..201bc65e79 100644 --- a/libraries/fullscreenintent/impl/src/main/kotlin/io/element/android/libraries/fullscreenintent/impl/FullScreenIntentPermissionsPresenter.kt +++ b/libraries/fullscreenintent/impl/src/main/kotlin/io/element/android/libraries/fullscreenintent/impl/FullScreenIntentPermissionsPresenter.kt @@ -15,11 +15,15 @@ import android.provider.Settings import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState 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.core.app.NotificationManagerCompat import androidx.core.net.toUri import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit +import androidx.lifecycle.compose.LifecycleResumeEffect import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.Inject import dev.zacsweers.metro.SingleIn @@ -61,9 +65,15 @@ class FullScreenIntentPermissionsPresenter( @Composable override fun present(): FullScreenIntentPermissionsState { val coroutineScope = rememberCoroutineScope() - val isGranted = notificationManagerCompat.canUseFullScreenIntent() + var isGranted by remember { mutableStateOf(notificationManagerCompat.canUseFullScreenIntent()) } val isBannerDismissed by isFullScreenIntentBannerDismissed.collectAsState(initial = true) + // The permission can be granted from the system settings, so check it again when the user comes back to the app. + LifecycleResumeEffect(Unit) { + isGranted = notificationManagerCompat.canUseFullScreenIntent() + onPauseOrDispose {} + } + fun handleEvent(event: FullScreenIntentPermissionsEvent) { when (event) { FullScreenIntentPermissionsEvent.Dismiss -> coroutineScope.launch { diff --git a/libraries/fullscreenintent/impl/src/test/kotlin/io/element/android/libraries/fullscreenintent/test/FullScreenIntentPermissionsPresenterTest.kt b/libraries/fullscreenintent/impl/src/test/kotlin/io/element/android/libraries/fullscreenintent/test/FullScreenIntentPermissionsPresenterTest.kt index 08bc2ed885..ec075bbc25 100644 --- a/libraries/fullscreenintent/impl/src/test/kotlin/io/element/android/libraries/fullscreenintent/test/FullScreenIntentPermissionsPresenterTest.kt +++ b/libraries/fullscreenintent/impl/src/test/kotlin/io/element/android/libraries/fullscreenintent/test/FullScreenIntentPermissionsPresenterTest.kt @@ -11,9 +11,7 @@ package io.element.android.libraries.fullscreenintent.test import android.content.Intent import android.os.Build import androidx.core.app.NotificationManagerCompat -import app.cash.molecule.RecompositionMode -import app.cash.molecule.moleculeFlow -import app.cash.turbine.test +import androidx.lifecycle.Lifecycle import com.google.common.truth.Truth.assertThat import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsEvent @@ -23,8 +21,10 @@ import io.element.android.libraries.preferences.test.FakePreferenceDataStoreFact import io.element.android.services.toolbox.api.intent.ExternalIntentLauncher import io.element.android.services.toolbox.test.intent.FakeExternalIntentLauncher import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider +import io.element.android.tests.testutils.FakeLifecycleOwner import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.testWithLifecycleOwner import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -45,9 +45,7 @@ class FullScreenIntentPermissionsPresenterTest { every { canUseFullScreenIntent() } returns false } ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.testWithLifecycleOwner { skipItems(1) val initialItem = awaitItem() assertThat(initialItem.shouldDisplayBanner).isTrue() @@ -61,21 +59,39 @@ class FullScreenIntentPermissionsPresenterTest { every { canUseFullScreenIntent() } returns true } ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.testWithLifecycleOwner { skipItems(1) val initialItem = awaitItem() assertThat(initialItem.shouldDisplayBanner).isFalse() } } + @Test + fun `shouldDisplay - becomes false when permission is granted while the app is in background`() = runTest { + var canUseFullScreenIntent = false + val presenter = createPresenter( + notificationManagerCompat = mockk { + every { canUseFullScreenIntent() } answers { canUseFullScreenIntent } + } + ) + val lifecycleOwner = FakeLifecycleOwner(Lifecycle.State.RESUMED) + presenter.testWithLifecycleOwner(lifecycleOwner) { + skipItems(1) + assertThat(awaitItem().shouldDisplayBanner).isTrue() + // User opens the system settings and grants the permission + lifecycleOwner.givenState(Lifecycle.State.STARTED) + canUseFullScreenIntent = true + lifecycleOwner.givenState(Lifecycle.State.RESUMED) + val finalItem = awaitItem() + assertThat(finalItem.permissionGranted).isTrue() + assertThat(finalItem.shouldDisplayBanner).isFalse() + } + } + @Test fun `dismissFullScreenIntentBanner - makes shouldDisplay false`() = runTest { val presenter = createPresenter() - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.testWithLifecycleOwner { skipItems(1) val loadedItem = awaitItem() loadedItem.eventSink(FullScreenIntentPermissionsEvent.Dismiss) @@ -89,9 +105,7 @@ class FullScreenIntentPermissionsPresenterTest { val launchLambda = lambdaRecorder { _ -> } val externalIntentLauncher = FakeExternalIntentLauncher(launchLambda) val presenter = createPresenter(externalIntentLauncher = externalIntentLauncher) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.testWithLifecycleOwner { skipItems(1) val loadedItem = awaitItem() loadedItem.eventSink(FullScreenIntentPermissionsEvent.OpenSettings) @@ -108,9 +122,7 @@ class FullScreenIntentPermissionsPresenterTest { buildVersionSdkIntProvider = FakeBuildVersionSdkIntProvider(Build.VERSION_CODES.Q), externalIntentLauncher = externalIntentLauncher, ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { + presenter.testWithLifecycleOwner { skipItems(1) val loadedItem = awaitItem() loadedItem.eventSink(FullScreenIntentPermissionsEvent.OpenSettings)