diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXFragment.kt index 219f78358d..222dbe7a78 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXFragment.kt @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.mediasend import android.Manifest import android.content.Context import android.content.pm.ActivityInfo +import android.content.pm.PackageManager import android.graphics.Bitmap import android.os.Build import android.os.Bundle @@ -139,6 +140,7 @@ class CameraXFragment : ComposeFragment(), CameraFragment { selectedMediaCount = selectedMediaCount.intValue, onCheckPermissions = { checkPermissions(isVideoEnabled) }, hasCameraPermission = { hasCameraPermission() }, + onRequestMicPermission = { requestMicPermission() }, createVideoFileDescriptor = { createVideoFileDescriptor() }, getMaxVideoDurationInSeconds = { getMaxVideoDurationInSeconds() }, cameraDisplay = CameraDisplay.getDisplay(requireActivity()) @@ -242,6 +244,16 @@ class CameraXFragment : ComposeFragment(), CameraFragment { return Permissions.hasAll(requireContext(), Manifest.permission.CAMERA) } + private fun requestMicPermission() { + Permissions.with(this) + .request(Manifest.permission.RECORD_AUDIO) + .ifNecessary() + .withRationaleDialog(getString(R.string.CameraXFragment_allow_access_microphone), getString(R.string.CameraXFragment_to_capture_videos_with_sound), R.drawable.ic_mic_24) + .withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_needs_the_recording_permissions_to_capture_video), null, R.string.CameraXFragment_allow_access_microphone, R.string.CameraXFragment_to_capture_videos, parentFragmentManager) + .onAnyDenied { Toast.makeText(requireContext(), R.string.CameraXFragment_signal_needs_microphone_access_video, Toast.LENGTH_LONG).show() } + .execute() + } + private fun createVideoFileDescriptor(): ParcelFileDescriptor? { if (Build.VERSION.SDK_INT < 26) { throw IllegalStateException("Video capture requires API 26 or higher") @@ -287,6 +299,7 @@ private fun CameraXScreen( selectedMediaCount: Int, onCheckPermissions: () -> Unit, hasCameraPermission: () -> Boolean, + onRequestMicPermission: () -> Unit, createVideoFileDescriptor: () -> ParcelFileDescriptor?, getMaxVideoDurationInSeconds: () -> Int, cameraDisplay: CameraDisplay, @@ -400,6 +413,7 @@ private fun CameraXScreen( modifier = Modifier.padding(bottom = hudBottomPaddingInsideViewport), maxRecordingDurationMs = getMaxVideoDurationInSeconds() * 1000L, mediaSelectionCount = selectedMediaCount, + hasAudioPermission = { context.checkSelfPermission(Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED }, emitter = { event -> handleHudEvent( event = event, @@ -407,6 +421,7 @@ private fun CameraXScreen( cameraViewModel = cameraViewModel, controller = controller, isVideoEnabled = isVideoEnabled, + onRequestMicPermission = onRequestMicPermission, createVideoFileDescriptor = createVideoFileDescriptor ) }, @@ -470,6 +485,7 @@ private fun handleHudEvent( cameraViewModel: CameraScreenViewModel, controller: CameraFragment.Controller?, isVideoEnabled: Boolean, + onRequestMicPermission: () -> Unit, createVideoFileDescriptor: () -> ParcelFileDescriptor? ) { when (event) { @@ -528,6 +544,10 @@ private fun handleHudEvent( is StandardCameraHudEvents.SetZoomLevel -> { cameraViewModel.onEvent(CameraScreenEvents.LinearZoom(event.zoomLevel)) } + + is StandardCameraHudEvents.AudioPermissionRequired -> { + onRequestMicPermission() + } } } @@ -579,6 +599,7 @@ private fun CameraXScreenPreview_20_9() { selectedMediaCount = 0, onCheckPermissions = {}, hasCameraPermission = { true }, + onRequestMicPermission = { }, createVideoFileDescriptor = { null }, getMaxVideoDurationInSeconds = { 60 }, cameraDisplay = CameraDisplay.DISPLAY_20_9, @@ -604,6 +625,7 @@ private fun CameraXScreenPreview_19_9() { selectedMediaCount = 0, onCheckPermissions = {}, hasCameraPermission = { true }, + onRequestMicPermission = { }, createVideoFileDescriptor = { null }, getMaxVideoDurationInSeconds = { 60 }, cameraDisplay = CameraDisplay.DISPLAY_19_9, @@ -629,6 +651,7 @@ private fun CameraXScreenPreview_18_9() { selectedMediaCount = 0, onCheckPermissions = {}, hasCameraPermission = { true }, + onRequestMicPermission = { }, createVideoFileDescriptor = { null }, getMaxVideoDurationInSeconds = { 60 }, cameraDisplay = CameraDisplay.DISPLAY_18_9, @@ -654,6 +677,7 @@ private fun CameraXScreenPreview_16_9() { selectedMediaCount = 0, onCheckPermissions = {}, hasCameraPermission = { true }, + onRequestMicPermission = { }, createVideoFileDescriptor = { null }, getMaxVideoDurationInSeconds = { 60 }, cameraDisplay = CameraDisplay.DISPLAY_16_9, @@ -679,6 +703,7 @@ private fun CameraXScreenPreview_6_5() { selectedMediaCount = 0, onCheckPermissions = {}, hasCameraPermission = { true }, + onRequestMicPermission = { }, createVideoFileDescriptor = { null }, getMaxVideoDurationInSeconds = { 60 }, cameraDisplay = CameraDisplay.DISPLAY_6_5, diff --git a/demo/camera/src/main/java/org/signal/camera/demo/screens/main/MainScreen.kt b/demo/camera/src/main/java/org/signal/camera/demo/screens/main/MainScreen.kt index 437cd04546..aa003eefad 100644 --- a/demo/camera/src/main/java/org/signal/camera/demo/screens/main/MainScreen.kt +++ b/demo/camera/src/main/java/org/signal/camera/demo/screens/main/MainScreen.kt @@ -147,6 +147,9 @@ fun MainScreen( is StandardCameraHudEvents.MediaSelectionClick -> { // Doesn't need to be handled } + is StandardCameraHudEvents.AudioPermissionRequired -> { + // Doesn't need to be handled + } } } ) diff --git a/feature/camera/src/main/java/org/signal/camera/hud/StandardCameraHud.kt b/feature/camera/src/main/java/org/signal/camera/hud/StandardCameraHud.kt index 8aa2912556..4e5c1827a9 100644 --- a/feature/camera/src/main/java/org/signal/camera/hud/StandardCameraHud.kt +++ b/feature/camera/src/main/java/org/signal/camera/hud/StandardCameraHud.kt @@ -104,6 +104,7 @@ fun BoxScope.StandardCameraHud( modifier: Modifier = Modifier, maxRecordingDurationMs: Long = DEFAULT_MAX_RECORDING_DURATION_MS, mediaSelectionCount: Int = 0, + hasAudioPermission: () -> Boolean = { true }, stringResources: StringResources = StringResources(0, 0) ) { val context = LocalContext.current @@ -131,6 +132,7 @@ fun BoxScope.StandardCameraHud( modifier = modifier, maxRecordingDurationMs = maxRecordingDurationMs, mediaSelectionCount = mediaSelectionCount, + hasAudioPermission = hasAudioPermission, stringResources = stringResources ) } @@ -142,6 +144,7 @@ private fun BoxScope.StandardCameraHudContent( modifier: Modifier = Modifier, maxRecordingDurationMs: Long = DEFAULT_MAX_RECORDING_DURATION_MS, mediaSelectionCount: Int = 0, + hasAudioPermission: () -> Boolean = { true }, stringResources: StringResources = StringResources() ) { val configuration = LocalConfiguration.current @@ -177,6 +180,7 @@ private fun BoxScope.StandardCameraHudContent( }, mediaSelectionCount = mediaSelectionCount, emitter = emitter, + hasAudioPermission = hasAudioPermission, stringResources = stringResources, modifier = modifier.align(if (isLandscape) Alignment.CenterEnd else Alignment.BottomCenter) ) @@ -209,6 +213,7 @@ private fun CameraControls( recordingProgress: Float, mediaSelectionCount: Int, emitter: (StandardCameraHudEvents) -> Unit, + hasAudioPermission: () -> Boolean, stringResources: StringResources, modifier: Modifier = Modifier ) { @@ -229,7 +234,13 @@ private fun CameraControls( isRecording = isRecording, recordingProgress = recordingProgress, onTap = { emitter(StandardCameraHudEvents.PhotoCaptureTriggered) }, - onLongPressStart = { emitter(StandardCameraHudEvents.VideoCaptureStarted) }, + onLongPressStart = { + if (hasAudioPermission()) { + emitter(StandardCameraHudEvents.VideoCaptureStarted) + } else { + emitter(StandardCameraHudEvents.AudioPermissionRequired) + } + }, onLongPressEnd = { emitter(StandardCameraHudEvents.VideoCaptureStopped) }, onZoomChange = { emitter(StandardCameraHudEvents.SetZoomLevel(it)) } ) diff --git a/feature/camera/src/main/java/org/signal/camera/hud/StandardCameraHudEvents.kt b/feature/camera/src/main/java/org/signal/camera/hud/StandardCameraHudEvents.kt index 62f8dd0492..35a81887b3 100644 --- a/feature/camera/src/main/java/org/signal/camera/hud/StandardCameraHudEvents.kt +++ b/feature/camera/src/main/java/org/signal/camera/hud/StandardCameraHudEvents.kt @@ -37,4 +37,9 @@ sealed interface StandardCameraHudEvents { * Emitted when a capture error should be cleared (after displaying to user). */ data object ClearCaptureError : StandardCameraHudEvents + + /** + * Emitted when the user attempts to start video recording but audio permission has not been granted. + */ + data object AudioPermissionRequired : StandardCameraHudEvents }