diff --git a/features/messages/impl/build.gradle.kts b/features/messages/impl/build.gradle.kts index cd0b6dda2d..058dafaf1f 100644 --- a/features/messages/impl/build.gradle.kts +++ b/features/messages/impl/build.gradle.kts @@ -48,6 +48,7 @@ dependencies { implementation(projects.libraries.mediapickers.api) implementation(projects.libraries.featureflag.api) implementation(projects.libraries.mediaupload.api) + implementation(projects.libraries.permissions.api) implementation(projects.libraries.preferences.api) implementation(projects.features.networkmonitor.api) implementation(projects.services.analytics.api) @@ -77,6 +78,7 @@ dependencies { testImplementation(projects.libraries.featureflag.test) testImplementation(projects.libraries.mediaupload.test) testImplementation(projects.libraries.mediapickers.test) + testImplementation(projects.libraries.permissions.test) testImplementation(projects.libraries.preferences.test) testImplementation(projects.libraries.textcomposer.test) testImplementation(libs.test.mockk) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt index a7728c2a00..dbf6afc46e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt @@ -16,6 +16,7 @@ package io.element.android.features.messages.impl.messagecomposer +import android.Manifest import android.annotation.SuppressLint import android.net.Uri import androidx.compose.runtime.Composable @@ -44,6 +45,8 @@ import io.element.android.libraries.matrix.api.core.ProgressCallback import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.mediapickers.api.PickerProvider import io.element.android.libraries.mediaupload.api.MediaSender +import io.element.android.libraries.permissions.api.PermissionsEvents +import io.element.android.libraries.permissions.api.PermissionsPresenter import io.element.android.libraries.textcomposer.Message import io.element.android.libraries.textcomposer.MessageComposerMode import io.element.android.services.analytics.api.AnalyticsService @@ -70,13 +73,18 @@ class MessageComposerPresenter @Inject constructor( private val analyticsService: AnalyticsService, private val messageComposerContext: MessageComposerContextImpl, private val richTextEditorStateFactory: RichTextEditorStateFactory, + permissionsPresenterFactory: PermissionsPresenter.Factory ) : Presenter { + private val cameraPermissionPresenter = permissionsPresenterFactory.create(Manifest.permission.CAMERA) + private var pendingEvent: MessageComposerEvents? = null + @SuppressLint("UnsafeOptInUsageError") @Composable override fun present(): MessageComposerState { val localCoroutineScope = rememberCoroutineScope() + val cameraPermissionState = cameraPermissionPresenter.present() val attachmentsState = remember { mutableStateOf(AttachmentsState.None) } @@ -132,6 +140,17 @@ class MessageComposerPresenter @Inject constructor( } } + LaunchedEffect(cameraPermissionState.permissionGranted) { + if (cameraPermissionState.permissionGranted) { + when (pendingEvent) { + is MessageComposerEvents.PickAttachmentSource.PhotoFromCamera -> cameraPhotoPicker.launch() + is MessageComposerEvents.PickAttachmentSource.VideoFromCamera -> cameraVideoPicker.launch() + else -> Unit + } + pendingEvent = null + } + } + fun handleEvents(event: MessageComposerEvents) { when (event) { MessageComposerEvents.ToggleFullScreenState -> isFullScreen.value = !isFullScreen.value @@ -163,11 +182,21 @@ class MessageComposerPresenter @Inject constructor( } MessageComposerEvents.PickAttachmentSource.PhotoFromCamera -> localCoroutineScope.launch { showAttachmentSourcePicker = false - cameraPhotoPicker.launch() + if (cameraPermissionState.permissionGranted) { + cameraPhotoPicker.launch() + } else { + pendingEvent = event + cameraPermissionState.eventSink(PermissionsEvents.AskPermissionToUser) + } } MessageComposerEvents.PickAttachmentSource.VideoFromCamera -> localCoroutineScope.launch { showAttachmentSourcePicker = false - cameraVideoPicker.launch() + if (cameraPermissionState.permissionGranted) { + cameraVideoPicker.launch() + } else { + pendingEvent = event + cameraPermissionState.eventSink(PermissionsEvents.AskPermissionToUser) + } } MessageComposerEvents.PickAttachmentSource.Location -> { showAttachmentSourcePicker = false @@ -301,16 +330,16 @@ class MessageComposerPresenter @Inject constructor( } mediaSender.sendMedia(uri, mimeType, compressIfPossible = false, progressCallback).getOrThrow() } - .onSuccess { - attachmentState.value = AttachmentsState.None - } - .onFailure { cause -> - attachmentState.value = AttachmentsState.None - if (cause is CancellationException) { - throw cause - } else { - val snackbarMessage = SnackbarMessage(sendAttachmentError(cause)) - snackbarDispatcher.post(snackbarMessage) + .onSuccess { + attachmentState.value = AttachmentsState.None + } + .onFailure { cause -> + attachmentState.value = AttachmentsState.None + if (cause is CancellationException) { + throw cause + } else { + val snackbarMessage = SnackbarMessage(sendAttachmentError(cause)) + snackbarDispatcher.post(snackbarMessage) + } } - } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt index db2abcc1dd..f835560a75 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt @@ -70,6 +70,8 @@ import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.libraries.mediapickers.test.FakePickerProvider import io.element.android.libraries.mediaupload.api.MediaSender 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.libraries.textcomposer.MessageComposerMode import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.WarmUpRule @@ -601,6 +603,7 @@ class MessagesPresenterTest { navigator: FakeMessagesNavigator = FakeMessagesNavigator(), clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(), analyticsService: FakeAnalyticsService = FakeAnalyticsService(), + permissionsPresenter: PermissionsPresenter = FakePermissionsPresenter(), ): MessagesPresenter { val messageComposerPresenter = MessageComposerPresenter( appCoroutineScope = this, @@ -613,8 +616,12 @@ class MessagesPresenterTest { analyticsService = analyticsService, messageComposerContext = MessageComposerContextImpl(), richTextEditorStateFactory = TestRichTextEditorStateFactory(), - - ) + permissionsPresenterFactory = object : PermissionsPresenter.Factory { + override fun create(permission: String): PermissionsPresenter { + return permissionsPresenter + } + } + ) val timelinePresenter = TimelinePresenter( timelineItemsFactory = aTimelineItemsFactory(), room = matrixRoom, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt index 3f66269fe1..40c65c7350 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt @@ -54,6 +54,8 @@ import io.element.android.libraries.mediaupload.api.MediaPreProcessor import io.element.android.libraries.mediaupload.api.MediaSender 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.libraries.textcomposer.Message import io.element.android.libraries.textcomposer.MessageComposerMode import io.element.android.services.analytics.test.FakeAnalyticsService @@ -623,6 +625,11 @@ class MessageComposerPresenterTest { analyticsService, MessageComposerContextImpl(), TestRichTextEditorStateFactory(), + permissionsPresenterFactory = object : PermissionsPresenter.Factory { + override fun create(permission: String): PermissionsPresenter { + return FakePermissionsPresenter().apply { setPermissionGranted() } + } + } ) }