Sander Roest

Merge branch 'master' into picklist_poc

Showing 46 changed files with 529 additions and 239 deletions
## NEXT
## 6.0.2
Bugs fixed:
* Fixed a bug that prevented `analyzeImage` from actually accepting the configured formats.
Improvements:
* [iOS] Excluded the `arm64` architecture for Simulators, which is unsupported by MLKit 7.0.0.
## 6.0.1
Bugs fixed:
* Fixed a bug that would cause onDetect to not handle errors.
Improvements:
* [iOS] Excluded the `armv7` architecture, which is unsupported by MLKit 7.0.0.
* Added a new `onDetectError` error handler to the `MobileScanner` widget, for use with `onDetect`.
## 6.0.0
**BREAKING CHANGES:**
* [iOS] iOS 15.5.0 is now the minimum supported iOS version.
* [iOS] Updates MLKit to version 7.0.0.
* [iOS] Updates the minimum supported XCode version to 15.3.0.
Improvements:
* [MacOS] Added the corners and size information to barcode results.
* [MacOS] Added support for `analyzeImage`.
* [MacOS] Added a Privacy Manifest.
* [web] Added the size information to barcode results.
* [web] Added the video output size information to barcode capture.
* Added support for barcode formats to image analysis.
* Updated the scanner to report any scanning errors that were encountered during processing.
* Introduced a new getter `hasCameraPermission` for the `MobileScannerState`.
* Fixed a bug in the lifecycle handling sample. Now instead of checking `isInitialized`,
the sample recommends using `hasCameraPermission`, which also guards against camera permission errors.
* Updated the behavior of `returnImage` to only determine if the camera output bytes should be sent.
* Updated the behavior of `BarcodeCapture.size` to always be provided when available, regardless of `returnImage`.
Bugs fixed:
* 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.
* [MacOS] Fixed a bug that prevented the `anaylzeImage()` sample from working properly.
## 5.2.3
... ...
... ... @@ -60,6 +60,10 @@ dev.steenbakker.mobile_scanner.useUnbundled=true
```
### iOS
_iOS arm64 Simulators are currently not yet supported, until the migration to the Vision API is complete._
_See_ https://github.com/juliansteenbakker/mobile_scanner/issues/1225
**Add the following keys to your Info.plist file, located in <project root>/ios/Runner/Info.plist:**
NSCameraUsageDescription - describe why your app needs access to the camera. This is called Privacy - Camera Usage Description in the visual editor.
... ... @@ -127,7 +131,7 @@ class MyState extends State<MyStatefulWidget> with WidgetsBindingObserver {
void didChangeAppLifecycleState(AppLifecycleState state) {
// If the controller is not ready, do not try to start or stop it.
// Permission dialogs can trigger lifecycle changes before the controller is ready.
if (!controller.value.isInitialized) {
if (!controller.value.hasCameraPermission) {
return;
}
... ...
... ... @@ -5,3 +5,4 @@ linter:
- combinators_ordering
- require_trailing_commas
- unnecessary_library_directive
- prefer_single_quotes
\ No newline at end of file
... ...
... ... @@ -18,6 +18,12 @@ class BarcodeHandler(binaryMessenger: BinaryMessenger) : EventChannel.StreamHand
eventChannel.setStreamHandler(this)
}
fun publishError(errorCode: String, errorMessage: String, errorDetails: Any?) {
Handler(Looper.getMainLooper()).post {
eventSink?.error(errorCode, errorMessage, errorDetails)
}
}
fun publishEvent(event: Map<String, Any>) {
Handler(Looper.getMainLooper()).post {
eventSink?.success(event)
... ...
... ... @@ -123,9 +123,8 @@ class MobileScanner(
mobileScannerCallback(
barcodeMap,
null,
null,
null
)
mediaImage.width,
mediaImage.height)
return@addOnSuccessListener
}
... ...
... ... @@ -10,6 +10,7 @@ import androidx.camera.core.ExperimentalGetImage
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
import dev.steenbakker.mobile_scanner.objects.BarcodeFormats
import dev.steenbakker.mobile_scanner.objects.DetectionSpeed
import dev.steenbakker.mobile_scanner.objects.MobileScannerErrorCodes
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
... ... @@ -28,7 +29,7 @@ class MobileScannerHandler(
private val analyzeImageErrorCallback: AnalyzerErrorCallback = {
Handler(Looper.getMainLooper()).post {
analyzerResult?.error("MobileScanner", it, null)
analyzerResult?.error(MobileScannerErrorCodes.BARCODE_ERROR, it, null)
analyzerResult = null
}
}
... ... @@ -46,29 +47,21 @@ class MobileScannerHandler(
private var analyzerResult: MethodChannel.Result? = null
private val callback: MobileScannerCallback = { barcodes: List<Map<String, Any?>>, image: ByteArray?, width: Int?, height: Int? ->
if (image != null) {
barcodeHandler.publishEvent(mapOf(
"name" to "barcode",
"data" to barcodes,
// The image dimensions are always provided.
// The image bytes are only non-null when `returnImage` is true.
"image" to mapOf(
"bytes" to image,
"width" to width?.toDouble(),
"height" to height?.toDouble(),
)
))
} else {
barcodeHandler.publishEvent(mapOf(
"name" to "barcode",
"data" to barcodes
))
}
}
private val errorCallback: MobileScannerErrorCallback = {error: String ->
barcodeHandler.publishEvent(mapOf(
"name" to "error",
"data" to error,
))
barcodeHandler.publishError(MobileScannerErrorCodes.BARCODE_ERROR, error, null)
}
private var methodChannel: MethodChannel? = null
... ... @@ -106,21 +99,21 @@ class MobileScannerHandler(
@ExperimentalGetImage
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
if (mobileScanner == null) {
result.error("MobileScanner", "Called ${call.method} before initializing.", null)
return
}
when (call.method) {
"state" -> result.success(permissions.hasCameraPermission(activity))
"request" -> permissions.requestPermission(
activity,
addPermissionListener,
object: MobileScannerPermissions.ResultCallback {
override fun onResult(errorCode: String?, errorDescription: String?) {
override fun onResult(errorCode: String?) {
when(errorCode) {
null -> result.success(true)
MobileScannerPermissions.CAMERA_ACCESS_DENIED -> result.success(false)
else -> result.error(errorCode, errorDescription, null)
MobileScannerErrorCodes.CAMERA_ACCESS_DENIED -> result.success(false)
MobileScannerErrorCodes.CAMERA_PERMISSIONS_REQUEST_ONGOING -> result.error(
MobileScannerErrorCodes.CAMERA_PERMISSIONS_REQUEST_ONGOING,
MobileScannerErrorCodes.CAMERA_PERMISSIONS_REQUEST_ONGOING_MESSAGE, null)
else -> result.error(
MobileScannerErrorCodes.GENERIC_ERROR, MobileScannerErrorCodes.GENERIC_ERROR_MESSAGE, null)
}
}
})
... ... @@ -185,29 +178,29 @@ class MobileScannerHandler(
when (it) {
is AlreadyStarted -> {
result.error(
"MobileScanner",
"Called start() while already started",
MobileScannerErrorCodes.ALREADY_STARTED_ERROR,
MobileScannerErrorCodes.ALREADY_STARTED_ERROR_MESSAGE,
null
)
}
is CameraError -> {
result.error(
"MobileScanner",
"Error occurred when setting up camera!",
MobileScannerErrorCodes.CAMERA_ERROR,
MobileScannerErrorCodes.CAMERA_ERROR_MESSAGE,
null
)
}
is NoCamera -> {
result.error(
"MobileScanner",
"No camera found or failed to open camera!",
MobileScannerErrorCodes.NO_CAMERA_ERROR,
MobileScannerErrorCodes.NO_CAMERA_ERROR_MESSAGE,
null
)
}
else -> {
result.error(
"MobileScanner",
"Unknown error occurred.",
MobileScannerErrorCodes.GENERIC_ERROR,
MobileScannerErrorCodes.GENERIC_ERROR_MESSAGE,
null
)
}
... ... @@ -252,9 +245,11 @@ class MobileScannerHandler(
mobileScanner!!.setScale(call.arguments as Double)
result.success(null)
} catch (e: ZoomWhenStopped) {
result.error("MobileScanner", "Called setScale() while stopped!", null)
result.error(
MobileScannerErrorCodes.SET_SCALE_WHEN_STOPPED_ERROR, MobileScannerErrorCodes.SET_SCALE_WHEN_STOPPED_ERROR_MESSAGE, null)
} catch (e: ZoomNotInRange) {
result.error("MobileScanner", "Scale should be within 0 and 1", null)
result.error(
MobileScannerErrorCodes.GENERIC_ERROR, MobileScannerErrorCodes.INVALID_ZOOM_SCALE_ERROR_MESSAGE, null)
}
}
... ... @@ -263,7 +258,8 @@ class MobileScannerHandler(
mobileScanner!!.resetScale()
result.success(null)
} catch (e: ZoomWhenStopped) {
result.error("MobileScanner", "Called resetScale() while stopped!", null)
result.error(
MobileScannerErrorCodes.SET_SCALE_WHEN_STOPPED_ERROR, MobileScannerErrorCodes.SET_SCALE_WHEN_STOPPED_ERROR_MESSAGE, null)
}
}
... ...
... ... @@ -5,6 +5,7 @@ import android.app.Activity
import android.content.pm.PackageManager
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import dev.steenbakker.mobile_scanner.objects.MobileScannerErrorCodes
import io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener
/**
... ... @@ -12,11 +13,6 @@ import io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener
*/
class MobileScannerPermissions {
companion object {
const val CAMERA_ACCESS_DENIED = "CameraAccessDenied"
const val CAMERA_ACCESS_DENIED_MESSAGE = "Camera access permission was denied."
const val CAMERA_PERMISSIONS_REQUEST_ONGOING = "CameraPermissionsRequestOngoing"
const val CAMERA_PERMISSIONS_REQUEST_ONGOING_MESSAGE = "Another request is ongoing and multiple requests cannot be handled at once."
/**
* When the application's activity is [androidx.fragment.app.FragmentActivity], requestCode can only use the lower 16 bits.
* @see androidx.fragment.app.FragmentActivity.validateRequestPermissionsRequestCode
... ... @@ -25,7 +21,7 @@ class MobileScannerPermissions {
}
interface ResultCallback {
fun onResult(errorCode: String?, errorDescription: String?)
fun onResult(errorCode: String?)
}
private var listener: RequestPermissionsResultListener? = null
... ... @@ -53,14 +49,13 @@ class MobileScannerPermissions {
addPermissionListener: (RequestPermissionsResultListener) -> Unit,
callback: ResultCallback) {
if (ongoing) {
callback.onResult(
CAMERA_PERMISSIONS_REQUEST_ONGOING, CAMERA_PERMISSIONS_REQUEST_ONGOING_MESSAGE)
callback.onResult(MobileScannerErrorCodes.CAMERA_PERMISSIONS_REQUEST_ONGOING)
return
}
if(hasCameraPermission(activity) == 1) {
// Permissions already exist. Call the callback with success.
callback.onResult(null, null)
callback.onResult(null)
return
}
... ... @@ -68,10 +63,10 @@ class MobileScannerPermissions {
// Keep track of the listener, so that it can be unregistered later.
listener = MobileScannerPermissionsListener(
object: ResultCallback {
override fun onResult(errorCode: String?, errorDescription: String?) {
override fun onResult(errorCode: String?) {
ongoing = false
listener = null
callback.onResult(errorCode, errorDescription)
callback.onResult(errorCode)
}
}
)
... ...
package dev.steenbakker.mobile_scanner
import android.content.pm.PackageManager
import dev.steenbakker.mobile_scanner.objects.MobileScannerErrorCodes
import io.flutter.plugin.common.PluginRegistry
/**
... ... @@ -29,11 +30,9 @@ internal class MobileScannerPermissionsListener(
// grantResults could be empty if the permissions request with the user is interrupted
// https://developer.android.com/reference/android/app/Activity#onRequestPermissionsResult(int,%20java.lang.String[],%20int[])
if (grantResults.isEmpty() || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
resultCallback.onResult(
MobileScannerPermissions.CAMERA_ACCESS_DENIED,
MobileScannerPermissions.CAMERA_ACCESS_DENIED_MESSAGE)
resultCallback.onResult(MobileScannerErrorCodes.CAMERA_ACCESS_DENIED)
} else {
resultCallback.onResult(null, null)
resultCallback.onResult(null)
}
return true
... ...
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
... ...
... ... @@ -5,9 +5,11 @@
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
... ...
... ... @@ -27,12 +27,12 @@ android {
compileSdk 34
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '1.8'
jvmTarget = '17'
}
sourceSets {
... ... @@ -42,7 +42,7 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "dev.steenbakker.mobile_scanner_example"
minSdkVersion 21
minSdkVersion 24
targetSdkVersion 34
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
... ...
#Thu May 02 10:24:49 CEST 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
... ...
... ... @@ -18,8 +18,8 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "7.3.0" apply false
id "org.jetbrains.kotlin.android" version "1.7.22" apply false
id "com.android.application" version "8.3.2" apply false
id "org.jetbrains.kotlin.android" version "2.0.20" apply false
}
include ":app"
\ No newline at end of file
... ...
... ... @@ -21,6 +21,6 @@
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
<string>15.5.0</string>
</dict>
</plist>
... ...
# Uncomment this line to define a global platform for your project
# platform :ios, '12.0'
# platform :ios, '15.5.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
... ... @@ -41,7 +41,7 @@ post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0'
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.5.0'
end
end
end
... ...
... ... @@ -470,7 +470,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.5.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
... ... @@ -488,14 +488,14 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 75Y2P2WSQQ;
DEVELOPMENT_TEAM = "";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "com.example.mobile-scanner";
PRODUCT_BUNDLE_IDENTIFIER = "com.example.mobile-scanner-example";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
... ... @@ -601,7 +601,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.5.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
... ... @@ -650,7 +650,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.5.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
... ... @@ -670,14 +670,14 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 75Y2P2WSQQ;
DEVELOPMENT_TEAM = "";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "com.example.mobile-scanner";
PRODUCT_BUNDLE_IDENTIFIER = "com.example.mobile-scanner-example";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
... ... @@ -696,14 +696,14 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 75Y2P2WSQQ;
DEVELOPMENT_TEAM = "";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "com.example.mobile-scanner";
PRODUCT_BUNDLE_IDENTIFIER = "com.example.mobile-scanner-example";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
... ...
... ... @@ -22,7 +22,14 @@ class _BarcodeScannerAnalyzeImageState
final XFile? file =
await ImagePicker().pickImage(source: ImageSource.gallery);
if (!mounted || file == null) {
if (!mounted) {
return;
}
if (file == null) {
setState(() {
_barcodeCapture = null;
});
return;
}
... ... @@ -43,7 +50,7 @@ class _BarcodeScannerAnalyzeImageState
if (_barcodeCapture != null) {
label = Text(
_barcodeCapture?.barcodes.firstOrNull?.displayValue ??
_barcodeCapture?.barcodes.firstOrNull?.rawValue ??
'No barcode detected',
);
}
... ...
... ... @@ -60,7 +60,7 @@ class _BarcodeScannerWithControllerState
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (!controller.value.isInitialized) {
if (!controller.value.hasCameraPermission) {
return;
}
... ...
... ... @@ -130,15 +130,15 @@ class ScannerOverlay extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
// TODO: use `Offset.zero & size` instead of Rect.largest
// we need to pass the size to the custom paint widget
final backgroundPath = Path()..addRect(Rect.largest);
final backgroundPath = Path()
..addRect(Rect.fromLTWH(0, 0, size.width, size.height));
final cutoutPath = Path()..addRect(scanWindow);
final backgroundPaint = Paint()
..color = Colors.black.withOpacity(0.5)
..style = PaintingStyle.fill
..blendMode = BlendMode.dstOut;
..blendMode = BlendMode.dstOver;
final backgroundWithCutout = Path.combine(
PathOperation.difference,
... ...
... ... @@ -102,9 +102,9 @@ class ScannerOverlay extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
// TODO: use `Offset.zero & size` instead of Rect.largest
// we need to pass the size to the custom paint widget
final backgroundPath = Path()..addRect(Rect.largest);
final backgroundPath = Path()
..addRect(Rect.fromLTWH(0, 0, size.width, size.height));
final cutoutPath = Path()
..addRRect(
... ... @@ -120,7 +120,7 @@ class ScannerOverlay extends CustomPainter {
final backgroundPaint = Paint()
..color = Colors.black.withOpacity(0.5)
..style = PaintingStyle.fill
..blendMode = BlendMode.dstOut;
..blendMode = BlendMode.dstOver;
final backgroundWithCutout = Path.combine(
PathOperation.difference,
... ...
... ... @@ -112,6 +112,7 @@ class SwitchCameraButton extends StatelessWidget {
}
return IconButton(
color: Colors.white,
iconSize: 32.0,
icon: icon,
onPressed: () async {
... ... @@ -166,9 +167,13 @@ class ToggleFlashlightButton extends StatelessWidget {
},
);
case TorchState.unavailable:
return const Icon(
return const SizedBox.square(
dimension: 48.0,
child: Icon(
Icons.no_flash,
size: 32.0,
color: Colors.grey,
),
);
}
},
... ...
... ... @@ -6,4 +6,8 @@ class AppDelegate: FlutterAppDelegate {
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
return true
}
}
... ...
... ... @@ -10,5 +10,7 @@
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
</dict>
</plist>
... ...
... ... @@ -21,7 +21,7 @@
<meta name="description" content="Demonstrates how to use the mobile_scanner plugin.">
<!-- iOS meta tags & icons -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="mobile_scanner_example">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
... ...
... ... @@ -19,6 +19,12 @@ public class BarcodeHandler: NSObject, FlutterStreamHandler {
eventChannel.setStreamHandler(self)
}
func publishError(_ error: FlutterError) {
DispatchQueue.main.async {
self.eventSink?(error)
}
}
func publishEvent(_ event: [String: Any?]) {
DispatchQueue.main.async {
self.eventSink?(event)
... ...
... ... @@ -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
... ...
//
// 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."
}
... ...
... ... @@ -42,7 +42,10 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
init(barcodeHandler: BarcodeHandler, registry: FlutterTextureRegistry) {
self.mobileScanner = MobileScanner(registry: registry, mobileScannerCallback: { barcodes, error, image in
if error != nil {
barcodeHandler.publishEvent(["name": "error", "data": error!.localizedDescription])
barcodeHandler.publishError(
FlutterError(code: MobileScannerErrorCodes.BARCODE_ERROR,
message: error?.localizedDescription,
details: nil))
return
}
... ... @@ -66,22 +69,18 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
return
}
if (!MobileScannerPlugin.returnImage) {
barcodeHandler.publishEvent([
"name": "barcode",
"data": barcodesMap,
])
return
}
// The image dimensions are always provided.
// The image bytes are only non-null when `returnImage` is true.
let imageData: [String: Any?] = [
"bytes": MobileScannerPlugin.returnImage ? FlutterStandardTypedData(bytes: image.jpegData(compressionQuality: 0.8)!) : nil,
"width": image.size.width,
"height": image.size.height,
]
barcodeHandler.publishEvent([
"name": "barcode",
"data": barcodesMap,
"image": [
"bytes": FlutterStandardTypedData(bytes: image.jpegData(compressionQuality: 0.8)!),
"width": image.size.width,
"height": image.size.height,
],
"image": imageData,
])
}, torchModeChangeCallback: { torchState in
barcodeHandler.publishEvent(["name": "torchState", "data": torchState])
... ... @@ -150,20 +149,20 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
}
}
} catch MobileScannerError.alreadyStarted {
result(FlutterError(code: "MobileScanner",
message: "Called start() while already started!",
result(FlutterError(code: MobileScannerErrorCodes.ALREADY_STARTED_ERROR,
message: MobileScannerErrorCodes.ALREADY_STARTED_ERROR_MESSAGE,
details: nil))
} catch MobileScannerError.noCamera {
result(FlutterError(code: "MobileScanner",
message: "No camera found or failed to open camera!",
result(FlutterError(code: MobileScannerErrorCodes.NO_CAMERA_ERROR,
message: MobileScannerErrorCodes.NO_CAMERA_ERROR_MESSAGE,
details: nil))
} catch MobileScannerError.cameraError(let error) {
result(FlutterError(code: "MobileScanner",
message: "Error occured when setting up camera!",
details: error))
result(FlutterError(code: MobileScannerErrorCodes.CAMERA_ERROR,
message: error.localizedDescription,
details: nil))
} catch {
result(FlutterError(code: "MobileScanner",
message: "Unknown error occured.",
result(FlutterError(code: MobileScannerErrorCodes.GENERIC_ERROR,
message: MobileScannerErrorCodes.GENERIC_ERROR_MESSAGE,
details: nil))
}
}
... ... @@ -186,25 +185,25 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
private func setScale(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
let scale = call.arguments as? CGFloat
if (scale == nil) {
result(FlutterError(code: "MobileScanner",
message: "You must provide a scale when calling setScale!",
details: nil))
result(FlutterError(code: MobileScannerErrorCodes.GENERIC_ERROR,
message: MobileScannerErrorCodes.INVALID_ZOOM_SCALE_ERROR_MESSAGE,
details: "The invalid zoom scale was nil."))
return
}
do {
try mobileScanner.setScale(scale!)
result(nil)
} catch MobileScannerError.zoomWhenStopped {
result(FlutterError(code: "MobileScanner",
message: "Called setScale() while stopped!",
result(FlutterError(code: MobileScannerErrorCodes.SET_SCALE_WHEN_STOPPED_ERROR,
message: MobileScannerErrorCodes.SET_SCALE_WHEN_STOPPED_ERROR_MESSAGE,
details: nil))
} catch MobileScannerError.zoomError(let error) {
result(FlutterError(code: "MobileScanner",
message: "Error while zooming.",
details: error))
result(FlutterError(code: MobileScannerErrorCodes.GENERIC_ERROR,
message: error.localizedDescription,
details: nil))
} catch {
result(FlutterError(code: "MobileScanner",
message: "Error while zooming.",
result(FlutterError(code: MobileScannerErrorCodes.GENERIC_ERROR,
message: MobileScannerErrorCodes.GENERIC_ERROR_MESSAGE,
details: nil))
}
}
... ... @@ -215,16 +214,16 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
try mobileScanner.resetScale()
result(nil)
} catch MobileScannerError.zoomWhenStopped {
result(FlutterError(code: "MobileScanner",
message: "Called resetScale() while stopped!",
result(FlutterError(code: MobileScannerErrorCodes.SET_SCALE_WHEN_STOPPED_ERROR,
message: MobileScannerErrorCodes.SET_SCALE_WHEN_STOPPED_ERROR_MESSAGE,
details: nil))
} catch MobileScannerError.zoomError(let error) {
result(FlutterError(code: "MobileScanner",
message: "Error while zooming.",
details: error))
result(FlutterError(code: MobileScannerErrorCodes.GENERIC_ERROR,
message: error.localizedDescription,
details: nil))
} catch {
result(FlutterError(code: "MobileScanner",
message: "Error while zooming.",
result(FlutterError(code: MobileScannerErrorCodes.GENERIC_ERROR,
message: MobileScannerErrorCodes.GENERIC_ERROR_MESSAGE,
details: nil))
}
}
... ... @@ -258,9 +257,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
let uiImage = UIImage(contentsOfFile: (call.arguments as! Dictionary<String, Any?>)["filePath"] as? String ?? "")
if (uiImage == nil) {
result(FlutterError(code: "MobileScanner",
message: "No image found in analyzeImage!",
details: nil))
result(nil)
return
}
... ... @@ -268,7 +265,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
barcodeScannerOptions: scannerOptions, callback: { barcodes, error in
if error != nil {
DispatchQueue.main.async {
result(FlutterError(code: "MobileScanner",
result(FlutterError(code: MobileScannerErrorCodes.BARCODE_ERROR,
message: error?.localizedDescription,
details: nil))
}
... ...
... ... @@ -4,7 +4,7 @@
#
Pod::Spec.new do |s|
s.name = 'mobile_scanner'
s.version = '5.2.3'
s.version = '6.0.2'
s.summary = 'An universal scanner for Flutter based on MLKit.'
s.description = <<-DESC
An universal scanner for Flutter based on MLKit.
... ... @@ -15,11 +15,16 @@ An universal scanner for Flutter based on MLKit.
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'Flutter'
s.dependency 'GoogleMLKit/BarcodeScanning', '~> 6.0.0'
s.platform = :ios, '12.0'
s.dependency 'GoogleMLKit/BarcodeScanning', '~> 7.0.0'
s.platform = :ios, '15.5.0'
s.static_framework = true
# Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
# Flutter.framework does not contain a i386 slice, and MLKit does not support armv7.
s.pod_target_xcconfig = {
'DEFINES_MODULE' => 'YES',
# TODO: add back arm64 (and armv7?) when switching to the Vision API.
'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386 armv7 arm64',
'EXCLUDED_ARCHS[sdk=iphoneos*]' => 'armv7',
}
s.swift_version = '5.0'
s.resource_bundles = { 'mobile_scanner_privacy' => ['Resources/PrivacyInfo.xcprivacy'] }
end
... ...
... ... @@ -11,8 +11,7 @@ export 'src/enums/phone_type.dart';
export 'src/enums/torch_state.dart';
export 'src/mobile_scanner.dart';
export 'src/mobile_scanner_controller.dart';
export 'src/mobile_scanner_exception.dart'
hide PermissionRequestPendingException;
export 'src/mobile_scanner_exception.dart';
export 'src/mobile_scanner_platform_interface.dart';
export 'src/objects/address.dart';
export 'src/objects/barcode.dart';
... ...
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,
};
}
}
... ...
... ... @@ -16,6 +16,14 @@ import 'package:mobile_scanner/src/objects/start_options.dart';
/// An implementation of [MobileScannerPlatform] that uses method channels.
class MethodChannelMobileScanner extends MobileScannerPlatform {
/// The name of the barcode event that is sent when a barcode is scanned.
@visibleForTesting
static const String kBarcodeEventName = 'barcode';
/// The name of the error event that is sent when a barcode scan error occurs.
@visibleForTesting
static const String kBarcodeErrorEventName = 'MOBILE_SCANNER_BARCODE_ERROR';
/// The method channel used to interact with the native platform.
@visibleForTesting
final methodChannel = const MethodChannel(
... ... @@ -79,6 +87,19 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
);
}
/// Parse a [MobileScannerBarcodeException] from the given [error] and [stackTrace], and throw it.
///
/// If the error is not a [PlatformException],
/// with [kBarcodeErrorEventName] as [PlatformException.code], the error is rethrown as-is.
Never _parseBarcodeError(Object error, StackTrace stackTrace) {
if (error case PlatformException(:final String code, :final String? message)
when code == kBarcodeErrorEventName) {
throw MobileScannerBarcodeException(message);
}
Error.throwWithStackTrace(error, stackTrace);
}
/// Request permission to access the camera.
///
/// Throws a [MobileScannerException] if the permission is not granted.
... ... @@ -121,9 +142,12 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
@override
Stream<BarcodeCapture?> get barcodesStream {
// Handle incoming barcode events.
// The error events are transformed to `MobileScannerBarcodeException` where possible.
return eventsStream
.where((event) => event['name'] == 'barcode')
.map((event) => _parseBarcode(event));
.where((e) => e['name'] == kBarcodeEventName)
.map((event) => _parseBarcode(event))
.handleError(_parseBarcodeError);
}
@override
... ... @@ -145,6 +169,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
String path, {
List<BarcodeFormat> formats = const <BarcodeFormat>[],
}) async {
try {
final Map<Object?, Object?>? result =
await methodChannel.invokeMapMethod<Object?, Object?>(
'analyzeImage',
... ... @@ -160,6 +185,14 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
);
return _parseBarcode(result);
} on PlatformException catch (error) {
// Handle any errors from analyze image requests.
if (error.code == kBarcodeErrorEventName) {
throw MobileScannerBarcodeException(error.message);
}
return null;
}
}
@override
... ... @@ -187,8 +220,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 +236,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 +272,13 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
startResult['currentTorchState'] as int? ?? -1,
);
final Map<Object?, Object?>? sizeInfo =
startResult['size'] as Map<Object?, Object?>?;
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(
... ...
... ... @@ -21,6 +21,7 @@ class MobileScanner extends StatefulWidget {
const MobileScanner({
this.controller,
this.onDetect,
this.onDetectError = _onDetectErrorHandler,
this.fit = BoxFit.cover,
this.errorBuilder,
this.overlayBuilder,
... ... @@ -34,9 +35,17 @@ class MobileScanner extends StatefulWidget {
final MobileScannerController? controller;
/// The function that signals when new codes were detected by the [controller].
/// If null, use the controller.barcodes stream directly to capture barcodes.
///
/// To handle both [BarcodeCapture]s and [MobileScannerBarcodeException]s,
/// use the [MobileScannerController.barcodes] stream directly (recommended),
/// or provide a function to [onDetectError].
final void Function(BarcodeCapture barcodes)? onDetect;
/// The error handler equivalent for the [onDetect] function.
///
/// If [onDetect] is not null, and this is null, errors are silently ignored.
final void Function(Object error, StackTrace stackTrace) onDetectError;
/// The error builder for the camera preview.
///
/// If this is null, a black [ColoredBox],
... ... @@ -116,6 +125,11 @@ class MobileScanner extends StatefulWidget {
@override
State<MobileScanner> createState() => _MobileScannerState();
/// This empty function is used as the default error handler for [onDetect].
static void _onDetectErrorHandler(Object error, StackTrace stackTrace) {
// Do nothing.
}
}
class _MobileScannerState extends State<MobileScanner>
... ... @@ -249,7 +263,11 @@ class _MobileScannerState extends State<MobileScanner>
void initState() {
if (widget.onDetect != null) {
WidgetsBinding.instance.addObserver(this);
_subscription = controller.barcodes.listen(widget.onDetect);
_subscription = controller.barcodes.listen(
widget.onDetect,
onError: widget.onDetectError,
cancelOnError: false,
);
}
if (controller.autoStart) {
controller.start();
... ... @@ -281,8 +299,7 @@ class _MobileScannerState extends State<MobileScanner>
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (widget.controller != null) return;
if (!controller.value.isInitialized) {
if (widget.controller != null || !controller.value.hasCameraPermission) {
return;
}
... ... @@ -292,7 +309,11 @@ class _MobileScannerState extends State<MobileScanner>
case AppLifecycleState.paused:
return;
case AppLifecycleState.resumed:
_subscription = controller.barcodes.listen(widget.onDetect);
_subscription = controller.barcodes.listen(
widget.onDetect,
onError: widget.onDetectError,
cancelOnError: false,
);
unawaited(controller.start());
case AppLifecycleState.inactive:
... ...
... ... @@ -75,8 +75,7 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
/// If this is empty, all supported formats are detected.
final List<BarcodeFormat> formats;
/// Whether scanned barcodes should contain the image
/// that is embedded into the barcode.
/// Whether the [BarcodeCapture.image] bytes should be provided.
///
/// If this is false, [BarcodeCapture.image] will always be null.
///
... ... @@ -102,6 +101,9 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
StreamController.broadcast();
/// Get the stream of scanned barcodes.
///
/// If an error occurred during the detection of a barcode,
/// a [MobileScannerBarcodeException] error is emitted to the stream.
Stream<BarcodeCapture> get barcodes => _barcodesController.stream;
StreamSubscription<BarcodeCapture?>? _barcodesSubscription;
... ... @@ -121,14 +123,25 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
}
void _setupListeners() {
_barcodesSubscription = MobileScannerPlatform.instance.barcodesStream
.listen((BarcodeCapture? barcode) {
_barcodesSubscription =
MobileScannerPlatform.instance.barcodesStream.listen(
(BarcodeCapture? barcode) {
if (_barcodesController.isClosed || barcode == null) {
return;
}
_barcodesController.add(barcode);
});
},
onError: (Object error) {
if (_barcodesController.isClosed) {
return;
}
_barcodesController.addError(error);
},
// Errors are handled gracefully by forwarding them.
cancelOnError: false,
);
_torchStateSubscription = MobileScannerPlatform.instance.torchStateStream
.listen((TorchState torchState) {
... ... @@ -173,12 +186,20 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
/// Analyze an image file.
///
/// The [path] points to a file on the device.
/// The [formats] specify the barcode formats that should be detected in the image.
/// If the [formats] are omitted or empty, all formats are detected.
///
/// This is only supported on Android, iOS and MacOS.
///
/// Returns the [BarcodeCapture] that was found in the image.
Future<BarcodeCapture?> analyzeImage(String path) {
return MobileScannerPlatform.instance.analyzeImage(path);
///
/// If an error occurred during the analysis of the image,
/// a [MobileScannerBarcodeException] error is thrown.
Future<BarcodeCapture?> analyzeImage(
String path, {
List<BarcodeFormat> formats = const <BarcodeFormat>[],
}) {
return MobileScannerPlatform.instance.analyzeImage(path, formats: formats);
}
/// Build a camera preview widget.
... ... @@ -246,13 +267,6 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
);
}
// Permission was denied, do nothing.
// When the controller is stopped,
// the error is reset so the permission can be requested again if possible.
if (value.error?.errorCode == MobileScannerErrorCode.permissionDenied) {
return;
}
// Do nothing if the camera is already running.
if (value.isRunning) {
return;
... ... @@ -292,6 +306,13 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
);
}
} on MobileScannerException catch (error) {
// If the controller is already initialized, ignore the error.
// Starting the controller while it is already started, or in the process of starting, is redundant.
if (error.errorCode ==
MobileScannerErrorCode.controllerAlreadyInitialized) {
return;
}
// The initialization finished with an error.
// To avoid stale values, reset the output size,
// torch state and zoom scale to the defaults.
... ... @@ -306,8 +327,6 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
zoomScale: 1.0,
);
}
} on PermissionRequestPendingException catch (_) {
// If a permission request was already pending, do nothing.
}
}
... ...
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})';
}
}
... ... @@ -40,9 +40,21 @@ class MobileScannerErrorDetails {
final String? message;
}
/// This class represents an exception that is thrown
/// when the scanner was (re)started while a permission request was pending.
///
/// 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.)';
}
}
... ...
... ... @@ -32,8 +32,6 @@ class BarcodeCapture {
/// This is the data that was used to detect the available [barcodes], the input [image] and the [size].
final Object? raw;
/// The size of the input [image].
///
/// If [image] is null, this will be [Size.zero].
/// The size of the camera input [image].
final Size size;
}
... ...
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,
... ...
... ... @@ -39,12 +39,6 @@ class MobileScannerWeb extends MobileScannerPlatform {
/// The container div element for the camera view.
late HTMLDivElement _divElement;
/// The flag that keeps track of whether a permission request is in progress.
///
/// On the web, a permission request triggers a dialog, that in turn triggers a lifecycle change.
/// While the permission request is in progress, any attempts at (re)starting the camera should be ignored.
bool _permissionRequestInProgress = false;
/// The stream controller for the media track settings stream.
///
/// Currently, only the facing mode setting can be supported,
... ... @@ -149,6 +143,7 @@ class MobileScannerWeb extends MobileScannerPlatform {
final JSArray<JSString>? facingModes = capabilities.facingModeNullable;
// TODO: this is an empty array on MacOS Chrome, where there is no facing mode, but one, user facing camera.
// We might be able to add a workaround, using the label of the video track.
// Facing mode is not supported by this track, do nothing.
if (facingModes == null || facingModes.toDart.isEmpty) {
return;
... ... @@ -199,17 +194,12 @@ class MobileScannerWeb extends MobileScannerPlatform {
}
try {
_permissionRequestInProgress = true;
// Retrieving the media devices requests the camera permission.
final MediaStream videoStream =
await window.navigator.mediaDevices.getUserMedia(constraints).toDart;
_permissionRequestInProgress = false;
return videoStream;
} on DOMException catch (error, stackTrace) {
_permissionRequestInProgress = false;
final String errorMessage = error.toString();
MobileScannerErrorCode errorCode = MobileScannerErrorCode.genericError;
... ... @@ -272,11 +262,13 @@ class MobileScannerWeb extends MobileScannerPlatform {
@override
Future<MobileScannerViewAttributes> start(StartOptions startOptions) async {
// If the permission request has not yet completed,
// the camera view is not ready yet.
// Prevent the permission popup from triggering a restart of the scanner.
if (_permissionRequestInProgress) {
throw PermissionRequestPendingException();
if (_barcodeReader != null) {
throw const MobileScannerException(
errorCode: MobileScannerErrorCode.controllerAlreadyInitialized,
errorDetails: MobileScannerErrorDetails(
message: 'The scanner was already started.',
),
);
}
_barcodeReader = ZXingBarcodeReader();
... ... @@ -285,16 +277,6 @@ class MobileScannerWeb extends MobileScannerPlatform {
alternateScriptUrl: _alternateScriptUrl,
);
if (_barcodeReader?.isScanning ?? false) {
throw const MobileScannerException(
errorCode: MobileScannerErrorCode.controllerAlreadyInitialized,
errorDetails: MobileScannerErrorDetails(
message:
'The scanner was already started. Call stop() before calling start() again.',
),
);
}
// Request camera permissions and prepare the video stream.
final MediaStream videoStream = await _prepareVideoStream(
startOptions.cameraDirection,
... ... @@ -341,6 +323,15 @@ class MobileScannerWeb extends MobileScannerPlatform {
_barcodesController.add(barcode);
},
onError: (Object error) {
if (_barcodesController.isClosed) {
return;
}
_barcodesController.addError(error);
},
// Errors are handled gracefully by forwarding them.
cancelOnError: false,
);
final bool hasTorch = await _barcodeReader?.hasTorch() ?? false;
... ...
... ... @@ -2,7 +2,9 @@ import 'dart:async';
import 'dart:js_interop';
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:mobile_scanner/src/enums/barcode_format.dart';
import 'package:mobile_scanner/src/mobile_scanner_exception.dart';
import 'package:mobile_scanner/src/objects/barcode_capture.dart';
import 'package:mobile_scanner/src/objects/start_options.dart';
import 'package:mobile_scanner/src/web/barcode_reader.dart';
... ... @@ -10,12 +12,18 @@ import 'package:mobile_scanner/src/web/javascript_map.dart';
import 'package:mobile_scanner/src/web/media_track_constraints_delegate.dart';
import 'package:mobile_scanner/src/web/zxing/result.dart';
import 'package:mobile_scanner/src/web/zxing/zxing_browser_multi_format_reader.dart';
import 'package:mobile_scanner/src/web/zxing/zxing_exception.dart';
import 'package:web/web.dart' as web;
/// A barcode reader implementation that uses the ZXing library.
final class ZXingBarcodeReader extends BarcodeReader {
ZXingBarcodeReader();
/// ZXing reports an error with this message if the code could not be detected.
@visibleForTesting
static const String kNoCodeDetectedErrorMessage =
'No MultiFormat Readers were able to detect the code.';
/// The listener for media track settings changes.
void Function(web.MediaTrackSettings)? _onMediaTrackSettingsChanged;
... ... @@ -98,16 +106,25 @@ final class ZXingBarcodeReader extends BarcodeReader {
_reader?.decodeContinuously.callAsFunction(
_reader,
_reader?.videoElement,
(Result? result, JSAny? error) {
if (controller.isClosed || result == null) {
(Result? result, ZXingException? error) {
if (controller.isClosed) {
return;
}
// Skip the event if no code was detected.
if (error != null && error.message != kNoCodeDetectedErrorMessage) {
controller.addError(MobileScannerBarcodeException(error.message));
return;
}
if (result != null) {
controller.add(
BarcodeCapture(
barcodes: [result.toBarcode],
size: videoSize,
),
);
}
}.toJS,
);
};
... ...
import 'dart:js_interop';
/// The JS static interop class for the Result class in the ZXing library.
///
/// See also: https://github.com/zxing-js/library/blob/master/src/core/Exception.ts
@JS('ZXing.Exception')
extension type ZXingException._(JSObject _) implements JSObject {
/// The error message of the exception, if any.
external String? get message;
}
... ...
... ... @@ -4,7 +4,7 @@
#
Pod::Spec.new do |s|
s.name = 'mobile_scanner'
s.version = '5.2.3'
s.version = '6.0.2'
s.summary = 'An universal scanner for Flutter based on MLKit.'
s.description = <<-DESC
An universal scanner for Flutter based on MLKit.
... ... @@ -18,4 +18,5 @@ An universal scanner for Flutter based on MLKit.
s.platform = :osx, '10.14'
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
s.swift_version = '5.0'
s.resource_bundles = {'mobile_scanner_macos_privacy' => ['mobile_scanner/Sources/mobile_scanner/Resources/PrivacyInfo.xcprivacy']}
end
... ...
... ... @@ -17,8 +17,7 @@ let package = Package(
name: "mobile_scanner",
dependencies: [],
resources: [
// To add other resources, see the instructions at
// https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package
.process("Resources"),
]
)
]
... ...
//
// 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."
}
... ...
... ... @@ -131,7 +131,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
if error != nil {
DispatchQueue.main.async {
self?.sink?(FlutterError(code: "MobileScanner", message: error?.localizedDescription, details: nil))
self?.sink?(FlutterError(
code: MobileScannerErrorCodes.BARCODE_ERROR,
message: error?.localizedDescription, details: nil))
}
return
}
... ... @@ -154,7 +156,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
})
DispatchQueue.main.async {
if (!MobileScannerPlugin.returnImage) {
guard let image = cgImage else {
self?.sink?([
"name": "barcode",
"data": barcodes.map({ $0.toMap() }),
... ... @@ -162,14 +164,18 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
return
}
// The image dimensions are always provided.
// The image bytes are only non-null when `returnImage` is true.
let imageData: [String: Any?] = [
"bytes": MobileScannerPlugin.returnImage ? FlutterStandardTypedData(bytes: image.jpegData(compressionQuality: 0.8)!) : nil,
"width": Double(image.width),
"height": Double(image.height),
]
self?.sink?([
"name": "barcode",
"data": barcodes.map({ $0.toMap() }),
"image": cgImage == nil ? nil : [
"bytes": FlutterStandardTypedData(bytes: cgImage!.jpegData(compressionQuality: 0.8)!),
"width": Double(cgImage!.width),
"height": Double(cgImage!.height),
],
"image": imageData,
])
}
})
... ... @@ -180,9 +186,11 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
}
try imageRequestHandler.perform([barcodeRequest])
} catch let e {
} catch let error {
DispatchQueue.main.async {
self?.sink?(FlutterError(code: "MobileScanner", message: e.localizedDescription, details: nil))
self?.sink?(FlutterError(
code: MobileScannerErrorCodes.BARCODE_ERROR,
message: error.localizedDescription, details: nil))
}
}
}
... ... @@ -262,8 +270,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
func start(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
if (device != nil || captureSession != nil) {
result(FlutterError(code: "MobileScanner",
message: "Called start() while already started!",
result(FlutterError(code: MobileScannerErrorCodes.ALREADY_STARTED_ERROR,
message: MobileScannerErrorCodes.ALREADY_STARTED_ERROR_MESSAGE,
details: nil))
return
}
... ... @@ -273,7 +281,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
... ... @@ -295,8 +302,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
}
if (device == nil) {
result(FlutterError(code: "MobileScanner",
message: "No camera found or failed to open camera!",
result(FlutterError(code: MobileScannerErrorCodes.NO_CAMERA_ERROR,
message: MobileScannerErrorCodes.NO_CAMERA_ERROR_MESSAGE,
details: nil))
return
}
... ... @@ -314,7 +321,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
let input = try AVCaptureDeviceInput(device: device)
captureSession!.addInput(input)
} catch {
result(FlutterError(code: "MobileScanner", message: error.localizedDescription, details: nil))
result(FlutterError(
code: MobileScannerErrorCodes.CAMERA_ERROR,
message: error.localizedDescription, details: nil))
return
}
captureSession!.sessionPreset = AVCaptureSession.Preset.photo
... ... @@ -327,7 +336,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 +467,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
}
... ... @@ -484,8 +486,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
if error != nil {
DispatchQueue.main.async {
// TODO: fix error code
result(FlutterError(code: "MobileScanner", message: error?.localizedDescription, details: nil))
result(FlutterError(
code: MobileScannerErrorCodes.BARCODE_ERROR,
message: error?.localizedDescription, details: nil))
}
return
}
... ... @@ -510,10 +513,11 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
}
try imageRequestHandler.perform([barcodeRequest])
} catch let e {
// TODO: fix error code
} catch let error {
DispatchQueue.main.async {
result(FlutterError(code: "MobileScanner", message: e.localizedDescription, details: nil))
result(FlutterError(
code: MobileScannerErrorCodes.BARCODE_ERROR,
message: error.localizedDescription, details: nil))
}
}
}
... ...
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- The key NSPrivacyAccessedAPITypes is not required for MacOS -->
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyTracking</key>
<false/>
</dict>
</plist>
\ No newline at end of file
... ...
name: mobile_scanner
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.
version: 5.2.3
version: 6.0.2
repository: https://github.com/juliansteenbakker/mobile_scanner
screenshots:
... ...