mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-14 23:18:43 +00:00
Prompt for microphone permission when recording video in new camera.
Previously, the new camera would silently record video without audio when microphone permission was missing. Now it shows the same rationale dialog and permanent denial flow as the old camera.
This commit is contained in:
@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.mediasend
|
|||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.ActivityInfo
|
import android.content.pm.ActivityInfo
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@@ -139,6 +140,7 @@ class CameraXFragment : ComposeFragment(), CameraFragment {
|
|||||||
selectedMediaCount = selectedMediaCount.intValue,
|
selectedMediaCount = selectedMediaCount.intValue,
|
||||||
onCheckPermissions = { checkPermissions(isVideoEnabled) },
|
onCheckPermissions = { checkPermissions(isVideoEnabled) },
|
||||||
hasCameraPermission = { hasCameraPermission() },
|
hasCameraPermission = { hasCameraPermission() },
|
||||||
|
onRequestMicPermission = { requestMicPermission() },
|
||||||
createVideoFileDescriptor = { createVideoFileDescriptor() },
|
createVideoFileDescriptor = { createVideoFileDescriptor() },
|
||||||
getMaxVideoDurationInSeconds = { getMaxVideoDurationInSeconds() },
|
getMaxVideoDurationInSeconds = { getMaxVideoDurationInSeconds() },
|
||||||
cameraDisplay = CameraDisplay.getDisplay(requireActivity())
|
cameraDisplay = CameraDisplay.getDisplay(requireActivity())
|
||||||
@@ -242,6 +244,16 @@ class CameraXFragment : ComposeFragment(), CameraFragment {
|
|||||||
return Permissions.hasAll(requireContext(), Manifest.permission.CAMERA)
|
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? {
|
private fun createVideoFileDescriptor(): ParcelFileDescriptor? {
|
||||||
if (Build.VERSION.SDK_INT < 26) {
|
if (Build.VERSION.SDK_INT < 26) {
|
||||||
throw IllegalStateException("Video capture requires API 26 or higher")
|
throw IllegalStateException("Video capture requires API 26 or higher")
|
||||||
@@ -287,6 +299,7 @@ private fun CameraXScreen(
|
|||||||
selectedMediaCount: Int,
|
selectedMediaCount: Int,
|
||||||
onCheckPermissions: () -> Unit,
|
onCheckPermissions: () -> Unit,
|
||||||
hasCameraPermission: () -> Boolean,
|
hasCameraPermission: () -> Boolean,
|
||||||
|
onRequestMicPermission: () -> Unit,
|
||||||
createVideoFileDescriptor: () -> ParcelFileDescriptor?,
|
createVideoFileDescriptor: () -> ParcelFileDescriptor?,
|
||||||
getMaxVideoDurationInSeconds: () -> Int,
|
getMaxVideoDurationInSeconds: () -> Int,
|
||||||
cameraDisplay: CameraDisplay,
|
cameraDisplay: CameraDisplay,
|
||||||
@@ -400,6 +413,7 @@ private fun CameraXScreen(
|
|||||||
modifier = Modifier.padding(bottom = hudBottomPaddingInsideViewport),
|
modifier = Modifier.padding(bottom = hudBottomPaddingInsideViewport),
|
||||||
maxRecordingDurationMs = getMaxVideoDurationInSeconds() * 1000L,
|
maxRecordingDurationMs = getMaxVideoDurationInSeconds() * 1000L,
|
||||||
mediaSelectionCount = selectedMediaCount,
|
mediaSelectionCount = selectedMediaCount,
|
||||||
|
hasAudioPermission = { context.checkSelfPermission(Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED },
|
||||||
emitter = { event ->
|
emitter = { event ->
|
||||||
handleHudEvent(
|
handleHudEvent(
|
||||||
event = event,
|
event = event,
|
||||||
@@ -407,6 +421,7 @@ private fun CameraXScreen(
|
|||||||
cameraViewModel = cameraViewModel,
|
cameraViewModel = cameraViewModel,
|
||||||
controller = controller,
|
controller = controller,
|
||||||
isVideoEnabled = isVideoEnabled,
|
isVideoEnabled = isVideoEnabled,
|
||||||
|
onRequestMicPermission = onRequestMicPermission,
|
||||||
createVideoFileDescriptor = createVideoFileDescriptor
|
createVideoFileDescriptor = createVideoFileDescriptor
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -470,6 +485,7 @@ private fun handleHudEvent(
|
|||||||
cameraViewModel: CameraScreenViewModel,
|
cameraViewModel: CameraScreenViewModel,
|
||||||
controller: CameraFragment.Controller?,
|
controller: CameraFragment.Controller?,
|
||||||
isVideoEnabled: Boolean,
|
isVideoEnabled: Boolean,
|
||||||
|
onRequestMicPermission: () -> Unit,
|
||||||
createVideoFileDescriptor: () -> ParcelFileDescriptor?
|
createVideoFileDescriptor: () -> ParcelFileDescriptor?
|
||||||
) {
|
) {
|
||||||
when (event) {
|
when (event) {
|
||||||
@@ -528,6 +544,10 @@ private fun handleHudEvent(
|
|||||||
is StandardCameraHudEvents.SetZoomLevel -> {
|
is StandardCameraHudEvents.SetZoomLevel -> {
|
||||||
cameraViewModel.onEvent(CameraScreenEvents.LinearZoom(event.zoomLevel))
|
cameraViewModel.onEvent(CameraScreenEvents.LinearZoom(event.zoomLevel))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is StandardCameraHudEvents.AudioPermissionRequired -> {
|
||||||
|
onRequestMicPermission()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -579,6 +599,7 @@ private fun CameraXScreenPreview_20_9() {
|
|||||||
selectedMediaCount = 0,
|
selectedMediaCount = 0,
|
||||||
onCheckPermissions = {},
|
onCheckPermissions = {},
|
||||||
hasCameraPermission = { true },
|
hasCameraPermission = { true },
|
||||||
|
onRequestMicPermission = { },
|
||||||
createVideoFileDescriptor = { null },
|
createVideoFileDescriptor = { null },
|
||||||
getMaxVideoDurationInSeconds = { 60 },
|
getMaxVideoDurationInSeconds = { 60 },
|
||||||
cameraDisplay = CameraDisplay.DISPLAY_20_9,
|
cameraDisplay = CameraDisplay.DISPLAY_20_9,
|
||||||
@@ -604,6 +625,7 @@ private fun CameraXScreenPreview_19_9() {
|
|||||||
selectedMediaCount = 0,
|
selectedMediaCount = 0,
|
||||||
onCheckPermissions = {},
|
onCheckPermissions = {},
|
||||||
hasCameraPermission = { true },
|
hasCameraPermission = { true },
|
||||||
|
onRequestMicPermission = { },
|
||||||
createVideoFileDescriptor = { null },
|
createVideoFileDescriptor = { null },
|
||||||
getMaxVideoDurationInSeconds = { 60 },
|
getMaxVideoDurationInSeconds = { 60 },
|
||||||
cameraDisplay = CameraDisplay.DISPLAY_19_9,
|
cameraDisplay = CameraDisplay.DISPLAY_19_9,
|
||||||
@@ -629,6 +651,7 @@ private fun CameraXScreenPreview_18_9() {
|
|||||||
selectedMediaCount = 0,
|
selectedMediaCount = 0,
|
||||||
onCheckPermissions = {},
|
onCheckPermissions = {},
|
||||||
hasCameraPermission = { true },
|
hasCameraPermission = { true },
|
||||||
|
onRequestMicPermission = { },
|
||||||
createVideoFileDescriptor = { null },
|
createVideoFileDescriptor = { null },
|
||||||
getMaxVideoDurationInSeconds = { 60 },
|
getMaxVideoDurationInSeconds = { 60 },
|
||||||
cameraDisplay = CameraDisplay.DISPLAY_18_9,
|
cameraDisplay = CameraDisplay.DISPLAY_18_9,
|
||||||
@@ -654,6 +677,7 @@ private fun CameraXScreenPreview_16_9() {
|
|||||||
selectedMediaCount = 0,
|
selectedMediaCount = 0,
|
||||||
onCheckPermissions = {},
|
onCheckPermissions = {},
|
||||||
hasCameraPermission = { true },
|
hasCameraPermission = { true },
|
||||||
|
onRequestMicPermission = { },
|
||||||
createVideoFileDescriptor = { null },
|
createVideoFileDescriptor = { null },
|
||||||
getMaxVideoDurationInSeconds = { 60 },
|
getMaxVideoDurationInSeconds = { 60 },
|
||||||
cameraDisplay = CameraDisplay.DISPLAY_16_9,
|
cameraDisplay = CameraDisplay.DISPLAY_16_9,
|
||||||
@@ -679,6 +703,7 @@ private fun CameraXScreenPreview_6_5() {
|
|||||||
selectedMediaCount = 0,
|
selectedMediaCount = 0,
|
||||||
onCheckPermissions = {},
|
onCheckPermissions = {},
|
||||||
hasCameraPermission = { true },
|
hasCameraPermission = { true },
|
||||||
|
onRequestMicPermission = { },
|
||||||
createVideoFileDescriptor = { null },
|
createVideoFileDescriptor = { null },
|
||||||
getMaxVideoDurationInSeconds = { 60 },
|
getMaxVideoDurationInSeconds = { 60 },
|
||||||
cameraDisplay = CameraDisplay.DISPLAY_6_5,
|
cameraDisplay = CameraDisplay.DISPLAY_6_5,
|
||||||
|
|||||||
@@ -147,6 +147,9 @@ fun MainScreen(
|
|||||||
is StandardCameraHudEvents.MediaSelectionClick -> {
|
is StandardCameraHudEvents.MediaSelectionClick -> {
|
||||||
// Doesn't need to be handled
|
// Doesn't need to be handled
|
||||||
}
|
}
|
||||||
|
is StandardCameraHudEvents.AudioPermissionRequired -> {
|
||||||
|
// Doesn't need to be handled
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ fun BoxScope.StandardCameraHud(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
maxRecordingDurationMs: Long = DEFAULT_MAX_RECORDING_DURATION_MS,
|
maxRecordingDurationMs: Long = DEFAULT_MAX_RECORDING_DURATION_MS,
|
||||||
mediaSelectionCount: Int = 0,
|
mediaSelectionCount: Int = 0,
|
||||||
|
hasAudioPermission: () -> Boolean = { true },
|
||||||
stringResources: StringResources = StringResources(0, 0)
|
stringResources: StringResources = StringResources(0, 0)
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -131,6 +132,7 @@ fun BoxScope.StandardCameraHud(
|
|||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
maxRecordingDurationMs = maxRecordingDurationMs,
|
maxRecordingDurationMs = maxRecordingDurationMs,
|
||||||
mediaSelectionCount = mediaSelectionCount,
|
mediaSelectionCount = mediaSelectionCount,
|
||||||
|
hasAudioPermission = hasAudioPermission,
|
||||||
stringResources = stringResources
|
stringResources = stringResources
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -142,6 +144,7 @@ private fun BoxScope.StandardCameraHudContent(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
maxRecordingDurationMs: Long = DEFAULT_MAX_RECORDING_DURATION_MS,
|
maxRecordingDurationMs: Long = DEFAULT_MAX_RECORDING_DURATION_MS,
|
||||||
mediaSelectionCount: Int = 0,
|
mediaSelectionCount: Int = 0,
|
||||||
|
hasAudioPermission: () -> Boolean = { true },
|
||||||
stringResources: StringResources = StringResources()
|
stringResources: StringResources = StringResources()
|
||||||
) {
|
) {
|
||||||
val configuration = LocalConfiguration.current
|
val configuration = LocalConfiguration.current
|
||||||
@@ -177,6 +180,7 @@ private fun BoxScope.StandardCameraHudContent(
|
|||||||
},
|
},
|
||||||
mediaSelectionCount = mediaSelectionCount,
|
mediaSelectionCount = mediaSelectionCount,
|
||||||
emitter = emitter,
|
emitter = emitter,
|
||||||
|
hasAudioPermission = hasAudioPermission,
|
||||||
stringResources = stringResources,
|
stringResources = stringResources,
|
||||||
modifier = modifier.align(if (isLandscape) Alignment.CenterEnd else Alignment.BottomCenter)
|
modifier = modifier.align(if (isLandscape) Alignment.CenterEnd else Alignment.BottomCenter)
|
||||||
)
|
)
|
||||||
@@ -209,6 +213,7 @@ private fun CameraControls(
|
|||||||
recordingProgress: Float,
|
recordingProgress: Float,
|
||||||
mediaSelectionCount: Int,
|
mediaSelectionCount: Int,
|
||||||
emitter: (StandardCameraHudEvents) -> Unit,
|
emitter: (StandardCameraHudEvents) -> Unit,
|
||||||
|
hasAudioPermission: () -> Boolean,
|
||||||
stringResources: StringResources,
|
stringResources: StringResources,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
@@ -229,7 +234,13 @@ private fun CameraControls(
|
|||||||
isRecording = isRecording,
|
isRecording = isRecording,
|
||||||
recordingProgress = recordingProgress,
|
recordingProgress = recordingProgress,
|
||||||
onTap = { emitter(StandardCameraHudEvents.PhotoCaptureTriggered) },
|
onTap = { emitter(StandardCameraHudEvents.PhotoCaptureTriggered) },
|
||||||
onLongPressStart = { emitter(StandardCameraHudEvents.VideoCaptureStarted) },
|
onLongPressStart = {
|
||||||
|
if (hasAudioPermission()) {
|
||||||
|
emitter(StandardCameraHudEvents.VideoCaptureStarted)
|
||||||
|
} else {
|
||||||
|
emitter(StandardCameraHudEvents.AudioPermissionRequired)
|
||||||
|
}
|
||||||
|
},
|
||||||
onLongPressEnd = { emitter(StandardCameraHudEvents.VideoCaptureStopped) },
|
onLongPressEnd = { emitter(StandardCameraHudEvents.VideoCaptureStopped) },
|
||||||
onZoomChange = { emitter(StandardCameraHudEvents.SetZoomLevel(it)) }
|
onZoomChange = { emitter(StandardCameraHudEvents.SetZoomLevel(it)) }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -37,4 +37,9 @@ sealed interface StandardCameraHudEvents {
|
|||||||
* Emitted when a capture error should be cleared (after displaying to user).
|
* Emitted when a capture error should be cleared (after displaying to user).
|
||||||
*/
|
*/
|
||||||
data object ClearCaptureError : StandardCameraHudEvents
|
data object ClearCaptureError : StandardCameraHudEvents
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emitted when the user attempts to start video recording but audio permission has not been granted.
|
||||||
|
*/
|
||||||
|
data object AudioPermissionRequired : StandardCameraHudEvents
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user