Julian Steenbakker
Committed by GitHub

Merge branch 'master' into pause_function

@@ -36,7 +36,7 @@ jobs: @@ -36,7 +36,7 @@ jobs:
36 - uses: subosito/flutter-action@v2.12.0 36 - uses: subosito/flutter-action@v2.12.0
37 with: 37 with:
38 cache: true 38 cache: true
39 - flutter-version: '3.19' 39 + flutter-version: '3.22'
40 channel: 'stable' 40 channel: 'stable'
41 - name: Version 41 - name: Version
42 run: flutter doctor -v 42 run: flutter doctor -v
  1 +## NEXT
  2 +* This release requires Flutter 3.22.0 and Dart 3.4.
  3 +
  4 +* [Android] Fixed a leak of the barcode scanner.
  5 +* [Android] Fixed a crash when encountering invalid numbers for the scan window.
  6 +* [Web] Migrates `package:web` to 1.0.0.
  7 +
1 ## 5.1.1 8 ## 5.1.1
2 * This release fixes an issue with automatic starts in the examples. 9 * This release fixes an issue with automatic starts in the examples.
3 10
@@ -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 112
106 - if (barcodeMap.isNotEmpty()) {  
107 - if (returnImage) {  
108 -  
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,26 +170,35 @@ class MobileScanner( @@ -161,26 +170,35 @@ 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  
166 - // scanWindow contains the barcode  
167 - private fun isBarcodeInScanWindow( 173 + // Scales the scanWindow to the provided inputImage and checks if that scaled
  174 + // scanWindow contains the barcode.
  175 + @VisibleForTesting
  176 + fun isBarcodeInScanWindow(
168 scanWindow: List<Float>, 177 scanWindow: List<Float>,
169 barcode: Barcode, 178 barcode: Barcode,
170 inputImage: ImageProxy 179 inputImage: ImageProxy
171 ): Boolean { 180 ): Boolean {
  181 + // TODO: use `cornerPoints` instead, since the bounding box is not bound to the coordinate system of the input image
  182 + // On iOS we do this correctly, so the calculation should match that.
172 val barcodeBoundingBox = barcode.boundingBox ?: return false 183 val barcodeBoundingBox = barcode.boundingBox ?: return false
173 184
174 - val imageWidth = inputImage.height  
175 - val imageHeight = inputImage.width 185 + try {
  186 + val imageWidth = inputImage.height
  187 + val imageHeight = inputImage.width
176 188
177 - val left = (scanWindow[0] * imageWidth).roundToInt()  
178 - val top = (scanWindow[1] * imageHeight).roundToInt()  
179 - val right = (scanWindow[2] * imageWidth).roundToInt()  
180 - val bottom = (scanWindow[3] * imageHeight).roundToInt() 189 + val left = (scanWindow[0] * imageWidth).roundToInt()
  190 + val top = (scanWindow[1] * imageHeight).roundToInt()
  191 + val right = (scanWindow[2] * imageWidth).roundToInt()
  192 + val bottom = (scanWindow[3] * imageHeight).roundToInt()
181 193
182 - val scaledScanWindow = Rect(left, top, right, bottom)  
183 - return scaledScanWindow.contains(barcodeBoundingBox) 194 + val scaledScanWindow = Rect(left, top, right, bottom)
  195 +
  196 + return scaledScanWindow.contains(barcodeBoundingBox)
  197 + } catch (exception: IllegalArgumentException) {
  198 + // Rounding of the scan window dimensions can fail, due to encountering NaN.
  199 + // If we get NaN, rather than give a false positive, just return false.
  200 + return false
  201 + }
184 } 202 }
185 203
186 // Return the best resolution for the actual device orientation. 204 // Return the best resolution for the actual device orientation.
@@ -240,11 +258,7 @@ class MobileScanner( @@ -240,11 +258,7 @@ class MobileScanner(
240 } 258 }
241 259
242 lastScanned = null 260 lastScanned = null
243 - scanner = if (barcodeScannerOptions != null) {  
244 - BarcodeScanning.getClient(barcodeScannerOptions)  
245 - } else {  
246 - BarcodeScanning.getClient()  
247 - } 261 + scanner = barcodeScannerFactory(barcodeScannerOptions)
248 262
249 val cameraProviderFuture = ProcessCameraProvider.getInstance(activity) 263 val cameraProviderFuture = ProcessCameraProvider.getInstance(activity)
250 val executor = ContextCompat.getMainExecutor(activity) 264 val executor = ContextCompat.getMainExecutor(activity)
@@ -427,12 +441,24 @@ class MobileScanner( @@ -427,12 +441,24 @@ class MobileScanner(
427 } 441 }
428 442
429 val owner = activity as LifecycleOwner 443 val owner = activity as LifecycleOwner
430 - camera?.cameraInfo?.torchState?.removeObservers(owner) 444 + // Release the camera observers first.
  445 + camera?.cameraInfo?.let {
  446 + it.torchState.removeObservers(owner)
  447 + it.zoomState.removeObservers(owner)
  448 + it.cameraState.removeObservers(owner)
  449 + }
  450 + // Unbind the camera use cases, the preview is a use case.
  451 + // The camera will be closed when the last use case is unbound.
431 cameraProvider?.unbindAll() 452 cameraProvider?.unbindAll()
432 453
433 - camera = null  
434 - preview = null  
435 - cameraProvider = null 454 + // Release the texture for the preview.
  455 + textureEntry?.release()
  456 + textureEntry = null
  457 +
  458 + // Release the scanner.
  459 + scanner?.close()
  460 + scanner = null
  461 + lastScanned = null
436 } 462 }
437 463
438 private fun releaseTexture() { 464 private fun releaseTexture() {
@@ -462,22 +488,29 @@ class MobileScanner( @@ -462,22 +488,29 @@ class MobileScanner(
462 /** 488 /**
463 * Analyze a single image. 489 * Analyze a single image.
464 */ 490 */
465 - fun analyzeImage(image: Uri, onSuccess: AnalyzerSuccessCallback, onError: AnalyzerErrorCallback) { 491 + fun analyzeImage(
  492 + image: Uri,
  493 + scannerOptions: BarcodeScannerOptions?,
  494 + onSuccess: AnalyzerSuccessCallback,
  495 + onError: AnalyzerErrorCallback) {
466 val inputImage = InputImage.fromFilePath(activity, image) 496 val inputImage = InputImage.fromFilePath(activity, image)
467 497
468 - scanner.process(inputImage)  
469 - .addOnSuccessListener { barcodes ->  
470 - val barcodeMap = barcodes.map { barcode -> barcode.data } 498 + // Use a short lived scanner instance, which is closed when the analysis is done.
  499 + val barcodeScanner: BarcodeScanner = barcodeScannerFactory(scannerOptions)
471 500
472 - if (barcodeMap.isNotEmpty()) {  
473 - onSuccess(barcodeMap)  
474 - } else {  
475 - onSuccess(null)  
476 - }  
477 - }  
478 - .addOnFailureListener { e ->  
479 - onError(e.localizedMessage ?: e.toString()) 501 + barcodeScanner.process(inputImage).addOnSuccessListener { barcodes ->
  502 + val barcodeMap = barcodes.map { barcode -> barcode.data }
  503 +
  504 + if (barcodeMap.isEmpty()) {
  505 + onSuccess(null)
  506 + } else {
  507 + onSuccess(barcodeMap)
480 } 508 }
  509 + }.addOnFailureListener { e ->
  510 + onError(e.localizedMessage ?: e.toString())
  511 + }.addOnCompleteListener {
  512 + barcodeScanner.close()
  513 + }
481 } 514 }
482 515
483 /** 516 /**
@@ -497,4 +530,14 @@ class MobileScanner( @@ -497,4 +530,14 @@ class MobileScanner(
497 camera?.cameraControl?.setZoomRatio(1f) 530 camera?.cameraControl?.setZoomRatio(1f)
498 } 531 }
499 532
  533 + /**
  534 + * Dispose of this scanner instance.
  535 + */
  536 + fun dispose() {
  537 + if (isStopped()) {
  538 + return
  539 + }
  540 +
  541 + stop() // Defer to the stop method, which disposes all resources anyway.
  542 + }
500 } 543 }
@@ -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()
@@ -255,7 +256,13 @@ class MobileScannerHandler( @@ -255,7 +256,13 @@ class MobileScannerHandler(
255 analyzerResult = result 256 analyzerResult = result
256 val uri = Uri.fromFile(File(call.arguments.toString())) 257 val uri = Uri.fromFile(File(call.arguments.toString()))
257 258
258 - mobileScanner!!.analyzeImage(uri, analyzeImageSuccessCallback, analyzeImageErrorCallback) 259 + // TODO: parse options from the method call
  260 + // See https://github.com/juliansteenbakker/mobile_scanner/issues/1069
  261 + mobileScanner!!.analyzeImage(
  262 + uri,
  263 + null,
  264 + analyzeImageSuccessCallback,
  265 + analyzeImageErrorCallback)
259 } 266 }
260 267
261 private fun toggleTorch(result: MethodChannel.Result) { 268 private fun toggleTorch(result: MethodChannel.Result) {
  1 +package dev.steenbakker.mobile_scanner
  2 +
  3 +import android.app.Activity
  4 +import android.graphics.Rect
  5 +import androidx.camera.core.ImageProxy
  6 +import com.google.mlkit.vision.barcode.BarcodeScanner
  7 +import com.google.mlkit.vision.barcode.BarcodeScannerOptions
  8 +import com.google.mlkit.vision.barcode.common.Barcode
  9 +import kotlin.test.Test
  10 +import org.mockito.Mockito
  11 +import io.flutter.view.TextureRegistry
  12 +import kotlin.test.expect
  13 +
  14 +/*
  15 + * This demonstrates a simple unit test of the Kotlin portion of this plugin's implementation.
  16 + *
  17 + * Once you have built the plugin's example app, you can run these tests from the command
  18 + * line by running `./gradlew testDebugUnitTest` in the `example/android/` directory, or
  19 + * you can run them directly from IDEs that support JUnit such as Android Studio.
  20 + */
  21 +
  22 +internal class MobileScannerTest {
  23 + @Test
  24 + fun isBarcodeInScanWindow_canHandleNaNValues() {
  25 + val barcodeScannerMock = Mockito.mock(BarcodeScanner::class.java)
  26 +
  27 + val mobileScanner = MobileScanner(
  28 + Mockito.mock(Activity::class.java),
  29 + Mockito.mock(TextureRegistry::class.java),
  30 + { _: List<Map<String, Any?>>, _: ByteArray?, _: Int?, _: Int? -> },
  31 + { _: String -> },
  32 + { _: BarcodeScannerOptions? -> barcodeScannerMock }
  33 + )
  34 +
  35 + // Intentional suppression for the mock value in the test,
  36 + // since there is no NaN constant.
  37 + @Suppress("DIVISION_BY_ZERO")
  38 + val notANumber = 0.0f / 0.0f
  39 +
  40 + val barcodeMock: Barcode = Mockito.mock(Barcode::class.java)
  41 + val imageMock: ImageProxy = Mockito.mock(ImageProxy::class.java)
  42 +
  43 + // TODO: use corner points instead of bounding box
  44 +
  45 + // Bounding box that is 100 pixels offset from the left and top,
  46 + // and is 100 pixels in width and height.
  47 + Mockito.`when`(barcodeMock.boundingBox).thenReturn(
  48 + Rect(100, 100, 200, 300))
  49 + Mockito.`when`(imageMock.height).thenReturn(400)
  50 + Mockito.`when`(imageMock.width).thenReturn(400)
  51 +
  52 + // Use a scan window that has an invalid value, but otherwise uses the entire image.
  53 + val scanWindow: List<Float> = listOf(0f, notANumber, 100f, 100f)
  54 +
  55 + expect(false) {
  56 + mobileScanner.isBarcodeInScanWindow(scanWindow, barcodeMock, imageMock)
  57 + }
  58 + }
  59 +}
@@ -5,7 +5,7 @@ Demonstrates how to use the mobile_scanner plugin. @@ -5,7 +5,7 @@ Demonstrates how to use the mobile_scanner plugin.
5 ## Run Examples 5 ## Run Examples
6 6
7 1. `git clone https://github.com/juliansteenbakker/mobile_scanner.git` 7 1. `git clone https://github.com/juliansteenbakker/mobile_scanner.git`
8 -2. `cd mobile_scanner/examples/lib` 8 +2. `cd mobile_scanner/example/lib`
9 3. `flutter pub get` 9 3. `flutter pub get`
10 4. `flutter run` 10 4. `flutter run`
11 11
@@ -31,29 +31,8 @@ @@ -31,29 +31,8 @@
31 31
32 <title>mobile_scanner_example</title> 32 <title>mobile_scanner_example</title>
33 <link rel="manifest" href="manifest.json"> 33 <link rel="manifest" href="manifest.json">
34 -  
35 - <script>  
36 - // The value below is injected by flutter build, do not touch.  
37 - const serviceWorkerVersion = null;  
38 - </script>  
39 - <!-- This script adds the flutter initialization JS code -->  
40 - <script src="flutter.js" defer></script>  
41 </head> 34 </head>
42 <body> 35 <body>
43 - <script>  
44 - window.addEventListener('load', function(ev) {  
45 - // Download main.dart.js  
46 - _flutter.loader.loadEntrypoint({  
47 - serviceWorker: {  
48 - serviceWorkerVersion: serviceWorkerVersion,  
49 - },  
50 - onEntrypointLoaded: function(engineInitializer) {  
51 - engineInitializer.initializeEngine().then(function(appRunner) {  
52 - appRunner.runApp();  
53 - });  
54 - }  
55 - });  
56 - });  
57 - </script> 36 + <script src="flutter_bootstrap.js" async></script>
58 </body> 37 </body>
59 </html> 38 </html>
@@ -292,6 +292,7 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { @@ -292,6 +292,7 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
292 formats: formats, 292 formats: formats,
293 returnImage: returnImage, 293 returnImage: returnImage,
294 torchEnabled: torchEnabled, 294 torchEnabled: torchEnabled,
  295 + useNewCameraSelector: useNewCameraSelector,
295 ); 296 );
296 297
297 try { 298 try {
@@ -14,6 +14,7 @@ class StartOptions { @@ -14,6 +14,7 @@ class StartOptions {
14 required this.formats, 14 required this.formats,
15 required this.returnImage, 15 required this.returnImage,
16 required this.torchEnabled, 16 required this.torchEnabled,
  17 + required this.useNewCameraSelector,
17 }); 18 });
18 19
19 /// The direction for the camera. 20 /// The direction for the camera.
@@ -37,6 +38,11 @@ class StartOptions { @@ -37,6 +38,11 @@ class StartOptions {
37 /// Whether the torch should be turned on when the scanner starts. 38 /// Whether the torch should be turned on when the scanner starts.
38 final bool torchEnabled; 39 final bool torchEnabled;
39 40
  41 + /// Whether the new resolution selector should be used.
  42 + ///
  43 + /// This option is only supported on Android. Other platforms will ignore this option.
  44 + final bool useNewCameraSelector;
  45 +
40 Map<String, Object?> toMap() { 46 Map<String, Object?> toMap() {
41 return <String, Object?>{ 47 return <String, Object?>{
42 if (cameraResolution != null) 48 if (cameraResolution != null)
@@ -51,6 +57,7 @@ class StartOptions { @@ -51,6 +57,7 @@ class StartOptions {
51 'speed': detectionSpeed.rawValue, 57 'speed': detectionSpeed.rawValue,
52 'timeout': detectionTimeoutMs, 58 'timeout': detectionTimeoutMs,
53 'torch': torchEnabled, 59 'torch': torchEnabled,
  60 + 'useNewCameraSelector': useNewCameraSelector,
54 }; 61 };
55 } 62 }
56 } 63 }
@@ -16,8 +16,8 @@ screenshots: @@ -16,8 +16,8 @@ screenshots:
16 path: example/screenshots/overlay.png 16 path: example/screenshots/overlay.png
17 17
18 environment: 18 environment:
19 - sdk: ">=3.3.0 <4.0.0"  
20 - flutter: ">=3.19.0" 19 + sdk: ">=3.4.0 <4.0.0"
  20 + flutter: ">=3.22.0"
21 21
22 dependencies: 22 dependencies:
23 flutter: 23 flutter:
@@ -25,7 +25,7 @@ dependencies: @@ -25,7 +25,7 @@ dependencies:
25 flutter_web_plugins: 25 flutter_web_plugins:
26 sdk: flutter 26 sdk: flutter
27 plugin_platform_interface: ^2.0.2 27 plugin_platform_interface: ^2.0.2
28 - web: ^0.5.1 28 + web: ^1.0.0
29 29
30 dev_dependencies: 30 dev_dependencies:
31 flutter_test: 31 flutter_test: