Sander Roest
Committed by GitHub

Merge branch 'juliansteenbakker:master' into master

1 ## NEXT 1 ## NEXT
2 2
  3 +Improvements:
3 * [MacOS] Added the corners and size information to barcode results. 4 * [MacOS] Added the corners and size information to barcode results.
4 * [MacOS] Added support for `analyzeImage`. 5 * [MacOS] Added support for `analyzeImage`.
  6 +* [MacOS] Added a Privacy Manifest.
5 * [web] Added the size information to barcode results. 7 * [web] Added the size information to barcode results.
  8 +* [web] Added the video output size information to barcode capture.
6 * Added support for barcode formats to image analysis. 9 * Added support for barcode formats to image analysis.
  10 +* Updated the scanner to report any scanning errors that were encountered during processing.
  11 +* Introduced a new getter `hasCameraPermission` for the `MobileScannerState`.
  12 +* Fixed a bug in the lifecycle handling sample. Now instead of checking `isInitialized`,
  13 +the sample recommends using `hasCameraPermission`, which also guards against camera permission errors.
  14 +* Updated the behavior of `returnImage` to only determine if the camera output bytes should be sent.
  15 +* Updated the behavior of `BarcodeCapture.size` to always be provided when available, regardless of `returnImage`.
  16 +
  17 +Bugs fixed:
  18 +* 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.
  19 +* [MacOS] Fixed a bug that prevented the `anaylzeImage()` sample from working properly.
7 20
8 ## 5.2.3 21 ## 5.2.3
9 22
@@ -127,7 +127,7 @@ class MyState extends State<MyStatefulWidget> with WidgetsBindingObserver { @@ -127,7 +127,7 @@ class MyState extends State<MyStatefulWidget> with WidgetsBindingObserver {
127 void didChangeAppLifecycleState(AppLifecycleState state) { 127 void didChangeAppLifecycleState(AppLifecycleState state) {
128 // If the controller is not ready, do not try to start or stop it. 128 // 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. 129 // Permission dialogs can trigger lifecycle changes before the controller is ready.
130 - if (!controller.value.isInitialized) { 130 + if (!controller.value.hasCameraPermission) {
131 return; 131 return;
132 } 132 }
133 133
@@ -192,4 +192,4 @@ Future<void> dispose() async { @@ -192,4 +192,4 @@ Future<void> dispose() async {
192 To display the camera preview, pass the controller to a `MobileScanner` widget. 192 To display the camera preview, pass the controller to a `MobileScanner` widget.
193 193
194 See the [examples](example/README.md) for runnable examples of various usages, 194 See the [examples](example/README.md) for runnable examples of various usages,
195 -such as the basic usage, applying a scan window, or retrieving images from the barcodes. 195 +such as the basic usage, applying a scan window, or retrieving images from the barcodes.
@@ -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)
@@ -120,7 +120,11 @@ class MobileScanner( @@ -120,7 +120,11 @@ class MobileScanner(
120 } 120 }
121 121
122 if (!returnImage) { 122 if (!returnImage) {
123 - mobileScannerCallback(barcodeMap, null, null, null) 123 + mobileScannerCallback(
  124 + barcodeMap,
  125 + null,
  126 + mediaImage.width,
  127 + mediaImage.height)
124 return@addOnSuccessListener 128 return@addOnSuccessListener
125 } 129 }
126 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(  
51 - "name" to "barcode",  
52 - "data" to barcodes,  
53 - "image" to mapOf(  
54 - "bytes" to image,  
55 - "width" to width?.toDouble(),  
56 - "height" to height?.toDouble(),  
57 - )  
58 - ))  
59 - } else {  
60 - barcodeHandler.publishEvent(mapOf(  
61 - "name" to "barcode",  
62 - "data" to barcodes  
63 - ))  
64 - } 50 + barcodeHandler.publishEvent(mapOf(
  51 + "name" to "barcode",
  52 + "data" to barcodes,
  53 + // The image dimensions are always provided.
  54 + // The image bytes are only non-null when `returnImage` is true.
  55 + "image" to mapOf(
  56 + "bytes" to image,
  57 + "width" to width?.toDouble(),
  58 + "height" to height?.toDouble(),
  59 + )
  60 + ))
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
@@ -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
@@ -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";
@@ -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 }
@@ -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 - Icons.no_flash,  
171 - color: Colors.grey, 170 + return const SizedBox.square(
  171 + dimension: 48.0,
  172 + child: Icon(
  173 + Icons.no_flash,
  174 + size: 32.0,
  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)
@@ -20,7 +20,7 @@ struct MobileScannerErrorCodes { @@ -20,7 +20,7 @@ struct MobileScannerErrorCodes {
20 // because it uses the error message from the undelying error. 20 // because it uses the error message from the undelying error.
21 static let BARCODE_ERROR = "MOBILE_SCANNER_BARCODE_ERROR" 21 static let BARCODE_ERROR = "MOBILE_SCANNER_BARCODE_ERROR"
22 // The error code 'CAMERA_ERROR' does not have an error message, 22 // The error code 'CAMERA_ERROR' does not have an error message,
23 - // because it uses the error message from the underlying error. 23 + // because it uses the error message from the underlying error.
24 static let CAMERA_ERROR = "MOBILE_SCANNER_CAMERA_ERROR" 24 static let CAMERA_ERROR = "MOBILE_SCANNER_CAMERA_ERROR"
25 static let GENERIC_ERROR = "MOBILE_SCANNER_GENERIC_ERROR" 25 static let GENERIC_ERROR = "MOBILE_SCANNER_GENERIC_ERROR"
26 static let GENERIC_ERROR_MESSAGE = "An unknown error occurred." 26 static let GENERIC_ERROR_MESSAGE = "An unknown error occurred."
@@ -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 }
@@ -266,7 +265,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { @@ -266,7 +265,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
266 barcodeScannerOptions: scannerOptions, callback: { barcodes, error in 265 barcodeScannerOptions: scannerOptions, callback: { barcodes, error in
267 if error != nil { 266 if error != nil {
268 DispatchQueue.main.async { 267 DispatchQueue.main.async {
269 - result(FlutterError(code: "MobileScanner", 268 + result(FlutterError(code: MobileScannerErrorCodes.BARCODE_ERROR,
270 message: error?.localizedDescription, 269 message: error?.localizedDescription,
271 details: nil)) 270 details: nil))
272 } 271 }
@@ -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';
@@ -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,21 +169,30 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { @@ -145,21 +169,30 @@ 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 {
148 - final Map<Object?, Object?>? result =  
149 - await methodChannel.invokeMapMethod<Object?, Object?>(  
150 - 'analyzeImage',  
151 - {  
152 - 'filePath': path,  
153 - 'formats': formats.isEmpty  
154 - ? null  
155 - : [  
156 - for (final BarcodeFormat format in formats)  
157 - if (format != BarcodeFormat.unknown) format.rawValue,  
158 - ],  
159 - },  
160 - ); 172 + try {
  173 + final Map<Object?, Object?>? result =
  174 + await methodChannel.invokeMapMethod<Object?, Object?>(
  175 + 'analyzeImage',
  176 + {
  177 + 'filePath': path,
  178 + 'formats': formats.isEmpty
  179 + ? null
  180 + : [
  181 + for (final BarcodeFormat format in formats)
  182 + if (format != BarcodeFormat.unknown) format.rawValue,
  183 + ],
  184 + },
  185 + );
  186 +
  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 + }
161 193
162 - return _parseBarcode(result); 194 + return null;
  195 + }
163 } 196 }
164 197
165 @override 198 @override
@@ -35,6 +35,12 @@ class MobileScanner extends StatefulWidget { @@ -35,6 +35,12 @@ class MobileScanner extends StatefulWidget {
35 35
36 /// The function that signals when new codes were detected by the [controller]. 36 /// The function that signals when new codes were detected by the [controller].
37 /// If null, use the controller.barcodes stream directly to capture barcodes. 37 /// If null, use the controller.barcodes stream directly to capture barcodes.
  38 + ///
  39 + /// This method does not receive any [MobileScannerBarcodeException]s
  40 + /// that are emitted by the scanner.
  41 + ///
  42 + /// To handle both [BarcodeCapture]s and [MobileScannerBarcodeException]s,
  43 + /// use the [MobileScannerController.barcodes] stream directly.
38 final void Function(BarcodeCapture barcodes)? onDetect; 44 final void Function(BarcodeCapture barcodes)? onDetect;
39 45
40 /// The error builder for the camera preview. 46 /// The error builder for the camera preview.
@@ -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 - if (_barcodesController.isClosed || barcode == null) {  
127 - return;  
128 - }  
129 -  
130 - _barcodesController.add(barcode);  
131 - }); 126 + _barcodesSubscription =
  127 + MobileScannerPlatform.instance.barcodesStream.listen(
  128 + (BarcodeCapture? barcode) {
  129 + if (_barcodesController.isClosed || barcode == null) {
  130 + return;
  131 + }
  132 +
  133 + _barcodesController.add(barcode);
  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) {
@@ -177,6 +190,9 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { @@ -177,6 +190,9 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
177 /// This is only supported on Android, iOS and MacOS. 190 /// This is only supported on Android, iOS and MacOS.
178 /// 191 ///
179 /// Returns the [BarcodeCapture] that was found in the image. 192 /// Returns the [BarcodeCapture] that was found in the image.
  193 + ///
  194 + /// If an error occurred during the analysis of the image,
  195 + /// a [MobileScannerBarcodeException] error is thrown.
180 Future<BarcodeCapture?> analyzeImage(String path) { 196 Future<BarcodeCapture?> analyzeImage(String path) {
181 return MobileScannerPlatform.instance.analyzeImage(path); 197 return MobileScannerPlatform.instance.analyzeImage(path);
182 } 198 }
@@ -246,13 +262,6 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { @@ -246,13 +262,6 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
246 ); 262 );
247 } 263 }
248 264
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. 265 // Do nothing if the camera is already running.
257 if (value.isRunning) { 266 if (value.isRunning) {
258 return; 267 return;
@@ -292,6 +301,13 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { @@ -292,6 +301,13 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
292 ); 301 );
293 } 302 }
294 } on MobileScannerException catch (error) { 303 } on MobileScannerException catch (error) {
  304 + // If the controller is already initialized, ignore the error.
  305 + // Starting the controller while it is already started, or in the process of starting, is redundant.
  306 + if (error.errorCode ==
  307 + MobileScannerErrorCode.controllerAlreadyInitialized) {
  308 + return;
  309 + }
  310 +
295 // The initialization finished with an error. 311 // The initialization finished with an error.
296 // To avoid stale values, reset the output size, 312 // To avoid stale values, reset the output size,
297 // torch state and zoom scale to the defaults. 313 // torch state and zoom scale to the defaults.
@@ -306,8 +322,6 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { @@ -306,8 +322,6 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
306 zoomScale: 1.0, 322 zoomScale: 1.0,
307 ); 323 );
308 } 324 }
309 - } on PermissionRequestPendingException catch (_) {  
310 - // If a permission request was already pending, do nothing.  
311 } 325 }
312 } 326 }
313 327
@@ -40,13 +40,6 @@ class MobileScannerErrorDetails { @@ -40,13 +40,6 @@ 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 {}  
49 -  
50 /// This class represents an exception thrown by the [MobileScannerController] 43 /// This class represents an exception thrown by the [MobileScannerController]
51 /// when a barcode scanning error occurs when processing an input frame. 44 /// when a barcode scanning error occurs when processing an input frame.
52 class MobileScannerBarcodeException implements Exception { 45 class MobileScannerBarcodeException implements Exception {
@@ -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 }
@@ -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
106 - controller.add(  
107 - BarcodeCapture(  
108 - barcodes: [result.toBarcode],  
109 - ),  
110 - ); 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) {
  121 + controller.add(
  122 + BarcodeCapture(
  123 + barcodes: [result.toBarcode],
  124 + size: videoSize,
  125 + ),
  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 +}
@@ -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 ]
@@ -14,7 +14,7 @@ struct MobileScannerErrorCodes { @@ -14,7 +14,7 @@ struct MobileScannerErrorCodes {
14 // because it uses the error message from the undelying error. 14 // because it uses the error message from the undelying error.
15 static let BARCODE_ERROR = "MOBILE_SCANNER_BARCODE_ERROR" 15 static let BARCODE_ERROR = "MOBILE_SCANNER_BARCODE_ERROR"
16 // The error code 'CAMERA_ERROR' does not have an error message, 16 // The error code 'CAMERA_ERROR' does not have an error message,
17 - // because it uses the error message from the underlying error. 17 + // because it uses the error message from the underlying error.
18 static let CAMERA_ERROR = "MOBILE_SCANNER_CAMERA_ERROR" 18 static let CAMERA_ERROR = "MOBILE_SCANNER_CAMERA_ERROR"
19 static let NO_CAMERA_ERROR = "MOBILE_SCANNER_NO_CAMERA_ERROR" 19 static let NO_CAMERA_ERROR = "MOBILE_SCANNER_NO_CAMERA_ERROR"
20 static let NO_CAMERA_ERROR_MESSAGE = "No cameras available." 20 static let NO_CAMERA_ERROR_MESSAGE = "No cameras available."
@@ -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,22 +156,26 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -154,22 +156,26 @@ 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() }),
161 ]) 163 ])
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 }
@@ -294,8 +302,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -294,8 +302,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
294 } 302 }
295 303
296 if (device == nil) { 304 if (device == nil) {
297 - result(FlutterError(code: "MobileScanner",  
298 - message: "No camera found or failed to open camera!", 305 + result(FlutterError(code: MobileScannerErrorCodes.NO_CAMERA_ERROR,
  306 + message: MobileScannerErrorCodes.NO_CAMERA_ERROR_MESSAGE,
299 details: nil)) 307 details: nil))
300 return 308 return
301 } 309 }
@@ -313,7 +321,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -313,7 +321,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
313 let input = try AVCaptureDeviceInput(device: device) 321 let input = try AVCaptureDeviceInput(device: device)
314 captureSession!.addInput(input) 322 captureSession!.addInput(input)
315 } catch { 323 } catch {
316 - result(FlutterError(code: "MobileScanner", message: error.localizedDescription, details: nil)) 324 + result(FlutterError(
  325 + code: MobileScannerErrorCodes.CAMERA_ERROR,
  326 + message: error.localizedDescription, details: nil))
317 return 327 return
318 } 328 }
319 captureSession!.sessionPreset = AVCaptureSession.Preset.photo 329 captureSession!.sessionPreset = AVCaptureSession.Preset.photo
@@ -476,8 +486,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -476,8 +486,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
476 486
477 if error != nil { 487 if error != nil {
478 DispatchQueue.main.async { 488 DispatchQueue.main.async {
479 - // TODO: fix error code  
480 - result(FlutterError(code: "MobileScanner", message: error?.localizedDescription, details: nil)) 489 + result(FlutterError(
  490 + code: MobileScannerErrorCodes.BARCODE_ERROR,
  491 + message: error?.localizedDescription, details: nil))
481 } 492 }
482 return 493 return
483 } 494 }
@@ -502,10 +513,11 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -502,10 +513,11 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
502 } 513 }
503 514
504 try imageRequestHandler.perform([barcodeRequest]) 515 try imageRequestHandler.perform([barcodeRequest])
505 - } catch let e {  
506 - // TODO: fix error code 516 + } catch let error {
507 DispatchQueue.main.async { 517 DispatchQueue.main.async {
508 - result(FlutterError(code: "MobileScanner", message: e.localizedDescription, details: nil)) 518 + result(FlutterError(
  519 + code: MobileScannerErrorCodes.BARCODE_ERROR,
  520 + message: error.localizedDescription, details: nil))
509 } 521 }
510 } 522 }
511 } 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>