From 10d26f8529cf8cd9da264204c7ed28009e95edfe Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Wed, 24 Apr 2024 14:20:31 +0200 Subject: [PATCH 01/17] take into account the auto mode on iOS/MacOS --- .../mobile_scanner/MobileScannerHandler.kt | 1 + example/lib/scanner_button_widgets.dart | 11 +++++++++++ ios/Classes/MobileScanner.swift | 2 +- lib/src/enums/torch_state.dart | 11 +++++++++-- macos/Classes/MobileScannerPlugin.swift | 2 +- test/enums/torch_state_test.dart | 8 +++++--- 6 files changed, 28 insertions(+), 7 deletions(-) diff --git a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerHandler.kt b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerHandler.kt index 17299ebd3..223798388 100644 --- a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerHandler.kt +++ b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerHandler.kt @@ -74,6 +74,7 @@ class MobileScannerHandler( private var mobileScanner: MobileScanner? = null private val torchStateCallback: TorchStateCallback = {state: Int -> + // Off = 0, On = 1 barcodeHandler.publishEvent(mapOf("name" to "torchState", "data" to state)) } diff --git a/example/lib/scanner_button_widgets.dart b/example/lib/scanner_button_widgets.dart index 427441cdc..8e05b238a 100644 --- a/example/lib/scanner_button_widgets.dart +++ b/example/lib/scanner_button_widgets.dart @@ -138,6 +138,17 @@ class ToggleFlashlightButton extends StatelessWidget { } switch (state.torchState) { + case TorchState.auto: + return IconButton( + color: Colors.white, + iconSize: 32.0, + icon: const Icon(Icons.flash_auto), + onPressed: () async { + // This button only turns off the auto mode. + // Perhaps we can switch between on / off / auto? + await controller.toggleTorch(); + }, + ); case TorchState.off: return IconButton( color: Colors.white, diff --git a/ios/Classes/MobileScanner.swift b/ios/Classes/MobileScanner.swift index 69ea169a1..d4b74fb45 100644 --- a/ios/Classes/MobileScanner.swift +++ b/ios/Classes/MobileScanner.swift @@ -347,7 +347,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { switch keyPath { case "torchMode": - // off = 0; on = 1; auto = 2 + // Off = 0, On = 1, Auto = 2 let state = change?[.newKey] as? Int torchModeChangeCallback(state) case "videoZoomFactor": diff --git a/lib/src/enums/torch_state.dart b/lib/src/enums/torch_state.dart index 4a6d9b1c5..31d91d8be 100644 --- a/lib/src/enums/torch_state.dart +++ b/lib/src/enums/torch_state.dart @@ -1,5 +1,10 @@ /// The state of the flashlight. enum TorchState { + /// The flashlight turns on automatically in low light conditions. + /// + /// This is currently only supported on iOS. + auto(2), + /// The flashlight is off. off(0), @@ -7,18 +12,20 @@ enum TorchState { on(1), /// The flashlight is unavailable. - unavailable(2); + unavailable(-1); const TorchState(this.rawValue); factory TorchState.fromRawValue(int value) { switch (value) { + case -1: + return TorchState.unavailable; case 0: return TorchState.off; case 1: return TorchState.on; case 2: - return TorchState.unavailable; + return TorchState.auto; default: throw ArgumentError.value(value, 'value', 'Invalid raw value.'); } diff --git a/macos/Classes/MobileScannerPlugin.swift b/macos/Classes/MobileScannerPlugin.swift index fcc6e36c1..b422f35eb 100644 --- a/macos/Classes/MobileScannerPlugin.swift +++ b/macos/Classes/MobileScannerPlugin.swift @@ -410,7 +410,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { switch keyPath { case "torchMode": - // off = 0 on = 1 auto = 2 + // Off = 0, On = 1, Auto = 2 let state = change?[.newKey] as? Int let event: [String: Any?] = ["name": "torchState", "data": state] sink?(event) diff --git a/test/enums/torch_state_test.dart b/test/enums/torch_state_test.dart index ad528832e..a4aa6c2a0 100644 --- a/test/enums/torch_state_test.dart +++ b/test/enums/torch_state_test.dart @@ -7,7 +7,8 @@ void main() { const values = { 0: TorchState.off, 1: TorchState.on, - 2: TorchState.unavailable, + 2: TorchState.auto, + -1: TorchState.unavailable, }; for (final MapEntry entry in values.entries) { @@ -18,7 +19,7 @@ void main() { }); test('invalid raw value throws argument error', () { - const int negative = -1; + const int negative = -2; const int outOfRange = 3; expect(() => TorchState.fromRawValue(negative), throwsArgumentError); @@ -27,9 +28,10 @@ void main() { test('can be converted to raw value', () { const values = { + TorchState.unavailable: -1, TorchState.off: 0, TorchState.on: 1, - TorchState.unavailable: 2, + TorchState.auto: 2, }; for (final MapEntry entry in values.entries) { From 35dc61cb55de4bd43e41f8b899970d26a8a6427b Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Fri, 26 Apr 2024 13:36:05 +0200 Subject: [PATCH 02/17] provide the current torch mode instead of `hasTorch` when the camera starts --- lib/src/method_channel/mobile_scanner_method_channel.dart | 6 ++++-- lib/src/mobile_scanner_controller.dart | 6 +++--- lib/src/mobile_scanner_view_attributes.dart | 8 +++++--- lib/src/web/mobile_scanner_web.dart | 4 +++- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/lib/src/method_channel/mobile_scanner_method_channel.dart b/lib/src/method_channel/mobile_scanner_method_channel.dart index c3861b882..d9661159e 100644 --- a/lib/src/method_channel/mobile_scanner_method_channel.dart +++ b/lib/src/method_channel/mobile_scanner_method_channel.dart @@ -246,7 +246,9 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { _textureId = textureId; final int? numberOfCameras = startResult['numberOfCameras'] as int?; - final bool hasTorch = startResult['torchable'] as bool? ?? false; + final TorchState currentTorchState = TorchState.fromRawValue( + startResult['currentTorchState'] as int? ?? -1, + ); final Map? sizeInfo = startResult['size'] as Map?; @@ -262,7 +264,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { } return MobileScannerViewAttributes( - hasTorch: hasTorch, + currentTorchMode: currentTorchState, numberOfCameras: numberOfCameras, size: size, ); diff --git a/lib/src/mobile_scanner_controller.dart b/lib/src/mobile_scanner_controller.dart index 5793a2476..321470e43 100644 --- a/lib/src/mobile_scanner_controller.dart +++ b/lib/src/mobile_scanner_controller.dart @@ -281,9 +281,9 @@ class MobileScannerController extends ValueNotifier { isInitialized: true, isRunning: true, size: viewAttributes.size, - // If the device has a flashlight, let the platform update the torch state. - // If it does not have one, provide the unavailable state directly. - torchState: viewAttributes.hasTorch ? null : TorchState.unavailable, + // Provide the current torch state. + // Updates are provided by the `torchStateStream`. + torchState: viewAttributes.currentTorchMode, ); } } on MobileScannerException catch (error) { diff --git a/lib/src/mobile_scanner_view_attributes.dart b/lib/src/mobile_scanner_view_attributes.dart index 1e5d72b1a..d203fd2ba 100644 --- a/lib/src/mobile_scanner_view_attributes.dart +++ b/lib/src/mobile_scanner_view_attributes.dart @@ -1,15 +1,17 @@ import 'dart:ui'; +import 'package:mobile_scanner/src/enums/torch_state.dart'; + /// This class defines the attributes for the mobile scanner view. class MobileScannerViewAttributes { const MobileScannerViewAttributes({ - required this.hasTorch, + required this.currentTorchMode, this.numberOfCameras, required this.size, }); - /// Whether the current active camera has a torch. - final bool hasTorch; + /// The current torch state of the active camera. + final TorchState currentTorchMode; /// The number of available cameras. final int? numberOfCameras; diff --git a/lib/src/web/mobile_scanner_web.dart b/lib/src/web/mobile_scanner_web.dart index cee84d6b8..8e3a811da 100644 --- a/lib/src/web/mobile_scanner_web.dart +++ b/lib/src/web/mobile_scanner_web.dart @@ -346,7 +346,9 @@ class MobileScannerWeb extends MobileScannerPlatform { } return MobileScannerViewAttributes( - hasTorch: hasTorch, + // The torch of a media stream is not available for video tracks. + // See https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints#instance_properties_of_video_tracks + currentTorchMode: TorchState.unavailable, size: _barcodeReader?.videoSize ?? Size.zero, ); } catch (error, stackTrace) { From 2178ccdab3fa15af2f60c3c568703e0d38f44cf1 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Fri, 26 Apr 2024 14:11:32 +0200 Subject: [PATCH 03/17] forward initial torch state on Android --- .../dev/steenbakker/mobile_scanner/MobileScanner.kt | 13 ++++++++++++- .../mobile_scanner/MobileScannerHandler.kt | 2 +- .../objects/MobileScannerStartParameters.kt | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt index 7cd091132..07ab32994 100644 --- a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt +++ b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt @@ -368,11 +368,22 @@ class MobileScanner( val height = resolution.height.toDouble() val portrait = (camera?.cameraInfo?.sensorRotationDegrees ?: 0) % 180 == 0 + // Start with 'unavailable' torch state. + var currentTorchState: Int = -1 + + camera?.cameraInfo?.let { + if (!it.hasFlashUnit()) { + return@let + } + + currentTorchState = it.torchState.value ?: -1 + } + mobileScannerStartedCallback( MobileScannerStartParameters( if (portrait) width else height, if (portrait) height else width, - camera?.cameraInfo?.hasFlashUnit() ?: false, + currentTorchState, textureEntry!!.id(), numberOfCameras ?: 0 ) diff --git a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerHandler.kt b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerHandler.kt index 223798388..af44ff1f6 100644 --- a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerHandler.kt +++ b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerHandler.kt @@ -183,7 +183,7 @@ class MobileScannerHandler( result.success(mapOf( "textureId" to it.id, "size" to mapOf("width" to it.width, "height" to it.height), - "torchable" to it.hasFlashUnit, + "currentTorchState" to it.currentTorchState, "numberOfCameras" to it.numberOfCameras )) } diff --git a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/objects/MobileScannerStartParameters.kt b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/objects/MobileScannerStartParameters.kt index b4982b573..b6d1bcdfb 100644 --- a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/objects/MobileScannerStartParameters.kt +++ b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/objects/MobileScannerStartParameters.kt @@ -3,7 +3,7 @@ package dev.steenbakker.mobile_scanner.objects class MobileScannerStartParameters( val width: Double = 0.0, val height: Double, - val hasFlashUnit: Boolean, + val currentTorchState: Int, val id: Long, val numberOfCameras: Int ) \ No newline at end of file From c0ed87b9c5cb34a5e0ed1862abf86a7122125877 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Fri, 26 Apr 2024 15:36:59 +0200 Subject: [PATCH 04/17] forward the current torch state on iOS / MacOS --- example/macos/Runner.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- ios/Classes/MobileScanner.swift | 5 ++--- ios/Classes/MobileScannerPlugin.swift | 3 ++- macos/Classes/MobileScannerPlugin.swift | 7 ++++++- 5 files changed, 12 insertions(+), 7 deletions(-) diff --git a/example/macos/Runner.xcodeproj/project.pbxproj b/example/macos/Runner.xcodeproj/project.pbxproj index 6216dc949..75936f351 100644 --- a/example/macos/Runner.xcodeproj/project.pbxproj +++ b/example/macos/Runner.xcodeproj/project.pbxproj @@ -259,7 +259,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 331C80D4294CF70F00263BE5 = { diff --git a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index ae5cd3579..a46d0db5e 100644 --- a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ Date: Mon, 29 Apr 2024 10:24:38 +0200 Subject: [PATCH 05/17] remove unused extension function --- ios/Classes/MobileScannerUtilities.swift | 25 ------------------------ 1 file changed, 25 deletions(-) diff --git a/ios/Classes/MobileScannerUtilities.swift b/ios/Classes/MobileScannerUtilities.swift index c449040a1..8bac36bc0 100644 --- a/ios/Classes/MobileScannerUtilities.swift +++ b/ios/Classes/MobileScannerUtilities.swift @@ -8,31 +8,6 @@ extension CVBuffer { let cgImage = CIContext().createCGImage(ciImage, from: ciImage.extent) return UIImage(cgImage: cgImage!) } - - var image1: UIImage { - // Lock the base address of the pixel buffer - CVPixelBufferLockBaseAddress(self, CVPixelBufferLockFlags.readOnly) - // Get the number of bytes per row for the pixel buffer - let baseAddress = CVPixelBufferGetBaseAddress(self) - // Get the number of bytes per row for the pixel buffer - let bytesPerRow = CVPixelBufferGetBytesPerRow(self) - // Get the pixel buffer width and height - let width = CVPixelBufferGetWidth(self) - let height = CVPixelBufferGetHeight(self) - // Create a device-dependent RGB color space - let colorSpace = CGColorSpaceCreateDeviceRGB() - // Create a bitmap graphics context with the sample buffer data - var bitmapInfo = CGBitmapInfo.byteOrder32Little.rawValue - bitmapInfo |= CGImageAlphaInfo.premultipliedFirst.rawValue & CGBitmapInfo.alphaInfoMask.rawValue - //let bitmapInfo: UInt32 = CGBitmapInfo.alphaInfoMask.rawValue - let context = CGContext(data: baseAddress, width: width, height: height, bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo) - // Create a Quartz image from the pixel data in the bitmap graphics context - let quartzImage = context?.makeImage() - // Unlock the pixel buffer - CVPixelBufferUnlockBaseAddress(self, CVPixelBufferLockFlags.readOnly) - // Create an image object from the Quartz image - return UIImage(cgImage: quartzImage!) - } } extension UIDeviceOrientation { From baf915f60ac10cfb0cdb3962ce61e4d390bc9c16 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Mon, 29 Apr 2024 10:25:23 +0200 Subject: [PATCH 06/17] update example xcodeproj --- example/ios/Runner.xcodeproj/project.pbxproj | 26 ++++++++++++++++--- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index f3ebff98f..fad022828 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -198,6 +198,7 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 3DBCC0215D7BED1D9A756EA3 /* [CP] Embed Pods Frameworks */, + BB0C8EA8DA81A75DE53F052F /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -215,7 +216,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 331C8080294A63A400263BE5 = { @@ -317,6 +318,23 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + BB0C8EA8DA81A75DE53F052F /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; C7DE006A696F551C4E067E41 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -495,7 +513,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.mobile-scanner.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.mobile-scanner.RunnerTests"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -513,7 +531,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.mobile-scanner.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.mobile-scanner.RunnerTests"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; @@ -529,7 +547,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.mobile-scanner.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.mobile-scanner.RunnerTests"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 87131a09b..8e3ca5dfe 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ Date: Mon, 29 Apr 2024 11:01:21 +0200 Subject: [PATCH 07/17] remove old permission workaround --- lib/mobile_scanner.dart | 3 +-- lib/src/mobile_scanner_controller.dart | 9 --------- lib/src/mobile_scanner_exception.dart | 7 ------- lib/src/web/mobile_scanner_web.dart | 22 ---------------------- 4 files changed, 1 insertion(+), 40 deletions(-) diff --git a/lib/mobile_scanner.dart b/lib/mobile_scanner.dart index d5bd591ef..347a90993 100644 --- a/lib/mobile_scanner.dart +++ b/lib/mobile_scanner.dart @@ -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'; diff --git a/lib/src/mobile_scanner_controller.dart b/lib/src/mobile_scanner_controller.dart index 321470e43..a364344ce 100644 --- a/lib/src/mobile_scanner_controller.dart +++ b/lib/src/mobile_scanner_controller.dart @@ -242,13 +242,6 @@ class MobileScannerController extends ValueNotifier { ); } - // 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; @@ -301,8 +294,6 @@ class MobileScannerController extends ValueNotifier { zoomScale: 1.0, ); } - } on PermissionRequestPendingException catch (_) { - // If a permission request was already pending, do nothing. } } diff --git a/lib/src/mobile_scanner_exception.dart b/lib/src/mobile_scanner_exception.dart index a35c16720..cca9bafef 100644 --- a/lib/src/mobile_scanner_exception.dart +++ b/lib/src/mobile_scanner_exception.dart @@ -39,10 +39,3 @@ class MobileScannerErrorDetails { /// The error message from the [PlatformException]. 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 {} diff --git a/lib/src/web/mobile_scanner_web.dart b/lib/src/web/mobile_scanner_web.dart index 8e3a811da..ab645ef96 100644 --- a/lib/src/web/mobile_scanner_web.dart +++ b/lib/src/web/mobile_scanner_web.dart @@ -38,12 +38,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, @@ -187,14 +181,9 @@ class MobileScannerWeb extends MobileScannerPlatform { try { // Retrieving the media devices requests the camera permission. - _permissionRequestInProgress = true; - final MediaStream videoStream = await window.navigator.mediaDevices.getUserMedia(constraints).toDart; - // At this point the permission is granted. - _permissionRequestInProgress = false; - return videoStream; } on DOMException catch (error, stackTrace) { final String errorMessage = error.toString(); @@ -209,10 +198,6 @@ class MobileScannerWeb extends MobileScannerPlatform { errorCode = MobileScannerErrorCode.permissionDenied; } - // At this point the permission request completed, although with an error, - // but the error is irrelevant. - _permissionRequestInProgress = false; - throw MobileScannerException( errorCode: errorCode, errorDetails: MobileScannerErrorDetails( @@ -268,13 +253,6 @@ class MobileScannerWeb extends MobileScannerPlatform { @override Future 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(); - } - _barcodeReader = ZXingBarcodeReader(); await _barcodeReader?.maybeLoadLibrary( From c3a0f8665a11695bb20b8611e0904b1dd3d6b41b Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Mon, 29 Apr 2024 11:03:31 +0200 Subject: [PATCH 08/17] report unavailable instead of off when stopping the camera if there was no torch --- lib/src/mobile_scanner_controller.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/src/mobile_scanner_controller.dart b/lib/src/mobile_scanner_controller.dart index a364344ce..3d9f7d7b6 100644 --- a/lib/src/mobile_scanner_controller.dart +++ b/lib/src/mobile_scanner_controller.dart @@ -313,11 +313,16 @@ class MobileScannerController extends ValueNotifier { _disposeListeners(); + final TorchState oldTorchState = value.torchState; + // After the camera stopped, set the torch state to off, // as the torch state callback is never called when the camera is stopped. + // If the device does not have a torch, do not report "off". value = value.copyWith( isRunning: false, - torchState: TorchState.off, + torchState: oldTorchState == TorchState.unavailable + ? TorchState.unavailable + : TorchState.off, ); await MobileScannerPlatform.instance.stop(); From 3c0c45a8f374bd35c709207825e15a6de7498413 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Mon, 29 Apr 2024 11:10:40 +0200 Subject: [PATCH 09/17] update code sample to fix a potential bug; update changelog --- CHANGELOG.md | 4 ++++ README.md | 6 +++++- example/ios/Runner/Info.plist | 8 ++++---- example/lib/barcode_scanner_controller.dart | 4 +++- ios/mobile_scanner.podspec | 2 +- macos/mobile_scanner.podspec | 2 +- 6 files changed, 18 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10f36b77b..ae37a03e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ Bugs fixed: * Fixed a crash when the controller is disposed while it is still starting. [#1036](https://github.com/juliansteenbakker/mobile_scanner/pull/1036) (thanks @EArminjon !) +* Fixed an issue that causes the initial torch state to be out of sync. + +Improvements: +* Updated the lifeycle code sample to handle not-initialized controllers. ## 5.0.1 diff --git a/README.md b/README.md index 64fe7723a..d9ca38d41 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,11 @@ class MyState extends State with WidgetsBindingObserver { @override void didChangeAppLifecycleState(AppLifecycleState state) { - super.didChangeAppLifecycleState(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) { + return; + } switch (state) { case AppLifecycleState.detached: diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index 701f82950..70029ca6b 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -2,6 +2,8 @@ + CADisableMinimumFrameDurationOnPhone + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName @@ -28,6 +30,8 @@ This app needs camera access to scan QR codes NSPhotoLibraryUsageDescription This app needs photos access to get QR code from photo library + UIApplicationSupportsIndirectInputEvents + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -47,9 +51,5 @@ UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - diff --git a/example/lib/barcode_scanner_controller.dart b/example/lib/barcode_scanner_controller.dart index 126297bd0..84df27200 100644 --- a/example/lib/barcode_scanner_controller.dart +++ b/example/lib/barcode_scanner_controller.dart @@ -63,7 +63,9 @@ class _BarcodeScannerWithControllerState @override void didChangeAppLifecycleState(AppLifecycleState state) { - super.didChangeAppLifecycleState(state); + if (!controller.value.isInitialized) { + return; + } switch (state) { case AppLifecycleState.detached: diff --git a/ios/mobile_scanner.podspec b/ios/mobile_scanner.podspec index 055577792..027cd235d 100644 --- a/ios/mobile_scanner.podspec +++ b/ios/mobile_scanner.podspec @@ -4,7 +4,7 @@ # Pod::Spec.new do |s| s.name = 'mobile_scanner' - s.version = '5.0.0' + s.version = '5.0.2' s.summary = 'An universal scanner for Flutter based on MLKit.' s.description = <<-DESC An universal scanner for Flutter based on MLKit. diff --git a/macos/mobile_scanner.podspec b/macos/mobile_scanner.podspec index 5858536e4..f9d01e8df 100644 --- a/macos/mobile_scanner.podspec +++ b/macos/mobile_scanner.podspec @@ -4,7 +4,7 @@ # Pod::Spec.new do |s| s.name = 'mobile_scanner' - s.version = '5.0.0' + s.version = '5.0.2' s.summary = 'An universal scanner for Flutter based on MLKit.' s.description = <<-DESC An universal scanner for Flutter based on MLKit. From 51c786d17797cbf14063f3d99153876e95fc1528 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Tue, 30 Apr 2024 11:38:12 +0200 Subject: [PATCH 10/17] let web throw when start is called once the barcode reader is set --- lib/src/mobile_scanner_controller.dart | 3 +++ lib/src/web/mobile_scanner_web.dart | 12 ++++++++++++ 2 files changed, 15 insertions(+) diff --git a/lib/src/mobile_scanner_controller.dart b/lib/src/mobile_scanner_controller.dart index 3d9f7d7b6..589fd2361 100644 --- a/lib/src/mobile_scanner_controller.dart +++ b/lib/src/mobile_scanner_controller.dart @@ -280,6 +280,9 @@ class MobileScannerController extends ValueNotifier { ); } } on MobileScannerException catch (error) { + // TODO: if the error code is `controllerAlreadyInitialized` ignore the error + // TODO: update the error reporting from the native side to report proper error codes. + // The initialization finished with an error. // To avoid stale values, reset the output size, // torch state and zoom scale to the defaults. diff --git a/lib/src/web/mobile_scanner_web.dart b/lib/src/web/mobile_scanner_web.dart index ab645ef96..976e3800a 100644 --- a/lib/src/web/mobile_scanner_web.dart +++ b/lib/src/web/mobile_scanner_web.dart @@ -253,6 +253,17 @@ class MobileScannerWeb extends MobileScannerPlatform { @override Future start(StartOptions startOptions) async { + // TODO: ignore double starts in the controller. + if (_barcodeReader != null) { + throw const MobileScannerException( + errorCode: MobileScannerErrorCode.controllerAlreadyInitialized, + errorDetails: MobileScannerErrorDetails( + message: + 'The scanner was already started. Call stop() before calling start() again.', + ), + ); + } + _barcodeReader = ZXingBarcodeReader(); await _barcodeReader?.maybeLoadLibrary( @@ -260,6 +271,7 @@ class MobileScannerWeb extends MobileScannerPlatform { ); if (_barcodeReader?.isScanning ?? false) { + // TODO: ignore double starts in the controller. throw const MobileScannerException( errorCode: MobileScannerErrorCode.controllerAlreadyInitialized, errorDetails: MobileScannerErrorDetails( From 8e1fdbd9ed2b81f8fdad9c0176bea3de23fa64ae Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Tue, 30 Apr 2024 11:51:53 +0200 Subject: [PATCH 11/17] remove `setTorchState()` from the platform interface --- .../method_channel/mobile_scanner_method_channel.dart | 9 +-------- lib/src/mobile_scanner_controller.dart | 7 ++----- lib/src/mobile_scanner_platform_interface.dart | 5 ----- lib/src/web/mobile_scanner_web.dart | 8 -------- 4 files changed, 3 insertions(+), 26 deletions(-) diff --git a/lib/src/method_channel/mobile_scanner_method_channel.dart b/lib/src/method_channel/mobile_scanner_method_channel.dart index d9661159e..eeb80d6be 100644 --- a/lib/src/method_channel/mobile_scanner_method_channel.dart +++ b/lib/src/method_channel/mobile_scanner_method_channel.dart @@ -177,14 +177,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { await methodChannel.invokeMethod('resetScale'); } - @override - Future setTorchState(TorchState torchState) async { - if (torchState == TorchState.unavailable) { - return; - } - - await methodChannel.invokeMethod('torch', torchState.rawValue); - } + // TODO: remove the 'torch' function from native @override Future setZoomScale(double zoomScale) async { diff --git a/lib/src/mobile_scanner_controller.dart b/lib/src/mobile_scanner_controller.dart index 589fd2361..4696cffa7 100644 --- a/lib/src/mobile_scanner_controller.dart +++ b/lib/src/mobile_scanner_controller.dart @@ -374,13 +374,10 @@ class MobileScannerController extends ValueNotifier { return; } - final TorchState newState = - torchState == TorchState.off ? TorchState.on : TorchState.off; - - // Update the torch state to the new state. + // Request the torch state to be switched to the opposite state. // When the platform has updated the torch state, // it will send an update through the torch state event stream. - await MobileScannerPlatform.instance.setTorchState(newState); + await MobileScannerPlatform.instance.toggleTorch(); } /// Update the scan window with the given [window] rectangle. diff --git a/lib/src/mobile_scanner_platform_interface.dart b/lib/src/mobile_scanner_platform_interface.dart index 958049c2d..798b6d535 100644 --- a/lib/src/mobile_scanner_platform_interface.dart +++ b/lib/src/mobile_scanner_platform_interface.dart @@ -67,11 +67,6 @@ abstract class MobileScannerPlatform extends PlatformInterface { /// This is only supported on the web. void setBarcodeLibraryScriptUrl(String scriptUrl) {} - /// Set the torch state of the active camera. - Future setTorchState(TorchState torchState) { - throw UnimplementedError('setTorchState() has not been implemented.'); - } - /// Set the zoom scale of the camera. /// /// The [zoomScale] must be between `0.0` and `1.0` (both inclusive). diff --git a/lib/src/web/mobile_scanner_web.dart b/lib/src/web/mobile_scanner_web.dart index 976e3800a..e07cc7d4e 100644 --- a/lib/src/web/mobile_scanner_web.dart +++ b/lib/src/web/mobile_scanner_web.dart @@ -235,14 +235,6 @@ class MobileScannerWeb extends MobileScannerPlatform { _alternateScriptUrl ??= scriptUrl; } - @override - Future setTorchState(TorchState torchState) { - throw UnsupportedError( - 'Setting the torch state is not supported for video tracks on the web.\n' - 'See https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints#instance_properties_of_video_tracks', - ); - } - @override Future setZoomScale(double zoomScale) { throw UnsupportedError( From f2152a31d584125338412628901ad87878a246b3 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Tue, 30 Apr 2024 11:59:56 +0200 Subject: [PATCH 12/17] add new toggleTorch() method to platform interface --- lib/src/method_channel/mobile_scanner_method_channel.dart | 7 +++++-- lib/src/mobile_scanner_platform_interface.dart | 5 +++++ lib/src/web/mobile_scanner_web.dart | 8 ++++++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/lib/src/method_channel/mobile_scanner_method_channel.dart b/lib/src/method_channel/mobile_scanner_method_channel.dart index eeb80d6be..1cc937cfd 100644 --- a/lib/src/method_channel/mobile_scanner_method_channel.dart +++ b/lib/src/method_channel/mobile_scanner_method_channel.dart @@ -177,8 +177,6 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { await methodChannel.invokeMethod('resetScale'); } - // TODO: remove the 'torch' function from native - @override Future setZoomScale(double zoomScale) async { await methodChannel.invokeMethod('setScale', zoomScale); @@ -274,6 +272,11 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { await methodChannel.invokeMethod('stop'); } + @override + Future toggleTorch() async { + await methodChannel.invokeMethod('toggleTorch'); + } + @override Future updateScanWindow(Rect? window) async { if (_textureId == null) { diff --git a/lib/src/mobile_scanner_platform_interface.dart b/lib/src/mobile_scanner_platform_interface.dart index 798b6d535..553602160 100644 --- a/lib/src/mobile_scanner_platform_interface.dart +++ b/lib/src/mobile_scanner_platform_interface.dart @@ -90,6 +90,11 @@ abstract class MobileScannerPlatform extends PlatformInterface { throw UnimplementedError('stop() has not been implemented.'); } + /// Toggle the torch on the active camera on or off. + Future toggleTorch() { + throw UnimplementedError('toggleTorch() has not been implemented.'); + } + /// Update the scan window to the given [window] rectangle. /// /// Any barcodes that do not intersect with the given [window] will be ignored. diff --git a/lib/src/web/mobile_scanner_web.dart b/lib/src/web/mobile_scanner_web.dart index e07cc7d4e..baf167688 100644 --- a/lib/src/web/mobile_scanner_web.dart +++ b/lib/src/web/mobile_scanner_web.dart @@ -354,6 +354,14 @@ class MobileScannerWeb extends MobileScannerPlatform { _barcodeReader = null; } + @override + Future toggleTorch() { + throw UnsupportedError( + 'Setting the torch state is not supported for video tracks on the web.\n' + 'See https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints#instance_properties_of_video_tracks', + ); + } + @override Future updateScanWindow(Rect? window) { // A scan window is not supported on the web, From 2e0160cb07f4fc26454d61de180baa6426acff00 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Tue, 30 Apr 2024 12:16:27 +0200 Subject: [PATCH 13/17] reimplement toggle torch on Android --- .../steenbakker/mobile_scanner/MobileScanner.kt | 16 ++++++++++------ .../mobile_scanner/MobileScannerHandler.kt | 8 ++++---- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt index 07ab32994..fa6f20c50 100644 --- a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt +++ b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt @@ -19,6 +19,7 @@ import androidx.camera.core.ExperimentalGetImage import androidx.camera.core.ImageAnalysis import androidx.camera.core.ImageProxy import androidx.camera.core.Preview +import androidx.camera.core.TorchState import androidx.camera.core.resolutionselector.AspectRatioStrategy import androidx.camera.core.resolutionselector.ResolutionSelector import androidx.camera.core.resolutionselector.ResolutionStrategy @@ -422,13 +423,16 @@ class MobileScanner( /** * Toggles the flash light on or off. */ - fun toggleTorch(enableTorch: Boolean) { - if (camera == null) { - return - } + fun toggleTorch() { + camera?.let { + if (!it.cameraInfo.hasFlashUnit()) { + return@let + } - if (camera?.cameraInfo?.hasFlashUnit() == true) { - camera?.cameraControl?.enableTorch(enableTorch) + when(it.cameraInfo.torchState.value) { + TorchState.OFF -> it.cameraControl.enableTorch(true) + TorchState.ON -> it.cameraControl.enableTorch(false) + } } } diff --git a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerHandler.kt b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerHandler.kt index af44ff1f6..3a23119b7 100644 --- a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerHandler.kt +++ b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerHandler.kt @@ -122,8 +122,8 @@ class MobileScannerHandler( } }) "start" -> start(call, result) - "torch" -> toggleTorch(call, result) "stop" -> stop(result) + "toggleTorch" -> toggleTorch(result) "analyzeImage" -> analyzeImage(call, result) "setScale" -> setScale(call, result) "resetScale" -> resetScale(result) @@ -168,7 +168,7 @@ class MobileScannerHandler( val position = if (facing == 0) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA - val detectionSpeed: DetectionSpeed = DetectionSpeed.values().first { it.intValue == speed} + val detectionSpeed: DetectionSpeed = DetectionSpeed.entries.first { it.intValue == speed} mobileScanner!!.start( barcodeScannerOptions, @@ -244,8 +244,8 @@ class MobileScannerHandler( mobileScanner!!.analyzeImage(uri, analyzeImageSuccessCallback, analyzeImageErrorCallback) } - private fun toggleTorch(call: MethodCall, result: MethodChannel.Result) { - mobileScanner!!.toggleTorch(call.arguments == 1) + private fun toggleTorch(result: MethodChannel.Result) { + mobileScanner?.toggleTorch() result.success(null) } From 986f130e083f570946d6b96a8e0a246e26e7b7c5 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Tue, 30 Apr 2024 13:11:49 +0200 Subject: [PATCH 14/17] reimplement toggle torch on MacOS --- macos/Classes/MobileScannerPlugin.swift | 76 ++++++++++++++++++------- 1 file changed, 55 insertions(+), 21 deletions(-) diff --git a/macos/Classes/MobileScannerPlugin.swift b/macos/Classes/MobileScannerPlugin.swift index 6e4dee7ab..9ba4f606f 100644 --- a/macos/Classes/MobileScannerPlugin.swift +++ b/macos/Classes/MobileScannerPlugin.swift @@ -59,8 +59,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, requestPermission(call, result) case "start": start(call, result) - case "torch": - toggleTorch(call, result) + case "toggleTorch": + toggleTorch(result) case "setScale": setScale(call, result) case "resetScale": @@ -288,12 +288,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, // Turn on the torch if requested. if (torch) { - do { - try self.toggleTorchInternal(.on) - } catch { - // If the torch could not be turned on, - // continue the capture session. - } + self.turnTorchOn() } device.addObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode), options: .new, context: nil) @@ -336,12 +331,12 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, } // TODO: this method should be removed when iOS and MacOS share their implementation. - private func toggleTorchInternal(_ torch: AVCaptureDevice.TorchMode) throws { + private func toggleTorchInternal() { guard let device = self.device else { return } - if (!device.hasTorch || !device.isTorchModeSupported(torch)) { + if (!device.hasTorch) { return } @@ -350,12 +345,57 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, return } } + + var newTorchMode: AVCaptureDevice.TorchMode = device.torchMode + + switch(device.torchMode) { + case AVCaptureDevice.TorchMode.auto: + if #available(macOS 10.15, *) { + newTorchMode = device.isTorchActive ? AVCaptureDevice.TorchMode.off : AVCaptureDevice.TorchMode.on + } + break; + case AVCaptureDevice.TorchMode.off: + newTorchMode = AVCaptureDevice.TorchMode.on + break; + case AVCaptureDevice.TorchMode.on: + newTorchMode = AVCaptureDevice.TorchMode.off + break; + default: + return; + } + + if (!device.isTorchModeSupported(newTorchMode) || device.torchMode == newTorchMode) { + return; + } - if (device.torchMode != torch) { + do { try device.lockForConfiguration() - device.torchMode = torch + device.torchMode = newTorchMode device.unlockForConfiguration() + } catch(_) {} + } + + /// Turn the torch on. + private func turnTorchOn() { + guard let device = self.device else { + return } + + if (!device.hasTorch || !device.isTorchModeSupported(.on) || device.torchMode == .on) { + return + } + + if #available(macOS 15.0, *) { + if(!device.isTorchAvailable) { + return + } + } + + do { + try device.lockForConfiguration() + device.torchMode = .on + device.unlockForConfiguration() + } catch(_) {} } /// Reset the zoom scale. @@ -370,15 +410,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, result(nil) } - private func toggleTorch(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { - let requestedTorchMode: AVCaptureDevice.TorchMode = call.arguments as! Int == 1 ? .on : .off - - do { - try self.toggleTorchInternal(requestedTorchMode) - result(nil) - } catch { - result(FlutterError(code: "MobileScanner", message: error.localizedDescription, details: nil)) - } + private func toggleTorch(_ result: @escaping FlutterResult) { + self.toggleTorchInternal() + result(nil) } // func switchAnalyzeMode(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { From c5449517a88e63eeb57bc4f0fe08115c2c202048 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Tue, 30 Apr 2024 13:22:07 +0200 Subject: [PATCH 15/17] reimplement toggle torch on iOS --- ios/Classes/MobileScanner.swift | 54 +++++++++++++++++++++------ ios/Classes/MobileScannerPlugin.swift | 14 +++---- 2 files changed, 48 insertions(+), 20 deletions(-) diff --git a/ios/Classes/MobileScanner.swift b/ios/Classes/MobileScanner.swift index 5f0b07858..e9c066f25 100644 --- a/ios/Classes/MobileScanner.swift +++ b/ios/Classes/MobileScanner.swift @@ -259,12 +259,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega // as they interact with the hardware camera. if (torch) { DispatchQueue.main.async { - do { - try self.toggleTorch(.on) - } catch { - // If the torch does not turn on, - // continue with the capture session anyway. - } + self.turnTorchOn() } } @@ -323,23 +318,60 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega device = nil } - /// Set the torch mode. + /// Toggle the torch. /// /// This method should be called on the main DispatchQueue. - func toggleTorch(_ torch: AVCaptureDevice.TorchMode) throws { + func toggleTorch() { guard let device = self.device else { return } - if (!device.hasTorch || !device.isTorchAvailable || !device.isTorchModeSupported(torch)) { + if (!device.hasTorch || !device.isTorchAvailable) { return } - if (device.torchMode != torch) { + var newTorchMode: AVCaptureDevice.TorchMode = device.torchMode + + switch(device.torchMode) { + case AVCaptureDevice.TorchMode.auto: + newTorchMode = device.isTorchActive ? AVCaptureDevice.TorchMode.off : AVCaptureDevice.TorchMode.on + break; + case AVCaptureDevice.TorchMode.off: + newTorchMode = AVCaptureDevice.TorchMode.on + break; + case AVCaptureDevice.TorchMode.on: + newTorchMode = AVCaptureDevice.TorchMode.off + break; + default: + return; + } + + if (!device.isTorchModeSupported(newTorchMode) || device.torchMode == newTorchMode) { + return; + } + + do { try device.lockForConfiguration() - device.torchMode = torch + device.torchMode = newTorchMode device.unlockForConfiguration() + } catch(_) {} + } + + /// Turn the torch on. + private func turnTorchOn() { + guard let device = self.device else { + return + } + + if (!device.hasTorch || !device.isTorchAvailable || !device.isTorchModeSupported(.on) || device.torchMode == .on) { + return } + + do { + try device.lockForConfiguration() + device.torchMode = .on + device.unlockForConfiguration() + } catch(_) {} } // Observer for torch state diff --git a/ios/Classes/MobileScannerPlugin.swift b/ios/Classes/MobileScannerPlugin.swift index 929bab9fd..eba359a36 100644 --- a/ios/Classes/MobileScannerPlugin.swift +++ b/ios/Classes/MobileScannerPlugin.swift @@ -80,8 +80,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { start(call, result) case "stop": stop(result) - case "torch": - toggleTorch(call, result) + case "toggleTorch": + toggleTorch(result) case "analyzeImage": analyzeImage(call, result) case "setScale": @@ -157,13 +157,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { } /// Toggles the torch. - private func toggleTorch(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { - do { - try mobileScanner.toggleTorch(call.arguments as? Int == 1 ? .on : .off) - result(nil) - } catch { - result(FlutterError(code: "MobileScanner", message: error.localizedDescription, details: nil)) - } + private func toggleTorch(_ result: @escaping FlutterResult) { + mobileScanner.toggleTorch() + result(nil) } /// Sets the zoomScale. From f57d19dbd0da356c47381dea5e89d59b5153bf70 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Tue, 30 Apr 2024 13:32:32 +0200 Subject: [PATCH 16/17] fix documentation for auto torch mode --- example/lib/scanner_button_widgets.dart | 2 -- lib/src/enums/torch_state.dart | 2 +- lib/src/mobile_scanner_controller.dart | 3 +++ 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/example/lib/scanner_button_widgets.dart b/example/lib/scanner_button_widgets.dart index 8e05b238a..b9297885d 100644 --- a/example/lib/scanner_button_widgets.dart +++ b/example/lib/scanner_button_widgets.dart @@ -144,8 +144,6 @@ class ToggleFlashlightButton extends StatelessWidget { iconSize: 32.0, icon: const Icon(Icons.flash_auto), onPressed: () async { - // This button only turns off the auto mode. - // Perhaps we can switch between on / off / auto? await controller.toggleTorch(); }, ); diff --git a/lib/src/enums/torch_state.dart b/lib/src/enums/torch_state.dart index 31d91d8be..5e45078f4 100644 --- a/lib/src/enums/torch_state.dart +++ b/lib/src/enums/torch_state.dart @@ -2,7 +2,7 @@ enum TorchState { /// The flashlight turns on automatically in low light conditions. /// - /// This is currently only supported on iOS. + /// This is currently only supported on iOS and MacOS. auto(2), /// The flashlight is off. diff --git a/lib/src/mobile_scanner_controller.dart b/lib/src/mobile_scanner_controller.dart index 4696cffa7..91ec82416 100644 --- a/lib/src/mobile_scanner_controller.dart +++ b/lib/src/mobile_scanner_controller.dart @@ -361,6 +361,9 @@ class MobileScannerController extends ValueNotifier { /// /// Does nothing if the device has no torch, /// or if the camera is not running. + /// + /// If the current torch state is [TorchState.auto], + /// the torch is turned on or off depending on its actual current state. Future toggleTorch() async { _throwIfNotInitialized(); From abaeadb19b59350674b94af880d32b329a00b2b8 Mon Sep 17 00:00:00 2001 From: Navaron Bracke Date: Tue, 30 Apr 2024 14:28:50 +0200 Subject: [PATCH 17/17] put back web only workaround for now --- lib/mobile_scanner.dart | 3 ++- lib/src/mobile_scanner_controller.dart | 12 +++++++++--- lib/src/mobile_scanner_exception.dart | 7 +++++++ lib/src/web/mobile_scanner_web.dart | 26 ++++++++++++++++---------- 4 files changed, 34 insertions(+), 14 deletions(-) diff --git a/lib/mobile_scanner.dart b/lib/mobile_scanner.dart index 347a90993..d5bd591ef 100644 --- a/lib/mobile_scanner.dart +++ b/lib/mobile_scanner.dart @@ -11,7 +11,8 @@ 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'; +export 'src/mobile_scanner_exception.dart' + hide PermissionRequestPendingException; export 'src/mobile_scanner_platform_interface.dart'; export 'src/objects/address.dart'; export 'src/objects/barcode.dart'; diff --git a/lib/src/mobile_scanner_controller.dart b/lib/src/mobile_scanner_controller.dart index 91ec82416..e29ff300b 100644 --- a/lib/src/mobile_scanner_controller.dart +++ b/lib/src/mobile_scanner_controller.dart @@ -242,6 +242,13 @@ class MobileScannerController extends ValueNotifier { ); } + // 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; @@ -280,9 +287,6 @@ class MobileScannerController extends ValueNotifier { ); } } on MobileScannerException catch (error) { - // TODO: if the error code is `controllerAlreadyInitialized` ignore the error - // TODO: update the error reporting from the native side to report proper error codes. - // The initialization finished with an error. // To avoid stale values, reset the output size, // torch state and zoom scale to the defaults. @@ -297,6 +301,8 @@ class MobileScannerController extends ValueNotifier { zoomScale: 1.0, ); } + } on PermissionRequestPendingException catch (_) { + // If a permission request was already pending, do nothing. } } diff --git a/lib/src/mobile_scanner_exception.dart b/lib/src/mobile_scanner_exception.dart index cca9bafef..a35c16720 100644 --- a/lib/src/mobile_scanner_exception.dart +++ b/lib/src/mobile_scanner_exception.dart @@ -39,3 +39,10 @@ class MobileScannerErrorDetails { /// The error message from the [PlatformException]. 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 {} diff --git a/lib/src/web/mobile_scanner_web.dart b/lib/src/web/mobile_scanner_web.dart index baf167688..e3ada8c8d 100644 --- a/lib/src/web/mobile_scanner_web.dart +++ b/lib/src/web/mobile_scanner_web.dart @@ -38,6 +38,12 @@ 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, @@ -180,12 +186,17 @@ 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; @@ -245,15 +256,11 @@ class MobileScannerWeb extends MobileScannerPlatform { @override Future start(StartOptions startOptions) async { - // TODO: ignore double starts in the controller. - if (_barcodeReader != null) { - throw const MobileScannerException( - errorCode: MobileScannerErrorCode.controllerAlreadyInitialized, - errorDetails: MobileScannerErrorDetails( - message: - 'The scanner was already started. Call stop() before calling start() again.', - ), - ); + // 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(); } _barcodeReader = ZXingBarcodeReader(); @@ -263,7 +270,6 @@ class MobileScannerWeb extends MobileScannerPlatform { ); if (_barcodeReader?.isScanning ?? false) { - // TODO: ignore double starts in the controller. throw const MobileScannerException( errorCode: MobileScannerErrorCode.controllerAlreadyInitialized, errorDetails: MobileScannerErrorDetails(