From 5a39d3848aa6debd7c82a8a00e9e675b91d19ca5 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 7 Nov 2025 13:13:21 +0200 Subject: [PATCH 1/2] [MS-1238] Unifying OCR preprocessing. Controlling preprocessing thread from the fragment via Job --- .../ExternalCredentialScanOcrFragment.kt | 53 ++++++++++--------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/ExternalCredentialScanOcrFragment.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/ExternalCredentialScanOcrFragment.kt index bc5ef22d0c..656a588389 100644 --- a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/ExternalCredentialScanOcrFragment.kt +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/ExternalCredentialScanOcrFragment.kt @@ -45,6 +45,7 @@ import com.simprints.infra.uibase.view.fadeOut import com.simprints.infra.uibase.viewbinding.viewBinding import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Job import kotlinx.coroutines.launch import java.util.concurrent.ExecutorService import java.util.concurrent.Executors @@ -85,6 +86,7 @@ internal class ExternalCredentialScanOcrFragment : Fragment(R.layout.fragment_ex private var checkAnimator: ViewPropertyAnimator? = null private var isAnimatingCompletion: Boolean = false private var pendingFinishAction: (() -> Unit)? = null + private var ocrPreProcessingJob: Job? = null @Inject lateinit var viewModelFactory: ExternalCredentialScanOcrViewModel.Factory @@ -132,11 +134,11 @@ internal class ExternalCredentialScanOcrFragment : Fragment(R.layout.fragment_ex } } - override fun onDestroy() { + override fun onDestroyView() { stopOcr() stopCamera() clearAnimations() - super.onDestroy() + super.onDestroyView() } private fun clearAnimations() { @@ -299,48 +301,46 @@ internal class ExternalCredentialScanOcrFragment : Fragment(R.layout.fragment_ex // Running OCR as often as we can while camera feedback is displayed to the user viewModel.ocrOnFrameStarted() if (viewModel.ocrConfig.useHighRes) { - captureHighResImageForOcr() videoFrame.close() + captureHighResImageForOcr { highResImage -> + preProcessImageAndRunOcr(highResImage) + } } else { - captureFrameFromVideoStreamForOcr(videoFrame) + preProcessImageAndRunOcr(videoFrame) } } } - private fun captureFrameFromVideoStreamForOcr(imageProxy: ImageProxy) { - lifecycleScope.launch(bgDispatcher) { + private fun preProcessImageAndRunOcr(imageProxy: ImageProxy) { + ocrPreProcessingJob?.cancel() + ocrPreProcessingJob = lifecycleScope.launch(bgDispatcher) { try { val (bitmap, imageInfo) = imageProxy.toBitmap() to imageProxy.imageInfo - val cropConfig: OcrCropConfig = buildOcrCropConfigUseCase( - rotationDegrees = imageInfo.rotationDegrees, - cameraPreview = binding.preview, - documentScannerArea = binding.documentScannerArea, - ) - viewModel.runOcrOnFrame(frame = bitmap, cropConfig) + if (ocrPreProcessingJob?.isActive == true) { + val cropConfig: OcrCropConfig = buildOcrCropConfigUseCase( + rotationDegrees = imageInfo.rotationDegrees, + cameraPreview = binding.preview, + documentScannerArea = binding.documentScannerArea, + ) + viewModel.runOcrOnFrame(frame = bitmap, cropConfig) + } else { + Simber.i( + "Unable to run OCR preprocessing, coroutine context is cancelled", + tag = MULTI_FACTOR_ID, + ) + } } finally { imageProxy.close() } } } - private fun captureHighResImageForOcr() { + private fun captureHighResImageForOcr(onImageCaptured: (ImageProxy) -> Unit) { imageCapture.takePicture( cameraExecutor, object : ImageCapture.OnImageCapturedCallback() { override fun onCaptureSuccess(imageProxy: ImageProxy) { - lifecycleScope.launch(bgDispatcher) { - try { - val (bitmap, imageInfo) = imageProxy.toBitmap() to imageProxy.imageInfo - val cropConfig: OcrCropConfig = buildOcrCropConfigUseCase( - rotationDegrees = imageInfo.rotationDegrees, - cameraPreview = binding.preview, - documentScannerArea = binding.documentScannerArea, - ) - viewModel.runOcrOnFrame(frame = bitmap, cropConfig) - } finally { - imageProxy.close() - } - } + onImageCaptured(imageProxy) } override fun onError(e: ImageCaptureException) { @@ -357,6 +357,7 @@ internal class ExternalCredentialScanOcrFragment : Fragment(R.layout.fragment_ex } private fun stopOcr() { + ocrPreProcessingJob?.cancel() if (::imageAnalysis.isInitialized) { imageAnalysis.clearAnalyzer() } From a870696dd5246450497ebd5db95040d331fd940d Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 7 Nov 2025 18:53:04 +0200 Subject: [PATCH 2/2] [MS-1238] Renaming 'state' to 'ocrState' for consistency --- .../screens/scanocr/ExternalCredentialScanOcrViewModel.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/ExternalCredentialScanOcrViewModel.kt b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/ExternalCredentialScanOcrViewModel.kt index b557c4895e..91a13dd5b3 100644 --- a/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/ExternalCredentialScanOcrViewModel.kt +++ b/feature/external-credential/src/main/java/com/simprints/feature/externalcredential/screens/scanocr/ExternalCredentialScanOcrViewModel.kt @@ -24,7 +24,6 @@ import com.simprints.feature.externalcredential.screens.scanocr.usecase.Normaliz import com.simprints.feature.externalcredential.screens.scanocr.usecase.ZoomOntoCredentialUseCase import com.simprints.feature.externalcredential.screens.search.model.ScannedCredential import com.simprints.infra.authstore.AuthStore -import com.simprints.infra.config.store.models.ExperimentalProjectConfiguration import com.simprints.infra.config.store.models.TokenKeyType import com.simprints.infra.config.store.models.experimental import com.simprints.infra.config.store.tokenization.TokenizationProcessor @@ -64,7 +63,7 @@ internal class ExternalCredentialScanOcrViewModel @AssistedInject constructor( private set val isOcrActive: Boolean get() = detectedBlocks.isNotEmpty() - private var state: ScanOcrState = ScanOcrState.EMPTY + private var ocrState: ScanOcrState = ScanOcrState.EMPTY set(value) { field = value _stateLiveData.postValue(value) @@ -91,7 +90,7 @@ internal class ExternalCredentialScanOcrViewModel @AssistedInject constructor( } private fun updateState(state: (ScanOcrState) -> ScanOcrState) { - this.state = state(this.state) + this.ocrState = state(this.ocrState) } fun getDocumentTypeRes(): Int = when (ocrDocumentType) {