From 926bee4705ddbad38096c5a9d984294f7e5b5245 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Thu, 8 Aug 2024 11:21:36 +0200 Subject: [PATCH 1/8] make scanner nullable to prevent leak; use early returns in process image callback; format --- .../mobile_scanner/MobileScanner.kt | 98 +++++++++---------- 1 file changed, 48 insertions(+), 50 deletions(-) diff --git a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt index fa6f20c50..3a9008053 100644 --- a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt +++ b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt @@ -20,12 +20,12 @@ import androidx.camera.core.ImageAnalysis import androidx.camera.core.ImageProxy import androidx.camera.core.Preview import androidx.camera.core.TorchState -import androidx.camera.core.resolutionselector.AspectRatioStrategy import androidx.camera.core.resolutionselector.ResolutionSelector import androidx.camera.core.resolutionselector.ResolutionStrategy import androidx.camera.lifecycle.ProcessCameraProvider import androidx.core.content.ContextCompat import androidx.lifecycle.LifecycleOwner +import com.google.mlkit.vision.barcode.BarcodeScanner import com.google.mlkit.vision.barcode.BarcodeScannerOptions import com.google.mlkit.vision.barcode.BarcodeScanning import com.google.mlkit.vision.barcode.common.Barcode @@ -37,7 +37,6 @@ import io.flutter.view.TextureRegistry import java.io.ByteArrayOutputStream import kotlin.math.roundToInt - class MobileScanner( private val activity: Activity, private val textureRegistry: TextureRegistry, @@ -50,7 +49,7 @@ class MobileScanner( private var camera: Camera? = null private var preview: Preview? = null private var textureEntry: TextureRegistry.SurfaceTextureEntry? = null - private var scanner = BarcodeScanning.getClient() + private var scanner: BarcodeScanner? = null private var lastScanned: List? = null private var scannerTimeout = false private var displayListener: DisplayManager.DisplayListener? = null @@ -76,76 +75,75 @@ class MobileScanner( scannerTimeout = true } - scanner.process(inputImage) - .addOnSuccessListener { barcodes -> + scanner?.let { + it.process(inputImage).addOnSuccessListener { barcodes -> if (detectionSpeed == DetectionSpeed.NO_DUPLICATES) { - val newScannedBarcodes = barcodes.mapNotNull { barcode -> barcode.rawValue }.sorted() + val newScannedBarcodes = barcodes.mapNotNull { + barcode -> barcode.rawValue + }.sorted() + if (newScannedBarcodes == lastScanned) { // New scanned is duplicate, returning return@addOnSuccessListener } - if (newScannedBarcodes.isNotEmpty()) lastScanned = newScannedBarcodes + if (newScannedBarcodes.isNotEmpty()) { + lastScanned = newScannedBarcodes + } } val barcodeMap: MutableList> = mutableListOf() for (barcode in barcodes) { - if (scanWindow != null) { - val match = isBarcodeInScanWindow(scanWindow!!, barcode, imageProxy) - if (!match) { - continue - } else { - barcodeMap.add(barcode.data) - } - } else { + if (scanWindow == null) { barcodeMap.add(barcode.data) + continue } - } + if (isBarcodeInScanWindow(scanWindow!!, barcode, imageProxy)) { + barcodeMap.add(barcode.data) + } + } - if (barcodeMap.isNotEmpty()) { - if (returnImage) { - - val bitmap = Bitmap.createBitmap(mediaImage.width, mediaImage.height, Bitmap.Config.ARGB_8888) - - val imageFormat = YuvToRgbConverter(activity.applicationContext) - - imageFormat.yuvToRgb(mediaImage, bitmap) + if (barcodeMap.isEmpty()) { + return@addOnSuccessListener + } - val bmResult = rotateBitmap(bitmap, camera?.cameraInfo?.sensorRotationDegrees?.toFloat() ?: 90f) + if (!returnImage) { + mobileScannerCallback( + barcodeMap, + null, + null, + null + ) + return@addOnSuccessListener + } - val stream = ByteArrayOutputStream() - bmResult.compress(Bitmap.CompressFormat.PNG, 100, stream) - val byteArray = stream.toByteArray() - val bmWidth = bmResult.width - val bmHeight = bmResult.height - bmResult.recycle() + val bitmap = Bitmap.createBitmap(mediaImage.width, mediaImage.height, Bitmap.Config.ARGB_8888) + val imageFormat = YuvToRgbConverter(activity.applicationContext) + imageFormat.yuvToRgb(mediaImage, bitmap) - mobileScannerCallback( - barcodeMap, - byteArray, - bmWidth, - bmHeight - ) + val bmResult = rotateBitmap(bitmap, camera?.cameraInfo?.sensorRotationDegrees?.toFloat() ?: 90f) - } else { + val stream = ByteArrayOutputStream() + bmResult.compress(Bitmap.CompressFormat.PNG, 100, stream) + val byteArray = stream.toByteArray() + val bmWidth = bmResult.width + val bmHeight = bmResult.height + bmResult.recycle() - mobileScannerCallback( - barcodeMap, - null, - null, - null - ) - } - } - } - .addOnFailureListener { e -> + mobileScannerCallback( + barcodeMap, + byteArray, + bmWidth, + bmHeight + ) + }.addOnFailureListener { e -> mobileScannerErrorCallback( e.localizedMessage ?: e.toString() ) - } - .addOnCompleteListener { imageProxy.close() } + }.addOnCompleteListener { imageProxy.close() } + } if (detectionSpeed == DetectionSpeed.NORMAL) { // Set timer and continue From 8951c43db11f54facbd72ab8999136152a8f5323 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Thu, 8 Aug 2024 11:35:53 +0200 Subject: [PATCH 2/8] call close on the barcode scanning client --- .../kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt | 7 +++++++ .../dev/steenbakker/mobile_scanner/MobileScannerHandler.kt | 1 + 2 files changed, 8 insertions(+) diff --git a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt index 3a9008053..f7be79b05 100644 --- a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt +++ b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt @@ -472,4 +472,11 @@ class MobileScanner( camera?.cameraControl?.setZoomRatio(1f) } + /** + * Dispose of this scanner instance. + */ + fun dispose() { + scanner?.close() + scanner = null + } } diff --git a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerHandler.kt b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerHandler.kt index ea6a352a7..3d6d0abb2 100644 --- a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerHandler.kt +++ b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerHandler.kt @@ -92,6 +92,7 @@ class MobileScannerHandler( fun dispose(activityPluginBinding: ActivityPluginBinding) { methodChannel?.setMethodCallHandler(null) methodChannel = null + mobileScanner?.dispose() mobileScanner = null val listener: RequestPermissionsResultListener? = permissions.getPermissionListener() From 8bc2cfe5bc43ea37f4d252fcdb3691631395aaac Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Thu, 8 Aug 2024 11:48:30 +0200 Subject: [PATCH 3/8] provide an overridable method to create the barcode scanner in tests --- .../mobile_scanner/MobileScanner.kt | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt index f7be79b05..12fd80d2a 100644 --- a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt +++ b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt @@ -13,6 +13,7 @@ import android.os.Looper import android.util.Size import android.view.Surface import android.view.WindowManager +import androidx.annotation.VisibleForTesting import androidx.camera.core.Camera import androidx.camera.core.CameraSelector import androidx.camera.core.ExperimentalGetImage @@ -41,7 +42,7 @@ class MobileScanner( private val activity: Activity, private val textureRegistry: TextureRegistry, private val mobileScannerCallback: MobileScannerCallback, - private val mobileScannerErrorCallback: MobileScannerErrorCallback + private val mobileScannerErrorCallback: MobileScannerErrorCallback, ) { /// Internal variables @@ -159,6 +160,15 @@ class MobileScanner( return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true) } + /** + * Create a barcode scanner from the given options. + * + * Can be overridden in tests. + */ + @VisibleForTesting + fun createBarcodeScanner(options: BarcodeScannerOptions?) : BarcodeScanner { + return if (options == null) BarcodeScanning.getClient() else BarcodeScanning.getClient(options) + } // scales the scanWindow to the provided inputImage and checks if that scaled // scanWindow contains the barcode @@ -238,11 +248,7 @@ class MobileScanner( } lastScanned = null - scanner = if (barcodeScannerOptions != null) { - BarcodeScanning.getClient(barcodeScannerOptions) - } else { - BarcodeScanning.getClient() - } + scanner = createBarcodeScanner(barcodeScannerOptions) val cameraProviderFuture = ProcessCameraProvider.getInstance(activity) val executor = ContextCompat.getMainExecutor(activity) From e59c0261ea2a18df7a1255f31ef905bb95ace8af Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Thu, 8 Aug 2024 11:59:14 +0200 Subject: [PATCH 4/8] use a short lived scanner for analyze image --- .../mobile_scanner/MobileScanner.kt | 31 ++++++++++++------- .../mobile_scanner/MobileScannerHandler.kt | 8 ++++- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt index 12fd80d2a..423e80b44 100644 --- a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt +++ b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt @@ -443,22 +443,29 @@ class MobileScanner( /** * Analyze a single image. */ - fun analyzeImage(image: Uri, onSuccess: AnalyzerSuccessCallback, onError: AnalyzerErrorCallback) { + fun analyzeImage( + image: Uri, + scannerOptions: BarcodeScannerOptions?, + onSuccess: AnalyzerSuccessCallback, + onError: AnalyzerErrorCallback) { val inputImage = InputImage.fromFilePath(activity, image) - scanner.process(inputImage) - .addOnSuccessListener { barcodes -> - val barcodeMap = barcodes.map { barcode -> barcode.data } + // Use a short lived scanner instance, which is closed when the analysis is done. + val barcodeScanner: BarcodeScanner = createBarcodeScanner(scannerOptions) - if (barcodeMap.isNotEmpty()) { - onSuccess(barcodeMap) - } else { - onSuccess(null) - } - } - .addOnFailureListener { e -> - onError(e.localizedMessage ?: e.toString()) + barcodeScanner.process(inputImage).addOnSuccessListener { barcodes -> + val barcodeMap = barcodes.map { barcode -> barcode.data } + + if (barcodeMap.isEmpty()) { + onSuccess(null) + } else { + onSuccess(barcodeMap) } + }.addOnFailureListener { e -> + onError(e.localizedMessage ?: e.toString()) + }.addOnCompleteListener { + barcodeScanner.close() + } } /** diff --git a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerHandler.kt b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerHandler.kt index 3d6d0abb2..b51260bd0 100644 --- a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerHandler.kt +++ b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerHandler.kt @@ -243,7 +243,13 @@ class MobileScannerHandler( analyzerResult = result val uri = Uri.fromFile(File(call.arguments.toString())) - mobileScanner!!.analyzeImage(uri, analyzeImageSuccessCallback, analyzeImageErrorCallback) + // TODO: parse options from the method call + // See https://github.com/juliansteenbakker/mobile_scanner/issues/1069 + mobileScanner!!.analyzeImage( + uri, + null, + analyzeImageSuccessCallback, + analyzeImageErrorCallback) } private fun toggleTorch(result: MethodChannel.Result) { From 64f57c27802accc1cd66b82b25962501c3a51233 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Thu, 8 Aug 2024 12:17:45 +0200 Subject: [PATCH 5/8] use injectable factory function with default instead --- .../steenbakker/mobile_scanner/MobileScanner.kt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt index 423e80b44..cc8171d76 100644 --- a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt +++ b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt @@ -43,6 +43,7 @@ class MobileScanner( private val textureRegistry: TextureRegistry, private val mobileScannerCallback: MobileScannerCallback, private val mobileScannerErrorCallback: MobileScannerErrorCallback, + private val barcodeScannerFactory: (options: BarcodeScannerOptions?) -> BarcodeScanner = ::defaultBarcodeScannerFactory, ) { /// Internal variables @@ -61,6 +62,15 @@ class MobileScanner( private var detectionTimeout: Long = 250 private var returnImage = false + companion object { + /** + * Create a barcode scanner from the given options. + */ + fun defaultBarcodeScannerFactory(options: BarcodeScannerOptions?) : BarcodeScanner { + return if (options == null) BarcodeScanning.getClient() else BarcodeScanning.getClient(options) + } + } + /** * callback for the camera. Every frame is passed through this function. */ @@ -248,7 +258,7 @@ class MobileScanner( } lastScanned = null - scanner = createBarcodeScanner(barcodeScannerOptions) + scanner = barcodeScannerFactory(barcodeScannerOptions) val cameraProviderFuture = ProcessCameraProvider.getInstance(activity) val executor = ContextCompat.getMainExecutor(activity) @@ -451,7 +461,7 @@ class MobileScanner( val inputImage = InputImage.fromFilePath(activity, image) // Use a short lived scanner instance, which is closed when the analysis is done. - val barcodeScanner: BarcodeScanner = createBarcodeScanner(scannerOptions) + val barcodeScanner: BarcodeScanner = barcodeScannerFactory(scannerOptions) barcodeScanner.process(inputImage).addOnSuccessListener { barcodes -> val barcodeMap = barcodes.map { barcode -> barcode.data } From ee336cd0c9119b63e30154d8285ece0232de1681 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Thu, 8 Aug 2024 12:49:31 +0200 Subject: [PATCH 6/8] fix bug with unbinding camera observers; release scanner on stop --- .../mobile_scanner/MobileScanner.kt | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt index cc8171d76..f8a257e2c 100644 --- a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt +++ b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt @@ -422,14 +422,27 @@ class MobileScanner( } val owner = activity as LifecycleOwner - camera?.cameraInfo?.torchState?.removeObservers(owner) + // Release the camera observers first. + camera?.cameraInfo?.let { + it.torchState.removeObservers(owner) + it.zoomState.removeObservers(owner) + it.cameraState.removeObservers(owner) + } + // Unbind the camera use cases, the preview is a use case. + // The camera will be closed when the last use case is unbound. cameraProvider?.unbindAll() - textureEntry?.release() - + cameraProvider = null camera = null preview = null + + // Release the texture for the preview. + textureEntry?.release() textureEntry = null - cameraProvider = null + + // Release the scanner. + scanner?.close() + scanner = null + lastScanned = null } private fun isStopped() = camera == null && preview == null From 6b06c5dadc11cb4107898c8043cebc36fd6ec403 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Thu, 8 Aug 2024 12:54:09 +0200 Subject: [PATCH 7/8] let the dispose method delegate to stop() on Android --- .../steenbakker/mobile_scanner/MobileScanner.kt | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt index f8a257e2c..6becac84d 100644 --- a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt +++ b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt @@ -170,16 +170,6 @@ class MobileScanner( return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true) } - /** - * Create a barcode scanner from the given options. - * - * Can be overridden in tests. - */ - @VisibleForTesting - fun createBarcodeScanner(options: BarcodeScannerOptions?) : BarcodeScanner { - return if (options == null) BarcodeScanning.getClient() else BarcodeScanning.getClient(options) - } - // scales the scanWindow to the provided inputImage and checks if that scaled // scanWindow contains the barcode private fun isBarcodeInScanWindow( @@ -512,7 +502,10 @@ class MobileScanner( * Dispose of this scanner instance. */ fun dispose() { - scanner?.close() - scanner = null + if (isStopped()) { + return + } + + stop() // Defer to the stop method, which disposes all resources anyway. } } From 5bf6bb3e0d713734c3d0412078a5f96b8295a850 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Thu, 8 Aug 2024 13:21:12 +0200 Subject: [PATCH 8/8] update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac8dc4024..aab8a4ce9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## NEXT +* Fixed a leak of the barcode scanner on Android. + ## 5.1.1 * This release fixes an issue with automatic starts in the examples.