Committed by
GitHub
Merge branch 'juliansteenbakker:master' into master
Showing
31 changed files
with
337 additions
and
213 deletions
| 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 |
| @@ -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 | } |
| @@ -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 | }; |
lib/src/web/zxing/zxing_exception.dart
0 → 100644
| 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> |
-
Please register or login to post a comment