Merge pull request #5877 from element-hq/feature/bma/fixPermissionSettingNavigation

Fix permission setting navigation
This commit is contained in:
Benoit Marty 2025-12-10 11:19:48 +01:00 committed by GitHub
commit 00265d9574
17 changed files with 149 additions and 179 deletions

View file

@ -40,7 +40,7 @@ import io.element.android.libraries.matrix.ui.room.address.RoomAddressValidityEf
import io.element.android.libraries.mediapickers.api.PickerProvider import io.element.android.libraries.mediapickers.api.PickerProvider
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProvider import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProvider
import io.element.android.libraries.mediaupload.api.MediaPreProcessor import io.element.android.libraries.mediaupload.api.MediaPreProcessor
import io.element.android.libraries.permissions.api.PermissionsEvents import io.element.android.libraries.permissions.api.PermissionsEvent
import io.element.android.libraries.permissions.api.PermissionsPresenter import io.element.android.libraries.permissions.api.PermissionsPresenter
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@ -132,7 +132,7 @@ class ConfigureRoomPresenter(
cameraPhotoPicker.launch() cameraPhotoPicker.launch()
} else { } else {
pendingPermissionRequest = true pendingPermissionRequest = true
cameraPermissionState.eventSink(PermissionsEvents.RequestPermissions) cameraPermissionState.eventSink(PermissionsEvent.RequestPermissions)
} }
AvatarAction.Remove -> dataStore.setAvatarUri(uri = null) AvatarAction.Remove -> dataStore.setAvatarUri(uri = null)
} }

View file

@ -19,7 +19,7 @@ import dev.zacsweers.metro.AssistedInject
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.permissions.api.PermissionStateProvider import io.element.android.libraries.permissions.api.PermissionStateProvider
import io.element.android.libraries.permissions.api.PermissionsEvents import io.element.android.libraries.permissions.api.PermissionsEvent
import io.element.android.libraries.permissions.api.PermissionsPresenter import io.element.android.libraries.permissions.api.PermissionsPresenter
import io.element.android.libraries.permissions.noop.NoopPermissionsPresenter import io.element.android.libraries.permissions.noop.NoopPermissionsPresenter
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
@ -58,7 +58,7 @@ class NotificationsOptInPresenter(
if (notificationsPermissionsState.permissionGranted) { if (notificationsPermissionsState.permissionGranted) {
callback.onNotificationsOptInFinished() callback.onNotificationsOptInFinished()
} else { } else {
notificationsPermissionsState.eventSink(PermissionsEvents.RequestPermissions) notificationsPermissionsState.eventSink(PermissionsEvent.RequestPermissions)
} }
} }
NotificationsOptInEvents.NotNowClicked -> { NotificationsOptInEvents.NotNowClicked -> {

View file

@ -18,7 +18,7 @@ import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Inject import dev.zacsweers.metro.Inject
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.permissions.api.PermissionsEvents import io.element.android.libraries.permissions.api.PermissionsEvent
import io.element.android.libraries.permissions.api.PermissionsPresenter import io.element.android.libraries.permissions.api.PermissionsPresenter
@Inject @Inject
@ -46,7 +46,7 @@ class QrCodeIntroPresenter(
canContinue = true canContinue = true
} else { } else {
pendingPermissionRequest = true pendingPermissionRequest = true
cameraPermissionState.eventSink(PermissionsEvents.RequestPermissions) cameraPermissionState.eventSink(PermissionsEvent.RequestPermissions)
} }
} }
} }

View file

@ -63,7 +63,7 @@ import io.element.android.libraries.mediapickers.api.PickerProvider
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProvider import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProvider
import io.element.android.libraries.mediaupload.api.MediaSenderFactory import io.element.android.libraries.mediaupload.api.MediaSenderFactory
import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory
import io.element.android.libraries.permissions.api.PermissionsEvents import io.element.android.libraries.permissions.api.PermissionsEvent
import io.element.android.libraries.permissions.api.PermissionsPresenter import io.element.android.libraries.permissions.api.PermissionsPresenter
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
import io.element.android.libraries.push.api.notifications.conversations.NotificationConversationService import io.element.android.libraries.push.api.notifications.conversations.NotificationConversationService
@ -284,7 +284,7 @@ class MessageComposerPresenter(
cameraPhotoPicker.launch() cameraPhotoPicker.launch()
} else { } else {
pendingEvent = event pendingEvent = event
cameraPermissionState.eventSink(PermissionsEvents.RequestPermissions) cameraPermissionState.eventSink(PermissionsEvent.RequestPermissions)
} }
} }
MessageComposerEvent.PickAttachmentSource.VideoFromCamera -> localCoroutineScope.launch { MessageComposerEvent.PickAttachmentSource.VideoFromCamera -> localCoroutineScope.launch {
@ -293,7 +293,7 @@ class MessageComposerPresenter(
cameraVideoPicker.launch() cameraVideoPicker.launch()
} else { } else {
pendingEvent = event pendingEvent = event
cameraPermissionState.eventSink(PermissionsEvents.RequestPermissions) cameraPermissionState.eventSink(PermissionsEvent.RequestPermissions)
} }
} }
MessageComposerEvent.PickAttachmentSource.Location -> { MessageComposerEvent.PickAttachmentSource.Location -> {

View file

@ -33,7 +33,7 @@ import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.mediaupload.api.MediaSenderFactory import io.element.android.libraries.mediaupload.api.MediaSenderFactory
import io.element.android.libraries.permissions.api.PermissionsEvents import io.element.android.libraries.permissions.api.PermissionsEvent
import io.element.android.libraries.permissions.api.PermissionsPresenter import io.element.android.libraries.permissions.api.PermissionsPresenter
import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent
import io.element.android.libraries.textcomposer.model.VoiceMessageRecorderEvent import io.element.android.libraries.textcomposer.model.VoiceMessageRecorderEvent
@ -111,7 +111,7 @@ class DefaultVoiceMessageComposerPresenter(
} }
else -> { else -> {
Timber.i("Voice message permission needed") Timber.i("Voice message permission needed")
permissionState.eventSink(PermissionsEvents.RequestPermissions) permissionState.eventSink(PermissionsEvent.RequestPermissions)
} }
} }
} }
@ -176,10 +176,10 @@ class DefaultVoiceMessageComposerPresenter(
localCoroutineScope.deleteRecording() localCoroutineScope.deleteRecording()
} }
VoiceMessageComposerEvent.DismissPermissionsRationale -> { VoiceMessageComposerEvent.DismissPermissionsRationale -> {
permissionState.eventSink(PermissionsEvents.CloseDialog) permissionState.eventSink(PermissionsEvent.CloseDialog)
} }
VoiceMessageComposerEvent.AcceptPermissionRationale -> { VoiceMessageComposerEvent.AcceptPermissionRationale -> {
permissionState.eventSink(PermissionsEvents.OpenSystemSettingAndCloseDialog) permissionState.eventSink(PermissionsEvent.OpenSystemSettingAndCloseDialog)
} }
is VoiceMessageComposerEvent.LifecycleEvent -> handleLifecycleEvent(event.event) is VoiceMessageComposerEvent.LifecycleEvent -> handleLifecycleEvent(event.event)
VoiceMessageComposerEvent.DismissSendFailureDialog -> { VoiceMessageComposerEvent.DismissSendFailureDialog -> {

View file

@ -35,7 +35,7 @@ import io.element.android.libraries.matrix.ui.media.AvatarAction
import io.element.android.libraries.mediapickers.api.PickerProvider import io.element.android.libraries.mediapickers.api.PickerProvider
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProvider import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProvider
import io.element.android.libraries.mediaupload.api.MediaPreProcessor import io.element.android.libraries.mediaupload.api.MediaPreProcessor
import io.element.android.libraries.permissions.api.PermissionsEvents import io.element.android.libraries.permissions.api.PermissionsEvent
import io.element.android.libraries.permissions.api.PermissionsPresenter import io.element.android.libraries.permissions.api.PermissionsPresenter
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -127,7 +127,7 @@ class EditUserProfilePresenter(
cameraPhotoPicker.launch() cameraPhotoPicker.launch()
} else { } else {
pendingPermissionRequest = true pendingPermissionRequest = true
cameraPermissionState.eventSink(PermissionsEvents.RequestPermissions) cameraPermissionState.eventSink(PermissionsEvent.RequestPermissions)
} }
AvatarAction.Remove -> { AvatarAction.Remove -> {
temporaryUriDeleter.delete(userAvatarUri?.toUri()) temporaryUriDeleter.delete(userAvatarUri?.toUri())

View file

@ -36,7 +36,7 @@ import io.element.android.libraries.matrix.ui.media.AvatarAction
import io.element.android.libraries.mediapickers.api.PickerProvider import io.element.android.libraries.mediapickers.api.PickerProvider
import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProvider import io.element.android.libraries.mediaupload.api.MediaOptimizationConfigProvider
import io.element.android.libraries.mediaupload.api.MediaPreProcessor import io.element.android.libraries.mediaupload.api.MediaPreProcessor
import io.element.android.libraries.permissions.api.PermissionsEvents import io.element.android.libraries.permissions.api.PermissionsEvent
import io.element.android.libraries.permissions.api.PermissionsPresenter import io.element.android.libraries.permissions.api.PermissionsPresenter
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -157,7 +157,7 @@ class RoomDetailsEditPresenter(
cameraPhotoPicker.launch() cameraPhotoPicker.launch()
} else { } else {
pendingPermissionRequest = true pendingPermissionRequest = true
cameraPermissionState.eventSink(PermissionsEvents.RequestPermissions) cameraPermissionState.eventSink(PermissionsEvent.RequestPermissions)
} }
AvatarAction.Remove -> { AvatarAction.Remove -> {
temporaryUriDeleter.delete(roomAvatarUriEdited?.toUri()) temporaryUriDeleter.delete(roomAvatarUriEdited?.toUri())

View file

@ -8,8 +8,8 @@
package io.element.android.libraries.permissions.api package io.element.android.libraries.permissions.api
sealed interface PermissionsEvents { sealed interface PermissionsEvent {
data object RequestPermissions : PermissionsEvents data object RequestPermissions : PermissionsEvent
data object CloseDialog : PermissionsEvents data object CloseDialog : PermissionsEvent
data object OpenSystemSettingAndCloseDialog : PermissionsEvents data object OpenSystemSettingAndCloseDialog : PermissionsEvent
} }

View file

@ -17,5 +17,5 @@ data class PermissionsState(
val permissionAlreadyAsked: Boolean, val permissionAlreadyAsked: Boolean,
// If true, there is no need to ask again, the system dialog will not be displayed // If true, there is no need to ask again, the system dialog will not be displayed
val permissionAlreadyDenied: Boolean, val permissionAlreadyDenied: Boolean,
val eventSink: (PermissionsEvents) -> Unit val eventSink: (PermissionsEvent) -> Unit
) )

View file

@ -35,9 +35,9 @@ fun PermissionsView(
content = content ?: state.permission.toDialogContent(), content = content ?: state.permission.toDialogContent(),
submitText = stringResource(id = CommonStrings.action_open_settings), submitText = stringResource(id = CommonStrings.action_open_settings),
onSubmitClick = { onSubmitClick = {
state.eventSink.invoke(PermissionsEvents.OpenSystemSettingAndCloseDialog) state.eventSink.invoke(PermissionsEvent.OpenSystemSettingAndCloseDialog)
}, },
onDismiss = { state.eventSink.invoke(PermissionsEvents.CloseDialog) }, onDismiss = { state.eventSink.invoke(PermissionsEvent.CloseDialog) },
icon = icon, icon = icon,
) )
} }

View file

@ -28,7 +28,7 @@ import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.AssistedInject
import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.permissions.api.PermissionsEvents import io.element.android.libraries.permissions.api.PermissionsEvent
import io.element.android.libraries.permissions.api.PermissionsPresenter import io.element.android.libraries.permissions.api.PermissionsPresenter
import io.element.android.libraries.permissions.api.PermissionsState import io.element.android.libraries.permissions.api.PermissionsState
import io.element.android.libraries.permissions.api.PermissionsStore import io.element.android.libraries.permissions.api.PermissionsStore
@ -100,20 +100,20 @@ class DefaultPermissionsPresenter(
val showDialog = rememberSaveable { mutableStateOf(false) } val showDialog = rememberSaveable { mutableStateOf(false) }
fun handleEvent(event: PermissionsEvents) { fun handleEvent(event: PermissionsEvent) {
when (event) { when (event) {
PermissionsEvents.CloseDialog -> { PermissionsEvent.CloseDialog -> {
showDialog.value = false showDialog.value = false
} }
PermissionsEvents.RequestPermissions -> { PermissionsEvent.RequestPermissions -> {
if (permissionState.status !is PermissionStatus.Granted && isAlreadyDenied) { if (permissionState.status !is PermissionStatus.Granted && isAlreadyDenied) {
showDialog.value = true showDialog.value = true
} else { } else {
permissionState.launchPermissionRequest() permissionState.launchPermissionRequest()
} }
} }
PermissionsEvents.OpenSystemSettingAndCloseDialog -> { PermissionsEvent.OpenSystemSettingAndCloseDialog -> {
permissionActions.openSettings() permissionActions.openSettings(permission)
showDialog.value = false showDialog.value = false
} }
} }

View file

@ -8,9 +8,11 @@
package io.element.android.libraries.permissions.impl.action package io.element.android.libraries.permissions.impl.action
import android.Manifest
import android.content.Context import android.content.Context
import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.androidutils.system.openAppSettingsPage
import io.element.android.libraries.androidutils.system.startNotificationSettingsIntent import io.element.android.libraries.androidutils.system.startNotificationSettingsIntent
import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.libraries.di.annotations.ApplicationContext
@ -18,7 +20,10 @@ import io.element.android.libraries.di.annotations.ApplicationContext
class AndroidPermissionActions( class AndroidPermissionActions(
@ApplicationContext private val context: Context @ApplicationContext private val context: Context
) : PermissionActions { ) : PermissionActions {
override fun openSettings() { override fun openSettings(permission: String) {
context.startNotificationSettingsIntent() when (permission) {
Manifest.permission.POST_NOTIFICATIONS -> context.startNotificationSettingsIntent()
else -> context.openAppSettingsPage()
}
} }
} }

View file

@ -9,5 +9,5 @@
package io.element.android.libraries.permissions.impl.action package io.element.android.libraries.permissions.impl.action
interface PermissionActions { interface PermissionActions {
fun openSettings() fun openSettings(permission: String)
} }

View file

@ -61,6 +61,6 @@ class NotificationTroubleshootCheckPermissionTest(
navigator: NotificationTroubleshootNavigator, navigator: NotificationTroubleshootNavigator,
) { ) {
// Do not bother about asking the permission inline, just lead the user to the settings // Do not bother about asking the permission inline, just lead the user to the settings
permissionActions.openSettings() permissionActions.openSettings(Manifest.permission.POST_NOTIFICATIONS)
} }
} }

View file

@ -10,21 +10,23 @@
package io.element.android.libraries.permissions.impl package io.element.android.libraries.permissions.impl
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionStatus import com.google.accompanist.permissions.PermissionStatus
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.permissions.api.PermissionsEvents import io.element.android.libraries.permissions.api.PermissionsEvent
import io.element.android.libraries.permissions.api.PermissionsStore
import io.element.android.libraries.permissions.impl.action.FakePermissionActions import io.element.android.libraries.permissions.impl.action.FakePermissionActions
import io.element.android.libraries.permissions.impl.action.PermissionActions
import io.element.android.libraries.permissions.test.InMemoryPermissionsStore import io.element.android.libraries.permissions.test.InMemoryPermissionsStore
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.value
import io.element.android.tests.testutils.test
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
const val A_PERMISSION = "A_PERMISSION" private const val A_PERMISSION = "A_PERMISSION"
class DefaultPermissionsPresenterTest { class DefaultPermissionsPresenterTest {
@get:Rule @get:Rule
@ -32,24 +34,8 @@ class DefaultPermissionsPresenterTest {
@Test @Test
fun `present - initial state`() = runTest { fun `present - initial state`() = runTest {
val permissionsStore = InMemoryPermissionsStore() val presenter = createPresenter()
val permissionState = FakePermissionState( presenter.test {
A_PERMISSION,
PermissionStatus.Granted
)
val permissionStateProvider =
FakeComposablePermissionStateProvider(
permissionState
)
val presenter = DefaultPermissionsPresenter(
A_PERMISSION,
permissionsStore,
permissionStateProvider,
FakePermissionActions(),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.permission).isEqualTo(A_PERMISSION) assertThat(initialState.permission).isEqualTo(A_PERMISSION)
assertThat(initialState.permissionGranted).isTrue() assertThat(initialState.permissionGranted).isTrue()
@ -66,29 +52,22 @@ class DefaultPermissionsPresenterTest {
permissionDenied = true, permissionDenied = true,
permissionAsked = true permissionAsked = true
) )
val permissionState = FakePermissionState( val permissionStateProvider = FakeComposablePermissionStateProvider(
A_PERMISSION, permissionState = aFakePermissionState(
PermissionStatus.Denied(shouldShowRationale = false) initialStatus = PermissionStatus.Denied(shouldShowRationale = false)
),
) )
val permissionStateProvider = val presenter = createPresenter(
FakeComposablePermissionStateProvider( permissionsStore = permissionsStore,
permissionState permissionStateProvider = permissionStateProvider,
)
val presenter = DefaultPermissionsPresenter(
A_PERMISSION,
permissionsStore,
permissionStateProvider,
FakePermissionActions(),
) )
moleculeFlow(RecompositionMode.Immediate) { presenter.test {
presenter.present()
}.test {
skipItems(1) skipItems(1)
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink.invoke(PermissionsEvents.RequestPermissions) initialState.eventSink.invoke(PermissionsEvent.RequestPermissions)
val withDialogState = awaitItem() val withDialogState = awaitItem()
assertThat(withDialogState.showDialog).isTrue() assertThat(withDialogState.showDialog).isTrue()
withDialogState.eventSink.invoke(PermissionsEvents.CloseDialog) withDialogState.eventSink.invoke(PermissionsEvent.CloseDialog)
assertThat(awaitItem().showDialog).isFalse() assertThat(awaitItem().showDialog).isFalse()
} }
} }
@ -99,59 +78,48 @@ class DefaultPermissionsPresenterTest {
permissionDenied = true, permissionDenied = true,
permissionAsked = true permissionAsked = true
) )
val permissionState = FakePermissionState( val permissionStateProvider = FakeComposablePermissionStateProvider(
A_PERMISSION, permissionState = aFakePermissionState(
PermissionStatus.Denied(shouldShowRationale = false) initialStatus = PermissionStatus.Denied(shouldShowRationale = false),
),
) )
val permissionStateProvider = val openSettingsAction = lambdaRecorder<String, Unit> { }
FakeComposablePermissionStateProvider( val permissionActions = FakePermissionActions(
permissionState openSettingsAction = openSettingsAction,
)
val permissionActions = FakePermissionActions()
val presenter = DefaultPermissionsPresenter(
A_PERMISSION,
permissionsStore,
permissionStateProvider,
permissionActions,
) )
moleculeFlow(RecompositionMode.Immediate) { val presenter = createPresenter(
presenter.present() permissionsStore = permissionsStore,
}.test { permissionStateProvider = permissionStateProvider,
permissionActions = permissionActions,
)
presenter.test {
skipItems(1) skipItems(1)
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink.invoke(PermissionsEvents.RequestPermissions) initialState.eventSink.invoke(PermissionsEvent.RequestPermissions)
val withDialogState = awaitItem() val withDialogState = awaitItem()
assertThat(withDialogState.showDialog).isTrue() assertThat(withDialogState.showDialog).isTrue()
assertThat(permissionActions.openSettingsCalled).isFalse() openSettingsAction.assertions().isNeverCalled()
withDialogState.eventSink.invoke(PermissionsEvents.OpenSystemSettingAndCloseDialog) withDialogState.eventSink.invoke(PermissionsEvent.OpenSystemSettingAndCloseDialog)
assertThat(awaitItem().showDialog).isFalse() assertThat(awaitItem().showDialog).isFalse()
assertThat(permissionActions.openSettingsCalled).isTrue() openSettingsAction.assertions().isCalledOnce().with(value(A_PERMISSION))
} }
} }
@Test @Test
fun `present - user does not grant permission`() = runTest { fun `present - user does not grant permission`() = runTest {
val permissionsStore = InMemoryPermissionsStore() val permissionState = aFakePermissionState(
val permissionState = FakePermissionState( initialStatus = PermissionStatus.Denied(shouldShowRationale = false)
A_PERMISSION,
PermissionStatus.Denied(shouldShowRationale = false)
) )
val permissionStateProvider = val permissionStateProvider = FakeComposablePermissionStateProvider(
FakeComposablePermissionStateProvider( permissionState = permissionState,
permissionState
)
val presenter = DefaultPermissionsPresenter(
A_PERMISSION,
permissionsStore,
permissionStateProvider,
FakePermissionActions(),
) )
moleculeFlow(RecompositionMode.Immediate) { val presenter = createPresenter(
presenter.present() permissionStateProvider = permissionStateProvider,
}.test { )
presenter.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.showDialog).isFalse() assertThat(initialState.showDialog).isFalse()
initialState.eventSink.invoke(PermissionsEvents.RequestPermissions) initialState.eventSink.invoke(PermissionsEvent.RequestPermissions)
assertThat(permissionState.launchPermissionRequestCalled).isTrue() assertThat(permissionState.launchPermissionRequestCalled).isTrue()
// User does not grant permission // User does not grant permission
permissionStateProvider.userGiveAnswer(answer = false, firstTime = true) permissionStateProvider.userGiveAnswer(answer = false, firstTime = true)
@ -166,27 +134,19 @@ class DefaultPermissionsPresenterTest {
@Test @Test
fun `present - user does not grant permission second time`() = runTest { fun `present - user does not grant permission second time`() = runTest {
val permissionsStore = InMemoryPermissionsStore() val permissionState = aFakePermissionState(
val permissionState = FakePermissionState( initialStatus = PermissionStatus.Denied(shouldShowRationale = true)
A_PERMISSION,
PermissionStatus.Denied(shouldShowRationale = true)
) )
val permissionStateProvider = val permissionStateProvider = FakeComposablePermissionStateProvider(
FakeComposablePermissionStateProvider( permissionState = permissionState,
permissionState
)
val presenter = DefaultPermissionsPresenter(
A_PERMISSION,
permissionsStore,
permissionStateProvider,
FakePermissionActions(),
) )
moleculeFlow(RecompositionMode.Immediate) { val presenter = createPresenter(
presenter.present() permissionStateProvider = permissionStateProvider,
}.test { )
presenter.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.showDialog).isFalse() assertThat(initialState.showDialog).isFalse()
initialState.eventSink.invoke(PermissionsEvents.RequestPermissions) initialState.eventSink.invoke(PermissionsEvent.RequestPermissions)
assertThat(permissionState.launchPermissionRequestCalled).isTrue() assertThat(permissionState.launchPermissionRequestCalled).isTrue()
// User does not grant permission // User does not grant permission
permissionStateProvider.userGiveAnswer(answer = false, firstTime = false) permissionStateProvider.userGiveAnswer(answer = false, firstTime = false)
@ -201,31 +161,24 @@ class DefaultPermissionsPresenterTest {
@Test @Test
fun `present - user does not grant permission third time`() = runTest { fun `present - user does not grant permission third time`() = runTest {
val permissionsStore = val permissionsStore = InMemoryPermissionsStore(
InMemoryPermissionsStore( permissionDenied = true,
permissionDenied = true, permissionAsked = true,
permissionAsked = true
)
val permissionState = FakePermissionState(
A_PERMISSION,
PermissionStatus.Denied(shouldShowRationale = false)
) )
val permissionStateProvider = val permissionState = aFakePermissionState(
FakeComposablePermissionStateProvider( initialStatus = PermissionStatus.Denied(shouldShowRationale = false),
permissionState
)
val presenter = DefaultPermissionsPresenter(
A_PERMISSION,
permissionsStore,
permissionStateProvider,
FakePermissionActions(),
) )
moleculeFlow(RecompositionMode.Immediate) { val permissionStateProvider = FakeComposablePermissionStateProvider(
presenter.present() permissionState = permissionState,
}.test { )
val presenter = createPresenter(
permissionsStore = permissionsStore,
permissionStateProvider = permissionStateProvider,
)
presenter.test {
skipItems(1) skipItems(1)
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink.invoke(PermissionsEvents.RequestPermissions) initialState.eventSink.invoke(PermissionsEvent.RequestPermissions)
val withDialogState = awaitItem() val withDialogState = awaitItem()
assertThat(withDialogState.showDialog).isTrue() assertThat(withDialogState.showDialog).isTrue()
assertThat(withDialogState.permissionGranted).isFalse() assertThat(withDialogState.permissionGranted).isFalse()
@ -236,27 +189,19 @@ class DefaultPermissionsPresenterTest {
@Test @Test
fun `present - user grants permission`() = runTest { fun `present - user grants permission`() = runTest {
val permissionsStore = InMemoryPermissionsStore() val permissionState = aFakePermissionState(
val permissionState = FakePermissionState( initialStatus = PermissionStatus.Denied(shouldShowRationale = false)
A_PERMISSION,
PermissionStatus.Denied(shouldShowRationale = false)
) )
val permissionStateProvider = val permissionStateProvider = FakeComposablePermissionStateProvider(
FakeComposablePermissionStateProvider( permissionState = permissionState,
permissionState
)
val presenter = DefaultPermissionsPresenter(
A_PERMISSION,
permissionsStore,
permissionStateProvider,
FakePermissionActions(),
) )
moleculeFlow(RecompositionMode.Immediate) { val presenter = createPresenter(
presenter.present() permissionStateProvider = permissionStateProvider,
}.test { )
presenter.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.showDialog).isFalse() assertThat(initialState.showDialog).isFalse()
initialState.eventSink.invoke(PermissionsEvents.RequestPermissions) initialState.eventSink.invoke(PermissionsEvent.RequestPermissions)
assertThat(permissionState.launchPermissionRequestCalled).isTrue() assertThat(permissionState.launchPermissionRequestCalled).isTrue()
// User grants permission // User grants permission
permissionStateProvider.userGiveAnswer(answer = true, firstTime = true) permissionStateProvider.userGiveAnswer(answer = true, firstTime = true)
@ -269,3 +214,25 @@ class DefaultPermissionsPresenterTest {
} }
} }
} }
private fun createPresenter(
permission: String = A_PERMISSION,
permissionsStore: PermissionsStore = InMemoryPermissionsStore(),
permissionStateProvider: ComposablePermissionStateProvider = FakeComposablePermissionStateProvider(
permissionState = aFakePermissionState(),
),
permissionActions: PermissionActions = FakePermissionActions(),
) = DefaultPermissionsPresenter(
permission = permission,
permissionsStore = permissionsStore,
composablePermissionStateProvider = permissionStateProvider,
permissionActions = permissionActions,
)
private fun aFakePermissionState(
permission: String = A_PERMISSION,
initialStatus: PermissionStatus = PermissionStatus.Granted,
) = FakePermissionState(
permission = permission,
initialStatus = initialStatus,
)

View file

@ -8,14 +8,12 @@
package io.element.android.libraries.permissions.impl.action package io.element.android.libraries.permissions.impl.action
class FakePermissionActions( import io.element.android.tests.testutils.lambda.lambdaError
val openSettingsAction: () -> Unit = {}
) : PermissionActions {
var openSettingsCalled = false
private set
override fun openSettings() { class FakePermissionActions(
openSettingsAction() val openSettingsAction: (String) -> Unit = { lambdaError() }
openSettingsCalled = true ) : PermissionActions {
override fun openSettings(permission: String) {
openSettingsAction(permission)
} }
} }

View file

@ -10,7 +10,7 @@ package io.element.android.libraries.permissions.test
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import io.element.android.libraries.permissions.api.PermissionsEvents import io.element.android.libraries.permissions.api.PermissionsEvent
import io.element.android.libraries.permissions.api.PermissionsPresenter import io.element.android.libraries.permissions.api.PermissionsPresenter
import io.element.android.libraries.permissions.api.PermissionsState import io.element.android.libraries.permissions.api.PermissionsState
import io.element.android.libraries.permissions.api.aPermissionsState import io.element.android.libraries.permissions.api.aPermissionsState
@ -18,11 +18,11 @@ import io.element.android.libraries.permissions.api.aPermissionsState
class FakePermissionsPresenter( class FakePermissionsPresenter(
private val initialState: PermissionsState = aPermissionsState(showDialog = false), private val initialState: PermissionsState = aPermissionsState(showDialog = false),
) : PermissionsPresenter { ) : PermissionsPresenter {
private fun handleEvent(event: PermissionsEvents) { private fun handleEvent(event: PermissionsEvent) {
when (event) { when (event) {
PermissionsEvents.RequestPermissions -> state.value = state.value.copy(showDialog = true, permissionAlreadyAsked = true) PermissionsEvent.RequestPermissions -> state.value = state.value.copy(showDialog = true, permissionAlreadyAsked = true)
PermissionsEvents.CloseDialog -> state.value = state.value.copy(showDialog = false) PermissionsEvent.CloseDialog -> state.value = state.value.copy(showDialog = false)
PermissionsEvents.OpenSystemSettingAndCloseDialog -> state.value = state.value.copy(showDialog = false) PermissionsEvent.OpenSystemSettingAndCloseDialog -> state.value = state.value.copy(showDialog = false)
} }
} }