From 4f5ce5711d41d2ca4a960c6bb19262763007a29e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 11 Dec 2025 17:05:42 +0100 Subject: [PATCH 1/4] Remove nullability --- .../io/element/android/libraries/qrcode/QRCodeAnalyzer.kt | 7 +++++-- .../element/android/libraries/qrcode/QrCodeCameraView.kt | 7 +------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/libraries/qrcode/src/main/kotlin/io/element/android/libraries/qrcode/QRCodeAnalyzer.kt b/libraries/qrcode/src/main/kotlin/io/element/android/libraries/qrcode/QRCodeAnalyzer.kt index ab2ee2ce7b..e9d43d86a3 100644 --- a/libraries/qrcode/src/main/kotlin/io/element/android/libraries/qrcode/QRCodeAnalyzer.kt +++ b/libraries/qrcode/src/main/kotlin/io/element/android/libraries/qrcode/QRCodeAnalyzer.kt @@ -15,7 +15,7 @@ import timber.log.Timber import zxingcpp.BarcodeReader internal class QRCodeAnalyzer( - private val onScanQrCode: (result: ByteArray?) -> Unit + private val onScanQrCode: (data: ByteArray) -> Unit ) : ImageAnalysis.Analyzer { private val reader by lazy { BarcodeReader() } @@ -23,7 +23,10 @@ internal class QRCodeAnalyzer( if (image.format in SUPPORTED_IMAGE_FORMATS) { try { val bytes = reader.read(image).firstNotNullOfOrNull { it.bytes } - bytes?.let { onScanQrCode(it) } + if (bytes != null) { + Timber.d("QR code scanned!") + onScanQrCode(bytes) + } } catch (e: Exception) { Timber.w(e, "Error decoding QR code") } finally { diff --git a/libraries/qrcode/src/main/kotlin/io/element/android/libraries/qrcode/QrCodeCameraView.kt b/libraries/qrcode/src/main/kotlin/io/element/android/libraries/qrcode/QrCodeCameraView.kt index a0d6613a3f..8c7563c5f8 100644 --- a/libraries/qrcode/src/main/kotlin/io/element/android/libraries/qrcode/QrCodeCameraView.kt +++ b/libraries/qrcode/src/main/kotlin/io/element/android/libraries/qrcode/QrCodeCameraView.kt @@ -80,12 +80,7 @@ fun QrCodeCameraView( .build() imageAnalysis.setAnalyzer( ContextCompat.getMainExecutor(previewView.context), - QRCodeAnalyzer { result -> - result?.let { - Timber.d("QR code scanned!") - onScanQrCode(it) - } - } + QRCodeAnalyzer(onScanQrCode) ) try { // Make sure we unbind all use cases before binding them again From efa5c7794d575014583f37af4e1eabad80c5571e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 11 Dec 2025 17:07:55 +0100 Subject: [PATCH 2/4] Improve code --- .../libraries/qrcode/QRCodeAnalyzer.kt | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/libraries/qrcode/src/main/kotlin/io/element/android/libraries/qrcode/QRCodeAnalyzer.kt b/libraries/qrcode/src/main/kotlin/io/element/android/libraries/qrcode/QRCodeAnalyzer.kt index e9d43d86a3..168e898965 100644 --- a/libraries/qrcode/src/main/kotlin/io/element/android/libraries/qrcode/QRCodeAnalyzer.kt +++ b/libraries/qrcode/src/main/kotlin/io/element/android/libraries/qrcode/QRCodeAnalyzer.kt @@ -20,17 +20,19 @@ internal class QRCodeAnalyzer( private val reader by lazy { BarcodeReader() } override fun analyze(image: ImageProxy) { - if (image.format in SUPPORTED_IMAGE_FORMATS) { - try { - val bytes = reader.read(image).firstNotNullOfOrNull { it.bytes } - if (bytes != null) { - Timber.d("QR code scanned!") - onScanQrCode(bytes) + image.use { + if (image.format in SUPPORTED_IMAGE_FORMATS) { + try { + val bytes = reader.read(image).firstNotNullOfOrNull { it.bytes } + if (bytes != null) { + Timber.d("QR code scanned!") + onScanQrCode(bytes) + } + } catch (e: Exception) { + Timber.w(e, "Error decoding QR code") } - } catch (e: Exception) { - Timber.w(e, "Error decoding QR code") - } finally { - image.close() + } else { + Timber.w("Unsupported image format: ${image.format}") } } } From 6100d4944f0f7f87b8a2975a56de3bf2af45200e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 11 Dec 2025 17:23:34 +0100 Subject: [PATCH 3/4] Cleanup --- .../screens/qrcode/scan/QrCodeScanView.kt | 2 +- .../libraries/qrcode/QrCodeCameraView.kt | 26 +++++++++++-------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanView.kt index 4f444b14bc..6fa3d1b0c7 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanView.kt @@ -106,7 +106,7 @@ private fun Content( QrCodeCameraView( modifier = Modifier.fillMaxSize(), onScanQrCode = { state.eventSink.invoke(QrCodeScanEvents.QrCodeScanned(it)) }, - renderPreview = state.isScanning, + isScanning = state.isScanning, ) } } diff --git a/libraries/qrcode/src/main/kotlin/io/element/android/libraries/qrcode/QrCodeCameraView.kt b/libraries/qrcode/src/main/kotlin/io/element/android/libraries/qrcode/QrCodeCameraView.kt index 8c7563c5f8..e687d4a945 100644 --- a/libraries/qrcode/src/main/kotlin/io/element/android/libraries/qrcode/QrCodeCameraView.kt +++ b/libraries/qrcode/src/main/kotlin/io/element/android/libraries/qrcode/QrCodeCameraView.kt @@ -45,7 +45,7 @@ import kotlin.coroutines.suspendCoroutine @Composable fun QrCodeCameraView( onScanQrCode: (ByteArray) -> Unit, - renderPreview: Boolean, + isScanning: Boolean, modifier: Modifier = Modifier, ) { if (LocalInspectionMode.current) { @@ -73,13 +73,13 @@ fun QrCodeCameraView( cameraProvider = localContext.getCameraProvider() } - suspend fun startQRCodeAnalysis(cameraProvider: ProcessCameraProvider, previewView: PreviewView, attempt: Int = 1) { + suspend fun startQRCodeAnalysis(cameraProvider: ProcessCameraProvider, attempt: Int = 1) { lastFrame = null val cameraSelector = CameraSelector.Builder() .requireLensFacing(CameraSelector.LENS_FACING_BACK) .build() imageAnalysis.setAnalyzer( - ContextCompat.getMainExecutor(previewView.context), + ContextCompat.getMainExecutor(localContext), QRCodeAnalyzer(onScanQrCode) ) try { @@ -100,7 +100,7 @@ fun QrCodeCameraView( } else { Timber.e(e, "Use case binding failed (attempt #$attempt). Retrying after a delay...") delay(100) - startQRCodeAnalysis(cameraProvider, previewView, attempt + 1) + startQRCodeAnalysis(cameraProvider, attempt + 1) } } } @@ -123,16 +123,18 @@ fun QrCodeCameraView( AndroidView( factory = { context -> val previewView = PreviewView(context) - previewUseCase.setSurfaceProvider(previewView.surfaceProvider) + previewUseCase.surfaceProvider = previewView.surfaceProvider previewView.previewStreamState.observe(lifecycleOwner) { state -> previewView.alpha = if (state == PreviewView.StreamState.STREAMING) 1f else 0f } previewView }, update = { previewView -> - if (renderPreview) { + if (isScanning) { cameraProvider?.let { provider -> - coroutineScope.launch { startQRCodeAnalysis(provider, previewView) } + coroutineScope.launch { + startQRCodeAnalysis(provider) + } } } else { stopQRCodeAnalysis(previewView) @@ -150,12 +152,14 @@ fun QrCodeCameraView( } } -@Suppress("BlockingMethodInNonBlockingContext") private suspend fun Context.getCameraProvider(): ProcessCameraProvider = suspendCoroutine { continuation -> ProcessCameraProvider.getInstance(this).also { cameraProvider -> - cameraProvider.addListener({ - continuation.resume(cameraProvider.get()) - }, ContextCompat.getMainExecutor(this)) + cameraProvider.addListener( + { + continuation.resume(cameraProvider.get()) + }, + ContextCompat.getMainExecutor(this) + ) } } From 06c20e3abf6870595b342ad6ad40d6ad0d2fc467 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 11 Dec 2025 17:33:24 +0100 Subject: [PATCH 4/4] Move usage of LocalInspectionMode.current into the deeper block. --- .../libraries/qrcode/QrCodeCameraView.kt | 134 +++++++++--------- 1 file changed, 67 insertions(+), 67 deletions(-) diff --git a/libraries/qrcode/src/main/kotlin/io/element/android/libraries/qrcode/QrCodeCameraView.kt b/libraries/qrcode/src/main/kotlin/io/element/android/libraries/qrcode/QrCodeCameraView.kt index e687d4a945..7f089818d0 100644 --- a/libraries/qrcode/src/main/kotlin/io/element/android/libraries/qrcode/QrCodeCameraView.kt +++ b/libraries/qrcode/src/main/kotlin/io/element/android/libraries/qrcode/QrCodeCameraView.kt @@ -48,78 +48,78 @@ fun QrCodeCameraView( isScanning: Boolean, modifier: Modifier = Modifier, ) { - if (LocalInspectionMode.current) { - Box( - modifier = modifier - .background(color = ElementTheme.colors.bgSubtlePrimary), - contentAlignment = Alignment.Center, - ) { - Text("CameraView") - } - } else { - val coroutineScope = rememberCoroutineScope() - val localContext = LocalContext.current - val lifecycleOwner = LocalLifecycleOwner.current - var cameraProvider by remember { mutableStateOf(null) } - val previewUseCase = remember { Preview.Builder().build() } - var lastFrame by remember { mutableStateOf(null) } - val imageAnalysis = remember { - ImageAnalysis.Builder() - .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) - .build() - } + val coroutineScope = rememberCoroutineScope() + val localContext = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current + var cameraProvider by remember { mutableStateOf(null) } + val previewUseCase = remember { Preview.Builder().build() } + var lastFrame by remember { mutableStateOf(null) } + val imageAnalysis = remember { + ImageAnalysis.Builder() + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) + .build() + } - LaunchedEffect(Unit) { - cameraProvider = localContext.getCameraProvider() - } + LaunchedEffect(Unit) { + cameraProvider = localContext.getCameraProvider() + } - suspend fun startQRCodeAnalysis(cameraProvider: ProcessCameraProvider, attempt: Int = 1) { - lastFrame = null - val cameraSelector = CameraSelector.Builder() - .requireLensFacing(CameraSelector.LENS_FACING_BACK) - .build() - imageAnalysis.setAnalyzer( - ContextCompat.getMainExecutor(localContext), - QRCodeAnalyzer(onScanQrCode) + suspend fun startQRCodeAnalysis(cameraProvider: ProcessCameraProvider, attempt: Int = 1) { + lastFrame = null + val cameraSelector = CameraSelector.Builder() + .requireLensFacing(CameraSelector.LENS_FACING_BACK) + .build() + imageAnalysis.setAnalyzer( + ContextCompat.getMainExecutor(localContext), + QRCodeAnalyzer(onScanQrCode) + ) + try { + // Make sure we unbind all use cases before binding them again + cameraProvider.unbindAll() + + cameraProvider.bindToLifecycle( + lifecycleOwner, + cameraSelector, + previewUseCase, + imageAnalysis, ) - try { - // Make sure we unbind all use cases before binding them again - cameraProvider.unbindAll() - - cameraProvider.bindToLifecycle( - lifecycleOwner, - cameraSelector, - previewUseCase, - imageAnalysis - ) - 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, 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, attempt + 1) } } + } - fun stopQRCodeAnalysis(previewView: PreviewView) { - // Stop analyzer - imageAnalysis.clearAnalyzer() + fun stopQRCodeAnalysis(previewView: PreviewView) { + // Stop analyzer + imageAnalysis.clearAnalyzer() - // Save last frame to display it as the 'frozen' preview - if (lastFrame == null) { - lastFrame = previewView.bitmap - Timber.d("Saving last frame for frozen preview.") - } - - // Unbind preview use case - cameraProvider?.unbindAll() + // Save last frame to display it as the 'frozen' preview + if (lastFrame == null) { + lastFrame = previewView.bitmap + Timber.d("Saving last frame for frozen preview.") } - 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( factory = { context -> val previewView = PreviewView(context) @@ -145,9 +145,9 @@ fun QrCodeCameraView( cameraProvider = null }, ) - lastFrame?.let { - Image(bitmap = it.asImageBitmap(), contentDescription = null) - } + } + lastFrame?.let { + Image(bitmap = it.asImageBitmap(), contentDescription = null) } } } @@ -159,7 +159,7 @@ private suspend fun Context.getCameraProvider(): ProcessCameraProvider = { continuation.resume(cameraProvider.get()) }, - ContextCompat.getMainExecutor(this) + ContextCompat.getMainExecutor(this), ) } }