Navaron Bracke
Committed by GitHub

Merge pull request #1048 from navaronbracke/fix_ios_torch_mode

fix: provide correct initial torch state
@@ -2,6 +2,10 @@ @@ -2,6 +2,10 @@
2 2
3 Bugs fixed: 3 Bugs fixed:
4 * Fixed a crash when the controller is disposed while it is still starting. [#1036](https://github.com/juliansteenbakker/mobile_scanner/pull/1036) (thanks @EArminjon !) 4 * Fixed a crash when the controller is disposed while it is still starting. [#1036](https://github.com/juliansteenbakker/mobile_scanner/pull/1036) (thanks @EArminjon !)
  5 +* Fixed an issue that causes the initial torch state to be out of sync.
  6 +
  7 +Improvements:
  8 +* Updated the lifeycle code sample to handle not-initialized controllers.
5 9
6 ## 5.0.1 10 ## 5.0.1
7 11
@@ -103,7 +103,11 @@ class MyState extends State<MyStatefulWidget> with WidgetsBindingObserver { @@ -103,7 +103,11 @@ class MyState extends State<MyStatefulWidget> with WidgetsBindingObserver {
103 103
104 @override 104 @override
105 void didChangeAppLifecycleState(AppLifecycleState state) { 105 void didChangeAppLifecycleState(AppLifecycleState state) {
106 - super.didChangeAppLifecycleState(state); 106 + // If the controller is not ready, do not try to start or stop it.
  107 + // Permission dialogs can trigger lifecycle changes before the controller is ready.
  108 + if (!controller.value.isInitialized) {
  109 + return;
  110 + }
107 111
108 switch (state) { 112 switch (state) {
109 case AppLifecycleState.detached: 113 case AppLifecycleState.detached:
@@ -19,6 +19,7 @@ import androidx.camera.core.ExperimentalGetImage @@ -19,6 +19,7 @@ import androidx.camera.core.ExperimentalGetImage
19 import androidx.camera.core.ImageAnalysis 19 import androidx.camera.core.ImageAnalysis
20 import androidx.camera.core.ImageProxy 20 import androidx.camera.core.ImageProxy
21 import androidx.camera.core.Preview 21 import androidx.camera.core.Preview
  22 +import androidx.camera.core.TorchState
22 import androidx.camera.core.resolutionselector.AspectRatioStrategy 23 import androidx.camera.core.resolutionselector.AspectRatioStrategy
23 import androidx.camera.core.resolutionselector.ResolutionSelector 24 import androidx.camera.core.resolutionselector.ResolutionSelector
24 import androidx.camera.core.resolutionselector.ResolutionStrategy 25 import androidx.camera.core.resolutionselector.ResolutionStrategy
@@ -368,11 +369,22 @@ class MobileScanner( @@ -368,11 +369,22 @@ class MobileScanner(
368 val height = resolution.height.toDouble() 369 val height = resolution.height.toDouble()
369 val portrait = (camera?.cameraInfo?.sensorRotationDegrees ?: 0) % 180 == 0 370 val portrait = (camera?.cameraInfo?.sensorRotationDegrees ?: 0) % 180 == 0
370 371
  372 + // Start with 'unavailable' torch state.
  373 + var currentTorchState: Int = -1
  374 +
  375 + camera?.cameraInfo?.let {
  376 + if (!it.hasFlashUnit()) {
  377 + return@let
  378 + }
  379 +
  380 + currentTorchState = it.torchState.value ?: -1
  381 + }
  382 +
371 mobileScannerStartedCallback( 383 mobileScannerStartedCallback(
372 MobileScannerStartParameters( 384 MobileScannerStartParameters(
373 if (portrait) width else height, 385 if (portrait) width else height,
374 if (portrait) height else width, 386 if (portrait) height else width,
375 - camera?.cameraInfo?.hasFlashUnit() ?: false, 387 + currentTorchState,
376 textureEntry!!.id(), 388 textureEntry!!.id(),
377 numberOfCameras ?: 0 389 numberOfCameras ?: 0
378 ) 390 )
@@ -411,13 +423,16 @@ class MobileScanner( @@ -411,13 +423,16 @@ class MobileScanner(
411 /** 423 /**
412 * Toggles the flash light on or off. 424 * Toggles the flash light on or off.
413 */ 425 */
414 - fun toggleTorch(enableTorch: Boolean) {  
415 - if (camera == null) {  
416 - return  
417 - } 426 + fun toggleTorch() {
  427 + camera?.let {
  428 + if (!it.cameraInfo.hasFlashUnit()) {
  429 + return@let
  430 + }
418 431
419 - if (camera?.cameraInfo?.hasFlashUnit() == true) {  
420 - camera?.cameraControl?.enableTorch(enableTorch) 432 + when(it.cameraInfo.torchState.value) {
  433 + TorchState.OFF -> it.cameraControl.enableTorch(true)
  434 + TorchState.ON -> it.cameraControl.enableTorch(false)
  435 + }
421 } 436 }
422 } 437 }
423 438
@@ -74,6 +74,7 @@ class MobileScannerHandler( @@ -74,6 +74,7 @@ class MobileScannerHandler(
74 private var mobileScanner: MobileScanner? = null 74 private var mobileScanner: MobileScanner? = null
75 75
76 private val torchStateCallback: TorchStateCallback = {state: Int -> 76 private val torchStateCallback: TorchStateCallback = {state: Int ->
  77 + // Off = 0, On = 1
77 barcodeHandler.publishEvent(mapOf("name" to "torchState", "data" to state)) 78 barcodeHandler.publishEvent(mapOf("name" to "torchState", "data" to state))
78 } 79 }
79 80
@@ -121,8 +122,8 @@ class MobileScannerHandler( @@ -121,8 +122,8 @@ class MobileScannerHandler(
121 } 122 }
122 }) 123 })
123 "start" -> start(call, result) 124 "start" -> start(call, result)
124 - "torch" -> toggleTorch(call, result)  
125 "stop" -> stop(result) 125 "stop" -> stop(result)
  126 + "toggleTorch" -> toggleTorch(result)
126 "analyzeImage" -> analyzeImage(call, result) 127 "analyzeImage" -> analyzeImage(call, result)
127 "setScale" -> setScale(call, result) 128 "setScale" -> setScale(call, result)
128 "resetScale" -> resetScale(result) 129 "resetScale" -> resetScale(result)
@@ -167,7 +168,7 @@ class MobileScannerHandler( @@ -167,7 +168,7 @@ class MobileScannerHandler(
167 val position = 168 val position =
168 if (facing == 0) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA 169 if (facing == 0) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA
169 170
170 - val detectionSpeed: DetectionSpeed = DetectionSpeed.values().first { it.intValue == speed} 171 + val detectionSpeed: DetectionSpeed = DetectionSpeed.entries.first { it.intValue == speed}
171 172
172 mobileScanner!!.start( 173 mobileScanner!!.start(
173 barcodeScannerOptions, 174 barcodeScannerOptions,
@@ -182,7 +183,7 @@ class MobileScannerHandler( @@ -182,7 +183,7 @@ class MobileScannerHandler(
182 result.success(mapOf( 183 result.success(mapOf(
183 "textureId" to it.id, 184 "textureId" to it.id,
184 "size" to mapOf("width" to it.width, "height" to it.height), 185 "size" to mapOf("width" to it.width, "height" to it.height),
185 - "torchable" to it.hasFlashUnit, 186 + "currentTorchState" to it.currentTorchState,
186 "numberOfCameras" to it.numberOfCameras 187 "numberOfCameras" to it.numberOfCameras
187 )) 188 ))
188 } 189 }
@@ -243,8 +244,8 @@ class MobileScannerHandler( @@ -243,8 +244,8 @@ class MobileScannerHandler(
243 mobileScanner!!.analyzeImage(uri, analyzeImageSuccessCallback, analyzeImageErrorCallback) 244 mobileScanner!!.analyzeImage(uri, analyzeImageSuccessCallback, analyzeImageErrorCallback)
244 } 245 }
245 246
246 - private fun toggleTorch(call: MethodCall, result: MethodChannel.Result) {  
247 - mobileScanner!!.toggleTorch(call.arguments == 1) 247 + private fun toggleTorch(result: MethodChannel.Result) {
  248 + mobileScanner?.toggleTorch()
248 result.success(null) 249 result.success(null)
249 } 250 }
250 251
@@ -3,7 +3,7 @@ package dev.steenbakker.mobile_scanner.objects @@ -3,7 +3,7 @@ package dev.steenbakker.mobile_scanner.objects
3 class MobileScannerStartParameters( 3 class MobileScannerStartParameters(
4 val width: Double = 0.0, 4 val width: Double = 0.0,
5 val height: Double, 5 val height: Double,
6 - val hasFlashUnit: Boolean, 6 + val currentTorchState: Int,
7 val id: Long, 7 val id: Long,
8 val numberOfCameras: Int 8 val numberOfCameras: Int
9 ) 9 )
@@ -198,6 +198,7 @@ @@ -198,6 +198,7 @@
198 9705A1C41CF9048500538489 /* Embed Frameworks */, 198 9705A1C41CF9048500538489 /* Embed Frameworks */,
199 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 199 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
200 3DBCC0215D7BED1D9A756EA3 /* [CP] Embed Pods Frameworks */, 200 3DBCC0215D7BED1D9A756EA3 /* [CP] Embed Pods Frameworks */,
  201 + BB0C8EA8DA81A75DE53F052F /* [CP] Copy Pods Resources */,
201 ); 202 );
202 buildRules = ( 203 buildRules = (
203 ); 204 );
@@ -215,7 +216,7 @@ @@ -215,7 +216,7 @@
215 isa = PBXProject; 216 isa = PBXProject;
216 attributes = { 217 attributes = {
217 BuildIndependentTargetsInParallel = YES; 218 BuildIndependentTargetsInParallel = YES;
218 - LastUpgradeCheck = 1430; 219 + LastUpgradeCheck = 1510;
219 ORGANIZATIONNAME = ""; 220 ORGANIZATIONNAME = "";
220 TargetAttributes = { 221 TargetAttributes = {
221 331C8080294A63A400263BE5 = { 222 331C8080294A63A400263BE5 = {
@@ -317,6 +318,23 @@ @@ -317,6 +318,23 @@
317 shellPath = /bin/sh; 318 shellPath = /bin/sh;
318 shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 319 shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
319 }; 320 };
  321 + BB0C8EA8DA81A75DE53F052F /* [CP] Copy Pods Resources */ = {
  322 + isa = PBXShellScriptBuildPhase;
  323 + buildActionMask = 2147483647;
  324 + files = (
  325 + );
  326 + inputFileListPaths = (
  327 + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
  328 + );
  329 + name = "[CP] Copy Pods Resources";
  330 + outputFileListPaths = (
  331 + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
  332 + );
  333 + runOnlyForDeploymentPostprocessing = 0;
  334 + shellPath = /bin/sh;
  335 + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
  336 + showEnvVarsInLog = 0;
  337 + };
320 C7DE006A696F551C4E067E41 /* [CP] Check Pods Manifest.lock */ = { 338 C7DE006A696F551C4E067E41 /* [CP] Check Pods Manifest.lock */ = {
321 isa = PBXShellScriptBuildPhase; 339 isa = PBXShellScriptBuildPhase;
322 buildActionMask = 2147483647; 340 buildActionMask = 2147483647;
@@ -495,7 +513,7 @@ @@ -495,7 +513,7 @@
495 CURRENT_PROJECT_VERSION = 1; 513 CURRENT_PROJECT_VERSION = 1;
496 GENERATE_INFOPLIST_FILE = YES; 514 GENERATE_INFOPLIST_FILE = YES;
497 MARKETING_VERSION = 1.0; 515 MARKETING_VERSION = 1.0;
498 - PRODUCT_BUNDLE_IDENTIFIER = com.example.mobile-scanner.RunnerTests; 516 + PRODUCT_BUNDLE_IDENTIFIER = "com.example.mobile-scanner.RunnerTests";
499 PRODUCT_NAME = "$(TARGET_NAME)"; 517 PRODUCT_NAME = "$(TARGET_NAME)";
500 SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 518 SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
501 SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 519 SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -513,7 +531,7 @@ @@ -513,7 +531,7 @@
513 CURRENT_PROJECT_VERSION = 1; 531 CURRENT_PROJECT_VERSION = 1;
514 GENERATE_INFOPLIST_FILE = YES; 532 GENERATE_INFOPLIST_FILE = YES;
515 MARKETING_VERSION = 1.0; 533 MARKETING_VERSION = 1.0;
516 - PRODUCT_BUNDLE_IDENTIFIER = com.example.mobile-scanner.RunnerTests; 534 + PRODUCT_BUNDLE_IDENTIFIER = "com.example.mobile-scanner.RunnerTests";
517 PRODUCT_NAME = "$(TARGET_NAME)"; 535 PRODUCT_NAME = "$(TARGET_NAME)";
518 SWIFT_VERSION = 5.0; 536 SWIFT_VERSION = 5.0;
519 TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; 537 TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@@ -529,7 +547,7 @@ @@ -529,7 +547,7 @@
529 CURRENT_PROJECT_VERSION = 1; 547 CURRENT_PROJECT_VERSION = 1;
530 GENERATE_INFOPLIST_FILE = YES; 548 GENERATE_INFOPLIST_FILE = YES;
531 MARKETING_VERSION = 1.0; 549 MARKETING_VERSION = 1.0;
532 - PRODUCT_BUNDLE_IDENTIFIER = com.example.mobile-scanner.RunnerTests; 550 + PRODUCT_BUNDLE_IDENTIFIER = "com.example.mobile-scanner.RunnerTests";
533 PRODUCT_NAME = "$(TARGET_NAME)"; 551 PRODUCT_NAME = "$(TARGET_NAME)";
534 SWIFT_VERSION = 5.0; 552 SWIFT_VERSION = 5.0;
535 TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; 553 TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
1 <?xml version="1.0" encoding="UTF-8"?> 1 <?xml version="1.0" encoding="UTF-8"?>
2 <Scheme 2 <Scheme
3 - LastUpgradeVersion = "1430" 3 + LastUpgradeVersion = "1510"
4 version = "1.3"> 4 version = "1.3">
5 <BuildAction 5 <BuildAction
6 parallelizeBuildables = "YES" 6 parallelizeBuildables = "YES"
@@ -2,6 +2,8 @@ @@ -2,6 +2,8 @@
2 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 2 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3 <plist version="1.0"> 3 <plist version="1.0">
4 <dict> 4 <dict>
  5 + <key>CADisableMinimumFrameDurationOnPhone</key>
  6 + <true/>
5 <key>CFBundleDevelopmentRegion</key> 7 <key>CFBundleDevelopmentRegion</key>
6 <string>$(DEVELOPMENT_LANGUAGE)</string> 8 <string>$(DEVELOPMENT_LANGUAGE)</string>
7 <key>CFBundleDisplayName</key> 9 <key>CFBundleDisplayName</key>
@@ -28,6 +30,8 @@ @@ -28,6 +30,8 @@
28 <string>This app needs camera access to scan QR codes</string> 30 <string>This app needs camera access to scan QR codes</string>
29 <key>NSPhotoLibraryUsageDescription</key> 31 <key>NSPhotoLibraryUsageDescription</key>
30 <string>This app needs photos access to get QR code from photo library</string> 32 <string>This app needs photos access to get QR code from photo library</string>
  33 + <key>UIApplicationSupportsIndirectInputEvents</key>
  34 + <true/>
31 <key>UILaunchStoryboardName</key> 35 <key>UILaunchStoryboardName</key>
32 <string>LaunchScreen</string> 36 <string>LaunchScreen</string>
33 <key>UIMainStoryboardFile</key> 37 <key>UIMainStoryboardFile</key>
@@ -47,9 +51,5 @@ @@ -47,9 +51,5 @@
47 </array> 51 </array>
48 <key>UIViewControllerBasedStatusBarAppearance</key> 52 <key>UIViewControllerBasedStatusBarAppearance</key>
49 <false/> 53 <false/>
50 - <key>CADisableMinimumFrameDurationOnPhone</key>  
51 - <true/>  
52 - <key>UIApplicationSupportsIndirectInputEvents</key>  
53 - <true/>  
54 </dict> 54 </dict>
55 </plist> 55 </plist>
@@ -63,7 +63,9 @@ class _BarcodeScannerWithControllerState @@ -63,7 +63,9 @@ class _BarcodeScannerWithControllerState
63 63
64 @override 64 @override
65 void didChangeAppLifecycleState(AppLifecycleState state) { 65 void didChangeAppLifecycleState(AppLifecycleState state) {
66 - super.didChangeAppLifecycleState(state); 66 + if (!controller.value.isInitialized) {
  67 + return;
  68 + }
67 69
68 switch (state) { 70 switch (state) {
69 case AppLifecycleState.detached: 71 case AppLifecycleState.detached:
@@ -138,6 +138,15 @@ class ToggleFlashlightButton extends StatelessWidget { @@ -138,6 +138,15 @@ class ToggleFlashlightButton extends StatelessWidget {
138 } 138 }
139 139
140 switch (state.torchState) { 140 switch (state.torchState) {
  141 + case TorchState.auto:
  142 + return IconButton(
  143 + color: Colors.white,
  144 + iconSize: 32.0,
  145 + icon: const Icon(Icons.flash_auto),
  146 + onPressed: () async {
  147 + await controller.toggleTorch();
  148 + },
  149 + );
141 case TorchState.off: 150 case TorchState.off:
142 return IconButton( 151 return IconButton(
143 color: Colors.white, 152 color: Colors.white,
@@ -259,7 +259,7 @@ @@ -259,7 +259,7 @@
259 isa = PBXProject; 259 isa = PBXProject;
260 attributes = { 260 attributes = {
261 LastSwiftUpdateCheck = 0920; 261 LastSwiftUpdateCheck = 0920;
262 - LastUpgradeCheck = 1430; 262 + LastUpgradeCheck = 1510;
263 ORGANIZATIONNAME = ""; 263 ORGANIZATIONNAME = "";
264 TargetAttributes = { 264 TargetAttributes = {
265 331C80D4294CF70F00263BE5 = { 265 331C80D4294CF70F00263BE5 = {
1 <?xml version="1.0" encoding="UTF-8"?> 1 <?xml version="1.0" encoding="UTF-8"?>
2 <Scheme 2 <Scheme
3 - LastUpgradeVersion = "1430" 3 + LastUpgradeVersion = "1510"
4 version = "1.3"> 4 version = "1.3">
5 <BuildAction 5 <BuildAction
6 parallelizeBuildables = "YES" 6 parallelizeBuildables = "YES"
@@ -259,12 +259,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -259,12 +259,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
259 // as they interact with the hardware camera. 259 // as they interact with the hardware camera.
260 if (torch) { 260 if (torch) {
261 DispatchQueue.main.async { 261 DispatchQueue.main.async {
262 - do {  
263 - try self.toggleTorch(.on)  
264 - } catch {  
265 - // If the torch does not turn on,  
266 - // continue with the capture session anyway.  
267 - } 262 + self.turnTorchOn()
268 } 263 }
269 } 264 }
270 265
@@ -283,13 +278,12 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -283,13 +278,12 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
283 // as this does not change the configuration of the hardware camera. 278 // as this does not change the configuration of the hardware camera.
284 let dimensions = CMVideoFormatDescriptionGetDimensions( 279 let dimensions = CMVideoFormatDescriptionGetDimensions(
285 device.activeFormat.formatDescription) 280 device.activeFormat.formatDescription)
286 - let hasTorch = device.hasTorch  
287 281
288 completion( 282 completion(
289 MobileScannerStartParameters( 283 MobileScannerStartParameters(
290 width: Double(dimensions.height), 284 width: Double(dimensions.height),
291 height: Double(dimensions.width), 285 height: Double(dimensions.width),
292 - hasTorch: hasTorch, 286 + currentTorchState: device.hasTorch ? device.torchMode.rawValue : -1,
293 textureId: self.textureId ?? 0 287 textureId: self.textureId ?? 0
294 ) 288 )
295 ) 289 )
@@ -324,30 +318,67 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -324,30 +318,67 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
324 device = nil 318 device = nil
325 } 319 }
326 320
327 - /// Set the torch mode. 321 + /// Toggle the torch.
328 /// 322 ///
329 /// This method should be called on the main DispatchQueue. 323 /// This method should be called on the main DispatchQueue.
330 - func toggleTorch(_ torch: AVCaptureDevice.TorchMode) throws { 324 + func toggleTorch() {
331 guard let device = self.device else { 325 guard let device = self.device else {
332 return 326 return
333 } 327 }
334 328
335 - if (!device.hasTorch || !device.isTorchAvailable || !device.isTorchModeSupported(torch)) { 329 + if (!device.hasTorch || !device.isTorchAvailable) {
336 return 330 return
337 } 331 }
338 332
339 - if (device.torchMode != torch) { 333 + var newTorchMode: AVCaptureDevice.TorchMode = device.torchMode
  334 +
  335 + switch(device.torchMode) {
  336 + case AVCaptureDevice.TorchMode.auto:
  337 + newTorchMode = device.isTorchActive ? AVCaptureDevice.TorchMode.off : AVCaptureDevice.TorchMode.on
  338 + break;
  339 + case AVCaptureDevice.TorchMode.off:
  340 + newTorchMode = AVCaptureDevice.TorchMode.on
  341 + break;
  342 + case AVCaptureDevice.TorchMode.on:
  343 + newTorchMode = AVCaptureDevice.TorchMode.off
  344 + break;
  345 + default:
  346 + return;
  347 + }
  348 +
  349 + if (!device.isTorchModeSupported(newTorchMode) || device.torchMode == newTorchMode) {
  350 + return;
  351 + }
  352 +
  353 + do {
340 try device.lockForConfiguration() 354 try device.lockForConfiguration()
341 - device.torchMode = torch 355 + device.torchMode = newTorchMode
342 device.unlockForConfiguration() 356 device.unlockForConfiguration()
  357 + } catch(_) {}
  358 + }
  359 +
  360 + /// Turn the torch on.
  361 + private func turnTorchOn() {
  362 + guard let device = self.device else {
  363 + return
  364 + }
  365 +
  366 + if (!device.hasTorch || !device.isTorchAvailable || !device.isTorchModeSupported(.on) || device.torchMode == .on) {
  367 + return
343 } 368 }
  369 +
  370 + do {
  371 + try device.lockForConfiguration()
  372 + device.torchMode = .on
  373 + device.unlockForConfiguration()
  374 + } catch(_) {}
344 } 375 }
345 376
346 // Observer for torch state 377 // Observer for torch state
347 public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { 378 public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
348 switch keyPath { 379 switch keyPath {
349 case "torchMode": 380 case "torchMode":
350 - // off = 0; on = 1; auto = 2 381 + // Off = 0, On = 1, Auto = 2
351 let state = change?[.newKey] as? Int 382 let state = change?[.newKey] as? Int
352 torchModeChangeCallback(state) 383 torchModeChangeCallback(state)
353 case "videoZoomFactor": 384 case "videoZoomFactor":
@@ -459,7 +490,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -459,7 +490,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
459 struct MobileScannerStartParameters { 490 struct MobileScannerStartParameters {
460 var width: Double = 0.0 491 var width: Double = 0.0
461 var height: Double = 0.0 492 var height: Double = 0.0
462 - var hasTorch = false 493 + var currentTorchState: Int = -1
463 var textureId: Int64 = 0 494 var textureId: Int64 = 0
464 } 495 }
465 } 496 }
@@ -80,8 +80,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { @@ -80,8 +80,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
80 start(call, result) 80 start(call, result)
81 case "stop": 81 case "stop":
82 stop(result) 82 stop(result)
83 - case "torch":  
84 - toggleTorch(call, result) 83 + case "toggleTorch":
  84 + toggleTorch(result)
85 case "analyzeImage": 85 case "analyzeImage":
86 analyzeImage(call, result) 86 analyzeImage(call, result)
87 case "setScale": 87 case "setScale":
@@ -125,7 +125,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { @@ -125,7 +125,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
125 result([ 125 result([
126 "textureId": parameters.textureId, 126 "textureId": parameters.textureId,
127 "size": ["width": parameters.width, "height": parameters.height], 127 "size": ["width": parameters.width, "height": parameters.height],
128 - "torchable": parameters.hasTorch]) 128 + "currentTorchState": parameters.currentTorchState,
  129 + ])
129 } 130 }
130 } 131 }
131 } catch MobileScannerError.alreadyStarted { 132 } catch MobileScannerError.alreadyStarted {
@@ -156,13 +157,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { @@ -156,13 +157,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
156 } 157 }
157 158
158 /// Toggles the torch. 159 /// Toggles the torch.
159 - private func toggleTorch(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {  
160 - do {  
161 - try mobileScanner.toggleTorch(call.arguments as? Int == 1 ? .on : .off)  
162 - result(nil)  
163 - } catch {  
164 - result(FlutterError(code: "MobileScanner", message: error.localizedDescription, details: nil))  
165 - } 160 + private func toggleTorch(_ result: @escaping FlutterResult) {
  161 + mobileScanner.toggleTorch()
  162 + result(nil)
166 } 163 }
167 164
168 /// Sets the zoomScale. 165 /// Sets the zoomScale.
@@ -8,31 +8,6 @@ extension CVBuffer { @@ -8,31 +8,6 @@ extension CVBuffer {
8 let cgImage = CIContext().createCGImage(ciImage, from: ciImage.extent) 8 let cgImage = CIContext().createCGImage(ciImage, from: ciImage.extent)
9 return UIImage(cgImage: cgImage!) 9 return UIImage(cgImage: cgImage!)
10 } 10 }
11 -  
12 - var image1: UIImage {  
13 - // Lock the base address of the pixel buffer  
14 - CVPixelBufferLockBaseAddress(self, CVPixelBufferLockFlags.readOnly)  
15 - // Get the number of bytes per row for the pixel buffer  
16 - let baseAddress = CVPixelBufferGetBaseAddress(self)  
17 - // Get the number of bytes per row for the pixel buffer  
18 - let bytesPerRow = CVPixelBufferGetBytesPerRow(self)  
19 - // Get the pixel buffer width and height  
20 - let width = CVPixelBufferGetWidth(self)  
21 - let height = CVPixelBufferGetHeight(self)  
22 - // Create a device-dependent RGB color space  
23 - let colorSpace = CGColorSpaceCreateDeviceRGB()  
24 - // Create a bitmap graphics context with the sample buffer data  
25 - var bitmapInfo = CGBitmapInfo.byteOrder32Little.rawValue  
26 - bitmapInfo |= CGImageAlphaInfo.premultipliedFirst.rawValue & CGBitmapInfo.alphaInfoMask.rawValue  
27 - //let bitmapInfo: UInt32 = CGBitmapInfo.alphaInfoMask.rawValue  
28 - let context = CGContext(data: baseAddress, width: width, height: height, bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo)  
29 - // Create a Quartz image from the pixel data in the bitmap graphics context  
30 - let quartzImage = context?.makeImage()  
31 - // Unlock the pixel buffer  
32 - CVPixelBufferUnlockBaseAddress(self, CVPixelBufferLockFlags.readOnly)  
33 - // Create an image object from the Quartz image  
34 - return UIImage(cgImage: quartzImage!)  
35 - }  
36 } 11 }
37 12
38 extension UIDeviceOrientation { 13 extension UIDeviceOrientation {
@@ -4,7 +4,7 @@ @@ -4,7 +4,7 @@
4 # 4 #
5 Pod::Spec.new do |s| 5 Pod::Spec.new do |s|
6 s.name = 'mobile_scanner' 6 s.name = 'mobile_scanner'
7 - s.version = '5.0.0' 7 + s.version = '5.0.2'
8 s.summary = 'An universal scanner for Flutter based on MLKit.' 8 s.summary = 'An universal scanner for Flutter based on MLKit.'
9 s.description = <<-DESC 9 s.description = <<-DESC
10 An universal scanner for Flutter based on MLKit. 10 An universal scanner for Flutter based on MLKit.
1 /// The state of the flashlight. 1 /// The state of the flashlight.
2 enum TorchState { 2 enum TorchState {
  3 + /// The flashlight turns on automatically in low light conditions.
  4 + ///
  5 + /// This is currently only supported on iOS and MacOS.
  6 + auto(2),
  7 +
3 /// The flashlight is off. 8 /// The flashlight is off.
4 off(0), 9 off(0),
5 10
@@ -7,18 +12,20 @@ enum TorchState { @@ -7,18 +12,20 @@ enum TorchState {
7 on(1), 12 on(1),
8 13
9 /// The flashlight is unavailable. 14 /// The flashlight is unavailable.
10 - unavailable(2); 15 + unavailable(-1);
11 16
12 const TorchState(this.rawValue); 17 const TorchState(this.rawValue);
13 18
14 factory TorchState.fromRawValue(int value) { 19 factory TorchState.fromRawValue(int value) {
15 switch (value) { 20 switch (value) {
  21 + case -1:
  22 + return TorchState.unavailable;
16 case 0: 23 case 0:
17 return TorchState.off; 24 return TorchState.off;
18 case 1: 25 case 1:
19 return TorchState.on; 26 return TorchState.on;
20 case 2: 27 case 2:
21 - return TorchState.unavailable; 28 + return TorchState.auto;
22 default: 29 default:
23 throw ArgumentError.value(value, 'value', 'Invalid raw value.'); 30 throw ArgumentError.value(value, 'value', 'Invalid raw value.');
24 } 31 }
@@ -178,15 +178,6 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { @@ -178,15 +178,6 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
178 } 178 }
179 179
180 @override 180 @override
181 - Future<void> setTorchState(TorchState torchState) async {  
182 - if (torchState == TorchState.unavailable) {  
183 - return;  
184 - }  
185 -  
186 - await methodChannel.invokeMethod<void>('torch', torchState.rawValue);  
187 - }  
188 -  
189 - @override  
190 Future<void> setZoomScale(double zoomScale) async { 181 Future<void> setZoomScale(double zoomScale) async {
191 await methodChannel.invokeMethod<void>('setScale', zoomScale); 182 await methodChannel.invokeMethod<void>('setScale', zoomScale);
192 } 183 }
@@ -246,7 +237,9 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { @@ -246,7 +237,9 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
246 _textureId = textureId; 237 _textureId = textureId;
247 238
248 final int? numberOfCameras = startResult['numberOfCameras'] as int?; 239 final int? numberOfCameras = startResult['numberOfCameras'] as int?;
249 - final bool hasTorch = startResult['torchable'] as bool? ?? false; 240 + final TorchState currentTorchState = TorchState.fromRawValue(
  241 + startResult['currentTorchState'] as int? ?? -1,
  242 + );
250 243
251 final Map<Object?, Object?>? sizeInfo = 244 final Map<Object?, Object?>? sizeInfo =
252 startResult['size'] as Map<Object?, Object?>?; 245 startResult['size'] as Map<Object?, Object?>?;
@@ -262,7 +255,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { @@ -262,7 +255,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
262 } 255 }
263 256
264 return MobileScannerViewAttributes( 257 return MobileScannerViewAttributes(
265 - hasTorch: hasTorch, 258 + currentTorchMode: currentTorchState,
266 numberOfCameras: numberOfCameras, 259 numberOfCameras: numberOfCameras,
267 size: size, 260 size: size,
268 ); 261 );
@@ -280,6 +273,11 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { @@ -280,6 +273,11 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
280 } 273 }
281 274
282 @override 275 @override
  276 + Future<void> toggleTorch() async {
  277 + await methodChannel.invokeMethod<void>('toggleTorch');
  278 + }
  279 +
  280 + @override
283 Future<void> updateScanWindow(Rect? window) async { 281 Future<void> updateScanWindow(Rect? window) async {
284 if (_textureId == null) { 282 if (_textureId == null) {
285 return; 283 return;
@@ -281,9 +281,9 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { @@ -281,9 +281,9 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
281 isInitialized: true, 281 isInitialized: true,
282 isRunning: true, 282 isRunning: true,
283 size: viewAttributes.size, 283 size: viewAttributes.size,
284 - // If the device has a flashlight, let the platform update the torch state.  
285 - // If it does not have one, provide the unavailable state directly.  
286 - torchState: viewAttributes.hasTorch ? null : TorchState.unavailable, 284 + // Provide the current torch state.
  285 + // Updates are provided by the `torchStateStream`.
  286 + torchState: viewAttributes.currentTorchMode,
287 ); 287 );
288 } 288 }
289 } on MobileScannerException catch (error) { 289 } on MobileScannerException catch (error) {
@@ -322,11 +322,16 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { @@ -322,11 +322,16 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
322 322
323 _disposeListeners(); 323 _disposeListeners();
324 324
  325 + final TorchState oldTorchState = value.torchState;
  326 +
325 // After the camera stopped, set the torch state to off, 327 // After the camera stopped, set the torch state to off,
326 // as the torch state callback is never called when the camera is stopped. 328 // as the torch state callback is never called when the camera is stopped.
  329 + // If the device does not have a torch, do not report "off".
327 value = value.copyWith( 330 value = value.copyWith(
328 isRunning: false, 331 isRunning: false,
329 - torchState: TorchState.off, 332 + torchState: oldTorchState == TorchState.unavailable
  333 + ? TorchState.unavailable
  334 + : TorchState.off,
330 ); 335 );
331 336
332 await MobileScannerPlatform.instance.stop(); 337 await MobileScannerPlatform.instance.stop();
@@ -362,6 +367,9 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { @@ -362,6 +367,9 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
362 /// 367 ///
363 /// Does nothing if the device has no torch, 368 /// Does nothing if the device has no torch,
364 /// or if the camera is not running. 369 /// or if the camera is not running.
  370 + ///
  371 + /// If the current torch state is [TorchState.auto],
  372 + /// the torch is turned on or off depending on its actual current state.
365 Future<void> toggleTorch() async { 373 Future<void> toggleTorch() async {
366 _throwIfNotInitialized(); 374 _throwIfNotInitialized();
367 375
@@ -375,13 +383,10 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { @@ -375,13 +383,10 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
375 return; 383 return;
376 } 384 }
377 385
378 - final TorchState newState =  
379 - torchState == TorchState.off ? TorchState.on : TorchState.off;  
380 -  
381 - // Update the torch state to the new state. 386 + // Request the torch state to be switched to the opposite state.
382 // When the platform has updated the torch state, 387 // When the platform has updated the torch state,
383 // it will send an update through the torch state event stream. 388 // it will send an update through the torch state event stream.
384 - await MobileScannerPlatform.instance.setTorchState(newState); 389 + await MobileScannerPlatform.instance.toggleTorch();
385 } 390 }
386 391
387 /// Update the scan window with the given [window] rectangle. 392 /// Update the scan window with the given [window] rectangle.
@@ -67,11 +67,6 @@ abstract class MobileScannerPlatform extends PlatformInterface { @@ -67,11 +67,6 @@ abstract class MobileScannerPlatform extends PlatformInterface {
67 /// This is only supported on the web. 67 /// This is only supported on the web.
68 void setBarcodeLibraryScriptUrl(String scriptUrl) {} 68 void setBarcodeLibraryScriptUrl(String scriptUrl) {}
69 69
70 - /// Set the torch state of the active camera.  
71 - Future<void> setTorchState(TorchState torchState) {  
72 - throw UnimplementedError('setTorchState() has not been implemented.');  
73 - }  
74 -  
75 /// Set the zoom scale of the camera. 70 /// Set the zoom scale of the camera.
76 /// 71 ///
77 /// The [zoomScale] must be between `0.0` and `1.0` (both inclusive). 72 /// The [zoomScale] must be between `0.0` and `1.0` (both inclusive).
@@ -95,6 +90,11 @@ abstract class MobileScannerPlatform extends PlatformInterface { @@ -95,6 +90,11 @@ abstract class MobileScannerPlatform extends PlatformInterface {
95 throw UnimplementedError('stop() has not been implemented.'); 90 throw UnimplementedError('stop() has not been implemented.');
96 } 91 }
97 92
  93 + /// Toggle the torch on the active camera on or off.
  94 + Future<void> toggleTorch() {
  95 + throw UnimplementedError('toggleTorch() has not been implemented.');
  96 + }
  97 +
98 /// Update the scan window to the given [window] rectangle. 98 /// Update the scan window to the given [window] rectangle.
99 /// 99 ///
100 /// Any barcodes that do not intersect with the given [window] will be ignored. 100 /// Any barcodes that do not intersect with the given [window] will be ignored.
1 import 'dart:ui'; 1 import 'dart:ui';
2 2
  3 +import 'package:mobile_scanner/src/enums/torch_state.dart';
  4 +
3 /// This class defines the attributes for the mobile scanner view. 5 /// This class defines the attributes for the mobile scanner view.
4 class MobileScannerViewAttributes { 6 class MobileScannerViewAttributes {
5 const MobileScannerViewAttributes({ 7 const MobileScannerViewAttributes({
6 - required this.hasTorch, 8 + required this.currentTorchMode,
7 this.numberOfCameras, 9 this.numberOfCameras,
8 required this.size, 10 required this.size,
9 }); 11 });
10 12
11 - /// Whether the current active camera has a torch.  
12 - final bool hasTorch; 13 + /// The current torch state of the active camera.
  14 + final TorchState currentTorchMode;
13 15
14 /// The number of available cameras. 16 /// The number of available cameras.
15 final int? numberOfCameras; 17 final int? numberOfCameras;
@@ -186,17 +186,17 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -186,17 +186,17 @@ class MobileScannerWeb extends MobileScannerPlatform {
186 } 186 }
187 187
188 try { 188 try {
189 - // Retrieving the media devices requests the camera permission.  
190 _permissionRequestInProgress = true; 189 _permissionRequestInProgress = true;
191 190
  191 + // Retrieving the media devices requests the camera permission.
192 final MediaStream videoStream = 192 final MediaStream videoStream =
193 await window.navigator.mediaDevices.getUserMedia(constraints).toDart; 193 await window.navigator.mediaDevices.getUserMedia(constraints).toDart;
194 194
195 - // At this point the permission is granted.  
196 _permissionRequestInProgress = false; 195 _permissionRequestInProgress = false;
197 196
198 return videoStream; 197 return videoStream;
199 } on DOMException catch (error, stackTrace) { 198 } on DOMException catch (error, stackTrace) {
  199 + _permissionRequestInProgress = false;
200 final String errorMessage = error.toString(); 200 final String errorMessage = error.toString();
201 201
202 MobileScannerErrorCode errorCode = MobileScannerErrorCode.genericError; 202 MobileScannerErrorCode errorCode = MobileScannerErrorCode.genericError;
@@ -209,10 +209,6 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -209,10 +209,6 @@ class MobileScannerWeb extends MobileScannerPlatform {
209 errorCode = MobileScannerErrorCode.permissionDenied; 209 errorCode = MobileScannerErrorCode.permissionDenied;
210 } 210 }
211 211
212 - // At this point the permission request completed, although with an error,  
213 - // but the error is irrelevant.  
214 - _permissionRequestInProgress = false;  
215 -  
216 throw MobileScannerException( 212 throw MobileScannerException(
217 errorCode: errorCode, 213 errorCode: errorCode,
218 errorDetails: MobileScannerErrorDetails( 214 errorDetails: MobileScannerErrorDetails(
@@ -251,14 +247,6 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -251,14 +247,6 @@ class MobileScannerWeb extends MobileScannerPlatform {
251 } 247 }
252 248
253 @override 249 @override
254 - Future<void> setTorchState(TorchState torchState) {  
255 - throw UnsupportedError(  
256 - 'Setting the torch state is not supported for video tracks on the web.\n'  
257 - 'See https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints#instance_properties_of_video_tracks',  
258 - );  
259 - }  
260 -  
261 - @override  
262 Future<void> setZoomScale(double zoomScale) { 250 Future<void> setZoomScale(double zoomScale) {
263 throw UnsupportedError( 251 throw UnsupportedError(
264 'Setting the zoom scale is not supported for video tracks on the web.\n' 252 'Setting the zoom scale is not supported for video tracks on the web.\n'
@@ -346,7 +334,9 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -346,7 +334,9 @@ class MobileScannerWeb extends MobileScannerPlatform {
346 } 334 }
347 335
348 return MobileScannerViewAttributes( 336 return MobileScannerViewAttributes(
349 - hasTorch: hasTorch, 337 + // The torch of a media stream is not available for video tracks.
  338 + // See https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints#instance_properties_of_video_tracks
  339 + currentTorchMode: TorchState.unavailable,
350 size: _barcodeReader?.videoSize ?? Size.zero, 340 size: _barcodeReader?.videoSize ?? Size.zero,
351 ); 341 );
352 } catch (error, stackTrace) { 342 } catch (error, stackTrace) {
@@ -371,6 +361,14 @@ class MobileScannerWeb extends MobileScannerPlatform { @@ -371,6 +361,14 @@ class MobileScannerWeb extends MobileScannerPlatform {
371 } 361 }
372 362
373 @override 363 @override
  364 + Future<void> toggleTorch() {
  365 + throw UnsupportedError(
  366 + 'Setting the torch state is not supported for video tracks on the web.\n'
  367 + 'See https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints#instance_properties_of_video_tracks',
  368 + );
  369 + }
  370 +
  371 + @override
374 Future<void> updateScanWindow(Rect? window) { 372 Future<void> updateScanWindow(Rect? window) {
375 // A scan window is not supported on the web, 373 // A scan window is not supported on the web,
376 // because the scanner does not expose size information for the barcodes. 374 // because the scanner does not expose size information for the barcodes.
@@ -59,8 +59,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -59,8 +59,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
59 requestPermission(call, result) 59 requestPermission(call, result)
60 case "start": 60 case "start":
61 start(call, result) 61 start(call, result)
62 - case "torch":  
63 - toggleTorch(call, result) 62 + case "toggleTorch":
  63 + toggleTorch(result)
64 case "setScale": 64 case "setScale":
65 setScale(call, result) 65 setScale(call, result)
66 case "resetScale": 66 case "resetScale":
@@ -288,12 +288,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -288,12 +288,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
288 288
289 // Turn on the torch if requested. 289 // Turn on the torch if requested.
290 if (torch) { 290 if (torch) {
291 - do {  
292 - try self.toggleTorchInternal(.on)  
293 - } catch {  
294 - // If the torch could not be turned on,  
295 - // continue the capture session.  
296 - } 291 + self.turnTorchOn()
297 } 292 }
298 293
299 device.addObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode), options: .new, context: nil) 294 device.addObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode), options: .new, context: nil)
@@ -326,17 +321,22 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -326,17 +321,22 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
326 captureSession!.startRunning() 321 captureSession!.startRunning()
327 let dimensions = CMVideoFormatDescriptionGetDimensions(device.activeFormat.formatDescription) 322 let dimensions = CMVideoFormatDescriptionGetDimensions(device.activeFormat.formatDescription)
328 let size = ["width": Double(dimensions.width), "height": Double(dimensions.height)] 323 let size = ["width": Double(dimensions.width), "height": Double(dimensions.height)]
329 - let answer: [String : Any?] = ["textureId": textureId, "size": size, "torchable": device.hasTorch] 324 +
  325 + let answer: [String : Any?] = [
  326 + "textureId": textureId,
  327 + "size": size,
  328 + "currentTorchState": device.hasTorch ? device.torchMode.rawValue : -1,
  329 + ]
330 result(answer) 330 result(answer)
331 } 331 }
332 332
333 // TODO: this method should be removed when iOS and MacOS share their implementation. 333 // TODO: this method should be removed when iOS and MacOS share their implementation.
334 - private func toggleTorchInternal(_ torch: AVCaptureDevice.TorchMode) throws { 334 + private func toggleTorchInternal() {
335 guard let device = self.device else { 335 guard let device = self.device else {
336 return 336 return
337 } 337 }
338 338
339 - if (!device.hasTorch || !device.isTorchModeSupported(torch)) { 339 + if (!device.hasTorch) {
340 return 340 return
341 } 341 }
342 342
@@ -345,12 +345,57 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -345,12 +345,57 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
345 return 345 return
346 } 346 }
347 } 347 }
  348 +
  349 + var newTorchMode: AVCaptureDevice.TorchMode = device.torchMode
  350 +
  351 + switch(device.torchMode) {
  352 + case AVCaptureDevice.TorchMode.auto:
  353 + if #available(macOS 10.15, *) {
  354 + newTorchMode = device.isTorchActive ? AVCaptureDevice.TorchMode.off : AVCaptureDevice.TorchMode.on
  355 + }
  356 + break;
  357 + case AVCaptureDevice.TorchMode.off:
  358 + newTorchMode = AVCaptureDevice.TorchMode.on
  359 + break;
  360 + case AVCaptureDevice.TorchMode.on:
  361 + newTorchMode = AVCaptureDevice.TorchMode.off
  362 + break;
  363 + default:
  364 + return;
  365 + }
  366 +
  367 + if (!device.isTorchModeSupported(newTorchMode) || device.torchMode == newTorchMode) {
  368 + return;
  369 + }
348 370
349 - if (device.torchMode != torch) { 371 + do {
350 try device.lockForConfiguration() 372 try device.lockForConfiguration()
351 - device.torchMode = torch 373 + device.torchMode = newTorchMode
352 device.unlockForConfiguration() 374 device.unlockForConfiguration()
  375 + } catch(_) {}
  376 + }
  377 +
  378 + /// Turn the torch on.
  379 + private func turnTorchOn() {
  380 + guard let device = self.device else {
  381 + return
  382 + }
  383 +
  384 + if (!device.hasTorch || !device.isTorchModeSupported(.on) || device.torchMode == .on) {
  385 + return
353 } 386 }
  387 +
  388 + if #available(macOS 15.0, *) {
  389 + if(!device.isTorchAvailable) {
  390 + return
  391 + }
  392 + }
  393 +
  394 + do {
  395 + try device.lockForConfiguration()
  396 + device.torchMode = .on
  397 + device.unlockForConfiguration()
  398 + } catch(_) {}
354 } 399 }
355 400
356 /// Reset the zoom scale. 401 /// Reset the zoom scale.
@@ -365,15 +410,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -365,15 +410,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
365 result(nil) 410 result(nil)
366 } 411 }
367 412
368 - private func toggleTorch(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {  
369 - let requestedTorchMode: AVCaptureDevice.TorchMode = call.arguments as! Int == 1 ? .on : .off  
370 -  
371 - do {  
372 - try self.toggleTorchInternal(requestedTorchMode)  
373 - result(nil)  
374 - } catch {  
375 - result(FlutterError(code: "MobileScanner", message: error.localizedDescription, details: nil))  
376 - } 413 + private func toggleTorch(_ result: @escaping FlutterResult) {
  414 + self.toggleTorchInternal()
  415 + result(nil)
377 } 416 }
378 417
379 // func switchAnalyzeMode(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { 418 // func switchAnalyzeMode(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
@@ -410,7 +449,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -410,7 +449,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
410 public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { 449 public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
411 switch keyPath { 450 switch keyPath {
412 case "torchMode": 451 case "torchMode":
413 - // off = 0 on = 1 auto = 2 452 + // Off = 0, On = 1, Auto = 2
414 let state = change?[.newKey] as? Int 453 let state = change?[.newKey] as? Int
415 let event: [String: Any?] = ["name": "torchState", "data": state] 454 let event: [String: Any?] = ["name": "torchState", "data": state]
416 sink?(event) 455 sink?(event)
@@ -4,7 +4,7 @@ @@ -4,7 +4,7 @@
4 # 4 #
5 Pod::Spec.new do |s| 5 Pod::Spec.new do |s|
6 s.name = 'mobile_scanner' 6 s.name = 'mobile_scanner'
7 - s.version = '5.0.0' 7 + s.version = '5.0.2'
8 s.summary = 'An universal scanner for Flutter based on MLKit.' 8 s.summary = 'An universal scanner for Flutter based on MLKit.'
9 s.description = <<-DESC 9 s.description = <<-DESC
10 An universal scanner for Flutter based on MLKit. 10 An universal scanner for Flutter based on MLKit.
@@ -7,7 +7,8 @@ void main() { @@ -7,7 +7,8 @@ void main() {
7 const values = <int, TorchState>{ 7 const values = <int, TorchState>{
8 0: TorchState.off, 8 0: TorchState.off,
9 1: TorchState.on, 9 1: TorchState.on,
10 - 2: TorchState.unavailable, 10 + 2: TorchState.auto,
  11 + -1: TorchState.unavailable,
11 }; 12 };
12 13
13 for (final MapEntry<int, TorchState> entry in values.entries) { 14 for (final MapEntry<int, TorchState> entry in values.entries) {
@@ -18,7 +19,7 @@ void main() { @@ -18,7 +19,7 @@ void main() {
18 }); 19 });
19 20
20 test('invalid raw value throws argument error', () { 21 test('invalid raw value throws argument error', () {
21 - const int negative = -1; 22 + const int negative = -2;
22 const int outOfRange = 3; 23 const int outOfRange = 3;
23 24
24 expect(() => TorchState.fromRawValue(negative), throwsArgumentError); 25 expect(() => TorchState.fromRawValue(negative), throwsArgumentError);
@@ -27,9 +28,10 @@ void main() { @@ -27,9 +28,10 @@ void main() {
27 28
28 test('can be converted to raw value', () { 29 test('can be converted to raw value', () {
29 const values = <TorchState, int>{ 30 const values = <TorchState, int>{
  31 + TorchState.unavailable: -1,
30 TorchState.off: 0, 32 TorchState.off: 0,
31 TorchState.on: 1, 33 TorchState.on: 1,
32 - TorchState.unavailable: 2, 34 + TorchState.auto: 2,
33 }; 35 };
34 36
35 for (final MapEntry<TorchState, int> entry in values.entries) { 37 for (final MapEntry<TorchState, int> entry in values.entries) {