Skip to content

Commit c72d51c

Browse files
Merge pull request juliansteenbakker#1139 from navaronbracke/fix_barcode_scanner_leak
fix: fix barcode scanner leak on Android
2 parents 1a3a0eb + 66f5cee commit c72d51c

File tree

3 files changed

+118
-74
lines changed

3 files changed

+118
-74
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
## NEXT
2+
* Fixed a leak of the barcode scanner on Android.
3+
14
## 5.1.1
25
* This release fixes an issue with automatic starts in the examples.
36

android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt

Lines changed: 107 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,20 @@ import android.os.Looper
1313
import android.util.Size
1414
import android.view.Surface
1515
import android.view.WindowManager
16+
import androidx.annotation.VisibleForTesting
1617
import androidx.camera.core.Camera
1718
import androidx.camera.core.CameraSelector
1819
import androidx.camera.core.ExperimentalGetImage
1920
import androidx.camera.core.ImageAnalysis
2021
import androidx.camera.core.ImageProxy
2122
import androidx.camera.core.Preview
2223
import androidx.camera.core.TorchState
23-
import androidx.camera.core.resolutionselector.AspectRatioStrategy
2424
import androidx.camera.core.resolutionselector.ResolutionSelector
2525
import androidx.camera.core.resolutionselector.ResolutionStrategy
2626
import androidx.camera.lifecycle.ProcessCameraProvider
2727
import androidx.core.content.ContextCompat
2828
import androidx.lifecycle.LifecycleOwner
29+
import com.google.mlkit.vision.barcode.BarcodeScanner
2930
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
3031
import com.google.mlkit.vision.barcode.BarcodeScanning
3132
import com.google.mlkit.vision.barcode.common.Barcode
@@ -37,20 +38,20 @@ import io.flutter.view.TextureRegistry
3738
import java.io.ByteArrayOutputStream
3839
import kotlin.math.roundToInt
3940

40-
4141
class MobileScanner(
4242
private val activity: Activity,
4343
private val textureRegistry: TextureRegistry,
4444
private val mobileScannerCallback: MobileScannerCallback,
45-
private val mobileScannerErrorCallback: MobileScannerErrorCallback
45+
private val mobileScannerErrorCallback: MobileScannerErrorCallback,
46+
private val barcodeScannerFactory: (options: BarcodeScannerOptions?) -> BarcodeScanner = ::defaultBarcodeScannerFactory,
4647
) {
4748

4849
/// Internal variables
4950
private var cameraProvider: ProcessCameraProvider? = null
5051
private var camera: Camera? = null
5152
private var preview: Preview? = null
5253
private var textureEntry: TextureRegistry.SurfaceTextureEntry? = null
53-
private var scanner = BarcodeScanning.getClient()
54+
private var scanner: BarcodeScanner? = null
5455
private var lastScanned: List<String?>? = null
5556
private var scannerTimeout = false
5657
private var displayListener: DisplayManager.DisplayListener? = null
@@ -61,6 +62,15 @@ class MobileScanner(
6162
private var detectionTimeout: Long = 250
6263
private var returnImage = false
6364

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+
6474
/**
6575
* callback for the camera. Every frame is passed through this function.
6676
*/
@@ -76,76 +86,75 @@ class MobileScanner(
7686
scannerTimeout = true
7787
}
7888

79-
scanner.process(inputImage)
80-
.addOnSuccessListener { barcodes ->
89+
scanner?.let {
90+
it.process(inputImage).addOnSuccessListener { barcodes ->
8191
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+
8396
if (newScannedBarcodes == lastScanned) {
8497
// New scanned is duplicate, returning
8598
return@addOnSuccessListener
8699
}
87-
if (newScannedBarcodes.isNotEmpty()) lastScanned = newScannedBarcodes
100+
if (newScannedBarcodes.isNotEmpty()) {
101+
lastScanned = newScannedBarcodes
102+
}
88103
}
89104

90105
val barcodeMap: MutableList<Map<String, Any?>> = mutableListOf()
91106

92107
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) {
101109
barcodeMap.add(barcode.data)
110+
continue
102111
}
103-
}
104-
105-
106-
if (barcodeMap.isNotEmpty()) {
107-
if (returnImage) {
108112

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+
}
112117

113-
imageFormat.yuvToRgb(mediaImage, bitmap)
118+
if (barcodeMap.isEmpty()) {
119+
return@addOnSuccessListener
120+
}
114121

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+
}
116131

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)
123134

135+
imageFormat.yuvToRgb(mediaImage, bitmap)
124136

125-
mobileScannerCallback(
126-
barcodeMap,
127-
byteArray,
128-
bmWidth,
129-
bmHeight
130-
)
137+
val bmResult = rotateBitmap(bitmap, camera?.cameraInfo?.sensorRotationDegrees?.toFloat() ?: 90f)
131138

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()
133145

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 ->
144153
mobileScannerErrorCallback(
145154
e.localizedMessage ?: e.toString()
146155
)
147-
}
148-
.addOnCompleteListener { imageProxy.close() }
156+
}.addOnCompleteListener { imageProxy.close() }
157+
}
149158

150159
if (detectionSpeed == DetectionSpeed.NORMAL) {
151160
// Set timer and continue
@@ -161,7 +170,6 @@ class MobileScanner(
161170
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
162171
}
163172

164-
165173
// scales the scanWindow to the provided inputImage and checks if that scaled
166174
// scanWindow contains the barcode
167175
private fun isBarcodeInScanWindow(
@@ -240,11 +248,7 @@ class MobileScanner(
240248
}
241249

242250
lastScanned = null
243-
scanner = if (barcodeScannerOptions != null) {
244-
BarcodeScanning.getClient(barcodeScannerOptions)
245-
} else {
246-
BarcodeScanning.getClient()
247-
}
251+
scanner = barcodeScannerFactory(barcodeScannerOptions)
248252

249253
val cameraProviderFuture = ProcessCameraProvider.getInstance(activity)
250254
val executor = ContextCompat.getMainExecutor(activity)
@@ -408,14 +412,27 @@ class MobileScanner(
408412
}
409413

410414
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.
412423
cameraProvider?.unbindAll()
413-
textureEntry?.release()
414-
424+
cameraProvider = null
415425
camera = null
416426
preview = null
427+
428+
// Release the texture for the preview.
429+
textureEntry?.release()
417430
textureEntry = null
418-
cameraProvider = null
431+
432+
// Release the scanner.
433+
scanner?.close()
434+
scanner = null
435+
lastScanned = null
419436
}
420437

421438
private fun isStopped() = camera == null && preview == null
@@ -439,22 +456,29 @@ class MobileScanner(
439456
/**
440457
* Analyze a single image.
441458
*/
442-
fun analyzeImage(image: Uri, onSuccess: AnalyzerSuccessCallback, onError: AnalyzerErrorCallback) {
459+
fun analyzeImage(
460+
image: Uri,
461+
scannerOptions: BarcodeScannerOptions?,
462+
onSuccess: AnalyzerSuccessCallback,
463+
onError: AnalyzerErrorCallback) {
443464
val inputImage = InputImage.fromFilePath(activity, image)
444465

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)
448468

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)
457476
}
477+
}.addOnFailureListener { e ->
478+
onError(e.localizedMessage ?: e.toString())
479+
}.addOnCompleteListener {
480+
barcodeScanner.close()
481+
}
458482
}
459483

460484
/**
@@ -474,4 +498,14 @@ class MobileScanner(
474498
camera?.cameraControl?.setZoomRatio(1f)
475499
}
476500

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+
}
477511
}

android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerHandler.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ class MobileScannerHandler(
9292
fun dispose(activityPluginBinding: ActivityPluginBinding) {
9393
methodChannel?.setMethodCallHandler(null)
9494
methodChannel = null
95+
mobileScanner?.dispose()
9596
mobileScanner = null
9697

9798
val listener: RequestPermissionsResultListener? = permissions.getPermissionListener()
@@ -242,7 +243,13 @@ class MobileScannerHandler(
242243
analyzerResult = result
243244
val uri = Uri.fromFile(File(call.arguments.toString()))
244245

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)
246253
}
247254

248255
private fun toggleTorch(result: MethodChannel.Result) {

0 commit comments

Comments
 (0)