From 31eca73e8d78348c0c08bf3418e23cdb98f7625b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 21 Sep 2023 10:54:02 +0200 Subject: [PATCH] Request Camera permission before launching the external Camera app (#1395) --- features/createroom/impl/build.gradle.kts | 2 ++ .../configureroom/ConfigureRoomPresenter.kt | 13 +++++++++- .../impl/configureroom/ConfigureRoomState.kt | 2 ++ .../ConfigureRoomStateProvider.kt | 2 ++ .../impl/configureroom/ConfigureRoomView.kt | 3 +++ .../ConfigureRoomPresenterTests.kt | 15 +++++++++-- .../NotificationsOptInStateProvider.kt | 2 +- features/preferences/impl/build.gradle.kts | 2 ++ .../editprofile/EditUserProfilePresenter.kt | 13 +++++++++- .../user/editprofile/EditUserProfileState.kt | 2 ++ .../EditUserProfileStateProvider.kt | 2 ++ .../user/editprofile/EditUserProfileView.kt | 2 ++ .../EditUserProfilePresenterTest.kt | 20 ++++++++++++-- features/roomdetails/impl/build.gradle.kts | 2 ++ .../impl/edit/RoomDetailsEditPresenter.kt | 13 +++++++++- .../impl/edit/RoomDetailsEditState.kt | 4 ++- .../impl/edit/RoomDetailsEditStateProvider.kt | 2 ++ .../impl/edit/RoomDetailsEditView.kt | 3 +++ .../edit/RoomDetailsEditPresenterTest.kt | 26 +++++++++++++++---- .../api/PermissionsViewStateProvider.kt | 12 +++++---- .../test/FakePermissionsPresenter.kt | 2 +- 21 files changed, 124 insertions(+), 20 deletions(-) diff --git a/features/createroom/impl/build.gradle.kts b/features/createroom/impl/build.gradle.kts index 3ea54ca4ea..ae9818b727 100644 --- a/features/createroom/impl/build.gradle.kts +++ b/features/createroom/impl/build.gradle.kts @@ -48,6 +48,7 @@ dependencies { implementation(projects.libraries.deeplink) implementation(projects.libraries.mediapickers.api) implementation(projects.libraries.mediaupload.api) + implementation(projects.libraries.permissions.api) implementation(projects.libraries.usersearch.impl) implementation(projects.services.analytics.api) implementation(libs.coil.compose) @@ -64,6 +65,7 @@ dependencies { testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.mediapickers.test) testImplementation(projects.libraries.mediaupload.test) + testImplementation(projects.libraries.permissions.test) testImplementation(projects.libraries.usersearch.test) testImplementation(projects.tests.testutils) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt index 21d8f8d13f..8b62e7ef74 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt @@ -40,6 +40,8 @@ import io.element.android.libraries.matrix.api.createroom.RoomVisibility import io.element.android.libraries.matrix.ui.media.AvatarAction import io.element.android.libraries.mediapickers.api.PickerProvider import io.element.android.libraries.mediaupload.api.MediaPreProcessor +import io.element.android.libraries.permissions.api.PermissionsEvents +import io.element.android.libraries.permissions.api.PermissionsPresenter import io.element.android.services.analytics.api.AnalyticsService import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope @@ -52,10 +54,14 @@ class ConfigureRoomPresenter @Inject constructor( private val mediaPickerProvider: PickerProvider, private val mediaPreProcessor: MediaPreProcessor, private val analyticsService: AnalyticsService, + permissionsPresenterFactory: PermissionsPresenter.Factory, ) : Presenter { + private val cameraPermissionPresenter: PermissionsPresenter = permissionsPresenterFactory.create(android.Manifest.permission.CAMERA) + @Composable override fun present(): ConfigureRoomState { + val cameraPermissionState = cameraPermissionPresenter.present() val createRoomConfig = dataStore.getCreateRoomConfig().collectAsState(CreateRoomConfig()) val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker( @@ -93,7 +99,11 @@ class ConfigureRoomPresenter @Inject constructor( is ConfigureRoomEvents.HandleAvatarAction -> { when (event.action) { AvatarAction.ChoosePhoto -> galleryImagePicker.launch() - AvatarAction.TakePhoto -> cameraPhotoPicker.launch() + AvatarAction.TakePhoto -> if (cameraPermissionState.permissionGranted) { + cameraPhotoPicker.launch() + } else { + cameraPermissionState.eventSink(PermissionsEvents.OpenSystemDialog) + } AvatarAction.Remove -> dataStore.setAvatarUri(uri = null) } } @@ -106,6 +116,7 @@ class ConfigureRoomPresenter @Inject constructor( config = createRoomConfig.value, avatarActions = avatarActions, createRoomAction = createRoomAction.value, + cameraPermissionState = cameraPermissionState, eventSink = ::handleEvents, ) } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt index 2e34f3bda2..7e16cedaa7 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt @@ -20,12 +20,14 @@ import io.element.android.libraries.matrix.ui.media.AvatarAction import io.element.android.features.createroom.impl.CreateRoomConfig import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.permissions.api.PermissionsState import kotlinx.collections.immutable.ImmutableList data class ConfigureRoomState( val config: CreateRoomConfig, val avatarActions: ImmutableList, val createRoomAction: Async, + val cameraPermissionState: PermissionsState, val eventSink: (ConfigureRoomEvents) -> Unit ) { val isCreateButtonEnabled: Boolean = config.roomName.isNullOrEmpty().not() diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt index 0e31e9e1c0..eafbe6915b 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.createroom.impl.CreateRoomConfig import io.element.android.features.createroom.impl.userlist.aListOfSelectedUsers import io.element.android.libraries.architecture.Async +import io.element.android.libraries.permissions.api.aPermissionsState import kotlinx.collections.immutable.persistentListOf open class ConfigureRoomStateProvider : PreviewParameterProvider { @@ -41,5 +42,6 @@ fun aConfigureRoomState() = ConfigureRoomState( config = CreateRoomConfig(), avatarActions = persistentListOf(), createRoomAction = Async.Uninitialized, + cameraPermissionState = aPermissionsState(showDialog = false), eventSink = { }, ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt index 872526fe09..14c9d90843 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt @@ -65,6 +65,7 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.ui.components.AvatarActionBottomSheet import io.element.android.libraries.matrix.ui.components.SelectedUsersList import io.element.android.libraries.matrix.ui.components.UnsavedAvatar +import io.element.android.libraries.permissions.api.PermissionsView import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.coroutines.launch @@ -172,6 +173,8 @@ fun ConfigureRoomView( else -> Unit } + + PermissionsView(state = state.cameraPermissionState) } @OptIn(ExperimentalMaterial3Api::class) diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt index a330384f60..767d13a523 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt @@ -37,6 +37,8 @@ import io.element.android.libraries.matrix.ui.media.AvatarAction import io.element.android.libraries.mediapickers.test.FakePickerProvider import io.element.android.libraries.mediaupload.api.MediaUploadInfo import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor +import io.element.android.libraries.permissions.api.PermissionsPresenter +import io.element.android.libraries.permissions.test.FakePermissionsPresenter import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.WarmUpRule import io.mockk.every @@ -70,6 +72,7 @@ class ConfigureRoomPresenterTests { private lateinit var fakePickerProvider: FakePickerProvider private lateinit var fakeMediaPreProcessor: FakeMediaPreProcessor private lateinit var fakeAnalyticsService: FakeAnalyticsService + private lateinit var fakePermissionsPresenter: FakePermissionsPresenter @Before fun setup() { @@ -79,12 +82,18 @@ class ConfigureRoomPresenterTests { fakePickerProvider = FakePickerProvider() fakeMediaPreProcessor = FakeMediaPreProcessor() fakeAnalyticsService = FakeAnalyticsService() + fakePermissionsPresenter = FakePermissionsPresenter() presenter = ConfigureRoomPresenter( dataStore = createRoomDataStore, matrixClient = fakeMatrixClient, mediaPickerProvider = fakePickerProvider, mediaPreProcessor = fakeMediaPreProcessor, analyticsService = fakeAnalyticsService, + permissionsPresenterFactory = object : PermissionsPresenter.Factory { + override fun create(permission: String): PermissionsPresenter { + return fakePermissionsPresenter + } + }, ) mockkStatic(File::readBytes) @@ -170,8 +179,6 @@ class ConfigureRoomPresenterTests { // Room avatar // Pick avatar fakePickerProvider.givenResult(null) - newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) - newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.TakePhoto)) // From gallery val uriFromGallery = Uri.parse(AN_URI_FROM_GALLERY) fakePickerProvider.givenResult(uriFromGallery) @@ -182,6 +189,10 @@ class ConfigureRoomPresenterTests { // From camera val uriFromCamera = Uri.parse(AN_URI_FROM_CAMERA) fakePickerProvider.givenResult(uriFromCamera) + assertThat(newState.cameraPermissionState.permissionGranted).isFalse() + fakePermissionsPresenter.setPermissionGranted() + newState = awaitItem() + assertThat(newState.cameraPermissionState.permissionGranted).isTrue() newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.TakePhoto)) newState = awaitItem() expectedConfig = expectedConfig.copy(avatarUri = uriFromCamera) diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInStateProvider.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInStateProvider.kt index 230e125c1b..49596856ba 100644 --- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInStateProvider.kt +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInStateProvider.kt @@ -28,6 +28,6 @@ open class NotificationsOptInStateProvider : PreviewParameterProvider { + private val cameraPermissionPresenter: PermissionsPresenter = permissionsPresenterFactory.create(android.Manifest.permission.CAMERA) + @AssistedFactory interface Factory { fun create(matrixUser: MatrixUser): EditUserProfilePresenter @@ -58,6 +63,7 @@ class EditUserProfilePresenter @AssistedInject constructor( @Composable override fun present(): EditUserProfileState { + val cameraPermissionState = cameraPermissionPresenter.present() var userAvatarUri by rememberSaveable { mutableStateOf(matrixUser.avatarUrl?.let { Uri.parse(it) }) } var userDisplayName by rememberSaveable { mutableStateOf(matrixUser.displayName) } val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker( @@ -85,7 +91,11 @@ class EditUserProfilePresenter @AssistedInject constructor( is EditUserProfileEvents.HandleAvatarAction -> { when (event.action) { AvatarAction.ChoosePhoto -> galleryImagePicker.launch() - AvatarAction.TakePhoto -> cameraPhotoPicker.launch() + AvatarAction.TakePhoto -> if (cameraPermissionState.permissionGranted) { + cameraPhotoPicker.launch() + } else { + cameraPermissionState.eventSink.invoke(PermissionsEvents.OpenSystemDialog) + } AvatarAction.Remove -> userAvatarUri = null } } @@ -108,6 +118,7 @@ class EditUserProfilePresenter @AssistedInject constructor( avatarActions = avatarActions, saveButtonEnabled = canSave && saveAction.value !is Async.Loading, saveAction = saveAction.value, + cameraPermissionState = cameraPermissionState, eventSink = { handleEvents(it) }, ) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileState.kt index 87668e6f45..9561c8609b 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileState.kt @@ -20,6 +20,7 @@ import android.net.Uri import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.ui.media.AvatarAction +import io.element.android.libraries.permissions.api.PermissionsState import kotlinx.collections.immutable.ImmutableList data class EditUserProfileState( @@ -29,5 +30,6 @@ data class EditUserProfileState( val avatarActions: ImmutableList, val saveButtonEnabled: Boolean, val saveAction: Async, + val cameraPermissionState: PermissionsState, val eventSink: (EditUserProfileEvents) -> Unit ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileStateProvider.kt index 5e4ccb95cb..c4a8431f4f 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileStateProvider.kt @@ -19,6 +19,7 @@ package io.element.android.features.preferences.impl.user.editprofile import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.permissions.api.aPermissionsState import kotlinx.collections.immutable.persistentListOf open class EditUserProfileStateProvider : PreviewParameterProvider { @@ -36,5 +37,6 @@ fun aEditUserProfileState() = EditUserProfileState( avatarActions = persistentListOf(), saveAction = Async.Uninitialized, saveButtonEnabled = true, + cameraPermissionState = aPermissionsState(showDialog = false), eventSink = {} ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileView.kt index 5b921c047c..ddcdecc6a5 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileView.kt @@ -58,6 +58,7 @@ import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.matrix.ui.components.AvatarActionBottomSheet import io.element.android.libraries.matrix.ui.components.EditableAvatarView +import io.element.android.libraries.permissions.api.PermissionsView import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.coroutines.launch @@ -168,6 +169,7 @@ fun EditUserProfileView( else -> Unit } } + PermissionsView(state = state.cameraPermissionState) } private fun Modifier.clearFocusOnTap(focusManager: FocusManager): Modifier = diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenterTest.kt index beece60c9a..779f9c1950 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfilePresenterTest.kt @@ -32,6 +32,8 @@ import io.element.android.libraries.matrix.ui.media.AvatarAction import io.element.android.libraries.mediapickers.test.FakePickerProvider import io.element.android.libraries.mediaupload.api.MediaUploadInfo import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor +import io.element.android.libraries.permissions.api.PermissionsPresenter +import io.element.android.libraries.permissions.test.FakePermissionsPresenter import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.consumeItemsUntilPredicate import io.mockk.every @@ -78,12 +80,18 @@ class EditUserProfilePresenterTest { private fun createEditUserProfilePresenter( matrixClient: MatrixClient = FakeMatrixClient(), matrixUser: MatrixUser = aMatrixUser(), + permissionsPresenter: PermissionsPresenter = FakePermissionsPresenter(), ): EditUserProfilePresenter { return EditUserProfilePresenter( matrixClient = matrixClient, matrixUser = matrixUser, mediaPickerProvider = fakePickerProvider, mediaPreProcessor = fakeMediaPreProcessor, + permissionsPresenterFactory = object : PermissionsPresenter.Factory { + override fun create(permission: String): PermissionsPresenter { + return permissionsPresenter + } + }, ) } @@ -157,13 +165,21 @@ class EditUserProfilePresenterTest { fun `present - obtains avatar uris from camera`() = runTest { val user = aMatrixUser(id = A_USER_ID.value, displayName = "Name", avatarUrl = AN_AVATAR_URL) fakePickerProvider.givenResult(anotherAvatarUri) - val presenter = createEditUserProfilePresenter(matrixUser = user) + val fakePermissionsPresenter = FakePermissionsPresenter() + val presenter = createEditUserProfilePresenter( + matrixUser = user, + permissionsPresenter = fakePermissionsPresenter, + ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { val initialState = awaitItem() assertThat(initialState.userAvatarUrl).isEqualTo(userAvatarUri) - initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.TakePhoto)) + assertThat(initialState.cameraPermissionState.permissionGranted).isFalse() + fakePermissionsPresenter.setPermissionGranted() + val stateWithPermission = awaitItem() + assertThat(stateWithPermission.cameraPermissionState.permissionGranted).isTrue() + stateWithPermission.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.TakePhoto)) awaitItem().apply { assertThat(userAvatarUrl).isEqualTo(anotherAvatarUri) } diff --git a/features/roomdetails/impl/build.gradle.kts b/features/roomdetails/impl/build.gradle.kts index 2137b1401d..44c711a371 100644 --- a/features/roomdetails/impl/build.gradle.kts +++ b/features/roomdetails/impl/build.gradle.kts @@ -43,6 +43,7 @@ dependencies { implementation(projects.libraries.mediapickers.api) implementation(projects.libraries.mediaupload.api) implementation(projects.libraries.featureflag.api) + implementation(projects.libraries.permissions.api) api(projects.features.roomdetails.api) api(projects.libraries.usersearch.api) api(projects.services.apperror.api) @@ -59,6 +60,7 @@ dependencies { testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.mediaupload.test) testImplementation(projects.libraries.mediapickers.test) + testImplementation(projects.libraries.permissions.test) testImplementation(projects.libraries.usersearch.test) testImplementation(projects.libraries.featureflag.test) testImplementation(projects.tests.testutils) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditPresenter.kt index 0024c64268..e73f15a1ba 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditPresenter.kt @@ -39,6 +39,8 @@ import io.element.android.libraries.matrix.api.room.powerlevels.canSendState import io.element.android.libraries.matrix.ui.media.AvatarAction import io.element.android.libraries.mediapickers.api.PickerProvider import io.element.android.libraries.mediaupload.api.MediaPreProcessor +import io.element.android.libraries.permissions.api.PermissionsEvents +import io.element.android.libraries.permissions.api.PermissionsPresenter import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -49,10 +51,14 @@ class RoomDetailsEditPresenter @Inject constructor( private val room: MatrixRoom, private val mediaPickerProvider: PickerProvider, private val mediaPreProcessor: MediaPreProcessor, + permissionsPresenterFactory: PermissionsPresenter.Factory, ) : Presenter { + private val cameraPermissionPresenter = permissionsPresenterFactory.create(android.Manifest.permission.CAMERA) + @Composable override fun present(): RoomDetailsEditState { + val cameraPermissionState = cameraPermissionPresenter.present() val roomSyncUpdateFlow = room.syncUpdateFlow.collectAsState() // Since there is no way to obtain the new avatar uri after uploading a new avatar, @@ -110,7 +116,11 @@ class RoomDetailsEditPresenter @Inject constructor( is RoomDetailsEditEvents.HandleAvatarAction -> { when (event.action) { AvatarAction.ChoosePhoto -> galleryImagePicker.launch() - AvatarAction.TakePhoto -> cameraPhotoPicker.launch() + AvatarAction.TakePhoto -> if (cameraPermissionState.permissionGranted) { + cameraPhotoPicker.launch() + } else { + cameraPermissionState.eventSink(PermissionsEvents.OpenSystemDialog) + } AvatarAction.Remove -> roomAvatarUri = null } } @@ -132,6 +142,7 @@ class RoomDetailsEditPresenter @Inject constructor( avatarActions = avatarActions, saveButtonEnabled = saveButtonEnabled, saveAction = saveAction.value, + cameraPermissionState = cameraPermissionState, eventSink = ::handleEvents, ) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditState.kt index ceb87b6f27..9258d882fc 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditState.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditState.kt @@ -17,8 +17,9 @@ package io.element.android.features.roomdetails.impl.edit import android.net.Uri -import io.element.android.libraries.matrix.ui.media.AvatarAction import io.element.android.libraries.architecture.Async +import io.element.android.libraries.matrix.ui.media.AvatarAction +import io.element.android.libraries.permissions.api.PermissionsState import kotlinx.collections.immutable.ImmutableList data class RoomDetailsEditState( @@ -32,5 +33,6 @@ data class RoomDetailsEditState( val avatarActions: ImmutableList, val saveButtonEnabled: Boolean, val saveAction: Async, + val cameraPermissionState: PermissionsState, val eventSink: (RoomDetailsEditEvents) -> Unit ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditStateProvider.kt index 96fd47c381..730acaa1c5 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditStateProvider.kt @@ -19,6 +19,7 @@ package io.element.android.features.roomdetails.impl.edit import android.net.Uri import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.architecture.Async +import io.element.android.libraries.permissions.api.aPermissionsState import kotlinx.collections.immutable.persistentListOf open class RoomDetailsEditStateProvider : PreviewParameterProvider { @@ -45,5 +46,6 @@ fun aRoomDetailsEditState() = RoomDetailsEditState( avatarActions = persistentListOf(), saveButtonEnabled = true, saveAction = Async.Uninitialized, + cameraPermissionState = aPermissionsState(showDialog = false), eventSink = {} ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditView.kt index 8775a079b1..89907fb024 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditView.kt @@ -62,6 +62,7 @@ import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.matrix.ui.components.AvatarActionBottomSheet import io.element.android.libraries.matrix.ui.components.EditableAvatarView +import io.element.android.libraries.permissions.api.PermissionsView import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.coroutines.launch @@ -193,6 +194,8 @@ fun RoomDetailsEditView( else -> Unit } + + PermissionsView(state = state.cameraPermissionState) } @Composable diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/edit/RoomDetailsEditPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/edit/RoomDetailsEditPresenterTest.kt index aeaefeab6e..e1645d9fac 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/edit/RoomDetailsEditPresenterTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/edit/RoomDetailsEditPresenterTest.kt @@ -32,6 +32,8 @@ import io.element.android.libraries.matrix.ui.media.AvatarAction import io.element.android.libraries.mediapickers.test.FakePickerProvider import io.element.android.libraries.mediaupload.api.MediaUploadInfo import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor +import io.element.android.libraries.permissions.api.PermissionsPresenter +import io.element.android.libraries.permissions.test.FakePermissionsPresenter import io.element.android.tests.testutils.WarmUpRule import io.mockk.every import io.mockk.mockk @@ -74,11 +76,19 @@ class RoomDetailsEditPresenterTest { unmockkAll() } - private fun aRoomDetailsEditPresenter(room: MatrixRoom): RoomDetailsEditPresenter { + private fun aRoomDetailsEditPresenter( + room: MatrixRoom, + permissionsPresenter: PermissionsPresenter = FakePermissionsPresenter(), + ): RoomDetailsEditPresenter { return RoomDetailsEditPresenter( room = room, mediaPickerProvider = fakePickerProvider, mediaPreProcessor = fakeMediaPreProcessor, + permissionsPresenterFactory = object : PermissionsPresenter.Factory { + override fun create(permission: String): PermissionsPresenter { + return permissionsPresenter + } + }, ) } @@ -252,16 +262,22 @@ class RoomDetailsEditPresenterTest { val room = aMatrixRoom(topic = "My topic", name = "Name", avatarUrl = AN_AVATAR_URL) fakePickerProvider.givenResult(anotherAvatarUri) - - val presenter = aRoomDetailsEditPresenter(room) + val fakePermissionsPresenter = FakePermissionsPresenter() + val presenter = aRoomDetailsEditPresenter( + room = room, + permissionsPresenter = fakePermissionsPresenter, + ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { val initialState = awaitItem() assertThat(initialState.roomAvatarUrl).isEqualTo(roomAvatarUri) - - initialState.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.TakePhoto)) + assertThat(initialState.cameraPermissionState.permissionGranted).isFalse() + fakePermissionsPresenter.setPermissionGranted() + val stateWithPermission = awaitItem() + assertThat(stateWithPermission.cameraPermissionState.permissionGranted).isTrue() + stateWithPermission.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.TakePhoto)) awaitItem().apply { assertThat(roomAvatarUrl).isEqualTo(anotherAvatarUri) } diff --git a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsViewStateProvider.kt b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsViewStateProvider.kt index e93b74d934..faa35f3c7f 100644 --- a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsViewStateProvider.kt +++ b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsViewStateProvider.kt @@ -22,17 +22,19 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider open class PermissionsViewStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( - aPermissionsState(), - aPermissionsState().copy(shouldShowRationale = true), - aPermissionsState().copy(permissionAlreadyDenied = true), + aPermissionsState(showDialog = true), + aPermissionsState(showDialog = true).copy(shouldShowRationale = true), + aPermissionsState(showDialog = true).copy(permissionAlreadyDenied = true), ) } -fun aPermissionsState() = PermissionsState( +fun aPermissionsState( + showDialog: Boolean, +) = PermissionsState( permission = Manifest.permission.INTERNET, permissionGranted = false, shouldShowRationale = false, - showDialog = true, + showDialog = showDialog, permissionAlreadyAsked = false, permissionAlreadyDenied = false, eventSink = {} diff --git a/libraries/permissions/test/src/main/kotlin/io/element/android/libraries/permissions/test/FakePermissionsPresenter.kt b/libraries/permissions/test/src/main/kotlin/io/element/android/libraries/permissions/test/FakePermissionsPresenter.kt index f26f268860..2332fb0b18 100644 --- a/libraries/permissions/test/src/main/kotlin/io/element/android/libraries/permissions/test/FakePermissionsPresenter.kt +++ b/libraries/permissions/test/src/main/kotlin/io/element/android/libraries/permissions/test/FakePermissionsPresenter.kt @@ -24,7 +24,7 @@ import io.element.android.libraries.permissions.api.PermissionsState import io.element.android.libraries.permissions.api.aPermissionsState class FakePermissionsPresenter( - private val initialState: PermissionsState = aPermissionsState().copy(showDialog = false), + private val initialState: PermissionsState = aPermissionsState(showDialog = false), ) : PermissionsPresenter { private fun eventSink(events: PermissionsEvents) {