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.
This commit is contained in:
Bardi Harborow
2026-10-06 09:39:18 +11:00
parent b76548f9a2
commit 90806a1c85
2 changed files with 41 additions and 19 deletions
@@ -15,11 +15,15 @@ import android.provider.Settings
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.edit
import androidx.lifecycle.compose.LifecycleResumeEffect
import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Inject import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.SingleIn import dev.zacsweers.metro.SingleIn
@@ -61,9 +65,15 @@ class FullScreenIntentPermissionsPresenter(
@Composable @Composable
override fun present(): FullScreenIntentPermissionsState { override fun present(): FullScreenIntentPermissionsState {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val isGranted = notificationManagerCompat.canUseFullScreenIntent() var isGranted by remember { mutableStateOf(notificationManagerCompat.canUseFullScreenIntent()) }
val isBannerDismissed by isFullScreenIntentBannerDismissed.collectAsState(initial = true) 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) { fun handleEvent(event: FullScreenIntentPermissionsEvent) {
when (event) { when (event) {
FullScreenIntentPermissionsEvent.Dismiss -> coroutineScope.launch { FullScreenIntentPermissionsEvent.Dismiss -> coroutineScope.launch {
@@ -11,9 +11,7 @@ package io.element.android.libraries.fullscreenintent.test
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import app.cash.molecule.RecompositionMode import androidx.lifecycle.Lifecycle
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsEvent 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.api.intent.ExternalIntentLauncher
import io.element.android.services.toolbox.test.intent.FakeExternalIntentLauncher import io.element.android.services.toolbox.test.intent.FakeExternalIntentLauncher
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider 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.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.testWithLifecycleOwner
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -45,9 +45,7 @@ class FullScreenIntentPermissionsPresenterTest {
every { canUseFullScreenIntent() } returns false every { canUseFullScreenIntent() } returns false
} }
) )
moleculeFlow(RecompositionMode.Immediate) { presenter.testWithLifecycleOwner {
presenter.present()
}.test {
skipItems(1) skipItems(1)
val initialItem = awaitItem() val initialItem = awaitItem()
assertThat(initialItem.shouldDisplayBanner).isTrue() assertThat(initialItem.shouldDisplayBanner).isTrue()
@@ -61,21 +59,39 @@ class FullScreenIntentPermissionsPresenterTest {
every { canUseFullScreenIntent() } returns true every { canUseFullScreenIntent() } returns true
} }
) )
moleculeFlow(RecompositionMode.Immediate) { presenter.testWithLifecycleOwner {
presenter.present()
}.test {
skipItems(1) skipItems(1)
val initialItem = awaitItem() val initialItem = awaitItem()
assertThat(initialItem.shouldDisplayBanner).isFalse() 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 @Test
fun `dismissFullScreenIntentBanner - makes shouldDisplay false`() = runTest { fun `dismissFullScreenIntentBanner - makes shouldDisplay false`() = runTest {
val presenter = createPresenter() val presenter = createPresenter()
moleculeFlow(RecompositionMode.Immediate) { presenter.testWithLifecycleOwner {
presenter.present()
}.test {
skipItems(1) skipItems(1)
val loadedItem = awaitItem() val loadedItem = awaitItem()
loadedItem.eventSink(FullScreenIntentPermissionsEvent.Dismiss) loadedItem.eventSink(FullScreenIntentPermissionsEvent.Dismiss)
@@ -89,9 +105,7 @@ class FullScreenIntentPermissionsPresenterTest {
val launchLambda = lambdaRecorder<Intent, Unit> { _ -> } val launchLambda = lambdaRecorder<Intent, Unit> { _ -> }
val externalIntentLauncher = FakeExternalIntentLauncher(launchLambda) val externalIntentLauncher = FakeExternalIntentLauncher(launchLambda)
val presenter = createPresenter(externalIntentLauncher = externalIntentLauncher) val presenter = createPresenter(externalIntentLauncher = externalIntentLauncher)
moleculeFlow(RecompositionMode.Immediate) { presenter.testWithLifecycleOwner {
presenter.present()
}.test {
skipItems(1) skipItems(1)
val loadedItem = awaitItem() val loadedItem = awaitItem()
loadedItem.eventSink(FullScreenIntentPermissionsEvent.OpenSettings) loadedItem.eventSink(FullScreenIntentPermissionsEvent.OpenSettings)
@@ -108,9 +122,7 @@ class FullScreenIntentPermissionsPresenterTest {
buildVersionSdkIntProvider = FakeBuildVersionSdkIntProvider(Build.VERSION_CODES.Q), buildVersionSdkIntProvider = FakeBuildVersionSdkIntProvider(Build.VERSION_CODES.Q),
externalIntentLauncher = externalIntentLauncher, externalIntentLauncher = externalIntentLauncher,
) )
moleculeFlow(RecompositionMode.Immediate) { presenter.testWithLifecycleOwner {
presenter.present()
}.test {
skipItems(1) skipItems(1)
val loadedItem = awaitItem() val loadedItem = awaitItem()
loadedItem.eventSink(FullScreenIntentPermissionsEvent.OpenSettings) loadedItem.eventSink(FullScreenIntentPermissionsEvent.OpenSettings)