Merge pull request #5891 from element-hq/feature/bma/qrCodeScannerCleanup

Qr code scanner cleanup
This commit is contained in:
Benoit Marty 2025-12-12 17:24:01 +01:00 committed by GitHub
commit b03b6b3da8
3 changed files with 93 additions and 89 deletions

View file

@ -106,7 +106,7 @@ private fun Content(
QrCodeCameraView( QrCodeCameraView(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
onScanQrCode = { state.eventSink.invoke(QrCodeScanEvents.QrCodeScanned(it)) }, onScanQrCode = { state.eventSink.invoke(QrCodeScanEvents.QrCodeScanned(it)) },
renderPreview = state.isScanning, isScanning = state.isScanning,
) )
} }
} }

View file

@ -15,19 +15,24 @@ import timber.log.Timber
import zxingcpp.BarcodeReader import zxingcpp.BarcodeReader
internal class QRCodeAnalyzer( internal class QRCodeAnalyzer(
private val onScanQrCode: (result: ByteArray?) -> Unit private val onScanQrCode: (data: ByteArray) -> Unit
) : ImageAnalysis.Analyzer { ) : ImageAnalysis.Analyzer {
private val reader by lazy { BarcodeReader() } private val reader by lazy { BarcodeReader() }
override fun analyze(image: ImageProxy) { override fun analyze(image: ImageProxy) {
if (image.format in SUPPORTED_IMAGE_FORMATS) { image.use {
try { if (image.format in SUPPORTED_IMAGE_FORMATS) {
val bytes = reader.read(image).firstNotNullOfOrNull { it.bytes } try {
bytes?.let { onScanQrCode(it) } val bytes = reader.read(image).firstNotNullOfOrNull { it.bytes }
} catch (e: Exception) { if (bytes != null) {
Timber.w(e, "Error decoding QR code") Timber.d("QR code scanned!")
} finally { onScanQrCode(bytes)
image.close() }
} catch (e: Exception) {
Timber.w(e, "Error decoding QR code")
}
} else {
Timber.w("Unsupported image format: ${image.format}")
} }
} }
} }

View file

@ -45,99 +45,96 @@ import kotlin.coroutines.suspendCoroutine
@Composable @Composable
fun QrCodeCameraView( fun QrCodeCameraView(
onScanQrCode: (ByteArray) -> Unit, onScanQrCode: (ByteArray) -> Unit,
renderPreview: Boolean, isScanning: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
if (LocalInspectionMode.current) { val coroutineScope = rememberCoroutineScope()
Box( val localContext = LocalContext.current
modifier = modifier val lifecycleOwner = LocalLifecycleOwner.current
.background(color = ElementTheme.colors.bgSubtlePrimary), var cameraProvider by remember { mutableStateOf<ProcessCameraProvider?>(null) }
contentAlignment = Alignment.Center, val previewUseCase = remember { Preview.Builder().build() }
) { var lastFrame by remember { mutableStateOf<Bitmap?>(null) }
Text("CameraView") val imageAnalysis = remember {
} ImageAnalysis.Builder()
} else { .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
val coroutineScope = rememberCoroutineScope() .build()
val localContext = LocalContext.current }
val lifecycleOwner = LocalLifecycleOwner.current
var cameraProvider by remember { mutableStateOf<ProcessCameraProvider?>(null) }
val previewUseCase = remember { Preview.Builder().build() }
var lastFrame by remember { mutableStateOf<Bitmap?>(null) }
val imageAnalysis = remember {
ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
cameraProvider = localContext.getCameraProvider() cameraProvider = localContext.getCameraProvider()
} }
suspend fun startQRCodeAnalysis(cameraProvider: ProcessCameraProvider, previewView: PreviewView, attempt: Int = 1) { suspend fun startQRCodeAnalysis(cameraProvider: ProcessCameraProvider, attempt: Int = 1) {
lastFrame = null lastFrame = null
val cameraSelector = CameraSelector.Builder() val cameraSelector = CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_BACK) .requireLensFacing(CameraSelector.LENS_FACING_BACK)
.build() .build()
imageAnalysis.setAnalyzer( imageAnalysis.setAnalyzer(
ContextCompat.getMainExecutor(previewView.context), ContextCompat.getMainExecutor(localContext),
QRCodeAnalyzer { result -> QRCodeAnalyzer(onScanQrCode)
result?.let { )
Timber.d("QR code scanned!") try {
onScanQrCode(it) // Make sure we unbind all use cases before binding them again
} cameraProvider.unbindAll()
}
cameraProvider.bindToLifecycle(
lifecycleOwner,
cameraSelector,
previewUseCase,
imageAnalysis,
) )
try { lastFrame = null
// Make sure we unbind all use cases before binding them again } catch (e: Exception) {
cameraProvider.unbindAll() val maxAttempts = 3
if (attempt > maxAttempts) {
cameraProvider.bindToLifecycle( Timber.e(e, "Use case binding failed after $maxAttempts attempts. Giving up.")
lifecycleOwner, } else {
cameraSelector, Timber.e(e, "Use case binding failed (attempt #$attempt). Retrying after a delay...")
previewUseCase, delay(100)
imageAnalysis startQRCodeAnalysis(cameraProvider, attempt + 1)
)
lastFrame = null
} catch (e: Exception) {
val maxAttempts = 3
if (attempt > maxAttempts) {
Timber.e(e, "Use case binding failed after $maxAttempts attempts. Giving up.")
} else {
Timber.e(e, "Use case binding failed (attempt #$attempt). Retrying after a delay...")
delay(100)
startQRCodeAnalysis(cameraProvider, previewView, attempt + 1)
}
} }
} }
}
fun stopQRCodeAnalysis(previewView: PreviewView) { fun stopQRCodeAnalysis(previewView: PreviewView) {
// Stop analyzer // Stop analyzer
imageAnalysis.clearAnalyzer() imageAnalysis.clearAnalyzer()
// Save last frame to display it as the 'frozen' preview // Save last frame to display it as the 'frozen' preview
if (lastFrame == null) { if (lastFrame == null) {
lastFrame = previewView.bitmap lastFrame = previewView.bitmap
Timber.d("Saving last frame for frozen preview.") Timber.d("Saving last frame for frozen preview.")
}
// Unbind preview use case
cameraProvider?.unbindAll()
} }
Box(modifier.clipToBounds()) { // Unbind preview use case
cameraProvider?.unbindAll()
}
Box(modifier.clipToBounds()) {
if (LocalInspectionMode.current) {
Box(
modifier = modifier
.background(color = ElementTheme.colors.bgSubtlePrimary),
contentAlignment = Alignment.Center,
) {
Text("CameraView")
}
} else {
AndroidView( AndroidView(
factory = { context -> factory = { context ->
val previewView = PreviewView(context) val previewView = PreviewView(context)
previewUseCase.setSurfaceProvider(previewView.surfaceProvider) previewUseCase.surfaceProvider = previewView.surfaceProvider
previewView.previewStreamState.observe(lifecycleOwner) { state -> previewView.previewStreamState.observe(lifecycleOwner) { state ->
previewView.alpha = if (state == PreviewView.StreamState.STREAMING) 1f else 0f previewView.alpha = if (state == PreviewView.StreamState.STREAMING) 1f else 0f
} }
previewView previewView
}, },
update = { previewView -> update = { previewView ->
if (renderPreview) { if (isScanning) {
cameraProvider?.let { provider -> cameraProvider?.let { provider ->
coroutineScope.launch { startQRCodeAnalysis(provider, previewView) } coroutineScope.launch {
startQRCodeAnalysis(provider)
}
} }
} else { } else {
stopQRCodeAnalysis(previewView) stopQRCodeAnalysis(previewView)
@ -148,19 +145,21 @@ fun QrCodeCameraView(
cameraProvider = null cameraProvider = null
}, },
) )
lastFrame?.let { }
Image(bitmap = it.asImageBitmap(), contentDescription = null) lastFrame?.let {
} Image(bitmap = it.asImageBitmap(), contentDescription = null)
} }
} }
} }
@Suppress("BlockingMethodInNonBlockingContext")
private suspend fun Context.getCameraProvider(): ProcessCameraProvider = private suspend fun Context.getCameraProvider(): ProcessCameraProvider =
suspendCoroutine { continuation -> suspendCoroutine { continuation ->
ProcessCameraProvider.getInstance(this).also { cameraProvider -> ProcessCameraProvider.getInstance(this).also { cameraProvider ->
cameraProvider.addListener({ cameraProvider.addListener(
continuation.resume(cameraProvider.get()) {
}, ContextCompat.getMainExecutor(this)) continuation.resume(cameraProvider.get())
},
ContextCompat.getMainExecutor(this),
)
} }
} }