Navaron Bracke
Committed by GitHub

Merge pull request #1086 from navaronbracke/fix_already_started_bug

fix: Ignore additional calls to start() when the controller is starting
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`.
5 * [MacOS] Added a Privacy Manifest. 6 * [MacOS] Added a Privacy Manifest.
6 * [web] Added the size information to barcode results. 7 * [web] Added the size information to barcode results.
7 * Added support for barcode formats to image analysis. 8 * Added support for barcode formats to image analysis.
  9 +* Updated the scanner to report any scanning errors that were encountered during processing.
  10 +* Introduced a new getter `hasCameraPermission` for the `MobileScannerState`.
  11 +* Fixed a bug in the lifecycle handling sample. Now instead of checking `isInitialized`,
  12 +the sample recommends using `hasCameraPermission`, which also guards against camera permission errors.
  13 +
  14 +Bugs fixed:
  15 +* 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.
  16 +* [MacOS] Fixed a bug that prevented the `anaylzeImage()` sample from working properly.
8 17
9 ## 5.2.3 18 ## 5.2.3
10 19
@@ -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)
@@ -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 }
@@ -65,10 +66,7 @@ class MobileScannerHandler( @@ -65,10 +66,7 @@ class MobileScannerHandler(
65 } 66 }
66 67
67 private val errorCallback: MobileScannerErrorCallback = {error: String -> 68 private val errorCallback: MobileScannerErrorCallback = {error: String ->
68 - barcodeHandler.publishEvent(mapOf(  
69 - "name" to "error",  
70 - "data" to error,  
71 - )) 69 + barcodeHandler.publishError(MobileScannerErrorCodes.BARCODE_ERROR, error, null)
72 } 70 }
73 71
74 private var methodChannel: MethodChannel? = null 72 private var methodChannel: MethodChannel? = null
@@ -106,21 +104,21 @@ class MobileScannerHandler( @@ -106,21 +104,21 @@ class MobileScannerHandler(
106 104
107 @ExperimentalGetImage 105 @ExperimentalGetImage
108 override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { 106 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) { 107 when (call.method) {
114 "state" -> result.success(permissions.hasCameraPermission(activity)) 108 "state" -> result.success(permissions.hasCameraPermission(activity))
115 "request" -> permissions.requestPermission( 109 "request" -> permissions.requestPermission(
116 activity, 110 activity,
117 addPermissionListener, 111 addPermissionListener,
118 object: MobileScannerPermissions.ResultCallback { 112 object: MobileScannerPermissions.ResultCallback {
119 - override fun onResult(errorCode: String?, errorDescription: String?) { 113 + override fun onResult(errorCode: String?) {
120 when(errorCode) { 114 when(errorCode) {
121 null -> result.success(true) 115 null -> result.success(true)
122 - MobileScannerPermissions.CAMERA_ACCESS_DENIED -> result.success(false)  
123 - else -> result.error(errorCode, errorDescription, null) 116 + MobileScannerErrorCodes.CAMERA_ACCESS_DENIED -> result.success(false)
  117 + MobileScannerErrorCodes.CAMERA_PERMISSIONS_REQUEST_ONGOING -> result.error(
  118 + MobileScannerErrorCodes.CAMERA_PERMISSIONS_REQUEST_ONGOING,
  119 + MobileScannerErrorCodes.CAMERA_PERMISSIONS_REQUEST_ONGOING_MESSAGE, null)
  120 + else -> result.error(
  121 + MobileScannerErrorCodes.GENERIC_ERROR, MobileScannerErrorCodes.GENERIC_ERROR_MESSAGE, null)
124 } 122 }
125 } 123 }
126 }) 124 })
@@ -185,29 +183,29 @@ class MobileScannerHandler( @@ -185,29 +183,29 @@ class MobileScannerHandler(
185 when (it) { 183 when (it) {
186 is AlreadyStarted -> { 184 is AlreadyStarted -> {
187 result.error( 185 result.error(
188 - "MobileScanner",  
189 - "Called start() while already started", 186 + MobileScannerErrorCodes.ALREADY_STARTED_ERROR,
  187 + MobileScannerErrorCodes.ALREADY_STARTED_ERROR_MESSAGE,
190 null 188 null
191 ) 189 )
192 } 190 }
193 is CameraError -> { 191 is CameraError -> {
194 result.error( 192 result.error(
195 - "MobileScanner",  
196 - "Error occurred when setting up camera!", 193 + MobileScannerErrorCodes.CAMERA_ERROR,
  194 + MobileScannerErrorCodes.CAMERA_ERROR_MESSAGE,
197 null 195 null
198 ) 196 )
199 } 197 }
200 is NoCamera -> { 198 is NoCamera -> {
201 result.error( 199 result.error(
202 - "MobileScanner",  
203 - "No camera found or failed to open camera!", 200 + MobileScannerErrorCodes.NO_CAMERA_ERROR,
  201 + MobileScannerErrorCodes.NO_CAMERA_ERROR_MESSAGE,
204 null 202 null
205 ) 203 )
206 } 204 }
207 else -> { 205 else -> {
208 result.error( 206 result.error(
209 - "MobileScanner",  
210 - "Unknown error occurred.", 207 + MobileScannerErrorCodes.GENERIC_ERROR,
  208 + MobileScannerErrorCodes.GENERIC_ERROR_MESSAGE,
211 null 209 null
212 ) 210 )
213 } 211 }
@@ -252,9 +250,11 @@ class MobileScannerHandler( @@ -252,9 +250,11 @@ class MobileScannerHandler(
252 mobileScanner!!.setScale(call.arguments as Double) 250 mobileScanner!!.setScale(call.arguments as Double)
253 result.success(null) 251 result.success(null)
254 } catch (e: ZoomWhenStopped) { 252 } catch (e: ZoomWhenStopped) {
255 - result.error("MobileScanner", "Called setScale() while stopped!", null) 253 + result.error(
  254 + MobileScannerErrorCodes.SET_SCALE_WHEN_STOPPED_ERROR, MobileScannerErrorCodes.SET_SCALE_WHEN_STOPPED_ERROR_MESSAGE, null)
256 } catch (e: ZoomNotInRange) { 255 } catch (e: ZoomNotInRange) {
257 - result.error("MobileScanner", "Scale should be within 0 and 1", null) 256 + result.error(
  257 + MobileScannerErrorCodes.GENERIC_ERROR, MobileScannerErrorCodes.INVALID_ZOOM_SCALE_ERROR_MESSAGE, null)
258 } 258 }
259 } 259 }
260 260
@@ -263,7 +263,8 @@ class MobileScannerHandler( @@ -263,7 +263,8 @@ class MobileScannerHandler(
263 mobileScanner!!.resetScale() 263 mobileScanner!!.resetScale()
264 result.success(null) 264 result.success(null)
265 } catch (e: ZoomWhenStopped) { 265 } catch (e: ZoomWhenStopped) {
266 - result.error("MobileScanner", "Called resetScale() while stopped!", null) 266 + result.error(
  267 + MobileScannerErrorCodes.SET_SCALE_WHEN_STOPPED_ERROR, MobileScannerErrorCodes.SET_SCALE_WHEN_STOPPED_ERROR_MESSAGE, null)
267 } 268 }
268 } 269 }
269 270
@@ -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 }
@@ -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
@@ -150,20 +153,20 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { @@ -150,20 +153,20 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
150 } 153 }
151 } 154 }
152 } catch MobileScannerError.alreadyStarted { 155 } catch MobileScannerError.alreadyStarted {
153 - result(FlutterError(code: "MobileScanner",  
154 - message: "Called start() while already started!", 156 + result(FlutterError(code: MobileScannerErrorCodes.ALREADY_STARTED_ERROR,
  157 + message: MobileScannerErrorCodes.ALREADY_STARTED_ERROR_MESSAGE,
155 details: nil)) 158 details: nil))
156 } catch MobileScannerError.noCamera { 159 } catch MobileScannerError.noCamera {
157 - result(FlutterError(code: "MobileScanner",  
158 - message: "No camera found or failed to open camera!", 160 + result(FlutterError(code: MobileScannerErrorCodes.NO_CAMERA_ERROR,
  161 + message: MobileScannerErrorCodes.NO_CAMERA_ERROR_MESSAGE,
159 details: nil)) 162 details: nil))
160 } catch MobileScannerError.cameraError(let error) { 163 } catch MobileScannerError.cameraError(let error) {
161 - result(FlutterError(code: "MobileScanner",  
162 - message: "Error occured when setting up camera!",  
163 - details: error)) 164 + result(FlutterError(code: MobileScannerErrorCodes.CAMERA_ERROR,
  165 + message: error.localizedDescription,
  166 + details: nil))
164 } catch { 167 } catch {
165 - result(FlutterError(code: "MobileScanner",  
166 - message: "Unknown error occured.", 168 + result(FlutterError(code: MobileScannerErrorCodes.GENERIC_ERROR,
  169 + message: MobileScannerErrorCodes.GENERIC_ERROR_MESSAGE,
167 details: nil)) 170 details: nil))
168 } 171 }
169 } 172 }
@@ -186,25 +189,25 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { @@ -186,25 +189,25 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
186 private func setScale(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { 189 private func setScale(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
187 let scale = call.arguments as? CGFloat 190 let scale = call.arguments as? CGFloat
188 if (scale == nil) { 191 if (scale == nil) {
189 - result(FlutterError(code: "MobileScanner",  
190 - message: "You must provide a scale when calling setScale!",  
191 - details: nil)) 192 + result(FlutterError(code: MobileScannerErrorCodes.GENERIC_ERROR,
  193 + message: MobileScannerErrorCodes.INVALID_ZOOM_SCALE_ERROR_MESSAGE,
  194 + details: "The invalid zoom scale was nil."))
192 return 195 return
193 } 196 }
194 do { 197 do {
195 try mobileScanner.setScale(scale!) 198 try mobileScanner.setScale(scale!)
196 result(nil) 199 result(nil)
197 } catch MobileScannerError.zoomWhenStopped { 200 } catch MobileScannerError.zoomWhenStopped {
198 - result(FlutterError(code: "MobileScanner",  
199 - message: "Called setScale() while stopped!", 201 + result(FlutterError(code: MobileScannerErrorCodes.SET_SCALE_WHEN_STOPPED_ERROR,
  202 + message: MobileScannerErrorCodes.SET_SCALE_WHEN_STOPPED_ERROR_MESSAGE,
200 details: nil)) 203 details: nil))
201 } catch MobileScannerError.zoomError(let error) { 204 } catch MobileScannerError.zoomError(let error) {
202 - result(FlutterError(code: "MobileScanner",  
203 - message: "Error while zooming.",  
204 - details: error)) 205 + result(FlutterError(code: MobileScannerErrorCodes.GENERIC_ERROR,
  206 + message: error.localizedDescription,
  207 + details: nil))
205 } catch { 208 } catch {
206 - result(FlutterError(code: "MobileScanner",  
207 - message: "Error while zooming.", 209 + result(FlutterError(code: MobileScannerErrorCodes.GENERIC_ERROR,
  210 + message: MobileScannerErrorCodes.GENERIC_ERROR_MESSAGE,
208 details: nil)) 211 details: nil))
209 } 212 }
210 } 213 }
@@ -215,16 +218,16 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { @@ -215,16 +218,16 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
215 try mobileScanner.resetScale() 218 try mobileScanner.resetScale()
216 result(nil) 219 result(nil)
217 } catch MobileScannerError.zoomWhenStopped { 220 } catch MobileScannerError.zoomWhenStopped {
218 - result(FlutterError(code: "MobileScanner",  
219 - message: "Called resetScale() while stopped!", 221 + result(FlutterError(code: MobileScannerErrorCodes.SET_SCALE_WHEN_STOPPED_ERROR,
  222 + message: MobileScannerErrorCodes.SET_SCALE_WHEN_STOPPED_ERROR_MESSAGE,
220 details: nil)) 223 details: nil))
221 } catch MobileScannerError.zoomError(let error) { 224 } catch MobileScannerError.zoomError(let error) {
222 - result(FlutterError(code: "MobileScanner",  
223 - message: "Error while zooming.",  
224 - details: error)) 225 + result(FlutterError(code: MobileScannerErrorCodes.GENERIC_ERROR,
  226 + message: error.localizedDescription,
  227 + details: nil))
225 } catch { 228 } catch {
226 - result(FlutterError(code: "MobileScanner",  
227 - message: "Error while zooming.", 229 + result(FlutterError(code: MobileScannerErrorCodes.GENERIC_ERROR,
  230 + message: MobileScannerErrorCodes.GENERIC_ERROR_MESSAGE,
228 details: nil)) 231 details: nil))
229 } 232 }
230 } 233 }
@@ -266,7 +269,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { @@ -266,7 +269,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
266 barcodeScannerOptions: scannerOptions, callback: { barcodes, error in 269 barcodeScannerOptions: scannerOptions, callback: { barcodes, error in
267 if error != nil { 270 if error != nil {
268 DispatchQueue.main.async { 271 DispatchQueue.main.async {
269 - result(FlutterError(code: "MobileScanner", 272 + result(FlutterError(code: MobileScannerErrorCodes.BARCODE_ERROR,
270 message: error?.localizedDescription, 273 message: error?.localizedDescription,
271 details: nil)) 274 details: nil))
272 } 275 }
@@ -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.
@@ -102,6 +102,9 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { @@ -102,6 +102,9 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
102 StreamController.broadcast(); 102 StreamController.broadcast();
103 103
104 /// Get the stream of scanned barcodes. 104 /// Get the stream of scanned barcodes.
  105 + ///
  106 + /// If an error occurred during the detection of a barcode,
  107 + /// a [MobileScannerBarcodeException] error is emitted to the stream.
105 Stream<BarcodeCapture> get barcodes => _barcodesController.stream; 108 Stream<BarcodeCapture> get barcodes => _barcodesController.stream;
106 109
107 StreamSubscription<BarcodeCapture?>? _barcodesSubscription; 110 StreamSubscription<BarcodeCapture?>? _barcodesSubscription;
@@ -121,14 +124,25 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { @@ -121,14 +124,25 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
121 } 124 }
122 125
123 void _setupListeners() { 126 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 - }); 127 + _barcodesSubscription =
  128 + MobileScannerPlatform.instance.barcodesStream.listen(
  129 + (BarcodeCapture? barcode) {
  130 + if (_barcodesController.isClosed || barcode == null) {
  131 + return;
  132 + }
  133 +
  134 + _barcodesController.add(barcode);
  135 + },
  136 + onError: (Object error) {
  137 + if (_barcodesController.isClosed) {
  138 + return;
  139 + }
  140 +
  141 + _barcodesController.addError(error);
  142 + },
  143 + // Errors are handled gracefully by forwarding them.
  144 + cancelOnError: false,
  145 + );
132 146
133 _torchStateSubscription = MobileScannerPlatform.instance.torchStateStream 147 _torchStateSubscription = MobileScannerPlatform.instance.torchStateStream
134 .listen((TorchState torchState) { 148 .listen((TorchState torchState) {
@@ -177,6 +191,9 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { @@ -177,6 +191,9 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
177 /// This is only supported on Android, iOS and MacOS. 191 /// This is only supported on Android, iOS and MacOS.
178 /// 192 ///
179 /// Returns the [BarcodeCapture] that was found in the image. 193 /// Returns the [BarcodeCapture] that was found in the image.
  194 + ///
  195 + /// If an error occurred during the analysis of the image,
  196 + /// a [MobileScannerBarcodeException] error is thrown.
180 Future<BarcodeCapture?> analyzeImage(String path) { 197 Future<BarcodeCapture?> analyzeImage(String path) {
181 return MobileScannerPlatform.instance.analyzeImage(path); 198 return MobileScannerPlatform.instance.analyzeImage(path);
182 } 199 }
@@ -246,13 +263,6 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { @@ -246,13 +263,6 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
246 ); 263 );
247 } 264 }
248 265
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. 266 // Do nothing if the camera is already running.
257 if (value.isRunning) { 267 if (value.isRunning) {
258 return; 268 return;
@@ -292,6 +302,13 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { @@ -292,6 +302,13 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
292 ); 302 );
293 } 303 }
294 } on MobileScannerException catch (error) { 304 } on MobileScannerException catch (error) {
  305 + // If the controller is already initialized, ignore the error.
  306 + // Starting the controller while it is already started, or in the process of starting, is redundant.
  307 + if (error.errorCode ==
  308 + MobileScannerErrorCode.controllerAlreadyInitialized) {
  309 + return;
  310 + }
  311 +
295 // The initialization finished with an error. 312 // The initialization finished with an error.
296 // To avoid stale values, reset the output size, 313 // To avoid stale values, reset the output size,
297 // torch state and zoom scale to the defaults. 314 // torch state and zoom scale to the defaults.
@@ -306,8 +323,6 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { @@ -306,8 +323,6 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
306 zoomScale: 1.0, 323 zoomScale: 1.0,
307 ); 324 );
308 } 325 }
309 - } on PermissionRequestPendingException catch (_) {  
310 - // If a permission request was already pending, do nothing.  
311 } 326 }
312 } 327 }
313 328
@@ -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 {
@@ -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,
@@ -199,17 +193,12 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -199,17 +193,12 @@ class MobileScannerWeb extends MobileScannerPlatform {
199 } 193 }
200 194
201 try { 195 try {
202 - _permissionRequestInProgress = true;  
203 -  
204 // Retrieving the media devices requests the camera permission. 196 // Retrieving the media devices requests the camera permission.
205 final MediaStream videoStream = 197 final MediaStream videoStream =
206 await window.navigator.mediaDevices.getUserMedia(constraints).toDart; 198 await window.navigator.mediaDevices.getUserMedia(constraints).toDart;
207 199
208 - _permissionRequestInProgress = false;  
209 -  
210 return videoStream; 200 return videoStream;
211 } on DOMException catch (error, stackTrace) { 201 } on DOMException catch (error, stackTrace) {
212 - _permissionRequestInProgress = false;  
213 final String errorMessage = error.toString(); 202 final String errorMessage = error.toString();
214 203
215 MobileScannerErrorCode errorCode = MobileScannerErrorCode.genericError; 204 MobileScannerErrorCode errorCode = MobileScannerErrorCode.genericError;
@@ -272,11 +261,13 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -272,11 +261,13 @@ class MobileScannerWeb extends MobileScannerPlatform {
272 261
273 @override 262 @override
274 Future<MobileScannerViewAttributes> start(StartOptions startOptions) async { 263 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(); 264 + if (_barcodeReader != null) {
  265 + throw const MobileScannerException(
  266 + errorCode: MobileScannerErrorCode.controllerAlreadyInitialized,
  267 + errorDetails: MobileScannerErrorDetails(
  268 + message: 'The scanner was already started.',
  269 + ),
  270 + );
280 } 271 }
281 272
282 _barcodeReader = ZXingBarcodeReader(); 273 _barcodeReader = ZXingBarcodeReader();
@@ -285,16 +276,6 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -285,16 +276,6 @@ class MobileScannerWeb extends MobileScannerPlatform {
285 alternateScriptUrl: _alternateScriptUrl, 276 alternateScriptUrl: _alternateScriptUrl,
286 ); 277 );
287 278
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. 279 // Request camera permissions and prepare the video stream.
299 final MediaStream videoStream = await _prepareVideoStream( 280 final MediaStream videoStream = await _prepareVideoStream(
300 startOptions.cameraDirection, 281 startOptions.cameraDirection,
@@ -341,6 +322,15 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -341,6 +322,15 @@ class MobileScannerWeb extends MobileScannerPlatform {
341 322
342 _barcodesController.add(barcode); 323 _barcodesController.add(barcode);
343 }, 324 },
  325 + onError: (Object error) {
  326 + if (_barcodesController.isClosed) {
  327 + return;
  328 + }
  329 +
  330 + _barcodesController.addError(error);
  331 + },
  332 + // Errors are handled gracefully by forwarding them.
  333 + cancelOnError: false,
344 ); 334 );
345 335
346 final bool hasTorch = await _barcodeReader?.hasTorch() ?? false; 336 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,24 @@ final class ZXingBarcodeReader extends BarcodeReader { @@ -98,16 +106,24 @@ 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 + ),
  125 + );
  126 + }
111 }.toJS, 127 }.toJS,
112 ); 128 );
113 }; 129 };
  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 +}
@@ -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 }
@@ -180,9 +182,11 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -180,9 +182,11 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
180 } 182 }
181 183
182 try imageRequestHandler.perform([barcodeRequest]) 184 try imageRequestHandler.perform([barcodeRequest])
183 - } catch let e { 185 + } catch let error {
184 DispatchQueue.main.async { 186 DispatchQueue.main.async {
185 - self?.sink?(FlutterError(code: "MobileScanner", message: e.localizedDescription, details: nil)) 187 + self?.sink?(FlutterError(
  188 + code: MobileScannerErrorCodes.BARCODE_ERROR,
  189 + message: error.localizedDescription, details: nil))
186 } 190 }
187 } 191 }
188 } 192 }
@@ -262,8 +266,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -262,8 +266,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
262 266
263 func start(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { 267 func start(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
264 if (device != nil || captureSession != nil) { 268 if (device != nil || captureSession != nil) {
265 - result(FlutterError(code: "MobileScanner",  
266 - message: "Called start() while already started!", 269 + result(FlutterError(code: MobileScannerErrorCodes.ALREADY_STARTED_ERROR,
  270 + message: MobileScannerErrorCodes.ALREADY_STARTED_ERROR_MESSAGE,
267 details: nil)) 271 details: nil))
268 return 272 return
269 } 273 }
@@ -294,8 +298,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -294,8 +298,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
294 } 298 }
295 299
296 if (device == nil) { 300 if (device == nil) {
297 - result(FlutterError(code: "MobileScanner",  
298 - message: "No camera found or failed to open camera!", 301 + result(FlutterError(code: MobileScannerErrorCodes.NO_CAMERA_ERROR,
  302 + message: MobileScannerErrorCodes.NO_CAMERA_ERROR_MESSAGE,
299 details: nil)) 303 details: nil))
300 return 304 return
301 } 305 }
@@ -313,7 +317,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -313,7 +317,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
313 let input = try AVCaptureDeviceInput(device: device) 317 let input = try AVCaptureDeviceInput(device: device)
314 captureSession!.addInput(input) 318 captureSession!.addInput(input)
315 } catch { 319 } catch {
316 - result(FlutterError(code: "MobileScanner", message: error.localizedDescription, details: nil)) 320 + result(FlutterError(
  321 + code: MobileScannerErrorCodes.CAMERA_ERROR,
  322 + message: error.localizedDescription, details: nil))
317 return 323 return
318 } 324 }
319 captureSession!.sessionPreset = AVCaptureSession.Preset.photo 325 captureSession!.sessionPreset = AVCaptureSession.Preset.photo
@@ -476,8 +482,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -476,8 +482,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
476 482
477 if error != nil { 483 if error != nil {
478 DispatchQueue.main.async { 484 DispatchQueue.main.async {
479 - // TODO: fix error code  
480 - result(FlutterError(code: "MobileScanner", message: error?.localizedDescription, details: nil)) 485 + result(FlutterError(
  486 + code: MobileScannerErrorCodes.BARCODE_ERROR,
  487 + message: error?.localizedDescription, details: nil))
481 } 488 }
482 return 489 return
483 } 490 }
@@ -502,10 +509,11 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -502,10 +509,11 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
502 } 509 }
503 510
504 try imageRequestHandler.perform([barcodeRequest]) 511 try imageRequestHandler.perform([barcodeRequest])
505 - } catch let e {  
506 - // TODO: fix error code 512 + } catch let error {
507 DispatchQueue.main.async { 513 DispatchQueue.main.async {
508 - result(FlutterError(code: "MobileScanner", message: e.localizedDescription, details: nil)) 514 + result(FlutterError(
  515 + code: MobileScannerErrorCodes.BARCODE_ERROR,
  516 + message: error.localizedDescription, details: nil))
509 } 517 }
510 } 518 }
511 } 519 }