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