mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-02 08:23:00 +01:00
Automatically reduce camera use cases on error.
This commit is contained in:
committed by
jeffrey-signal
parent
5140c41c58
commit
3c5774960a
@@ -24,6 +24,7 @@ import androidx.camera.core.ImageCaptureException
|
|||||||
import androidx.camera.core.ImageProxy
|
import androidx.camera.core.ImageProxy
|
||||||
import androidx.camera.core.Preview
|
import androidx.camera.core.Preview
|
||||||
import androidx.camera.core.SurfaceOrientedMeteringPointFactory
|
import androidx.camera.core.SurfaceOrientedMeteringPointFactory
|
||||||
|
import androidx.camera.core.UseCase
|
||||||
import androidx.camera.core.resolutionselector.AspectRatioStrategy
|
import androidx.camera.core.resolutionselector.AspectRatioStrategy
|
||||||
import androidx.camera.core.resolutionselector.ResolutionSelector
|
import androidx.camera.core.resolutionselector.ResolutionSelector
|
||||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||||
@@ -328,6 +329,47 @@ class CameraScreenViewModel : ViewModel() {
|
|||||||
state: CameraScreenState,
|
state: CameraScreenState,
|
||||||
event: CameraScreenEvents.BindCamera
|
event: CameraScreenEvents.BindCamera
|
||||||
) {
|
) {
|
||||||
|
val cameraSelector = CameraSelector.Builder()
|
||||||
|
.requireLensFacing(state.lensFacing)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
// Build binding attempts with progressively fewer optional use cases.
|
||||||
|
// Some devices cannot support all use cases simultaneously, so we fall back
|
||||||
|
// by first dropping video capture, then QR scanning.
|
||||||
|
val bindingAttempts = buildBindingAttempts(event)
|
||||||
|
|
||||||
|
for ((index, attempt) in bindingAttempts.withIndex()) {
|
||||||
|
try {
|
||||||
|
event.cameraProvider.unbindAll()
|
||||||
|
|
||||||
|
camera = event.cameraProvider.bindToLifecycle(
|
||||||
|
event.lifecycleOwner,
|
||||||
|
cameraSelector,
|
||||||
|
*attempt.toTypedArray()
|
||||||
|
)
|
||||||
|
|
||||||
|
if (index > 0) {
|
||||||
|
Log.w(TAG, "Use case binding succeeded on fallback attempt ${index + 1} of ${bindingAttempts.size}")
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycleOwner = event.lifecycleOwner
|
||||||
|
cameraProvider = event.cameraProvider
|
||||||
|
imageCapture = attempt.imageCapture
|
||||||
|
videoCapture = attempt.videoCapture
|
||||||
|
|
||||||
|
setupOrientationListener(event.context)
|
||||||
|
return
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Use case binding failed (attempt ${index + 1} of ${bindingAttempts.size})", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.e(TAG, "All use case binding attempts failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildBindingAttempts(
|
||||||
|
event: CameraScreenEvents.BindCamera
|
||||||
|
): List<BindingAttempt> {
|
||||||
val resolutionSelector = ResolutionSelector.Builder()
|
val resolutionSelector = ResolutionSelector.Builder()
|
||||||
.setAspectRatioStrategy(AspectRatioStrategy.RATIO_16_9_FALLBACK_AUTO_STRATEGY)
|
.setAspectRatioStrategy(AspectRatioStrategy.RATIO_16_9_FALLBACK_AUTO_STRATEGY)
|
||||||
.build()
|
.build()
|
||||||
@@ -339,16 +381,12 @@ class CameraScreenViewModel : ViewModel() {
|
|||||||
.also { it.surfaceProvider = event.surfaceProvider }
|
.also { it.surfaceProvider = event.surfaceProvider }
|
||||||
|
|
||||||
// Image capture with 16:9 aspect ratio (optimized for speed)
|
// Image capture with 16:9 aspect ratio (optimized for speed)
|
||||||
val imageCaptureUseCase = ImageCapture.Builder()
|
val imageCapture = ImageCapture.Builder()
|
||||||
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
|
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
|
||||||
.setResolutionSelector(resolutionSelector)
|
.setResolutionSelector(resolutionSelector)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
// Build the list of use cases based on device capabilities
|
val videoCapture: VideoCapture<Recorder>? = if (event.enableVideoCapture) {
|
||||||
val useCases = mutableListOf<androidx.camera.core.UseCase>(preview, imageCaptureUseCase)
|
|
||||||
|
|
||||||
var videoCaptureUseCase: VideoCapture<Recorder>? = null
|
|
||||||
if (event.enableVideoCapture) {
|
|
||||||
val recorder = Recorder.Builder()
|
val recorder = Recorder.Builder()
|
||||||
.setAspectRatio(AspectRatio.RATIO_16_9)
|
.setAspectRatio(AspectRatio.RATIO_16_9)
|
||||||
.setQualitySelector(
|
.setQualitySelector(
|
||||||
@@ -358,12 +396,13 @@ class CameraScreenViewModel : ViewModel() {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
videoCaptureUseCase = VideoCapture.withOutput(recorder)
|
VideoCapture.withOutput(recorder)
|
||||||
useCases += videoCaptureUseCase
|
} else {
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.enableQrScanning) {
|
val qrAnalysis: ImageAnalysis? = if (event.enableQrScanning) {
|
||||||
val imageAnalysisUseCase = ImageAnalysis.Builder()
|
ImageAnalysis.Builder()
|
||||||
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||||||
.build()
|
.build()
|
||||||
.also {
|
.also {
|
||||||
@@ -371,33 +410,23 @@ class CameraScreenViewModel : ViewModel() {
|
|||||||
processImageForQrCode(imageProxy)
|
processImageForQrCode(imageProxy)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
useCases += imageAnalysisUseCase
|
} else {
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Select camera based on lensFacing
|
return buildList {
|
||||||
val cameraSelector = CameraSelector.Builder()
|
// Attempt 1: All use cased
|
||||||
.requireLensFacing(state.lensFacing)
|
add(BindingAttempt(preview = preview, imageCapture = imageCapture, videoCapture = videoCapture, imageAnalysis = qrAnalysis))
|
||||||
.build()
|
|
||||||
|
|
||||||
try {
|
// Attempt 2: Drop video capture
|
||||||
// Unbind use cases before rebinding
|
if (videoCapture != null) {
|
||||||
event.cameraProvider.unbindAll()
|
add(BindingAttempt(preview = preview, imageCapture = imageCapture, videoCapture = null, imageAnalysis = qrAnalysis))
|
||||||
|
}
|
||||||
|
|
||||||
// Bind use cases to camera
|
// Attempt 3: Drop QR scanning
|
||||||
camera = event.cameraProvider.bindToLifecycle(
|
if (qrAnalysis != null) {
|
||||||
event.lifecycleOwner,
|
add(BindingAttempt(preview = preview, imageCapture = imageCapture, videoCapture = null, imageAnalysis = null))
|
||||||
cameraSelector,
|
}
|
||||||
*useCases.toTypedArray()
|
|
||||||
)
|
|
||||||
|
|
||||||
lifecycleOwner = event.lifecycleOwner
|
|
||||||
cameraProvider = event.cameraProvider
|
|
||||||
imageCapture = imageCaptureUseCase
|
|
||||||
videoCapture = videoCaptureUseCase
|
|
||||||
|
|
||||||
setupOrientationListener(event.context)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Use case binding failed", e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -682,4 +711,15 @@ class CameraScreenViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private data class BindingAttempt(
|
||||||
|
val preview: Preview,
|
||||||
|
val imageCapture: ImageCapture,
|
||||||
|
val videoCapture: VideoCapture<Recorder>?,
|
||||||
|
val imageAnalysis: ImageAnalysis?
|
||||||
|
) {
|
||||||
|
fun toTypedArray(): Array<UseCase> {
|
||||||
|
return listOfNotNull(preview, imageCapture, videoCapture, imageAnalysis).toTypedArray()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user