Julian Steenbakker

Merge branch 'master' into feature/scan-window

# Conflicts:
#	android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt
#	example/ios/Runner.xcodeproj/project.pbxproj
#	example/lib/main.dart
#	ios/Classes/SwiftMobileScannerPlugin.swift
#	lib/src/mobile_scanner.dart
#	lib/src/mobile_scanner_controller.dart
Showing 64 changed files with 2682 additions and 1146 deletions
  1 +# These owners will be the default owners for everything in
  2 +# the repo. Unless a later match takes precedence,
  3 +# review when someone opens a pull request.
  4 +# For more on how to customize the CODEOWNERS file - https://help.github.com/en/articles/about-code-owners
  5 +
  6 +* @juliansteenbakker
@@ -6,15 +6,33 @@ updates: @@ -6,15 +6,33 @@ updates:
6 interval: "weekly" 6 interval: "weekly"
7 reviewers: 7 reviewers:
8 - "juliansteenbakker" 8 - "juliansteenbakker"
  9 + commit-message:
  10 + prefix: "chore"
  11 + include: "scope"
9 - package-ecosystem: gradle 12 - package-ecosystem: gradle
10 directory: "/android" 13 directory: "/android"
11 schedule: 14 schedule:
12 interval: "weekly" 15 interval: "weekly"
13 reviewers: 16 reviewers:
14 - "juliansteenbakker" 17 - "juliansteenbakker"
  18 + commit-message:
  19 + prefix: "chore"
  20 + include: "scope"
15 - package-ecosystem: gradle 21 - package-ecosystem: gradle
16 directory: "/example/android" 22 directory: "/example/android"
17 schedule: 23 schedule:
18 interval: "weekly" 24 interval: "weekly"
19 reviewers: 25 reviewers:
20 - "juliansteenbakker" 26 - "juliansteenbakker"
  27 + commit-message:
  28 + prefix: "chore"
  29 + include: "scope"
  30 + - package-ecosystem: "pub"
  31 + directory: "/"
  32 + schedule:
  33 + interval: "weekly"
  34 + commit-message:
  35 + prefix: "chore"
  36 + include: "scope"
  37 + reviewers:
  38 + - "juliansteenbakker"
  1 +# .github/workflows/auto-author-assign.yml
  2 +name: 'Auto Author Assign'
  3 +
  4 +on:
  5 + pull_request_target:
  6 + types: [opened, reopened]
  7 +
  8 +permissions:
  9 + pull-requests: write
  10 +
  11 +jobs:
  12 + assign-author:
  13 + runs-on: ubuntu-latest
  14 + steps:
  15 + - uses: toshimaru/auto-author-assign@v1.6.1
@@ -11,12 +11,12 @@ jobs: @@ -11,12 +11,12 @@ jobs:
11 analysis: 11 analysis:
12 runs-on: ubuntu-latest 12 runs-on: ubuntu-latest
13 steps: 13 steps:
14 - - uses: actions/checkout@v3.0.2  
15 - - uses: actions/setup-java@v3.4.1 14 + - uses: actions/checkout@v3.1.0
  15 + - uses: actions/setup-java@v3.6.0
16 with: 16 with:
17 java-version: 11 17 java-version: 11
18 distribution: temurin 18 distribution: temurin
19 - - uses: subosito/flutter-action@v2.6.1 19 + - uses: subosito/flutter-action@v2.8.0
20 with: 20 with:
21 cache: true 21 cache: true
22 - name: Version 22 - name: Version
@@ -28,12 +28,12 @@ jobs: @@ -28,12 +28,12 @@ jobs:
28 formatting: 28 formatting:
29 runs-on: ubuntu-latest 29 runs-on: ubuntu-latest
30 steps: 30 steps:
31 - - uses: actions/checkout@v3.0.2  
32 - - uses: actions/setup-java@v3.4.1 31 + - uses: actions/checkout@v3.1.0
  32 + - uses: actions/setup-java@v3.6.0
33 with: 33 with:
34 java-version: 11 34 java-version: 11
35 distribution: temurin 35 distribution: temurin
36 - - uses: subosito/flutter-action@v2.6.1 36 + - uses: subosito/flutter-action@v2.8.0
37 with: 37 with:
38 cache: true 38 cache: true
39 - name: Format 39 - name: Format
  1 + name: Run release-please
  2 + on:
  3 + push:
  4 + branches:
  5 + - master
  6 + jobs:
  7 + release-please:
  8 + runs-on: ubuntu-latest
  9 + steps:
  10 + - uses: GoogleCloudPlatform/release-please-action@v3.6.1
  11 + with:
  12 + token: ${{ secrets.GITHUB_TOKEN }}
  13 + release-type: simple
  1 +name: "Semantic PRs"
  2 +
  3 +on:
  4 + pull_request_target:
  5 + types:
  6 + - opened
  7 + - edited
  8 + - synchronize
  9 +
  10 +jobs:
  11 + main:
  12 + name: Validate PR title
  13 + runs-on: ubuntu-latest
  14 + steps:
  15 + - uses: amannn/action-semantic-pull-request@v5
  16 + env:
  17 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  1 +## 3.0.0-beta.3
  2 +Deprecated:
  3 +* The `onStart` method has been renamed to `onScannerStarted`.
  4 +* The `onPermissionSet` argument of the `MobileScannerController` is now deprecated.
  5 +
  6 +Breaking changes:
  7 +* `MobileScannerException` now uses an `errorCode` instead of a `message`.
  8 +* `MobileScannerException` now contains additional details from the original error.
  9 +* Refactored `MobileScannerController.start()` to throw `MobileScannerException`s
  10 + with consistent error codes, rather than string messages.
  11 + To handle permission errors, consider catching the result of `MobileScannerController.start()`.
  12 +* The `autoResume` attribute has been removed from the `MobileScanner` widget.
  13 + The controller already automatically resumes, so it had no effect.
  14 +* Removed `MobileScannerCallback` and `MobileScannerArgumentsCallback` typedef.
  15 +
  16 +Improvements:
  17 +* Toggling the device torch now does nothing if the device has no torch, rather than throwing an error.
  18 +* Removed `called stop while already stopped` messages.
  19 +
  20 +Features:
  21 +* Added a new `placeholderBuilder` function to the `MobileScanner` widget to customize the preview placeholder.
  22 +* Added `autoStart` parameter to MobileScannerController(). If set to false, controller won't start automatically.
  23 +* Added `hasTorch` function on MobileScannerController(). After starting the controller, you can check if the device has a torch.
  24 +
  25 +Fixes:
  26 +* Fixes the missing gradle setup for the Android project, which prevented gradle sync from working.
  27 +* Fixes `MobileScannerController.stop()` throwing when already stopped.
  28 +* Fixes `MobileScannerController.toggleTorch()` throwing if the device has no torch.
  29 + Now it does nothing if the torch is not available.
  30 +* Fixes a memory leak where the `MobileScanner` would keep listening to the barcode events.
  31 +* Fixes the `MobileScanner` preview depending on all attributes of `MediaQueryData`.
  32 + Now it only depends on its layout constraints.
  33 +* Fixed a potential crash when the scanner is restarted due to the app being resumed.
  34 +
  35 +## 3.0.0-beta.2
  36 +Breaking changes:
  37 +* The arguments parameter of onDetect is removed. The data is now returned by the onStart callback
  38 +in the MobileScanner widget.
  39 +* onDetect now returns the object BarcodeCapture, which contains a List of barcodes and, if enabled, an image.
  40 +* allowDuplicates is removed and replaced by MobileScannerSpeed enum.
  41 +* onPermissionSet in MobileScanner widget is deprecated and will be removed. Use the onPermissionSet
  42 +onPermissionSet callback in MobileScannerController instead.
  43 +* [iOS] The minimum deployment target is now 11.0 or higher.
  44 +
  45 +Features:
  46 +* The returnImage is working for both iOS and Android. You can enable it in the MobileScannerController.
  47 +The image will be returned in the BarcodeCapture object provided by onDetect.
  48 +* You can now control the DetectionSpeed, as well as the timeout of the DetectionSpeed. For more
  49 +info see the DetectionSpeed documentation. This replaces the allowDuplicates function.
  50 +
  51 +Other improvements:
  52 +* Both the [iOS] and [Android] codebases have been refactored completely.
  53 +* [iOS] Updated POD dependencies
  54 +
  55 +## 3.0.0-beta.1
  56 +Breaking changes:
  57 +* [Android] SDK updated to SDK 33.
  58 +
  59 +Features:
  60 +* [Web] Add binaryData for raw value.
  61 +* [iOS] Captures the last scanned barcode with Barcode.image.
  62 +* [iOS] Add support for multiple formats on iOS with BarcodeScannerOptions.
  63 +* Add displayValue which returns barcode value in a user-friendly format.
  64 +* Add autoResume option to MobileScannerController which automatically resumes the camera when the application is resumed
  65 +
  66 +Other changes:
  67 +* [Android] Revert camera2 dependency to stable release
  68 +* [iOS] Update barcode scanning library to latest version
  69 +* Several minor code improvements
  70 +
1 ## 2.0.0 71 ## 2.0.0
2 Breaking changes: 72 Breaking changes:
3 This version is only compatible with flutter 3.0.0 and later. 73 This version is only compatible with flutter 3.0.0 and later.
@@ -37,16 +37,17 @@ NSPhotoLibraryUsageDescription - describe why your app needs permission for the @@ -37,16 +37,17 @@ NSPhotoLibraryUsageDescription - describe why your app needs permission for the
37 ### macOS 37 ### macOS
38 macOS 10.13 or newer. Reason: Apple Vision library. 38 macOS 10.13 or newer. Reason: Apple Vision library.
39 39
  40 +Ensure that you granted camera permission in XCode -> Signing & Capabilities:
  41 +
  42 +<img width="696" alt="Screenshot of XCode where Camera is checked" src="https://user-images.githubusercontent.com/24459435/193464115-d76f81d0-6355-4cb2-8bee-538e413a3ad0.png">
  43 +
40 ### Web 44 ### Web
41 Add this to `web/index.html`: 45 Add this to `web/index.html`:
42 46
43 ```html 47 ```html
44 -<script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js"></script> 48 +<script type="text/javascript" src="https://unpkg.com/@zxing/library@0.19.1"></script>
45 ``` 49 ```
46 50
47 -Web only supports QR codes for now.  
48 -Do you have experience with Flutter Web development? [Help me with migrating from jsQR to qr-scanner for full barcode support!](https://github.com/juliansteenbakker/mobile_scanner/issues/54)  
49 -  
50 ## Features Supported 51 ## Features Supported
51 52
52 | Features | Android | iOS | macOS | Web | 53 | Features | Android | iOS | macOS | Web |
@@ -71,15 +72,15 @@ import 'package:mobile_scanner/mobile_scanner.dart'; @@ -71,15 +72,15 @@ import 'package:mobile_scanner/mobile_scanner.dart';
71 return Scaffold( 72 return Scaffold(
72 appBar: AppBar(title: const Text('Mobile Scanner')), 73 appBar: AppBar(title: const Text('Mobile Scanner')),
73 body: MobileScanner( 74 body: MobileScanner(
74 - allowDuplicates: false,  
75 - onDetect: (barcode, args) {  
76 - if (barcode.rawValue == null) {  
77 - debugPrint('Failed to scan Barcode');  
78 - } else {  
79 - final String code = barcode.rawValue!;  
80 - debugPrint('Barcode found! $code'); 75 + // fit: BoxFit.contain,
  76 + onDetect: (capture) {
  77 + final List<Barcode> barcodes = capture.barcodes;
  78 + final Uint8List? image = capture.image;
  79 + for (final barcode in barcodes) {
  80 + debugPrint('Barcode found! ${barcode.rawValue}');
81 } 81 }
82 - }), 82 + },
  83 + ),
83 ); 84 );
84 } 85 }
85 ``` 86 ```
@@ -94,17 +95,18 @@ import 'package:mobile_scanner/mobile_scanner.dart'; @@ -94,17 +95,18 @@ import 'package:mobile_scanner/mobile_scanner.dart';
94 return Scaffold( 95 return Scaffold(
95 appBar: AppBar(title: const Text('Mobile Scanner')), 96 appBar: AppBar(title: const Text('Mobile Scanner')),
96 body: MobileScanner( 97 body: MobileScanner(
97 - allowDuplicates: false, 98 + // fit: BoxFit.contain,
98 controller: MobileScannerController( 99 controller: MobileScannerController(
99 - facing: CameraFacing.front, torchEnabled: true),  
100 - onDetect: (barcode, args) {  
101 - if (barcode.rawValue == null) {  
102 - debugPrint('Failed to scan Barcode');  
103 - } else {  
104 - final String code = barcode.rawValue!;  
105 - debugPrint('Barcode found! $code'); 100 + facing: CameraFacing.front, torchEnabled: true,
  101 + ),
  102 + onDetect: (capture) {
  103 + final List<Barcode> barcodes = capture.barcodes;
  104 + final Uint8List? image = capture.image;
  105 + for (final barcode in barcodes) {
  106 + debugPrint('Barcode found! ${barcode.rawValue}');
106 } 107 }
107 - }), 108 + },
  109 + ),
108 ); 110 );
109 } 111 }
110 ``` 112 ```
@@ -157,15 +159,82 @@ import 'package:mobile_scanner/mobile_scanner.dart'; @@ -157,15 +159,82 @@ import 'package:mobile_scanner/mobile_scanner.dart';
157 ], 159 ],
158 ), 160 ),
159 body: MobileScanner( 161 body: MobileScanner(
160 - allowDuplicates: false, 162 + // fit: BoxFit.contain,
161 controller: cameraController, 163 controller: cameraController,
162 - onDetect: (barcode, args) {  
163 - if (barcode.rawValue == null) {  
164 - debugPrint('Failed to scan Barcode');  
165 - } else {  
166 - final String code = barcode.rawValue!;  
167 - debugPrint('Barcode found! $code'); 164 + onDetect: (capture) {
  165 + final List<Barcode> barcodes = capture.barcodes;
  166 + final Uint8List? image = capture.image;
  167 + for (final barcode in barcodes) {
  168 + debugPrint('Barcode found! ${barcode.rawValue}');
  169 + }
  170 + },
  171 + ),
  172 + );
  173 + }
  174 +```
  175 +
  176 +Example with controller and returning images
  177 +
  178 +```dart
  179 +import 'package:mobile_scanner/mobile_scanner.dart';
  180 +
  181 + @override
  182 + Widget build(BuildContext context) {
  183 + return Scaffold(
  184 + appBar: AppBar(title: const Text('Mobile Scanner')),
  185 + body: MobileScanner(
  186 + fit: BoxFit.contain,
  187 + controller: MobileScannerController(
  188 + // facing: CameraFacing.back,
  189 + // torchEnabled: false,
  190 + returnImage: true,
  191 + ),
  192 + onDetect: (capture) {
  193 + final List<Barcode> barcodes = capture.barcodes;
  194 + final Uint8List? image = capture.image;
  195 + for (final barcode in barcodes) {
  196 + debugPrint('Barcode found! ${barcode.rawValue}');
  197 + }
  198 + if (image != null) {
  199 + showDialog(
  200 + context: context,
  201 + builder: (context) =>
  202 + Image(image: MemoryImage(image)),
  203 + );
  204 + Future.delayed(const Duration(seconds: 5), () {
  205 + Navigator.pop(context);
  206 + });
168 } 207 }
169 - })); 208 + },
  209 + ),
  210 + );
170 } 211 }
171 ``` 212 ```
  213 +
  214 +### BarcodeCapture
  215 +
  216 +The onDetect function returns a BarcodeCapture objects which contains the following items.
  217 +
  218 +| Property name | Type | Description |
  219 +|---------------|---------------|-----------------------------------|
  220 +| barcodes | List<Barcode> | A list with scanned barcodes. |
  221 +| image | Uint8List? | If enabled, an image of the scan. |
  222 +
  223 +You can use the following properties of the Barcode object.
  224 +
  225 +| Property name | Type | Description |
  226 +|---------------|----------------|-------------------------------------|
  227 +| format | BarcodeFormat | |
  228 +| rawBytes | Uint8List? | binary scan result |
  229 +| rawValue | String? | Value if barcode is in UTF-8 format |
  230 +| displayValue | String? | |
  231 +| type | BarcodeType | |
  232 +| calendarEvent | CalendarEvent? | |
  233 +| contactInfo | ContactInfo? | |
  234 +| driverLicense | DriverLicense? | |
  235 +| email | Email? | |
  236 +| geoPoint | GeoPoint? | |
  237 +| phone | Phone? | |
  238 +| sms | SMS? | |
  239 +| url | UrlBookmark? | |
  240 +| wifi | WiFi? | WiFi Access-Point details |
@@ -2,14 +2,15 @@ group 'dev.steenbakker.mobile_scanner' @@ -2,14 +2,15 @@ group 'dev.steenbakker.mobile_scanner'
2 version '1.0-SNAPSHOT' 2 version '1.0-SNAPSHOT'
3 3
4 buildscript { 4 buildscript {
5 - ext.kotlin_version = '1.7.10' 5 + ext.kotlin_version = '1.7.22'
6 repositories { 6 repositories {
7 google() 7 google()
8 mavenCentral() 8 mavenCentral()
9 } 9 }
10 10
11 dependencies { 11 dependencies {
12 - classpath 'com.android.tools.build:gradle:7.2.1' 12 + classpath 'com.android.tools.build:gradle:7.3.1'
  13 + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
13 } 14 }
14 } 15 }
15 16
@@ -24,7 +25,7 @@ apply plugin: 'com.android.library' @@ -24,7 +25,7 @@ apply plugin: 'com.android.library'
24 apply plugin: 'kotlin-android' 25 apply plugin: 'kotlin-android'
25 26
26 android { 27 android {
27 - compileSdkVersion 32 28 + compileSdkVersion 33
28 29
29 compileOptions { 30 compileOptions {
30 sourceCompatibility JavaVersion.VERSION_1_8 31 sourceCompatibility JavaVersion.VERSION_1_8
@@ -50,18 +51,8 @@ dependencies { @@ -50,18 +51,8 @@ dependencies {
50 // Use this dependency to bundle the model with your app 51 // Use this dependency to bundle the model with your app
51 implementation 'com.google.mlkit:barcode-scanning:17.0.2' 52 implementation 'com.google.mlkit:barcode-scanning:17.0.2'
52 // Use this dependency to use the dynamically downloaded model in Google Play Services 53 // Use this dependency to use the dynamically downloaded model in Google Play Services
53 -// implementation 'com.google.android.gms:play-services-mlkit-barcode-scanning:18.0.0' 54 +// implementation 'com.google.android.gms:play-services-mlkit-barcode-scanning:18.1.0'
54 55
55 - implementation "androidx.camera:camera-camera2:1.2.0-alpha04"  
56 - implementation 'androidx.camera:camera-lifecycle:1.2.0-alpha04'  
57 -  
58 -// // The following line is optional, as the core library is included indirectly by camera-camera2  
59 -// implementation "androidx.camera:camera-core:1.1.0-alpha11"  
60 -// implementation "androidx.camera:camera-camera2:1.1.0-alpha11"  
61 -// // If you want to additionally use the CameraX Lifecycle library  
62 -// implementation "androidx.camera:camera-lifecycle:1.1.0-alpha11"  
63 -// // If you want to additionally use the CameraX View class  
64 -// implementation "androidx.camera:camera-view:1.0.0-alpha31"  
65 -// // If you want to additionally use the CameraX Extensions library  
66 -// implementation "androidx.camera:camera-extensions:1.0.0-alpha31" 56 + implementation 'androidx.camera:camera-camera2:1.1.0'
  57 + implementation 'androidx.camera:camera-lifecycle:1.1.0'
67 } 58 }
  1 +#Fri Jun 23 08:50:38 CEST 2017
  2 +distributionBase=GRADLE_USER_HOME
  3 +distributionPath=wrapper/dists
  4 +zipStoreBase=GRADLE_USER_HOME
  5 +zipStorePath=wrapper/dists
  6 +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
1 -package dev.steenbakker.mobile_scanner  
2 -  
3 -import androidx.annotation.IntDef  
4 -  
5 -@IntDef(AnalyzeMode.NONE, AnalyzeMode.BARCODE)  
6 -@Target(AnnotationTarget.FIELD)  
7 -@Retention(AnnotationRetention.SOURCE)  
8 -annotation class AnalyzeMode {  
9 - companion object {  
10 - const val NONE = 0  
11 - const val BARCODE = 1  
12 - }  
13 -}  
  1 +package dev.steenbakker.mobile_scanner
  2 +
  3 +import android.os.Handler
  4 +import android.os.Looper
  5 +import io.flutter.embedding.engine.plugins.FlutterPlugin
  6 +import io.flutter.plugin.common.EventChannel
  7 +
  8 +class BarcodeHandler(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) : EventChannel.StreamHandler {
  9 +
  10 + private var eventSink: EventChannel.EventSink? = null
  11 +
  12 + private val eventChannel = EventChannel(
  13 + flutterPluginBinding.binaryMessenger,
  14 + "dev.steenbakker.mobile_scanner/scanner/event"
  15 + )
  16 +
  17 + init {
  18 + eventChannel.setStreamHandler(this)
  19 + }
  20 +
  21 + fun publishEvent(event: Map<String, Any>) {
  22 + Handler(Looper.getMainLooper()).post {
  23 + eventSink?.success(event)
  24 + }
  25 + }
  26 +
  27 + override fun onListen(event: Any?, eventSink: EventChannel.EventSink?) {
  28 + this.eventSink = eventSink
  29 + }
  30 +
  31 + override fun onCancel(event: Any?) {
  32 + this.eventSink = null
  33 + }
  34 +}
@@ -7,12 +7,9 @@ import android.graphics.Point @@ -7,12 +7,9 @@ import android.graphics.Point
7 import android.graphics.Rect 7 import android.graphics.Rect
8 import android.graphics.RectF 8 import android.graphics.RectF
9 import android.net.Uri 9 import android.net.Uri
10 -import android.util.Log  
11 -import android.util.Size  
12 -import android.util.Rational  
13 -import android.media.Image 10 +import android.os.Handler
  11 +import android.os.Looper
14 import android.view.Surface 12 import android.view.Surface
15 -import androidx.annotation.NonNull  
16 import androidx.camera.core.* 13 import androidx.camera.core.*
17 import androidx.camera.lifecycle.ProcessCameraProvider 14 import androidx.camera.lifecycle.ProcessCameraProvider
18 import androidx.core.app.ActivityCompat 15 import androidx.core.app.ActivityCompat
@@ -20,117 +17,160 @@ import androidx.core.content.ContextCompat @@ -20,117 +17,160 @@ import androidx.core.content.ContextCompat
20 import androidx.lifecycle.LifecycleOwner 17 import androidx.lifecycle.LifecycleOwner
21 import com.google.mlkit.vision.barcode.BarcodeScannerOptions 18 import com.google.mlkit.vision.barcode.BarcodeScannerOptions
22 import com.google.mlkit.vision.barcode.BarcodeScanning 19 import com.google.mlkit.vision.barcode.BarcodeScanning
23 -import com.google.mlkit.vision.barcode.common.Barcode  
24 import com.google.mlkit.vision.common.InputImage 20 import com.google.mlkit.vision.common.InputImage
25 -import com.google.mlkit.vision.common.InputImage.IMAGE_FORMAT_NV21  
26 -import io.flutter.plugin.common.EventChannel  
27 -import io.flutter.plugin.common.MethodCall 21 +import dev.steenbakker.mobile_scanner.objects.DetectionSpeed
  22 +import dev.steenbakker.mobile_scanner.objects.MobileScannerStartParameters
28 import io.flutter.plugin.common.MethodChannel 23 import io.flutter.plugin.common.MethodChannel
29 import io.flutter.plugin.common.PluginRegistry 24 import io.flutter.plugin.common.PluginRegistry
30 import io.flutter.view.TextureRegistry 25 import io.flutter.view.TextureRegistry
  26 +typealias MobileScannerCallback = (barcodes: List<Map<String, Any?>>, image: ByteArray?) -> Unit
  27 +typealias AnalyzerCallback = (barcodes: List<Map<String, Any?>>?) -> Unit
  28 +typealias MobileScannerErrorCallback = (error: String) -> Unit
  29 +typealias TorchStateCallback = (state: Int) -> Unit
  30 +typealias MobileScannerStartedCallback = (parameters: MobileScannerStartParameters) -> Unit
31 import java.io.File 31 import java.io.File
32 import kotlin.math.roundToInt 32 import kotlin.math.roundToInt
33 33
34 -  
35 -class MobileScanner(private val activity: Activity, private val textureRegistry: TextureRegistry)  
36 - : MethodChannel.MethodCallHandler, EventChannel.StreamHandler, PluginRegistry.RequestPermissionsResultListener { 34 +class NoCamera : Exception()
  35 +class AlreadyStarted : Exception()
  36 +class AlreadyStopped : Exception()
  37 +class TorchError : Exception()
  38 +class CameraError : Exception()
  39 +class TorchWhenStopped : Exception()
  40 +
  41 +class MobileScanner(
  42 + private val activity: Activity,
  43 + private val textureRegistry: TextureRegistry,
  44 + private val mobileScannerCallback: MobileScannerCallback,
  45 + private val mobileScannerErrorCallback: MobileScannerErrorCallback
  46 +) :
  47 + PluginRegistry.RequestPermissionsResultListener {
37 companion object { 48 companion object {
38 - private const val REQUEST_CODE = 22022022  
39 - private val TAG = MobileScanner::class.java.simpleName 49 + /**
  50 + * When the application's activity is [androidx.fragment.app.FragmentActivity], requestCode can only use the lower 16 bits.
  51 + * @see androidx.fragment.app.FragmentActivity.validateRequestPermissionsRequestCode
  52 + */
  53 + private const val REQUEST_CODE = 0x0786
40 } 54 }
41 55
42 - private var sink: EventChannel.EventSink? = null  
43 - private var listener: PluginRegistry.RequestPermissionsResultListener? = null  
44 -  
45 private var cameraProvider: ProcessCameraProvider? = null 56 private var cameraProvider: ProcessCameraProvider? = null
46 private var camera: Camera? = null 57 private var camera: Camera? = null
  58 + private var pendingPermissionResult: MethodChannel.Result? = null
47 private var preview: Preview? = null 59 private var preview: Preview? = null
48 private var textureEntry: TextureRegistry.SurfaceTextureEntry? = null 60 private var textureEntry: TextureRegistry.SurfaceTextureEntry? = null
49 private var scanWindow: List<Float>? = null; 61 private var scanWindow: List<Float>? = null;
50 62
51 -// @AnalyzeMode  
52 -// private var analyzeMode: Int = AnalyzeMode.NONE 63 + private var detectionSpeed: DetectionSpeed = DetectionSpeed.NO_DUPLICATES
  64 + private var detectionTimeout: Long = 250
  65 + private var lastScanned: List<String?>? = null
53 66
54 - @ExperimentalGetImage  
55 - override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: MethodChannel.Result) {  
56 - when (call.method) {  
57 - "state" -> checkPermission(result)  
58 - "request" -> requestPermission(result)  
59 - "start" -> start(call, result)  
60 - "torch" -> toggleTorch(call, result)  
61 -// "analyze" -> switchAnalyzeMode(call, result)  
62 - "stop" -> stop(result)  
63 - "analyzeImage" -> analyzeImage(call, result)  
64 - "updateScanWindow" -> updateScanWindow(call)  
65 - else -> result.notImplemented()  
66 - }  
67 - } 67 + private var scannerTimeout = false
68 68
69 - override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {  
70 - this.sink = events  
71 - } 69 + private var returnImage = false
72 70
73 - override fun onCancel(arguments: Any?) {  
74 - sink = null  
75 - }  
76 -  
77 - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray): Boolean {  
78 - return listener?.onRequestPermissionsResult(requestCode, permissions, grantResults) ?: false  
79 - } 71 + private var scanner = BarcodeScanning.getClient()
80 72
81 - private fun checkPermission(result: MethodChannel.Result) { 73 + /**
  74 + * Check if we already have camera permission.
  75 + */
  76 + fun hasCameraPermission(): Int {
82 // Can't get exact denied or not_determined state without request. Just return not_determined when state isn't authorized 77 // Can't get exact denied or not_determined state without request. Just return not_determined when state isn't authorized
83 - val state =  
84 - if (ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) 1  
85 - else 0  
86 - result.success(state)  
87 - } 78 + val hasPermission = ContextCompat.checkSelfPermission(
  79 + activity,
  80 + Manifest.permission.CAMERA
  81 + ) == PackageManager.PERMISSION_GRANTED
88 82
89 - private fun requestPermission(result: MethodChannel.Result) {  
90 - listener = PluginRegistry.RequestPermissionsResultListener { requestCode, _, grantResults ->  
91 - if (requestCode != REQUEST_CODE) {  
92 - false 83 + return if (hasPermission) {
  84 + 1
93 } else { 85 } else {
94 - val authorized = grantResults[0] == PackageManager.PERMISSION_GRANTED  
95 - result.success(authorized)  
96 - listener = null  
97 - true 86 + 0
  87 + }
98 } 88 }
  89 +
  90 + /**
  91 + * Request camera permissions.
  92 + */
  93 + fun requestPermission(result: MethodChannel.Result) {
  94 + if(pendingPermissionResult != null) {
  95 + return
99 } 96 }
  97 +
  98 + pendingPermissionResult = result
100 val permissions = arrayOf(Manifest.permission.CAMERA) 99 val permissions = arrayOf(Manifest.permission.CAMERA)
101 ActivityCompat.requestPermissions(activity, permissions, REQUEST_CODE) 100 ActivityCompat.requestPermissions(activity, permissions, REQUEST_CODE)
102 } 101 }
103 102
  103 + /**
  104 + * Calls the callback after permissions are requested.
  105 + */
  106 + override fun onRequestPermissionsResult(
  107 + requestCode: Int,
  108 + permissions: Array<out String>,
  109 + grantResults: IntArray
  110 + ): Boolean {
  111 + if (requestCode != REQUEST_CODE) {
  112 + return false
  113 + }
  114 +
  115 + pendingPermissionResult?.success(grantResults[0] == PackageManager.PERMISSION_GRANTED)
  116 + pendingPermissionResult = null
104 117
  118 + return true
  119 + }
  120 +
  121 + /**
  122 + * callback for the camera. Every frame is passed through this function.
  123 + */
105 @ExperimentalGetImage 124 @ExperimentalGetImage
106 - val analyzer = ImageAnalysis.Analyzer { imageProxy -> // YUV_420_888 format 125 + val captureOutput = ImageAnalysis.Analyzer { imageProxy -> // YUV_420_888 format
107 val mediaImage = imageProxy.image ?: return@Analyzer 126 val mediaImage = imageProxy.image ?: return@Analyzer
108 - var inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees) 127 + val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
  128 +
  129 + if (detectionSpeed == DetectionSpeed.NORMAL && scannerTimeout) {
  130 + imageProxy.close()
  131 + return@Analyzer
  132 + } else if (detectionSpeed == DetectionSpeed.NORMAL) {
  133 + scannerTimeout = true
  134 + }
109 135
110 scanner.process(inputImage) 136 scanner.process(inputImage)
111 .addOnSuccessListener { barcodes -> 137 .addOnSuccessListener { barcodes ->
112 - for (barcode in barcodes) {  
113 - print("image: ")  
114 - println(inputImage.getWidth());  
115 - println(inputImage.getHeight()); 138 + if (detectionSpeed == DetectionSpeed.NO_DUPLICATES) {
  139 + val newScannedBarcodes = barcodes.map { barcode -> barcode.rawValue }
  140 + if (newScannedBarcodes == lastScanned) {
  141 + // New scanned is duplicate, returning
  142 + return@addOnSuccessListener
  143 + }
  144 + lastScanned = newScannedBarcodes
  145 + }
116 146
117 - print("barcode: ")  
118 - println(barcode.getBoundingBox()); 147 + val barcodeMap = barcodes.map { barcode -> barcode.data }
119 148
120 if(scanWindow != null) { 149 if(scanWindow != null) {
121 val match = isbarCodeInScanWindow(scanWindow!!, barcode, imageProxy) 150 val match = isbarCodeInScanWindow(scanWindow!!, barcode, imageProxy)
122 if(!match) continue 151 if(!match) continue
123 } 152 }
124 -  
125 - val event = mapOf("name" to "barcode", "data" to barcode.data)  
126 - sink?.success(event) 153 + if (barcodeMap.isNotEmpty()) {
  154 + mobileScannerCallback(
  155 + barcodeMap,
  156 + if (returnImage) mediaImage.toByteArray() else null
  157 + )
127 } 158 }
128 } 159 }
129 - .addOnFailureListener { e -> Log.e(TAG, e.message, e) }  
130 - .addOnCompleteListener { imageProxy.close() } 160 + .addOnFailureListener { e ->
  161 + mobileScannerErrorCallback(
  162 + e.localizedMessage ?: e.toString()
  163 + )
131 } 164 }
  165 + .addOnCompleteListener { imageProxy.close() }
132 166
133 - private var scanner = BarcodeScanning.getClient() 167 + if (detectionSpeed == DetectionSpeed.NORMAL) {
  168 + // Set timer and continue
  169 + Handler(Looper.getMainLooper()).postDelayed({
  170 + scannerTimeout = false
  171 + }, detectionTimeout)
  172 + }
  173 + }
134 174
135 private fun updateScanWindow(call: MethodCall) { 175 private fun updateScanWindow(call: MethodCall) {
136 scanWindow = call.argument<List<Float>>("rect") 176 scanWindow = call.argument<List<Float>>("rect")
@@ -157,100 +197,85 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: @@ -157,100 +197,85 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
157 return scaledScanWindow.contains(barcodeBoundingBox) 197 return scaledScanWindow.contains(barcodeBoundingBox)
158 } 198 }
159 199
160 -  
161 - 200 + /**
  201 + * Start barcode scanning by initializing the camera and barcode scanner.
  202 + */
162 @ExperimentalGetImage 203 @ExperimentalGetImage
163 - private fun start(call: MethodCall, result: MethodChannel.Result) { 204 + fun start(
  205 + barcodeScannerOptions: BarcodeScannerOptions?,
  206 + returnImage: Boolean,
  207 + cameraPosition: CameraSelector,
  208 + torch: Boolean,
  209 + detectionSpeed: DetectionSpeed,
  210 + torchStateCallback: TorchStateCallback,
  211 + mobileScannerStartedCallback: MobileScannerStartedCallback,
  212 + detectionTimeout: Long
  213 + ) {
  214 + this.detectionSpeed = detectionSpeed
  215 + this.detectionTimeout = detectionTimeout
  216 + this.returnImage = returnImage
  217 +
164 if (camera?.cameraInfo != null && preview != null && textureEntry != null) { 218 if (camera?.cameraInfo != null && preview != null && textureEntry != null) {
165 - val resolution = preview!!.resolutionInfo!!.resolution  
166 - val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0  
167 - val width = resolution.width.toDouble()  
168 - val height = resolution.height.toDouble()  
169 - val size = if (portrait) mapOf("width" to width, "height" to height) else mapOf("width" to height, "height" to width)  
170 - val answer = mapOf("textureId" to textureEntry!!.id(), "size" to size, "torchable" to camera!!.cameraInfo.hasFlashUnit())  
171 - result.success(answer)  
172 - } else {  
173 - val facing: Int = call.argument<Int>("facing") ?: 0  
174 - val ratio: Int = call.argument<Int>("ratio") ?: 1  
175 - val torch: Boolean = call.argument<Boolean>("torch") ?: false  
176 - val formats: List<Int>? = call.argument<List<Int>>("formats")  
177 -  
178 - if (formats != null) {  
179 - val formatsList: MutableList<Int> = mutableListOf()  
180 - for (index in formats) {  
181 - formatsList.add(BarcodeFormats.values()[index].intValue)  
182 - }  
183 - scanner = if (formatsList.size == 1) {  
184 - BarcodeScanning.getClient(BarcodeScannerOptions.Builder().setBarcodeFormats(formatsList.first()).build())  
185 - } else {  
186 - BarcodeScanning.getClient(BarcodeScannerOptions.Builder().setBarcodeFormats(formatsList.first(), *formatsList.subList(1, formatsList.size).toIntArray()).build()) 219 + throw AlreadyStarted()
187 } 220 }
  221 +
  222 + scanner = if (barcodeScannerOptions != null) {
  223 + BarcodeScanning.getClient(barcodeScannerOptions)
  224 + } else {
  225 + BarcodeScanning.getClient()
188 } 226 }
189 227
190 - val future = ProcessCameraProvider.getInstance(activity) 228 + val cameraProviderFuture = ProcessCameraProvider.getInstance(activity)
191 val executor = ContextCompat.getMainExecutor(activity) 229 val executor = ContextCompat.getMainExecutor(activity)
192 230
193 - future.addListener({  
194 - cameraProvider = future.get() 231 + cameraProviderFuture.addListener({
  232 + cameraProvider = cameraProviderFuture.get()
195 if (cameraProvider == null) { 233 if (cameraProvider == null) {
196 - result.error("cameraProvider", "cameraProvider is null", null)  
197 - return@addListener 234 + throw CameraError()
198 } 235 }
199 cameraProvider!!.unbindAll() 236 cameraProvider!!.unbindAll()
200 textureEntry = textureRegistry.createSurfaceTexture() 237 textureEntry = textureRegistry.createSurfaceTexture()
201 - if (textureEntry == null) {  
202 - result.error("textureEntry", "textureEntry is null", null)  
203 - return@addListener  
204 - }  
205 -  
206 - // Select the correct camera  
207 - val selector = if (facing == 0) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA  
208 238
209 // Preview 239 // Preview
210 val surfaceProvider = Preview.SurfaceProvider { request -> 240 val surfaceProvider = Preview.SurfaceProvider { request ->
211 val texture = textureEntry!!.surfaceTexture() 241 val texture = textureEntry!!.surfaceTexture()
212 - texture.setDefaultBufferSize(request.resolution.width, request.resolution.height) 242 + texture.setDefaultBufferSize(
  243 + request.resolution.width,
  244 + request.resolution.height
  245 + )
  246 +
213 val surface = Surface(texture) 247 val surface = Surface(texture)
214 request.provideSurface(surface, executor) { } 248 request.provideSurface(surface, executor) { }
215 } 249 }
216 250
217 // Build the preview to be shown on the Flutter texture 251 // Build the preview to be shown on the Flutter texture
218 - val previewBuilder = Preview.Builder().setTargetAspectRatio(ratio) 252 + val previewBuilder = Preview.Builder()
219 preview = previewBuilder.build().apply { setSurfaceProvider(surfaceProvider) } 253 preview = previewBuilder.build().apply { setSurfaceProvider(surfaceProvider) }
220 254
221 - // bind to lifecycle temporarily to fetch dimensions.  
222 - cameraProvider!!.bindToLifecycle(activity as LifecycleOwner, selector, preview)  
223 - val previewResolution = preview!!.resolutionInfo!!.resolution  
224 - val previewRotation = preview!!.getTargetRotation()  
225 -  
226 // Build the analyzer to be passed on to MLKit 255 // Build the analyzer to be passed on to MLKit
227 val analysisBuilder = ImageAnalysis.Builder() 256 val analysisBuilder = ImageAnalysis.Builder()
228 .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) 257 .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
229 - .setTargetAspectRatio(ratio)  
230 - .setTargetRotation(previewRotation)  
231 - val analysis = analysisBuilder.build().apply { setAnalyzer(executor, analyzer) }  
232 -  
233 - val viewPort = ViewPort.Builder(Rational(previewResolution.width, previewResolution.height), previewRotation).build()  
234 - val useCaseGroup = UseCaseGroup.Builder()  
235 - .setViewPort(viewPort)  
236 - .addUseCase(preview!!)  
237 - .addUseCase(analysis)  
238 - .build() 258 +// analysisBuilder.setTargetResolution(Size(1440, 1920))
  259 + val analysis = analysisBuilder.build().apply { setAnalyzer(executor, captureOutput) }
239 260
240 - cameraProvider!!.unbindAll()  
241 - camera = cameraProvider!!.bindToLifecycle(activity as LifecycleOwner, selector, useCaseGroup)  
242 -  
243 - if (camera == null) {  
244 - result.error("camera", "camera is null", null)  
245 - return@addListener  
246 - } 261 + camera = cameraProvider!!.bindToLifecycle(
  262 + activity as LifecycleOwner,
  263 + cameraPosition,
  264 + preview,
  265 + analysis
  266 + )
247 267
248 // Register the torch listener 268 // Register the torch listener
249 camera!!.cameraInfo.torchState.observe(activity) { state -> 269 camera!!.cameraInfo.torchState.observe(activity) { state ->
250 // TorchState.OFF = 0; TorchState.ON = 1 270 // TorchState.OFF = 0; TorchState.ON = 1
251 - sink?.success(mapOf("name" to "torchState", "data" to state)) 271 + torchStateCallback(state)
252 } 272 }
253 273
  274 +// val analysisSize = analysis.resolutionInfo?.resolution ?: Size(0, 0)
  275 +// val previewSize = preview!!.resolutionInfo?.resolution ?: Size(0, 0)
  276 +// Log.i("LOG", "Analyzer: $analysisSize")
  277 +// Log.i("LOG", "Preview: $previewSize")
  278 +
254 // Enable torch if provided 279 // Enable torch if provided
255 camera!!.cameraControl.enableTorch(torch) 280 camera!!.cameraControl.enableTorch(torch)
256 281
@@ -258,49 +283,25 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: @@ -258,49 +283,25 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
258 val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0 283 val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0
259 val width = resolution.width.toDouble() 284 val width = resolution.width.toDouble()
260 val height = resolution.height.toDouble() 285 val height = resolution.height.toDouble()
261 - val size = if (portrait) mapOf("width" to width, "height" to height) else mapOf("width" to height, "height" to width)  
262 - val answer = mapOf("textureId" to textureEntry!!.id(), "size" to size, "torchable" to camera!!.cameraInfo.hasFlashUnit())  
263 - result.success(answer)  
264 - }, executor)  
265 - }  
266 - }  
267 286
268 - private fun toggleTorch(call: MethodCall, result: MethodChannel.Result) {  
269 - if (camera == null) {  
270 - result.error(TAG,"Called toggleTorch() while stopped!", null)  
271 - return  
272 - }  
273 - camera!!.cameraControl.enableTorch(call.arguments == 1)  
274 - result.success(null)  
275 - }  
276 -  
277 -// private fun switchAnalyzeMode(call: MethodCall, result: MethodChannel.Result) {  
278 -// analyzeMode = call.arguments as Int  
279 -// result.success(null)  
280 -// }  
281 -  
282 - private fun analyzeImage(call: MethodCall, result: MethodChannel.Result) {  
283 - val uri = Uri.fromFile( File(call.arguments.toString()))  
284 - val inputImage = InputImage.fromFilePath(activity, uri)  
285 -  
286 - var barcodeFound = false  
287 - scanner.process(inputImage)  
288 - .addOnSuccessListener { barcodes ->  
289 - for (barcode in barcodes) {  
290 - barcodeFound = true  
291 - sink?.success(mapOf("name" to "barcode", "data" to barcode.data))  
292 - }  
293 - }  
294 - .addOnFailureListener { e -> Log.e(TAG, e.message, e)  
295 - result.error(TAG, e.message, e)}  
296 - .addOnCompleteListener { result.success(barcodeFound) } 287 + mobileScannerStartedCallback(
  288 + MobileScannerStartParameters(
  289 + if (portrait) width else height,
  290 + if (portrait) height else width,
  291 + camera!!.cameraInfo.hasFlashUnit(),
  292 + textureEntry!!.id()
  293 + )
  294 + )
  295 + }, executor)
297 296
298 } 297 }
299 298
300 - private fun stop(result: MethodChannel.Result) { 299 + /**
  300 + * Stop barcode scanning.
  301 + */
  302 + fun stop() {
301 if (camera == null && preview == null) { 303 if (camera == null && preview == null) {
302 - result.error(TAG,"Called stop() while already stopped!", null)  
303 - return 304 + throw AlreadyStopped()
304 } 305 }
305 306
306 val owner = activity as LifecycleOwner 307 val owner = activity as LifecycleOwner
@@ -308,68 +309,43 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: @@ -308,68 +309,43 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
308 cameraProvider?.unbindAll() 309 cameraProvider?.unbindAll()
309 textureEntry?.release() 310 textureEntry?.release()
310 311
311 -// analyzeMode = AnalyzeMode.NONE  
312 camera = null 312 camera = null
313 preview = null 313 preview = null
314 textureEntry = null 314 textureEntry = null
315 cameraProvider = null 315 cameraProvider = null
316 -  
317 - result.success(null)  
318 } 316 }
319 317
  318 + /**
  319 + * Toggles the flash light on or off.
  320 + */
  321 + fun toggleTorch(enableTorch: Boolean) {
  322 + if (camera == null) {
  323 + throw TorchWhenStopped()
  324 + }
  325 + camera!!.cameraControl.enableTorch(enableTorch)
  326 + }
320 327
321 - private val Barcode.data: Map<String, Any?>  
322 - get() = mapOf("corners" to cornerPoints?.map { corner -> corner.data }, "format" to format,  
323 - "rawBytes" to rawBytes, "rawValue" to rawValue, "type" to valueType,  
324 - "calendarEvent" to calendarEvent?.data, "contactInfo" to contactInfo?.data,  
325 - "driverLicense" to driverLicense?.data, "email" to email?.data,  
326 - "geoPoint" to geoPoint?.data, "phone" to phone?.data, "sms" to sms?.data,  
327 - "url" to url?.data, "wifi" to wifi?.data, "displayValue" to displayValue)  
328 -  
329 - private val Point.data: Map<String, Double>  
330 - get() = mapOf("x" to x.toDouble(), "y" to y.toDouble())  
331 -  
332 - private val Barcode.CalendarEvent.data: Map<String, Any?>  
333 - get() = mapOf("description" to description, "end" to end?.rawValue, "location" to location,  
334 - "organizer" to organizer, "start" to start?.rawValue, "status" to status,  
335 - "summary" to summary)  
336 -  
337 - private val Barcode.ContactInfo.data: Map<String, Any?>  
338 - get() = mapOf("addresses" to addresses.map { address -> address.data },  
339 - "emails" to emails.map { email -> email.data }, "name" to name?.data,  
340 - "organization" to organization, "phones" to phones.map { phone -> phone.data },  
341 - "title" to title, "urls" to urls)  
342 -  
343 - private val Barcode.Address.data: Map<String, Any?>  
344 - get() = mapOf("addressLines" to addressLines.map { addressLine -> addressLine.toString() }, "type" to type)  
345 -  
346 - private val Barcode.PersonName.data: Map<String, Any?>  
347 - get() = mapOf("first" to first, "formattedName" to formattedName, "last" to last,  
348 - "middle" to middle, "prefix" to prefix, "pronunciation" to pronunciation,  
349 - "suffix" to suffix)  
350 -  
351 - private val Barcode.DriverLicense.data: Map<String, Any?>  
352 - get() = mapOf("addressCity" to addressCity, "addressState" to addressState,  
353 - "addressStreet" to addressStreet, "addressZip" to addressZip, "birthDate" to birthDate,  
354 - "documentType" to documentType, "expiryDate" to expiryDate, "firstName" to firstName,  
355 - "gender" to gender, "issueDate" to issueDate, "issuingCountry" to issuingCountry,  
356 - "lastName" to lastName, "licenseNumber" to licenseNumber, "middleName" to middleName)  
357 -  
358 - private val Barcode.Email.data: Map<String, Any?>  
359 - get() = mapOf("address" to address, "body" to body, "subject" to subject, "type" to type)  
360 -  
361 - private val Barcode.GeoPoint.data: Map<String, Any?>  
362 - get() = mapOf("latitude" to lat, "longitude" to lng)  
363 -  
364 - private val Barcode.Phone.data: Map<String, Any?>  
365 - get() = mapOf("number" to number, "type" to type) 328 + /**
  329 + * Analyze a single image.
  330 + */
  331 + fun analyzeImage(image: Uri, analyzerCallback: AnalyzerCallback) {
  332 + val inputImage = InputImage.fromFilePath(activity, image)
366 333
367 - private val Barcode.Sms.data: Map<String, Any?>  
368 - get() = mapOf("message" to message, "phoneNumber" to phoneNumber) 334 + scanner.process(inputImage)
  335 + .addOnSuccessListener { barcodes ->
  336 + val barcodeMap = barcodes.map { barcode -> barcode.data }
369 337
370 - private val Barcode.UrlBookmark.data: Map<String, Any?>  
371 - get() = mapOf("title" to title, "url" to url) 338 + if (barcodeMap.isNotEmpty()) {
  339 + analyzerCallback(barcodeMap)
  340 + } else {
  341 + analyzerCallback(null)
  342 + }
  343 + }
  344 + .addOnFailureListener { e ->
  345 + mobileScannerErrorCallback(
  346 + e.localizedMessage ?: e.toString()
  347 + )
  348 + }
  349 + }
372 350
373 - private val Barcode.WiFi.data: Map<String, Any?>  
374 - get() = mapOf("encryptionType" to encryptionType, "password" to password, "ssid" to ssid)  
375 } 351 }
1 package dev.steenbakker.mobile_scanner 1 package dev.steenbakker.mobile_scanner
2 2
3 -import androidx.annotation.NonNull 3 +import android.net.Uri
  4 +import androidx.camera.core.CameraSelector
  5 +import androidx.camera.core.ExperimentalGetImage
  6 +import com.google.mlkit.vision.barcode.BarcodeScannerOptions
  7 +import dev.steenbakker.mobile_scanner.objects.BarcodeFormats
  8 +import dev.steenbakker.mobile_scanner.objects.DetectionSpeed
4 import io.flutter.embedding.engine.plugins.FlutterPlugin 9 import io.flutter.embedding.engine.plugins.FlutterPlugin
5 import io.flutter.embedding.engine.plugins.activity.ActivityAware 10 import io.flutter.embedding.engine.plugins.activity.ActivityAware
6 import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding 11 import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
7 -import io.flutter.plugin.common.EventChannel 12 +import io.flutter.plugin.common.MethodCall
8 import io.flutter.plugin.common.MethodChannel 13 import io.flutter.plugin.common.MethodChannel
  14 +import java.io.File
9 15
10 /** MobileScannerPlugin */ 16 /** MobileScannerPlugin */
11 -class MobileScannerPlugin : FlutterPlugin, ActivityAware {  
12 - private var flutter: FlutterPlugin.FlutterPluginBinding? = null  
13 - private var activity: ActivityPluginBinding? = null 17 +class MobileScannerPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCallHandler {
  18 +
  19 + private var flutterPluginBinding: FlutterPlugin.FlutterPluginBinding? = null
  20 + private var activityPluginBinding: ActivityPluginBinding? = null
14 private var handler: MobileScanner? = null 21 private var handler: MobileScanner? = null
15 private var method: MethodChannel? = null 22 private var method: MethodChannel? = null
16 - private var event: EventChannel? = null  
17 23
18 - override fun onAttachedToEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {  
19 - this.flutter = binding 24 + private lateinit var barcodeHandler: BarcodeHandler
  25 +
  26 + private var analyzerResult: MethodChannel.Result? = null
  27 +
  28 + private val callback: MobileScannerCallback = { barcodes: List<Map<String, Any?>>, image: ByteArray? ->
  29 + if (image != null) {
  30 + barcodeHandler.publishEvent(mapOf(
  31 + "name" to "barcode",
  32 + "data" to barcodes,
  33 + "image" to image
  34 + ))
  35 + } else {
  36 + barcodeHandler.publishEvent(mapOf(
  37 + "name" to "barcode",
  38 + "data" to barcodes
  39 + ))
  40 + }
  41 + }
  42 +
  43 + private val analyzerCallback: AnalyzerCallback = { barcodes: List<Map<String, Any?>>?->
  44 + if (barcodes != null) {
  45 + barcodeHandler.publishEvent(mapOf(
  46 + "name" to "barcode",
  47 + "data" to barcodes
  48 + ))
  49 + analyzerResult?.success(true)
  50 + } else {
  51 + analyzerResult?.success(false)
  52 + }
  53 + analyzerResult = null
  54 + }
  55 +
  56 + private val errorCallback: MobileScannerErrorCallback = {error: String ->
  57 + barcodeHandler.publishEvent(mapOf(
  58 + "name" to "error",
  59 + "data" to error,
  60 + ))
  61 + }
  62 +
  63 + private val torchStateCallback: TorchStateCallback = {state: Int ->
  64 + barcodeHandler.publishEvent(mapOf("name" to "torchState", "data" to state))
  65 + }
  66 +
  67 + @ExperimentalGetImage
  68 + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
  69 + if (handler == null) {
  70 + result.error("MobileScanner", "Called ${call.method} before initializing.", null)
  71 + return
  72 + }
  73 + when (call.method) {
  74 + "state" -> result.success(handler!!.hasCameraPermission())
  75 + "request" -> handler!!.requestPermission(result)
  76 + "start" -> start(call, result)
  77 + "torch" -> toggleTorch(call, result)
  78 + "stop" -> stop(result)
  79 + "analyzeImage" -> analyzeImage(call, result)
  80 + else -> result.notImplemented()
  81 + }
  82 + }
  83 +
  84 + override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
  85 + method = MethodChannel(binding.binaryMessenger, "dev.steenbakker.mobile_scanner/scanner/method")
  86 + method!!.setMethodCallHandler(this)
  87 +
  88 + barcodeHandler = BarcodeHandler(binding)
  89 +
  90 + this.flutterPluginBinding = binding
20 } 91 }
21 92
22 - override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {  
23 - this.flutter = null 93 + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
  94 + this.flutterPluginBinding = null
24 } 95 }
25 96
26 - override fun onAttachedToActivity(binding: ActivityPluginBinding) {  
27 - activity = binding  
28 - handler = MobileScanner(activity!!.activity, flutter!!.textureRegistry)  
29 - method = MethodChannel(flutter!!.binaryMessenger, "dev.steenbakker.mobile_scanner/scanner/method")  
30 - event = EventChannel(flutter!!.binaryMessenger, "dev.steenbakker.mobile_scanner/scanner/event")  
31 - method!!.setMethodCallHandler(handler)  
32 - event!!.setStreamHandler(handler)  
33 - activity!!.addRequestPermissionsResultListener(handler!!) 97 + override fun onAttachedToActivity(activityPluginBinding: ActivityPluginBinding) {
  98 + handler = MobileScanner(activityPluginBinding.activity, flutterPluginBinding!!.textureRegistry, callback, errorCallback
  99 + )
  100 + activityPluginBinding.addRequestPermissionsResultListener(handler!!)
  101 +
  102 + this.activityPluginBinding = activityPluginBinding
34 } 103 }
35 104
36 override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { 105 override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
@@ -38,16 +107,112 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware { @@ -38,16 +107,112 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware {
38 } 107 }
39 108
40 override fun onDetachedFromActivity() { 109 override fun onDetachedFromActivity() {
41 - activity!!.removeRequestPermissionsResultListener(handler!!)  
42 - event!!.setStreamHandler(null) 110 + activityPluginBinding!!.removeRequestPermissionsResultListener(handler!!)
43 method!!.setMethodCallHandler(null) 111 method!!.setMethodCallHandler(null)
44 - event = null  
45 method = null 112 method = null
46 handler = null 113 handler = null
47 - activity = null 114 + activityPluginBinding = null
48 } 115 }
49 116
50 override fun onDetachedFromActivityForConfigChanges() { 117 override fun onDetachedFromActivityForConfigChanges() {
51 onDetachedFromActivity() 118 onDetachedFromActivity()
52 } 119 }
  120 +
  121 + @ExperimentalGetImage
  122 + private fun start(call: MethodCall, result: MethodChannel.Result) {
  123 + val torch: Boolean = call.argument<Boolean>("torch") ?: false
  124 + val facing: Int = call.argument<Int>("facing") ?: 0
  125 + val formats: List<Int>? = call.argument<List<Int>>("formats")
  126 + val returnImage: Boolean = call.argument<Boolean>("returnImage") ?: false
  127 + val speed: Int = call.argument<Int>("speed") ?: 1
  128 + val timeout: Int = call.argument<Int>("timeout") ?: 250
  129 +
  130 + var barcodeScannerOptions: BarcodeScannerOptions? = null
  131 + if (formats != null) {
  132 + val formatsList: MutableList<Int> = mutableListOf()
  133 + for (index in formats) {
  134 + formatsList.add(BarcodeFormats.values()[index].intValue)
  135 + }
  136 + barcodeScannerOptions = if (formatsList.size == 1) {
  137 + BarcodeScannerOptions.Builder().setBarcodeFormats(formatsList.first())
  138 + .build()
  139 + } else {
  140 + BarcodeScannerOptions.Builder().setBarcodeFormats(
  141 + formatsList.first(),
  142 + *formatsList.subList(1, formatsList.size).toIntArray()
  143 + ).build()
  144 + }
  145 + }
  146 +
  147 + val position =
  148 + if (facing == 0) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA
  149 +
  150 + val detectionSpeed: DetectionSpeed = DetectionSpeed.values().first { it.intValue == speed}
  151 +
  152 + try {
  153 + handler!!.start(barcodeScannerOptions, returnImage, position, torch, detectionSpeed, torchStateCallback, mobileScannerStartedCallback = {
  154 + result.success(mapOf(
  155 + "textureId" to it.id,
  156 + "size" to mapOf("width" to it.width, "height" to it.height),
  157 + "torchable" to it.hasFlashUnit
  158 + ))
  159 + },
  160 + timeout.toLong())
  161 +
  162 + } catch (e: AlreadyStarted) {
  163 + result.error(
  164 + "MobileScanner",
  165 + "Called start() while already started",
  166 + null
  167 + )
  168 + } catch (e: NoCamera) {
  169 + result.error(
  170 + "MobileScanner",
  171 + "No camera found or failed to open camera!",
  172 + null
  173 + )
  174 + } catch (e: TorchError) {
  175 + result.error(
  176 + "MobileScanner",
  177 + "Error occurred when setting torch!",
  178 + null
  179 + )
  180 + } catch (e: CameraError) {
  181 + result.error(
  182 + "MobileScanner",
  183 + "Error occurred when setting up camera!",
  184 + null
  185 + )
  186 + } catch (e: Exception) {
  187 + result.error(
  188 + "MobileScanner",
  189 + "Unknown error occurred..",
  190 + null
  191 + )
  192 + }
  193 + }
  194 +
  195 + private fun stop(result: MethodChannel.Result) {
  196 + try {
  197 + handler!!.stop()
  198 + result.success(null)
  199 + } catch (e: AlreadyStopped) {
  200 + result.success(null)
  201 + }
  202 + }
  203 +
  204 + private fun analyzeImage(call: MethodCall, result: MethodChannel.Result) {
  205 + analyzerResult = result
  206 + val uri = Uri.fromFile(File(call.arguments.toString()))
  207 + handler!!.analyzeImage(uri, analyzerCallback)
  208 + }
  209 +
  210 + private fun toggleTorch(call: MethodCall, result: MethodChannel.Result) {
  211 + try {
  212 + handler!!.toggleTorch(call.arguments == 1)
  213 + result.success(null)
  214 + } catch (e: AlreadyStopped) {
  215 + result.error("MobileScanner", "Called toggleTorch() while stopped!", null)
  216 + }
  217 + }
53 } 218 }
  1 +package dev.steenbakker.mobile_scanner
  2 +
  3 +import android.graphics.ImageFormat
  4 +import android.graphics.Point
  5 +import android.graphics.Rect
  6 +import android.graphics.YuvImage
  7 +import android.media.Image
  8 +import com.google.mlkit.vision.barcode.common.Barcode
  9 +import java.io.ByteArrayOutputStream
  10 +
  11 +fun Image.toByteArray(): ByteArray {
  12 + val yBuffer = planes[0].buffer // Y
  13 + val vuBuffer = planes[2].buffer // VU
  14 +
  15 + val ySize = yBuffer.remaining()
  16 + val vuSize = vuBuffer.remaining()
  17 +
  18 + val nv21 = ByteArray(ySize + vuSize)
  19 +
  20 + yBuffer.get(nv21, 0, ySize)
  21 + vuBuffer.get(nv21, ySize, vuSize)
  22 +
  23 + val yuvImage = YuvImage(nv21, ImageFormat.NV21, this.width, this.height, null)
  24 + val out = ByteArrayOutputStream()
  25 + yuvImage.compressToJpeg(Rect(0, 0, yuvImage.width, yuvImage.height), 50, out)
  26 + return out.toByteArray()
  27 +}
  28 +
  29 +val Barcode.data: Map<String, Any?>
  30 + get() = mapOf(
  31 + "corners" to cornerPoints?.map { corner -> corner.data }, "format" to format,
  32 + "rawBytes" to rawBytes, "rawValue" to rawValue, "type" to valueType,
  33 + "calendarEvent" to calendarEvent?.data, "contactInfo" to contactInfo?.data,
  34 + "driverLicense" to driverLicense?.data, "email" to email?.data,
  35 + "geoPoint" to geoPoint?.data, "phone" to phone?.data, "sms" to sms?.data,
  36 + "url" to url?.data, "wifi" to wifi?.data, "displayValue" to displayValue
  37 + )
  38 +
  39 +private val Point.data: Map<String, Double>
  40 + get() = mapOf("x" to x.toDouble(), "y" to y.toDouble())
  41 +
  42 +private val Barcode.CalendarEvent.data: Map<String, Any?>
  43 + get() = mapOf(
  44 + "description" to description, "end" to end?.rawValue, "location" to location,
  45 + "organizer" to organizer, "start" to start?.rawValue, "status" to status,
  46 + "summary" to summary
  47 + )
  48 +
  49 +private val Barcode.ContactInfo.data: Map<String, Any?>
  50 + get() = mapOf(
  51 + "addresses" to addresses.map { address -> address.data },
  52 + "emails" to emails.map { email -> email.data }, "name" to name?.data,
  53 + "organization" to organization, "phones" to phones.map { phone -> phone.data },
  54 + "title" to title, "urls" to urls
  55 + )
  56 +
  57 +private val Barcode.Address.data: Map<String, Any?>
  58 + get() = mapOf(
  59 + "addressLines" to addressLines.map { addressLine -> addressLine.toString() },
  60 + "type" to type
  61 + )
  62 +
  63 +private val Barcode.PersonName.data: Map<String, Any?>
  64 + get() = mapOf(
  65 + "first" to first, "formattedName" to formattedName, "last" to last,
  66 + "middle" to middle, "prefix" to prefix, "pronunciation" to pronunciation,
  67 + "suffix" to suffix
  68 + )
  69 +
  70 +private val Barcode.DriverLicense.data: Map<String, Any?>
  71 + get() = mapOf(
  72 + "addressCity" to addressCity, "addressState" to addressState,
  73 + "addressStreet" to addressStreet, "addressZip" to addressZip, "birthDate" to birthDate,
  74 + "documentType" to documentType, "expiryDate" to expiryDate, "firstName" to firstName,
  75 + "gender" to gender, "issueDate" to issueDate, "issuingCountry" to issuingCountry,
  76 + "lastName" to lastName, "licenseNumber" to licenseNumber, "middleName" to middleName
  77 + )
  78 +
  79 +private val Barcode.Email.data: Map<String, Any?>
  80 + get() = mapOf("address" to address, "body" to body, "subject" to subject, "type" to type)
  81 +
  82 +private val Barcode.GeoPoint.data: Map<String, Any?>
  83 + get() = mapOf("latitude" to lat, "longitude" to lng)
  84 +
  85 +private val Barcode.Phone.data: Map<String, Any?>
  86 + get() = mapOf("number" to number, "type" to type)
  87 +
  88 +private val Barcode.Sms.data: Map<String, Any?>
  89 + get() = mapOf("message" to message, "phoneNumber" to phoneNumber)
  90 +
  91 +private val Barcode.UrlBookmark.data: Map<String, Any?>
  92 + get() = mapOf("title" to title, "url" to url)
  93 +
  94 +private val Barcode.WiFi.data: Map<String, Any?>
  95 + get() = mapOf("encryptionType" to encryptionType, "password" to password, "ssid" to ssid)
1 -package dev.steenbakker.mobile_scanner.exceptions  
2 -  
3 -internal class NoPermissionException : RuntimeException()  
4 -  
5 -//internal class Exception(val reason: Reason) :  
6 -// java.lang.Exception("Mobile Scanner failed because $reason") {  
7 -//  
8 -// internal enum class Reason {  
9 -// noHardware, noPermissions, noBackCamera  
10 -// }  
11 -//}  
1 -package dev.steenbakker.mobile_scanner  
2 -  
3 -import com.google.mlkit.vision.barcode.BarcodeScannerOptions 1 +package dev.steenbakker.mobile_scanner.objects
4 2
5 enum class BarcodeFormats(val intValue: Int) { 3 enum class BarcodeFormats(val intValue: Int) {
6 UNKNOWN(com.google.mlkit.vision.barcode.common.Barcode.FORMAT_UNKNOWN), 4 UNKNOWN(com.google.mlkit.vision.barcode.common.Barcode.FORMAT_UNKNOWN),
  1 +package dev.steenbakker.mobile_scanner.objects
  2 +
  3 +enum class DetectionSpeed(val intValue: Int) {
  4 + NO_DUPLICATES(0),
  5 + NORMAL(1),
  6 + UNRESTRICTED(2)
  7 +}
  1 +package dev.steenbakker.mobile_scanner.objects
  2 +
  3 +class MobileScannerStartParameters(
  4 + val width: Double = 0.0,
  5 + val height: Double,
  6 + val hasFlashUnit: Boolean,
  7 + val id: Long
  8 +)
@@ -26,7 +26,7 @@ apply plugin: 'kotlin-android' @@ -26,7 +26,7 @@ apply plugin: 'kotlin-android'
26 apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 26 apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
27 27
28 android { 28 android {
29 - compileSdkVersion 32 29 + compileSdkVersion 33
30 30
31 compileOptions { 31 compileOptions {
32 sourceCompatibility JavaVersion.VERSION_1_8 32 sourceCompatibility JavaVersion.VERSION_1_8
@@ -45,7 +45,7 @@ android { @@ -45,7 +45,7 @@ android {
45 // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 45 // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
46 applicationId "dev.steenbakker.mobile_scanner_example" 46 applicationId "dev.steenbakker.mobile_scanner_example"
47 minSdkVersion 21 47 minSdkVersion 21
48 - targetSdkVersion 32 48 + targetSdkVersion 33
49 versionCode flutterVersionCode.toInteger() 49 versionCode flutterVersionCode.toInteger()
50 versionName flutterVersionName 50 versionName flutterVersionName
51 } 51 }
@@ -5,7 +5,7 @@ @@ -5,7 +5,7 @@
5 android:label="mobile_scanner_example" 5 android:label="mobile_scanner_example"
6 android:icon="@mipmap/ic_launcher"> 6 android:icon="@mipmap/ic_launcher">
7 <activity 7 <activity
8 - android:name="io.flutter.embedding.android.FlutterActivity" 8 + android:name=".MainActivity"
9 android:exported="true" 9 android:exported="true"
10 android:launchMode="singleTop" 10 android:launchMode="singleTop"
11 android:theme="@style/LaunchTheme" 11 android:theme="@style/LaunchTheme"
1 package dev.steenbakker.mobile_scanner_example 1 package dev.steenbakker.mobile_scanner_example
2 2
3 -import io.flutter.embedding.android.FlutterActivity 3 +import io.flutter.embedding.android.FlutterFragmentActivity
4 4
5 -class MainActivity: FlutterActivity() { 5 +class MainActivity : FlutterFragmentActivity() {
6 } 6 }
1 buildscript { 1 buildscript {
2 - ext.kotlin_version = '1.7.10' 2 + ext.kotlin_version = '1.7.22'
3 repositories { 3 repositories {
4 google() 4 google()
5 mavenCentral() 5 mavenCentral()
6 } 6 }
7 7
8 dependencies { 8 dependencies {
9 - classpath 'com.android.tools.build:gradle:7.2.1' 9 + classpath 'com.android.tools.build:gradle:7.3.1'
10 classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 10 classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
11 } 11 }
12 } 12 }
1 -#Tue May 31 10:34:01 CEST 2022 1 +#Tue Aug 23 15:51:00 CEST 2022
2 distributionBase=GRADLE_USER_HOME 2 distributionBase=GRADLE_USER_HOME
3 -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip 3 +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
4 distributionPath=wrapper/dists 4 distributionPath=wrapper/dists
5 zipStorePath=wrapper/dists 5 zipStorePath=wrapper/dists
6 zipStoreBase=GRADLE_USER_HOME 6 zipStoreBase=GRADLE_USER_HOME
@@ -21,6 +21,6 @@ @@ -21,6 +21,6 @@
21 <key>CFBundleVersion</key> 21 <key>CFBundleVersion</key>
22 <string>1.0</string> 22 <string>1.0</string>
23 <key>MinimumOSVersion</key> 23 <key>MinimumOSVersion</key>
24 - <string>9.0</string> 24 + <string>11.0</string>
25 </dict> 25 </dict>
26 </plist> 26 </plist>
1 # Uncomment this line to define a global platform for your project 1 # Uncomment this line to define a global platform for your project
2 -platform :ios, '10.0' 2 +platform :ios, '11.0'
3 3
4 # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 4 # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
5 ENV['COCOAPODS_DISABLE_STATS'] = 'true' 5 ENV['COCOAPODS_DISABLE_STATS'] = 'true'
@@ -37,5 +37,9 @@ end @@ -37,5 +37,9 @@ end
37 post_install do |installer| 37 post_install do |installer|
38 installer.pods_project.targets.each do |target| 38 installer.pods_project.targets.each do |target|
39 flutter_additional_ios_build_settings(target) 39 flutter_additional_ios_build_settings(target)
  40 + target.build_configurations.each do |config|
  41 + config.build_settings['ENABLE_BITCODE'] = 'NO'
  42 + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0'
  43 + end
40 end 44 end
41 end 45 end
@@ -13,7 +13,7 @@ @@ -13,7 +13,7 @@
13 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 13 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
14 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 14 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
15 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 15 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
16 - C80F46710D9B9F4F17AD4E3D /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5E133769572782C32D37D8AC /* Pods_Runner.framework */; }; 16 + A5A2C2B73A9F26060DE9FB22 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 54E006799E73DEAB41FD3623 /* Pods_Runner.framework */; };
17 /* End PBXBuildFile section */ 17 /* End PBXBuildFile section */
18 18
19 /* Begin PBXCopyFilesBuildPhase section */ 19 /* Begin PBXCopyFilesBuildPhase section */
@@ -32,9 +32,9 @@ @@ -32,9 +32,9 @@
32 /* Begin PBXFileReference section */ 32 /* Begin PBXFileReference section */
33 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; }; 33 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
34 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; }; 34 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
35 - 1CD9C88F6BFEF6CB7CA6746B /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; }; 35 + 32FD382A786B3A0080FE63FD /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
36 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; }; 36 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
37 - 5E133769572782C32D37D8AC /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 37 + 54E006799E73DEAB41FD3623 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
38 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; }; 38 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
39 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; 39 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
40 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; }; 40 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
@@ -45,8 +45,8 @@ @@ -45,8 +45,8 @@
45 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; 45 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
46 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; }; 46 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
47 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 47 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
48 - E29A089CD1D61281C49DBB79 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };  
49 - E33BE6AC5C06F7A45470ADE0 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; }; 48 + D5B36FCD262B39F867CFDEEE /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
  49 + F0D5742F0690BE32D07B033A /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
50 /* End PBXFileReference section */ 50 /* End PBXFileReference section */
51 51
52 /* Begin PBXFrameworksBuildPhase section */ 52 /* Begin PBXFrameworksBuildPhase section */
@@ -54,27 +54,19 @@ @@ -54,27 +54,19 @@
54 isa = PBXFrameworksBuildPhase; 54 isa = PBXFrameworksBuildPhase;
55 buildActionMask = 2147483647; 55 buildActionMask = 2147483647;
56 files = ( 56 files = (
57 - C80F46710D9B9F4F17AD4E3D /* Pods_Runner.framework in Frameworks */, 57 + A5A2C2B73A9F26060DE9FB22 /* Pods_Runner.framework in Frameworks */,
58 ); 58 );
59 runOnlyForDeploymentPostprocessing = 0; 59 runOnlyForDeploymentPostprocessing = 0;
60 }; 60 };
61 /* End PBXFrameworksBuildPhase section */ 61 /* End PBXFrameworksBuildPhase section */
62 62
63 /* Begin PBXGroup section */ 63 /* Begin PBXGroup section */
64 - 0F766276E0F46921DEBF581B /* Frameworks */ = {  
65 - isa = PBXGroup;  
66 - children = (  
67 - 5E133769572782C32D37D8AC /* Pods_Runner.framework */,  
68 - );  
69 - name = Frameworks;  
70 - sourceTree = "<group>";  
71 - };  
72 203D5C95A734778D93D18369 /* Pods */ = { 64 203D5C95A734778D93D18369 /* Pods */ = {
73 isa = PBXGroup; 65 isa = PBXGroup;
74 children = ( 66 children = (
75 - E33BE6AC5C06F7A45470ADE0 /* Pods-Runner.debug.xcconfig */,  
76 - 1CD9C88F6BFEF6CB7CA6746B /* Pods-Runner.release.xcconfig */,  
77 - E29A089CD1D61281C49DBB79 /* Pods-Runner.profile.xcconfig */, 67 + D5B36FCD262B39F867CFDEEE /* Pods-Runner.debug.xcconfig */,
  68 + 32FD382A786B3A0080FE63FD /* Pods-Runner.release.xcconfig */,
  69 + F0D5742F0690BE32D07B033A /* Pods-Runner.profile.xcconfig */,
78 ); 70 );
79 path = Pods; 71 path = Pods;
80 sourceTree = "<group>"; 72 sourceTree = "<group>";
@@ -97,7 +89,7 @@ @@ -97,7 +89,7 @@
97 97C146F01CF9000F007C117D /* Runner */, 89 97C146F01CF9000F007C117D /* Runner */,
98 97C146EF1CF9000F007C117D /* Products */, 90 97C146EF1CF9000F007C117D /* Products */,
99 203D5C95A734778D93D18369 /* Pods */, 91 203D5C95A734778D93D18369 /* Pods */,
100 - 0F766276E0F46921DEBF581B /* Frameworks */, 92 + FF36E403CAC9E06A5A96BB9F /* Frameworks */,
101 ); 93 );
102 sourceTree = "<group>"; 94 sourceTree = "<group>";
103 }; 95 };
@@ -124,6 +116,14 @@ @@ -124,6 +116,14 @@
124 path = Runner; 116 path = Runner;
125 sourceTree = "<group>"; 117 sourceTree = "<group>";
126 }; 118 };
  119 + FF36E403CAC9E06A5A96BB9F /* Frameworks */ = {
  120 + isa = PBXGroup;
  121 + children = (
  122 + 54E006799E73DEAB41FD3623 /* Pods_Runner.framework */,
  123 + );
  124 + name = Frameworks;
  125 + sourceTree = "<group>";
  126 + };
127 /* End PBXGroup section */ 127 /* End PBXGroup section */
128 128
129 /* Begin PBXNativeTarget section */ 129 /* Begin PBXNativeTarget section */
@@ -131,14 +131,14 @@ @@ -131,14 +131,14 @@
131 isa = PBXNativeTarget; 131 isa = PBXNativeTarget;
132 buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; 132 buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
133 buildPhases = ( 133 buildPhases = (
134 - 1C759CA63421B131D22BB688 /* [CP] Check Pods Manifest.lock */, 134 + B086C54F5791A4E759CB6822 /* [CP] Check Pods Manifest.lock */,
135 9740EEB61CF901F6004384FC /* Run Script */, 135 9740EEB61CF901F6004384FC /* Run Script */,
136 97C146EA1CF9000F007C117D /* Sources */, 136 97C146EA1CF9000F007C117D /* Sources */,
137 97C146EB1CF9000F007C117D /* Frameworks */, 137 97C146EB1CF9000F007C117D /* Frameworks */,
138 97C146EC1CF9000F007C117D /* Resources */, 138 97C146EC1CF9000F007C117D /* Resources */,
139 9705A1C41CF9048500538489 /* Embed Frameworks */, 139 9705A1C41CF9048500538489 /* Embed Frameworks */,
140 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 140 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
141 - EE97B31B239E017B5516C6AD /* [CP] Embed Pods Frameworks */, 141 + F825A499E8C466DB9DC6247D /* [CP] Embed Pods Frameworks */,
142 ); 142 );
143 buildRules = ( 143 buildRules = (
144 ); 144 );
@@ -197,57 +197,57 @@ @@ -197,57 +197,57 @@
197 /* End PBXResourcesBuildPhase section */ 197 /* End PBXResourcesBuildPhase section */
198 198
199 /* Begin PBXShellScriptBuildPhase section */ 199 /* Begin PBXShellScriptBuildPhase section */
200 - 1C759CA63421B131D22BB688 /* [CP] Check Pods Manifest.lock */ = { 200 + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
201 isa = PBXShellScriptBuildPhase; 201 isa = PBXShellScriptBuildPhase;
202 buildActionMask = 2147483647; 202 buildActionMask = 2147483647;
203 files = ( 203 files = (
204 ); 204 );
205 - inputFileListPaths = (  
206 - );  
207 inputPaths = ( 205 inputPaths = (
208 - "${PODS_PODFILE_DIR_PATH}/Podfile.lock",  
209 - "${PODS_ROOT}/Manifest.lock",  
210 - );  
211 - name = "[CP] Check Pods Manifest.lock";  
212 - outputFileListPaths = (  
213 ); 206 );
  207 + name = "Thin Binary";
214 outputPaths = ( 208 outputPaths = (
215 - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",  
216 ); 209 );
217 runOnlyForDeploymentPostprocessing = 0; 210 runOnlyForDeploymentPostprocessing = 0;
218 shellPath = /bin/sh; 211 shellPath = /bin/sh;
219 - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";  
220 - showEnvVarsInLog = 0; 212 + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
221 }; 213 };
222 - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 214 + 9740EEB61CF901F6004384FC /* Run Script */ = {
223 isa = PBXShellScriptBuildPhase; 215 isa = PBXShellScriptBuildPhase;
224 buildActionMask = 2147483647; 216 buildActionMask = 2147483647;
225 files = ( 217 files = (
226 ); 218 );
227 inputPaths = ( 219 inputPaths = (
228 ); 220 );
229 - name = "Thin Binary"; 221 + name = "Run Script";
230 outputPaths = ( 222 outputPaths = (
231 ); 223 );
232 runOnlyForDeploymentPostprocessing = 0; 224 runOnlyForDeploymentPostprocessing = 0;
233 shellPath = /bin/sh; 225 shellPath = /bin/sh;
234 - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; 226 + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
235 }; 227 };
236 - 9740EEB61CF901F6004384FC /* Run Script */ = { 228 + B086C54F5791A4E759CB6822 /* [CP] Check Pods Manifest.lock */ = {
237 isa = PBXShellScriptBuildPhase; 229 isa = PBXShellScriptBuildPhase;
238 buildActionMask = 2147483647; 230 buildActionMask = 2147483647;
239 files = ( 231 files = (
240 ); 232 );
  233 + inputFileListPaths = (
  234 + );
241 inputPaths = ( 235 inputPaths = (
  236 + "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
  237 + "${PODS_ROOT}/Manifest.lock",
  238 + );
  239 + name = "[CP] Check Pods Manifest.lock";
  240 + outputFileListPaths = (
242 ); 241 );
243 - name = "Run Script";  
244 outputPaths = ( 242 outputPaths = (
  243 + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
245 ); 244 );
246 runOnlyForDeploymentPostprocessing = 0; 245 runOnlyForDeploymentPostprocessing = 0;
247 shellPath = /bin/sh; 246 shellPath = /bin/sh;
248 - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 247 + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
  248 + showEnvVarsInLog = 0;
249 }; 249 };
250 - EE97B31B239E017B5516C6AD /* [CP] Embed Pods Frameworks */ = { 250 + F825A499E8C466DB9DC6247D /* [CP] Embed Pods Frameworks */ = {
251 isa = PBXShellScriptBuildPhase; 251 isa = PBXShellScriptBuildPhase;
252 buildActionMask = 2147483647; 252 buildActionMask = 2147483647;
253 files = ( 253 files = (
@@ -339,7 +339,7 @@ @@ -339,7 +339,7 @@
339 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 339 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
340 GCC_WARN_UNUSED_FUNCTION = YES; 340 GCC_WARN_UNUSED_FUNCTION = YES;
341 GCC_WARN_UNUSED_VARIABLE = YES; 341 GCC_WARN_UNUSED_VARIABLE = YES;
342 - IPHONEOS_DEPLOYMENT_TARGET = 9.0; 342 + IPHONEOS_DEPLOYMENT_TARGET = 11.0;
343 MTL_ENABLE_DEBUG_INFO = NO; 343 MTL_ENABLE_DEBUG_INFO = NO;
344 SDKROOT = iphoneos; 344 SDKROOT = iphoneos;
345 SUPPORTED_PLATFORMS = iphoneos; 345 SUPPORTED_PLATFORMS = iphoneos;
@@ -364,7 +364,7 @@ @@ -364,7 +364,7 @@
364 "$(inherited)", 364 "$(inherited)",
365 "@executable_path/Frameworks", 365 "@executable_path/Frameworks",
366 ); 366 );
367 - PRODUCT_BUNDLE_IDENTIFIER = dev.casvanluijtelaar.mobileScannerExample; 367 + PRODUCT_BUNDLE_IDENTIFIER = dev.steenbakker.mobileScanner;
368 PRODUCT_NAME = "$(TARGET_NAME)"; 368 PRODUCT_NAME = "$(TARGET_NAME)";
369 PROVISIONING_PROFILE_SPECIFIER = ""; 369 PROVISIONING_PROFILE_SPECIFIER = "";
370 SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 370 SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -420,7 +420,7 @@ @@ -420,7 +420,7 @@
420 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 420 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
421 GCC_WARN_UNUSED_FUNCTION = YES; 421 GCC_WARN_UNUSED_FUNCTION = YES;
422 GCC_WARN_UNUSED_VARIABLE = YES; 422 GCC_WARN_UNUSED_VARIABLE = YES;
423 - IPHONEOS_DEPLOYMENT_TARGET = 9.0; 423 + IPHONEOS_DEPLOYMENT_TARGET = 11.0;
424 MTL_ENABLE_DEBUG_INFO = YES; 424 MTL_ENABLE_DEBUG_INFO = YES;
425 ONLY_ACTIVE_ARCH = YES; 425 ONLY_ACTIVE_ARCH = YES;
426 SDKROOT = iphoneos; 426 SDKROOT = iphoneos;
@@ -469,7 +469,7 @@ @@ -469,7 +469,7 @@
469 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 469 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
470 GCC_WARN_UNUSED_FUNCTION = YES; 470 GCC_WARN_UNUSED_FUNCTION = YES;
471 GCC_WARN_UNUSED_VARIABLE = YES; 471 GCC_WARN_UNUSED_VARIABLE = YES;
472 - IPHONEOS_DEPLOYMENT_TARGET = 9.0; 472 + IPHONEOS_DEPLOYMENT_TARGET = 11.0;
473 MTL_ENABLE_DEBUG_INFO = NO; 473 MTL_ENABLE_DEBUG_INFO = NO;
474 SDKROOT = iphoneos; 474 SDKROOT = iphoneos;
475 SUPPORTED_PLATFORMS = iphoneos; 475 SUPPORTED_PLATFORMS = iphoneos;
@@ -496,7 +496,7 @@ @@ -496,7 +496,7 @@
496 "$(inherited)", 496 "$(inherited)",
497 "@executable_path/Frameworks", 497 "@executable_path/Frameworks",
498 ); 498 );
499 - PRODUCT_BUNDLE_IDENTIFIER = dev.casvanluijtelaar.mobileScannerExample; 499 + PRODUCT_BUNDLE_IDENTIFIER = dev.steenbakker.mobileScanner;
500 PRODUCT_NAME = "$(TARGET_NAME)"; 500 PRODUCT_NAME = "$(TARGET_NAME)";
501 PROVISIONING_PROFILE_SPECIFIER = ""; 501 PROVISIONING_PROFILE_SPECIFIER = "";
502 SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 502 SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -522,7 +522,7 @@ @@ -522,7 +522,7 @@
522 "$(inherited)", 522 "$(inherited)",
523 "@executable_path/Frameworks", 523 "@executable_path/Frameworks",
524 ); 524 );
525 - PRODUCT_BUNDLE_IDENTIFIER = dev.casvanluijtelaar.mobileScannerExample; 525 + PRODUCT_BUNDLE_IDENTIFIER = dev.steenbakker.mobileScanner;
526 PRODUCT_NAME = "$(TARGET_NAME)"; 526 PRODUCT_NAME = "$(TARGET_NAME)";
527 PROVISIONING_PROFILE_SPECIFIER = ""; 527 PROVISIONING_PROFILE_SPECIFIER = "";
528 SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 528 SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
  1 +import 'package:flutter/material.dart';
  2 +import 'package:image_picker/image_picker.dart';
  3 +import 'package:mobile_scanner/mobile_scanner.dart';
  4 +
  5 +class BarcodeListScannerWithController extends StatefulWidget {
  6 + const BarcodeListScannerWithController({Key? key}) : super(key: key);
  7 +
  8 + @override
  9 + _BarcodeListScannerWithControllerState createState() =>
  10 + _BarcodeListScannerWithControllerState();
  11 +}
  12 +
  13 +class _BarcodeListScannerWithControllerState
  14 + extends State<BarcodeListScannerWithController>
  15 + with SingleTickerProviderStateMixin {
  16 + BarcodeCapture? barcodeCapture;
  17 +
  18 + final MobileScannerController controller = MobileScannerController(
  19 + torchEnabled: true,
  20 + // formats: [BarcodeFormat.qrCode]
  21 + // facing: CameraFacing.front,
  22 + // detectionSpeed: DetectionSpeed.normal
  23 + // detectionTimeoutMs: 1000,
  24 + // returnImage: false,
  25 + );
  26 +
  27 + bool isStarted = true;
  28 +
  29 + void _startOrStop() {
  30 + if (isStarted) {
  31 + controller.stop();
  32 + } else {
  33 + controller.start().catchError((error) {
  34 + final exception = error as MobileScannerException;
  35 +
  36 + switch (exception.errorCode) {
  37 + case MobileScannerErrorCode.controllerUninitialized:
  38 + break; // This error code is not used by `start()`.
  39 + case MobileScannerErrorCode.genericError:
  40 + debugPrint('Scanner failed to start');
  41 + break;
  42 + case MobileScannerErrorCode.permissionDenied:
  43 + debugPrint('Camera permission denied');
  44 + break;
  45 + }
  46 + });
  47 + }
  48 +
  49 + setState(() {
  50 + isStarted = !isStarted;
  51 + });
  52 + }
  53 +
  54 + @override
  55 + Widget build(BuildContext context) {
  56 + return Scaffold(
  57 + backgroundColor: Colors.black,
  58 + body: Builder(
  59 + builder: (context) {
  60 + return Stack(
  61 + children: [
  62 + MobileScanner(
  63 + controller: controller,
  64 + fit: BoxFit.contain,
  65 + onDetect: (barcodeCapture) {
  66 + setState(() {
  67 + this.barcodeCapture = barcodeCapture;
  68 + });
  69 + },
  70 + onScannerStarted: (arguments) {
  71 + // Do something with arguments.
  72 + },
  73 + ),
  74 + Align(
  75 + alignment: Alignment.bottomCenter,
  76 + child: Container(
  77 + alignment: Alignment.bottomCenter,
  78 + height: 100,
  79 + color: Colors.black.withOpacity(0.4),
  80 + child: Row(
  81 + mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  82 + children: [
  83 + IconButton(
  84 + color: Colors.white,
  85 + icon: ValueListenableBuilder(
  86 + valueListenable: controller.torchState,
  87 + builder: (context, state, child) {
  88 + if (state == null) {
  89 + return const Icon(
  90 + Icons.flash_off,
  91 + color: Colors.grey,
  92 + );
  93 + }
  94 + switch (state as TorchState) {
  95 + case TorchState.off:
  96 + return const Icon(
  97 + Icons.flash_off,
  98 + color: Colors.grey,
  99 + );
  100 + case TorchState.on:
  101 + return const Icon(
  102 + Icons.flash_on,
  103 + color: Colors.yellow,
  104 + );
  105 + }
  106 + },
  107 + ),
  108 + iconSize: 32.0,
  109 + onPressed: () => controller.toggleTorch(),
  110 + ),
  111 + IconButton(
  112 + color: Colors.white,
  113 + icon: isStarted
  114 + ? const Icon(Icons.stop)
  115 + : const Icon(Icons.play_arrow),
  116 + iconSize: 32.0,
  117 + onPressed: _startOrStop,
  118 + ),
  119 + Center(
  120 + child: SizedBox(
  121 + width: MediaQuery.of(context).size.width - 200,
  122 + height: 50,
  123 + child: FittedBox(
  124 + child: Text(
  125 + '${barcodeCapture?.barcodes.map((e) => e.rawValue) ?? 'Scan something!'}',
  126 + overflow: TextOverflow.fade,
  127 + style: Theme.of(context)
  128 + .textTheme
  129 + .headline4!
  130 + .copyWith(color: Colors.white),
  131 + ),
  132 + ),
  133 + ),
  134 + ),
  135 + IconButton(
  136 + color: Colors.white,
  137 + icon: ValueListenableBuilder(
  138 + valueListenable: controller.cameraFacingState,
  139 + builder: (context, state, child) {
  140 + if (state == null) {
  141 + return const Icon(Icons.camera_front);
  142 + }
  143 + switch (state as CameraFacing) {
  144 + case CameraFacing.front:
  145 + return const Icon(Icons.camera_front);
  146 + case CameraFacing.back:
  147 + return const Icon(Icons.camera_rear);
  148 + }
  149 + },
  150 + ),
  151 + iconSize: 32.0,
  152 + onPressed: () => controller.switchCamera(),
  153 + ),
  154 + IconButton(
  155 + color: Colors.white,
  156 + icon: const Icon(Icons.image),
  157 + iconSize: 32.0,
  158 + onPressed: () async {
  159 + final ImagePicker picker = ImagePicker();
  160 + // Pick an image
  161 + final XFile? image = await picker.pickImage(
  162 + source: ImageSource.gallery,
  163 + );
  164 + if (image != null) {
  165 + if (await controller.analyzeImage(image.path)) {
  166 + if (!mounted) return;
  167 + ScaffoldMessenger.of(context).showSnackBar(
  168 + const SnackBar(
  169 + content: Text('Barcode found!'),
  170 + backgroundColor: Colors.green,
  171 + ),
  172 + );
  173 + } else {
  174 + if (!mounted) return;
  175 + ScaffoldMessenger.of(context).showSnackBar(
  176 + const SnackBar(
  177 + content: Text('No barcode found!'),
  178 + backgroundColor: Colors.red,
  179 + ),
  180 + );
  181 + }
  182 + }
  183 + },
  184 + ),
  185 + ],
  186 + ),
  187 + ),
  188 + ),
  189 + ],
  190 + );
  191 + },
  192 + ),
  193 + );
  194 + }
  195 +
  196 + @override
  197 + void dispose() {
  198 + controller.dispose();
  199 + super.dispose();
  200 + }
  201 +}
@@ -13,16 +13,44 @@ class BarcodeScannerWithController extends StatefulWidget { @@ -13,16 +13,44 @@ class BarcodeScannerWithController extends StatefulWidget {
13 class _BarcodeScannerWithControllerState 13 class _BarcodeScannerWithControllerState
14 extends State<BarcodeScannerWithController> 14 extends State<BarcodeScannerWithController>
15 with SingleTickerProviderStateMixin { 15 with SingleTickerProviderStateMixin {
16 - String? barcode; 16 + BarcodeCapture? barcode;
17 17
18 - MobileScannerController controller = MobileScannerController( 18 + final MobileScannerController controller = MobileScannerController(
19 torchEnabled: true, 19 torchEnabled: true,
20 // formats: [BarcodeFormat.qrCode] 20 // formats: [BarcodeFormat.qrCode]
21 // facing: CameraFacing.front, 21 // facing: CameraFacing.front,
  22 + // detectionSpeed: DetectionSpeed.normal
  23 + // detectionTimeoutMs: 1000,
  24 + // returnImage: false,
22 ); 25 );
23 26
24 bool isStarted = true; 27 bool isStarted = true;
25 28
  29 + void _startOrStop() {
  30 + if (isStarted) {
  31 + controller.stop();
  32 + } else {
  33 + controller.start().catchError((error) {
  34 + final exception = error as MobileScannerException;
  35 +
  36 + switch (exception.errorCode) {
  37 + case MobileScannerErrorCode.controllerUninitialized:
  38 + break; // This error code is not used by `start()`.
  39 + case MobileScannerErrorCode.genericError:
  40 + debugPrint('Scanner failed to start');
  41 + break;
  42 + case MobileScannerErrorCode.permissionDenied:
  43 + debugPrint('Camera permission denied');
  44 + break;
  45 + }
  46 + });
  47 + }
  48 +
  49 + setState(() {
  50 + isStarted = !isStarted;
  51 + });
  52 + }
  53 +
26 @override 54 @override
27 Widget build(BuildContext context) { 55 Widget build(BuildContext context) {
28 return Scaffold( 56 return Scaffold(
@@ -34,14 +62,9 @@ class _BarcodeScannerWithControllerState @@ -34,14 +62,9 @@ class _BarcodeScannerWithControllerState
34 MobileScanner( 62 MobileScanner(
35 controller: controller, 63 controller: controller,
36 fit: BoxFit.contain, 64 fit: BoxFit.contain,
37 - // allowDuplicates: true,  
38 - // controller: MobileScannerController(  
39 - // torchEnabled: true,  
40 - // facing: CameraFacing.front,  
41 - // ),  
42 - onDetect: (barcode, args) { 65 + onDetect: (barcode) {
43 setState(() { 66 setState(() {
44 - this.barcode = barcode.rawValue; 67 + this.barcode = barcode;
45 }); 68 });
46 }, 69 },
47 ), 70 ),
@@ -88,10 +111,7 @@ class _BarcodeScannerWithControllerState @@ -88,10 +111,7 @@ class _BarcodeScannerWithControllerState
88 ? const Icon(Icons.stop) 111 ? const Icon(Icons.stop)
89 : const Icon(Icons.play_arrow), 112 : const Icon(Icons.play_arrow),
90 iconSize: 32.0, 113 iconSize: 32.0,
91 - onPressed: () => setState(() {  
92 - isStarted ? controller.stop() : controller.start();  
93 - isStarted = !isStarted;  
94 - }), 114 + onPressed: _startOrStop,
95 ), 115 ),
96 Center( 116 Center(
97 child: SizedBox( 117 child: SizedBox(
@@ -99,7 +119,8 @@ class _BarcodeScannerWithControllerState @@ -99,7 +119,8 @@ class _BarcodeScannerWithControllerState
99 height: 50, 119 height: 50,
100 child: FittedBox( 120 child: FittedBox(
101 child: Text( 121 child: Text(
102 - barcode ?? 'Scan something!', 122 + barcode?.barcodes.first.rawValue ??
  123 + 'Scan something!',
103 overflow: TextOverflow.fade, 124 overflow: TextOverflow.fade,
104 style: Theme.of(context) 125 style: Theme.of(context)
105 .textTheme 126 .textTheme
@@ -169,4 +190,10 @@ class _BarcodeScannerWithControllerState @@ -169,4 +190,10 @@ class _BarcodeScannerWithControllerState
169 ), 190 ),
170 ); 191 );
171 } 192 }
  193 +
  194 + @override
  195 + void dispose() {
  196 + controller.dispose();
  197 + super.dispose();
  198 + }
172 } 199 }
  1 +import 'dart:math';
  2 +
  3 +import 'package:flutter/material.dart';
  4 +import 'package:mobile_scanner/mobile_scanner.dart';
  5 +
  6 +class BarcodeScannerReturningImage extends StatefulWidget {
  7 + const BarcodeScannerReturningImage({Key? key}) : super(key: key);
  8 +
  9 + @override
  10 + _BarcodeScannerReturningImageState createState() =>
  11 + _BarcodeScannerReturningImageState();
  12 +}
  13 +
  14 +class _BarcodeScannerReturningImageState
  15 + extends State<BarcodeScannerReturningImage>
  16 + with SingleTickerProviderStateMixin {
  17 + BarcodeCapture? barcode;
  18 + MobileScannerArguments? arguments;
  19 +
  20 + final MobileScannerController controller = MobileScannerController(
  21 + torchEnabled: true,
  22 + // formats: [BarcodeFormat.qrCode]
  23 + // facing: CameraFacing.front,
  24 + // detectionSpeed: DetectionSpeed.normal
  25 + // detectionTimeoutMs: 1000,
  26 + returnImage: true,
  27 + );
  28 +
  29 + bool isStarted = true;
  30 +
  31 + void _startOrStop() {
  32 + if (isStarted) {
  33 + controller.stop();
  34 + } else {
  35 + controller.start().catchError((error) {
  36 + final exception = error as MobileScannerException;
  37 +
  38 + switch (exception.errorCode) {
  39 + case MobileScannerErrorCode.controllerUninitialized:
  40 + break; // This error code is not used by `start()`.
  41 + case MobileScannerErrorCode.genericError:
  42 + debugPrint('Scanner failed to start');
  43 + break;
  44 + case MobileScannerErrorCode.permissionDenied:
  45 + debugPrint('Camera permission denied');
  46 + break;
  47 + }
  48 + });
  49 + }
  50 +
  51 + setState(() {
  52 + isStarted = !isStarted;
  53 + });
  54 + }
  55 +
  56 + @override
  57 + Widget build(BuildContext context) {
  58 + return Scaffold(
  59 + body: SafeArea(
  60 + child: Column(
  61 + children: [
  62 + Expanded(
  63 + child: barcode?.image != null
  64 + ? Transform.rotate(
  65 + angle: 90 * pi / 180,
  66 + child: Image(
  67 + gaplessPlayback: true,
  68 + image: MemoryImage(barcode!.image!),
  69 + fit: BoxFit.contain,
  70 + ),
  71 + )
  72 + : const Center(
  73 + child: Text(
  74 + 'Your scanned barcode will appear here!',
  75 + ),
  76 + ),
  77 + ),
  78 + Expanded(
  79 + flex: 2,
  80 + child: ColoredBox(
  81 + color: Colors.grey,
  82 + child: Stack(
  83 + children: [
  84 + MobileScanner(
  85 + controller: controller,
  86 + fit: BoxFit.contain,
  87 + onDetect: (barcode) {
  88 + setState(() {
  89 + this.barcode = barcode;
  90 + });
  91 + },
  92 + ),
  93 + Align(
  94 + alignment: Alignment.bottomCenter,
  95 + child: Container(
  96 + alignment: Alignment.bottomCenter,
  97 + height: 100,
  98 + color: Colors.black.withOpacity(0.4),
  99 + child: Row(
  100 + mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  101 + children: [
  102 + IconButton(
  103 + color: Colors.white,
  104 + icon: ValueListenableBuilder(
  105 + valueListenable: controller.torchState,
  106 + builder: (context, state, child) {
  107 + if (state == null) {
  108 + return const Icon(
  109 + Icons.flash_off,
  110 + color: Colors.grey,
  111 + );
  112 + }
  113 + switch (state as TorchState) {
  114 + case TorchState.off:
  115 + return const Icon(
  116 + Icons.flash_off,
  117 + color: Colors.grey,
  118 + );
  119 + case TorchState.on:
  120 + return const Icon(
  121 + Icons.flash_on,
  122 + color: Colors.yellow,
  123 + );
  124 + }
  125 + },
  126 + ),
  127 + iconSize: 32.0,
  128 + onPressed: () => controller.toggleTorch(),
  129 + ),
  130 + IconButton(
  131 + color: Colors.white,
  132 + icon: isStarted
  133 + ? const Icon(Icons.stop)
  134 + : const Icon(Icons.play_arrow),
  135 + iconSize: 32.0,
  136 + onPressed: _startOrStop,
  137 + ),
  138 + Center(
  139 + child: SizedBox(
  140 + width: MediaQuery.of(context).size.width - 200,
  141 + height: 50,
  142 + child: FittedBox(
  143 + child: Text(
  144 + barcode?.barcodes.first.rawValue ??
  145 + 'Scan something!',
  146 + overflow: TextOverflow.fade,
  147 + style: Theme.of(context)
  148 + .textTheme
  149 + .headline4!
  150 + .copyWith(color: Colors.white),
  151 + ),
  152 + ),
  153 + ),
  154 + ),
  155 + IconButton(
  156 + color: Colors.white,
  157 + icon: ValueListenableBuilder(
  158 + valueListenable: controller.cameraFacingState,
  159 + builder: (context, state, child) {
  160 + if (state == null) {
  161 + return const Icon(Icons.camera_front);
  162 + }
  163 + switch (state as CameraFacing) {
  164 + case CameraFacing.front:
  165 + return const Icon(Icons.camera_front);
  166 + case CameraFacing.back:
  167 + return const Icon(Icons.camera_rear);
  168 + }
  169 + },
  170 + ),
  171 + iconSize: 32.0,
  172 + onPressed: () => controller.switchCamera(),
  173 + ),
  174 + ],
  175 + ),
  176 + ),
  177 + ),
  178 + ],
  179 + ),
  180 + ),
  181 + ),
  182 + ],
  183 + ),
  184 + ),
  185 + );
  186 + }
  187 +
  188 + @override
  189 + void dispose() {
  190 + controller.dispose();
  191 + super.dispose();
  192 + }
  193 +}
@@ -12,7 +12,7 @@ class BarcodeScannerWithoutController extends StatefulWidget { @@ -12,7 +12,7 @@ class BarcodeScannerWithoutController extends StatefulWidget {
12 class _BarcodeScannerWithoutControllerState 12 class _BarcodeScannerWithoutControllerState
13 extends State<BarcodeScannerWithoutController> 13 extends State<BarcodeScannerWithoutController>
14 with SingleTickerProviderStateMixin { 14 with SingleTickerProviderStateMixin {
15 - String? barcode; 15 + BarcodeCapture? capture;
16 16
17 @override 17 @override
18 Widget build(BuildContext context) { 18 Widget build(BuildContext context) {
@@ -24,10 +24,9 @@ class _BarcodeScannerWithoutControllerState @@ -24,10 +24,9 @@ class _BarcodeScannerWithoutControllerState
24 children: [ 24 children: [
25 MobileScanner( 25 MobileScanner(
26 fit: BoxFit.contain, 26 fit: BoxFit.contain,
27 - // allowDuplicates: false,  
28 - onDetect: (barcode, args) { 27 + onDetect: (capture) {
29 setState(() { 28 setState(() {
30 - this.barcode = barcode.rawValue; 29 + this.capture = capture;
31 }); 30 });
32 }, 31 },
33 ), 32 ),
@@ -46,7 +45,8 @@ class _BarcodeScannerWithoutControllerState @@ -46,7 +45,8 @@ class _BarcodeScannerWithoutControllerState
46 height: 50, 45 height: 50,
47 child: FittedBox( 46 child: FittedBox(
48 child: Text( 47 child: Text(
49 - barcode ?? 'Scan something!', 48 + capture?.barcodes.first.rawValue ??
  49 + 'Scan something!',
50 overflow: TextOverflow.fade, 50 overflow: TextOverflow.fade,
51 style: Theme.of(context) 51 style: Theme.of(context)
52 .textTheme 52 .textTheme
1 import 'package:flutter/material.dart'; 1 import 'package:flutter/material.dart';
  2 +import 'package:mobile_scanner_example/barcode_list_scanner_controller.dart';
2 import 'package:mobile_scanner_example/barcode_scanner_controller.dart'; 3 import 'package:mobile_scanner_example/barcode_scanner_controller.dart';
  4 +import 'package:mobile_scanner_example/barcode_scanner_returning_image.dart';
3 import 'package:mobile_scanner_example/barcode_scanner_window.dart'; 5 import 'package:mobile_scanner_example/barcode_scanner_window.dart';
4 import 'package:mobile_scanner_example/barcode_scanner_without_controller.dart'; 6 import 'package:mobile_scanner_example/barcode_scanner_without_controller.dart';
5 7
@@ -22,6 +24,17 @@ class MyHome extends StatelessWidget { @@ -22,6 +24,17 @@ class MyHome extends StatelessWidget {
22 onPressed: () { 24 onPressed: () {
23 Navigator.of(context).push( 25 Navigator.of(context).push(
24 MaterialPageRoute( 26 MaterialPageRoute(
  27 + builder: (context) =>
  28 + const BarcodeListScannerWithController(),
  29 + ),
  30 + );
  31 + },
  32 + child: const Text('MobileScanner with List Controller'),
  33 + ),
  34 + ElevatedButton(
  35 + onPressed: () {
  36 + Navigator.of(context).push(
  37 + MaterialPageRoute(
25 builder: (context) => const BarcodeScannerWithController(), 38 builder: (context) => const BarcodeScannerWithController(),
26 ), 39 ),
27 ); 40 );
@@ -42,6 +55,17 @@ class MyHome extends StatelessWidget { @@ -42,6 +55,17 @@ class MyHome extends StatelessWidget {
42 onPressed: () { 55 onPressed: () {
43 Navigator.of(context).push( 56 Navigator.of(context).push(
44 MaterialPageRoute( 57 MaterialPageRoute(
  58 + builder: (context) => const BarcodeScannerReturningImage(),
  59 + ),
  60 + );
  61 + },
  62 + child:
  63 + const Text('MobileScanner with Controller (returning image)'),
  64 + ),
  65 + ElevatedButton(
  66 + onPressed: () {
  67 + Navigator.of(context).push(
  68 + MaterialPageRoute(
45 builder: (context) => 69 builder: (context) =>
46 const BarcodeScannerWithoutController(), 70 const BarcodeScannerWithoutController(),
47 ), 71 ),
@@ -8,7 +8,7 @@ environment: @@ -8,7 +8,7 @@ environment:
8 dependencies: 8 dependencies:
9 flutter: 9 flutter:
10 sdk: flutter 10 sdk: flutter
11 - image_picker: ^0.8.5+3 11 + image_picker: ^0.8.6
12 12
13 mobile_scanner: 13 mobile_scanner:
14 path: ../ 14 path: ../
@@ -28,8 +28,8 @@ @@ -28,8 +28,8 @@
28 28
29 <title>example</title> 29 <title>example</title>
30 <link rel="manifest" href="manifest.json"> 30 <link rel="manifest" href="manifest.json">
31 -<!-- <script src="https://cdn.jsdelivr.net/npm/qr-scanner@1.4.1/qr-scanner.min.js"></script>-->  
32 <script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js"></script> 31 <script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js"></script>
  32 + <script type="text/javascript" src="https://unpkg.com/@zxing/library@0.19.1"></script>
33 </head> 33 </head>
34 <body> 34 <body>
35 <!-- This script installs service_worker.js to provide PWA functionality to 35 <!-- This script installs service_worker.js to provide PWA functionality to
  1 +//
  2 +// BarcodeHandler.swift
  3 +// mobile_scanner
  4 +//
  5 +// Created by Julian Steenbakker on 24/08/2022.
  6 +//
  7 +
  8 +import Foundation
  9 +
  10 +public class BarcodeHandler: NSObject, FlutterStreamHandler {
  11 +
  12 + var event: [String: Any?] = [:]
  13 +
  14 + private var eventSink: FlutterEventSink?
  15 + private let eventChannel: FlutterEventChannel
  16 +
  17 + init(registrar: FlutterPluginRegistrar) {
  18 + eventChannel = FlutterEventChannel(name:
  19 + "dev.steenbakker.mobile_scanner/scanner/event", binaryMessenger: registrar.messenger())
  20 + super.init()
  21 + eventChannel.setStreamHandler(self)
  22 + }
  23 +
  24 + func publishEvent(_ event: [String: Any?]) {
  25 + self.event = event
  26 + eventSink?(event)
  27 + }
  28 +
  29 + public func onListen(withArguments arguments: Any?,
  30 + eventSink: @escaping FlutterEventSink) -> FlutterError? {
  31 + self.eventSink = eventSink
  32 + return nil
  33 + }
  34 +
  35 + public func onCancel(withArguments arguments: Any?) -> FlutterError? {
  36 + eventSink = nil
  37 + return nil
  38 + }
  39 +}
  1 +//
  2 +// DetectionSpeed.swift
  3 +// mobile_scanner
  4 +//
  5 +// Created by Julian Steenbakker on 11/11/2022.
  6 +//
  7 +
  8 +enum DetectionSpeed: Int {
  9 + case noDuplicates = 0
  10 + case normal = 1
  11 + case unrestricted = 2
  12 +}
  1 +//
  2 +// SwiftMobileScanner.swift
  3 +// mobile_scanner
  4 +//
  5 +// Created by Julian Steenbakker on 15/02/2022.
  6 +//
  7 +
  8 +import Foundation
  9 +
  10 +import AVFoundation
  11 +import MLKitVision
  12 +import MLKitBarcodeScanning
  13 +
  14 +typealias MobileScannerCallback = ((Array<Barcode>?, Error?, UIImage) -> ())
  15 +
  16 +public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate, FlutterTexture {
  17 + /// Capture session of the camera
  18 + var captureSession: AVCaptureSession!
  19 +
  20 + /// The selected camera
  21 + var device: AVCaptureDevice!
  22 +
  23 + /// Barcode scanner for results
  24 + var scanner = BarcodeScanner.barcodeScanner()
  25 +
  26 + /// Return image buffer with the Barcode event
  27 + var returnImage: Bool = false
  28 +
  29 + /// Default position of camera
  30 + var videoPosition: AVCaptureDevice.Position = AVCaptureDevice.Position.back
  31 +
  32 + /// When results are found, this callback will be called
  33 + let mobileScannerCallback: MobileScannerCallback
  34 +
  35 + /// If provided, the Flutter registry will be used to send the output of the CaptureOutput to a Flutter texture.
  36 + private let registry: FlutterTextureRegistry?
  37 +
  38 + /// Image to be sent to the texture
  39 + var latestBuffer: CVImageBuffer!
  40 +
  41 + /// Texture id of the camera preview for Flutter
  42 + private var textureId: Int64!
  43 +
  44 + var detectionSpeed: DetectionSpeed = DetectionSpeed.noDuplicates
  45 +
  46 + init(registry: FlutterTextureRegistry?, mobileScannerCallback: @escaping MobileScannerCallback) {
  47 + self.registry = registry
  48 + self.mobileScannerCallback = mobileScannerCallback
  49 + super.init()
  50 + }
  51 +
  52 + /// Check if we already have camera permission.
  53 + func checkPermission() -> Int {
  54 + let status = AVCaptureDevice.authorizationStatus(for: .video)
  55 + switch status {
  56 + case .notDetermined:
  57 + return 0
  58 + case .authorized:
  59 + return 1
  60 + default:
  61 + return 2
  62 + }
  63 + }
  64 +
  65 + /// Request permissions for video
  66 + func requestPermission(_ result: @escaping FlutterResult) {
  67 + AVCaptureDevice.requestAccess(for: .video, completionHandler: { result($0) })
  68 + }
  69 +
  70 + /// Gets called when a new image is added to the buffer
  71 + public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
  72 + guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
  73 + print("Failed to get image buffer from sample buffer.")
  74 + return
  75 + }
  76 + latestBuffer = imageBuffer
  77 + registry?.textureFrameAvailable(textureId)
  78 + if ((detectionSpeed == DetectionSpeed.normal || detectionSpeed == DetectionSpeed.noDuplicates) && i > 10 || detectionSpeed == DetectionSpeed.unrestricted) {
  79 + i = 0
  80 + let ciImage = latestBuffer.image
  81 +
  82 + let image = VisionImage(image: ciImage)
  83 + image.orientation = imageOrientation(
  84 + deviceOrientation: UIDevice.current.orientation,
  85 + defaultOrientation: .portrait,
  86 + position: videoPosition
  87 + )
  88 +
  89 + scanner.process(image) { [self] barcodes, error in
  90 + if (detectionSpeed == DetectionSpeed.noDuplicates) {
  91 + let newScannedBarcodes = barcodes?.map { barcode in
  92 + return barcode.rawValue
  93 + }
  94 + if (error == nil && barcodesString != nil && newScannedBarcodes != nil && barcodesString!.elementsEqual(newScannedBarcodes!)) {
  95 + return
  96 + } else {
  97 + barcodesString = newScannedBarcodes
  98 + }
  99 + }
  100 +
  101 + mobileScannerCallback(barcodes, error, ciImage)
  102 + }
  103 + } else {
  104 + i+=1
  105 + }
  106 + }
  107 +
  108 + /// Start scanning for barcodes
  109 + func start(barcodeScannerOptions: BarcodeScannerOptions?, returnImage: Bool, cameraPosition: AVCaptureDevice.Position, torch: AVCaptureDevice.TorchMode, detectionSpeed: DetectionSpeed) throws -> MobileScannerStartParameters {
  110 + self.detectionSpeed = detectionSpeed
  111 + if (device != nil) {
  112 + throw MobileScannerError.alreadyStarted
  113 + }
  114 +
  115 + scanner = barcodeScannerOptions != nil ? BarcodeScanner.barcodeScanner(options: barcodeScannerOptions!) : BarcodeScanner.barcodeScanner()
  116 + captureSession = AVCaptureSession()
  117 + textureId = registry?.register(self)
  118 +
  119 + // Open the camera device
  120 + if #available(iOS 10.0, *) {
  121 + device = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: cameraPosition).devices.first
  122 + } else {
  123 + device = AVCaptureDevice.devices(for: .video).filter({$0.position == cameraPosition}).first
  124 + }
  125 +
  126 + if (device == nil) {
  127 + throw MobileScannerError.noCamera
  128 + }
  129 +
  130 + // Enable the torch if parameter is set and torch is available
  131 + if (device.hasTorch && device.isTorchAvailable) {
  132 + do {
  133 + try device.lockForConfiguration()
  134 + device.torchMode = torch
  135 + device.unlockForConfiguration()
  136 + } catch {
  137 + throw MobileScannerError.torchError(error)
  138 + }
  139 + }
  140 +
  141 + device.addObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode), options: .new, context: nil)
  142 + captureSession.beginConfiguration()
  143 +
  144 + // Add device input
  145 + do {
  146 + let input = try AVCaptureDeviceInput(device: device)
  147 + captureSession.addInput(input)
  148 + } catch {
  149 + throw MobileScannerError.cameraError(error)
  150 + }
  151 +
  152 + captureSession.sessionPreset = AVCaptureSession.Preset.photo;
  153 + // Add video output.
  154 + let videoOutput = AVCaptureVideoDataOutput()
  155 +
  156 + videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA]
  157 + videoOutput.alwaysDiscardsLateVideoFrames = true
  158 +
  159 + videoPosition = cameraPosition
  160 + // calls captureOutput()
  161 + videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue.main)
  162 +
  163 + captureSession.addOutput(videoOutput)
  164 + for connection in videoOutput.connections {
  165 + connection.videoOrientation = .portrait
  166 + if cameraPosition == .front && connection.isVideoMirroringSupported {
  167 + connection.isVideoMirrored = true
  168 + }
  169 + }
  170 + captureSession.commitConfiguration()
  171 + captureSession.startRunning()
  172 + let dimensions = CMVideoFormatDescriptionGetDimensions(device.activeFormat.formatDescription)
  173 +
  174 + return MobileScannerStartParameters(width: Double(dimensions.height), height: Double(dimensions.width), hasTorch: device.hasTorch, textureId: textureId)
  175 + }
  176 +
  177 + /// Stop scanning for barcodes
  178 + func stop() throws {
  179 + if (device == nil) {
  180 + throw MobileScannerError.alreadyStopped
  181 + }
  182 + captureSession.stopRunning()
  183 + for input in captureSession.inputs {
  184 + captureSession.removeInput(input)
  185 + }
  186 + for output in captureSession.outputs {
  187 + captureSession.removeOutput(output)
  188 + }
  189 + device.removeObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode))
  190 + registry?.unregisterTexture(textureId)
  191 + textureId = nil
  192 + captureSession = nil
  193 + device = nil
  194 + }
  195 +
  196 + /// Toggle the flashlight between on and off
  197 + func toggleTorch(_ torch: AVCaptureDevice.TorchMode) throws {
  198 + if (device == nil) {
  199 + throw MobileScannerError.torchWhenStopped
  200 + }
  201 + do {
  202 + try device.lockForConfiguration()
  203 + device.torchMode = torch
  204 + device.unlockForConfiguration()
  205 + } catch {
  206 + throw MobileScannerError.torchError(error)
  207 + }
  208 + }
  209 +
  210 + /// Analyze a single image
  211 + func analyzeImage(image: UIImage, position: AVCaptureDevice.Position, callback: @escaping BarcodeScanningCallback) {
  212 + let image = VisionImage(image: image)
  213 + image.orientation = imageOrientation(
  214 + deviceOrientation: UIDevice.current.orientation,
  215 + defaultOrientation: .portrait,
  216 + position: position
  217 + )
  218 +
  219 + scanner.process(image, completion: callback)
  220 + }
  221 +
  222 + var i = 0
  223 +
  224 + var barcodesString: Array<String?>?
  225 +
  226 +
  227 +
  228 +// /// Convert image buffer to jpeg
  229 +// private func ciImageToJpeg(ciImage: CIImage) -> Data {
  230 +//
  231 +// // let ciImage = CIImage(cvPixelBuffer: latestBuffer)
  232 +// let context:CIContext = CIContext.init(options: nil)
  233 +// let cgImage:CGImage = context.createCGImage(ciImage, from: ciImage.extent)!
  234 +// let uiImage:UIImage = UIImage(cgImage: cgImage, scale: 1, orientation: UIImage.Orientation.up)
  235 +//
  236 +// return uiImage.jpegData(compressionQuality: 0.8)!;
  237 +// }
  238 +
  239 + /// Rotates images accordingly
  240 + func imageOrientation(
  241 + deviceOrientation: UIDeviceOrientation,
  242 + defaultOrientation: UIDeviceOrientation,
  243 + position: AVCaptureDevice.Position
  244 + ) -> UIImage.Orientation {
  245 + switch deviceOrientation {
  246 + case .portrait:
  247 + return position == .front ? .leftMirrored : .right
  248 + case .landscapeLeft:
  249 + return position == .front ? .downMirrored : .up
  250 + case .portraitUpsideDown:
  251 + return position == .front ? .rightMirrored : .left
  252 + case .landscapeRight:
  253 + return position == .front ? .upMirrored : .down
  254 + case .faceDown, .faceUp, .unknown:
  255 + return .up
  256 + @unknown default:
  257 + return imageOrientation(deviceOrientation: defaultOrientation, defaultOrientation: .portrait, position: .back)
  258 + }
  259 + }
  260 +
  261 + /// Sends output of OutputBuffer to a Flutter texture
  262 + public func copyPixelBuffer() -> Unmanaged<CVPixelBuffer>? {
  263 + if latestBuffer == nil {
  264 + return nil
  265 + }
  266 + return Unmanaged<CVPixelBuffer>.passRetained(latestBuffer)
  267 + }
  268 +
  269 + struct MobileScannerStartParameters {
  270 + var width: Double = 0.0
  271 + var height: Double = 0.0
  272 + var hasTorch = false
  273 + var textureId: Int64 = 0
  274 + }
  275 +}
  276 +
  1 +//
  2 +// MobileScannerError.swift
  3 +// mobile_scanner
  4 +//
  5 +// Created by Julian Steenbakker on 24/08/2022.
  6 +//
  7 +import Foundation
  8 +
  9 +enum MobileScannerError: Error {
  10 + case noCamera
  11 + case alreadyStarted
  12 + case alreadyStopped
  13 + case torchError(_ error: Error)
  14 + case cameraError(_ error: Error)
  15 + case torchWhenStopped
  16 + case analyzerError(_ error: Error)
  17 +}
@@ -21,7 +21,7 @@ extension CVBuffer { @@ -21,7 +21,7 @@ extension CVBuffer {
21 var image: UIImage { 21 var image: UIImage {
22 let ciImage = CIImage(cvPixelBuffer: self) 22 let ciImage = CIImage(cvPixelBuffer: self)
23 let cgImage = CIContext().createCGImage(ciImage, from: ciImage.extent) 23 let cgImage = CIContext().createCGImage(ciImage, from: ciImage.extent)
24 - return UIImage(cgImage: cgImage!) 24 + return UIImage(cgImage: cgImage!, scale: 1.0, orientation: UIImage.Orientation.left)
25 } 25 }
26 26
27 var image1: UIImage { 27 var image1: UIImage {
1 -//  
2 -// SwiftMobileScanner.swift  
3 -// mobile_scanner  
4 -//  
5 -// Created by Julian Steenbakker on 15/02/2022.  
6 -//  
7 -  
8 -import Foundation  
1 -import AVFoundation  
2 import Flutter 1 import Flutter
3 import MLKitVision 2 import MLKitVision
4 import MLKitBarcodeScanning 3 import MLKitBarcodeScanning
  4 +import AVFoundation
5 import UIKit 5 import UIKit
6 6
7 -public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, FlutterTexture, AVCaptureVideoDataOutputSampleBufferDelegate {  
8 -  
9 - let registry: FlutterTextureRegistry  
10 -  
11 - // Sink for publishing event changes  
12 - var sink: FlutterEventSink!  
13 -  
14 - // Texture id of the camera preview  
15 - var textureId: Int64! 7 +public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
16 8
17 - // Capture session of the camera  
18 - var captureSession: AVCaptureSession! 9 + /// The mobile scanner object that handles all logic
  10 + private let mobileScanner: MobileScanner
19 11
20 - // The selected camera  
21 - var device: AVCaptureDevice!  
22 -  
23 - // Image to be sent to the texture  
24 - var latestBuffer: CVImageBuffer!  
25 -  
26 -// var analyzeMode: Int = 0  
27 - var analyzing: Bool = false  
28 - var position = AVCaptureDevice.Position.back 12 + /// The handler sends all information via an event channel back to Flutter
  13 + private let barcodeHandler: BarcodeHandler
29 14
30 var scanWindow: CGRect? 15 var scanWindow: CGRect?
31 16
32 - var scanner = BarcodeScanner.barcodeScanner()  
33 -  
34 - public static func register(with registrar: FlutterPluginRegistrar) {  
35 - let instance = SwiftMobileScannerPlugin(registrar.textures())  
36 -  
37 - let method = FlutterMethodChannel(name:  
38 - "dev.steenbakker.mobile_scanner/scanner/method", binaryMessenger: registrar.messenger())  
39 - let event = FlutterEventChannel(name:  
40 - "dev.steenbakker.mobile_scanner/scanner/event", binaryMessenger: registrar.messenger())  
41 - registrar.addMethodCallDelegate(instance, channel: method)  
42 - event.setStreamHandler(instance) 17 + init(barcodeHandler: BarcodeHandler, registry: FlutterTextureRegistry) {
  18 + self.mobileScanner = MobileScanner(registry: registry, mobileScannerCallback: { barcodes, error, image in
  19 + if barcodes != nil {
  20 + let barcodesMap = barcodes!.map { barcode in
  21 + return barcode.data
43 } 22 }
44 -  
45 - init(_ registry: FlutterTextureRegistry) {  
46 - self.registry = registry 23 + if (!barcodesMap.isEmpty) {
  24 + barcodeHandler.publishEvent(["name": "barcode", "data": barcodesMap, "image": FlutterStandardTypedData(bytes: image.jpegData(compressionQuality: 0.8)!)])
  25 + }
  26 + } else if (error != nil){
  27 + barcodeHandler.publishEvent(["name": "error", "data": error!.localizedDescription])
  28 + }
  29 + })
  30 + self.barcodeHandler = barcodeHandler
47 super.init() 31 super.init()
48 } 32 }
49 33
  34 + public static func register(with registrar: FlutterPluginRegistrar) {
  35 + let instance = SwiftMobileScannerPlugin(barcodeHandler: BarcodeHandler(registrar: registrar), registry: registrar.textures())
  36 + let methodChannel = FlutterMethodChannel(name:
  37 + "dev.steenbakker.mobile_scanner/scanner/method", binaryMessenger: registrar.messenger())
  38 + registrar.addMethodCallDelegate(instance, channel: methodChannel)
  39 + }
50 40
51 public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { 41 public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
52 switch call.method { 42 switch call.method {
53 case "state": 43 case "state":
54 - checkPermission(call, result) 44 + result(mobileScanner.checkPermission())
55 case "request": 45 case "request":
56 - requestPermission(call, result) 46 + AVCaptureDevice.requestAccess(for: .video, completionHandler: { result($0) })
57 case "start": 47 case "start":
58 start(call, result) 48 start(call, result)
59 - case "torch":  
60 - toggleTorch(call, result)  
61 -// case "analyze":  
62 -// switchAnalyzeMode(call, result)  
63 case "stop": 49 case "stop":
64 stop(result) 50 stop(result)
  51 + case "torch":
  52 + toggleTorch(call, result)
65 case "analyzeImage": 53 case "analyzeImage":
66 analyzeImage(call, result) 54 analyzeImage(call, result)
67 case "updateScanWindow": 55 case "updateScanWindow":
@@ -71,253 +59,78 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan @@ -71,253 +59,78 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan
71 } 59 }
72 } 60 }
73 61
74 - // FlutterStreamHandler  
75 - public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {  
76 - sink = events  
77 - return nil  
78 - }  
79 -  
80 - // FlutterStreamHandler  
81 - public func onCancel(withArguments arguments: Any?) -> FlutterError? {  
82 - sink = nil  
83 - return nil  
84 - }  
85 -  
86 - // FlutterTexture  
87 - public func copyPixelBuffer() -> Unmanaged<CVPixelBuffer>? {  
88 - if latestBuffer == nil {  
89 - return nil  
90 - }  
91 - return Unmanaged<CVPixelBuffer>.passRetained(latestBuffer)  
92 - }  
93 -  
94 - // Gets called when a new image is added to the buffer  
95 - public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {  
96 -  
97 - latestBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)  
98 - registry.textureFrameAvailable(textureId)  
99 -  
100 -// switch analyzeMode {  
101 -// case 1: // barcode  
102 - if analyzing {  
103 - return  
104 - }  
105 - analyzing = true  
106 - let buffer = CMSampleBufferGetImageBuffer(sampleBuffer)  
107 - var image = VisionImage(image: buffer!.image)  
108 -  
109 - image.orientation = imageOrientation(  
110 - deviceOrientation: UIDevice.current.orientation,  
111 - defaultOrientation: .portrait  
112 - )  
113 -  
114 - scanner.process(image) { [self] barcodes, error in  
115 - if error == nil && barcodes != nil {  
116 - for barcode in barcodes! {  
117 -  
118 - if scanWindow != nil {  
119 - let match = isbarCodeInScanWindow(scanWindow!, barcode, buffer!.image)  
120 - if (!match) {  
121 - continue  
122 - }  
123 - }  
124 -  
125 - let event: [String: Any?] = ["name": "barcode", "data": barcode.data]  
126 - sink?(event)  
127 - }  
128 - }  
129 - analyzing = false  
130 - }  
131 -// default: // none  
132 -// break  
133 -// }  
134 - } 62 + /// Parses all parameters and starts the mobileScanner
  63 + private func start(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
  64 + let torch: Bool = (call.arguments as! Dictionary<String, Any?>)["torch"] as? Bool ?? false
  65 + let facing: Int = (call.arguments as! Dictionary<String, Any?>)["facing"] as? Int ?? 1
  66 + let formats: Array<Int> = (call.arguments as! Dictionary<String, Any?>)["formats"] as? Array ?? []
  67 + let returnImage: Bool = (call.arguments as! Dictionary<String, Any?>)["returnImage"] as? Bool ?? false
  68 + let speed: Int = (call.arguments as! Dictionary<String, Any?>)["speed"] as? Int ?? 0
135 69
136 - func imageOrientation(  
137 - deviceOrientation: UIDeviceOrientation,  
138 - defaultOrientation: UIDeviceOrientation  
139 - ) -> UIImage.Orientation {  
140 - switch deviceOrientation {  
141 - case .portrait:  
142 - return position == .front ? .leftMirrored : .right  
143 - case .landscapeLeft:  
144 - return position == .front ? .downMirrored : .up  
145 - case .portraitUpsideDown:  
146 - return position == .front ? .rightMirrored : .left  
147 - case .landscapeRight:  
148 - return position == .front ? .upMirrored : .down  
149 - case .faceDown, .faceUp, .unknown:  
150 - return .up  
151 - @unknown default:  
152 - return imageOrientation(deviceOrientation: defaultOrientation, defaultOrientation: .portrait)  
153 - }  
154 - }  
155 -  
156 - func checkPermission(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {  
157 - let status = AVCaptureDevice.authorizationStatus(for: .video)  
158 - switch status {  
159 - case .notDetermined:  
160 - result(0)  
161 - case .authorized:  
162 - result(1)  
163 - default:  
164 - result(2)  
165 - }  
166 - } 70 + let formatList = formats.map { format in return BarcodeFormat(rawValue: format)}
  71 + var barcodeOptions: BarcodeScannerOptions? = nil
167 72
168 - func requestPermission(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {  
169 - AVCaptureDevice.requestAccess(for: .video, completionHandler: { result($0) })  
170 - }  
171 -  
172 - func updateScanWindow(_ call: FlutterMethodCall) {  
173 - let argReader = MapArgumentReader(call.arguments as? [String: Any])  
174 - let scanWindowData: Array? = argReader.floatArray(key: "rect")  
175 -  
176 - if (scanWindowData == nil) {  
177 - return 73 + if (formatList.count != 0) {
  74 + var barcodeFormats: BarcodeFormat = []
  75 + for index in formats {
  76 + barcodeFormats.insert(BarcodeFormat(rawValue: index))
178 } 77 }
179 -  
180 - let minX = scanWindowData![0]  
181 - let minY = scanWindowData![1]  
182 -  
183 - let width = scanWindowData![2] - minX  
184 - let height = scanWindowData![3] - minY  
185 -  
186 - scanWindow = CGRect(x: minX, y: minY, width: width, height: height) 78 + barcodeOptions = BarcodeScannerOptions(formats: barcodeFormats)
187 } 79 }
188 80
189 - func isbarCodeInScanWindow(_ scanWindow: CGRect, _ barcode: Barcode, _ inputImage: UIImage) -> Bool {  
190 - let barcodeBoundingBox = barcode.frame  
191 -  
192 - let imageWidth = inputImage.size.width;  
193 - let imageHeight = inputImage.size.height;  
194 81
195 - let minX = scanWindow.minX * imageWidth  
196 - let minY = scanWindow.minY * imageHeight  
197 - let width = scanWindow.width * imageWidth  
198 - let height = scanWindow.height * imageHeight 82 + let position = facing == 0 ? AVCaptureDevice.Position.front : .back
  83 + let detectionSpeed: DetectionSpeed = DetectionSpeed(rawValue: speed)!
199 84
200 - let scaledScanWindow = CGRect(x: minX, y: minY, width: width, height: height)  
201 - return scaledScanWindow.contains(barcodeBoundingBox)  
202 - }  
203 -  
204 - func start(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {  
205 - if (device != nil) { 85 + do {
  86 + let parameters = try mobileScanner.start(barcodeScannerOptions: barcodeOptions, returnImage: returnImage, cameraPosition: position, torch: torch ? .on : .off, detectionSpeed: detectionSpeed)
  87 + result(["textureId": parameters.textureId, "size": ["width": parameters.width, "height": parameters.height], "torchable": parameters.hasTorch])
  88 + } catch MobileScannerError.alreadyStarted {
206 result(FlutterError(code: "MobileScanner", 89 result(FlutterError(code: "MobileScanner",
207 message: "Called start() while already started!", 90 message: "Called start() while already started!",
208 details: nil)) 91 details: nil))
209 - return  
210 - }  
211 -  
212 - textureId = registry.register(self)  
213 - captureSession = AVCaptureSession()  
214 -  
215 - let argReader = MapArgumentReader(call.arguments as? [String: Any])  
216 -  
217 -// let ratio: Int = argReader.int(key: "ratio")  
218 - let torch: Bool = argReader.bool(key: "torch") ?? false  
219 - let facing: Int = argReader.int(key: "facing") ?? 1  
220 - let formats: Array = argReader.intArray(key: "formats") ?? []  
221 -  
222 - let formatList: NSMutableArray = []  
223 - for index in formats {  
224 - formatList.add(BarcodeFormat(rawValue: index))  
225 - }  
226 -  
227 - if (formatList.count != 0) {  
228 - let barcodeOptions = BarcodeScannerOptions(formats: formatList.firstObject as! BarcodeFormat)  
229 - scanner = BarcodeScanner.barcodeScanner(options: barcodeOptions)  
230 - }  
231 -  
232 - // Set the camera to use  
233 - position = facing == 0 ? AVCaptureDevice.Position.front : .back  
234 -  
235 - // Open the camera device  
236 - if #available(iOS 10.0, *) {  
237 - device = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: position).devices.first  
238 - } else {  
239 - device = AVCaptureDevice.devices(for: .video).filter({$0.position == position}).first  
240 - }  
241 -  
242 - if (device == nil) { 92 + } catch MobileScannerError.noCamera {
243 result(FlutterError(code: "MobileScanner", 93 result(FlutterError(code: "MobileScanner",
244 message: "No camera found or failed to open camera!", 94 message: "No camera found or failed to open camera!",
245 details: nil)) 95 details: nil))
246 - return  
247 - }  
248 -  
249 - // Enable the torch if parameter is set and torch is available  
250 - if (device.hasTorch && device.isTorchAvailable) {  
251 - do {  
252 - try device.lockForConfiguration()  
253 - device.torchMode = torch ? .on : .off  
254 - device.unlockForConfiguration() 96 + } catch MobileScannerError.torchError(let error) {
  97 + result(FlutterError(code: "MobileScanner",
  98 + message: "Error occured when setting torch!",
  99 + details: error))
  100 + } catch MobileScannerError.cameraError(let error) {
  101 + result(FlutterError(code: "MobileScanner",
  102 + message: "Error occured when setting up camera!",
  103 + details: error))
255 } catch { 104 } catch {
256 - error.throwNative(result) 105 + result(FlutterError(code: "MobileScanner",
  106 + message: "Unknown error occured..",
  107 + details: nil))
257 } 108 }
258 } 109 }
259 110
260 - device.addObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode), options: .new, context: nil)  
261 - captureSession.beginConfiguration()  
262 -  
263 - // Add device input 111 + /// Stops the mobileScanner and closes the texture
  112 + private func stop(_ result: @escaping FlutterResult) {
264 do { 113 do {
265 - let input = try AVCaptureDeviceInput(device: device)  
266 - captureSession.addInput(input)  
267 - } catch {  
268 - error.throwNative(result)  
269 - }  
270 - captureSession.sessionPreset = AVCaptureSession.Preset.photo;  
271 - // Add video output.  
272 - let videoOutput = AVCaptureVideoDataOutput()  
273 -  
274 - videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA]  
275 - videoOutput.alwaysDiscardsLateVideoFrames = true  
276 -  
277 - videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue.main)  
278 -  
279 - captureSession.addOutput(videoOutput)  
280 - for connection in videoOutput.connections {  
281 - connection.videoOrientation = .portrait  
282 - if position == .front && connection.isVideoMirroringSupported {  
283 - connection.isVideoMirrored = true  
284 - }  
285 - }  
286 - captureSession.commitConfiguration()  
287 - captureSession.startRunning()  
288 -  
289 - let demensions = CMVideoFormatDescriptionGetDimensions(device.activeFormat.formatDescription)  
290 - let width = Double(demensions.height)  
291 - let height = Double(demensions.width)  
292 - let size = ["width": width, "height": height]  
293 - let answer: [String : Any?] = ["textureId": textureId, "size": size, "torchable": device.hasTorch]  
294 - result(answer) 114 + try mobileScanner.stop()
  115 + } catch {}
  116 + result(nil)
295 } 117 }
296 118
297 - func toggleTorch(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {  
298 - if (device == nil) { 119 + /// Toggles the torch
  120 + private func toggleTorch(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
  121 + do {
  122 + try mobileScanner.toggleTorch(call.arguments as? Int == 1 ? .on : .off)
  123 + } catch {
299 result(FlutterError(code: "MobileScanner", 124 result(FlutterError(code: "MobileScanner",
300 message: "Called toggleTorch() while stopped!", 125 message: "Called toggleTorch() while stopped!",
301 details: nil)) 126 details: nil))
302 - return  
303 } 127 }
304 - do {  
305 - try device.lockForConfiguration()  
306 - device.torchMode = call.arguments as! Int == 1 ? .on : .off  
307 - device.unlockForConfiguration()  
308 result(nil) 128 result(nil)
309 - } catch {  
310 - error.throwNative(result)  
311 - }  
312 } 129 }
313 130
314 -// func switchAnalyzeMode(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {  
315 -// analyzeMode = call.arguments as! Int  
316 -// result(nil)  
317 -// }  
318 -  
319 - func analyzeImage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {  
320 - let uiImage = UIImage(contentsOfFile: call.arguments as! String) 131 + /// Analyzes a single image
  132 + private func analyzeImage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
  133 + let uiImage = UIImage(contentsOfFile: call.arguments as? String ?? "")
321 134
322 if (uiImage == nil) { 135 if (uiImage == nil) {
323 result(FlutterError(code: "MobileScanner", 136 result(FlutterError(code: "MobileScanner",
@@ -325,111 +138,34 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan @@ -325,111 +138,34 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan
325 details: nil)) 138 details: nil))
326 return 139 return
327 } 140 }
328 -  
329 - let image = VisionImage(image: uiImage!)  
330 - image.orientation = imageOrientation(  
331 - deviceOrientation: UIDevice.current.orientation,  
332 - defaultOrientation: .portrait  
333 - )  
334 -  
335 - var barcodeFound = false  
336 -  
337 - scanner.process(image) { [self] barcodes, error in 141 + mobileScanner.analyzeImage(image: uiImage!, position: AVCaptureDevice.Position.back, callback: { [self] barcodes, error in
338 if error == nil && barcodes != nil { 142 if error == nil && barcodes != nil {
339 for barcode in barcodes! { 143 for barcode in barcodes! {
340 -  
341 if scanWindow != nil { 144 if scanWindow != nil {
342 - let match = isbarCodeInScanWindow(scanWindow!, barcode, uiImage!) 145 + let match = isbarCodeInScanWindow(scanWindow!, barcode, buffer!.image)
343 if (!match) { 146 if (!match) {
344 continue 147 continue
345 } 148 }
346 } 149 }
347 -  
348 - barcodeFound = true  
349 let event: [String: Any?] = ["name": "barcode", "data": barcode.data] 150 let event: [String: Any?] = ["name": "barcode", "data": barcode.data]
350 - sink?(event) 151 + barcodeHandler.publishEvent(event)
351 } 152 }
352 } else if error != nil { 153 } else if error != nil {
353 - result(FlutterError(code: "MobileScanner",  
354 - message: error?.localizedDescription,  
355 - details: "analyzeImage()"))  
356 - }  
357 - analyzing = false  
358 - result(barcodeFound)  
359 - }  
360 - 154 + barcodeHandler.publishEvent(["name": "error", "message": error?.localizedDescription])
361 } 155 }
362 -  
363 - func stop(_ result: FlutterResult) {  
364 - if (device == nil) {  
365 - result(FlutterError(code: "MobileScanner",  
366 - message: "Called stop() while already stopped!",  
367 - details: nil))  
368 - return  
369 - }  
370 - captureSession.stopRunning()  
371 - for input in captureSession.inputs {  
372 - captureSession.removeInput(input)  
373 - }  
374 - for output in captureSession.outputs {  
375 - captureSession.removeOutput(output)  
376 - }  
377 - device.removeObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode))  
378 - registry.unregisterTexture(textureId)  
379 -  
380 -// analyzeMode = 0  
381 - latestBuffer = nil  
382 - captureSession = nil  
383 - device = nil  
384 - textureId = nil  
385 - 156 + })
386 result(nil) 157 result(nil)
387 } 158 }
388 159
389 - // Observer for torch state 160 + /// Observer for torch state
390 public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { 161 public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
391 switch keyPath { 162 switch keyPath {
392 case "torchMode": 163 case "torchMode":
393 // off = 0; on = 1; auto = 2; 164 // off = 0; on = 1; auto = 2;
394 let state = change?[.newKey] as? Int 165 let state = change?[.newKey] as? Int
395 - let event: [String: Any?] = ["name": "torchState", "data": state]  
396 - sink?(event) 166 + barcodeHandler.publishEvent(["name": "torchState", "data": state])
397 default: 167 default:
398 break 168 break
399 } 169 }
400 } 170 }
401 } 171 }
402 -  
403 -class MapArgumentReader {  
404 -  
405 - let args: [String: Any]?  
406 -  
407 - init(_ args: [String: Any]?) {  
408 - self.args = args  
409 - }  
410 -  
411 - func string(key: String) -> String? {  
412 - return args?[key] as? String  
413 - }  
414 -  
415 - func int(key: String) -> Int? {  
416 - return (args?[key] as? NSNumber)?.intValue  
417 - }  
418 -  
419 - func bool(key: String) -> Bool? {  
420 - return (args?[key] as? NSNumber)?.boolValue  
421 - }  
422 -  
423 - func stringArray(key: String) -> [String]? {  
424 - return args?[key] as? [String]  
425 - }  
426 -  
427 - func intArray(key: String) -> [Int]? {  
428 - return args?[key] as? [Int]  
429 - }  
430 -  
431 - func floatArray(key: String) -> [CGFloat]? {  
432 - return args?[key] as? [CGFloat]  
433 - }  
434 -  
435 -}  
@@ -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 = '0.0.1' 7 + s.version = '3.0.0'
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.
@@ -15,8 +15,8 @@ An universal scanner for Flutter based on MLKit. @@ -15,8 +15,8 @@ An universal scanner for Flutter based on MLKit.
15 s.source = { :path => '.' } 15 s.source = { :path => '.' }
16 s.source_files = 'Classes/**/*' 16 s.source_files = 'Classes/**/*'
17 s.dependency 'Flutter' 17 s.dependency 'Flutter'
18 - s.dependency 'GoogleMLKit/BarcodeScanning', '~> 2.6.0'  
19 - s.platform = :ios, '10.0' 18 + s.dependency 'GoogleMLKit/BarcodeScanning', '~> 3.2.0'
  19 + s.platform = :ios, '11.0'
20 s.static_framework = true 20 s.static_framework = true
21 # Flutter.framework does not contain a i386 slice. 21 # Flutter.framework does not contain a i386 slice.
22 s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } 22 s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
1 library mobile_scanner; 1 library mobile_scanner;
2 2
  3 +export 'src/enums/camera_facing.dart';
  4 +export 'src/enums/detection_speed.dart';
  5 +export 'src/enums/mobile_scanner_error_code.dart';
  6 +export 'src/enums/mobile_scanner_state.dart';
  7 +export 'src/enums/ratio.dart';
  8 +export 'src/enums/torch_state.dart';
3 export 'src/mobile_scanner.dart'; 9 export 'src/mobile_scanner.dart';
4 -export 'src/mobile_scanner_arguments.dart';  
5 export 'src/mobile_scanner_controller.dart'; 10 export 'src/mobile_scanner_controller.dart';
  11 +export 'src/mobile_scanner_exception.dart';
6 export 'src/objects/barcode.dart'; 12 export 'src/objects/barcode.dart';
  13 +export 'src/objects/barcode_capture.dart';
  14 +export 'src/objects/mobile_scanner_arguments.dart';
  1 +library mobile_scanner_web;
  2 +
  3 +export 'src/web/base.dart';
  4 +export 'src/web/jsqr.dart';
  5 +export 'src/web/zxing.dart';
@@ -2,12 +2,10 @@ import 'dart:async'; @@ -2,12 +2,10 @@ import 'dart:async';
2 import 'dart:html' as html; 2 import 'dart:html' as html;
3 import 'dart:ui' as ui; 3 import 'dart:ui' as ui;
4 4
5 -import 'package:flutter/material.dart';  
6 import 'package:flutter/services.dart'; 5 import 'package:flutter/services.dart';
7 import 'package:flutter_web_plugins/flutter_web_plugins.dart'; 6 import 'package:flutter_web_plugins/flutter_web_plugins.dart';
8 -import 'package:mobile_scanner/mobile_scanner.dart';  
9 -import 'package:mobile_scanner/src/web/jsqr.dart';  
10 -import 'package:mobile_scanner/src/web/media.dart'; 7 +import 'package:mobile_scanner/mobile_scanner_web.dart';
  8 +import 'package:mobile_scanner/src/enums/camera_facing.dart';
11 9
12 /// This plugin is the web implementation of mobile_scanner. 10 /// This plugin is the web implementation of mobile_scanner.
13 /// It only supports QR codes. 11 /// It only supports QR codes.
@@ -24,7 +22,6 @@ class MobileScannerWebPlugin { @@ -24,7 +22,6 @@ class MobileScannerWebPlugin {
24 registrar, 22 registrar,
25 ); 23 );
26 final MobileScannerWebPlugin instance = MobileScannerWebPlugin(); 24 final MobileScannerWebPlugin instance = MobileScannerWebPlugin();
27 - WidgetsFlutterBinding.ensureInitialized();  
28 25
29 channel.setMethodCallHandler(instance.handleMethodCall); 26 channel.setMethodCallHandler(instance.handleMethodCall);
30 event.setController(instance.controller); 27 event.setController(instance.controller);
@@ -33,20 +30,14 @@ class MobileScannerWebPlugin { @@ -33,20 +30,14 @@ class MobileScannerWebPlugin {
33 // Controller to send events back to the framework 30 // Controller to send events back to the framework
34 StreamController controller = StreamController.broadcast(); 31 StreamController controller = StreamController.broadcast();
35 32
36 - // The video stream. Will be initialized later to see which camera needs to be used.  
37 - html.MediaStream? _localStream;  
38 - html.VideoElement video = html.VideoElement();  
39 -  
40 // ID of the video feed 33 // ID of the video feed
41 String viewID = 'WebScanner-${DateTime.now().millisecondsSinceEpoch}'; 34 String viewID = 'WebScanner-${DateTime.now().millisecondsSinceEpoch}';
42 35
43 - // Determine wether device has flas  
44 - bool hasFlash = false;  
45 -  
46 - // Timer used to capture frames to be analyzed  
47 - Timer? _frameInterval; 36 + static final html.DivElement vidDiv = html.DivElement();
48 37
49 - html.DivElement vidDiv = html.DivElement(); 38 + static WebBarcodeReaderBase barCodeReader =
  39 + ZXingBarcodeReader(videoContainer: vidDiv);
  40 + StreamSubscription? _barCodeStreamSubscription;
50 41
51 /// Handle incomming messages 42 /// Handle incomming messages
52 Future<dynamic> handleMethodCall(MethodCall call) async { 43 Future<dynamic> handleMethodCall(MethodCall call) async {
@@ -68,20 +59,11 @@ class MobileScannerWebPlugin { @@ -68,20 +59,11 @@ class MobileScannerWebPlugin {
68 59
69 /// Can enable or disable the flash if available 60 /// Can enable or disable the flash if available
70 Future<void> _torch(arguments) async { 61 Future<void> _torch(arguments) async {
71 - if (hasFlash) {  
72 - final track = _localStream?.getVideoTracks();  
73 - await track!.first.applyConstraints({  
74 - 'advanced': {'torch': arguments == 1}  
75 - });  
76 - } else {  
77 - controller.addError('Device has no flash');  
78 - } 62 + barCodeReader.toggleTorch(enabled: arguments == 1);
79 } 63 }
80 64
81 /// Starts the video stream and the scanner 65 /// Starts the video stream and the scanner
82 Future<Map> _start(Map arguments) async { 66 Future<Map> _start(Map arguments) async {
83 - vidDiv.children = [video];  
84 -  
85 var cameraFacing = CameraFacing.front; 67 var cameraFacing = CameraFacing.front;
86 if (arguments.containsKey('facing')) { 68 if (arguments.containsKey('facing')) {
87 cameraFacing = CameraFacing.values[arguments['facing'] as int]; 69 cameraFacing = CameraFacing.values[arguments['facing'] as int];
@@ -91,64 +73,45 @@ class MobileScannerWebPlugin { @@ -91,64 +73,45 @@ class MobileScannerWebPlugin {
91 // ignore: UNDEFINED_PREFIXED_NAME, avoid_dynamic_calls 73 // ignore: UNDEFINED_PREFIXED_NAME, avoid_dynamic_calls
92 ui.platformViewRegistry.registerViewFactory( 74 ui.platformViewRegistry.registerViewFactory(
93 viewID, 75 viewID,
94 - (int id) => vidDiv 76 + (int id) {
  77 + return vidDiv
95 ..style.width = '100%' 78 ..style.width = '100%'
96 - ..style.height = '100%', 79 + ..style.height = '100%';
  80 + },
97 ); 81 );
98 82
99 // Check if stream is running 83 // Check if stream is running
100 - if (_localStream != null) { 84 + if (barCodeReader.isStarted) {
101 return { 85 return {
102 'ViewID': viewID, 86 'ViewID': viewID,
103 - 'videoWidth': video.videoWidth,  
104 - 'videoHeight': video.videoHeight 87 + 'videoWidth': barCodeReader.videoWidth,
  88 + 'videoHeight': barCodeReader.videoHeight,
  89 + 'torchable': barCodeReader.hasTorch,
105 }; 90 };
106 } 91 }
107 -  
108 try { 92 try {
109 - // Check if browser supports multiple camera's and set if supported  
110 - final Map? capabilities =  
111 - html.window.navigator.mediaDevices?.getSupportedConstraints();  
112 - if (capabilities != null && capabilities['facingMode'] as bool) {  
113 - final constraints = {  
114 - 'video': VideoOptions(  
115 - facingMode:  
116 - cameraFacing == CameraFacing.front ? 'user' : 'environment',  
117 - )  
118 - }; 93 + await barCodeReader.start(
  94 + cameraFacing: cameraFacing,
  95 + );
119 96
120 - _localStream =  
121 - await html.window.navigator.mediaDevices?.getUserMedia(constraints);  
122 - } else {  
123 - _localStream = await html.window.navigator.mediaDevices  
124 - ?.getUserMedia({'video': true}); 97 + _barCodeStreamSubscription =
  98 + barCodeReader.detectBarcodeContinuously().listen((code) {
  99 + if (code != null) {
  100 + controller.add({
  101 + 'name': 'barcodeWeb',
  102 + 'data': {
  103 + 'rawValue': code.rawValue,
  104 + 'rawBytes': code.rawBytes,
  105 + },
  106 + });
125 } 107 }
126 -  
127 - video.srcObject = _localStream;  
128 -  
129 - // TODO: fix flash light. See https://github.com/dart-lang/sdk/issues/48533  
130 - // final track = _localStream?.getVideoTracks();  
131 - // if (track != null) {  
132 - // final imageCapture = html.ImageCapture(track.first);  
133 - // final photoCapabilities = await imageCapture.getPhotoCapabilities();  
134 - // }  
135 -  
136 - // required to tell iOS safari we don't want fullscreen  
137 - video.setAttribute('playsinline', 'true');  
138 -  
139 - await video.play();  
140 -  
141 - // Then capture a frame to be analyzed every 200 miliseconds  
142 - _frameInterval =  
143 - Timer.periodic(const Duration(milliseconds: 200), (timer) {  
144 - _captureFrame();  
145 }); 108 });
146 109
147 return { 110 return {
148 'ViewID': viewID, 111 'ViewID': viewID,
149 - 'videoWidth': video.videoWidth,  
150 - 'videoHeight': video.videoHeight,  
151 - 'torchable': hasFlash 112 + 'videoWidth': barCodeReader.videoWidth,
  113 + 'videoHeight': barCodeReader.videoHeight,
  114 + 'torchable': barCodeReader.hasTorch,
152 }; 115 };
153 } catch (e) { 116 } catch (e) {
154 throw PlatformException(code: 'MobileScannerWeb', message: '$e'); 117 throw PlatformException(code: 'MobileScannerWeb', message: '$e');
@@ -171,36 +134,8 @@ class MobileScannerWebPlugin { @@ -171,36 +134,8 @@ class MobileScannerWebPlugin {
171 134
172 /// Stops the video feed and analyzer 135 /// Stops the video feed and analyzer
173 Future<void> cancel() async { 136 Future<void> cancel() async {
174 - try {  
175 - // Stop the camera stream  
176 - _localStream?.getTracks().forEach((track) {  
177 - if (track.readyState == 'live') {  
178 - track.stop();  
179 - }  
180 - });  
181 - } catch (e) {  
182 - debugPrint('Failed to stop stream: $e');  
183 - }  
184 -  
185 - video.srcObject = null;  
186 - _localStream = null;  
187 - _frameInterval?.cancel();  
188 - _frameInterval = null;  
189 - }  
190 -  
191 - /// Captures a frame and analyzes it for QR codes  
192 - Future<dynamic> _captureFrame() async {  
193 - if (_localStream == null) return null;  
194 - final canvas =  
195 - html.CanvasElement(width: video.videoWidth, height: video.videoHeight);  
196 - final ctx = canvas.context2D;  
197 -  
198 - ctx.drawImage(video, 0, 0);  
199 - final imgData = ctx.getImageData(0, 0, canvas.width!, canvas.height!);  
200 -  
201 - final code = jsQR(imgData.data, canvas.width, canvas.height);  
202 - if (code != null) {  
203 - controller.add({'name': 'barcodeWeb', 'data': code.data});  
204 - } 137 + barCodeReader.stop();
  138 + await _barCodeStreamSubscription?.cancel();
  139 + _barCodeStreamSubscription = null;
205 } 140 }
206 } 141 }
  1 +/// The facing of a camera.
  2 +enum CameraFacing {
  3 + /// Front facing camera.
  4 + front,
  5 +
  6 + /// Back facing camera.
  7 + back,
  8 +}
  1 +/// The detection speed of the scanner.
  2 +enum DetectionSpeed {
  3 + /// The scanner will only scan a barcode once, and never again until another
  4 + /// barcode has been scanned.
  5 + ///
  6 + /// NOTE: This mode does analyze every frame in order to check if the value
  7 + /// has changed.
  8 + noDuplicates,
  9 +
  10 + /// The barcode scanner will scan one barcode, and wait 250 Miliseconds before
  11 + /// scanning again. This will prevent memory issues on older devices.
  12 + ///
  13 + /// You can change the timeout duration with [detectionTimeout] parameter.
  14 + normal,
  15 +
  16 + /// Let the scanner detect barcodes without restriction.
  17 + ///
  18 + /// NOTE: This can cause memory issues with older devices.
  19 + unrestricted,
  20 +}
  1 +/// This enum defines the different error codes for the mobile scanner.
  2 +enum MobileScannerErrorCode {
  3 + /// The controller was used
  4 + /// while it was not yet initialized using [MobileScannerController.start].
  5 + controllerUninitialized,
  6 +
  7 + /// A generic error occurred.
  8 + ///
  9 + /// This error code is used for all errors that do not have a specific error code.
  10 + genericError,
  11 +
  12 + /// The permission to use the camera was denied.
  13 + permissionDenied,
  14 +}
  1 +/// The authorization state of the scanner.
  2 +enum MobileScannerState {
  3 + /// The scanner has not yet requested the required permissions.
  4 + undetermined,
  5 +
  6 + /// The scanner has the required permissions.
  7 + authorized,
  8 +
  9 + /// The user denied the required permissions.
  10 + denied
  11 +}
  1 +// This enum is not used yet
  2 +// enum Ratio { ratio_4_3, ratio_16_9 }
  1 +/// The state of torch.
  2 +enum TorchState {
  3 + /// Torch is off.
  4 + off,
  5 +
  6 + /// Torch is on.
  7 + on,
  8 +}
  1 +import 'dart:async';
  2 +
1 import 'package:flutter/foundation.dart'; 3 import 'package:flutter/foundation.dart';
2 import 'package:flutter/material.dart' hide applyBoxFit; 4 import 'package:flutter/material.dart' hide applyBoxFit;
3 -import 'package:mobile_scanner/mobile_scanner.dart';  
4 -import 'package:mobile_scanner/src/objects/barcode_utility.dart';  
5 -  
6 -enum Ratio { ratio_4_3, ratio_16_9 } 5 +import 'package:mobile_scanner/src/mobile_scanner_controller.dart';
  6 +import 'package:mobile_scanner/src/objects/barcode_capture.dart';
  7 +import 'package:mobile_scanner/src/objects/mobile_scanner_arguments.dart';
7 8
8 -/// A widget showing a live camera preview. 9 +/// The [MobileScanner] widget displays a live camera preview.
9 class MobileScanner extends StatefulWidget { 10 class MobileScanner extends StatefulWidget {
10 - /// The controller of the camera. 11 + /// The controller that manages the barcode scanner.
  12 + ///
  13 + /// If this is null, the scanner will manage its own controller.
11 final MobileScannerController? controller; 14 final MobileScannerController? controller;
12 15
13 - /// Function that gets called when a Barcode is detected. 16 + /// The [BoxFit] for the camera preview.
14 /// 17 ///
15 - /// [barcode] The barcode object with all information about the scanned code.  
16 - /// [args] Information about the state of the MobileScanner widget  
17 - final Function(Barcode barcode, MobileScannerArguments? args) onDetect; 18 + /// Defaults to [BoxFit.cover].
  19 + final BoxFit fit;
18 20
19 - /// TODO: Function that gets called when the Widget is initialized. Can be usefull  
20 - /// to check wether the device has a torch(flash) or not.  
21 - ///  
22 - /// [args] Information about the state of the MobileScanner widget  
23 - // final Function(MobileScannerArguments args)? onInitialize; 21 + /// The function that signals when new codes were detected by the [controller].
  22 + final void Function(BarcodeCapture barcodes) onDetect;
24 23
25 - /// Handles how the widget should fit the screen.  
26 - final BoxFit fit; 24 + /// The function that signals when the barcode scanner is started.
  25 + @Deprecated('Use onScannerStarted() instead.')
  26 + final void Function(MobileScannerArguments? arguments)? onStart;
27 27
28 - /// Set to false if you don't want duplicate scans.  
29 - final bool allowDuplicates; 28 + /// The function that signals when the barcode scanner is started.
  29 + final void Function(MobileScannerArguments? arguments)? onScannerStarted;
  30 +
  31 + /// The function that builds a placeholder widget when the scanner
  32 + /// is not yet displaying its camera preview.
  33 + ///
  34 + /// If this is null, a black [ColoredBox] is used as placeholder.
  35 + final Widget Function(BuildContext, Widget?)? placeholderBuilder;
30 36
31 /// if set barcodes will only be scanned if they fall within this [Rect] 37 /// if set barcodes will only be scanned if they fall within this [Rect]
32 /// useful for having a cut-out overlay for example. these [Rect] 38 /// useful for having a cut-out overlay for example. these [Rect]
@@ -35,14 +41,17 @@ class MobileScanner extends StatefulWidget { @@ -35,14 +41,17 @@ class MobileScanner extends StatefulWidget {
35 /// [BoxFit] 41 /// [BoxFit]
36 final Rect? scanWindow; 42 final Rect? scanWindow;
37 43
38 - /// Create a [MobileScanner] with a [controller], the [controller] must has been initialized. 44 + /// Create a new [MobileScanner] using the provided [controller]
  45 + /// and [onBarcodeDetected] callback.
39 const MobileScanner({ 46 const MobileScanner({
40 - super.key,  
41 - required this.onDetect,  
42 this.controller, 47 this.controller,
43 this.fit = BoxFit.cover, 48 this.fit = BoxFit.cover,
44 - this.allowDuplicates = false, 49 + required this.onDetect,
  50 + @Deprecated('Use onScannerStarted() instead.') this.onStart,
  51 + this.onScannerStarted,
  52 + this.placeholderBuilder,
45 this.scanWindow, 53 this.scanWindow,
  54 + super.key,
46 }); 55 });
47 56
48 @override 57 @override
@@ -51,32 +60,71 @@ class MobileScanner extends StatefulWidget { @@ -51,32 +60,71 @@ class MobileScanner extends StatefulWidget {
51 60
52 class _MobileScannerState extends State<MobileScanner> 61 class _MobileScannerState extends State<MobileScanner>
53 with WidgetsBindingObserver { 62 with WidgetsBindingObserver {
54 - late MobileScannerController controller; 63 + /// The subscription that listens to barcode detection.
  64 + StreamSubscription<BarcodeCapture>? _barcodesSubscription;
  65 +
  66 + /// The internally managed controller.
  67 + late MobileScannerController _controller;
  68 +
  69 + /// Whether the controller should resume
  70 + /// when the application comes back to the foreground.
  71 + bool _resumeFromBackground = false;
  72 +
  73 + /// Start the given [scanner].
  74 + void _startScanner(MobileScannerController scanner) {
  75 + if (!_controller.autoStart) {
  76 + debugPrint(
  77 + 'mobile_scanner: not starting automatically because autoStart is set to false in the controller.',
  78 + );
  79 + return;
  80 + }
  81 + scanner.start().then((arguments) {
  82 + // ignore: deprecated_member_use_from_same_package
  83 + widget.onStart?.call(arguments);
  84 + widget.onScannerStarted?.call(arguments);
  85 + });
  86 + }
55 87
56 @override 88 @override
57 void initState() { 89 void initState() {
58 super.initState(); 90 super.initState();
59 WidgetsBinding.instance.addObserver(this); 91 WidgetsBinding.instance.addObserver(this);
60 - controller = widget.controller ?? MobileScannerController();  
61 - if (!controller.isStarting) controller.start(); 92 + _controller = widget.controller ?? MobileScannerController();
  93 +
  94 + _barcodesSubscription = _controller.barcodes.listen(
  95 + widget.onDetect,
  96 + );
  97 +
  98 + if (!_controller.isStarting) {
  99 + _startScanner(_controller);
  100 + }
62 } 101 }
63 102
64 @override 103 @override
65 void didChangeAppLifecycleState(AppLifecycleState state) { 104 void didChangeAppLifecycleState(AppLifecycleState state) {
  105 + // App state changed before the controller was initialized.
  106 + if (_controller.isStarting) {
  107 + return;
  108 + }
  109 +
66 switch (state) { 110 switch (state) {
67 case AppLifecycleState.resumed: 111 case AppLifecycleState.resumed:
68 - if (!controller.isStarting && controller.autoResume) controller.start(); 112 + _resumeFromBackground = false;
  113 + _startScanner(_controller);
69 break; 114 break;
70 - case AppLifecycleState.inactive:  
71 case AppLifecycleState.paused: 115 case AppLifecycleState.paused:
  116 + _resumeFromBackground = true;
  117 + break;
  118 + case AppLifecycleState.inactive:
  119 + if (!_resumeFromBackground) {
  120 + _controller.stop();
  121 + }
  122 + break;
72 case AppLifecycleState.detached: 123 case AppLifecycleState.detached:
73 - controller.stop();  
74 break; 124 break;
75 } 125 }
76 } 126 }
77 127
78 - String? lastScanned;  
79 -  
80 /// the [scanWindow] rect will be relative and scaled to the [widgetSize] not the texture. so it is possible, 128 /// the [scanWindow] rect will be relative and scaled to the [widgetSize] not the texture. so it is possible,
81 /// depending on the [fit], for the [scanWindow] to partially or not at all overlap the [textureSize] 129 /// depending on the [fit], for the [scanWindow] to partially or not at all overlap the [textureSize]
82 /// 130 ///
@@ -128,15 +176,14 @@ class _MobileScannerState extends State<MobileScanner> @@ -128,15 +176,14 @@ class _MobileScannerState extends State<MobileScanner>
128 176
129 @override 177 @override
130 Widget build(BuildContext context) { 178 Widget build(BuildContext context) {
131 - return LayoutBuilder(  
132 - builder: (context, BoxConstraints constraints) {  
133 - return ValueListenableBuilder(  
134 - valueListenable: controller.args, 179 + return ValueListenableBuilder<MobileScannerArguments?>(
  180 + valueListenable: _controller.startArguments,
135 builder: (context, value, child) { 181 builder: (context, value, child) {
136 - value = value as MobileScannerArguments?;  
137 if (value == null) { 182 if (value == null) {
138 - return const ColoredBox(color: Colors.black);  
139 - } else { 183 + return widget.placeholderBuilder?.call(context, child) ??
  184 + const ColoredBox(color: Colors.black);
  185 + }
  186 +
140 if (widget.scanWindow != null) { 187 if (widget.scanWindow != null) {
141 final window = calculateScanWindowRelativeToTextureInPercentage( 188 final window = calculateScanWindowRelativeToTextureInPercentage(
142 widget.fit, 189 widget.fit,
@@ -147,19 +194,12 @@ class _MobileScannerState extends State<MobileScanner> @@ -147,19 +194,12 @@ class _MobileScannerState extends State<MobileScanner>
147 controller.updateScanWindow(window); 194 controller.updateScanWindow(window);
148 } 195 }
149 196
150 - controller.barcodes.listen((barcode) {  
151 - if (!widget.allowDuplicates) {  
152 - if (lastScanned == barcode.rawValue) return;  
153 - lastScanned = barcode.rawValue;  
154 - widget.onDetect(barcode, value! as MobileScannerArguments);  
155 - } else {  
156 - widget.onDetect(barcode, value! as MobileScannerArguments);  
157 - }  
158 - }); 197 +
159 return ClipRect( 198 return ClipRect(
160 - child: SizedBox(  
161 - width: MediaQuery.of(context).size.width,  
162 - height: MediaQuery.of(context).size.height, 199 + child: LayoutBuilder(
  200 + builder: (_, constraints) {
  201 + return SizedBox.fromSize(
  202 + size: constraints.biggest,
163 child: FittedBox( 203 child: FittedBox(
164 fit: widget.fit, 204 fit: widget.fit,
165 child: SizedBox( 205 child: SizedBox(
@@ -170,36 +210,19 @@ class _MobileScannerState extends State<MobileScanner> @@ -170,36 +210,19 @@ class _MobileScannerState extends State<MobileScanner>
170 : Texture(textureId: value.textureId!), 210 : Texture(textureId: value.textureId!),
171 ), 211 ),
172 ), 212 ),
173 - ),  
174 ); 213 );
175 - }  
176 }, 214 },
  215 + ),
177 ); 216 );
178 }, 217 },
179 ); 218 );
180 } 219 }
181 220
182 @override 221 @override
183 - void didUpdateWidget(covariant MobileScanner oldWidget) {  
184 - super.didUpdateWidget(oldWidget);  
185 - if (oldWidget.controller == null) {  
186 - if (widget.controller != null) {  
187 - controller.dispose();  
188 - controller = widget.controller!;  
189 - }  
190 - } else {  
191 - if (widget.controller == null) {  
192 - controller = MobileScannerController();  
193 - } else if (oldWidget.controller != widget.controller) {  
194 - controller = widget.controller!;  
195 - }  
196 - }  
197 - }  
198 -  
199 - @override  
200 void dispose() { 222 void dispose() {
201 - controller.dispose();  
202 WidgetsBinding.instance.removeObserver(this); 223 WidgetsBinding.instance.removeObserver(this);
  224 + _barcodesSubscription?.cancel();
  225 + _controller.dispose();
203 super.dispose(); 226 super.dispose();
204 } 227 }
205 } 228 }
@@ -5,160 +5,125 @@ import 'package:flutter/cupertino.dart'; @@ -5,160 +5,125 @@ import 'package:flutter/cupertino.dart';
5 import 'package:flutter/foundation.dart'; 5 import 'package:flutter/foundation.dart';
6 import 'package:flutter/services.dart'; 6 import 'package:flutter/services.dart';
7 import 'package:mobile_scanner/mobile_scanner.dart'; 7 import 'package:mobile_scanner/mobile_scanner.dart';
8 -import 'package:mobile_scanner/src/objects/barcode_utility.dart'; 8 +import 'package:mobile_scanner/src/barcode_utility.dart';
9 9
10 -/// The facing of a camera.  
11 -enum CameraFacing {  
12 - /// Front facing camera.  
13 - front,  
14 -  
15 - /// Back facing camera.  
16 - back,  
17 -}  
18 -  
19 -enum MobileScannerState { undetermined, authorized, denied }  
20 -  
21 -/// The state of torch.  
22 -enum TorchState {  
23 - /// Torch is off.  
24 - off, 10 +/// The [MobileScannerController] holds all the logic of this plugin,
  11 +/// where as the [MobileScanner] class is the frontend of this plugin.
  12 +class MobileScannerController {
  13 + MobileScannerController({
  14 + this.facing = CameraFacing.back,
  15 + this.detectionSpeed = DetectionSpeed.normal,
  16 + this.detectionTimeoutMs = 250,
  17 + this.torchEnabled = false,
  18 + this.formats,
  19 + this.returnImage = false,
  20 + @Deprecated('Instead, use the result of calling `start()` to determine if permissions were granted.')
  21 + this.onPermissionSet,
  22 + this.autoStart = true,
  23 + }) {
  24 + // In case a new instance is created before calling dispose()
  25 + if (controllerHashcode != null) {
  26 + stop();
  27 + }
  28 + controllerHashcode = hashCode;
  29 + events = _eventChannel
  30 + .receiveBroadcastStream()
  31 + .listen((data) => _handleEvent(data as Map));
  32 + }
25 33
26 - /// Torch is on.  
27 - on,  
28 -} 34 + /// The hashcode of the controller to check if the correct object is mounted.
  35 + /// Must be static to keep the same value on new instances
  36 + static int? controllerHashcode;
29 37
30 -// enum AnalyzeMode { none, barcode } 38 + /// Select which camera should be used.
  39 + ///
  40 + /// Default: CameraFacing.back
  41 + final CameraFacing facing;
31 42
32 -class MobileScannerController {  
33 - MethodChannel methodChannel =  
34 - const MethodChannel('dev.steenbakker.mobile_scanner/scanner/method');  
35 - EventChannel eventChannel =  
36 - const EventChannel('dev.steenbakker.mobile_scanner/scanner/event'); 43 + /// Enable or disable the torch (Flash) on start
  44 + ///
  45 + /// Default: disabled
  46 + final bool torchEnabled;
37 47
38 - //Must be static to keep the same value on new instances  
39 - static int? _controllerHashcode;  
40 - StreamSubscription? events; 48 + /// Set to true if you want to return the image buffer with the Barcode event
  49 + ///
  50 + /// Only supported on iOS and Android
  51 + final bool returnImage;
41 52
42 - final ValueNotifier<MobileScannerArguments?> args = ValueNotifier(null);  
43 - final ValueNotifier<TorchState> torchState = ValueNotifier(TorchState.off);  
44 - late final ValueNotifier<CameraFacing> cameraFacingState;  
45 - final Ratio? ratio;  
46 - final bool? torchEnabled; 53 + /// If provided, the scanner will only detect those specific formats
  54 + final List<BarcodeFormat>? formats;
47 55
48 - /// If provided, the scanner will only detect those specific formats. 56 + /// Sets the speed of detections.
49 /// 57 ///
50 - /// WARNING: On iOS, only 1 format is supported.  
51 - final List<BarcodeFormat>? formats; 58 + /// WARNING: DetectionSpeed.unrestricted can cause memory issues on some devices
  59 + final DetectionSpeed detectionSpeed;
52 60
53 - CameraFacing facing;  
54 - bool hasTorch = false;  
55 - late StreamController<Barcode> barcodesController; 61 + /// Sets the timeout of scanner.
  62 + /// The timeout is set in miliseconds.
  63 + ///
  64 + /// NOTE: The timeout only works if the [detectionSpeed] is set to
  65 + /// [DetectionSpeed.normal] (which is the default value).
  66 + final int detectionTimeoutMs;
56 67
57 - /// Whether to automatically resume the camera when the application is resumed  
58 - bool autoResume; 68 + /// Automatically start the mobileScanner on initialization.
  69 + final bool autoStart;
59 70
60 - Stream<Barcode> get barcodes => barcodesController.stream; 71 + /// Sets the barcode stream
  72 + final StreamController<BarcodeCapture> _barcodesController =
  73 + StreamController.broadcast();
  74 + Stream<BarcodeCapture> get barcodes => _barcodesController.stream;
61 75
62 - MobileScannerController({  
63 - this.facing = CameraFacing.back,  
64 - this.ratio,  
65 - this.torchEnabled,  
66 - this.formats,  
67 - this.autoResume = true,  
68 - }) {  
69 - // In case a new instance is created before calling dispose()  
70 - if (_controllerHashcode != null) {  
71 - stop();  
72 - }  
73 - _controllerHashcode = hashCode; 76 + static const MethodChannel _methodChannel =
  77 + MethodChannel('dev.steenbakker.mobile_scanner/scanner/method');
  78 + static const EventChannel _eventChannel =
  79 + EventChannel('dev.steenbakker.mobile_scanner/scanner/event');
74 80
75 - cameraFacingState = ValueNotifier(facing); 81 + @Deprecated(
  82 + 'Instead, use the result of calling `start()` to determine if permissions were granted.',
  83 + )
  84 + Function(bool permissionGranted)? onPermissionSet;
76 85
77 - // Sets analyze mode and barcode stream  
78 - barcodesController = StreamController.broadcast(  
79 - // onListen: () => setAnalyzeMode(AnalyzeMode.barcode.index),  
80 - // onCancel: () => setAnalyzeMode(AnalyzeMode.none.index),  
81 - ); 86 + /// Listen to events from the platform specific code
  87 + late StreamSubscription events;
82 88
83 - // Listen to events from the platform specific code  
84 - events = eventChannel  
85 - .receiveBroadcastStream()  
86 - .listen((data) => handleEvent(data as Map));  
87 - } 89 + /// A notifier that provides several arguments about the MobileScanner
  90 + final ValueNotifier<MobileScannerArguments?> startArguments =
  91 + ValueNotifier(null);
88 92
89 - void handleEvent(Map event) {  
90 - final name = event['name'];  
91 - final data = event['data'];  
92 - switch (name) {  
93 - case 'torchState':  
94 - final state = TorchState.values[data as int? ?? 0];  
95 - torchState.value = state;  
96 - break;  
97 - case 'barcode':  
98 - final barcode = Barcode.fromNative(data as Map? ?? {});  
99 - barcodesController.add(barcode);  
100 - break;  
101 - case 'barcodeMac':  
102 - barcodesController.add(  
103 - Barcode(  
104 - rawValue: (data as Map)['payload'] as String?,  
105 - ),  
106 - );  
107 - break;  
108 - case 'barcodeWeb':  
109 - barcodesController.add(Barcode(rawValue: data as String?));  
110 - break;  
111 - default:  
112 - throw UnimplementedError();  
113 - }  
114 - } 93 + /// A notifier that provides the state of the Torch (Flash)
  94 + final ValueNotifier<TorchState> torchState = ValueNotifier(TorchState.off);
115 95
116 - // TODO: Add more analyzers like text analyzer  
117 - // void setAnalyzeMode(int mode) {  
118 - // if (hashCode != _controllerHashcode) {  
119 - // return;  
120 - // }  
121 - // methodChannel.invokeMethod('analyze', mode);  
122 - // } 96 + /// A notifier that provides the state of which camera is being used
  97 + late final ValueNotifier<CameraFacing> cameraFacingState =
  98 + ValueNotifier(facing);
123 99
124 - // List<BarcodeFormats>? formats = _defaultBarcodeFormats,  
125 bool isStarting = false; 100 bool isStarting = false;
126 101
127 - /// Start barcode scanning. This will first check if the required permissions  
128 - /// are set.  
129 - Future<void> start() async {  
130 - ensure('startAsync');  
131 - if (isStarting) {  
132 - throw Exception('mobile_scanner: Called start() while already starting.');  
133 - }  
134 - isStarting = true;  
135 - // setAnalyzeMode(AnalyzeMode.barcode.index); 102 + bool? _hasTorch;
136 103
137 - // Check authorization status  
138 - if (!kIsWeb) {  
139 - MobileScannerState state = MobileScannerState  
140 - .values[await methodChannel.invokeMethod('state') as int? ?? 0];  
141 - switch (state) {  
142 - case MobileScannerState.undetermined:  
143 - final bool result =  
144 - await methodChannel.invokeMethod('request') as bool? ?? false;  
145 - state = result  
146 - ? MobileScannerState.authorized  
147 - : MobileScannerState.denied;  
148 - break;  
149 - case MobileScannerState.denied:  
150 - isStarting = false;  
151 - throw PlatformException(code: 'NO ACCESS');  
152 - case MobileScannerState.authorized:  
153 - break; 104 + /// Returns whether the device has a torch.
  105 + ///
  106 + /// Throws an error if the controller is not initialized.
  107 + bool get hasTorch {
  108 + if (_hasTorch == null) {
  109 + throw const MobileScannerException(
  110 + errorCode: MobileScannerErrorCode.controllerUninitialized,
  111 + );
154 } 112 }
  113 +
  114 + return _hasTorch!;
155 } 115 }
156 116
157 - cameraFacingState.value = facing; 117 + /// Set the starting arguments for the camera
  118 + Map<String, dynamic> _argumentsToMap({CameraFacing? cameraFacingOverride}) {
  119 + final Map<String, dynamic> arguments = {};
  120 +
  121 + cameraFacingState.value = cameraFacingOverride ?? facing;
  122 + arguments['facing'] = cameraFacingState.value.index;
  123 + arguments['torch'] = torchEnabled;
  124 + arguments['speed'] = detectionSpeed.index;
  125 + arguments['timeout'] = detectionTimeoutMs;
158 126
159 - // Set the starting arguments for the camera  
160 - final Map arguments = {};  
161 - arguments['facing'] = facing.index;  
162 /* if (scanWindow != null) { 127 /* if (scanWindow != null) {
163 arguments['scanWindow'] = [ 128 arguments['scanWindow'] = [
164 scanWindow!.left, 129 scanWindow!.left,
@@ -167,8 +132,6 @@ class MobileScannerController { @@ -167,8 +132,6 @@ class MobileScannerController {
167 scanWindow!.bottom, 132 scanWindow!.bottom,
168 ]; 133 ];
169 } */ 134 } */
170 - if (ratio != null) arguments['ratio'] = ratio;  
171 - if (torchEnabled != null) arguments['torch'] = torchEnabled;  
172 135
173 if (formats != null) { 136 if (formats != null) {
174 if (Platform.isAndroid) { 137 if (Platform.isAndroid) {
@@ -177,92 +140,145 @@ class MobileScannerController { @@ -177,92 +140,145 @@ class MobileScannerController {
177 arguments['formats'] = formats!.map((e) => e.rawValue).toList(); 140 arguments['formats'] = formats!.map((e) => e.rawValue).toList();
178 } 141 }
179 } 142 }
  143 + arguments['returnImage'] = true;
  144 + return arguments;
  145 + }
  146 +
  147 + /// Start scanning for barcodes.
  148 + /// Upon calling this method, the necessary camera permission will be requested.
  149 + ///
  150 + /// Returns an instance of [MobileScannerArguments]
  151 + /// when the scanner was successfully started.
  152 + /// Returns null if the scanner is currently starting.
  153 + ///
  154 + /// Throws a [MobileScannerException] if starting the scanner failed.
  155 + Future<MobileScannerArguments?> start({
  156 + CameraFacing? cameraFacingOverride,
  157 + }) async {
  158 + if (isStarting) {
  159 + debugPrint("Called start() while starting.");
  160 + return null;
  161 + }
  162 +
  163 + isStarting = true;
  164 +
  165 + // Check authorization status
  166 + if (!kIsWeb) {
  167 + final MobileScannerState state = MobileScannerState
  168 + .values[await _methodChannel.invokeMethod('state') as int? ?? 0];
  169 + switch (state) {
  170 + case MobileScannerState.undetermined:
  171 + final bool result =
  172 + await _methodChannel.invokeMethod('request') as bool? ?? false;
  173 + if (!result) {
  174 + isStarting = false;
  175 + throw const MobileScannerException(
  176 + errorCode: MobileScannerErrorCode.permissionDenied,
  177 + );
  178 + }
  179 + break;
  180 + case MobileScannerState.denied:
  181 + isStarting = false;
  182 + throw const MobileScannerException(
  183 + errorCode: MobileScannerErrorCode.permissionDenied,
  184 + );
  185 + case MobileScannerState.authorized:
  186 + break;
  187 + }
  188 + }
180 189
181 // Start the camera with arguments 190 // Start the camera with arguments
182 Map<String, dynamic>? startResult = {}; 191 Map<String, dynamic>? startResult = {};
183 try { 192 try {
184 - startResult = await methodChannel.invokeMapMethod<String, dynamic>( 193 + startResult = await _methodChannel.invokeMapMethod<String, dynamic>(
185 'start', 194 'start',
186 - arguments, 195 + _argumentsToMap(cameraFacingOverride: cameraFacingOverride),
187 ); 196 );
188 } on PlatformException catch (error) { 197 } on PlatformException catch (error) {
189 - debugPrint('${error.code}: ${error.message}'); 198 + MobileScannerErrorCode errorCode = MobileScannerErrorCode.genericError;
  199 +
  200 + if (error.code == "MobileScannerWeb") {
  201 + errorCode = MobileScannerErrorCode.permissionDenied;
  202 + }
190 isStarting = false; 203 isStarting = false;
191 - // setAnalyzeMode(AnalyzeMode.none.index);  
192 - return; 204 +
  205 + throw MobileScannerException(
  206 + errorCode: errorCode,
  207 + errorDetails: MobileScannerErrorDetails(
  208 + code: error.code,
  209 + details: error.details as Object?,
  210 + message: error.message,
  211 + ),
  212 + );
193 } 213 }
194 214
195 if (startResult == null) { 215 if (startResult == null) {
196 isStarting = false; 216 isStarting = false;
197 - throw PlatformException(code: 'INITIALIZATION ERROR'); 217 + throw const MobileScannerException(
  218 + errorCode: MobileScannerErrorCode.genericError,
  219 + );
198 } 220 }
199 221
200 - hasTorch = startResult['torchable'] as bool? ?? false; 222 + _hasTorch = startResult['torchable'] as bool? ?? false;
  223 + if (_hasTorch! && torchEnabled) {
  224 + torchState.value = TorchState.on;
  225 + }
201 226
202 - if (kIsWeb) {  
203 - args.value = MobileScannerArguments(  
204 - webId: startResult['ViewID'] as String?,  
205 - size: Size( 227 + isStarting = false;
  228 + return startArguments.value = MobileScannerArguments(
  229 + size: kIsWeb
  230 + ? Size(
206 startResult['videoWidth'] as double? ?? 0, 231 startResult['videoWidth'] as double? ?? 0,
207 startResult['videoHeight'] as double? ?? 0, 232 startResult['videoHeight'] as double? ?? 0,
208 - ),  
209 - hasTorch: hasTorch,  
210 - );  
211 - } else {  
212 - args.value = MobileScannerArguments(  
213 - textureId: startResult['textureId'] as int?,  
214 - size: toSize(startResult['size'] as Map? ?? {}),  
215 - hasTorch: hasTorch, 233 + )
  234 + : toSize(startResult['size'] as Map? ?? {}),
  235 + hasTorch: _hasTorch!,
  236 + textureId: kIsWeb ? null : startResult['textureId'] as int?,
  237 + webId: kIsWeb ? startResult['ViewID'] as String? : null,
216 ); 238 );
217 } 239 }
218 240
219 - isStarting = false;  
220 - }  
221 - 241 + /// Stops the camera, but does not dispose this controller.
222 Future<void> stop() async { 242 Future<void> stop() async {
223 try { 243 try {
224 - await methodChannel.invokeMethod('stop');  
225 - } on PlatformException catch (error) {  
226 - debugPrint('${error.code}: ${error.message}'); 244 + await _methodChannel.invokeMethod('stop');
  245 + } catch (e) {
  246 + debugPrint('$e');
227 } 247 }
228 } 248 }
229 249
230 /// Switches the torch on or off. 250 /// Switches the torch on or off.
231 /// 251 ///
232 - /// Only works if torch is available. 252 + /// Does nothing if the device has no torch.
  253 + ///
  254 + /// Throws if the controller was not initialized.
233 Future<void> toggleTorch() async { 255 Future<void> toggleTorch() async {
234 - ensure('toggleTorch');  
235 - if (!hasTorch) {  
236 - debugPrint('Device has no torch/flash.'); 256 + final hasTorch = _hasTorch;
  257 +
  258 + if (hasTorch == null) {
  259 + throw const MobileScannerException(
  260 + errorCode: MobileScannerErrorCode.controllerUninitialized,
  261 + );
  262 + } else if (!hasTorch) {
237 return; 263 return;
238 } 264 }
239 265
240 - final TorchState state = 266 + torchState.value =
241 torchState.value == TorchState.off ? TorchState.on : TorchState.off; 267 torchState.value == TorchState.off ? TorchState.on : TorchState.off;
242 268
243 - try {  
244 - await methodChannel.invokeMethod('torch', state.index);  
245 - } on PlatformException catch (error) {  
246 - debugPrint('${error.code}: ${error.message}');  
247 - } 269 + await _methodChannel.invokeMethod('torch', torchState.value.index);
248 } 270 }
249 271
250 /// Switches the torch on or off. 272 /// Switches the torch on or off.
251 /// 273 ///
252 /// Only works if torch is available. 274 /// Only works if torch is available.
253 Future<void> switchCamera() async { 275 Future<void> switchCamera() async {
254 - ensure('switchCamera');  
255 - try {  
256 - await methodChannel.invokeMethod('stop');  
257 - } on PlatformException catch (error) {  
258 - debugPrint(  
259 - '${error.code}: camera is stopped! Please start before switching camera.',  
260 - );  
261 - return;  
262 - }  
263 - facing =  
264 - facing == CameraFacing.back ? CameraFacing.front : CameraFacing.back;  
265 - await start(); 276 + await _methodChannel.invokeMethod('stop');
  277 + final CameraFacing facingToUse =
  278 + cameraFacingState.value == CameraFacing.back
  279 + ? CameraFacing.front
  280 + : CameraFacing.back;
  281 + await start(cameraFacingOverride: facingToUse);
266 } 282 }
267 283
268 /// Handles a local image file. 284 /// Handles a local image file.
@@ -271,28 +287,77 @@ class MobileScannerController { @@ -271,28 +287,77 @@ class MobileScannerController {
271 /// 287 ///
272 /// [path] The path of the image on the devices 288 /// [path] The path of the image on the devices
273 Future<bool> analyzeImage(String path) async { 289 Future<bool> analyzeImage(String path) async {
274 - return methodChannel 290 + return _methodChannel
275 .invokeMethod<bool>('analyzeImage', path) 291 .invokeMethod<bool>('analyzeImage', path)
276 .then<bool>((bool? value) => value ?? false); 292 .then<bool>((bool? value) => value ?? false);
277 } 293 }
278 294
279 /// Disposes the MobileScannerController and closes all listeners. 295 /// Disposes the MobileScannerController and closes all listeners.
  296 + ///
  297 + /// If you call this, you cannot use this controller object anymore.
280 void dispose() { 298 void dispose() {
281 - if (hashCode == _controllerHashcode) {  
282 stop(); 299 stop();
283 - events?.cancel();  
284 - events = null;  
285 - _controllerHashcode = null; 300 + events.cancel();
  301 + _barcodesController.close();
  302 + if (hashCode == controllerHashcode) {
  303 + controllerHashcode = null;
286 } 304 }
287 - barcodesController.close();  
288 } 305 }
289 306
290 - /// Checks if the MobileScannerController is bound to the correct MobileScanner object.  
291 - void ensure(String name) {  
292 - final message =  
293 - 'MobileScannerController.$name called after MobileScannerController.dispose\n'  
294 - 'MobileScannerController methods should not be used after calling dispose.';  
295 - assert(hashCode == _controllerHashcode, message); 307 + /// Handles a returning event from the platform side
  308 + void _handleEvent(Map event) {
  309 + final name = event['name'];
  310 + final data = event['data'];
  311 +
  312 + switch (name) {
  313 + case 'torchState':
  314 + final state = TorchState.values[data as int? ?? 0];
  315 + torchState.value = state;
  316 + break;
  317 + case 'barcode':
  318 + if (data == null) return;
  319 + final parsed = (data as List)
  320 + .map((value) => Barcode.fromNative(value as Map))
  321 + .toList();
  322 + _barcodesController.add(
  323 + BarcodeCapture(
  324 + barcodes: parsed,
  325 + image: event['image'] as Uint8List?,
  326 + ),
  327 + );
  328 + break;
  329 + case 'barcodeMac':
  330 + _barcodesController.add(
  331 + BarcodeCapture(
  332 + barcodes: [
  333 + Barcode(
  334 + rawValue: (data as Map)['payload'] as String?,
  335 + )
  336 + ],
  337 + ),
  338 + );
  339 + break;
  340 + case 'barcodeWeb':
  341 + final barcode = data as Map?;
  342 + _barcodesController.add(
  343 + BarcodeCapture(
  344 + barcodes: [
  345 + Barcode(
  346 + rawValue: barcode?['rawValue'] as String?,
  347 + rawBytes: barcode?['rawBytes'] as Uint8List?,
  348 + )
  349 + ],
  350 + ),
  351 + );
  352 + break;
  353 + case 'error':
  354 + throw MobileScannerException(
  355 + errorCode: MobileScannerErrorCode.genericError,
  356 + errorDetails: MobileScannerErrorDetails(message: data as String?),
  357 + );
  358 + default:
  359 + throw UnimplementedError(name as String?);
  360 + }
296 } 361 }
297 362
298 /// updates the native scanwindow 363 /// updates the native scanwindow
  1 +import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart';
  2 +
  3 +/// This class represents an exception thrown by the mobile scanner.
  4 +class MobileScannerException implements Exception {
  5 + const MobileScannerException({
  6 + required this.errorCode,
  7 + this.errorDetails,
  8 + });
  9 +
  10 + /// The error code of the exception.
  11 + final MobileScannerErrorCode errorCode;
  12 +
  13 + /// The additional error details that came with the [errorCode].
  14 + final MobileScannerErrorDetails? errorDetails;
  15 +
  16 + @override
  17 + String toString() {
  18 + if (errorDetails != null && errorDetails?.message != null) {
  19 + return "MobileScannerException: code ${errorCode.name}, message: ${errorDetails?.message}";
  20 + }
  21 + return "MobileScannerException: ${errorCode.name}";
  22 + }
  23 +}
  24 +
  25 +/// The raw error details for a [MobileScannerException].
  26 +class MobileScannerErrorDetails {
  27 + const MobileScannerErrorDetails({
  28 + this.code,
  29 + this.details,
  30 + this.message,
  31 + });
  32 +
  33 + /// The error code from the [PlatformException].
  34 + final String? code;
  35 +
  36 + /// The details from the [PlatformException].
  37 + final Object? details;
  38 +
  39 + /// The error message from the [PlatformException].
  40 + final String? message;
  41 +}
1 import 'dart:typed_data'; 1 import 'dart:typed_data';
2 import 'dart:ui'; 2 import 'dart:ui';
3 3
4 -import 'package:mobile_scanner/src/objects/barcode_utility.dart'; 4 +import 'package:mobile_scanner/src/barcode_utility.dart';
5 5
6 /// Represents a single recognized barcode and its value. 6 /// Represents a single recognized barcode and its value.
7 class Barcode { 7 class Barcode {
  1 +import 'dart:typed_data';
  2 +
  3 +import 'package:mobile_scanner/src/objects/barcode.dart';
  4 +
  5 +/// The return object after a frame is scanned.
  6 +///
  7 +/// [barcodes] A list with barcodes. A scanned frame can contain multiple
  8 +/// barcodes.
  9 +/// [image] If enabled, an image of the scanned frame.
  10 +class BarcodeCapture {
  11 + final List<Barcode> barcodes;
  12 +
  13 + final Uint8List? image;
  14 +
  15 + BarcodeCapture({
  16 + required this.barcodes,
  17 + this.image,
  18 + });
  19 +}
1 import 'package:flutter/material.dart'; 1 import 'package:flutter/material.dart';
2 2
3 -/// Camera args for [CameraView]. 3 +/// The start arguments of the scanner.
4 class MobileScannerArguments { 4 class MobileScannerArguments {
5 - /// The texture id.  
6 - final int? textureId;  
7 -  
8 - /// Size of the texture. 5 + /// The output size of the camera.
  6 + /// This value can be used to draw a box in the image.
9 final Size size; 7 final Size size;
10 8
  9 + /// A bool which is true if the device has a torch.
11 final bool hasTorch; 10 final bool hasTorch;
12 11
  12 + /// The texture id of the capture used internally.
  13 + final int? textureId;
  14 +
  15 + /// The texture id of the capture used internally if device is web.
13 final String? webId; 16 final String? webId;
14 17
15 - /// Create a [MobileScannerArguments].  
16 MobileScannerArguments({ 18 MobileScannerArguments({
17 - this.textureId,  
18 required this.size, 19 required this.size,
19 required this.hasTorch, 20 required this.hasTorch,
  21 + this.textureId,
20 this.webId, 22 this.webId,
21 }); 23 });
22 } 24 }
  1 +import 'dart:html';
  2 +
  3 +import 'package:flutter/material.dart';
  4 +import 'package:mobile_scanner/src/enums/camera_facing.dart';
  5 +import 'package:mobile_scanner/src/objects/barcode.dart';
  6 +import 'package:mobile_scanner/src/web/media.dart';
  7 +
  8 +abstract class WebBarcodeReaderBase {
  9 + /// Timer used to capture frames to be analyzed
  10 + final Duration frameInterval;
  11 + final DivElement videoContainer;
  12 +
  13 + const WebBarcodeReaderBase({
  14 + required this.videoContainer,
  15 + this.frameInterval = const Duration(milliseconds: 200),
  16 + });
  17 +
  18 + bool get isStarted;
  19 +
  20 + int get videoWidth;
  21 + int get videoHeight;
  22 +
  23 + /// Starts streaming video
  24 + Future<void> start({
  25 + required CameraFacing cameraFacing,
  26 + });
  27 +
  28 + /// Starts scanning QR codes or barcodes
  29 + Stream<Barcode?> detectBarcodeContinuously();
  30 +
  31 + /// Stops streaming video
  32 + Future<void> stop();
  33 +
  34 + /// Can enable or disable the flash if available
  35 + Future<void> toggleTorch({required bool enabled});
  36 +
  37 + /// Determine whether device has flash
  38 + bool get hasTorch;
  39 +}
  40 +
  41 +mixin InternalStreamCreation on WebBarcodeReaderBase {
  42 + /// The video stream.
  43 + /// Will be initialized later to see which camera needs to be used.
  44 + MediaStream? localMediaStream;
  45 + final VideoElement video = VideoElement();
  46 +
  47 + @override
  48 + int get videoWidth => video.videoWidth;
  49 + @override
  50 + int get videoHeight => video.videoHeight;
  51 +
  52 + Future<MediaStream?> initMediaStream(CameraFacing cameraFacing) async {
  53 + // Check if browser supports multiple camera's and set if supported
  54 + final Map? capabilities =
  55 + window.navigator.mediaDevices?.getSupportedConstraints();
  56 + final Map<String, dynamic> constraints;
  57 + if (capabilities != null && capabilities['facingMode'] as bool) {
  58 + constraints = {
  59 + 'video': VideoOptions(
  60 + facingMode:
  61 + cameraFacing == CameraFacing.front ? 'user' : 'environment',
  62 + )
  63 + };
  64 + } else {
  65 + constraints = {'video': true};
  66 + }
  67 + final stream =
  68 + await window.navigator.mediaDevices?.getUserMedia(constraints);
  69 + return stream;
  70 + }
  71 +
  72 + void prepareVideoElement(VideoElement videoSource);
  73 +
  74 + Future<void> attachStreamToVideo(
  75 + MediaStream stream,
  76 + VideoElement videoSource,
  77 + );
  78 +
  79 + @override
  80 + Future<void> stop() async {
  81 + try {
  82 + // Stop the camera stream
  83 + localMediaStream?.getTracks().forEach((track) {
  84 + if (track.readyState == 'live') {
  85 + track.stop();
  86 + }
  87 + });
  88 + } catch (e) {
  89 + debugPrint('Failed to stop stream: $e');
  90 + }
  91 + video.srcObject = null;
  92 + localMediaStream = null;
  93 + videoContainer.children = [];
  94 + }
  95 +}
  96 +
  97 +/// Mixin for libraries that don't have built-in torch support
  98 +mixin InternalTorchDetection on InternalStreamCreation {
  99 + @override
  100 + bool get hasTorch {
  101 + // TODO: fix flash light. See https://github.com/dart-lang/sdk/issues/48533
  102 + // final track = _localStream?.getVideoTracks();
  103 + // if (track != null) {
  104 + // final imageCapture = html.ImageCapture(track.first);
  105 + // final photoCapabilities = await imageCapture.getPhotoCapabilities();
  106 + // }
  107 + return false;
  108 + }
  109 +
  110 + @override
  111 + Future<void> toggleTorch({required bool enabled}) async {
  112 + if (hasTorch) {
  113 + final track = localMediaStream?.getVideoTracks();
  114 + await track?.first.applyConstraints({
  115 + 'advanced': [
  116 + {'torch': enabled}
  117 + ]
  118 + });
  119 + }
  120 + }
  121 +}
1 @JS() 1 @JS()
2 library jsqr; 2 library jsqr;
3 3
  4 +import 'dart:async';
  5 +import 'dart:html';
  6 +import 'dart:typed_data';
  7 +
4 import 'package:js/js.dart'; 8 import 'package:js/js.dart';
  9 +import 'package:mobile_scanner/src/enums/camera_facing.dart';
  10 +import 'package:mobile_scanner/src/objects/barcode.dart';
  11 +import 'package:mobile_scanner/src/web/base.dart';
5 12
6 @JS('jsQR') 13 @JS('jsQR')
7 external Code? jsQR(dynamic data, int? width, int? height); 14 external Code? jsQR(dynamic data, int? width, int? height);
@@ -9,4 +16,75 @@ external Code? jsQR(dynamic data, int? width, int? height); @@ -9,4 +16,75 @@ external Code? jsQR(dynamic data, int? width, int? height);
9 @JS() 16 @JS()
10 class Code { 17 class Code {
11 external String get data; 18 external String get data;
  19 +
  20 + external Uint8ClampedList get binaryData;
  21 +}
  22 +
  23 +class JsQrCodeReader extends WebBarcodeReaderBase
  24 + with InternalStreamCreation, InternalTorchDetection {
  25 + JsQrCodeReader({required super.videoContainer});
  26 +
  27 + @override
  28 + bool get isStarted => localMediaStream != null;
  29 +
  30 + @override
  31 + Future<void> start({
  32 + required CameraFacing cameraFacing,
  33 + }) async {
  34 + videoContainer.children = [video];
  35 +
  36 + final stream = await initMediaStream(cameraFacing);
  37 +
  38 + prepareVideoElement(video);
  39 + if (stream != null) {
  40 + await attachStreamToVideo(stream, video);
  41 + }
  42 + }
  43 +
  44 + @override
  45 + void prepareVideoElement(VideoElement videoSource) {
  46 + // required to tell iOS safari we don't want fullscreen
  47 + videoSource.setAttribute('playsinline', 'true');
  48 + }
  49 +
  50 + @override
  51 + Future<void> attachStreamToVideo(
  52 + MediaStream stream,
  53 + VideoElement videoSource,
  54 + ) async {
  55 + localMediaStream = stream;
  56 + videoSource.srcObject = stream;
  57 + await videoSource.play();
  58 + }
  59 +
  60 + @override
  61 + Stream<Barcode?> detectBarcodeContinuously() async* {
  62 + yield* Stream.periodic(frameInterval, (_) {
  63 + return _captureFrame(video);
  64 + }).asyncMap((event) async {
  65 + final code = await event;
  66 + if (code == null) {
  67 + return null;
  68 + }
  69 + return Barcode(
  70 + rawValue: code.data,
  71 + rawBytes: Uint8List.fromList(code.binaryData),
  72 + format: BarcodeFormat.qrCode,
  73 + );
  74 + });
  75 + }
  76 +
  77 + /// Captures a frame and analyzes it for QR codes
  78 + Future<Code?> _captureFrame(VideoElement video) async {
  79 + if (localMediaStream == null) return null;
  80 + final canvas =
  81 + CanvasElement(width: video.videoWidth, height: video.videoHeight);
  82 + final ctx = canvas.context2D;
  83 +
  84 + ctx.drawImage(video, 0, 0);
  85 + final imgData = ctx.getImageData(0, 0, canvas.width!, canvas.height!);
  86 +
  87 + final code = jsQR(imgData.data, canvas.width, canvas.height);
  88 + return code;
  89 + }
12 } 90 }
1 -@JS()  
2 -library qrscanner;  
3 -  
4 -import 'package:js/js.dart';  
5 -  
6 -@JS('QrScanner')  
7 -external String scanImage(dynamic data);  
8 -  
9 -@JS()  
10 -class QrScanner {  
11 - external String get scanImage;  
12 -}  
  1 +import 'dart:async';
  2 +import 'dart:html';
  3 +import 'dart:typed_data';
  4 +
  5 +import 'package:js/js.dart';
  6 +import 'package:mobile_scanner/src/enums/camera_facing.dart';
  7 +import 'package:mobile_scanner/src/objects/barcode.dart';
  8 +import 'package:mobile_scanner/src/web/base.dart';
  9 +
  10 +@JS('Promise')
  11 +@staticInterop
  12 +class Promise<T> {}
  13 +
  14 +@JS('ZXing.BrowserMultiFormatReader')
  15 +@staticInterop
  16 +class JsZXingBrowserMultiFormatReader {
  17 + /// https://github.com/zxing-js/library/blob/1e9ccb3b6b28d75b9eef866dba196d8937eb4449/src/browser/BrowserMultiFormatReader.ts#L11
  18 + external factory JsZXingBrowserMultiFormatReader(
  19 + dynamic hints,
  20 + int timeBetweenScansMillis,
  21 + );
  22 +}
  23 +
  24 +@JS()
  25 +@anonymous
  26 +abstract class Result {
  27 + /// raw text encoded by the barcode
  28 + external String get text;
  29 +
  30 + /// Returns raw bytes encoded by the barcode, if applicable, otherwise null
  31 + external Uint8ClampedList? get rawBytes;
  32 +
  33 + /// Representing the format of the barcode that was decoded
  34 + external int? format;
  35 +}
  36 +
  37 +extension ResultExt on Result {
  38 + Barcode toBarcode() {
  39 + final rawBytes = this.rawBytes;
  40 + return Barcode(
  41 + rawValue: text,
  42 + rawBytes: rawBytes != null ? Uint8List.fromList(rawBytes) : null,
  43 + format: barcodeFormat,
  44 + );
  45 + }
  46 +
  47 + /// https://github.com/zxing-js/library/blob/1e9ccb3b6b28d75b9eef866dba196d8937eb4449/src/core/BarcodeFormat.ts#L28
  48 + BarcodeFormat get barcodeFormat {
  49 + switch (format) {
  50 + case 1:
  51 + return BarcodeFormat.aztec;
  52 + case 2:
  53 + return BarcodeFormat.codebar;
  54 + case 3:
  55 + return BarcodeFormat.code39;
  56 + case 4:
  57 + return BarcodeFormat.code128;
  58 + case 5:
  59 + return BarcodeFormat.dataMatrix;
  60 + case 6:
  61 + return BarcodeFormat.ean8;
  62 + case 7:
  63 + return BarcodeFormat.ean13;
  64 + case 8:
  65 + return BarcodeFormat.itf;
  66 + // case 9:
  67 + // return BarcodeFormat.maxicode;
  68 + case 10:
  69 + return BarcodeFormat.pdf417;
  70 + case 11:
  71 + return BarcodeFormat.qrCode;
  72 + // case 12:
  73 + // return BarcodeFormat.rss14;
  74 + // case 13:
  75 + // return BarcodeFormat.rssExp;
  76 + case 14:
  77 + return BarcodeFormat.upcA;
  78 + case 15:
  79 + return BarcodeFormat.upcE;
  80 + default:
  81 + return BarcodeFormat.unknown;
  82 + }
  83 + }
  84 +}
  85 +
  86 +typedef BarcodeDetectionCallback = void Function(
  87 + Result? result,
  88 + dynamic error,
  89 +);
  90 +
  91 +extension JsZXingBrowserMultiFormatReaderExt
  92 + on JsZXingBrowserMultiFormatReader {
  93 + external Promise<void> decodeFromVideoElementContinuously(
  94 + VideoElement source,
  95 + BarcodeDetectionCallback callbackFn,
  96 + );
  97 +
  98 + /// Continuously decodes from video input
  99 + external void decodeContinuously(
  100 + VideoElement element,
  101 + BarcodeDetectionCallback callbackFn,
  102 + );
  103 +
  104 + external Promise<void> decodeFromStream(
  105 + MediaStream stream,
  106 + VideoElement videoSource,
  107 + BarcodeDetectionCallback callbackFn,
  108 + );
  109 +
  110 + external Promise<void> decodeFromConstraints(
  111 + dynamic constraints,
  112 + VideoElement videoSource,
  113 + BarcodeDetectionCallback callbackFn,
  114 + );
  115 +
  116 + external void stopContinuousDecode();
  117 +
  118 + external VideoElement prepareVideoElement(VideoElement videoSource);
  119 +
  120 + /// Defines what the [videoElement] src will be.
  121 + external void addVideoSource(
  122 + VideoElement videoElement,
  123 + MediaStream stream,
  124 + );
  125 +
  126 + external bool isVideoPlaying(VideoElement video);
  127 +
  128 + external void reset();
  129 +
  130 + /// The HTML video element, used to display the camera stream.
  131 + external VideoElement? videoElement;
  132 +
  133 + /// The stream output from camera.
  134 + external MediaStream? stream;
  135 +}
  136 +
  137 +class ZXingBarcodeReader extends WebBarcodeReaderBase
  138 + with InternalStreamCreation, InternalTorchDetection {
  139 + late final JsZXingBrowserMultiFormatReader _reader =
  140 + JsZXingBrowserMultiFormatReader(
  141 + null,
  142 + frameInterval.inMilliseconds,
  143 + );
  144 +
  145 + ZXingBarcodeReader({required super.videoContainer});
  146 +
  147 + @override
  148 + bool get isStarted => localMediaStream != null;
  149 +
  150 + @override
  151 + Future<void> start({
  152 + required CameraFacing cameraFacing,
  153 + }) async {
  154 + videoContainer.children = [video];
  155 +
  156 + final stream = await initMediaStream(cameraFacing);
  157 +
  158 + prepareVideoElement(video);
  159 + if (stream != null) {
  160 + await attachStreamToVideo(stream, video);
  161 + }
  162 + }
  163 +
  164 + @override
  165 + void prepareVideoElement(VideoElement videoSource) {
  166 + _reader.prepareVideoElement(videoSource);
  167 + }
  168 +
  169 + @override
  170 + Future<void> attachStreamToVideo(
  171 + MediaStream stream,
  172 + VideoElement videoSource,
  173 + ) async {
  174 + _reader.addVideoSource(videoSource, stream);
  175 + _reader.videoElement = videoSource;
  176 + _reader.stream = stream;
  177 + localMediaStream = stream;
  178 + await videoSource.play();
  179 + }
  180 +
  181 + @override
  182 + Stream<Barcode?> detectBarcodeContinuously() {
  183 + final controller = StreamController<Barcode?>();
  184 + controller.onListen = () async {
  185 + _reader.decodeContinuously(
  186 + video,
  187 + allowInterop((result, error) {
  188 + if (result != null) {
  189 + controller.add(result.toBarcode());
  190 + }
  191 + }),
  192 + );
  193 + };
  194 + controller.onCancel = () {
  195 + _reader.stopContinuousDecode();
  196 + };
  197 + return controller.stream;
  198 + }
  199 +
  200 + @override
  201 + Future<void> stop() async {
  202 + _reader.reset();
  203 + super.stop();
  204 + }
  205 +}
@@ -283,9 +283,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -283,9 +283,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
283 283
284 func toggleTorch(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { 284 func toggleTorch(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
285 if (device == nil) { 285 if (device == nil) {
286 - result(FlutterError(code: "MobileScanner",  
287 - message: "Called toggleTorch() while stopped!",  
288 - details: nil)) 286 + result(nil)
289 return 287 return
290 } 288 }
291 do { 289 do {
@@ -305,9 +303,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, @@ -305,9 +303,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
305 303
306 func stop(_ result: FlutterResult) { 304 func stop(_ result: FlutterResult) {
307 if (device == nil) { 305 if (device == nil) {
308 - result(FlutterError(code: "MobileScanner",  
309 - message: "Called stop() while already stopped!",  
310 - details: nil)) 306 + result(nil)
  307 +
311 return 308 return
312 } 309 }
313 captureSession.stopRunning() 310 captureSession.stopRunning()
1 name: mobile_scanner 1 name: mobile_scanner
2 description: A universal barcode and QR code scanner for Flutter based on MLKit. Uses CameraX on Android, AVFoundation on iOS and Apple Vision & AVFoundation on macOS. 2 description: A universal barcode and QR code scanner for Flutter based on MLKit. Uses CameraX on Android, AVFoundation on iOS and Apple Vision & AVFoundation on macOS.
3 -version: 2.0.0 3 +version: 3.0.0-beta.3
4 repository: https://github.com/juliansteenbakker/mobile_scanner 4 repository: https://github.com/juliansteenbakker/mobile_scanner
5 5
6 environment: 6 environment:
@@ -14,10 +14,11 @@ dependencies: @@ -14,10 +14,11 @@ dependencies:
14 sdk: flutter 14 sdk: flutter
15 js: ^0.6.3 15 js: ^0.6.3
16 16
  17 +
17 dev_dependencies: 18 dev_dependencies:
18 flutter_test: 19 flutter_test:
19 sdk: flutter 20 sdk: flutter
20 - lint: ^1.10.0 21 + lint: ">=1.10.0 <3.0.0"
21 22
22 flutter: 23 flutter:
23 plugin: 24 plugin: