Committed by
GitHub
Merge pull request #1139 from navaronbracke/fix_barcode_scanner_leak
fix: fix barcode scanner leak on Android
Showing
3 changed files
with
118 additions
and
74 deletions
| @@ -13,6 +13,7 @@ import android.os.Looper | @@ -13,6 +13,7 @@ import android.os.Looper | ||
| 13 | import android.util.Size | 13 | import android.util.Size |
| 14 | import android.view.Surface | 14 | import android.view.Surface |
| 15 | import android.view.WindowManager | 15 | import android.view.WindowManager |
| 16 | +import androidx.annotation.VisibleForTesting | ||
| 16 | import androidx.camera.core.Camera | 17 | import androidx.camera.core.Camera |
| 17 | import androidx.camera.core.CameraSelector | 18 | import androidx.camera.core.CameraSelector |
| 18 | import androidx.camera.core.ExperimentalGetImage | 19 | import androidx.camera.core.ExperimentalGetImage |
| @@ -20,12 +21,12 @@ import androidx.camera.core.ImageAnalysis | @@ -20,12 +21,12 @@ import androidx.camera.core.ImageAnalysis | ||
| 20 | import androidx.camera.core.ImageProxy | 21 | import androidx.camera.core.ImageProxy |
| 21 | import androidx.camera.core.Preview | 22 | import androidx.camera.core.Preview |
| 22 | import androidx.camera.core.TorchState | 23 | import androidx.camera.core.TorchState |
| 23 | -import androidx.camera.core.resolutionselector.AspectRatioStrategy | ||
| 24 | import androidx.camera.core.resolutionselector.ResolutionSelector | 24 | import androidx.camera.core.resolutionselector.ResolutionSelector |
| 25 | import androidx.camera.core.resolutionselector.ResolutionStrategy | 25 | import androidx.camera.core.resolutionselector.ResolutionStrategy |
| 26 | import androidx.camera.lifecycle.ProcessCameraProvider | 26 | import androidx.camera.lifecycle.ProcessCameraProvider |
| 27 | import androidx.core.content.ContextCompat | 27 | import androidx.core.content.ContextCompat |
| 28 | import androidx.lifecycle.LifecycleOwner | 28 | import androidx.lifecycle.LifecycleOwner |
| 29 | +import com.google.mlkit.vision.barcode.BarcodeScanner | ||
| 29 | import com.google.mlkit.vision.barcode.BarcodeScannerOptions | 30 | import com.google.mlkit.vision.barcode.BarcodeScannerOptions |
| 30 | import com.google.mlkit.vision.barcode.BarcodeScanning | 31 | import com.google.mlkit.vision.barcode.BarcodeScanning |
| 31 | import com.google.mlkit.vision.barcode.common.Barcode | 32 | import com.google.mlkit.vision.barcode.common.Barcode |
| @@ -37,12 +38,12 @@ import io.flutter.view.TextureRegistry | @@ -37,12 +38,12 @@ import io.flutter.view.TextureRegistry | ||
| 37 | import java.io.ByteArrayOutputStream | 38 | import java.io.ByteArrayOutputStream |
| 38 | import kotlin.math.roundToInt | 39 | import kotlin.math.roundToInt |
| 39 | 40 | ||
| 40 | - | ||
| 41 | class MobileScanner( | 41 | class MobileScanner( |
| 42 | private val activity: Activity, | 42 | private val activity: Activity, |
| 43 | private val textureRegistry: TextureRegistry, | 43 | private val textureRegistry: TextureRegistry, |
| 44 | private val mobileScannerCallback: MobileScannerCallback, | 44 | private val mobileScannerCallback: MobileScannerCallback, |
| 45 | - private val mobileScannerErrorCallback: MobileScannerErrorCallback | 45 | + private val mobileScannerErrorCallback: MobileScannerErrorCallback, |
| 46 | + private val barcodeScannerFactory: (options: BarcodeScannerOptions?) -> BarcodeScanner = ::defaultBarcodeScannerFactory, | ||
| 46 | ) { | 47 | ) { |
| 47 | 48 | ||
| 48 | /// Internal variables | 49 | /// Internal variables |
| @@ -50,7 +51,7 @@ class MobileScanner( | @@ -50,7 +51,7 @@ class MobileScanner( | ||
| 50 | private var camera: Camera? = null | 51 | private var camera: Camera? = null |
| 51 | private var preview: Preview? = null | 52 | private var preview: Preview? = null |
| 52 | private var textureEntry: TextureRegistry.SurfaceTextureEntry? = null | 53 | private var textureEntry: TextureRegistry.SurfaceTextureEntry? = null |
| 53 | - private var scanner = BarcodeScanning.getClient() | 54 | + private var scanner: BarcodeScanner? = null |
| 54 | private var lastScanned: List<String?>? = null | 55 | private var lastScanned: List<String?>? = null |
| 55 | private var scannerTimeout = false | 56 | private var scannerTimeout = false |
| 56 | private var displayListener: DisplayManager.DisplayListener? = null | 57 | private var displayListener: DisplayManager.DisplayListener? = null |
| @@ -61,6 +62,15 @@ class MobileScanner( | @@ -61,6 +62,15 @@ class MobileScanner( | ||
| 61 | private var detectionTimeout: Long = 250 | 62 | private var detectionTimeout: Long = 250 |
| 62 | private var returnImage = false | 63 | private var returnImage = false |
| 63 | 64 | ||
| 65 | + companion object { | ||
| 66 | + /** | ||
| 67 | + * Create a barcode scanner from the given options. | ||
| 68 | + */ | ||
| 69 | + fun defaultBarcodeScannerFactory(options: BarcodeScannerOptions?) : BarcodeScanner { | ||
| 70 | + return if (options == null) BarcodeScanning.getClient() else BarcodeScanning.getClient(options) | ||
| 71 | + } | ||
| 72 | + } | ||
| 73 | + | ||
| 64 | /** | 74 | /** |
| 65 | * callback for the camera. Every frame is passed through this function. | 75 | * callback for the camera. Every frame is passed through this function. |
| 66 | */ | 76 | */ |
| @@ -76,76 +86,75 @@ class MobileScanner( | @@ -76,76 +86,75 @@ class MobileScanner( | ||
| 76 | scannerTimeout = true | 86 | scannerTimeout = true |
| 77 | } | 87 | } |
| 78 | 88 | ||
| 79 | - scanner.process(inputImage) | ||
| 80 | - .addOnSuccessListener { barcodes -> | 89 | + scanner?.let { |
| 90 | + it.process(inputImage).addOnSuccessListener { barcodes -> | ||
| 81 | if (detectionSpeed == DetectionSpeed.NO_DUPLICATES) { | 91 | if (detectionSpeed == DetectionSpeed.NO_DUPLICATES) { |
| 82 | - val newScannedBarcodes = barcodes.mapNotNull { barcode -> barcode.rawValue }.sorted() | 92 | + val newScannedBarcodes = barcodes.mapNotNull { |
| 93 | + barcode -> barcode.rawValue | ||
| 94 | + }.sorted() | ||
| 95 | + | ||
| 83 | if (newScannedBarcodes == lastScanned) { | 96 | if (newScannedBarcodes == lastScanned) { |
| 84 | // New scanned is duplicate, returning | 97 | // New scanned is duplicate, returning |
| 85 | return@addOnSuccessListener | 98 | return@addOnSuccessListener |
| 86 | } | 99 | } |
| 87 | - if (newScannedBarcodes.isNotEmpty()) lastScanned = newScannedBarcodes | 100 | + if (newScannedBarcodes.isNotEmpty()) { |
| 101 | + lastScanned = newScannedBarcodes | ||
| 102 | + } | ||
| 88 | } | 103 | } |
| 89 | 104 | ||
| 90 | val barcodeMap: MutableList<Map<String, Any?>> = mutableListOf() | 105 | val barcodeMap: MutableList<Map<String, Any?>> = mutableListOf() |
| 91 | 106 | ||
| 92 | for (barcode in barcodes) { | 107 | for (barcode in barcodes) { |
| 93 | - if (scanWindow != null) { | ||
| 94 | - val match = isBarcodeInScanWindow(scanWindow!!, barcode, imageProxy) | ||
| 95 | - if (!match) { | ||
| 96 | - continue | ||
| 97 | - } else { | ||
| 98 | - barcodeMap.add(barcode.data) | ||
| 99 | - } | ||
| 100 | - } else { | 108 | + if (scanWindow == null) { |
| 101 | barcodeMap.add(barcode.data) | 109 | barcodeMap.add(barcode.data) |
| 110 | + continue | ||
| 102 | } | 111 | } |
| 103 | - } | ||
| 104 | - | ||
| 105 | - | ||
| 106 | - if (barcodeMap.isNotEmpty()) { | ||
| 107 | - if (returnImage) { | ||
| 108 | 112 | ||
| 109 | - val bitmap = Bitmap.createBitmap(mediaImage.width, mediaImage.height, Bitmap.Config.ARGB_8888) | ||
| 110 | - | ||
| 111 | - val imageFormat = YuvToRgbConverter(activity.applicationContext) | 113 | + if (isBarcodeInScanWindow(scanWindow!!, barcode, imageProxy)) { |
| 114 | + barcodeMap.add(barcode.data) | ||
| 115 | + } | ||
| 116 | + } | ||
| 112 | 117 | ||
| 113 | - imageFormat.yuvToRgb(mediaImage, bitmap) | 118 | + if (barcodeMap.isEmpty()) { |
| 119 | + return@addOnSuccessListener | ||
| 120 | + } | ||
| 114 | 121 | ||
| 115 | - val bmResult = rotateBitmap(bitmap, camera?.cameraInfo?.sensorRotationDegrees?.toFloat() ?: 90f) | 122 | + if (!returnImage) { |
| 123 | + mobileScannerCallback( | ||
| 124 | + barcodeMap, | ||
| 125 | + null, | ||
| 126 | + null, | ||
| 127 | + null | ||
| 128 | + ) | ||
| 129 | + return@addOnSuccessListener | ||
| 130 | + } | ||
| 116 | 131 | ||
| 117 | - val stream = ByteArrayOutputStream() | ||
| 118 | - bmResult.compress(Bitmap.CompressFormat.PNG, 100, stream) | ||
| 119 | - val byteArray = stream.toByteArray() | ||
| 120 | - val bmWidth = bmResult.width | ||
| 121 | - val bmHeight = bmResult.height | ||
| 122 | - bmResult.recycle() | 132 | + val bitmap = Bitmap.createBitmap(mediaImage.width, mediaImage.height, Bitmap.Config.ARGB_8888) |
| 133 | + val imageFormat = YuvToRgbConverter(activity.applicationContext) | ||
| 123 | 134 | ||
| 135 | + imageFormat.yuvToRgb(mediaImage, bitmap) | ||
| 124 | 136 | ||
| 125 | - mobileScannerCallback( | ||
| 126 | - barcodeMap, | ||
| 127 | - byteArray, | ||
| 128 | - bmWidth, | ||
| 129 | - bmHeight | ||
| 130 | - ) | 137 | + val bmResult = rotateBitmap(bitmap, camera?.cameraInfo?.sensorRotationDegrees?.toFloat() ?: 90f) |
| 131 | 138 | ||
| 132 | - } else { | 139 | + val stream = ByteArrayOutputStream() |
| 140 | + bmResult.compress(Bitmap.CompressFormat.PNG, 100, stream) | ||
| 141 | + val byteArray = stream.toByteArray() | ||
| 142 | + val bmWidth = bmResult.width | ||
| 143 | + val bmHeight = bmResult.height | ||
| 144 | + bmResult.recycle() | ||
| 133 | 145 | ||
| 134 | - mobileScannerCallback( | ||
| 135 | - barcodeMap, | ||
| 136 | - null, | ||
| 137 | - null, | ||
| 138 | - null | ||
| 139 | - ) | ||
| 140 | - } | ||
| 141 | - } | ||
| 142 | - } | ||
| 143 | - .addOnFailureListener { e -> | 146 | + mobileScannerCallback( |
| 147 | + barcodeMap, | ||
| 148 | + byteArray, | ||
| 149 | + bmWidth, | ||
| 150 | + bmHeight | ||
| 151 | + ) | ||
| 152 | + }.addOnFailureListener { e -> | ||
| 144 | mobileScannerErrorCallback( | 153 | mobileScannerErrorCallback( |
| 145 | e.localizedMessage ?: e.toString() | 154 | e.localizedMessage ?: e.toString() |
| 146 | ) | 155 | ) |
| 147 | - } | ||
| 148 | - .addOnCompleteListener { imageProxy.close() } | 156 | + }.addOnCompleteListener { imageProxy.close() } |
| 157 | + } | ||
| 149 | 158 | ||
| 150 | if (detectionSpeed == DetectionSpeed.NORMAL) { | 159 | if (detectionSpeed == DetectionSpeed.NORMAL) { |
| 151 | // Set timer and continue | 160 | // Set timer and continue |
| @@ -161,7 +170,6 @@ class MobileScanner( | @@ -161,7 +170,6 @@ class MobileScanner( | ||
| 161 | return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true) | 170 | return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true) |
| 162 | } | 171 | } |
| 163 | 172 | ||
| 164 | - | ||
| 165 | // scales the scanWindow to the provided inputImage and checks if that scaled | 173 | // scales the scanWindow to the provided inputImage and checks if that scaled |
| 166 | // scanWindow contains the barcode | 174 | // scanWindow contains the barcode |
| 167 | private fun isBarcodeInScanWindow( | 175 | private fun isBarcodeInScanWindow( |
| @@ -240,11 +248,7 @@ class MobileScanner( | @@ -240,11 +248,7 @@ class MobileScanner( | ||
| 240 | } | 248 | } |
| 241 | 249 | ||
| 242 | lastScanned = null | 250 | lastScanned = null |
| 243 | - scanner = if (barcodeScannerOptions != null) { | ||
| 244 | - BarcodeScanning.getClient(barcodeScannerOptions) | ||
| 245 | - } else { | ||
| 246 | - BarcodeScanning.getClient() | ||
| 247 | - } | 251 | + scanner = barcodeScannerFactory(barcodeScannerOptions) |
| 248 | 252 | ||
| 249 | val cameraProviderFuture = ProcessCameraProvider.getInstance(activity) | 253 | val cameraProviderFuture = ProcessCameraProvider.getInstance(activity) |
| 250 | val executor = ContextCompat.getMainExecutor(activity) | 254 | val executor = ContextCompat.getMainExecutor(activity) |
| @@ -408,14 +412,27 @@ class MobileScanner( | @@ -408,14 +412,27 @@ class MobileScanner( | ||
| 408 | } | 412 | } |
| 409 | 413 | ||
| 410 | val owner = activity as LifecycleOwner | 414 | val owner = activity as LifecycleOwner |
| 411 | - camera?.cameraInfo?.torchState?.removeObservers(owner) | 415 | + // Release the camera observers first. |
| 416 | + camera?.cameraInfo?.let { | ||
| 417 | + it.torchState.removeObservers(owner) | ||
| 418 | + it.zoomState.removeObservers(owner) | ||
| 419 | + it.cameraState.removeObservers(owner) | ||
| 420 | + } | ||
| 421 | + // Unbind the camera use cases, the preview is a use case. | ||
| 422 | + // The camera will be closed when the last use case is unbound. | ||
| 412 | cameraProvider?.unbindAll() | 423 | cameraProvider?.unbindAll() |
| 413 | - textureEntry?.release() | ||
| 414 | - | 424 | + cameraProvider = null |
| 415 | camera = null | 425 | camera = null |
| 416 | preview = null | 426 | preview = null |
| 427 | + | ||
| 428 | + // Release the texture for the preview. | ||
| 429 | + textureEntry?.release() | ||
| 417 | textureEntry = null | 430 | textureEntry = null |
| 418 | - cameraProvider = null | 431 | + |
| 432 | + // Release the scanner. | ||
| 433 | + scanner?.close() | ||
| 434 | + scanner = null | ||
| 435 | + lastScanned = null | ||
| 419 | } | 436 | } |
| 420 | 437 | ||
| 421 | private fun isStopped() = camera == null && preview == null | 438 | private fun isStopped() = camera == null && preview == null |
| @@ -439,22 +456,29 @@ class MobileScanner( | @@ -439,22 +456,29 @@ class MobileScanner( | ||
| 439 | /** | 456 | /** |
| 440 | * Analyze a single image. | 457 | * Analyze a single image. |
| 441 | */ | 458 | */ |
| 442 | - fun analyzeImage(image: Uri, onSuccess: AnalyzerSuccessCallback, onError: AnalyzerErrorCallback) { | 459 | + fun analyzeImage( |
| 460 | + image: Uri, | ||
| 461 | + scannerOptions: BarcodeScannerOptions?, | ||
| 462 | + onSuccess: AnalyzerSuccessCallback, | ||
| 463 | + onError: AnalyzerErrorCallback) { | ||
| 443 | val inputImage = InputImage.fromFilePath(activity, image) | 464 | val inputImage = InputImage.fromFilePath(activity, image) |
| 444 | 465 | ||
| 445 | - scanner.process(inputImage) | ||
| 446 | - .addOnSuccessListener { barcodes -> | ||
| 447 | - val barcodeMap = barcodes.map { barcode -> barcode.data } | 466 | + // Use a short lived scanner instance, which is closed when the analysis is done. |
| 467 | + val barcodeScanner: BarcodeScanner = barcodeScannerFactory(scannerOptions) | ||
| 448 | 468 | ||
| 449 | - if (barcodeMap.isNotEmpty()) { | ||
| 450 | - onSuccess(barcodeMap) | ||
| 451 | - } else { | ||
| 452 | - onSuccess(null) | ||
| 453 | - } | ||
| 454 | - } | ||
| 455 | - .addOnFailureListener { e -> | ||
| 456 | - onError(e.localizedMessage ?: e.toString()) | 469 | + barcodeScanner.process(inputImage).addOnSuccessListener { barcodes -> |
| 470 | + val barcodeMap = barcodes.map { barcode -> barcode.data } | ||
| 471 | + | ||
| 472 | + if (barcodeMap.isEmpty()) { | ||
| 473 | + onSuccess(null) | ||
| 474 | + } else { | ||
| 475 | + onSuccess(barcodeMap) | ||
| 457 | } | 476 | } |
| 477 | + }.addOnFailureListener { e -> | ||
| 478 | + onError(e.localizedMessage ?: e.toString()) | ||
| 479 | + }.addOnCompleteListener { | ||
| 480 | + barcodeScanner.close() | ||
| 481 | + } | ||
| 458 | } | 482 | } |
| 459 | 483 | ||
| 460 | /** | 484 | /** |
| @@ -474,4 +498,14 @@ class MobileScanner( | @@ -474,4 +498,14 @@ class MobileScanner( | ||
| 474 | camera?.cameraControl?.setZoomRatio(1f) | 498 | camera?.cameraControl?.setZoomRatio(1f) |
| 475 | } | 499 | } |
| 476 | 500 | ||
| 501 | + /** | ||
| 502 | + * Dispose of this scanner instance. | ||
| 503 | + */ | ||
| 504 | + fun dispose() { | ||
| 505 | + if (isStopped()) { | ||
| 506 | + return | ||
| 507 | + } | ||
| 508 | + | ||
| 509 | + stop() // Defer to the stop method, which disposes all resources anyway. | ||
| 510 | + } | ||
| 477 | } | 511 | } |
| @@ -92,6 +92,7 @@ class MobileScannerHandler( | @@ -92,6 +92,7 @@ class MobileScannerHandler( | ||
| 92 | fun dispose(activityPluginBinding: ActivityPluginBinding) { | 92 | fun dispose(activityPluginBinding: ActivityPluginBinding) { |
| 93 | methodChannel?.setMethodCallHandler(null) | 93 | methodChannel?.setMethodCallHandler(null) |
| 94 | methodChannel = null | 94 | methodChannel = null |
| 95 | + mobileScanner?.dispose() | ||
| 95 | mobileScanner = null | 96 | mobileScanner = null |
| 96 | 97 | ||
| 97 | val listener: RequestPermissionsResultListener? = permissions.getPermissionListener() | 98 | val listener: RequestPermissionsResultListener? = permissions.getPermissionListener() |
| @@ -242,7 +243,13 @@ class MobileScannerHandler( | @@ -242,7 +243,13 @@ class MobileScannerHandler( | ||
| 242 | analyzerResult = result | 243 | analyzerResult = result |
| 243 | val uri = Uri.fromFile(File(call.arguments.toString())) | 244 | val uri = Uri.fromFile(File(call.arguments.toString())) |
| 244 | 245 | ||
| 245 | - mobileScanner!!.analyzeImage(uri, analyzeImageSuccessCallback, analyzeImageErrorCallback) | 246 | + // TODO: parse options from the method call |
| 247 | + // See https://github.com/juliansteenbakker/mobile_scanner/issues/1069 | ||
| 248 | + mobileScanner!!.analyzeImage( | ||
| 249 | + uri, | ||
| 250 | + null, | ||
| 251 | + analyzeImageSuccessCallback, | ||
| 252 | + analyzeImageErrorCallback) | ||
| 246 | } | 253 | } |
| 247 | 254 | ||
| 248 | private fun toggleTorch(result: MethodChannel.Result) { | 255 | private fun toggleTorch(result: MethodChannel.Result) { |
-
Please register or login to post a comment