Merge pull request #5891 from element-hq/feature/bma/qrCodeScannerCleanup
Qr code scanner cleanup
This commit is contained in:
@@ -106,7 +106,7 @@ private fun Content(
|
||||
QrCodeCameraView(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
onScanQrCode = { state.eventSink.invoke(QrCodeScanEvents.QrCodeScanned(it)) },
|
||||
renderPreview = state.isScanning,
|
||||
isScanning = state.isScanning,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,19 +15,24 @@ 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() }
|
||||
|
||||
override fun analyze(image: ImageProxy) {
|
||||
if (image.format in SUPPORTED_IMAGE_FORMATS) {
|
||||
try {
|
||||
val bytes = reader.read(image).firstNotNullOfOrNull { it.bytes }
|
||||
bytes?.let { onScanQrCode(it) }
|
||||
} catch (e: Exception) {
|
||||
Timber.w(e, "Error decoding QR code")
|
||||
} finally {
|
||||
image.close()
|
||||
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")
|
||||
}
|
||||
} else {
|
||||
Timber.w("Unsupported image format: ${image.format}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,99 +45,96 @@ import kotlin.coroutines.suspendCoroutine
|
||||
@Composable
|
||||
fun QrCodeCameraView(
|
||||
onScanQrCode: (ByteArray) -> Unit,
|
||||
renderPreview: Boolean,
|
||||
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<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()
|
||||
}
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
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) {
|
||||
cameraProvider = localContext.getCameraProvider()
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
cameraProvider = localContext.getCameraProvider()
|
||||
}
|
||||
|
||||
suspend fun startQRCodeAnalysis(cameraProvider: ProcessCameraProvider, previewView: PreviewView, attempt: Int = 1) {
|
||||
lastFrame = null
|
||||
val cameraSelector = CameraSelector.Builder()
|
||||
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
|
||||
.build()
|
||||
imageAnalysis.setAnalyzer(
|
||||
ContextCompat.getMainExecutor(previewView.context),
|
||||
QRCodeAnalyzer { result ->
|
||||
result?.let {
|
||||
Timber.d("QR code scanned!")
|
||||
onScanQrCode(it)
|
||||
}
|
||||
}
|
||||
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, previewView, 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)
|
||||
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)
|
||||
@@ -148,19 +145,21 @@ fun QrCodeCameraView(
|
||||
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 =
|
||||
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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user