diff --git a/analysis_options.yaml b/analysis_options.yaml index 7ed3d5c5f..abd9f6a85 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -4,4 +4,5 @@ linter: rules: - combinators_ordering - require_trailing_commas - - unnecessary_library_directive \ No newline at end of file + - unnecessary_library_directive + - prefer_single_quotes \ No newline at end of file diff --git a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt index 4b2b2376e..47f7b5779 100644 --- a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt +++ b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt @@ -120,12 +120,7 @@ class MobileScanner( } if (!returnImage) { - mobileScannerCallback( - barcodeMap, - null, - null, - null - ) + mobileScannerCallback(barcodeMap, null, null, null) return@addOnSuccessListener } diff --git a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/objects/MobileScannerErrorCodes.kt b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/objects/MobileScannerErrorCodes.kt new file mode 100644 index 000000000..497b0b562 --- /dev/null +++ b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/objects/MobileScannerErrorCodes.kt @@ -0,0 +1,25 @@ +package dev.steenbakker.mobile_scanner.objects + +class MobileScannerErrorCodes { + companion object { + const val ALREADY_STARTED_ERROR = "MOBILE_SCANNER_ALREADY_STARTED_ERROR" + const val ALREADY_STARTED_ERROR_MESSAGE = "The scanner was already started." + // The error code 'BARCODE_ERROR' does not have an error message, + // because it uses the error message from the underlying error. + const val BARCODE_ERROR = "MOBILE_SCANNER_BARCODE_ERROR" + // The error code 'CAMERA_ACCESS_DENIED' does not have an error message, + // because it is used for a boolean result. + const val CAMERA_ACCESS_DENIED = "MOBILE_SCANNER_CAMERA_PERMISSION_DENIED" + const val CAMERA_ERROR = "MOBILE_SCANNER_CAMERA_ERROR" + const val CAMERA_ERROR_MESSAGE = "An error occurred when opening the camera." + const val CAMERA_PERMISSIONS_REQUEST_ONGOING = "MOBILE_SCANNER_CAMERA_PERMISSION_REQUEST_PENDING" + const val CAMERA_PERMISSIONS_REQUEST_ONGOING_MESSAGE = "Another request is ongoing and multiple requests cannot be handled at once." + const val GENERIC_ERROR = "MOBILE_SCANNER_GENERIC_ERROR" + const val GENERIC_ERROR_MESSAGE = "An unknown error occurred." + const val INVALID_ZOOM_SCALE_ERROR_MESSAGE = "The zoom scale should be between 0 and 1 (both inclusive)" + const val NO_CAMERA_ERROR = "MOBILE_SCANNER_NO_CAMERA_ERROR" + const val NO_CAMERA_ERROR_MESSAGE = "No cameras available." + const val SET_SCALE_WHEN_STOPPED_ERROR = "MOBILE_SCANNER_SET_SCALE_WHEN_STOPPED_ERROR" + const val SET_SCALE_WHEN_STOPPED_ERROR_MESSAGE = "The zoom scale cannot be changed when the camera is stopped." + } +} \ No newline at end of file diff --git a/example/lib/barcode_scanner_controller.dart b/example/lib/barcode_scanner_controller.dart index bcfb55616..15bfbcfc6 100644 --- a/example/lib/barcode_scanner_controller.dart +++ b/example/lib/barcode_scanner_controller.dart @@ -60,7 +60,7 @@ class _BarcodeScannerWithControllerState @override void didChangeAppLifecycleState(AppLifecycleState state) { - if (!controller.value.isInitialized) { + if (!controller.value.hasCameraPermission) { return; } diff --git a/ios/Classes/MobileScannerError.swift b/ios/Classes/MobileScannerError.swift index 34da9e883..d3fc285e6 100644 --- a/ios/Classes/MobileScannerError.swift +++ b/ios/Classes/MobileScannerError.swift @@ -6,6 +6,12 @@ // import Foundation +// TODO: decide if we should keep or discard this enum +// When merging the iOS / MacOS implementations we should either keep the enum or remove it + +// This enum is a bit of a leftover from older parts of the iOS implementation. +// It is used by the handler that throws these error codes, +// while the plugin class intercepts these and converts them to `FlutterError()`s. enum MobileScannerError: Error { case noCamera case alreadyStarted diff --git a/ios/Classes/MobileScannerErrorCodes.swift b/ios/Classes/MobileScannerErrorCodes.swift new file mode 100644 index 000000000..d70f53390 --- /dev/null +++ b/ios/Classes/MobileScannerErrorCodes.swift @@ -0,0 +1,33 @@ +// +// MobileScannerErrorCodes.swift +// mobile_scanner +// +// Created by Navaron Bracke on 28/05/2024. +// + +import Foundation + +/// This struct defines the error codes and error messages for MobileScanner errors. +/// +/// These are used by `FlutterError` as error code and error message. +/// +/// This struct should not be confused with `MobileScannerError`, +/// which is an implementation detail for the iOS implementation. +struct MobileScannerErrorCodes { + static let ALREADY_STARTED_ERROR = "MOBILE_SCANNER_ALREADY_STARTED_ERROR" + static let ALREADY_STARTED_ERROR_MESSAGE = "The scanner was already started." + // The error code 'BARCODE_ERROR' does not have an error message, + // because it uses the error message from the undelying error. + static let BARCODE_ERROR = "MOBILE_SCANNER_BARCODE_ERROR" + // The error code 'CAMERA_ERROR' does not have an error message, + // because it uses the error message from the underlying error. + static let CAMERA_ERROR = "MOBILE_SCANNER_CAMERA_ERROR" + static let GENERIC_ERROR = "MOBILE_SCANNER_GENERIC_ERROR" + static let GENERIC_ERROR_MESSAGE = "An unknown error occurred." + // This message is used with the 'GENERIC_ERROR' error code. + static let INVALID_ZOOM_SCALE_ERROR_MESSAGE = "The zoom scale should be between 0 and 1 (both inclusive)" + static let NO_CAMERA_ERROR = "MOBILE_SCANNER_NO_CAMERA_ERROR" + static let NO_CAMERA_ERROR_MESSAGE = "No cameras available." + static let SET_SCALE_WHEN_STOPPED_ERROR = "MOBILE_SCANNER_SET_SCALE_WHEN_STOPPED_ERROR" + static let SET_SCALE_WHEN_STOPPED_ERROR_MESSAGE = "The zoom scale cannot be changed when the camera is stopped." +} diff --git a/ios/Classes/MobileScannerPlugin.swift b/ios/Classes/MobileScannerPlugin.swift index d0a5f153a..2b1f2a9b0 100644 --- a/ios/Classes/MobileScannerPlugin.swift +++ b/ios/Classes/MobileScannerPlugin.swift @@ -258,9 +258,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { let uiImage = UIImage(contentsOfFile: (call.arguments as! Dictionary)["filePath"] as? String ?? "") if (uiImage == nil) { - result(FlutterError(code: "MobileScanner", - message: "No image found in analyzeImage!", - details: nil)) + result(nil) return } diff --git a/lib/src/enums/mobile_scanner_error_code.dart b/lib/src/enums/mobile_scanner_error_code.dart index 0fbf3d46d..c37651c2b 100644 --- a/lib/src/enums/mobile_scanner_error_code.dart +++ b/lib/src/enums/mobile_scanner_error_code.dart @@ -1,3 +1,4 @@ +import 'package:flutter/services.dart'; import 'package:mobile_scanner/src/mobile_scanner_controller.dart'; /// This enum defines the different error codes for the mobile scanner. @@ -24,5 +25,25 @@ enum MobileScannerErrorCode { permissionDenied, /// Scanning is unsupported on the current device. - unsupported, + unsupported; + + /// Convert the given [PlatformException.code] to a [MobileScannerErrorCode]. + factory MobileScannerErrorCode.fromPlatformException( + PlatformException exception, + ) { + // The following error code mapping should be kept in sync with their native counterparts. + // These are located in `MobileScannerErrorCodes.kt` and `MobileScannerErrorCodes.swift`. + return switch (exception.code) { + // In case the scanner was already started, report the right error code. + // If the scanner is already starting, + // this error code is a signal to the controller to just ignore the attempt. + 'MOBILE_SCANNER_ALREADY_STARTED_ERROR' => + MobileScannerErrorCode.controllerAlreadyInitialized, + // In case no cameras are available, using the scanner is not supported. + 'MOBILE_SCANNER_NO_CAMERA_ERROR' => MobileScannerErrorCode.unsupported, + 'MOBILE_SCANNER_CAMERA_PERMISSION_DENIED' => + MobileScannerErrorCode.permissionDenied, + _ => MobileScannerErrorCode.genericError, + }; + } } diff --git a/lib/src/method_channel/mobile_scanner_method_channel.dart b/lib/src/method_channel/mobile_scanner_method_channel.dart index 7c6900dc1..a5134dc01 100644 --- a/lib/src/method_channel/mobile_scanner_method_channel.dart +++ b/lib/src/method_channel/mobile_scanner_method_channel.dart @@ -187,8 +187,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { throw const MobileScannerException( errorCode: MobileScannerErrorCode.controllerAlreadyInitialized, errorDetails: MobileScannerErrorDetails( - message: - 'The scanner was already started. Call stop() before calling start() again.', + message: 'The scanner was already started.', ), ); } @@ -204,7 +203,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { ); } on PlatformException catch (error) { throw MobileScannerException( - errorCode: MobileScannerErrorCode.genericError, + errorCode: MobileScannerErrorCode.fromPlatformException(error), errorDetails: MobileScannerErrorDetails( code: error.code, details: error.details as Object?, @@ -240,17 +239,13 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { startResult['currentTorchState'] as int? ?? -1, ); - final Map? sizeInfo = - startResult['size'] as Map?; - final double? width = sizeInfo?['width'] as double?; - final double? height = sizeInfo?['height'] as double?; - final Size size; - if (width == null || height == null) { - size = Size.zero; - } else { + if (startResult['size'] + case {'width': final double width, 'height': final double height}) { size = Size(width, height); + } else { + size = Size.zero; } return MobileScannerViewAttributes( diff --git a/lib/src/mobile_scanner.dart b/lib/src/mobile_scanner.dart index 1dde70bd3..39f1541b5 100644 --- a/lib/src/mobile_scanner.dart +++ b/lib/src/mobile_scanner.dart @@ -281,8 +281,7 @@ class _MobileScannerState extends State @override void didChangeAppLifecycleState(AppLifecycleState state) { - if (widget.controller != null) return; - if (!controller.value.isInitialized) { + if (widget.controller != null || !controller.value.hasCameraPermission) { return; } diff --git a/lib/src/mobile_scanner_exception.dart b/lib/src/mobile_scanner_exception.dart index a35c16720..879403cd2 100644 --- a/lib/src/mobile_scanner_exception.dart +++ b/lib/src/mobile_scanner_exception.dart @@ -1,6 +1,6 @@ import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart'; -/// This class represents an exception thrown by the mobile scanner. +/// This class represents an exception thrown by the [MobileScannerController]. class MobileScannerException implements Exception { const MobileScannerException({ required this.errorCode, @@ -16,9 +16,9 @@ class MobileScannerException implements Exception { @override String toString() { if (errorDetails != null && errorDetails?.message != null) { - return "MobileScannerException: code ${errorCode.name}, message: ${errorDetails?.message}"; + return 'MobileScannerException(${errorCode.name}, ${errorDetails?.message})'; } - return "MobileScannerException: ${errorCode.name}"; + return 'MobileScannerException(${errorCode.name})'; } } @@ -46,3 +46,22 @@ class MobileScannerErrorDetails { /// This exception type is only used internally, /// and is not part of the public API. class PermissionRequestPendingException implements Exception {} + +/// This class represents an exception thrown by the [MobileScannerController] +/// when a barcode scanning error occurs when processing an input frame. +class MobileScannerBarcodeException implements Exception { + /// Creates a new [MobileScannerBarcodeException] with the given error message. + const MobileScannerBarcodeException(this.message); + + /// The error message of the exception. + final String? message; + + @override + String toString() { + if (message?.isNotEmpty ?? false) { + return 'MobileScannerBarcodeException($message)'; + } + + return 'MobileScannerBarcodeException(Could not detect a barcode in the input image.)'; + } +} diff --git a/lib/src/objects/mobile_scanner_state.dart b/lib/src/objects/mobile_scanner_state.dart index 976168eb3..f097ebb9c 100644 --- a/lib/src/objects/mobile_scanner_state.dart +++ b/lib/src/objects/mobile_scanner_state.dart @@ -1,6 +1,7 @@ import 'dart:ui'; import 'package:mobile_scanner/src/enums/camera_facing.dart'; +import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart'; import 'package:mobile_scanner/src/enums/torch_state.dart'; import 'package:mobile_scanner/src/mobile_scanner_exception.dart'; @@ -43,7 +44,8 @@ class MobileScannerState { /// Whether the mobile scanner has initialized successfully. /// - /// This is `true` if the camera is ready to be used. + /// This does not indicate that the camera permission was granted. + /// To check if the camera permission was granted, use [hasCameraPermission]. final bool isInitialized; /// Whether the mobile scanner is currently running. @@ -60,6 +62,12 @@ class MobileScannerState { /// The current zoom scale of the camera. final double zoomScale; + /// Whether permission to access the camera was granted. + bool get hasCameraPermission { + return isInitialized && + error?.errorCode != MobileScannerErrorCode.permissionDenied; + } + /// Create a copy of this state with the given parameters. MobileScannerState copyWith({ int? availableCameras, diff --git a/macos/mobile_scanner/Sources/mobile_scanner/MobileScannerErrorCodes.swift b/macos/mobile_scanner/Sources/mobile_scanner/MobileScannerErrorCodes.swift new file mode 100644 index 000000000..821ce3c42 --- /dev/null +++ b/macos/mobile_scanner/Sources/mobile_scanner/MobileScannerErrorCodes.swift @@ -0,0 +1,21 @@ +// +// MobileScannerErrorCodes.swift +// mobile_scanner +// +// Created by Navaron Bracke on 27/05/2024. +// + +import Foundation + +struct MobileScannerErrorCodes { + static let ALREADY_STARTED_ERROR = "MOBILE_SCANNER_ALREADY_STARTED_ERROR" + static let ALREADY_STARTED_ERROR_MESSAGE = "The scanner was already started." + // The error code 'BARCODE_ERROR' does not have an error message, + // because it uses the error message from the undelying error. + static let BARCODE_ERROR = "MOBILE_SCANNER_BARCODE_ERROR" + // The error code 'CAMERA_ERROR' does not have an error message, + // because it uses the error message from the underlying error. + static let CAMERA_ERROR = "MOBILE_SCANNER_CAMERA_ERROR" + static let NO_CAMERA_ERROR = "MOBILE_SCANNER_NO_CAMERA_ERROR" + static let NO_CAMERA_ERROR_MESSAGE = "No cameras available." +} diff --git a/macos/mobile_scanner/Sources/mobile_scanner/MobileScannerPlugin.swift b/macos/mobile_scanner/Sources/mobile_scanner/MobileScannerPlugin.swift index b840e5a0e..349b41af4 100644 --- a/macos/mobile_scanner/Sources/mobile_scanner/MobileScannerPlugin.swift +++ b/macos/mobile_scanner/Sources/mobile_scanner/MobileScannerPlugin.swift @@ -273,7 +273,6 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, let argReader = MapArgumentReader(call.arguments as? [String: Any]) - // let ratio: Int = argReader.int(key: "ratio") let torch:Bool = argReader.bool(key: "torch") ?? false let facing:Int = argReader.int(key: "facing") ?? 1 let speed:Int = argReader.int(key: "speed") ?? 0 @@ -327,7 +326,6 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue.main) captureSession!.addOutput(videoOutput) for connection in videoOutput.connections { - // connection.videoOrientation = .portrait if position == .front && connection.isVideoMirroringSupported { connection.isVideoMirrored = true } @@ -459,20 +457,14 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, let symbologies:[VNBarcodeSymbology] = argReader.toSymbology() guard let filePath: String = argReader.string(key: "filePath") else { - // TODO: fix error code - result(FlutterError(code: "MobileScanner", - message: "No image found in analyzeImage!", - details: nil)) + result(nil) return } let fileUrl = URL(fileURLWithPath: filePath) guard let ciImage = CIImage(contentsOf: fileUrl) else { - // TODO: fix error code - result(FlutterError(code: "MobileScanner", - message: "No image found in analyzeImage!", - details: nil)) + result(nil) return }