From c37bb96aabe7eea3c42e3487b76c22494573709d Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Wed, 25 Feb 2026 15:29:32 +0000 Subject: [PATCH] Only bind camera use cases that the device supports. The new camera implementation always bound all four CameraX use cases (preview, image capture, video capture, and image analysis) regardless of device capabilities. On devices with LEGACY camera hardware level, this causes image capture to fail with "Capture request failed with reason ERROR" because the hardware cannot handle that many simultaneous use cases. This change makes video capture and QR scanning use case binding conditional based on CameraXModePolicy, which already determines device capabilities. Video capture is only bound when the device supports mixed mode (image + video simultaneously). QR scanning analysis is only bound when explicitly requested. --- .../securesms/mediasend/CameraXFragment.kt | 9 ++++ .../java/org/signal/camera/CameraScreen.kt | 6 ++- .../org/signal/camera/CameraScreenEvents.kt | 6 ++- .../signal/camera/CameraScreenViewModel.kt | 51 ++++++++++--------- 4 files changed, 46 insertions(+), 26 deletions(-) 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 3190b8ced9..69f1d0959c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXFragment.kt @@ -138,6 +138,7 @@ class CameraXFragment : ComposeFragment(), CameraFragment { controller = controller, isVideoEnabled = isVideoEnabled && Build.VERSION.SDK_INT >= 26, isQrScanEnabled = isQrScanEnabled, + isVideoCaptureBindingEnabled = cameraXModePolicy is CameraXModePolicy.Mixed, controlsVisible = controlsVisible.value, selectedMediaCount = selectedMediaCount.intValue, onCheckPermissions = { checkPermissions(isVideoEnabled) }, @@ -298,6 +299,7 @@ private fun CameraXScreen( controller: CameraFragment.Controller?, isVideoEnabled: Boolean, isQrScanEnabled: Boolean, + isVideoCaptureBindingEnabled: Boolean, controlsVisible: Boolean, selectedMediaCount: Int, onCheckPermissions: () -> Unit, @@ -404,6 +406,8 @@ private fun CameraXScreen( emitter = { event -> cameraViewModel.onEvent(event) }, roundCorners = cameraDisplay.roundViewFinderCorners, contentAlignment = cameraAlignment, + enableVideoCapture = isVideoCaptureBindingEnabled, + enableQrScanning = isQrScanEnabled, modifier = Modifier.padding(bottom = viewportBottomMargin) ) { AnimatedVisibility( @@ -611,6 +615,7 @@ private fun CameraXScreenPreview_20_9() { controller = null, isVideoEnabled = true, isQrScanEnabled = false, + isVideoCaptureBindingEnabled = true, controlsVisible = true, selectedMediaCount = 0, onCheckPermissions = {}, @@ -638,6 +643,7 @@ private fun CameraXScreenPreview_19_9() { controller = null, isVideoEnabled = true, isQrScanEnabled = false, + isVideoCaptureBindingEnabled = true, controlsVisible = true, selectedMediaCount = 0, onCheckPermissions = {}, @@ -665,6 +671,7 @@ private fun CameraXScreenPreview_18_9() { controller = null, isVideoEnabled = true, isQrScanEnabled = false, + isVideoCaptureBindingEnabled = true, controlsVisible = true, selectedMediaCount = 0, onCheckPermissions = {}, @@ -692,6 +699,7 @@ private fun CameraXScreenPreview_16_9() { controller = null, isVideoEnabled = true, isQrScanEnabled = false, + isVideoCaptureBindingEnabled = true, controlsVisible = true, selectedMediaCount = 0, onCheckPermissions = {}, @@ -719,6 +727,7 @@ private fun CameraXScreenPreview_6_5() { controller = null, isVideoEnabled = true, isQrScanEnabled = false, + isVideoCaptureBindingEnabled = true, controlsVisible = true, selectedMediaCount = 0, onCheckPermissions = {}, diff --git a/feature/camera/src/main/java/org/signal/camera/CameraScreen.kt b/feature/camera/src/main/java/org/signal/camera/CameraScreen.kt index 40e0da45dc..65b966ed1b 100644 --- a/feature/camera/src/main/java/org/signal/camera/CameraScreen.kt +++ b/feature/camera/src/main/java/org/signal/camera/CameraScreen.kt @@ -76,6 +76,8 @@ fun CameraScreen( modifier: Modifier = Modifier, roundCorners: Boolean = true, contentAlignment: Alignment = Alignment.Center, + enableVideoCapture: Boolean = true, + enableQrScanning: Boolean = false, content: @Composable BoxScope.() -> Unit = {} ) { val context = LocalContext.current @@ -103,7 +105,9 @@ fun CameraScreen( lifecycleOwner = lifecycleOwner, cameraProvider = cameraProvider, surfaceProvider = surfaceProvider, - context = context + context = context, + enableVideoCapture = enableVideoCapture, + enableQrScanning = enableQrScanning ) ) } diff --git a/feature/camera/src/main/java/org/signal/camera/CameraScreenEvents.kt b/feature/camera/src/main/java/org/signal/camera/CameraScreenEvents.kt index 5615f35540..b29a2df02c 100644 --- a/feature/camera/src/main/java/org/signal/camera/CameraScreenEvents.kt +++ b/feature/camera/src/main/java/org/signal/camera/CameraScreenEvents.kt @@ -8,12 +8,14 @@ import androidx.lifecycle.LifecycleOwner sealed interface CameraScreenEvents { - /** Binds a camera to a sruface provider. */ + /** Binds a camera to a surface provider. */ data class BindCamera( val lifecycleOwner: LifecycleOwner, val cameraProvider: ProcessCameraProvider, val surfaceProvider: Preview.SurfaceProvider, - val context: Context + val context: Context, + val enableVideoCapture: Boolean = true, + val enableQrScanning: Boolean = false ) : CameraScreenEvents /** Focuses the camera on a point. */ diff --git a/feature/camera/src/main/java/org/signal/camera/CameraScreenViewModel.kt b/feature/camera/src/main/java/org/signal/camera/CameraScreenViewModel.kt index c19c72d80b..0f551a1a80 100644 --- a/feature/camera/src/main/java/org/signal/camera/CameraScreenViewModel.kt +++ b/feature/camera/src/main/java/org/signal/camera/CameraScreenViewModel.kt @@ -344,27 +344,35 @@ class CameraScreenViewModel : ViewModel() { .setResolutionSelector(resolutionSelector) .build() - // Video capture (16:9 is default for video) - val recorder = Recorder.Builder() - .setAspectRatio(AspectRatio.RATIO_16_9) - .setQualitySelector( - androidx.camera.video.QualitySelector.from( - androidx.camera.video.Quality.HIGHEST, - androidx.camera.video.FallbackStrategy.higherQualityOrLowerThan(androidx.camera.video.Quality.HD) - ) - ) - .build() - val videoCaptureUseCase = VideoCapture.withOutput(recorder) + // Build the list of use cases based on device capabilities + val useCases = mutableListOf(preview, imageCaptureUseCase) - // Image analysis for QR code detection - val imageAnalysisUseCase = ImageAnalysis.Builder() - .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) - .build() - .also { - it.setAnalyzer(imageAnalysisExecutor) { imageProxy -> - processImageForQrCode(imageProxy) + var videoCaptureUseCase: VideoCapture? = null + if (event.enableVideoCapture) { + val recorder = Recorder.Builder() + .setAspectRatio(AspectRatio.RATIO_16_9) + .setQualitySelector( + androidx.camera.video.QualitySelector.from( + androidx.camera.video.Quality.HIGHEST, + androidx.camera.video.FallbackStrategy.higherQualityOrLowerThan(androidx.camera.video.Quality.HD) + ) + ) + .build() + videoCaptureUseCase = VideoCapture.withOutput(recorder) + useCases += videoCaptureUseCase + } + + if (event.enableQrScanning) { + val imageAnalysisUseCase = ImageAnalysis.Builder() + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) + .build() + .also { + it.setAnalyzer(imageAnalysisExecutor) { imageProxy -> + processImageForQrCode(imageProxy) + } } - } + useCases += imageAnalysisUseCase + } // Select camera based on lensFacing val cameraSelector = CameraSelector.Builder() @@ -379,10 +387,7 @@ class CameraScreenViewModel : ViewModel() { camera = event.cameraProvider.bindToLifecycle( event.lifecycleOwner, cameraSelector, - preview, - imageCaptureUseCase, - videoCaptureUseCase, - imageAnalysisUseCase + *useCases.toTypedArray() ) lifecycleOwner = event.lifecycleOwner