Sander Roest

Merge branch 'master' into picklist_poc

Showing 46 changed files with 529 additions and 239 deletions
1 -## NEXT 1 +## 6.0.2
2 2
  3 +Bugs fixed:
  4 +* Fixed a bug that prevented `analyzeImage` from actually accepting the configured formats.
  5 +
  6 +Improvements:
  7 +* [iOS] Excluded the `arm64` architecture for Simulators, which is unsupported by MLKit 7.0.0.
  8 +
  9 +## 6.0.1
  10 +
  11 +Bugs fixed:
  12 +* Fixed a bug that would cause onDetect to not handle errors.
  13 +
  14 +Improvements:
  15 +* [iOS] Excluded the `armv7` architecture, which is unsupported by MLKit 7.0.0.
  16 +* Added a new `onDetectError` error handler to the `MobileScanner` widget, for use with `onDetect`.
  17 +
  18 +## 6.0.0
  19 +
  20 +**BREAKING CHANGES:**
  21 +
  22 +* [iOS] iOS 15.5.0 is now the minimum supported iOS version.
  23 +* [iOS] Updates MLKit to version 7.0.0.
  24 +* [iOS] Updates the minimum supported XCode version to 15.3.0.
  25 +
  26 +Improvements:
3 * [MacOS] Added the corners and size information to barcode results. 27 * [MacOS] Added the corners and size information to barcode results.
4 * [MacOS] Added support for `analyzeImage`. 28 * [MacOS] Added support for `analyzeImage`.
  29 +* [MacOS] Added a Privacy Manifest.
5 * [web] Added the size information to barcode results. 30 * [web] Added the size information to barcode results.
  31 +* [web] Added the video output size information to barcode capture.
6 * Added support for barcode formats to image analysis. 32 * Added support for barcode formats to image analysis.
  33 +* Updated the scanner to report any scanning errors that were encountered during processing.
  34 +* Introduced a new getter `hasCameraPermission` for the `MobileScannerState`.
  35 +* Fixed a bug in the lifecycle handling sample. Now instead of checking `isInitialized`,
  36 +the sample recommends using `hasCameraPermission`, which also guards against camera permission errors.
  37 +* Updated the behavior of `returnImage` to only determine if the camera output bytes should be sent.
  38 +* Updated the behavior of `BarcodeCapture.size` to always be provided when available, regardless of `returnImage`.
  39 +
  40 +Bugs fixed:
  41 +* Fixed a bug that would cause the scanner to emit an error when it was already started. Now it ignores any calls to start while it is starting.
  42 +* [MacOS] Fixed a bug that prevented the `anaylzeImage()` sample from working properly.
7 43
8 ## 5.2.3 44 ## 5.2.3
9 45
@@ -60,6 +60,10 @@ dev.steenbakker.mobile_scanner.useUnbundled=true @@ -60,6 +60,10 @@ dev.steenbakker.mobile_scanner.useUnbundled=true
60 ``` 60 ```
61 61
62 ### iOS 62 ### iOS
  63 +
  64 +_iOS arm64 Simulators are currently not yet supported, until the migration to the Vision API is complete._
  65 +_See_ https://github.com/juliansteenbakker/mobile_scanner/issues/1225
  66 +
63 **Add the following keys to your Info.plist file, located in <project root>/ios/Runner/Info.plist:** 67 **Add the following keys to your Info.plist file, located in <project root>/ios/Runner/Info.plist:**
64 NSCameraUsageDescription - describe why your app needs access to the camera. This is called Privacy - Camera Usage Description in the visual editor. 68 NSCameraUsageDescription - describe why your app needs access to the camera. This is called Privacy - Camera Usage Description in the visual editor.
65 69
@@ -127,7 +131,7 @@ class MyState extends State<MyStatefulWidget> with WidgetsBindingObserver { @@ -127,7 +131,7 @@ class MyState extends State<MyStatefulWidget> with WidgetsBindingObserver {
127 void didChangeAppLifecycleState(AppLifecycleState state) { 131 void didChangeAppLifecycleState(AppLifecycleState state) {
128 // If the controller is not ready, do not try to start or stop it. 132 // If the controller is not ready, do not try to start or stop it.
129 // Permission dialogs can trigger lifecycle changes before the controller is ready. 133 // Permission dialogs can trigger lifecycle changes before the controller is ready.
130 - if (!controller.value.isInitialized) { 134 + if (!controller.value.hasCameraPermission) {
131 return; 135 return;
132 } 136 }
133 137
@@ -5,3 +5,4 @@ linter: @@ -5,3 +5,4 @@ linter:
5 - combinators_ordering 5 - combinators_ordering
6 - require_trailing_commas 6 - require_trailing_commas
7 - unnecessary_library_directive 7 - unnecessary_library_directive
  8 + - prefer_single_quotes
@@ -18,6 +18,12 @@ class BarcodeHandler(binaryMessenger: BinaryMessenger) : EventChannel.StreamHand @@ -18,6 +18,12 @@ class BarcodeHandler(binaryMessenger: BinaryMessenger) : EventChannel.StreamHand
18 eventChannel.setStreamHandler(this) 18 eventChannel.setStreamHandler(this)
19 } 19 }
20 20
  21 + fun publishError(errorCode: String, errorMessage: String, errorDetails: Any?) {
  22 + Handler(Looper.getMainLooper()).post {
  23 + eventSink?.error(errorCode, errorMessage, errorDetails)
  24 + }
  25 + }
  26 +
21 fun publishEvent(event: Map<String, Any>) { 27 fun publishEvent(event: Map<String, Any>) {
22 Handler(Looper.getMainLooper()).post { 28 Handler(Looper.getMainLooper()).post {
23 eventSink?.success(event) 29 eventSink?.success(event)
@@ -123,9 +123,8 @@ class MobileScanner( @@ -123,9 +123,8 @@ class MobileScanner(
123 mobileScannerCallback( 123 mobileScannerCallback(
124 barcodeMap, 124 barcodeMap,
125 null, 125 null,
126 - null,  
127 - null  
128 - ) 126 + mediaImage.width,
  127 + mediaImage.height)
129 return@addOnSuccessListener 128 return@addOnSuccessListener
130 } 129 }
131 130
@@ -10,6 +10,7 @@ import androidx.camera.core.ExperimentalGetImage @@ -10,6 +10,7 @@ import androidx.camera.core.ExperimentalGetImage
10 import com.google.mlkit.vision.barcode.BarcodeScannerOptions 10 import com.google.mlkit.vision.barcode.BarcodeScannerOptions
11 import dev.steenbakker.mobile_scanner.objects.BarcodeFormats 11 import dev.steenbakker.mobile_scanner.objects.BarcodeFormats
12 import dev.steenbakker.mobile_scanner.objects.DetectionSpeed 12 import dev.steenbakker.mobile_scanner.objects.DetectionSpeed
  13 +import dev.steenbakker.mobile_scanner.objects.MobileScannerErrorCodes
13 import io.flutter.plugin.common.BinaryMessenger 14 import io.flutter.plugin.common.BinaryMessenger
14 import io.flutter.plugin.common.MethodCall 15 import io.flutter.plugin.common.MethodCall
15 import io.flutter.plugin.common.MethodChannel 16 import io.flutter.plugin.common.MethodChannel
@@ -28,7 +29,7 @@ class MobileScannerHandler( @@ -28,7 +29,7 @@ class MobileScannerHandler(
28 29
29 private val analyzeImageErrorCallback: AnalyzerErrorCallback = { 30 private val analyzeImageErrorCallback: AnalyzerErrorCallback = {
30 Handler(Looper.getMainLooper()).post { 31 Handler(Looper.getMainLooper()).post {
31 - analyzerResult?.error("MobileScanner", it, null) 32 + analyzerResult?.error(MobileScannerErrorCodes.BARCODE_ERROR, it, null)
32 analyzerResult = null 33 analyzerResult = null
33 } 34 }
34 } 35 }
@@ -46,29 +47,21 @@ class MobileScannerHandler( @@ -46,29 +47,21 @@ class MobileScannerHandler(
46 private var analyzerResult: MethodChannel.Result? = null 47 private var analyzerResult: MethodChannel.Result? = null
47 48
48 private val callback: MobileScannerCallback = { barcodes: List<Map<String, Any?>>, image: ByteArray?, width: Int?, height: Int? -> 49 private val callback: MobileScannerCallback = { barcodes: List<Map<String, Any?>>, image: ByteArray?, width: Int?, height: Int? ->
49 - if (image != null) {  
50 barcodeHandler.publishEvent(mapOf( 50 barcodeHandler.publishEvent(mapOf(
51 "name" to "barcode", 51 "name" to "barcode",
52 "data" to barcodes, 52 "data" to barcodes,
  53 + // The image dimensions are always provided.
  54 + // The image bytes are only non-null when `returnImage` is true.
53 "image" to mapOf( 55 "image" to mapOf(
54 "bytes" to image, 56 "bytes" to image,
55 "width" to width?.toDouble(), 57 "width" to width?.toDouble(),
56 "height" to height?.toDouble(), 58 "height" to height?.toDouble(),
57 ) 59 )
58 )) 60 ))
59 - } else {  
60 - barcodeHandler.publishEvent(mapOf(  
61 - "name" to "barcode",  
62 - "data" to barcodes  
63 - ))  
64 - }  
65 } 61 }
66 62
67 private val errorCallback: MobileScannerErrorCallback = {error: String -> 63 private val errorCallback: MobileScannerErrorCallback = {error: String ->
68 - barcodeHandler.publishEvent(mapOf(  
69 - "name" to "error",  
70 - "data" to error,  
71 - )) 64 + barcodeHandler.publishError(MobileScannerErrorCodes.BARCODE_ERROR, error, null)
72 } 65 }
73 66
74 private var methodChannel: MethodChannel? = null 67 private var methodChannel: MethodChannel? = null
@@ -106,21 +99,21 @@ class MobileScannerHandler( @@ -106,21 +99,21 @@ class MobileScannerHandler(
106 99
107 @ExperimentalGetImage 100 @ExperimentalGetImage
108 override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { 101 override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
109 - if (mobileScanner == null) {  
110 - result.error("MobileScanner", "Called ${call.method} before initializing.", null)  
111 - return  
112 - }  
113 when (call.method) { 102 when (call.method) {
114 "state" -> result.success(permissions.hasCameraPermission(activity)) 103 "state" -> result.success(permissions.hasCameraPermission(activity))
115 "request" -> permissions.requestPermission( 104 "request" -> permissions.requestPermission(
116 activity, 105 activity,
117 addPermissionListener, 106 addPermissionListener,
118 object: MobileScannerPermissions.ResultCallback { 107 object: MobileScannerPermissions.ResultCallback {
119 - override fun onResult(errorCode: String?, errorDescription: String?) { 108 + override fun onResult(errorCode: String?) {
120 when(errorCode) { 109 when(errorCode) {
121 null -> result.success(true) 110 null -> result.success(true)
122 - MobileScannerPermissions.CAMERA_ACCESS_DENIED -> result.success(false)  
123 - else -> result.error(errorCode, errorDescription, null) 111 + MobileScannerErrorCodes.CAMERA_ACCESS_DENIED -> result.success(false)
  112 + MobileScannerErrorCodes.CAMERA_PERMISSIONS_REQUEST_ONGOING -> result.error(
  113 + MobileScannerErrorCodes.CAMERA_PERMISSIONS_REQUEST_ONGOING,
  114 + MobileScannerErrorCodes.CAMERA_PERMISSIONS_REQUEST_ONGOING_MESSAGE, null)
  115 + else -> result.error(
  116 + MobileScannerErrorCodes.GENERIC_ERROR, MobileScannerErrorCodes.GENERIC_ERROR_MESSAGE, null)
124 } 117 }
125 } 118 }
126 }) 119 })
@@ -185,29 +178,29 @@ class MobileScannerHandler( @@ -185,29 +178,29 @@ class MobileScannerHandler(
185 when (it) { 178 when (it) {
186 is AlreadyStarted -> { 179 is AlreadyStarted -> {
187 result.error( 180 result.error(
188 - "MobileScanner",  
189 - "Called start() while already started", 181 + MobileScannerErrorCodes.ALREADY_STARTED_ERROR,
  182 + MobileScannerErrorCodes.ALREADY_STARTED_ERROR_MESSAGE,
190 null 183 null
191 ) 184 )
192 } 185 }
193 is CameraError -> { 186 is CameraError -> {
194 result.error( 187 result.error(
195 - "MobileScanner",  
196 - "Error occurred when setting up camera!", 188 + MobileScannerErrorCodes.CAMERA_ERROR,
  189 + MobileScannerErrorCodes.CAMERA_ERROR_MESSAGE,
197 null 190 null
198 ) 191 )
199 } 192 }
200 is NoCamera -> { 193 is NoCamera -> {
201 result.error( 194 result.error(
202 - "MobileScanner",  
203 - "No camera found or failed to open camera!", 195 + MobileScannerErrorCodes.NO_CAMERA_ERROR,
  196 + MobileScannerErrorCodes.NO_CAMERA_ERROR_MESSAGE,
204 null 197 null
205 ) 198 )
206 } 199 }
207 else -> { 200 else -> {
208 result.error( 201 result.error(
209 - "MobileScanner",  
210 - "Unknown error occurred.", 202 + MobileScannerErrorCodes.GENERIC_ERROR,
  203 + MobileScannerErrorCodes.GENERIC_ERROR_MESSAGE,
211 null 204 null
212 ) 205 )
213 } 206 }
@@ -252,9 +245,11 @@ class MobileScannerHandler( @@ -252,9 +245,11 @@ class MobileScannerHandler(
252 mobileScanner!!.setScale(call.arguments as Double) 245 mobileScanner!!.setScale(call.arguments as Double)
253 result.success(null) 246 result.success(null)
254 } catch (e: ZoomWhenStopped) { 247 } catch (e: ZoomWhenStopped) {
255 - result.error("MobileScanner", "Called setScale() while stopped!", null) 248 + result.error(
  249 + MobileScannerErrorCodes.SET_SCALE_WHEN_STOPPED_ERROR, MobileScannerErrorCodes.SET_SCALE_WHEN_STOPPED_ERROR_MESSAGE, null)
256 } catch (e: ZoomNotInRange) { 250 } catch (e: ZoomNotInRange) {
257 - result.error("MobileScanner", "Scale should be within 0 and 1", null) 251 + result.error(
  252 + MobileScannerErrorCodes.GENERIC_ERROR, MobileScannerErrorCodes.INVALID_ZOOM_SCALE_ERROR_MESSAGE, null)
258 } 253 }
259 } 254 }
260 255
@@ -263,7 +258,8 @@ class MobileScannerHandler( @@ -263,7 +258,8 @@ class MobileScannerHandler(
263 mobileScanner!!.resetScale() 258 mobileScanner!!.resetScale()
264 result.success(null) 259 result.success(null)
265 } catch (e: ZoomWhenStopped) { 260 } catch (e: ZoomWhenStopped) {
266 - result.error("MobileScanner", "Called resetScale() while stopped!", null) 261 + result.error(
  262 + MobileScannerErrorCodes.SET_SCALE_WHEN_STOPPED_ERROR, MobileScannerErrorCodes.SET_SCALE_WHEN_STOPPED_ERROR_MESSAGE, null)
267 } 263 }
268 } 264 }
269 265
@@ -5,6 +5,7 @@ import android.app.Activity @@ -5,6 +5,7 @@ import android.app.Activity
5 import android.content.pm.PackageManager 5 import android.content.pm.PackageManager
6 import androidx.core.app.ActivityCompat 6 import androidx.core.app.ActivityCompat
7 import androidx.core.content.ContextCompat 7 import androidx.core.content.ContextCompat
  8 +import dev.steenbakker.mobile_scanner.objects.MobileScannerErrorCodes
8 import io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener 9 import io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener
9 10
10 /** 11 /**
@@ -12,11 +13,6 @@ import io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener @@ -12,11 +13,6 @@ import io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener
12 */ 13 */
13 class MobileScannerPermissions { 14 class MobileScannerPermissions {
14 companion object { 15 companion object {
15 - const val CAMERA_ACCESS_DENIED = "CameraAccessDenied"  
16 - const val CAMERA_ACCESS_DENIED_MESSAGE = "Camera access permission was denied."  
17 - const val CAMERA_PERMISSIONS_REQUEST_ONGOING = "CameraPermissionsRequestOngoing"  
18 - const val CAMERA_PERMISSIONS_REQUEST_ONGOING_MESSAGE = "Another request is ongoing and multiple requests cannot be handled at once."  
19 -  
20 /** 16 /**
21 * When the application's activity is [androidx.fragment.app.FragmentActivity], requestCode can only use the lower 16 bits. 17 * When the application's activity is [androidx.fragment.app.FragmentActivity], requestCode can only use the lower 16 bits.
22 * @see androidx.fragment.app.FragmentActivity.validateRequestPermissionsRequestCode 18 * @see androidx.fragment.app.FragmentActivity.validateRequestPermissionsRequestCode
@@ -25,7 +21,7 @@ class MobileScannerPermissions { @@ -25,7 +21,7 @@ class MobileScannerPermissions {
25 } 21 }
26 22
27 interface ResultCallback { 23 interface ResultCallback {
28 - fun onResult(errorCode: String?, errorDescription: String?) 24 + fun onResult(errorCode: String?)
29 } 25 }
30 26
31 private var listener: RequestPermissionsResultListener? = null 27 private var listener: RequestPermissionsResultListener? = null
@@ -53,14 +49,13 @@ class MobileScannerPermissions { @@ -53,14 +49,13 @@ class MobileScannerPermissions {
53 addPermissionListener: (RequestPermissionsResultListener) -> Unit, 49 addPermissionListener: (RequestPermissionsResultListener) -> Unit,
54 callback: ResultCallback) { 50 callback: ResultCallback) {
55 if (ongoing) { 51 if (ongoing) {
56 - callback.onResult(  
57 - CAMERA_PERMISSIONS_REQUEST_ONGOING, CAMERA_PERMISSIONS_REQUEST_ONGOING_MESSAGE) 52 + callback.onResult(MobileScannerErrorCodes.CAMERA_PERMISSIONS_REQUEST_ONGOING)
58 return 53 return
59 } 54 }
60 55
61 if(hasCameraPermission(activity) == 1) { 56 if(hasCameraPermission(activity) == 1) {
62 // Permissions already exist. Call the callback with success. 57 // Permissions already exist. Call the callback with success.
63 - callback.onResult(null, null) 58 + callback.onResult(null)
64 return 59 return
65 } 60 }
66 61
@@ -68,10 +63,10 @@ class MobileScannerPermissions { @@ -68,10 +63,10 @@ class MobileScannerPermissions {
68 // Keep track of the listener, so that it can be unregistered later. 63 // Keep track of the listener, so that it can be unregistered later.
69 listener = MobileScannerPermissionsListener( 64 listener = MobileScannerPermissionsListener(
70 object: ResultCallback { 65 object: ResultCallback {
71 - override fun onResult(errorCode: String?, errorDescription: String?) { 66 + override fun onResult(errorCode: String?) {
72 ongoing = false 67 ongoing = false
73 listener = null 68 listener = null
74 - callback.onResult(errorCode, errorDescription) 69 + callback.onResult(errorCode)
75 } 70 }
76 } 71 }
77 ) 72 )
1 package dev.steenbakker.mobile_scanner 1 package dev.steenbakker.mobile_scanner
2 2
3 import android.content.pm.PackageManager 3 import android.content.pm.PackageManager
  4 +import dev.steenbakker.mobile_scanner.objects.MobileScannerErrorCodes
4 import io.flutter.plugin.common.PluginRegistry 5 import io.flutter.plugin.common.PluginRegistry
5 6
6 /** 7 /**
@@ -29,11 +30,9 @@ internal class MobileScannerPermissionsListener( @@ -29,11 +30,9 @@ internal class MobileScannerPermissionsListener(
29 // grantResults could be empty if the permissions request with the user is interrupted 30 // grantResults could be empty if the permissions request with the user is interrupted
30 // https://developer.android.com/reference/android/app/Activity#onRequestPermissionsResult(int,%20java.lang.String[],%20int[]) 31 // https://developer.android.com/reference/android/app/Activity#onRequestPermissionsResult(int,%20java.lang.String[],%20int[])
31 if (grantResults.isEmpty() || grantResults[0] != PackageManager.PERMISSION_GRANTED) { 32 if (grantResults.isEmpty() || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
32 - resultCallback.onResult(  
33 - MobileScannerPermissions.CAMERA_ACCESS_DENIED,  
34 - MobileScannerPermissions.CAMERA_ACCESS_DENIED_MESSAGE) 33 + resultCallback.onResult(MobileScannerErrorCodes.CAMERA_ACCESS_DENIED)
35 } else { 34 } else {
36 - resultCallback.onResult(null, null) 35 + resultCallback.onResult(null)
37 } 36 }
38 37
39 return true 38 return true
  1 +package dev.steenbakker.mobile_scanner.objects
  2 +
  3 +class MobileScannerErrorCodes {
  4 + companion object {
  5 + const val ALREADY_STARTED_ERROR = "MOBILE_SCANNER_ALREADY_STARTED_ERROR"
  6 + const val ALREADY_STARTED_ERROR_MESSAGE = "The scanner was already started."
  7 + // The error code 'BARCODE_ERROR' does not have an error message,
  8 + // because it uses the error message from the underlying error.
  9 + const val BARCODE_ERROR = "MOBILE_SCANNER_BARCODE_ERROR"
  10 + // The error code 'CAMERA_ACCESS_DENIED' does not have an error message,
  11 + // because it is used for a boolean result.
  12 + const val CAMERA_ACCESS_DENIED = "MOBILE_SCANNER_CAMERA_PERMISSION_DENIED"
  13 + const val CAMERA_ERROR = "MOBILE_SCANNER_CAMERA_ERROR"
  14 + const val CAMERA_ERROR_MESSAGE = "An error occurred when opening the camera."
  15 + const val CAMERA_PERMISSIONS_REQUEST_ONGOING = "MOBILE_SCANNER_CAMERA_PERMISSION_REQUEST_PENDING"
  16 + const val CAMERA_PERMISSIONS_REQUEST_ONGOING_MESSAGE = "Another request is ongoing and multiple requests cannot be handled at once."
  17 + const val GENERIC_ERROR = "MOBILE_SCANNER_GENERIC_ERROR"
  18 + const val GENERIC_ERROR_MESSAGE = "An unknown error occurred."
  19 + const val INVALID_ZOOM_SCALE_ERROR_MESSAGE = "The zoom scale should be between 0 and 1 (both inclusive)"
  20 + const val NO_CAMERA_ERROR = "MOBILE_SCANNER_NO_CAMERA_ERROR"
  21 + const val NO_CAMERA_ERROR_MESSAGE = "No cameras available."
  22 + const val SET_SCALE_WHEN_STOPPED_ERROR = "MOBILE_SCANNER_SET_SCALE_WHEN_STOPPED_ERROR"
  23 + const val SET_SCALE_WHEN_STOPPED_ERROR_MESSAGE = "The zoom scale cannot be changed when the camera is stopped."
  24 + }
  25 +}
@@ -5,9 +5,11 @@ @@ -5,9 +5,11 @@
5 *.swp 5 *.swp
6 .DS_Store 6 .DS_Store
7 .atom/ 7 .atom/
  8 +.build/
8 .buildlog/ 9 .buildlog/
9 .history 10 .history
10 .svn/ 11 .svn/
  12 +.swiftpm/
11 migrate_working_dir/ 13 migrate_working_dir/
12 14
13 # IntelliJ related 15 # IntelliJ related
@@ -27,12 +27,12 @@ android { @@ -27,12 +27,12 @@ android {
27 compileSdk 34 27 compileSdk 34
28 28
29 compileOptions { 29 compileOptions {
30 - sourceCompatibility JavaVersion.VERSION_1_8  
31 - targetCompatibility JavaVersion.VERSION_1_8 30 + sourceCompatibility JavaVersion.VERSION_17
  31 + targetCompatibility JavaVersion.VERSION_17
32 } 32 }
33 33
34 kotlinOptions { 34 kotlinOptions {
35 - jvmTarget = '1.8' 35 + jvmTarget = '17'
36 } 36 }
37 37
38 sourceSets { 38 sourceSets {
@@ -42,7 +42,7 @@ android { @@ -42,7 +42,7 @@ android {
42 defaultConfig { 42 defaultConfig {
43 // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 43 // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
44 applicationId "dev.steenbakker.mobile_scanner_example" 44 applicationId "dev.steenbakker.mobile_scanner_example"
45 - minSdkVersion 21 45 + minSdkVersion 24
46 targetSdkVersion 34 46 targetSdkVersion 34
47 versionCode flutterVersionCode.toInteger() 47 versionCode flutterVersionCode.toInteger()
48 versionName flutterVersionName 48 versionName flutterVersionName
1 #Thu May 02 10:24:49 CEST 2024 1 #Thu May 02 10:24:49 CEST 2024
2 distributionBase=GRADLE_USER_HOME 2 distributionBase=GRADLE_USER_HOME
3 distributionPath=wrapper/dists 3 distributionPath=wrapper/dists
4 -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-bin.zip 4 +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
5 zipStoreBase=GRADLE_USER_HOME 5 zipStoreBase=GRADLE_USER_HOME
6 zipStorePath=wrapper/dists 6 zipStorePath=wrapper/dists
@@ -18,8 +18,8 @@ pluginManagement { @@ -18,8 +18,8 @@ pluginManagement {
18 18
19 plugins { 19 plugins {
20 id "dev.flutter.flutter-plugin-loader" version "1.0.0" 20 id "dev.flutter.flutter-plugin-loader" version "1.0.0"
21 - id "com.android.application" version "7.3.0" apply false  
22 - id "org.jetbrains.kotlin.android" version "1.7.22" apply false 21 + id "com.android.application" version "8.3.2" apply false
  22 + id "org.jetbrains.kotlin.android" version "2.0.20" apply false
23 } 23 }
24 24
25 include ":app" 25 include ":app"
@@ -21,6 +21,6 @@ @@ -21,6 +21,6 @@
21 <key>CFBundleVersion</key> 21 <key>CFBundleVersion</key>
22 <string>1.0</string> 22 <string>1.0</string>
23 <key>MinimumOSVersion</key> 23 <key>MinimumOSVersion</key>
24 - <string>12.0</string> 24 + <string>15.5.0</string>
25 </dict> 25 </dict>
26 </plist> 26 </plist>
1 # Uncomment this line to define a global platform for your project 1 # Uncomment this line to define a global platform for your project
2 -# platform :ios, '12.0' 2 +# platform :ios, '15.5.0'
3 3
4 # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 4 # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
5 ENV['COCOAPODS_DISABLE_STATS'] = 'true' 5 ENV['COCOAPODS_DISABLE_STATS'] = 'true'
@@ -41,7 +41,7 @@ post_install do |installer| @@ -41,7 +41,7 @@ post_install do |installer|
41 installer.pods_project.targets.each do |target| 41 installer.pods_project.targets.each do |target|
42 flutter_additional_ios_build_settings(target) 42 flutter_additional_ios_build_settings(target)
43 target.build_configurations.each do |config| 43 target.build_configurations.each do |config|
44 - config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0' 44 + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.5.0'
45 end 45 end
46 end 46 end
47 end 47 end
@@ -470,7 +470,7 @@ @@ -470,7 +470,7 @@
470 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 470 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
471 GCC_WARN_UNUSED_FUNCTION = YES; 471 GCC_WARN_UNUSED_FUNCTION = YES;
472 GCC_WARN_UNUSED_VARIABLE = YES; 472 GCC_WARN_UNUSED_VARIABLE = YES;
473 - IPHONEOS_DEPLOYMENT_TARGET = 12.0; 473 + IPHONEOS_DEPLOYMENT_TARGET = 15.5.0;
474 MTL_ENABLE_DEBUG_INFO = NO; 474 MTL_ENABLE_DEBUG_INFO = NO;
475 SDKROOT = iphoneos; 475 SDKROOT = iphoneos;
476 SUPPORTED_PLATFORMS = iphoneos; 476 SUPPORTED_PLATFORMS = iphoneos;
@@ -488,14 +488,14 @@ @@ -488,14 +488,14 @@
488 CODE_SIGN_IDENTITY = "Apple Development"; 488 CODE_SIGN_IDENTITY = "Apple Development";
489 CODE_SIGN_STYLE = Automatic; 489 CODE_SIGN_STYLE = Automatic;
490 CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 490 CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
491 - DEVELOPMENT_TEAM = 75Y2P2WSQQ; 491 + DEVELOPMENT_TEAM = "";
492 ENABLE_BITCODE = NO; 492 ENABLE_BITCODE = NO;
493 INFOPLIST_FILE = Runner/Info.plist; 493 INFOPLIST_FILE = Runner/Info.plist;
494 LD_RUNPATH_SEARCH_PATHS = ( 494 LD_RUNPATH_SEARCH_PATHS = (
495 "$(inherited)", 495 "$(inherited)",
496 "@executable_path/Frameworks", 496 "@executable_path/Frameworks",
497 ); 497 );
498 - PRODUCT_BUNDLE_IDENTIFIER = "com.example.mobile-scanner"; 498 + PRODUCT_BUNDLE_IDENTIFIER = "com.example.mobile-scanner-example";
499 PRODUCT_NAME = "$(TARGET_NAME)"; 499 PRODUCT_NAME = "$(TARGET_NAME)";
500 PROVISIONING_PROFILE_SPECIFIER = ""; 500 PROVISIONING_PROFILE_SPECIFIER = "";
501 SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 501 SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -601,7 +601,7 @@ @@ -601,7 +601,7 @@
601 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 601 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
602 GCC_WARN_UNUSED_FUNCTION = YES; 602 GCC_WARN_UNUSED_FUNCTION = YES;
603 GCC_WARN_UNUSED_VARIABLE = YES; 603 GCC_WARN_UNUSED_VARIABLE = YES;
604 - IPHONEOS_DEPLOYMENT_TARGET = 12.0; 604 + IPHONEOS_DEPLOYMENT_TARGET = 15.5.0;
605 MTL_ENABLE_DEBUG_INFO = YES; 605 MTL_ENABLE_DEBUG_INFO = YES;
606 ONLY_ACTIVE_ARCH = YES; 606 ONLY_ACTIVE_ARCH = YES;
607 SDKROOT = iphoneos; 607 SDKROOT = iphoneos;
@@ -650,7 +650,7 @@ @@ -650,7 +650,7 @@
650 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 650 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
651 GCC_WARN_UNUSED_FUNCTION = YES; 651 GCC_WARN_UNUSED_FUNCTION = YES;
652 GCC_WARN_UNUSED_VARIABLE = YES; 652 GCC_WARN_UNUSED_VARIABLE = YES;
653 - IPHONEOS_DEPLOYMENT_TARGET = 12.0; 653 + IPHONEOS_DEPLOYMENT_TARGET = 15.5.0;
654 MTL_ENABLE_DEBUG_INFO = NO; 654 MTL_ENABLE_DEBUG_INFO = NO;
655 SDKROOT = iphoneos; 655 SDKROOT = iphoneos;
656 SUPPORTED_PLATFORMS = iphoneos; 656 SUPPORTED_PLATFORMS = iphoneos;
@@ -670,14 +670,14 @@ @@ -670,14 +670,14 @@
670 CODE_SIGN_IDENTITY = "Apple Development"; 670 CODE_SIGN_IDENTITY = "Apple Development";
671 CODE_SIGN_STYLE = Automatic; 671 CODE_SIGN_STYLE = Automatic;
672 CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 672 CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
673 - DEVELOPMENT_TEAM = 75Y2P2WSQQ; 673 + DEVELOPMENT_TEAM = "";
674 ENABLE_BITCODE = NO; 674 ENABLE_BITCODE = NO;
675 INFOPLIST_FILE = Runner/Info.plist; 675 INFOPLIST_FILE = Runner/Info.plist;
676 LD_RUNPATH_SEARCH_PATHS = ( 676 LD_RUNPATH_SEARCH_PATHS = (
677 "$(inherited)", 677 "$(inherited)",
678 "@executable_path/Frameworks", 678 "@executable_path/Frameworks",
679 ); 679 );
680 - PRODUCT_BUNDLE_IDENTIFIER = "com.example.mobile-scanner"; 680 + PRODUCT_BUNDLE_IDENTIFIER = "com.example.mobile-scanner-example";
681 PRODUCT_NAME = "$(TARGET_NAME)"; 681 PRODUCT_NAME = "$(TARGET_NAME)";
682 PROVISIONING_PROFILE_SPECIFIER = ""; 682 PROVISIONING_PROFILE_SPECIFIER = "";
683 SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 683 SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -696,14 +696,14 @@ @@ -696,14 +696,14 @@
696 CODE_SIGN_IDENTITY = "Apple Development"; 696 CODE_SIGN_IDENTITY = "Apple Development";
697 CODE_SIGN_STYLE = Automatic; 697 CODE_SIGN_STYLE = Automatic;
698 CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 698 CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
699 - DEVELOPMENT_TEAM = 75Y2P2WSQQ; 699 + DEVELOPMENT_TEAM = "";
700 ENABLE_BITCODE = NO; 700 ENABLE_BITCODE = NO;
701 INFOPLIST_FILE = Runner/Info.plist; 701 INFOPLIST_FILE = Runner/Info.plist;
702 LD_RUNPATH_SEARCH_PATHS = ( 702 LD_RUNPATH_SEARCH_PATHS = (
703 "$(inherited)", 703 "$(inherited)",
704 "@executable_path/Frameworks", 704 "@executable_path/Frameworks",
705 ); 705 );
706 - PRODUCT_BUNDLE_IDENTIFIER = "com.example.mobile-scanner"; 706 + PRODUCT_BUNDLE_IDENTIFIER = "com.example.mobile-scanner-example";
707 PRODUCT_NAME = "$(TARGET_NAME)"; 707 PRODUCT_NAME = "$(TARGET_NAME)";
708 PROVISIONING_PROFILE_SPECIFIER = ""; 708 PROVISIONING_PROFILE_SPECIFIER = "";
709 SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 709 SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -22,7 +22,14 @@ class _BarcodeScannerAnalyzeImageState @@ -22,7 +22,14 @@ class _BarcodeScannerAnalyzeImageState
22 final XFile? file = 22 final XFile? file =
23 await ImagePicker().pickImage(source: ImageSource.gallery); 23 await ImagePicker().pickImage(source: ImageSource.gallery);
24 24
25 - if (!mounted || file == null) { 25 + if (!mounted) {
  26 + return;
  27 + }
  28 +
  29 + if (file == null) {
  30 + setState(() {
  31 + _barcodeCapture = null;
  32 + });
26 return; 33 return;
27 } 34 }
28 35
@@ -43,7 +50,7 @@ class _BarcodeScannerAnalyzeImageState @@ -43,7 +50,7 @@ class _BarcodeScannerAnalyzeImageState
43 50
44 if (_barcodeCapture != null) { 51 if (_barcodeCapture != null) {
45 label = Text( 52 label = Text(
46 - _barcodeCapture?.barcodes.firstOrNull?.displayValue ?? 53 + _barcodeCapture?.barcodes.firstOrNull?.rawValue ??
47 'No barcode detected', 54 'No barcode detected',
48 ); 55 );
49 } 56 }
@@ -60,7 +60,7 @@ class _BarcodeScannerWithControllerState @@ -60,7 +60,7 @@ class _BarcodeScannerWithControllerState
60 60
61 @override 61 @override
62 void didChangeAppLifecycleState(AppLifecycleState state) { 62 void didChangeAppLifecycleState(AppLifecycleState state) {
63 - if (!controller.value.isInitialized) { 63 + if (!controller.value.hasCameraPermission) {
64 return; 64 return;
65 } 65 }
66 66
@@ -130,15 +130,15 @@ class ScannerOverlay extends CustomPainter { @@ -130,15 +130,15 @@ class ScannerOverlay extends CustomPainter {
130 130
131 @override 131 @override
132 void paint(Canvas canvas, Size size) { 132 void paint(Canvas canvas, Size size) {
133 - // TODO: use `Offset.zero & size` instead of Rect.largest  
134 // we need to pass the size to the custom paint widget 133 // we need to pass the size to the custom paint widget
135 - final backgroundPath = Path()..addRect(Rect.largest); 134 + final backgroundPath = Path()
  135 + ..addRect(Rect.fromLTWH(0, 0, size.width, size.height));
136 final cutoutPath = Path()..addRect(scanWindow); 136 final cutoutPath = Path()..addRect(scanWindow);
137 137
138 final backgroundPaint = Paint() 138 final backgroundPaint = Paint()
139 ..color = Colors.black.withOpacity(0.5) 139 ..color = Colors.black.withOpacity(0.5)
140 ..style = PaintingStyle.fill 140 ..style = PaintingStyle.fill
141 - ..blendMode = BlendMode.dstOut; 141 + ..blendMode = BlendMode.dstOver;
142 142
143 final backgroundWithCutout = Path.combine( 143 final backgroundWithCutout = Path.combine(
144 PathOperation.difference, 144 PathOperation.difference,
@@ -102,9 +102,9 @@ class ScannerOverlay extends CustomPainter { @@ -102,9 +102,9 @@ class ScannerOverlay extends CustomPainter {
102 102
103 @override 103 @override
104 void paint(Canvas canvas, Size size) { 104 void paint(Canvas canvas, Size size) {
105 - // TODO: use `Offset.zero & size` instead of Rect.largest  
106 // we need to pass the size to the custom paint widget 105 // we need to pass the size to the custom paint widget
107 - final backgroundPath = Path()..addRect(Rect.largest); 106 + final backgroundPath = Path()
  107 + ..addRect(Rect.fromLTWH(0, 0, size.width, size.height));
108 108
109 final cutoutPath = Path() 109 final cutoutPath = Path()
110 ..addRRect( 110 ..addRRect(
@@ -120,7 +120,7 @@ class ScannerOverlay extends CustomPainter { @@ -120,7 +120,7 @@ class ScannerOverlay extends CustomPainter {
120 final backgroundPaint = Paint() 120 final backgroundPaint = Paint()
121 ..color = Colors.black.withOpacity(0.5) 121 ..color = Colors.black.withOpacity(0.5)
122 ..style = PaintingStyle.fill 122 ..style = PaintingStyle.fill
123 - ..blendMode = BlendMode.dstOut; 123 + ..blendMode = BlendMode.dstOver;
124 124
125 final backgroundWithCutout = Path.combine( 125 final backgroundWithCutout = Path.combine(
126 PathOperation.difference, 126 PathOperation.difference,
@@ -112,6 +112,7 @@ class SwitchCameraButton extends StatelessWidget { @@ -112,6 +112,7 @@ class SwitchCameraButton extends StatelessWidget {
112 } 112 }
113 113
114 return IconButton( 114 return IconButton(
  115 + color: Colors.white,
115 iconSize: 32.0, 116 iconSize: 32.0,
116 icon: icon, 117 icon: icon,
117 onPressed: () async { 118 onPressed: () async {
@@ -166,9 +167,13 @@ class ToggleFlashlightButton extends StatelessWidget { @@ -166,9 +167,13 @@ class ToggleFlashlightButton extends StatelessWidget {
166 }, 167 },
167 ); 168 );
168 case TorchState.unavailable: 169 case TorchState.unavailable:
169 - return const Icon( 170 + return const SizedBox.square(
  171 + dimension: 48.0,
  172 + child: Icon(
170 Icons.no_flash, 173 Icons.no_flash,
  174 + size: 32.0,
171 color: Colors.grey, 175 color: Colors.grey,
  176 + ),
172 ); 177 );
173 } 178 }
174 }, 179 },
@@ -6,4 +6,8 @@ class AppDelegate: FlutterAppDelegate { @@ -6,4 +6,8 @@ class AppDelegate: FlutterAppDelegate {
6 override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 6 override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
7 return true 7 return true
8 } 8 }
  9 +
  10 + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
  11 + return true
  12 + }
9 } 13 }
@@ -10,5 +10,7 @@ @@ -10,5 +10,7 @@
10 <true/> 10 <true/>
11 <key>com.apple.security.network.server</key> 11 <key>com.apple.security.network.server</key>
12 <true/> 12 <true/>
  13 + <key>com.apple.security.files.user-selected.read-only</key>
  14 + <true/>
13 </dict> 15 </dict>
14 </plist> 16 </plist>
@@ -21,7 +21,7 @@ @@ -21,7 +21,7 @@
21 <meta name="description" content="Demonstrates how to use the mobile_scanner plugin."> 21 <meta name="description" content="Demonstrates how to use the mobile_scanner plugin.">
22 22
23 <!-- iOS meta tags & icons --> 23 <!-- iOS meta tags & icons -->
24 - <meta name="apple-mobile-web-app-capable" content="yes"> 24 + <meta name="mobile-web-app-capable" content="yes">
25 <meta name="apple-mobile-web-app-status-bar-style" content="black"> 25 <meta name="apple-mobile-web-app-status-bar-style" content="black">
26 <meta name="apple-mobile-web-app-title" content="mobile_scanner_example"> 26 <meta name="apple-mobile-web-app-title" content="mobile_scanner_example">
27 <link rel="apple-touch-icon" href="icons/Icon-192.png"> 27 <link rel="apple-touch-icon" href="icons/Icon-192.png">
@@ -19,6 +19,12 @@ public class BarcodeHandler: NSObject, FlutterStreamHandler { @@ -19,6 +19,12 @@ public class BarcodeHandler: NSObject, FlutterStreamHandler {
19 eventChannel.setStreamHandler(self) 19 eventChannel.setStreamHandler(self)
20 } 20 }
21 21
  22 + func publishError(_ error: FlutterError) {
  23 + DispatchQueue.main.async {
  24 + self.eventSink?(error)
  25 + }
  26 + }
  27 +
22 func publishEvent(_ event: [String: Any?]) { 28 func publishEvent(_ event: [String: Any?]) {
23 DispatchQueue.main.async { 29 DispatchQueue.main.async {
24 self.eventSink?(event) 30 self.eventSink?(event)
@@ -6,6 +6,12 @@ @@ -6,6 +6,12 @@
6 // 6 //
7 import Foundation 7 import Foundation
8 8
  9 +// TODO: decide if we should keep or discard this enum
  10 +// When merging the iOS / MacOS implementations we should either keep the enum or remove it
  11 +
  12 +// This enum is a bit of a leftover from older parts of the iOS implementation.
  13 +// It is used by the handler that throws these error codes,
  14 +// while the plugin class intercepts these and converts them to `FlutterError()`s.
9 enum MobileScannerError: Error { 15 enum MobileScannerError: Error {
10 case noCamera 16 case noCamera
11 case alreadyStarted 17 case alreadyStarted
  1 +//
  2 +// MobileScannerErrorCodes.swift
  3 +// mobile_scanner
  4 +//
  5 +// Created by Navaron Bracke on 28/05/2024.
  6 +//
  7 +
  8 +import Foundation
  9 +
  10 +/// This struct defines the error codes and error messages for MobileScanner errors.
  11 +///
  12 +/// These are used by `FlutterError` as error code and error message.
  13 +///
  14 +/// This struct should not be confused with `MobileScannerError`,
  15 +/// which is an implementation detail for the iOS implementation.
  16 +struct MobileScannerErrorCodes {
  17 + static let ALREADY_STARTED_ERROR = "MOBILE_SCANNER_ALREADY_STARTED_ERROR"
  18 + static let ALREADY_STARTED_ERROR_MESSAGE = "The scanner was already started."
  19 + // The error code 'BARCODE_ERROR' does not have an error message,
  20 + // because it uses the error message from the undelying error.
  21 + static let BARCODE_ERROR = "MOBILE_SCANNER_BARCODE_ERROR"
  22 + // The error code 'CAMERA_ERROR' does not have an error message,
  23 + // because it uses the error message from the underlying error.
  24 + static let CAMERA_ERROR = "MOBILE_SCANNER_CAMERA_ERROR"
  25 + static let GENERIC_ERROR = "MOBILE_SCANNER_GENERIC_ERROR"
  26 + static let GENERIC_ERROR_MESSAGE = "An unknown error occurred."
  27 + // This message is used with the 'GENERIC_ERROR' error code.
  28 + static let INVALID_ZOOM_SCALE_ERROR_MESSAGE = "The zoom scale should be between 0 and 1 (both inclusive)"
  29 + static let NO_CAMERA_ERROR = "MOBILE_SCANNER_NO_CAMERA_ERROR"
  30 + static let NO_CAMERA_ERROR_MESSAGE = "No cameras available."
  31 + static let SET_SCALE_WHEN_STOPPED_ERROR = "MOBILE_SCANNER_SET_SCALE_WHEN_STOPPED_ERROR"
  32 + static let SET_SCALE_WHEN_STOPPED_ERROR_MESSAGE = "The zoom scale cannot be changed when the camera is stopped."
  33 +}
@@ -42,7 +42,10 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { @@ -42,7 +42,10 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
42 init(barcodeHandler: BarcodeHandler, registry: FlutterTextureRegistry) { 42 init(barcodeHandler: BarcodeHandler, registry: FlutterTextureRegistry) {
43 self.mobileScanner = MobileScanner(registry: registry, mobileScannerCallback: { barcodes, error, image in 43 self.mobileScanner = MobileScanner(registry: registry, mobileScannerCallback: { barcodes, error, image in
44 if error != nil { 44 if error != nil {
45 - barcodeHandler.publishEvent(["name": "error", "data": error!.localizedDescription]) 45 + barcodeHandler.publishError(
  46 + FlutterError(code: MobileScannerErrorCodes.BARCODE_ERROR,
  47 + message: error?.localizedDescription,
  48 + details: nil))
46 return 49 return
47 } 50 }
48 51
@@ -66,22 +69,18 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { @@ -66,22 +69,18 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
66 return 69 return
67 } 70 }
68 71
69 - if (!MobileScannerPlugin.returnImage) {  
70 - barcodeHandler.publishEvent([  
71 - "name": "barcode",  
72 - "data": barcodesMap,  
73 - ])  
74 - return  
75 - } 72 + // The image dimensions are always provided.
  73 + // The image bytes are only non-null when `returnImage` is true.
  74 + let imageData: [String: Any?] = [
  75 + "bytes": MobileScannerPlugin.returnImage ? FlutterStandardTypedData(bytes: image.jpegData(compressionQuality: 0.8)!) : nil,
  76 + "width": image.size.width,
  77 + "height": image.size.height,
  78 + ]
76 79
77 barcodeHandler.publishEvent([ 80 barcodeHandler.publishEvent([
78 "name": "barcode", 81 "name": "barcode",
79 "data": barcodesMap, 82 "data": barcodesMap,
80 - "image": [  
81 - "bytes": FlutterStandardTypedData(bytes: image.jpegData(compressionQuality: 0.8)!),  
82 - "width": image.size.width,  
83 - "height": image.size.height,  
84 - ], 83 + "image": imageData,
85 ]) 84 ])
86 }, torchModeChangeCallback: { torchState in 85 }, torchModeChangeCallback: { torchState in
87 barcodeHandler.publishEvent(["name": "torchState", "data": torchState]) 86 barcodeHandler.publishEvent(["name": "torchState", "data": torchState])
@@ -150,20 +149,20 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { @@ -150,20 +149,20 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
150 } 149 }
151 } 150 }
152 } catch MobileScannerError.alreadyStarted { 151 } catch MobileScannerError.alreadyStarted {
153 - result(FlutterError(code: "MobileScanner",  
154 - message: "Called start() while already started!", 152 + result(FlutterError(code: MobileScannerErrorCodes.ALREADY_STARTED_ERROR,
  153 + message: MobileScannerErrorCodes.ALREADY_STARTED_ERROR_MESSAGE,
155 details: nil)) 154 details: nil))
156 } catch MobileScannerError.noCamera { 155 } catch MobileScannerError.noCamera {
157 - result(FlutterError(code: "MobileScanner",  
158 - message: "No camera found or failed to open camera!", 156 + result(FlutterError(code: MobileScannerErrorCodes.NO_CAMERA_ERROR,
  157 + message: MobileScannerErrorCodes.NO_CAMERA_ERROR_MESSAGE,
159 details: nil)) 158 details: nil))
160 } catch MobileScannerError.cameraError(let error) { 159 } catch MobileScannerError.cameraError(let error) {
161 - result(FlutterError(code: "MobileScanner",  
162 - message: "Error occured when setting up camera!",  
163 - details: error)) 160 + result(FlutterError(code: MobileScannerErrorCodes.CAMERA_ERROR,
  161 + message: error.localizedDescription,
  162 + details: nil))
164 } catch { 163 } catch {
165 - result(FlutterError(code: "MobileScanner",  
166 - message: "Unknown error occured.", 164 + result(FlutterError(code: MobileScannerErrorCodes.GENERIC_ERROR,
  165 + message: MobileScannerErrorCodes.GENERIC_ERROR_MESSAGE,
167 details: nil)) 166 details: nil))
168 } 167 }
169 } 168 }
@@ -186,25 +185,25 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { @@ -186,25 +185,25 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
186 private func setScale(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { 185 private func setScale(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
187 let scale = call.arguments as? CGFloat 186 let scale = call.arguments as? CGFloat
188 if (scale == nil) { 187 if (scale == nil) {
189 - result(FlutterError(code: "MobileScanner",  
190 - message: "You must provide a scale when calling setScale!",  
191 - details: nil)) 188 + result(FlutterError(code: MobileScannerErrorCodes.GENERIC_ERROR,
  189 + message: MobileScannerErrorCodes.INVALID_ZOOM_SCALE_ERROR_MESSAGE,
  190 + details: "The invalid zoom scale was nil."))
192 return 191 return
193 } 192 }
194 do { 193 do {
195 try mobileScanner.setScale(scale!) 194 try mobileScanner.setScale(scale!)
196 result(nil) 195 result(nil)
197 } catch MobileScannerError.zoomWhenStopped { 196 } catch MobileScannerError.zoomWhenStopped {
198 - result(FlutterError(code: "MobileScanner",  
199 - message: "Called setScale() while stopped!", 197 + result(FlutterError(code: MobileScannerErrorCodes.SET_SCALE_WHEN_STOPPED_ERROR,
  198 + message: MobileScannerErrorCodes.SET_SCALE_WHEN_STOPPED_ERROR_MESSAGE,
200 details: nil)) 199 details: nil))
201 } catch MobileScannerError.zoomError(let error) { 200 } catch MobileScannerError.zoomError(let error) {
202 - result(FlutterError(code: "MobileScanner",  
203 - message: "Error while zooming.",  
204 - details: error)) 201 + result(FlutterError(code: MobileScannerErrorCodes.GENERIC_ERROR,
  202 + message: error.localizedDescription,
  203 + details: nil))
205 } catch { 204 } catch {
206 - result(FlutterError(code: "MobileScanner",  
207 - message: "Error while zooming.", 205 + result(FlutterError(code: MobileScannerErrorCodes.GENERIC_ERROR,
  206 + message: MobileScannerErrorCodes.GENERIC_ERROR_MESSAGE,
208 details: nil)) 207 details: nil))
209 } 208 }
210 } 209 }
@@ -215,16 +214,16 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { @@ -215,16 +214,16 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
215 try mobileScanner.resetScale() 214 try mobileScanner.resetScale()
216 result(nil) 215 result(nil)
217 } catch MobileScannerError.zoomWhenStopped { 216 } catch MobileScannerError.zoomWhenStopped {
218 - result(FlutterError(code: "MobileScanner",  
219 - message: "Called resetScale() while stopped!", 217 + result(FlutterError(code: MobileScannerErrorCodes.SET_SCALE_WHEN_STOPPED_ERROR,
  218 + message: MobileScannerErrorCodes.SET_SCALE_WHEN_STOPPED_ERROR_MESSAGE,
220 details: nil)) 219 details: nil))
221 } catch MobileScannerError.zoomError(let error) { 220 } catch MobileScannerError.zoomError(let error) {
222 - result(FlutterError(code: "MobileScanner",  
223 - message: "Error while zooming.",  
224 - details: error)) 221 + result(FlutterError(code: MobileScannerErrorCodes.GENERIC_ERROR,
  222 + message: error.localizedDescription,
  223 + details: nil))
225 } catch { 224 } catch {
226 - result(FlutterError(code: "MobileScanner",  
227 - message: "Error while zooming.", 225 + result(FlutterError(code: MobileScannerErrorCodes.GENERIC_ERROR,
  226 + message: MobileScannerErrorCodes.GENERIC_ERROR_MESSAGE,
228 details: nil)) 227 details: nil))
229 } 228 }
230 } 229 }
@@ -258,9 +257,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { @@ -258,9 +257,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
258 let uiImage = UIImage(contentsOfFile: (call.arguments as! Dictionary<String, Any?>)["filePath"] as? String ?? "") 257 let uiImage = UIImage(contentsOfFile: (call.arguments as! Dictionary<String, Any?>)["filePath"] as? String ?? "")
259 258
260 if (uiImage == nil) { 259 if (uiImage == nil) {
261 - result(FlutterError(code: "MobileScanner",  
262 - message: "No image found in analyzeImage!",  
263 - details: nil)) 260 + result(nil)
264 return 261 return
265 } 262 }
266 263
@@ -268,7 +265,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { @@ -268,7 +265,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
268 barcodeScannerOptions: scannerOptions, callback: { barcodes, error in 265 barcodeScannerOptions: scannerOptions, callback: { barcodes, error in
269 if error != nil { 266 if error != nil {
270 DispatchQueue.main.async { 267 DispatchQueue.main.async {
271 - result(FlutterError(code: "MobileScanner", 268 + result(FlutterError(code: MobileScannerErrorCodes.BARCODE_ERROR,
272 message: error?.localizedDescription, 269 message: error?.localizedDescription,
273 details: nil)) 270 details: nil))
274 } 271 }
@@ -4,7 +4,7 @@ @@ -4,7 +4,7 @@
4 # 4 #
5 Pod::Spec.new do |s| 5 Pod::Spec.new do |s|
6 s.name = 'mobile_scanner' 6 s.name = 'mobile_scanner'
7 - s.version = '5.2.3' 7 + s.version = '6.0.2'
8 s.summary = 'An universal scanner for Flutter based on MLKit.' 8 s.summary = 'An universal scanner for Flutter based on MLKit.'
9 s.description = <<-DESC 9 s.description = <<-DESC
10 An universal scanner for Flutter based on MLKit. 10 An universal scanner for Flutter based on MLKit.
@@ -15,11 +15,16 @@ An universal scanner for Flutter based on MLKit. @@ -15,11 +15,16 @@ An universal scanner for Flutter based on MLKit.
15 s.source = { :path => '.' } 15 s.source = { :path => '.' }
16 s.source_files = 'Classes/**/*' 16 s.source_files = 'Classes/**/*'
17 s.dependency 'Flutter' 17 s.dependency 'Flutter'
18 - s.dependency 'GoogleMLKit/BarcodeScanning', '~> 6.0.0'  
19 - s.platform = :ios, '12.0' 18 + s.dependency 'GoogleMLKit/BarcodeScanning', '~> 7.0.0'
  19 + s.platform = :ios, '15.5.0'
20 s.static_framework = true 20 s.static_framework = true
21 - # Flutter.framework does not contain a i386 slice.  
22 - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } 21 + # Flutter.framework does not contain a i386 slice, and MLKit does not support armv7.
  22 + s.pod_target_xcconfig = {
  23 + 'DEFINES_MODULE' => 'YES',
  24 + # TODO: add back arm64 (and armv7?) when switching to the Vision API.
  25 + 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386 armv7 arm64',
  26 + 'EXCLUDED_ARCHS[sdk=iphoneos*]' => 'armv7',
  27 + }
23 s.swift_version = '5.0' 28 s.swift_version = '5.0'
24 s.resource_bundles = { 'mobile_scanner_privacy' => ['Resources/PrivacyInfo.xcprivacy'] } 29 s.resource_bundles = { 'mobile_scanner_privacy' => ['Resources/PrivacyInfo.xcprivacy'] }
25 end 30 end
@@ -11,8 +11,7 @@ export 'src/enums/phone_type.dart'; @@ -11,8 +11,7 @@ export 'src/enums/phone_type.dart';
11 export 'src/enums/torch_state.dart'; 11 export 'src/enums/torch_state.dart';
12 export 'src/mobile_scanner.dart'; 12 export 'src/mobile_scanner.dart';
13 export 'src/mobile_scanner_controller.dart'; 13 export 'src/mobile_scanner_controller.dart';
14 -export 'src/mobile_scanner_exception.dart'  
15 - hide PermissionRequestPendingException; 14 +export 'src/mobile_scanner_exception.dart';
16 export 'src/mobile_scanner_platform_interface.dart'; 15 export 'src/mobile_scanner_platform_interface.dart';
17 export 'src/objects/address.dart'; 16 export 'src/objects/address.dart';
18 export 'src/objects/barcode.dart'; 17 export 'src/objects/barcode.dart';
  1 +import 'package:flutter/services.dart';
1 import 'package:mobile_scanner/src/mobile_scanner_controller.dart'; 2 import 'package:mobile_scanner/src/mobile_scanner_controller.dart';
2 3
3 /// This enum defines the different error codes for the mobile scanner. 4 /// This enum defines the different error codes for the mobile scanner.
@@ -24,5 +25,25 @@ enum MobileScannerErrorCode { @@ -24,5 +25,25 @@ enum MobileScannerErrorCode {
24 permissionDenied, 25 permissionDenied,
25 26
26 /// Scanning is unsupported on the current device. 27 /// Scanning is unsupported on the current device.
27 - unsupported, 28 + unsupported;
  29 +
  30 + /// Convert the given [PlatformException.code] to a [MobileScannerErrorCode].
  31 + factory MobileScannerErrorCode.fromPlatformException(
  32 + PlatformException exception,
  33 + ) {
  34 + // The following error code mapping should be kept in sync with their native counterparts.
  35 + // These are located in `MobileScannerErrorCodes.kt` and `MobileScannerErrorCodes.swift`.
  36 + return switch (exception.code) {
  37 + // In case the scanner was already started, report the right error code.
  38 + // If the scanner is already starting,
  39 + // this error code is a signal to the controller to just ignore the attempt.
  40 + 'MOBILE_SCANNER_ALREADY_STARTED_ERROR' =>
  41 + MobileScannerErrorCode.controllerAlreadyInitialized,
  42 + // In case no cameras are available, using the scanner is not supported.
  43 + 'MOBILE_SCANNER_NO_CAMERA_ERROR' => MobileScannerErrorCode.unsupported,
  44 + 'MOBILE_SCANNER_CAMERA_PERMISSION_DENIED' =>
  45 + MobileScannerErrorCode.permissionDenied,
  46 + _ => MobileScannerErrorCode.genericError,
  47 + };
  48 + }
28 } 49 }
@@ -16,6 +16,14 @@ import 'package:mobile_scanner/src/objects/start_options.dart'; @@ -16,6 +16,14 @@ import 'package:mobile_scanner/src/objects/start_options.dart';
16 16
17 /// An implementation of [MobileScannerPlatform] that uses method channels. 17 /// An implementation of [MobileScannerPlatform] that uses method channels.
18 class MethodChannelMobileScanner extends MobileScannerPlatform { 18 class MethodChannelMobileScanner extends MobileScannerPlatform {
  19 + /// The name of the barcode event that is sent when a barcode is scanned.
  20 + @visibleForTesting
  21 + static const String kBarcodeEventName = 'barcode';
  22 +
  23 + /// The name of the error event that is sent when a barcode scan error occurs.
  24 + @visibleForTesting
  25 + static const String kBarcodeErrorEventName = 'MOBILE_SCANNER_BARCODE_ERROR';
  26 +
19 /// The method channel used to interact with the native platform. 27 /// The method channel used to interact with the native platform.
20 @visibleForTesting 28 @visibleForTesting
21 final methodChannel = const MethodChannel( 29 final methodChannel = const MethodChannel(
@@ -79,6 +87,19 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { @@ -79,6 +87,19 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
79 ); 87 );
80 } 88 }
81 89
  90 + /// Parse a [MobileScannerBarcodeException] from the given [error] and [stackTrace], and throw it.
  91 + ///
  92 + /// If the error is not a [PlatformException],
  93 + /// with [kBarcodeErrorEventName] as [PlatformException.code], the error is rethrown as-is.
  94 + Never _parseBarcodeError(Object error, StackTrace stackTrace) {
  95 + if (error case PlatformException(:final String code, :final String? message)
  96 + when code == kBarcodeErrorEventName) {
  97 + throw MobileScannerBarcodeException(message);
  98 + }
  99 +
  100 + Error.throwWithStackTrace(error, stackTrace);
  101 + }
  102 +
82 /// Request permission to access the camera. 103 /// Request permission to access the camera.
83 /// 104 ///
84 /// Throws a [MobileScannerException] if the permission is not granted. 105 /// Throws a [MobileScannerException] if the permission is not granted.
@@ -121,9 +142,12 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { @@ -121,9 +142,12 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
121 142
122 @override 143 @override
123 Stream<BarcodeCapture?> get barcodesStream { 144 Stream<BarcodeCapture?> get barcodesStream {
  145 + // Handle incoming barcode events.
  146 + // The error events are transformed to `MobileScannerBarcodeException` where possible.
124 return eventsStream 147 return eventsStream
125 - .where((event) => event['name'] == 'barcode')  
126 - .map((event) => _parseBarcode(event)); 148 + .where((e) => e['name'] == kBarcodeEventName)
  149 + .map((event) => _parseBarcode(event))
  150 + .handleError(_parseBarcodeError);
127 } 151 }
128 152
129 @override 153 @override
@@ -145,6 +169,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { @@ -145,6 +169,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
145 String path, { 169 String path, {
146 List<BarcodeFormat> formats = const <BarcodeFormat>[], 170 List<BarcodeFormat> formats = const <BarcodeFormat>[],
147 }) async { 171 }) async {
  172 + try {
148 final Map<Object?, Object?>? result = 173 final Map<Object?, Object?>? result =
149 await methodChannel.invokeMapMethod<Object?, Object?>( 174 await methodChannel.invokeMapMethod<Object?, Object?>(
150 'analyzeImage', 175 'analyzeImage',
@@ -160,6 +185,14 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { @@ -160,6 +185,14 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
160 ); 185 );
161 186
162 return _parseBarcode(result); 187 return _parseBarcode(result);
  188 + } on PlatformException catch (error) {
  189 + // Handle any errors from analyze image requests.
  190 + if (error.code == kBarcodeErrorEventName) {
  191 + throw MobileScannerBarcodeException(error.message);
  192 + }
  193 +
  194 + return null;
  195 + }
163 } 196 }
164 197
165 @override 198 @override
@@ -187,8 +220,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { @@ -187,8 +220,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
187 throw const MobileScannerException( 220 throw const MobileScannerException(
188 errorCode: MobileScannerErrorCode.controllerAlreadyInitialized, 221 errorCode: MobileScannerErrorCode.controllerAlreadyInitialized,
189 errorDetails: MobileScannerErrorDetails( 222 errorDetails: MobileScannerErrorDetails(
190 - message:  
191 - 'The scanner was already started. Call stop() before calling start() again.', 223 + message: 'The scanner was already started.',
192 ), 224 ),
193 ); 225 );
194 } 226 }
@@ -204,7 +236,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { @@ -204,7 +236,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
204 ); 236 );
205 } on PlatformException catch (error) { 237 } on PlatformException catch (error) {
206 throw MobileScannerException( 238 throw MobileScannerException(
207 - errorCode: MobileScannerErrorCode.genericError, 239 + errorCode: MobileScannerErrorCode.fromPlatformException(error),
208 errorDetails: MobileScannerErrorDetails( 240 errorDetails: MobileScannerErrorDetails(
209 code: error.code, 241 code: error.code,
210 details: error.details as Object?, 242 details: error.details as Object?,
@@ -240,17 +272,13 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { @@ -240,17 +272,13 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
240 startResult['currentTorchState'] as int? ?? -1, 272 startResult['currentTorchState'] as int? ?? -1,
241 ); 273 );
242 274
243 - final Map<Object?, Object?>? sizeInfo =  
244 - startResult['size'] as Map<Object?, Object?>?;  
245 - final double? width = sizeInfo?['width'] as double?;  
246 - final double? height = sizeInfo?['height'] as double?;  
247 -  
248 final Size size; 275 final Size size;
249 276
250 - if (width == null || height == null) {  
251 - size = Size.zero;  
252 - } else { 277 + if (startResult['size']
  278 + case {'width': final double width, 'height': final double height}) {
253 size = Size(width, height); 279 size = Size(width, height);
  280 + } else {
  281 + size = Size.zero;
254 } 282 }
255 283
256 return MobileScannerViewAttributes( 284 return MobileScannerViewAttributes(
@@ -21,6 +21,7 @@ class MobileScanner extends StatefulWidget { @@ -21,6 +21,7 @@ class MobileScanner extends StatefulWidget {
21 const MobileScanner({ 21 const MobileScanner({
22 this.controller, 22 this.controller,
23 this.onDetect, 23 this.onDetect,
  24 + this.onDetectError = _onDetectErrorHandler,
24 this.fit = BoxFit.cover, 25 this.fit = BoxFit.cover,
25 this.errorBuilder, 26 this.errorBuilder,
26 this.overlayBuilder, 27 this.overlayBuilder,
@@ -34,9 +35,17 @@ class MobileScanner extends StatefulWidget { @@ -34,9 +35,17 @@ class MobileScanner extends StatefulWidget {
34 final MobileScannerController? controller; 35 final MobileScannerController? controller;
35 36
36 /// The function that signals when new codes were detected by the [controller]. 37 /// The function that signals when new codes were detected by the [controller].
37 - /// If null, use the controller.barcodes stream directly to capture barcodes. 38 + ///
  39 + /// To handle both [BarcodeCapture]s and [MobileScannerBarcodeException]s,
  40 + /// use the [MobileScannerController.barcodes] stream directly (recommended),
  41 + /// or provide a function to [onDetectError].
38 final void Function(BarcodeCapture barcodes)? onDetect; 42 final void Function(BarcodeCapture barcodes)? onDetect;
39 43
  44 + /// The error handler equivalent for the [onDetect] function.
  45 + ///
  46 + /// If [onDetect] is not null, and this is null, errors are silently ignored.
  47 + final void Function(Object error, StackTrace stackTrace) onDetectError;
  48 +
40 /// The error builder for the camera preview. 49 /// The error builder for the camera preview.
41 /// 50 ///
42 /// If this is null, a black [ColoredBox], 51 /// If this is null, a black [ColoredBox],
@@ -116,6 +125,11 @@ class MobileScanner extends StatefulWidget { @@ -116,6 +125,11 @@ class MobileScanner extends StatefulWidget {
116 125
117 @override 126 @override
118 State<MobileScanner> createState() => _MobileScannerState(); 127 State<MobileScanner> createState() => _MobileScannerState();
  128 +
  129 + /// This empty function is used as the default error handler for [onDetect].
  130 + static void _onDetectErrorHandler(Object error, StackTrace stackTrace) {
  131 + // Do nothing.
  132 + }
119 } 133 }
120 134
121 class _MobileScannerState extends State<MobileScanner> 135 class _MobileScannerState extends State<MobileScanner>
@@ -249,7 +263,11 @@ class _MobileScannerState extends State<MobileScanner> @@ -249,7 +263,11 @@ class _MobileScannerState extends State<MobileScanner>
249 void initState() { 263 void initState() {
250 if (widget.onDetect != null) { 264 if (widget.onDetect != null) {
251 WidgetsBinding.instance.addObserver(this); 265 WidgetsBinding.instance.addObserver(this);
252 - _subscription = controller.barcodes.listen(widget.onDetect); 266 + _subscription = controller.barcodes.listen(
  267 + widget.onDetect,
  268 + onError: widget.onDetectError,
  269 + cancelOnError: false,
  270 + );
253 } 271 }
254 if (controller.autoStart) { 272 if (controller.autoStart) {
255 controller.start(); 273 controller.start();
@@ -281,8 +299,7 @@ class _MobileScannerState extends State<MobileScanner> @@ -281,8 +299,7 @@ class _MobileScannerState extends State<MobileScanner>
281 299
282 @override 300 @override
283 void didChangeAppLifecycleState(AppLifecycleState state) { 301 void didChangeAppLifecycleState(AppLifecycleState state) {
284 - if (widget.controller != null) return;  
285 - if (!controller.value.isInitialized) { 302 + if (widget.controller != null || !controller.value.hasCameraPermission) {
286 return; 303 return;
287 } 304 }
288 305
@@ -292,7 +309,11 @@ class _MobileScannerState extends State<MobileScanner> @@ -292,7 +309,11 @@ class _MobileScannerState extends State<MobileScanner>
292 case AppLifecycleState.paused: 309 case AppLifecycleState.paused:
293 return; 310 return;
294 case AppLifecycleState.resumed: 311 case AppLifecycleState.resumed:
295 - _subscription = controller.barcodes.listen(widget.onDetect); 312 + _subscription = controller.barcodes.listen(
  313 + widget.onDetect,
  314 + onError: widget.onDetectError,
  315 + cancelOnError: false,
  316 + );
296 317
297 unawaited(controller.start()); 318 unawaited(controller.start());
298 case AppLifecycleState.inactive: 319 case AppLifecycleState.inactive:
@@ -75,8 +75,7 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { @@ -75,8 +75,7 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
75 /// If this is empty, all supported formats are detected. 75 /// If this is empty, all supported formats are detected.
76 final List<BarcodeFormat> formats; 76 final List<BarcodeFormat> formats;
77 77
78 - /// Whether scanned barcodes should contain the image  
79 - /// that is embedded into the barcode. 78 + /// Whether the [BarcodeCapture.image] bytes should be provided.
80 /// 79 ///
81 /// If this is false, [BarcodeCapture.image] will always be null. 80 /// If this is false, [BarcodeCapture.image] will always be null.
82 /// 81 ///
@@ -102,6 +101,9 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { @@ -102,6 +101,9 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
102 StreamController.broadcast(); 101 StreamController.broadcast();
103 102
104 /// Get the stream of scanned barcodes. 103 /// Get the stream of scanned barcodes.
  104 + ///
  105 + /// If an error occurred during the detection of a barcode,
  106 + /// a [MobileScannerBarcodeException] error is emitted to the stream.
105 Stream<BarcodeCapture> get barcodes => _barcodesController.stream; 107 Stream<BarcodeCapture> get barcodes => _barcodesController.stream;
106 108
107 StreamSubscription<BarcodeCapture?>? _barcodesSubscription; 109 StreamSubscription<BarcodeCapture?>? _barcodesSubscription;
@@ -121,14 +123,25 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { @@ -121,14 +123,25 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
121 } 123 }
122 124
123 void _setupListeners() { 125 void _setupListeners() {
124 - _barcodesSubscription = MobileScannerPlatform.instance.barcodesStream  
125 - .listen((BarcodeCapture? barcode) { 126 + _barcodesSubscription =
  127 + MobileScannerPlatform.instance.barcodesStream.listen(
  128 + (BarcodeCapture? barcode) {
126 if (_barcodesController.isClosed || barcode == null) { 129 if (_barcodesController.isClosed || barcode == null) {
127 return; 130 return;
128 } 131 }
129 132
130 _barcodesController.add(barcode); 133 _barcodesController.add(barcode);
131 - }); 134 + },
  135 + onError: (Object error) {
  136 + if (_barcodesController.isClosed) {
  137 + return;
  138 + }
  139 +
  140 + _barcodesController.addError(error);
  141 + },
  142 + // Errors are handled gracefully by forwarding them.
  143 + cancelOnError: false,
  144 + );
132 145
133 _torchStateSubscription = MobileScannerPlatform.instance.torchStateStream 146 _torchStateSubscription = MobileScannerPlatform.instance.torchStateStream
134 .listen((TorchState torchState) { 147 .listen((TorchState torchState) {
@@ -173,12 +186,20 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { @@ -173,12 +186,20 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
173 /// Analyze an image file. 186 /// Analyze an image file.
174 /// 187 ///
175 /// The [path] points to a file on the device. 188 /// The [path] points to a file on the device.
  189 + /// The [formats] specify the barcode formats that should be detected in the image.
  190 + /// If the [formats] are omitted or empty, all formats are detected.
176 /// 191 ///
177 /// This is only supported on Android, iOS and MacOS. 192 /// This is only supported on Android, iOS and MacOS.
178 /// 193 ///
179 /// Returns the [BarcodeCapture] that was found in the image. 194 /// Returns the [BarcodeCapture] that was found in the image.
180 - Future<BarcodeCapture?> analyzeImage(String path) {  
181 - return MobileScannerPlatform.instance.analyzeImage(path); 195 + ///
  196 + /// If an error occurred during the analysis of the image,
  197 + /// a [MobileScannerBarcodeException] error is thrown.
  198 + Future<BarcodeCapture?> analyzeImage(
  199 + String path, {
  200 + List<BarcodeFormat> formats = const <BarcodeFormat>[],
  201 + }) {
  202 + return MobileScannerPlatform.instance.analyzeImage(path, formats: formats);
182 } 203 }
183 204
184 /// Build a camera preview widget. 205 /// Build a camera preview widget.
@@ -246,13 +267,6 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { @@ -246,13 +267,6 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
246 ); 267 );
247 } 268 }
248 269
249 - // Permission was denied, do nothing.  
250 - // When the controller is stopped,  
251 - // the error is reset so the permission can be requested again if possible.  
252 - if (value.error?.errorCode == MobileScannerErrorCode.permissionDenied) {  
253 - return;  
254 - }  
255 -  
256 // Do nothing if the camera is already running. 270 // Do nothing if the camera is already running.
257 if (value.isRunning) { 271 if (value.isRunning) {
258 return; 272 return;
@@ -292,6 +306,13 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { @@ -292,6 +306,13 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
292 ); 306 );
293 } 307 }
294 } on MobileScannerException catch (error) { 308 } on MobileScannerException catch (error) {
  309 + // If the controller is already initialized, ignore the error.
  310 + // Starting the controller while it is already started, or in the process of starting, is redundant.
  311 + if (error.errorCode ==
  312 + MobileScannerErrorCode.controllerAlreadyInitialized) {
  313 + return;
  314 + }
  315 +
295 // The initialization finished with an error. 316 // The initialization finished with an error.
296 // To avoid stale values, reset the output size, 317 // To avoid stale values, reset the output size,
297 // torch state and zoom scale to the defaults. 318 // torch state and zoom scale to the defaults.
@@ -306,8 +327,6 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { @@ -306,8 +327,6 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
306 zoomScale: 1.0, 327 zoomScale: 1.0,
307 ); 328 );
308 } 329 }
309 - } on PermissionRequestPendingException catch (_) {  
310 - // If a permission request was already pending, do nothing.  
311 } 330 }
312 } 331 }
313 332
1 import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart'; 1 import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart';
2 2
3 -/// This class represents an exception thrown by the mobile scanner. 3 +/// This class represents an exception thrown by the [MobileScannerController].
4 class MobileScannerException implements Exception { 4 class MobileScannerException implements Exception {
5 const MobileScannerException({ 5 const MobileScannerException({
6 required this.errorCode, 6 required this.errorCode,
@@ -16,9 +16,9 @@ class MobileScannerException implements Exception { @@ -16,9 +16,9 @@ class MobileScannerException implements Exception {
16 @override 16 @override
17 String toString() { 17 String toString() {
18 if (errorDetails != null && errorDetails?.message != null) { 18 if (errorDetails != null && errorDetails?.message != null) {
19 - return "MobileScannerException: code ${errorCode.name}, message: ${errorDetails?.message}"; 19 + return 'MobileScannerException(${errorCode.name}, ${errorDetails?.message})';
20 } 20 }
21 - return "MobileScannerException: ${errorCode.name}"; 21 + return 'MobileScannerException(${errorCode.name})';
22 } 22 }
23 } 23 }
24 24
@@ -40,9 +40,21 @@ class MobileScannerErrorDetails { @@ -40,9 +40,21 @@ class MobileScannerErrorDetails {
40 final String? message; 40 final String? message;
41 } 41 }
42 42
43 -/// This class represents an exception that is thrown  
44 -/// when the scanner was (re)started while a permission request was pending.  
45 -///  
46 -/// This exception type is only used internally,  
47 -/// and is not part of the public API.  
48 -class PermissionRequestPendingException implements Exception {} 43 +/// This class represents an exception thrown by the [MobileScannerController]
  44 +/// when a barcode scanning error occurs when processing an input frame.
  45 +class MobileScannerBarcodeException implements Exception {
  46 + /// Creates a new [MobileScannerBarcodeException] with the given error message.
  47 + const MobileScannerBarcodeException(this.message);
  48 +
  49 + /// The error message of the exception.
  50 + final String? message;
  51 +
  52 + @override
  53 + String toString() {
  54 + if (message?.isNotEmpty ?? false) {
  55 + return 'MobileScannerBarcodeException($message)';
  56 + }
  57 +
  58 + return 'MobileScannerBarcodeException(Could not detect a barcode in the input image.)';
  59 + }
  60 +}
@@ -32,8 +32,6 @@ class BarcodeCapture { @@ -32,8 +32,6 @@ class BarcodeCapture {
32 /// This is the data that was used to detect the available [barcodes], the input [image] and the [size]. 32 /// This is the data that was used to detect the available [barcodes], the input [image] and the [size].
33 final Object? raw; 33 final Object? raw;
34 34
35 - /// The size of the input [image].  
36 - ///  
37 - /// If [image] is null, this will be [Size.zero]. 35 + /// The size of the camera input [image].
38 final Size size; 36 final Size size;
39 } 37 }
1 import 'dart:ui'; 1 import 'dart:ui';
2 2
3 import 'package:mobile_scanner/src/enums/camera_facing.dart'; 3 import 'package:mobile_scanner/src/enums/camera_facing.dart';
  4 +import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart';
4 import 'package:mobile_scanner/src/enums/torch_state.dart'; 5 import 'package:mobile_scanner/src/enums/torch_state.dart';
5 import 'package:mobile_scanner/src/mobile_scanner_exception.dart'; 6 import 'package:mobile_scanner/src/mobile_scanner_exception.dart';
6 7
@@ -43,7 +44,8 @@ class MobileScannerState { @@ -43,7 +44,8 @@ class MobileScannerState {
43 44
44 /// Whether the mobile scanner has initialized successfully. 45 /// Whether the mobile scanner has initialized successfully.
45 /// 46 ///
46 - /// This is `true` if the camera is ready to be used. 47 + /// This does not indicate that the camera permission was granted.
  48 + /// To check if the camera permission was granted, use [hasCameraPermission].
47 final bool isInitialized; 49 final bool isInitialized;
48 50
49 /// Whether the mobile scanner is currently running. 51 /// Whether the mobile scanner is currently running.
@@ -60,6 +62,12 @@ class MobileScannerState { @@ -60,6 +62,12 @@ class MobileScannerState {
60 /// The current zoom scale of the camera. 62 /// The current zoom scale of the camera.
61 final double zoomScale; 63 final double zoomScale;
62 64
  65 + /// Whether permission to access the camera was granted.
  66 + bool get hasCameraPermission {
  67 + return isInitialized &&
  68 + error?.errorCode != MobileScannerErrorCode.permissionDenied;
  69 + }
  70 +
63 /// Create a copy of this state with the given parameters. 71 /// Create a copy of this state with the given parameters.
64 MobileScannerState copyWith({ 72 MobileScannerState copyWith({
65 int? availableCameras, 73 int? availableCameras,
@@ -39,12 +39,6 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -39,12 +39,6 @@ class MobileScannerWeb extends MobileScannerPlatform {
39 /// The container div element for the camera view. 39 /// The container div element for the camera view.
40 late HTMLDivElement _divElement; 40 late HTMLDivElement _divElement;
41 41
42 - /// The flag that keeps track of whether a permission request is in progress.  
43 - ///  
44 - /// On the web, a permission request triggers a dialog, that in turn triggers a lifecycle change.  
45 - /// While the permission request is in progress, any attempts at (re)starting the camera should be ignored.  
46 - bool _permissionRequestInProgress = false;  
47 -  
48 /// The stream controller for the media track settings stream. 42 /// The stream controller for the media track settings stream.
49 /// 43 ///
50 /// Currently, only the facing mode setting can be supported, 44 /// Currently, only the facing mode setting can be supported,
@@ -149,6 +143,7 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -149,6 +143,7 @@ class MobileScannerWeb extends MobileScannerPlatform {
149 final JSArray<JSString>? facingModes = capabilities.facingModeNullable; 143 final JSArray<JSString>? facingModes = capabilities.facingModeNullable;
150 144
151 // TODO: this is an empty array on MacOS Chrome, where there is no facing mode, but one, user facing camera. 145 // TODO: this is an empty array on MacOS Chrome, where there is no facing mode, but one, user facing camera.
  146 + // We might be able to add a workaround, using the label of the video track.
152 // Facing mode is not supported by this track, do nothing. 147 // Facing mode is not supported by this track, do nothing.
153 if (facingModes == null || facingModes.toDart.isEmpty) { 148 if (facingModes == null || facingModes.toDart.isEmpty) {
154 return; 149 return;
@@ -199,17 +194,12 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -199,17 +194,12 @@ class MobileScannerWeb extends MobileScannerPlatform {
199 } 194 }
200 195
201 try { 196 try {
202 - _permissionRequestInProgress = true;  
203 -  
204 // Retrieving the media devices requests the camera permission. 197 // Retrieving the media devices requests the camera permission.
205 final MediaStream videoStream = 198 final MediaStream videoStream =
206 await window.navigator.mediaDevices.getUserMedia(constraints).toDart; 199 await window.navigator.mediaDevices.getUserMedia(constraints).toDart;
207 200
208 - _permissionRequestInProgress = false;  
209 -  
210 return videoStream; 201 return videoStream;
211 } on DOMException catch (error, stackTrace) { 202 } on DOMException catch (error, stackTrace) {
212 - _permissionRequestInProgress = false;  
213 final String errorMessage = error.toString(); 203 final String errorMessage = error.toString();
214 204
215 MobileScannerErrorCode errorCode = MobileScannerErrorCode.genericError; 205 MobileScannerErrorCode errorCode = MobileScannerErrorCode.genericError;
@@ -272,11 +262,13 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -272,11 +262,13 @@ class MobileScannerWeb extends MobileScannerPlatform {
272 262
273 @override 263 @override
274 Future<MobileScannerViewAttributes> start(StartOptions startOptions) async { 264 Future<MobileScannerViewAttributes> start(StartOptions startOptions) async {
275 - // If the permission request has not yet completed,  
276 - // the camera view is not ready yet.  
277 - // Prevent the permission popup from triggering a restart of the scanner.  
278 - if (_permissionRequestInProgress) {  
279 - throw PermissionRequestPendingException(); 265 + if (_barcodeReader != null) {
  266 + throw const MobileScannerException(
  267 + errorCode: MobileScannerErrorCode.controllerAlreadyInitialized,
  268 + errorDetails: MobileScannerErrorDetails(
  269 + message: 'The scanner was already started.',
  270 + ),
  271 + );
280 } 272 }
281 273
282 _barcodeReader = ZXingBarcodeReader(); 274 _barcodeReader = ZXingBarcodeReader();
@@ -285,16 +277,6 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -285,16 +277,6 @@ class MobileScannerWeb extends MobileScannerPlatform {
285 alternateScriptUrl: _alternateScriptUrl, 277 alternateScriptUrl: _alternateScriptUrl,
286 ); 278 );
287 279
288 - if (_barcodeReader?.isScanning ?? false) {  
289 - throw const MobileScannerException(  
290 - errorCode: MobileScannerErrorCode.controllerAlreadyInitialized,  
291 - errorDetails: MobileScannerErrorDetails(  
292 - message:  
293 - 'The scanner was already started. Call stop() before calling start() again.',  
294 - ),  
295 - );  
296 - }  
297 -  
298 // Request camera permissions and prepare the video stream. 280 // Request camera permissions and prepare the video stream.
299 final MediaStream videoStream = await _prepareVideoStream( 281 final MediaStream videoStream = await _prepareVideoStream(
300 startOptions.cameraDirection, 282 startOptions.cameraDirection,
@@ -341,6 +323,15 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -341,6 +323,15 @@ class MobileScannerWeb extends MobileScannerPlatform {
341 323
342 _barcodesController.add(barcode); 324 _barcodesController.add(barcode);
343 }, 325 },
  326 + onError: (Object error) {
  327 + if (_barcodesController.isClosed) {
  328 + return;
  329 + }
  330 +
  331 + _barcodesController.addError(error);
  332 + },
  333 + // Errors are handled gracefully by forwarding them.
  334 + cancelOnError: false,
344 ); 335 );
345 336
346 final bool hasTorch = await _barcodeReader?.hasTorch() ?? false; 337 final bool hasTorch = await _barcodeReader?.hasTorch() ?? false;
@@ -2,7 +2,9 @@ import 'dart:async'; @@ -2,7 +2,9 @@ import 'dart:async';
2 import 'dart:js_interop'; 2 import 'dart:js_interop';
3 import 'dart:ui'; 3 import 'dart:ui';
4 4
  5 +import 'package:flutter/foundation.dart';
5 import 'package:mobile_scanner/src/enums/barcode_format.dart'; 6 import 'package:mobile_scanner/src/enums/barcode_format.dart';
  7 +import 'package:mobile_scanner/src/mobile_scanner_exception.dart';
6 import 'package:mobile_scanner/src/objects/barcode_capture.dart'; 8 import 'package:mobile_scanner/src/objects/barcode_capture.dart';
7 import 'package:mobile_scanner/src/objects/start_options.dart'; 9 import 'package:mobile_scanner/src/objects/start_options.dart';
8 import 'package:mobile_scanner/src/web/barcode_reader.dart'; 10 import 'package:mobile_scanner/src/web/barcode_reader.dart';
@@ -10,12 +12,18 @@ import 'package:mobile_scanner/src/web/javascript_map.dart'; @@ -10,12 +12,18 @@ import 'package:mobile_scanner/src/web/javascript_map.dart';
10 import 'package:mobile_scanner/src/web/media_track_constraints_delegate.dart'; 12 import 'package:mobile_scanner/src/web/media_track_constraints_delegate.dart';
11 import 'package:mobile_scanner/src/web/zxing/result.dart'; 13 import 'package:mobile_scanner/src/web/zxing/result.dart';
12 import 'package:mobile_scanner/src/web/zxing/zxing_browser_multi_format_reader.dart'; 14 import 'package:mobile_scanner/src/web/zxing/zxing_browser_multi_format_reader.dart';
  15 +import 'package:mobile_scanner/src/web/zxing/zxing_exception.dart';
13 import 'package:web/web.dart' as web; 16 import 'package:web/web.dart' as web;
14 17
15 /// A barcode reader implementation that uses the ZXing library. 18 /// A barcode reader implementation that uses the ZXing library.
16 final class ZXingBarcodeReader extends BarcodeReader { 19 final class ZXingBarcodeReader extends BarcodeReader {
17 ZXingBarcodeReader(); 20 ZXingBarcodeReader();
18 21
  22 + /// ZXing reports an error with this message if the code could not be detected.
  23 + @visibleForTesting
  24 + static const String kNoCodeDetectedErrorMessage =
  25 + 'No MultiFormat Readers were able to detect the code.';
  26 +
19 /// The listener for media track settings changes. 27 /// The listener for media track settings changes.
20 void Function(web.MediaTrackSettings)? _onMediaTrackSettingsChanged; 28 void Function(web.MediaTrackSettings)? _onMediaTrackSettingsChanged;
21 29
@@ -98,16 +106,25 @@ final class ZXingBarcodeReader extends BarcodeReader { @@ -98,16 +106,25 @@ final class ZXingBarcodeReader extends BarcodeReader {
98 _reader?.decodeContinuously.callAsFunction( 106 _reader?.decodeContinuously.callAsFunction(
99 _reader, 107 _reader,
100 _reader?.videoElement, 108 _reader?.videoElement,
101 - (Result? result, JSAny? error) {  
102 - if (controller.isClosed || result == null) { 109 + (Result? result, ZXingException? error) {
  110 + if (controller.isClosed) {
103 return; 111 return;
104 } 112 }
105 113
  114 + // Skip the event if no code was detected.
  115 + if (error != null && error.message != kNoCodeDetectedErrorMessage) {
  116 + controller.addError(MobileScannerBarcodeException(error.message));
  117 + return;
  118 + }
  119 +
  120 + if (result != null) {
106 controller.add( 121 controller.add(
107 BarcodeCapture( 122 BarcodeCapture(
108 barcodes: [result.toBarcode], 123 barcodes: [result.toBarcode],
  124 + size: videoSize,
109 ), 125 ),
110 ); 126 );
  127 + }
111 }.toJS, 128 }.toJS,
112 ); 129 );
113 }; 130 };
  1 +import 'dart:js_interop';
  2 +
  3 +/// The JS static interop class for the Result class in the ZXing library.
  4 +///
  5 +/// See also: https://github.com/zxing-js/library/blob/master/src/core/Exception.ts
  6 +@JS('ZXing.Exception')
  7 +extension type ZXingException._(JSObject _) implements JSObject {
  8 + /// The error message of the exception, if any.
  9 + external String? get message;
  10 +}
@@ -4,7 +4,7 @@ @@ -4,7 +4,7 @@
4 # 4 #
5 Pod::Spec.new do |s| 5 Pod::Spec.new do |s|
6 s.name = 'mobile_scanner' 6 s.name = 'mobile_scanner'
7 - s.version = '5.2.3' 7 + s.version = '6.0.2'
8 s.summary = 'An universal scanner for Flutter based on MLKit.' 8 s.summary = 'An universal scanner for Flutter based on MLKit.'
9 s.description = <<-DESC 9 s.description = <<-DESC
10 An universal scanner for Flutter based on MLKit. 10 An universal scanner for Flutter based on MLKit.
@@ -18,4 +18,5 @@ An universal scanner for Flutter based on MLKit. @@ -18,4 +18,5 @@ An universal scanner for Flutter based on MLKit.
18 s.platform = :osx, '10.14' 18 s.platform = :osx, '10.14'
19 s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } 19 s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
20 s.swift_version = '5.0' 20 s.swift_version = '5.0'
  21 + s.resource_bundles = {'mobile_scanner_macos_privacy' => ['mobile_scanner/Sources/mobile_scanner/Resources/PrivacyInfo.xcprivacy']}
21 end 22 end
@@ -17,8 +17,7 @@ let package = Package( @@ -17,8 +17,7 @@ let package = Package(
17 name: "mobile_scanner", 17 name: "mobile_scanner",
18 dependencies: [], 18 dependencies: [],
19 resources: [ 19 resources: [
20 - // To add other resources, see the instructions at  
21 - // https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package 20 + .process("Resources"),
22 ] 21 ]
23 ) 22 )
24 ] 23 ]
  1 +//
  2 +// MobileScannerErrorCodes.swift
  3 +// mobile_scanner
  4 +//
  5 +// Created by Navaron Bracke on 27/05/2024.
  6 +//
  7 +
  8 +import Foundation
  9 +
  10 +struct MobileScannerErrorCodes {
  11 + static let ALREADY_STARTED_ERROR = "MOBILE_SCANNER_ALREADY_STARTED_ERROR"
  12 + static let ALREADY_STARTED_ERROR_MESSAGE = "The scanner was already started."
  13 + // The error code 'BARCODE_ERROR' does not have an error message,
  14 + // because it uses the error message from the undelying error.
  15 + static let BARCODE_ERROR = "MOBILE_SCANNER_BARCODE_ERROR"
  16 + // The error code 'CAMERA_ERROR' does not have an error message,
  17 + // because it uses the error message from the underlying error.
  18 + static let CAMERA_ERROR = "MOBILE_SCANNER_CAMERA_ERROR"
  19 + static let NO_CAMERA_ERROR = "MOBILE_SCANNER_NO_CAMERA_ERROR"
  20 + static let NO_CAMERA_ERROR_MESSAGE = "No cameras available."
  21 +}
@@ -131,7 +131,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -131,7 +131,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
131 131
132 if error != nil { 132 if error != nil {
133 DispatchQueue.main.async { 133 DispatchQueue.main.async {
134 - self?.sink?(FlutterError(code: "MobileScanner", message: error?.localizedDescription, details: nil)) 134 + self?.sink?(FlutterError(
  135 + code: MobileScannerErrorCodes.BARCODE_ERROR,
  136 + message: error?.localizedDescription, details: nil))
135 } 137 }
136 return 138 return
137 } 139 }
@@ -154,7 +156,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -154,7 +156,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
154 }) 156 })
155 157
156 DispatchQueue.main.async { 158 DispatchQueue.main.async {
157 - if (!MobileScannerPlugin.returnImage) { 159 + guard let image = cgImage else {
158 self?.sink?([ 160 self?.sink?([
159 "name": "barcode", 161 "name": "barcode",
160 "data": barcodes.map({ $0.toMap() }), 162 "data": barcodes.map({ $0.toMap() }),
@@ -162,14 +164,18 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -162,14 +164,18 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
162 return 164 return
163 } 165 }
164 166
  167 + // The image dimensions are always provided.
  168 + // The image bytes are only non-null when `returnImage` is true.
  169 + let imageData: [String: Any?] = [
  170 + "bytes": MobileScannerPlugin.returnImage ? FlutterStandardTypedData(bytes: image.jpegData(compressionQuality: 0.8)!) : nil,
  171 + "width": Double(image.width),
  172 + "height": Double(image.height),
  173 + ]
  174 +
165 self?.sink?([ 175 self?.sink?([
166 "name": "barcode", 176 "name": "barcode",
167 "data": barcodes.map({ $0.toMap() }), 177 "data": barcodes.map({ $0.toMap() }),
168 - "image": cgImage == nil ? nil : [  
169 - "bytes": FlutterStandardTypedData(bytes: cgImage!.jpegData(compressionQuality: 0.8)!),  
170 - "width": Double(cgImage!.width),  
171 - "height": Double(cgImage!.height),  
172 - ], 178 + "image": imageData,
173 ]) 179 ])
174 } 180 }
175 }) 181 })
@@ -180,9 +186,11 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -180,9 +186,11 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
180 } 186 }
181 187
182 try imageRequestHandler.perform([barcodeRequest]) 188 try imageRequestHandler.perform([barcodeRequest])
183 - } catch let e { 189 + } catch let error {
184 DispatchQueue.main.async { 190 DispatchQueue.main.async {
185 - self?.sink?(FlutterError(code: "MobileScanner", message: e.localizedDescription, details: nil)) 191 + self?.sink?(FlutterError(
  192 + code: MobileScannerErrorCodes.BARCODE_ERROR,
  193 + message: error.localizedDescription, details: nil))
186 } 194 }
187 } 195 }
188 } 196 }
@@ -262,8 +270,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -262,8 +270,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
262 270
263 func start(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { 271 func start(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
264 if (device != nil || captureSession != nil) { 272 if (device != nil || captureSession != nil) {
265 - result(FlutterError(code: "MobileScanner",  
266 - message: "Called start() while already started!", 273 + result(FlutterError(code: MobileScannerErrorCodes.ALREADY_STARTED_ERROR,
  274 + message: MobileScannerErrorCodes.ALREADY_STARTED_ERROR_MESSAGE,
267 details: nil)) 275 details: nil))
268 return 276 return
269 } 277 }
@@ -273,7 +281,6 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -273,7 +281,6 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
273 281
274 let argReader = MapArgumentReader(call.arguments as? [String: Any]) 282 let argReader = MapArgumentReader(call.arguments as? [String: Any])
275 283
276 - // let ratio: Int = argReader.int(key: "ratio")  
277 let torch:Bool = argReader.bool(key: "torch") ?? false 284 let torch:Bool = argReader.bool(key: "torch") ?? false
278 let facing:Int = argReader.int(key: "facing") ?? 1 285 let facing:Int = argReader.int(key: "facing") ?? 1
279 let speed:Int = argReader.int(key: "speed") ?? 0 286 let speed:Int = argReader.int(key: "speed") ?? 0
@@ -295,8 +302,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -295,8 +302,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
295 } 302 }
296 303
297 if (device == nil) { 304 if (device == nil) {
298 - result(FlutterError(code: "MobileScanner",  
299 - message: "No camera found or failed to open camera!", 305 + result(FlutterError(code: MobileScannerErrorCodes.NO_CAMERA_ERROR,
  306 + message: MobileScannerErrorCodes.NO_CAMERA_ERROR_MESSAGE,
300 details: nil)) 307 details: nil))
301 return 308 return
302 } 309 }
@@ -314,7 +321,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -314,7 +321,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
314 let input = try AVCaptureDeviceInput(device: device) 321 let input = try AVCaptureDeviceInput(device: device)
315 captureSession!.addInput(input) 322 captureSession!.addInput(input)
316 } catch { 323 } catch {
317 - result(FlutterError(code: "MobileScanner", message: error.localizedDescription, details: nil)) 324 + result(FlutterError(
  325 + code: MobileScannerErrorCodes.CAMERA_ERROR,
  326 + message: error.localizedDescription, details: nil))
318 return 327 return
319 } 328 }
320 captureSession!.sessionPreset = AVCaptureSession.Preset.photo 329 captureSession!.sessionPreset = AVCaptureSession.Preset.photo
@@ -327,7 +336,6 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -327,7 +336,6 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
327 videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue.main) 336 videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue.main)
328 captureSession!.addOutput(videoOutput) 337 captureSession!.addOutput(videoOutput)
329 for connection in videoOutput.connections { 338 for connection in videoOutput.connections {
330 - // connection.videoOrientation = .portrait  
331 if position == .front && connection.isVideoMirroringSupported { 339 if position == .front && connection.isVideoMirroringSupported {
332 connection.isVideoMirrored = true 340 connection.isVideoMirrored = true
333 } 341 }
@@ -459,20 +467,14 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -459,20 +467,14 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
459 let symbologies:[VNBarcodeSymbology] = argReader.toSymbology() 467 let symbologies:[VNBarcodeSymbology] = argReader.toSymbology()
460 468
461 guard let filePath: String = argReader.string(key: "filePath") else { 469 guard let filePath: String = argReader.string(key: "filePath") else {
462 - // TODO: fix error code  
463 - result(FlutterError(code: "MobileScanner",  
464 - message: "No image found in analyzeImage!",  
465 - details: nil)) 470 + result(nil)
466 return 471 return
467 } 472 }
468 473
469 let fileUrl = URL(fileURLWithPath: filePath) 474 let fileUrl = URL(fileURLWithPath: filePath)
470 475
471 guard let ciImage = CIImage(contentsOf: fileUrl) else { 476 guard let ciImage = CIImage(contentsOf: fileUrl) else {
472 - // TODO: fix error code  
473 - result(FlutterError(code: "MobileScanner",  
474 - message: "No image found in analyzeImage!",  
475 - details: nil)) 477 + result(nil)
476 return 478 return
477 } 479 }
478 480
@@ -484,8 +486,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -484,8 +486,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
484 486
485 if error != nil { 487 if error != nil {
486 DispatchQueue.main.async { 488 DispatchQueue.main.async {
487 - // TODO: fix error code  
488 - result(FlutterError(code: "MobileScanner", message: error?.localizedDescription, details: nil)) 489 + result(FlutterError(
  490 + code: MobileScannerErrorCodes.BARCODE_ERROR,
  491 + message: error?.localizedDescription, details: nil))
489 } 492 }
490 return 493 return
491 } 494 }
@@ -510,10 +513,11 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -510,10 +513,11 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
510 } 513 }
511 514
512 try imageRequestHandler.perform([barcodeRequest]) 515 try imageRequestHandler.perform([barcodeRequest])
513 - } catch let e {  
514 - // TODO: fix error code 516 + } catch let error {
515 DispatchQueue.main.async { 517 DispatchQueue.main.async {
516 - result(FlutterError(code: "MobileScanner", message: e.localizedDescription, details: nil)) 518 + result(FlutterError(
  519 + code: MobileScannerErrorCodes.BARCODE_ERROR,
  520 + message: error.localizedDescription, details: nil))
517 } 521 }
518 } 522 }
519 } 523 }
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
  3 +<plist version="1.0">
  4 +<dict>
  5 + <!-- The key NSPrivacyAccessedAPITypes is not required for MacOS -->
  6 + <key>NSPrivacyTrackingDomains</key>
  7 + <array/>
  8 + <key>NSPrivacyCollectedDataTypes</key>
  9 + <array/>
  10 + <key>NSPrivacyTracking</key>
  11 + <false/>
  12 +</dict>
  13 +</plist>
1 name: mobile_scanner 1 name: mobile_scanner
2 description: A universal barcode and QR code scanner for Flutter based on MLKit. Uses CameraX on Android, AVFoundation on iOS and Apple Vision & AVFoundation on macOS. 2 description: A universal barcode and QR code scanner for Flutter based on MLKit. Uses CameraX on Android, AVFoundation on iOS and Apple Vision & AVFoundation on macOS.
3 -version: 5.2.3 3 +version: 6.0.2
4 repository: https://github.com/juliansteenbakker/mobile_scanner 4 repository: https://github.com/juliansteenbakker/mobile_scanner
5 5
6 screenshots: 6 screenshots: