@@ -13,19 +13,20 @@ import android.os.Looper
13
13
import android.util.Size
14
14
import android.view.Surface
15
15
import android.view.WindowManager
16
+ import androidx.annotation.VisibleForTesting
16
17
import androidx.camera.core.Camera
17
18
import androidx.camera.core.CameraSelector
18
19
import androidx.camera.core.ExperimentalGetImage
19
20
import androidx.camera.core.ImageAnalysis
20
21
import androidx.camera.core.ImageProxy
21
22
import androidx.camera.core.Preview
22
23
import androidx.camera.core.TorchState
23
- import androidx.camera.core.resolutionselector.AspectRatioStrategy
24
24
import androidx.camera.core.resolutionselector.ResolutionSelector
25
25
import androidx.camera.core.resolutionselector.ResolutionStrategy
26
26
import androidx.camera.lifecycle.ProcessCameraProvider
27
27
import androidx.core.content.ContextCompat
28
28
import androidx.lifecycle.LifecycleOwner
29
+ import com.google.mlkit.vision.barcode.BarcodeScanner
29
30
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
30
31
import com.google.mlkit.vision.barcode.BarcodeScanning
31
32
import com.google.mlkit.vision.barcode.common.Barcode
@@ -37,20 +38,20 @@ import io.flutter.view.TextureRegistry
37
38
import java.io.ByteArrayOutputStream
38
39
import kotlin.math.roundToInt
39
40
40
-
41
41
class MobileScanner (
42
42
private val activity : Activity ,
43
43
private val textureRegistry : TextureRegistry ,
44
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
49
// / Internal variables
49
50
private var cameraProvider: ProcessCameraProvider ? = null
50
51
private var camera: Camera ? = null
51
52
private var preview: Preview ? = null
52
53
private var textureEntry: TextureRegistry .SurfaceTextureEntry ? = null
53
- private var scanner = BarcodeScanning .getClient()
54
+ private var scanner: BarcodeScanner ? = null
54
55
private var lastScanned: List <String ?>? = null
55
56
private var scannerTimeout = false
56
57
private var displayListener: DisplayManager .DisplayListener ? = null
@@ -61,6 +62,15 @@ class MobileScanner(
61
62
private var detectionTimeout: Long = 250
62
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
75
* callback for the camera. Every frame is passed through this function.
66
76
*/
@@ -76,76 +86,75 @@ class MobileScanner(
76
86
scannerTimeout = true
77
87
}
78
88
79
- scanner.process(inputImage)
80
- .addOnSuccessListener { barcodes ->
89
+ scanner?. let {
90
+ it.process(inputImage) .addOnSuccessListener { barcodes ->
81
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
96
if (newScannedBarcodes == lastScanned) {
84
97
// New scanned is duplicate, returning
85
98
return @addOnSuccessListener
86
99
}
87
- if (newScannedBarcodes.isNotEmpty()) lastScanned = newScannedBarcodes
100
+ if (newScannedBarcodes.isNotEmpty()) {
101
+ lastScanned = newScannedBarcodes
102
+ }
88
103
}
89
104
90
105
val barcodeMap: MutableList <Map <String , Any ?>> = mutableListOf ()
91
106
92
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
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
153
mobileScannerErrorCallback(
145
154
e.localizedMessage ? : e.toString()
146
155
)
147
- }
148
- .addOnCompleteListener { imageProxy.close() }
156
+ }.addOnCompleteListener { imageProxy.close() }
157
+ }
149
158
150
159
if (detectionSpeed == DetectionSpeed .NORMAL ) {
151
160
// Set timer and continue
@@ -161,7 +170,6 @@ class MobileScanner(
161
170
return Bitmap .createBitmap(bitmap, 0 , 0 , bitmap.width, bitmap.height, matrix, true )
162
171
}
163
172
164
-
165
173
// scales the scanWindow to the provided inputImage and checks if that scaled
166
174
// scanWindow contains the barcode
167
175
private fun isBarcodeInScanWindow (
@@ -240,11 +248,7 @@ class MobileScanner(
240
248
}
241
249
242
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
253
val cameraProviderFuture = ProcessCameraProvider .getInstance(activity)
250
254
val executor = ContextCompat .getMainExecutor(activity)
@@ -408,14 +412,27 @@ class MobileScanner(
408
412
}
409
413
410
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
423
cameraProvider?.unbindAll()
413
- textureEntry?.release()
414
-
424
+ cameraProvider = null
415
425
camera = null
416
426
preview = null
427
+
428
+ // Release the texture for the preview.
429
+ textureEntry?.release()
417
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
438
private fun isStopped () = camera == null && preview == null
@@ -439,22 +456,29 @@ class MobileScanner(
439
456
/* *
440
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
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
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
}
0 commit comments