Sander Roest
Committed by GitHub

Merge branch 'juliansteenbakker:master' into master

## NEXT
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
... ...
... ... @@ -127,7 +127,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;
}
... ... @@ -192,4 +192,4 @@ Future<void> dispose() async {
To display the camera preview, pass the controller to a `MobileScanner` widget.
See the [examples](example/README.md) for runnable examples of various usages,
such as the basic usage, applying a scan window, or retrieving images from the barcodes.
such as the basic usage, applying a scan window, or retrieving images from the barcodes.
\ 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)
... ...
... ... @@ -120,7 +120,11 @@ class MobileScanner(
}
if (!returnImage) {
mobileScannerCallback(barcodeMap, null, null, null)
mobileScannerCallback(
barcodeMap,
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,
"image" to mapOf(
"bytes" to image,
"width" to width?.toDouble(),
"height" to height?.toDouble(),
)
))
} else {
barcodeHandler.publishEvent(mapOf(
"name" to "barcode",
"data" to barcodes
))
}
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(),
)
))
}
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
... ...
... ... @@ -5,9 +5,11 @@
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
... ...
... ... @@ -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";
... ... @@ -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',
);
}
... ...
... ... @@ -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(
Icons.no_flash,
color: Colors.grey,
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)
... ...
... ... @@ -20,7 +20,7 @@ struct MobileScannerErrorCodes {
// 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.
// 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."
... ...
... ... @@ -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))
}
}
... ... @@ -266,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))
}
... ...
... ... @@ -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';
... ...
... ... @@ -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,21 +169,30 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
String path, {
List<BarcodeFormat> formats = const <BarcodeFormat>[],
}) async {
final Map<Object?, Object?>? result =
await methodChannel.invokeMapMethod<Object?, Object?>(
'analyzeImage',
{
'filePath': path,
'formats': formats.isEmpty
? null
: [
for (final BarcodeFormat format in formats)
if (format != BarcodeFormat.unknown) format.rawValue,
],
},
);
try {
final Map<Object?, Object?>? result =
await methodChannel.invokeMapMethod<Object?, Object?>(
'analyzeImage',
{
'filePath': path,
'formats': formats.isEmpty
? null
: [
for (final BarcodeFormat format in formats)
if (format != BarcodeFormat.unknown) format.rawValue,
],
},
);
return _parseBarcode(result);
} on PlatformException catch (error) {
// Handle any errors from analyze image requests.
if (error.code == kBarcodeErrorEventName) {
throw MobileScannerBarcodeException(error.message);
}
return _parseBarcode(result);
return null;
}
}
@override
... ...
... ... @@ -35,6 +35,12 @@ class MobileScanner extends StatefulWidget {
/// The function that signals when new codes were detected by the [controller].
/// If null, use the controller.barcodes stream directly to capture barcodes.
///
/// This method does not receive any [MobileScannerBarcodeException]s
/// that are emitted by the scanner.
///
/// To handle both [BarcodeCapture]s and [MobileScannerBarcodeException]s,
/// use the [MobileScannerController.barcodes] stream directly.
final void Function(BarcodeCapture barcodes)? onDetect;
/// The error builder for the camera preview.
... ...
... ... @@ -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) {
if (_barcodesController.isClosed || barcode == null) {
return;
}
_barcodesController.add(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) {
... ... @@ -177,6 +190,9 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
/// This is only supported on Android, iOS and MacOS.
///
/// Returns the [BarcodeCapture] that was found in the image.
///
/// If an error occurred during the analysis of the image,
/// a [MobileScannerBarcodeException] error is thrown.
Future<BarcodeCapture?> analyzeImage(String path) {
return MobileScannerPlatform.instance.analyzeImage(path);
}
... ... @@ -246,13 +262,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 +301,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 +322,6 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
zoomScale: 1.0,
);
}
} on PermissionRequestPendingException catch (_) {
// If a permission request was already pending, do nothing.
}
}
... ...
... ... @@ -40,13 +40,6 @@ 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 {
... ...
... ... @@ -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;
}
... ...
... ... @@ -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;
}
controller.add(
BarcodeCapture(
barcodes: [result.toBarcode],
),
);
// 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;
}
... ...
... ... @@ -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"),
]
)
]
... ...
... ... @@ -14,7 +14,7 @@ struct MobileScannerErrorCodes {
// 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.
// 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,22 +156,26 @@ 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() }),
])
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
}
... ... @@ -294,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
}
... ... @@ -313,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
... ... @@ -476,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
}
... ... @@ -502,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
... ...