Julian Steenbakker
Committed by GitHub

Merge branch 'master' into dependabot/gradle/android/com.android.tools.build-gradle-7.3.1

@@ -11,7 +11,7 @@ jobs: @@ -11,7 +11,7 @@ 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 14 + - uses: actions/checkout@v3.1.0
15 - uses: actions/setup-java@v3.5.1 15 - uses: actions/setup-java@v3.5.1
16 with: 16 with:
17 java-version: 11 17 java-version: 11
@@ -28,7 +28,7 @@ jobs: @@ -28,7 +28,7 @@ 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 31 + - uses: actions/checkout@v3.1.0
32 - uses: actions/setup-java@v3.5.1 32 - uses: actions/setup-java@v3.5.1
33 with: 33 with:
34 java-version: 11 34 java-version: 11
  1 +## NEXT
  2 +Breaking changes:
  3 +* [iOS] The minimum deployment target is now 11.0 or higher.
  4 +* [iOS] Updated POD dependencies
  5 +
1 ## 3.0.0-beta.1 6 ## 3.0.0-beta.1
2 Breaking changes: 7 Breaking changes:
3 * [Android] SDK updated to SDK 33. 8 * [Android] SDK updated to SDK 33.
@@ -2,7 +2,7 @@ group 'dev.steenbakker.mobile_scanner' @@ -2,7 +2,7 @@ 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.20'
6 repositories { 6 repositories {
7 google() 7 google()
8 mavenCentral() 8 mavenCentral()
@@ -50,7 +50,7 @@ dependencies { @@ -50,7 +50,7 @@ dependencies {
50 // Use this dependency to bundle the model with your app 50 // Use this dependency to bundle the model with your app
51 implementation 'com.google.mlkit:barcode-scanning:17.0.2' 51 implementation 'com.google.mlkit:barcode-scanning:17.0.2'
52 // Use this dependency to use the dynamically downloaded model in Google Play Services 52 // 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' 53 +// implementation 'com.google.android.gms:play-services-mlkit-barcode-scanning:18.1.0'
54 54
55 implementation 'androidx.camera:camera-camera2:1.1.0' 55 implementation 'androidx.camera:camera-camera2:1.1.0'
56 implementation 'androidx.camera:camera-lifecycle:1.1.0' 56 implementation 'androidx.camera:camera-lifecycle:1.1.0'
@@ -3,7 +3,11 @@ package dev.steenbakker.mobile_scanner @@ -3,7 +3,11 @@ package dev.steenbakker.mobile_scanner
3 import android.Manifest 3 import android.Manifest
4 import android.app.Activity 4 import android.app.Activity
5 import android.content.pm.PackageManager 5 import android.content.pm.PackageManager
  6 +import android.graphics.ImageFormat
6 import android.graphics.Point 7 import android.graphics.Point
  8 +import android.graphics.Rect
  9 +import android.graphics.YuvImage
  10 +import android.media.Image
7 import android.net.Uri 11 import android.net.Uri
8 import android.util.Log 12 import android.util.Log
9 import android.util.Size 13 import android.util.Size
@@ -23,11 +27,13 @@ import io.flutter.plugin.common.MethodCall @@ -23,11 +27,13 @@ import io.flutter.plugin.common.MethodCall
23 import io.flutter.plugin.common.MethodChannel 27 import io.flutter.plugin.common.MethodChannel
24 import io.flutter.plugin.common.PluginRegistry 28 import io.flutter.plugin.common.PluginRegistry
25 import io.flutter.view.TextureRegistry 29 import io.flutter.view.TextureRegistry
  30 +import java.io.ByteArrayOutputStream
26 import java.io.File 31 import java.io.File
27 32
28 33
29 -class MobileScanner(private val activity: Activity, private val textureRegistry: TextureRegistry)  
30 - : MethodChannel.MethodCallHandler, EventChannel.StreamHandler, PluginRegistry.RequestPermissionsResultListener { 34 +class MobileScanner(private val activity: Activity, private val textureRegistry: TextureRegistry) :
  35 + MethodChannel.MethodCallHandler, EventChannel.StreamHandler,
  36 + PluginRegistry.RequestPermissionsResultListener {
31 companion object { 37 companion object {
32 /** 38 /**
33 * When the application's activity is [androidx.fragment.app.FragmentActivity], requestCode can only use the lower 16 bits. 39 * When the application's activity is [androidx.fragment.app.FragmentActivity], requestCode can only use the lower 16 bits.
@@ -70,14 +76,22 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: @@ -70,14 +76,22 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
70 sink = null 76 sink = null
71 } 77 }
72 78
73 - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray): Boolean { 79 + override fun onRequestPermissionsResult(
  80 + requestCode: Int,
  81 + permissions: Array<out String>,
  82 + grantResults: IntArray
  83 + ): Boolean {
74 return listener?.onRequestPermissionsResult(requestCode, permissions, grantResults) ?: false 84 return listener?.onRequestPermissionsResult(requestCode, permissions, grantResults) ?: false
75 } 85 }
76 86
77 private fun checkPermission(result: MethodChannel.Result) { 87 private fun checkPermission(result: MethodChannel.Result) {
78 // Can't get exact denied or not_determined state without request. Just return not_determined when state isn't authorized 88 // Can't get exact denied or not_determined state without request. Just return not_determined when state isn't authorized
79 val state = 89 val state =
80 - if (ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) 1 90 + if (ContextCompat.checkSelfPermission(
  91 + activity,
  92 + Manifest.permission.CAMERA
  93 + ) == PackageManager.PERMISSION_GRANTED
  94 + ) 1
81 else 0 95 else 0
82 result.success(state) 96 result.success(state)
83 } 97 }
@@ -96,32 +110,83 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: @@ -96,32 +110,83 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
96 val permissions = arrayOf(Manifest.permission.CAMERA) 110 val permissions = arrayOf(Manifest.permission.CAMERA)
97 ActivityCompat.requestPermissions(activity, permissions, REQUEST_CODE) 111 ActivityCompat.requestPermissions(activity, permissions, REQUEST_CODE)
98 } 112 }
99 - 113 +// var lastScanned: List<Barcode>? = null
  114 +// var isAnalyzing: Boolean = false
100 115
101 @ExperimentalGetImage 116 @ExperimentalGetImage
102 val analyzer = ImageAnalysis.Analyzer { imageProxy -> // YUV_420_888 format 117 val analyzer = ImageAnalysis.Analyzer { imageProxy -> // YUV_420_888 format
103 -// when (analyzeMode) {  
104 -// AnalyzeMode.BARCODE -> { 118 +
105 val mediaImage = imageProxy.image ?: return@Analyzer 119 val mediaImage = imageProxy.image ?: return@Analyzer
106 val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees) 120 val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
107 121
108 scanner.process(inputImage) 122 scanner.process(inputImage)
109 .addOnSuccessListener { barcodes -> 123 .addOnSuccessListener { barcodes ->
110 - for (barcode in barcodes) {  
111 - val event = mapOf("name" to "barcode", "data" to barcode.data)  
112 - sink?.success(event)  
113 - }  
114 - }  
115 - .addOnFailureListener { e -> Log.e(TAG, e.message, e) }  
116 - .addOnCompleteListener { imageProxy.close() } 124 +// if (isAnalyzing) {
  125 +// Log.d("scanner", "SKIPPING" )
  126 +// return@addOnSuccessListener
  127 +// }
  128 +// isAnalyzing = true
  129 + val barcodeMap = barcodes.map { barcode -> barcode.data }
  130 + if (barcodeMap.isNotEmpty()) {
  131 + sink?.success(mapOf(
  132 + "name" to "barcode",
  133 + "data" to barcodeMap,
  134 + "image" to mediaImage.toByteArray()
  135 + ))
  136 + }
  137 +// for (barcode in barcodes) {
  138 +//// if (lastScanned?.contains(barcodes.first) == true) continue;
  139 +// if (lastScanned == null) {
  140 +// lastScanned = barcodes
  141 +// } else if (lastScanned!!.contains(barcode)) {
  142 +// // Duplicate, don't send image
  143 +// sink?.success(mapOf(
  144 +// "name" to "barcode",
  145 +// "data" to barcode.data,
  146 +// ))
  147 +// } else {
  148 +// if (byteArray.isEmpty()) {
  149 +// Log.d("scanner", "EMPTY" )
  150 +// return@addOnSuccessListener
117 // } 151 // }
118 -// else -> imageProxy.close() 152 +//
  153 +// Log.d("scanner", "SCANNED IMAGE: $byteArray")
  154 +// lastScanned = barcodes;
  155 +//
  156 +//
119 // } 157 // }
  158 +//
  159 +// }
  160 +// isAnalyzing = false
120 } 161 }
  162 + .addOnFailureListener { e -> sink?.success(mapOf(
  163 + "name" to "error",
  164 + "data" to e.localizedMessage
  165 + )) }
  166 + .addOnCompleteListener { imageProxy.close() }
  167 + }
  168 +
  169 + private fun Image.toByteArray(): ByteArray {
  170 + val yBuffer = planes[0].buffer // Y
  171 + val vuBuffer = planes[2].buffer // VU
  172 +
  173 + val ySize = yBuffer.remaining()
  174 + val vuSize = vuBuffer.remaining()
  175 +
  176 + val nv21 = ByteArray(ySize + vuSize)
121 177
  178 + yBuffer.get(nv21, 0, ySize)
  179 + vuBuffer.get(nv21, ySize, vuSize)
  180 +
  181 + val yuvImage = YuvImage(nv21, ImageFormat.NV21, this.width, this.height, null)
  182 + val out = ByteArrayOutputStream()
  183 + yuvImage.compressToJpeg(Rect(0, 0, yuvImage.width, yuvImage.height), 50, out)
  184 + return out.toByteArray()
  185 + }
122 186
123 private var scanner = BarcodeScanning.getClient() 187 private var scanner = BarcodeScanning.getClient()
124 188
  189 +
125 @ExperimentalGetImage 190 @ExperimentalGetImage
126 private fun start(call: MethodCall, result: MethodChannel.Result) { 191 private fun start(call: MethodCall, result: MethodChannel.Result) {
127 if (camera?.cameraInfo != null && preview != null && textureEntry != null) { 192 if (camera?.cameraInfo != null && preview != null && textureEntry != null) {
@@ -129,14 +194,23 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: @@ -129,14 +194,23 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
129 val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0 194 val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0
130 val width = resolution.width.toDouble() 195 val width = resolution.width.toDouble()
131 val height = resolution.height.toDouble() 196 val height = resolution.height.toDouble()
132 - val size = if (portrait) mapOf("width" to width, "height" to height) else mapOf("width" to height, "height" to width)  
133 - val answer = mapOf("textureId" to textureEntry!!.id(), "size" to size, "torchable" to camera!!.cameraInfo.hasFlashUnit()) 197 + val size = if (portrait) mapOf(
  198 + "width" to width,
  199 + "height" to height
  200 + ) else mapOf("width" to height, "height" to width)
  201 + val answer = mapOf(
  202 + "textureId" to textureEntry!!.id(),
  203 + "size" to size,
  204 + "torchable" to camera!!.cameraInfo.hasFlashUnit()
  205 + )
134 result.success(answer) 206 result.success(answer)
135 } else { 207 } else {
136 val facing: Int = call.argument<Int>("facing") ?: 0 208 val facing: Int = call.argument<Int>("facing") ?: 0
137 val ratio: Int? = call.argument<Int>("ratio") 209 val ratio: Int? = call.argument<Int>("ratio")
138 val torch: Boolean = call.argument<Boolean>("torch") ?: false 210 val torch: Boolean = call.argument<Boolean>("torch") ?: false
139 val formats: List<Int>? = call.argument<List<Int>>("formats") 211 val formats: List<Int>? = call.argument<List<Int>>("formats")
  212 +// val analyzerWidth = call.argument<Int>("ratio")
  213 +// val analyzeRHEIG = call.argument<Int>("ratio")
140 214
141 if (formats != null) { 215 if (formats != null) {
142 val formatsList: MutableList<Int> = mutableListOf() 216 val formatsList: MutableList<Int> = mutableListOf()
@@ -144,9 +218,17 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: @@ -144,9 +218,17 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
144 formatsList.add(BarcodeFormats.values()[index].intValue) 218 formatsList.add(BarcodeFormats.values()[index].intValue)
145 } 219 }
146 scanner = if (formatsList.size == 1) { 220 scanner = if (formatsList.size == 1) {
147 - BarcodeScanning.getClient(BarcodeScannerOptions.Builder().setBarcodeFormats(formatsList.first()).build()) 221 + BarcodeScanning.getClient(
  222 + BarcodeScannerOptions.Builder().setBarcodeFormats(formatsList.first())
  223 + .build()
  224 + )
148 } else { 225 } else {
149 - BarcodeScanning.getClient(BarcodeScannerOptions.Builder().setBarcodeFormats(formatsList.first(), *formatsList.subList(1, formatsList.size).toIntArray()).build()) 226 + BarcodeScanning.getClient(
  227 + BarcodeScannerOptions.Builder().setBarcodeFormats(
  228 + formatsList.first(),
  229 + *formatsList.subList(1, formatsList.size).toIntArray()
  230 + ).build()
  231 + )
150 } 232 }
151 } 233 }
152 234
@@ -168,7 +250,10 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: @@ -168,7 +250,10 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
168 // Preview 250 // Preview
169 val surfaceProvider = Preview.SurfaceProvider { request -> 251 val surfaceProvider = Preview.SurfaceProvider { request ->
170 val texture = textureEntry!!.surfaceTexture() 252 val texture = textureEntry!!.surfaceTexture()
171 - texture.setDefaultBufferSize(request.resolution.width, request.resolution.height) 253 + texture.setDefaultBufferSize(
  254 + request.resolution.width,
  255 + request.resolution.height
  256 + )
172 val surface = Surface(texture) 257 val surface = Surface(texture)
173 request.provideSurface(surface, executor) { } 258 request.provideSurface(surface, executor) { }
174 } 259 }
@@ -186,12 +271,19 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: @@ -186,12 +271,19 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
186 if (ratio != null) { 271 if (ratio != null) {
187 analysisBuilder.setTargetAspectRatio(ratio) 272 analysisBuilder.setTargetAspectRatio(ratio)
188 } 273 }
  274 +// analysisBuilder.setTargetResolution(Size(1440, 1920))
189 val analysis = analysisBuilder.build().apply { setAnalyzer(executor, analyzer) } 275 val analysis = analysisBuilder.build().apply { setAnalyzer(executor, analyzer) }
190 276
191 // Select the correct camera 277 // Select the correct camera
192 - val selector = if (facing == 0) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA 278 + val selector =
  279 + if (facing == 0) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA
193 280
194 - camera = cameraProvider!!.bindToLifecycle(activity as LifecycleOwner, selector, preview, analysis) 281 + camera = cameraProvider!!.bindToLifecycle(
  282 + activity as LifecycleOwner,
  283 + selector,
  284 + preview,
  285 + analysis
  286 + )
195 287
196 val analysisSize = analysis.resolutionInfo?.resolution ?: Size(0, 0) 288 val analysisSize = analysis.resolutionInfo?.resolution ?: Size(0, 0)
197 val previewSize = preview!!.resolutionInfo?.resolution ?: Size(0, 0) 289 val previewSize = preview!!.resolutionInfo?.resolution ?: Size(0, 0)
@@ -216,8 +308,15 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: @@ -216,8 +308,15 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
216 val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0 308 val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0
217 val width = resolution.width.toDouble() 309 val width = resolution.width.toDouble()
218 val height = resolution.height.toDouble() 310 val height = resolution.height.toDouble()
219 - val size = if (portrait) mapOf("width" to width, "height" to height) else mapOf("width" to height, "height" to width)  
220 - val answer = mapOf("textureId" to textureEntry!!.id(), "size" to size, "torchable" to camera!!.cameraInfo.hasFlashUnit()) 311 + val size = if (portrait) mapOf(
  312 + "width" to width,
  313 + "height" to height
  314 + ) else mapOf("width" to height, "height" to width)
  315 + val answer = mapOf(
  316 + "textureId" to textureEntry!!.id(),
  317 + "size" to size,
  318 + "torchable" to camera!!.cameraInfo.hasFlashUnit()
  319 + )
221 result.success(answer) 320 result.success(answer)
222 }, executor) 321 }, executor)
223 } 322 }
@@ -225,7 +324,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: @@ -225,7 +324,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
225 324
226 private fun toggleTorch(call: MethodCall, result: MethodChannel.Result) { 325 private fun toggleTorch(call: MethodCall, result: MethodChannel.Result) {
227 if (camera == null) { 326 if (camera == null) {
228 - result.error(TAG,"Called toggleTorch() while stopped!", null) 327 + result.error(TAG, "Called toggleTorch() while stopped!", null)
229 return 328 return
230 } 329 }
231 camera!!.cameraControl.enableTorch(call.arguments == 1) 330 camera!!.cameraControl.enableTorch(call.arguments == 1)
@@ -238,7 +337,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: @@ -238,7 +337,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
238 // } 337 // }
239 338
240 private fun analyzeImage(call: MethodCall, result: MethodChannel.Result) { 339 private fun analyzeImage(call: MethodCall, result: MethodChannel.Result) {
241 - val uri = Uri.fromFile( File(call.arguments.toString())) 340 + val uri = Uri.fromFile(File(call.arguments.toString()))
242 val inputImage = InputImage.fromFilePath(activity, uri) 341 val inputImage = InputImage.fromFilePath(activity, uri)
243 342
244 var barcodeFound = false 343 var barcodeFound = false
@@ -249,15 +348,17 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: @@ -249,15 +348,17 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
249 sink?.success(mapOf("name" to "barcode", "data" to barcode.data)) 348 sink?.success(mapOf("name" to "barcode", "data" to barcode.data))
250 } 349 }
251 } 350 }
252 - .addOnFailureListener { e -> Log.e(TAG, e.message, e)  
253 - result.error(TAG, e.message, e)} 351 + .addOnFailureListener { e ->
  352 + Log.e(TAG, e.message, e)
  353 + result.error(TAG, e.message, e)
  354 + }
254 .addOnCompleteListener { result.success(barcodeFound) } 355 .addOnCompleteListener { result.success(barcodeFound) }
255 356
256 } 357 }
257 358
258 private fun stop(result: MethodChannel.Result) { 359 private fun stop(result: MethodChannel.Result) {
259 if (camera == null && preview == null) { 360 if (camera == null && preview == null) {
260 - result.error(TAG,"Called stop() while already stopped!", null) 361 + result.error(TAG, "Called stop() while already stopped!", null)
261 return 362 return
262 } 363 }
263 364
@@ -277,41 +378,54 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: @@ -277,41 +378,54 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
277 378
278 379
279 private val Barcode.data: Map<String, Any?> 380 private val Barcode.data: Map<String, Any?>
280 - get() = mapOf("corners" to cornerPoints?.map { corner -> corner.data }, "format" to format, 381 + get() = mapOf(
  382 + "corners" to cornerPoints?.map { corner -> corner.data }, "format" to format,
281 "rawBytes" to rawBytes, "rawValue" to rawValue, "type" to valueType, 383 "rawBytes" to rawBytes, "rawValue" to rawValue, "type" to valueType,
282 "calendarEvent" to calendarEvent?.data, "contactInfo" to contactInfo?.data, 384 "calendarEvent" to calendarEvent?.data, "contactInfo" to contactInfo?.data,
283 "driverLicense" to driverLicense?.data, "email" to email?.data, 385 "driverLicense" to driverLicense?.data, "email" to email?.data,
284 "geoPoint" to geoPoint?.data, "phone" to phone?.data, "sms" to sms?.data, 386 "geoPoint" to geoPoint?.data, "phone" to phone?.data, "sms" to sms?.data,
285 - "url" to url?.data, "wifi" to wifi?.data, "displayValue" to displayValue) 387 + "url" to url?.data, "wifi" to wifi?.data, "displayValue" to displayValue
  388 + )
286 389
287 private val Point.data: Map<String, Double> 390 private val Point.data: Map<String, Double>
288 get() = mapOf("x" to x.toDouble(), "y" to y.toDouble()) 391 get() = mapOf("x" to x.toDouble(), "y" to y.toDouble())
289 392
290 private val Barcode.CalendarEvent.data: Map<String, Any?> 393 private val Barcode.CalendarEvent.data: Map<String, Any?>
291 - get() = mapOf("description" to description, "end" to end?.rawValue, "location" to location, 394 + get() = mapOf(
  395 + "description" to description, "end" to end?.rawValue, "location" to location,
292 "organizer" to organizer, "start" to start?.rawValue, "status" to status, 396 "organizer" to organizer, "start" to start?.rawValue, "status" to status,
293 - "summary" to summary) 397 + "summary" to summary
  398 + )
294 399
295 private val Barcode.ContactInfo.data: Map<String, Any?> 400 private val Barcode.ContactInfo.data: Map<String, Any?>
296 - get() = mapOf("addresses" to addresses.map { address -> address.data }, 401 + get() = mapOf(
  402 + "addresses" to addresses.map { address -> address.data },
297 "emails" to emails.map { email -> email.data }, "name" to name?.data, 403 "emails" to emails.map { email -> email.data }, "name" to name?.data,
298 "organization" to organization, "phones" to phones.map { phone -> phone.data }, 404 "organization" to organization, "phones" to phones.map { phone -> phone.data },
299 - "title" to title, "urls" to urls) 405 + "title" to title, "urls" to urls
  406 + )
300 407
301 private val Barcode.Address.data: Map<String, Any?> 408 private val Barcode.Address.data: Map<String, Any?>
302 - get() = mapOf("addressLines" to addressLines.map { addressLine -> addressLine.toString() }, "type" to type) 409 + get() = mapOf(
  410 + "addressLines" to addressLines.map { addressLine -> addressLine.toString() },
  411 + "type" to type
  412 + )
303 413
304 private val Barcode.PersonName.data: Map<String, Any?> 414 private val Barcode.PersonName.data: Map<String, Any?>
305 - get() = mapOf("first" to first, "formattedName" to formattedName, "last" to last, 415 + get() = mapOf(
  416 + "first" to first, "formattedName" to formattedName, "last" to last,
306 "middle" to middle, "prefix" to prefix, "pronunciation" to pronunciation, 417 "middle" to middle, "prefix" to prefix, "pronunciation" to pronunciation,
307 - "suffix" to suffix) 418 + "suffix" to suffix
  419 + )
308 420
309 private val Barcode.DriverLicense.data: Map<String, Any?> 421 private val Barcode.DriverLicense.data: Map<String, Any?>
310 - get() = mapOf("addressCity" to addressCity, "addressState" to addressState, 422 + get() = mapOf(
  423 + "addressCity" to addressCity, "addressState" to addressState,
311 "addressStreet" to addressStreet, "addressZip" to addressZip, "birthDate" to birthDate, 424 "addressStreet" to addressStreet, "addressZip" to addressZip, "birthDate" to birthDate,
312 "documentType" to documentType, "expiryDate" to expiryDate, "firstName" to firstName, 425 "documentType" to documentType, "expiryDate" to expiryDate, "firstName" to firstName,
313 "gender" to gender, "issueDate" to issueDate, "issuingCountry" to issuingCountry, 426 "gender" to gender, "issueDate" to issueDate, "issuingCountry" to issuingCountry,
314 - "lastName" to lastName, "licenseNumber" to licenseNumber, "middleName" to middleName) 427 + "lastName" to lastName, "licenseNumber" to licenseNumber, "middleName" to middleName
  428 + )
315 429
316 private val Barcode.Email.data: Map<String, Any?> 430 private val Barcode.Email.data: Map<String, Any?>
317 get() = mapOf("address" to address, "body" to body, "subject" to subject, "type" to type) 431 get() = mapOf("address" to address, "body" to body, "subject" to subject, "type" to type)
@@ -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
@@ -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;
@@ -355,7 +355,7 @@ @@ -355,7 +355,7 @@
355 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 355 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
356 CLANG_ENABLE_MODULES = YES; 356 CLANG_ENABLE_MODULES = YES;
357 CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 357 CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
358 - DEVELOPMENT_TEAM = RCH2VG82SH; 358 + DEVELOPMENT_TEAM = 75Y2P2WSQQ;
359 ENABLE_BITCODE = NO; 359 ENABLE_BITCODE = NO;
360 INFOPLIST_FILE = Runner/Info.plist; 360 INFOPLIST_FILE = Runner/Info.plist;
361 LD_RUNPATH_SEARCH_PATHS = ( 361 LD_RUNPATH_SEARCH_PATHS = (
@@ -417,7 +417,7 @@ @@ -417,7 +417,7 @@
417 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 417 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
418 GCC_WARN_UNUSED_FUNCTION = YES; 418 GCC_WARN_UNUSED_FUNCTION = YES;
419 GCC_WARN_UNUSED_VARIABLE = YES; 419 GCC_WARN_UNUSED_VARIABLE = YES;
420 - IPHONEOS_DEPLOYMENT_TARGET = 9.0; 420 + IPHONEOS_DEPLOYMENT_TARGET = 11.0;
421 MTL_ENABLE_DEBUG_INFO = YES; 421 MTL_ENABLE_DEBUG_INFO = YES;
422 ONLY_ACTIVE_ARCH = YES; 422 ONLY_ACTIVE_ARCH = YES;
423 SDKROOT = iphoneos; 423 SDKROOT = iphoneos;
@@ -466,7 +466,7 @@ @@ -466,7 +466,7 @@
466 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 466 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
467 GCC_WARN_UNUSED_FUNCTION = YES; 467 GCC_WARN_UNUSED_FUNCTION = YES;
468 GCC_WARN_UNUSED_VARIABLE = YES; 468 GCC_WARN_UNUSED_VARIABLE = YES;
469 - IPHONEOS_DEPLOYMENT_TARGET = 9.0; 469 + IPHONEOS_DEPLOYMENT_TARGET = 11.0;
470 MTL_ENABLE_DEBUG_INFO = NO; 470 MTL_ENABLE_DEBUG_INFO = NO;
471 SDKROOT = iphoneos; 471 SDKROOT = iphoneos;
472 SUPPORTED_PLATFORMS = iphoneos; 472 SUPPORTED_PLATFORMS = iphoneos;
@@ -484,7 +484,7 @@ @@ -484,7 +484,7 @@
484 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 484 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
485 CLANG_ENABLE_MODULES = YES; 485 CLANG_ENABLE_MODULES = YES;
486 CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 486 CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
487 - DEVELOPMENT_TEAM = RCH2VG82SH; 487 + DEVELOPMENT_TEAM = 75Y2P2WSQQ;
488 ENABLE_BITCODE = NO; 488 ENABLE_BITCODE = NO;
489 INFOPLIST_FILE = Runner/Info.plist; 489 INFOPLIST_FILE = Runner/Info.plist;
490 LD_RUNPATH_SEARCH_PATHS = ( 490 LD_RUNPATH_SEARCH_PATHS = (
@@ -507,7 +507,7 @@ @@ -507,7 +507,7 @@
507 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 507 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
508 CLANG_ENABLE_MODULES = YES; 508 CLANG_ENABLE_MODULES = YES;
509 CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 509 CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
510 - DEVELOPMENT_TEAM = RCH2VG82SH; 510 + DEVELOPMENT_TEAM = 75Y2P2WSQQ;
511 ENABLE_BITCODE = NO; 511 ENABLE_BITCODE = NO;
512 INFOPLIST_FILE = Runner/Info.plist; 512 INFOPLIST_FILE = Runner/Info.plist;
513 LD_RUNPATH_SEARCH_PATHS = ( 513 LD_RUNPATH_SEARCH_PATHS = (
  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 + MobileScannerController controller = MobileScannerController(
  19 + torchEnabled: true,
  20 + // formats: [BarcodeFormat.qrCode]
  21 + // facing: CameraFacing.front,
  22 + );
  23 +
  24 + bool isStarted = true;
  25 +
  26 + @override
  27 + Widget build(BuildContext context) {
  28 + return Scaffold(
  29 + backgroundColor: Colors.black,
  30 + body: Builder(
  31 + builder: (context) {
  32 + return Stack(
  33 + children: [
  34 + MobileScanner(
  35 + controller: controller,
  36 + fit: BoxFit.contain,
  37 + // allowDuplicates: true,
  38 + // controller: MobileScannerController(
  39 + // torchEnabled: true,
  40 + // facing: CameraFacing.front,
  41 + // ),
  42 + onDetect: (barcodeCapture, arguments) {
  43 + setState(() {
  44 + this.barcodeCapture = barcodeCapture;
  45 + });
  46 + },
  47 + ),
  48 + Align(
  49 + alignment: Alignment.bottomCenter,
  50 + child: Container(
  51 + alignment: Alignment.bottomCenter,
  52 + height: 100,
  53 + color: Colors.black.withOpacity(0.4),
  54 + child: Row(
  55 + mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  56 + children: [
  57 + IconButton(
  58 + color: Colors.white,
  59 + icon: ValueListenableBuilder(
  60 + valueListenable: controller.torchState,
  61 + builder: (context, state, child) {
  62 + if (state == null) {
  63 + return const Icon(
  64 + Icons.flash_off,
  65 + color: Colors.grey,
  66 + );
  67 + }
  68 + switch (state as TorchState) {
  69 + case TorchState.off:
  70 + return const Icon(
  71 + Icons.flash_off,
  72 + color: Colors.grey,
  73 + );
  74 + case TorchState.on:
  75 + return const Icon(
  76 + Icons.flash_on,
  77 + color: Colors.yellow,
  78 + );
  79 + }
  80 + },
  81 + ),
  82 + iconSize: 32.0,
  83 + onPressed: () => controller.toggleTorch(),
  84 + ),
  85 + IconButton(
  86 + color: Colors.white,
  87 + icon: isStarted
  88 + ? const Icon(Icons.stop)
  89 + : const Icon(Icons.play_arrow),
  90 + iconSize: 32.0,
  91 + onPressed: () => setState(() {
  92 + isStarted ? controller.stop() : controller.start();
  93 + isStarted = !isStarted;
  94 + }),
  95 + ),
  96 + Center(
  97 + child: SizedBox(
  98 + width: MediaQuery.of(context).size.width - 200,
  99 + height: 50,
  100 + child: FittedBox(
  101 + child: Text(
  102 + '${barcodeCapture?.barcodes.map((e) => e.rawValue)}',
  103 + overflow: TextOverflow.fade,
  104 + style: Theme.of(context)
  105 + .textTheme
  106 + .headline4!
  107 + .copyWith(color: Colors.white),
  108 + ),
  109 + ),
  110 + ),
  111 + ),
  112 + IconButton(
  113 + color: Colors.white,
  114 + icon: ValueListenableBuilder(
  115 + valueListenable: controller.cameraFacingState,
  116 + builder: (context, state, child) {
  117 + if (state == null) {
  118 + return const Icon(Icons.camera_front);
  119 + }
  120 + switch (state as CameraFacing) {
  121 + case CameraFacing.front:
  122 + return const Icon(Icons.camera_front);
  123 + case CameraFacing.back:
  124 + return const Icon(Icons.camera_rear);
  125 + }
  126 + },
  127 + ),
  128 + iconSize: 32.0,
  129 + onPressed: () => controller.switchCamera(),
  130 + ),
  131 + IconButton(
  132 + color: Colors.white,
  133 + icon: const Icon(Icons.image),
  134 + iconSize: 32.0,
  135 + onPressed: () async {
  136 + final ImagePicker picker = ImagePicker();
  137 + // Pick an image
  138 + final XFile? image = await picker.pickImage(
  139 + source: ImageSource.gallery,
  140 + );
  141 + if (image != null) {
  142 + if (await controller.analyzeImage(image.path)) {
  143 + if (!mounted) return;
  144 + ScaffoldMessenger.of(context).showSnackBar(
  145 + const SnackBar(
  146 + content: Text('Barcode found!'),
  147 + backgroundColor: Colors.green,
  148 + ),
  149 + );
  150 + } else {
  151 + if (!mounted) return;
  152 + ScaffoldMessenger.of(context).showSnackBar(
  153 + const SnackBar(
  154 + content: Text('No barcode found!'),
  155 + backgroundColor: Colors.red,
  156 + ),
  157 + );
  158 + }
  159 + }
  160 + },
  161 + ),
  162 + ],
  163 + ),
  164 + ),
  165 + ),
  166 + ],
  167 + );
  168 + },
  169 + ),
  170 + );
  171 + }
  172 +}
@@ -13,10 +13,10 @@ class BarcodeScannerWithController extends StatefulWidget { @@ -13,10 +13,10 @@ 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 MobileScannerController controller = MobileScannerController(
19 - torchEnabled: true, 19 + torchEnabled: true, detectionSpeed: DetectionSpeed.unrestricted,
20 // formats: [BarcodeFormat.qrCode] 20 // formats: [BarcodeFormat.qrCode]
21 // facing: CameraFacing.front, 21 // facing: CameraFacing.front,
22 ); 22 );
@@ -41,7 +41,7 @@ class _BarcodeScannerWithControllerState @@ -41,7 +41,7 @@ class _BarcodeScannerWithControllerState
41 // ), 41 // ),
42 onDetect: (barcode, args) { 42 onDetect: (barcode, args) {
43 setState(() { 43 setState(() {
44 - this.barcode = barcode.rawValue; 44 + this.barcode = barcode;
45 }); 45 });
46 }, 46 },
47 ), 47 ),
@@ -99,7 +99,8 @@ class _BarcodeScannerWithControllerState @@ -99,7 +99,8 @@ class _BarcodeScannerWithControllerState
99 height: 50, 99 height: 50,
100 child: FittedBox( 100 child: FittedBox(
101 child: Text( 101 child: Text(
102 - barcode ?? 'Scan something!', 102 + barcode?.barcodes.first.rawValue ??
  103 + 'Scan something!',
103 overflow: TextOverflow.fade, 104 overflow: TextOverflow.fade,
104 style: Theme.of(context) 105 style: Theme.of(context)
105 .textTheme 106 .textTheme
1 -import 'dart:typed_data'; 1 +import 'dart:math';
2 2
3 import 'package:flutter/material.dart'; 3 import 'package:flutter/material.dart';
4 import 'package:mobile_scanner/mobile_scanner.dart'; 4 import 'package:mobile_scanner/mobile_scanner.dart';
@@ -14,11 +14,11 @@ class BarcodeScannerReturningImage extends StatefulWidget { @@ -14,11 +14,11 @@ class BarcodeScannerReturningImage extends StatefulWidget {
14 class _BarcodeScannerReturningImageState 14 class _BarcodeScannerReturningImageState
15 extends State<BarcodeScannerReturningImage> 15 extends State<BarcodeScannerReturningImage>
16 with SingleTickerProviderStateMixin { 16 with SingleTickerProviderStateMixin {
17 - String? barcode;  
18 - Uint8List? image; 17 + BarcodeCapture? barcode;
  18 + MobileScannerArguments? arguments;
19 19
20 MobileScannerController controller = MobileScannerController( 20 MobileScannerController controller = MobileScannerController(
21 - torchEnabled: true, 21 + // torchEnabled: true,
22 returnImage: true, 22 returnImage: true,
23 // formats: [BarcodeFormat.qrCode] 23 // formats: [BarcodeFormat.qrCode]
24 // facing: CameraFacing.front, 24 // facing: CameraFacing.front,
@@ -32,7 +32,34 @@ class _BarcodeScannerReturningImageState @@ -32,7 +32,34 @@ class _BarcodeScannerReturningImageState
32 backgroundColor: Colors.black, 32 backgroundColor: Colors.black,
33 body: Builder( 33 body: Builder(
34 builder: (context) { 34 builder: (context) {
35 - return Stack( 35 + return Column(
  36 + children: [
  37 + Container(
  38 + color: Colors.blueGrey,
  39 + width: double.infinity,
  40 + height: 0.33 * MediaQuery.of(context).size.height,
  41 + child: barcode?.image != null
  42 + ? Transform.rotate(
  43 + angle: 90 * pi / 180,
  44 + child: Image(
  45 + gaplessPlayback: true,
  46 + image: MemoryImage(barcode!.image!),
  47 + fit: BoxFit.contain,
  48 + ),
  49 + )
  50 + : const ColoredBox(
  51 + color: Colors.white,
  52 + child: Center(
  53 + child: Text(
  54 + 'Your scanned barcode will appear here!',
  55 + ),
  56 + ),
  57 + ),
  58 + ),
  59 + Container(
  60 + height: 0.66 * MediaQuery.of(context).size.height,
  61 + color: Colors.grey,
  62 + child: Stack(
36 children: [ 63 children: [
37 MobileScanner( 64 MobileScanner(
38 controller: controller, 65 controller: controller,
@@ -42,17 +69,10 @@ class _BarcodeScannerReturningImageState @@ -42,17 +69,10 @@ class _BarcodeScannerReturningImageState
42 // torchEnabled: true, 69 // torchEnabled: true,
43 // facing: CameraFacing.front, 70 // facing: CameraFacing.front,
44 // ), 71 // ),
45 - onDetect: (barcode, args) { 72 + onDetect: (barcode, arguments) {
46 setState(() { 73 setState(() {
47 - this.barcode = barcode.rawValue;  
48 - showDialog(  
49 - context: context,  
50 - builder: (context) => Image(  
51 - image: MemoryImage(image!),  
52 - fit: BoxFit.contain,  
53 - ),  
54 - );  
55 - image = barcode.image; 74 + this.arguments = arguments;
  75 + this.barcode = barcode;
56 }); 76 });
57 }, 77 },
58 ), 78 ),
@@ -65,8 +85,12 @@ class _BarcodeScannerReturningImageState @@ -65,8 +85,12 @@ class _BarcodeScannerReturningImageState
65 child: Row( 85 child: Row(
66 mainAxisAlignment: MainAxisAlignment.spaceEvenly, 86 mainAxisAlignment: MainAxisAlignment.spaceEvenly,
67 children: [ 87 children: [
68 - IconButton(  
69 - color: Colors.white, 88 + ColoredBox(
  89 + color: arguments != null && !arguments!.hasTorch
  90 + ? Colors.red
  91 + : Colors.white,
  92 + child: IconButton(
  93 + // color: ,
70 icon: ValueListenableBuilder( 94 icon: ValueListenableBuilder(
71 valueListenable: controller.torchState, 95 valueListenable: controller.torchState,
72 builder: (context, state, child) { 96 builder: (context, state, child) {
@@ -93,6 +117,7 @@ class _BarcodeScannerReturningImageState @@ -93,6 +117,7 @@ class _BarcodeScannerReturningImageState
93 iconSize: 32.0, 117 iconSize: 32.0,
94 onPressed: () => controller.toggleTorch(), 118 onPressed: () => controller.toggleTorch(),
95 ), 119 ),
  120 + ),
96 IconButton( 121 IconButton(
97 color: Colors.white, 122 color: Colors.white,
98 icon: isStarted 123 icon: isStarted
@@ -100,7 +125,9 @@ class _BarcodeScannerReturningImageState @@ -100,7 +125,9 @@ class _BarcodeScannerReturningImageState
100 : const Icon(Icons.play_arrow), 125 : const Icon(Icons.play_arrow),
101 iconSize: 32.0, 126 iconSize: 32.0,
102 onPressed: () => setState(() { 127 onPressed: () => setState(() {
103 - isStarted ? controller.stop() : controller.start(); 128 + isStarted
  129 + ? controller.stop()
  130 + : controller.start();
104 isStarted = !isStarted; 131 isStarted = !isStarted;
105 }), 132 }),
106 ), 133 ),
@@ -110,7 +137,8 @@ class _BarcodeScannerReturningImageState @@ -110,7 +137,8 @@ class _BarcodeScannerReturningImageState
110 height: 50, 137 height: 50,
111 child: FittedBox( 138 child: FittedBox(
112 child: Text( 139 child: Text(
113 - barcode ?? 'Scan something!', 140 + barcode?.barcodes.first.rawValue ??
  141 + 'Scan something!',
114 overflow: TextOverflow.fade, 142 overflow: TextOverflow.fade,
115 style: Theme.of(context) 143 style: Theme.of(context)
116 .textTheme 144 .textTheme
@@ -139,21 +167,14 @@ class _BarcodeScannerReturningImageState @@ -139,21 +167,14 @@ class _BarcodeScannerReturningImageState
139 iconSize: 32.0, 167 iconSize: 32.0,
140 onPressed: () => controller.switchCamera(), 168 onPressed: () => controller.switchCamera(),
141 ), 169 ),
142 - SizedBox(  
143 - width: 50,  
144 - height: 50,  
145 - child: image != null  
146 - ? Image(  
147 - image: MemoryImage(image!),  
148 - fit: BoxFit.contain,  
149 - )  
150 - : Container(),  
151 - ),  
152 ], 170 ],
153 ), 171 ),
154 ), 172 ),
155 ), 173 ),
156 ], 174 ],
  175 + ),
  176 + ),
  177 + ],
157 ); 178 );
158 }, 179 },
159 ), 180 ),
@@ -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) {
@@ -25,9 +25,9 @@ class _BarcodeScannerWithoutControllerState @@ -25,9 +25,9 @@ class _BarcodeScannerWithoutControllerState
25 MobileScanner( 25 MobileScanner(
26 fit: BoxFit.contain, 26 fit: BoxFit.contain,
27 // allowDuplicates: false, 27 // allowDuplicates: false,
28 - onDetect: (barcode, args) { 28 + onDetect: (capture, arguments) {
29 setState(() { 29 setState(() {
30 - this.barcode = barcode.rawValue; 30 + this.capture = capture;
31 }); 31 });
32 }, 32 },
33 ), 33 ),
@@ -46,7 +46,8 @@ class _BarcodeScannerWithoutControllerState @@ -46,7 +46,8 @@ class _BarcodeScannerWithoutControllerState
46 height: 50, 46 height: 50,
47 child: FittedBox( 47 child: FittedBox(
48 child: Text( 48 child: Text(
49 - barcode ?? 'Scan something!', 49 + capture?.barcodes.first.rawValue ??
  50 + 'Scan something!',
50 overflow: TextOverflow.fade, 51 overflow: TextOverflow.fade,
51 style: Theme.of(context) 52 style: Theme.of(context)
52 .textTheme 53 .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';
3 import 'package:mobile_scanner_example/barcode_scanner_returning_image.dart'; 4 import 'package:mobile_scanner_example/barcode_scanner_returning_image.dart';
4 import 'package:mobile_scanner_example/barcode_scanner_without_controller.dart'; 5 import 'package:mobile_scanner_example/barcode_scanner_without_controller.dart';
@@ -22,6 +23,17 @@ class MyHome extends StatelessWidget { @@ -22,6 +23,17 @@ class MyHome extends StatelessWidget {
22 onPressed: () { 23 onPressed: () {
23 Navigator.of(context).push( 24 Navigator.of(context).push(
24 MaterialPageRoute( 25 MaterialPageRoute(
  26 + builder: (context) =>
  27 + const BarcodeListScannerWithController(),
  28 + ),
  29 + );
  30 + },
  31 + child: const Text('MobileScanner with List Controller'),
  32 + ),
  33 + ElevatedButton(
  34 + onPressed: () {
  35 + Navigator.of(context).push(
  36 + MaterialPageRoute(
25 builder: (context) => const BarcodeScannerWithController(), 37 builder: (context) => const BarcodeScannerWithController(),
26 ), 38 ),
27 ); 39 );
  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 +// 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 permissions for video
  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 + /// Start scanning for barcodes
  71 + func start(barcodeScannerOptions: BarcodeScannerOptions?, returnImage: Bool, cameraPosition: AVCaptureDevice.Position, torch: AVCaptureDevice.TorchMode, detectionSpeed: DetectionSpeed) throws -> MobileScannerStartParameters {
  72 + self.detectionSpeed = detectionSpeed
  73 + if (device != nil) {
  74 + throw MobileScannerError.alreadyStarted
  75 + }
  76 +
  77 + scanner = barcodeScannerOptions != nil ? BarcodeScanner.barcodeScanner(options: barcodeScannerOptions!) : BarcodeScanner.barcodeScanner()
  78 + captureSession = AVCaptureSession()
  79 + textureId = registry?.register(self)
  80 +
  81 + // Open the camera device
  82 + if #available(iOS 10.0, *) {
  83 + device = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: cameraPosition).devices.first
  84 + } else {
  85 + device = AVCaptureDevice.devices(for: .video).filter({$0.position == cameraPosition}).first
  86 + }
  87 +
  88 + if (device == nil) {
  89 + throw MobileScannerError.noCamera
  90 + }
  91 +
  92 + // Enable the torch if parameter is set and torch is available
  93 + if (device.hasTorch && device.isTorchAvailable) {
  94 + do {
  95 + try device.lockForConfiguration()
  96 + device.torchMode = torch
  97 + device.unlockForConfiguration()
  98 + } catch {
  99 + throw MobileScannerError.torchError(error)
  100 + }
  101 + }
  102 +
  103 + device.addObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode), options: .new, context: nil)
  104 + captureSession.beginConfiguration()
  105 +
  106 + // Add device input
  107 + do {
  108 + let input = try AVCaptureDeviceInput(device: device)
  109 + captureSession.addInput(input)
  110 + } catch {
  111 + throw MobileScannerError.cameraError(error)
  112 + }
  113 +
  114 + captureSession.sessionPreset = AVCaptureSession.Preset.photo;
  115 + // Add video output.
  116 + let videoOutput = AVCaptureVideoDataOutput()
  117 +
  118 + videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA]
  119 + videoOutput.alwaysDiscardsLateVideoFrames = true
  120 +
  121 + videoPosition = cameraPosition
  122 + // calls captureOutput()
  123 + videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue.main)
  124 +
  125 + captureSession.addOutput(videoOutput)
  126 + for connection in videoOutput.connections {
  127 + connection.videoOrientation = .portrait
  128 + if cameraPosition == .front && connection.isVideoMirroringSupported {
  129 + connection.isVideoMirrored = true
  130 + }
  131 + }
  132 + captureSession.commitConfiguration()
  133 + captureSession.startRunning()
  134 + let dimensions = CMVideoFormatDescriptionGetDimensions(device.activeFormat.formatDescription)
  135 +
  136 + return MobileScannerStartParameters(width: Double(dimensions.height), height: Double(dimensions.width), hasTorch: device.hasTorch, textureId: textureId)
  137 + }
  138 +
  139 + struct MobileScannerStartParameters {
  140 + var width: Double = 0.0
  141 + var height: Double = 0.0
  142 + var hasTorch = false
  143 + var textureId: Int64 = 0
  144 + }
  145 +
  146 + /// Stop scanning for barcodes
  147 + func stop() throws {
  148 + if (device == nil) {
  149 + throw MobileScannerError.alreadyStopped
  150 + }
  151 + captureSession.stopRunning()
  152 + for input in captureSession.inputs {
  153 + captureSession.removeInput(input)
  154 + }
  155 + for output in captureSession.outputs {
  156 + captureSession.removeOutput(output)
  157 + }
  158 + device.removeObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode))
  159 + registry?.unregisterTexture(textureId)
  160 + textureId = nil
  161 + captureSession = nil
  162 + device = nil
  163 + }
  164 +
  165 + /// Toggle the flashlight between on and off
  166 + func toggleTorch(_ torch: AVCaptureDevice.TorchMode) throws {
  167 + if (device == nil) {
  168 + throw MobileScannerError.torchWhenStopped
  169 + }
  170 + do {
  171 + try device.lockForConfiguration()
  172 + device.torchMode = torch
  173 + device.unlockForConfiguration()
  174 + } catch {
  175 + throw MobileScannerError.torchError(error)
  176 + }
  177 + }
  178 +
  179 + /// Analyze a single image
  180 + func analyzeImage(image: UIImage, position: AVCaptureDevice.Position, callback: @escaping BarcodeScanningCallback) {
  181 + let image = VisionImage(image: image)
  182 + image.orientation = imageOrientation(
  183 + deviceOrientation: UIDevice.current.orientation,
  184 + defaultOrientation: .portrait,
  185 + position: position
  186 + )
  187 +
  188 + scanner.process(image, completion: callback)
  189 + }
  190 +
  191 + var i = 0
  192 +
  193 + var barcodesString: Array<String?>?
  194 +
  195 + /// Gets called when a new image is added to the buffer
  196 + public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
  197 + guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
  198 + print("Failed to get image buffer from sample buffer.")
  199 + return
  200 + }
  201 + latestBuffer = imageBuffer
  202 + registry?.textureFrameAvailable(textureId)
  203 + if ((detectionSpeed == DetectionSpeed.normal || detectionSpeed == DetectionSpeed.noDuplicates) && i > 10 || detectionSpeed == DetectionSpeed.unrestricted) {
  204 + i = 0
  205 + let ciImage = latestBuffer.image
  206 +
  207 + let image = VisionImage(image: ciImage)
  208 + image.orientation = imageOrientation(
  209 + deviceOrientation: UIDevice.current.orientation,
  210 + defaultOrientation: .portrait,
  211 + position: videoPosition
  212 + )
  213 +
  214 + scanner.process(image) { [self] barcodes, error in
  215 + if (detectionSpeed == DetectionSpeed.noDuplicates) {
  216 + let newScannedBarcodes = barcodes?.map { barcode in
  217 + return barcode.rawValue
  218 + }
  219 + if (error == nil && barcodesString != nil && newScannedBarcodes != nil && barcodesString!.elementsEqual(newScannedBarcodes!)) {
  220 + return
  221 + } else {
  222 + barcodesString = newScannedBarcodes
  223 + }
  224 + }
  225 +
  226 + mobileScannerCallback(barcodes, error, ciImage)
  227 + }
  228 + } else {
  229 + i+=1
  230 + }
  231 + }
  232 +
  233 + /// Convert image buffer to jpeg
  234 + private func ciImageToJpeg(ciImage: CIImage) -> Data {
  235 +
  236 + // let ciImage = CIImage(cvPixelBuffer: latestBuffer)
  237 + let context:CIContext = CIContext.init(options: nil)
  238 + let cgImage:CGImage = context.createCGImage(ciImage, from: ciImage.extent)!
  239 + let uiImage:UIImage = UIImage(cgImage: cgImage, scale: 1, orientation: UIImage.Orientation.up)
  240 +
  241 + return uiImage.jpegData(compressionQuality: 0.8)!;
  242 + }
  243 +
  244 + /// Rotates images accordingly
  245 + func imageOrientation(
  246 + deviceOrientation: UIDeviceOrientation,
  247 + defaultOrientation: UIDeviceOrientation,
  248 + position: AVCaptureDevice.Position
  249 + ) -> UIImage.Orientation {
  250 + switch deviceOrientation {
  251 + case .portrait:
  252 + return position == .front ? .leftMirrored : .right
  253 + case .landscapeLeft:
  254 + return position == .front ? .downMirrored : .up
  255 + case .portraitUpsideDown:
  256 + return position == .front ? .rightMirrored : .left
  257 + case .landscapeRight:
  258 + return position == .front ? .upMirrored : .down
  259 + case .faceDown, .faceUp, .unknown:
  260 + return .up
  261 + @unknown default:
  262 + return imageOrientation(deviceOrientation: defaultOrientation, defaultOrientation: .portrait, position: .back)
  263 + }
  264 + }
  265 +
  266 + /// Sends output of OutputBuffer to a Flutter texture
  267 + public func copyPixelBuffer() -> Unmanaged<CVPixelBuffer>? {
  268 + if latestBuffer == nil {
  269 + return nil
  270 + }
  271 + return Unmanaged<CVPixelBuffer>.passRetained(latestBuffer)
  272 + }
  273 +
  274 +}
  275 +
  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 +}
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 5
6 -public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, FlutterTexture, AVCaptureVideoDataOutputSampleBufferDelegate {  
7 -  
8 - let registry: FlutterTextureRegistry  
9 -  
10 - // Sink for publishing event changes  
11 - var sink: FlutterEventSink!  
12 -  
13 - // Texture id of the camera preview  
14 - var textureId: Int64!  
15 -  
16 - // Capture session of the camera  
17 - var captureSession: AVCaptureSession!  
18 -  
19 - // The selected camera  
20 - var device: AVCaptureDevice!  
21 -  
22 - // Image to be sent to the texture  
23 - var latestBuffer: CVImageBuffer!  
24 -  
25 - // Return image buffer with the Barcode event  
26 - var returnImage: Bool = false  
27 -  
28 -// var analyzeMode: Int = 0  
29 - var analyzing: Bool = false  
30 - var position = AVCaptureDevice.Position.back 6 +public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
31 7
32 - var scanner = BarcodeScanner.barcodeScanner() 8 + /// The mobile scanner object that handles all logic
  9 + private let mobileScanner: MobileScanner
33 10
34 - public static func register(with registrar: FlutterPluginRegistrar) {  
35 - let instance = SwiftMobileScannerPlugin(registrar.textures()) 11 + /// The handler sends all information via an event channel back to Flutter
  12 + private let barcodeHandler: BarcodeHandler
36 13
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) 14 + init(barcodeHandler: BarcodeHandler, registry: FlutterTextureRegistry) {
  15 + self.mobileScanner = MobileScanner(registry: registry, mobileScannerCallback: { barcodes, error, image in
  16 + if barcodes != nil {
  17 + let barcodesMap = barcodes!.map { barcode in
  18 + return barcode.data
43 } 19 }
44 -  
45 - init(_ registry: FlutterTextureRegistry) {  
46 - self.registry = registry 20 + if (!barcodesMap.isEmpty) {
  21 + barcodeHandler.publishEvent(["name": "barcode", "data": barcodesMap, "image": FlutterStandardTypedData(bytes: image.jpegData(compressionQuality: 0.8)!)])
  22 + }
  23 + } else if (error != nil){
  24 + barcodeHandler.publishEvent(["name": "error", "data": error!.localizedDescription])
  25 + }
  26 + })
  27 + self.barcodeHandler = barcodeHandler
47 super.init() 28 super.init()
48 } 29 }
49 30
  31 + public static func register(with registrar: FlutterPluginRegistrar) {
  32 + let instance = SwiftMobileScannerPlugin(barcodeHandler: BarcodeHandler(registrar: registrar), registry: registrar.textures())
  33 + let methodChannel = FlutterMethodChannel(name:
  34 + "dev.steenbakker.mobile_scanner/scanner/method", binaryMessenger: registrar.messenger())
  35 + registrar.addMethodCallDelegate(instance, channel: methodChannel)
  36 + }
50 37
51 public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { 38 public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
52 switch call.method { 39 switch call.method {
53 case "state": 40 case "state":
54 - checkPermission(call, result) 41 + result(mobileScanner.checkPermission())
55 case "request": 42 case "request":
56 - requestPermission(call, result) 43 + AVCaptureDevice.requestAccess(for: .video, completionHandler: { result($0) })
57 case "start": 44 case "start":
58 start(call, result) 45 start(call, result)
59 - case "torch":  
60 - toggleTorch(call, result)  
61 -// case "analyze":  
62 -// switchAnalyzeMode(call, result)  
63 case "stop": 46 case "stop":
64 stop(result) 47 stop(result)
  48 + case "torch":
  49 + toggleTorch(call, result)
65 case "analyzeImage": 50 case "analyzeImage":
66 analyzeImage(call, result) 51 analyzeImage(call, result)
67 -  
68 default: 52 default:
69 result(FlutterMethodNotImplemented) 53 result(FlutterMethodNotImplemented)
70 } 54 }
71 } 55 }
72 56
73 - // FlutterStreamHandler  
74 - public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {  
75 - sink = events  
76 - return nil  
77 - }  
78 -  
79 - // FlutterStreamHandler  
80 - public func onCancel(withArguments arguments: Any?) -> FlutterError? {  
81 - sink = nil  
82 - return nil  
83 - }  
84 -  
85 - // FlutterTexture  
86 - public func copyPixelBuffer() -> Unmanaged<CVPixelBuffer>? {  
87 - if latestBuffer == nil {  
88 - return nil  
89 - }  
90 - return Unmanaged<CVPixelBuffer>.passRetained(latestBuffer)  
91 - }  
92 -  
93 - private func ciImageToJpeg(ciImage: CIImage) -> Data {  
94 -  
95 - // let ciImage = CIImage(cvPixelBuffer: latestBuffer)  
96 - let context:CIContext = CIContext.init(options: nil)  
97 - let cgImage:CGImage = context.createCGImage(ciImage, from: ciImage.extent)!  
98 - let uiImage:UIImage = UIImage(cgImage: cgImage, scale: 1, orientation: UIImage.Orientation.up)  
99 -  
100 - return uiImage.jpegData(compressionQuality: 0.8)!;  
101 - }  
102 -  
103 - // Gets called when a new image is added to the buffer  
104 - public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {  
105 -  
106 - latestBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)  
107 - registry.textureFrameAvailable(textureId)  
108 -  
109 -// switch analyzeMode {  
110 -// case 1: // barcode  
111 - if analyzing {  
112 - return  
113 - }  
114 - analyzing = true  
115 - let buffer = CMSampleBufferGetImageBuffer(sampleBuffer)  
116 - let image = VisionImage(image: buffer!.image)  
117 - image.orientation = imageOrientation(  
118 - deviceOrientation: UIDevice.current.orientation,  
119 - defaultOrientation: .portrait  
120 - )  
121 -  
122 - scanner.process(image) { [self] barcodes, error in  
123 - if error == nil && barcodes != nil {  
124 - for barcode in barcodes! {  
125 -  
126 - var event: [String: Any?] = ["name": "barcode", "data": barcode.data]  
127 - if (returnImage && latestBuffer != nil) {  
128 - let image: CIImage = CIImage(cvPixelBuffer: latestBuffer)  
129 -  
130 - event["image"] = FlutterStandardTypedData(bytes: ciImageToJpeg(ciImage: image))  
131 - }  
132 - sink?(event)  
133 - }  
134 - }  
135 - analyzing = false  
136 - }  
137 -// default: // none  
138 -// break  
139 -// }  
140 - }  
141 -  
142 - func imageOrientation(  
143 - deviceOrientation: UIDeviceOrientation,  
144 - defaultOrientation: UIDeviceOrientation  
145 - ) -> UIImage.Orientation {  
146 - switch deviceOrientation {  
147 - case .portrait:  
148 - return position == .front ? .leftMirrored : .right  
149 - case .landscapeLeft:  
150 - return position == .front ? .downMirrored : .up  
151 - case .portraitUpsideDown:  
152 - return position == .front ? .rightMirrored : .left  
153 - case .landscapeRight:  
154 - return position == .front ? .upMirrored : .down  
155 - case .faceDown, .faceUp, .unknown:  
156 - return .up  
157 - @unknown default:  
158 - return imageOrientation(deviceOrientation: defaultOrientation, defaultOrientation: .portrait)  
159 - }  
160 - }  
161 -  
162 - func checkPermission(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {  
163 - let status = AVCaptureDevice.authorizationStatus(for: .video)  
164 - switch status {  
165 - case .notDetermined:  
166 - result(0)  
167 - case .authorized:  
168 - result(1)  
169 - default:  
170 - result(2)  
171 - }  
172 - }  
173 -  
174 - func requestPermission(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {  
175 - AVCaptureDevice.requestAccess(for: .video, completionHandler: { result($0) })  
176 - }  
177 -  
178 - func start(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {  
179 - if (device != nil) {  
180 - result(FlutterError(code: "MobileScanner",  
181 - message: "Called start() while already started!",  
182 - details: nil))  
183 - return  
184 - } 57 + /// Parses all parameters and starts the mobileScanner
  58 + private func start(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
  59 + // let ratio: Int = (call.arguments as! Dictionary<String, Any?>)["ratio"] as! Int
  60 + let torch: Bool = (call.arguments as! Dictionary<String, Any?>)["torch"] as? Bool ?? false
  61 + let facing: Int = (call.arguments as! Dictionary<String, Any?>)["facing"] as? Int ?? 1
  62 + let formats: Array<Int> = (call.arguments as! Dictionary<String, Any?>)["formats"] as? Array ?? []
  63 + let returnImage: Bool = (call.arguments as! Dictionary<String, Any?>)["returnImage"] as? Bool ?? false
185 64
186 - textureId = registry.register(self)  
187 - captureSession = AVCaptureSession() 65 + let formatList = formats.map { format in return BarcodeFormat(rawValue: format)}
  66 + var barcodeOptions: BarcodeScannerOptions? = nil
188 67
189 - let argReader = MapArgumentReader(call.arguments as? [String: Any])  
190 -  
191 - returnImage = argReader.bool(key: "returnImage") ?? false  
192 -  
193 -// let ratio: Int = argReader.int(key: "ratio")  
194 - let torch: Bool = argReader.bool(key: "torch") ?? false  
195 - let facing: Int = argReader.int(key: "facing") ?? 1  
196 - let formats: Array = argReader.intArray(key: "formats") ?? []  
197 -  
198 - if (formats.count != 0) { 68 + if (formatList.count != 0) {
199 var barcodeFormats: BarcodeFormat = [] 69 var barcodeFormats: BarcodeFormat = []
200 for index in formats { 70 for index in formats {
201 barcodeFormats.insert(BarcodeFormat(rawValue: index)) 71 barcodeFormats.insert(BarcodeFormat(rawValue: index))
202 } 72 }
203 -  
204 - let barcodeOptions = BarcodeScannerOptions(formats: barcodeFormats)  
205 - scanner = BarcodeScanner.barcodeScanner(options: barcodeOptions) 73 + barcodeOptions = BarcodeScannerOptions(formats: barcodeFormats)
206 } 74 }
207 75
208 - // Set the camera to use  
209 - position = facing == 0 ? AVCaptureDevice.Position.front : .back  
210 76
211 - // Open the camera device  
212 - if #available(iOS 10.0, *) {  
213 - device = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: position).devices.first  
214 - } else {  
215 - device = AVCaptureDevice.devices(for: .video).filter({$0.position == position}).first  
216 - } 77 + let position = facing == 0 ? AVCaptureDevice.Position.front : .back
  78 + let speed: DetectionSpeed = DetectionSpeed(rawValue: (call.arguments as! Dictionary<String, Any?>)["speed"] as? Int ?? 0)!
217 79
218 - if (device == nil) { 80 + do {
  81 + let parameters = try mobileScanner.start(barcodeScannerOptions: barcodeOptions, returnImage: returnImage, cameraPosition: position, torch: torch ? .on : .off, detectionSpeed: speed)
  82 + result(["textureId": parameters.textureId, "size": ["width": parameters.width, "height": parameters.height], "torchable": parameters.hasTorch])
  83 + } catch MobileScannerError.alreadyStarted {
  84 + result(FlutterError(code: "MobileScanner",
  85 + message: "Called start() while already started!",
  86 + details: nil))
  87 + } catch MobileScannerError.noCamera {
219 result(FlutterError(code: "MobileScanner", 88 result(FlutterError(code: "MobileScanner",
220 message: "No camera found or failed to open camera!", 89 message: "No camera found or failed to open camera!",
221 details: nil)) 90 details: nil))
222 - return  
223 - }  
224 -  
225 - // Enable the torch if parameter is set and torch is available  
226 - if (device.hasTorch && device.isTorchAvailable) {  
227 - do {  
228 - try device.lockForConfiguration()  
229 - device.torchMode = torch ? .on : .off  
230 - device.unlockForConfiguration() 91 + } catch MobileScannerError.torchError(let error) {
  92 + result(FlutterError(code: "MobileScanner",
  93 + message: "Error occured when setting toch!",
  94 + details: error))
  95 + } catch MobileScannerError.cameraError(let error) {
  96 + result(FlutterError(code: "MobileScanner",
  97 + message: "Error occured when setting up camera!",
  98 + details: error))
231 } catch { 99 } catch {
232 - error.throwNative(result) 100 + result(FlutterError(code: "MobileScanner",
  101 + message: "Unknown error occured..",
  102 + details: nil))
233 } 103 }
234 } 104 }
235 105
236 - device.addObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode), options: .new, context: nil)  
237 - captureSession.beginConfiguration()  
238 -  
239 - // Add device input 106 + /// Stops the mobileScanner and closes the texture
  107 + private func stop(_ result: @escaping FlutterResult) {
240 do { 108 do {
241 - let input = try AVCaptureDeviceInput(device: device)  
242 - captureSession.addInput(input) 109 + try mobileScanner.stop()
243 } catch { 110 } catch {
244 - error.throwNative(result)  
245 - }  
246 - captureSession.sessionPreset = AVCaptureSession.Preset.photo;  
247 - // Add video output.  
248 - let videoOutput = AVCaptureVideoDataOutput()  
249 -  
250 - videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA]  
251 - videoOutput.alwaysDiscardsLateVideoFrames = true  
252 -  
253 - videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue.main)  
254 - captureSession.addOutput(videoOutput)  
255 - for connection in videoOutput.connections {  
256 - connection.videoOrientation = .portrait  
257 - if position == .front && connection.isVideoMirroringSupported {  
258 - connection.isVideoMirrored = true  
259 - } 111 + result(FlutterError(code: "MobileScanner",
  112 + message: "Called stop() while already stopped!",
  113 + details: nil))
260 } 114 }
261 - captureSession.commitConfiguration()  
262 - captureSession.startRunning()  
263 - let demensions = CMVideoFormatDescriptionGetDimensions(device.activeFormat.formatDescription)  
264 - let width = Double(demensions.height)  
265 - let height = Double(demensions.width)  
266 - let size = ["width": width, "height": height]  
267 - let answer: [String : Any?] = ["textureId": textureId, "size": size, "torchable": device.hasTorch]  
268 - result(answer) 115 + result(nil)
269 } 116 }
270 117
271 - func toggleTorch(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {  
272 - if (device == nil) { 118 + /// Toggles the torch
  119 + private func toggleTorch(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
  120 + do {
  121 + try mobileScanner.toggleTorch(call.arguments as? Int == 1 ? .on : .off)
  122 + } catch {
273 result(FlutterError(code: "MobileScanner", 123 result(FlutterError(code: "MobileScanner",
274 message: "Called toggleTorch() while stopped!", 124 message: "Called toggleTorch() while stopped!",
275 details: nil)) 125 details: nil))
276 - return  
277 } 126 }
278 - do {  
279 - try device.lockForConfiguration()  
280 - device.torchMode = call.arguments as! Int == 1 ? .on : .off  
281 - device.unlockForConfiguration()  
282 result(nil) 127 result(nil)
283 - } catch {  
284 - error.throwNative(result)  
285 - }  
286 } 128 }
287 129
288 -// func switchAnalyzeMode(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {  
289 -// analyzeMode = call.arguments as! Int  
290 -// result(nil)  
291 -// }  
292 -  
293 - func analyzeImage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {  
294 - let uiImage = UIImage(contentsOfFile: call.arguments as! String) 130 + /// Analyzes a single image
  131 + private func analyzeImage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
  132 + let uiImage = UIImage(contentsOfFile: call.arguments as? String ?? "")
295 133
296 if (uiImage == nil) { 134 if (uiImage == nil) {
297 result(FlutterError(code: "MobileScanner", 135 result(FlutterError(code: "MobileScanner",
@@ -299,99 +137,34 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan @@ -299,99 +137,34 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan
299 details: nil)) 137 details: nil))
300 return 138 return
301 } 139 }
302 -  
303 - let image = VisionImage(image: uiImage!)  
304 - image.orientation = imageOrientation(  
305 - deviceOrientation: UIDevice.current.orientation,  
306 - defaultOrientation: .portrait  
307 - )  
308 -  
309 - var barcodeFound = false  
310 -  
311 - scanner.process(image) { [self] barcodes, error in 140 + mobileScanner.analyzeImage(image: uiImage!, position: AVCaptureDevice.Position.back, callback: { [self] barcodes, error in
312 if error == nil && barcodes != nil { 141 if error == nil && barcodes != nil {
313 for barcode in barcodes! { 142 for barcode in barcodes! {
314 - barcodeFound = true  
315 let event: [String: Any?] = ["name": "barcode", "data": barcode.data] 143 let event: [String: Any?] = ["name": "barcode", "data": barcode.data]
316 - sink?(event) 144 + barcodeHandler.publishEvent(event)
317 } 145 }
318 } else if error != nil { 146 } else if error != nil {
319 - result(FlutterError(code: "MobileScanner",  
320 - message: error?.localizedDescription,  
321 - details: "analyzeImage()"))  
322 - }  
323 - analyzing = false  
324 - result(barcodeFound)  
325 - }  
326 -  
327 - }  
328 -  
329 - func stop(_ result: FlutterResult) {  
330 - if (device == nil) {  
331 - result(FlutterError(code: "MobileScanner",  
332 - message: "Called stop() while already stopped!",  
333 - details: nil))  
334 - return  
335 - }  
336 - captureSession.stopRunning()  
337 - for input in captureSession.inputs {  
338 - captureSession.removeInput(input)  
339 - }  
340 - for output in captureSession.outputs {  
341 - captureSession.removeOutput(output) 147 + barcodeHandler.publishEvent(["name": "error", "message": error?.localizedDescription])
342 } 148 }
343 - device.removeObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode))  
344 - registry.unregisterTexture(textureId)  
345 -  
346 -// analyzeMode = 0  
347 - latestBuffer = nil  
348 - captureSession = nil  
349 - device = nil  
350 - textureId = nil  
351 - 149 + })
352 result(nil) 150 result(nil)
353 } 151 }
354 152
355 - // Observer for torch state 153 + /// Observer for torch state
356 public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { 154 public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
357 switch keyPath { 155 switch keyPath {
358 case "torchMode": 156 case "torchMode":
359 // off = 0; on = 1; auto = 2; 157 // off = 0; on = 1; auto = 2;
360 let state = change?[.newKey] as? Int 158 let state = change?[.newKey] as? Int
361 - let event: [String: Any?] = ["name": "torchState", "data": state]  
362 - sink?(event) 159 + barcodeHandler.publishEvent(["name": "torchState", "data": state])
363 default: 160 default:
364 break 161 break
365 } 162 }
366 } 163 }
367 } 164 }
368 165
369 -class MapArgumentReader {  
370 -  
371 - let args: [String: Any]?  
372 -  
373 - init(_ args: [String: Any]?) {  
374 - self.args = args  
375 - }  
376 -  
377 - func string(key: String) -> String? {  
378 - return args?[key] as? String  
379 - }  
380 -  
381 - func int(key: String) -> Int? {  
382 - return (args?[key] as? NSNumber)?.intValue  
383 - }  
384 -  
385 - func bool(key: String) -> Bool? {  
386 - return (args?[key] as? NSNumber)?.boolValue  
387 - }  
388 -  
389 - func stringArray(key: String) -> [String]? {  
390 - return args?[key] as? [String]  
391 - }  
392 -  
393 - func intArray(key: String) -> [Int]? {  
394 - return args?[key] as? [Int]  
395 - }  
396 - 166 +enum DetectionSpeed: Int {
  167 + case noDuplicates = 0
  168 + case normal = 1
  169 + case unrestricted = 2
397 } 170 }
@@ -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.
@@ -16,7 +16,7 @@ An universal scanner for Flutter based on MLKit. @@ -16,7 +16,7 @@ An universal scanner for Flutter based on MLKit.
16 s.source_files = 'Classes/**/*' 16 s.source_files = 'Classes/**/*'
17 s.dependency 'Flutter' 17 s.dependency 'Flutter'
18 s.dependency 'GoogleMLKit/BarcodeScanning', '~> 3.2.0' 18 s.dependency 'GoogleMLKit/BarcodeScanning', '~> 3.2.0'
19 - s.platform = :ios, '10.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/barcode.dart';
  4 +export 'src/barcode_capture.dart';
  5 +export 'src/enums/camera_facing.dart';
  6 +export 'src/enums/detection_speed.dart';
  7 +export 'src/enums/mobile_scanner_state.dart';
  8 +export 'src/enums/ratio.dart';
  9 +export 'src/enums/torch_state.dart';
3 export 'src/mobile_scanner.dart'; 10 export 'src/mobile_scanner.dart';
4 export 'src/mobile_scanner_arguments.dart'; 11 export 'src/mobile_scanner_arguments.dart';
5 export 'src/mobile_scanner_controller.dart'; 12 export 'src/mobile_scanner_controller.dart';
6 -export 'src/objects/barcode.dart';  
@@ -5,7 +5,7 @@ import 'dart:ui' as ui; @@ -5,7 +5,7 @@ import 'dart:ui' as ui;
5 import 'package:flutter/material.dart'; 5 import 'package:flutter/material.dart';
6 import 'package:flutter/services.dart'; 6 import 'package:flutter/services.dart';
7 import 'package:flutter_web_plugins/flutter_web_plugins.dart'; 7 import 'package:flutter_web_plugins/flutter_web_plugins.dart';
8 -import 'package:mobile_scanner/mobile_scanner.dart'; 8 +import 'package:mobile_scanner/src/enums/camera_facing.dart';
9 import 'package:mobile_scanner/src/web/jsqr.dart'; 9 import 'package:mobile_scanner/src/web/jsqr.dart';
10 import 'package:mobile_scanner/src/web/media.dart'; 10 import 'package:mobile_scanner/src/web/media.dart';
11 11
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 {
@@ -97,7 +97,7 @@ class Barcode { @@ -97,7 +97,7 @@ class Barcode {
97 }); 97 });
98 98
99 /// Create a [Barcode] from native data. 99 /// Create a [Barcode] from native data.
100 - Barcode.fromNative(Map data, this.image) 100 + Barcode.fromNative(Map data, {this.image})
101 : corners = toCorners(data['corners'] as List?), 101 : corners = toCorners(data['corners'] as List?),
102 format = toFormat(data['format'] as int), 102 format = toFormat(data['format'] as int),
103 rawBytes = data['rawBytes'] as Uint8List?, 103 rawBytes = data['rawBytes'] as Uint8List?,
  1 +import 'dart:typed_data';
  2 +
  3 +import 'package:mobile_scanner/src/barcode.dart';
  4 +
  5 +class BarcodeCapture {
  6 + List<Barcode> barcodes;
  7 + Uint8List? image;
  8 +
  9 + BarcodeCapture({
  10 + required this.barcodes,
  11 + this.image,
  12 + });
  13 +}
  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 + noDuplicates,
  6 +
  7 + /// Front facing camera.
  8 + normal,
  9 +
  10 + /// Back facing camera.
  11 + unrestricted,
  12 +}
  1 +enum MobileScannerState { undetermined, authorized, denied }
  1 +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 'package:flutter/foundation.dart'; 1 import 'package:flutter/foundation.dart';
2 import 'package:flutter/material.dart'; 2 import 'package:flutter/material.dart';
3 -import 'package:mobile_scanner/mobile_scanner.dart';  
4 -  
5 -enum Ratio { ratio_4_3, ratio_16_9 } 3 +import 'package:mobile_scanner/src/barcode_capture.dart';
  4 +import 'package:mobile_scanner/src/mobile_scanner_arguments.dart';
  5 +import 'package:mobile_scanner/src/mobile_scanner_controller.dart';
6 6
7 /// A widget showing a live camera preview. 7 /// A widget showing a live camera preview.
8 class MobileScanner extends StatefulWidget { 8 class MobileScanner extends StatefulWidget {
@@ -15,28 +15,23 @@ class MobileScanner extends StatefulWidget { @@ -15,28 +15,23 @@ class MobileScanner extends StatefulWidget {
15 /// Function that gets called when a Barcode is detected. 15 /// Function that gets called when a Barcode is detected.
16 /// 16 ///
17 /// [barcode] The barcode object with all information about the scanned code. 17 /// [barcode] The barcode object with all information about the scanned code.
18 - /// [args] Information about the state of the MobileScanner widget  
19 - final Function(Barcode barcode, MobileScannerArguments? args) onDetect;  
20 -  
21 - /// TODO: Function that gets called when the Widget is initialized. Can be usefull  
22 - /// to check wether the device has a torch(flash) or not.  
23 - ///  
24 - /// [args] Information about the state of the MobileScanner widget  
25 - // final Function(MobileScannerArguments args)? onInitialize; 18 + /// [startArguments] Information about the state of the MobileScanner widget
  19 + final Function(BarcodeCapture capture, MobileScannerArguments? arguments)
  20 + onDetect;
26 21
27 /// Handles how the widget should fit the screen. 22 /// Handles how the widget should fit the screen.
28 final BoxFit fit; 23 final BoxFit fit;
29 24
30 - /// Set to false if you don't want duplicate scans.  
31 - final bool allowDuplicates; 25 + /// Whether to automatically resume the camera when the application is resumed
  26 + final bool autoResume;
32 27
33 /// Create a [MobileScanner] with a [controller], the [controller] must has been initialized. 28 /// Create a [MobileScanner] with a [controller], the [controller] must has been initialized.
34 const MobileScanner({ 29 const MobileScanner({
35 super.key, 30 super.key,
36 required this.onDetect, 31 required this.onDetect,
37 this.controller, 32 this.controller,
  33 + this.autoResume = true,
38 this.fit = BoxFit.cover, 34 this.fit = BoxFit.cover,
39 - this.allowDuplicates = false,  
40 this.onPermissionSet, 35 this.onPermissionSet,
41 }); 36 });
42 37
@@ -57,40 +52,37 @@ class _MobileScannerState extends State<MobileScanner> @@ -57,40 +52,37 @@ class _MobileScannerState extends State<MobileScanner>
57 if (!controller.isStarting) controller.start(); 52 if (!controller.isStarting) controller.start();
58 } 53 }
59 54
  55 + AppLifecycleState? _lastState;
  56 +
60 @override 57 @override
61 void didChangeAppLifecycleState(AppLifecycleState state) { 58 void didChangeAppLifecycleState(AppLifecycleState state) {
62 switch (state) { 59 switch (state) {
63 case AppLifecycleState.resumed: 60 case AppLifecycleState.resumed:
64 - if (!controller.isStarting && controller.autoResume) controller.start(); 61 + if (!controller.isStarting &&
  62 + widget.autoResume &&
  63 + _lastState != AppLifecycleState.inactive) controller.start();
65 break; 64 break;
66 - case AppLifecycleState.inactive:  
67 case AppLifecycleState.paused: 65 case AppLifecycleState.paused:
68 case AppLifecycleState.detached: 66 case AppLifecycleState.detached:
69 controller.stop(); 67 controller.stop();
70 break; 68 break;
  69 + default:
  70 + break;
71 } 71 }
  72 + _lastState = state;
72 } 73 }
73 74
74 - Uint8List? lastScanned;  
75 -  
76 @override 75 @override
77 Widget build(BuildContext context) { 76 Widget build(BuildContext context) {
78 return ValueListenableBuilder( 77 return ValueListenableBuilder(
79 - valueListenable: controller.args, 78 + valueListenable: controller.startArguments,
80 builder: (context, value, child) { 79 builder: (context, value, child) {
81 value = value as MobileScannerArguments?; 80 value = value as MobileScannerArguments?;
82 if (value == null) { 81 if (value == null) {
83 return const ColoredBox(color: Colors.black); 82 return const ColoredBox(color: Colors.black);
84 } else { 83 } else {
85 controller.barcodes.listen((barcode) { 84 controller.barcodes.listen((barcode) {
86 - if (!widget.allowDuplicates) {  
87 - if (lastScanned != barcode.rawBytes) {  
88 - lastScanned = barcode.rawBytes;  
89 widget.onDetect(barcode, value! as MobileScannerArguments); 85 widget.onDetect(barcode, value! as MobileScannerArguments);
90 - }  
91 - } else {  
92 - widget.onDetect(barcode, value! as MobileScannerArguments);  
93 - }  
94 }); 86 });
95 return ClipRect( 87 return ClipRect(
96 child: SizedBox( 88 child: SizedBox(
@@ -5,192 +5,155 @@ import 'package:flutter/cupertino.dart'; @@ -5,192 +5,155 @@ 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 +import 'package:mobile_scanner/src/mobile_scanner_exception.dart';
9 10
10 -/// The facing of a camera.  
11 -enum CameraFacing {  
12 - /// Front facing camera.  
13 - front, 11 +/// The [MobileScannerController] holds all the logic of this plugin,
  12 +/// where as the [MobileScanner] class is the frontend of this plugin.
  13 +class MobileScannerController {
  14 + MobileScannerController({
  15 + this.facing = CameraFacing.back,
  16 + this.detectionSpeed = DetectionSpeed.noDuplicates,
  17 + // this.ratio,
  18 + this.torchEnabled = false,
  19 + this.formats,
  20 + // this.autoResume = true,
  21 + this.returnImage = false,
  22 + this.onPermissionSet,
  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 + }
14 33
15 - /// Back facing camera.  
16 - back,  
17 -} 34 + //Must be static to keep the same value on new instances
  35 + static int? controllerHashcode;
18 36
19 -enum MobileScannerState { undetermined, authorized, denied } 37 + /// Select which camera should be used.
  38 + ///
  39 + /// Default: CameraFacing.back
  40 + final CameraFacing facing;
20 41
21 -/// The state of torch.  
22 -enum TorchState {  
23 - /// Torch is off.  
24 - off, 42 + // /// Analyze the image in 4:3 or 16:9
  43 + // ///
  44 + // /// Only on Android
  45 + // final Ratio? ratio;
25 46
26 - /// Torch is on.  
27 - on,  
28 -} 47 + /// Enable or disable the torch (Flash) on start
  48 + ///
  49 + /// Default: disabled
  50 + final bool torchEnabled;
29 51
30 -// enum AnalyzeMode { none, barcode } 52 + /// Set to true if you want to return the image buffer with the Barcode event
  53 + ///
  54 + /// Only supported on iOS and Android
  55 + final bool returnImage;
31 56
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'); 57 + /// If provided, the scanner will only detect those specific formats
  58 + final List<BarcodeFormat>? formats;
37 59
38 - //Must be static to keep the same value on new instances  
39 - static int? _controllerHashcode;  
40 - StreamSubscription? events; 60 + /// Sets the speed of detections.
  61 + ///
  62 + /// WARNING: DetectionSpeed.unrestricted can cause memory issues on some devices
  63 + final DetectionSpeed detectionSpeed;
  64 +
  65 + /// Sets the barcode stream
  66 + final StreamController<BarcodeCapture> _barcodesController =
  67 + StreamController.broadcast();
  68 + Stream<BarcodeCapture> get barcodes => _barcodesController.stream;
  69 +
  70 + static const MethodChannel _methodChannel =
  71 + MethodChannel('dev.steenbakker.mobile_scanner/scanner/method');
  72 + static const EventChannel _eventChannel =
  73 + EventChannel('dev.steenbakker.mobile_scanner/scanner/event');
41 74
42 Function(bool permissionGranted)? onPermissionSet; 75 Function(bool permissionGranted)? onPermissionSet;
43 - final ValueNotifier<MobileScannerArguments?> args = ValueNotifier(null);  
44 - final ValueNotifier<TorchState> torchState = ValueNotifier(TorchState.off);  
45 - late final ValueNotifier<CameraFacing> cameraFacingState;  
46 - final Ratio? ratio;  
47 - final bool? torchEnabled;  
48 - // Whether to return the image buffer with the Barcode event  
49 - final bool returnImage;  
50 76
51 - /// If provided, the scanner will only detect those specific formats.  
52 - final List<BarcodeFormat>? formats; 77 + /// Listen to events from the platform specific code
  78 + late StreamSubscription events;
53 79
54 - CameraFacing facing;  
55 - bool hasTorch = false;  
56 - late StreamController<Barcode> barcodesController; 80 + /// A notifier that provides several arguments about the MobileScanner
  81 + final ValueNotifier<MobileScannerArguments?> startArguments =
  82 + ValueNotifier(null);
57 83
58 - /// Whether to automatically resume the camera when the application is resumed  
59 - bool autoResume; 84 + /// A notifier that provides the state of the Torch (Flash)
  85 + final ValueNotifier<TorchState> torchState = ValueNotifier(TorchState.off);
60 86
61 - Stream<Barcode> get barcodes => barcodesController.stream; 87 + /// A notifier that provides the state of which camera is being used
  88 + late final ValueNotifier<CameraFacing> cameraFacingState =
  89 + ValueNotifier(facing);
62 90
63 - MobileScannerController({  
64 - this.facing = CameraFacing.back,  
65 - this.ratio,  
66 - this.torchEnabled,  
67 - this.formats,  
68 - this.onPermissionSet,  
69 - this.autoResume = true,  
70 - this.returnImage = false,  
71 - }) {  
72 - // In case a new instance is created before calling dispose()  
73 - if (_controllerHashcode != null) {  
74 - stop();  
75 - }  
76 - _controllerHashcode = hashCode; 91 + bool isStarting = false;
  92 + bool? _hasTorch;
77 93
78 - cameraFacingState = ValueNotifier(facing); 94 + /// Set the starting arguments for the camera
  95 + Map<String, dynamic> _argumentsToMap({CameraFacing? cameraFacingOverride}) {
  96 + final Map<String, dynamic> arguments = {};
79 97
80 - // Sets analyze mode and barcode stream  
81 - barcodesController = StreamController.broadcast(  
82 - // onListen: () => setAnalyzeMode(AnalyzeMode.barcode.index),  
83 - // onCancel: () => setAnalyzeMode(AnalyzeMode.none.index),  
84 - ); 98 + cameraFacingState.value = cameraFacingOverride ?? facing;
  99 + arguments['facing'] = cameraFacingState.value.index;
85 100
86 - // Listen to events from the platform specific code  
87 - events = eventChannel  
88 - .receiveBroadcastStream()  
89 - .listen((data) => handleEvent(data as Map));  
90 - } 101 + // if (ratio != null) arguments['ratio'] = ratio;
  102 + arguments['torch'] = torchEnabled;
  103 + arguments['speed'] = detectionSpeed.index;
91 104
92 - void handleEvent(Map event) {  
93 - final name = event['name'];  
94 - final data = event['data'];  
95 - final binaryData = event['binaryData'];  
96 - switch (name) {  
97 - case 'torchState':  
98 - final state = TorchState.values[data as int? ?? 0];  
99 - torchState.value = state;  
100 - break;  
101 - case 'barcode':  
102 - final image = returnImage ? event['image'] as Uint8List : null;  
103 - final barcode = Barcode.fromNative(data as Map? ?? {}, image);  
104 - barcodesController.add(barcode);  
105 - break;  
106 - case 'barcodeMac':  
107 - barcodesController.add(  
108 - Barcode(  
109 - rawValue: (data as Map)['payload'] as String?,  
110 - ),  
111 - );  
112 - break;  
113 - case 'barcodeWeb':  
114 - final bytes = (binaryData as List).cast<int>();  
115 - barcodesController.add(  
116 - Barcode(  
117 - rawValue: data as String?,  
118 - rawBytes: Uint8List.fromList(bytes),  
119 - ),  
120 - );  
121 - break;  
122 - default:  
123 - throw UnimplementedError(); 105 + if (formats != null) {
  106 + if (Platform.isAndroid) {
  107 + arguments['formats'] = formats!.map((e) => e.index).toList();
  108 + } else if (Platform.isIOS || Platform.isMacOS) {
  109 + arguments['formats'] = formats!.map((e) => e.rawValue).toList();
124 } 110 }
125 } 111 }
126 -  
127 - // TODO: Add more analyzers like text analyzer  
128 - // void setAnalyzeMode(int mode) {  
129 - // if (hashCode != _controllerHashcode) {  
130 - // return;  
131 - // }  
132 - // methodChannel.invokeMethod('analyze', mode);  
133 - // }  
134 -  
135 - // List<BarcodeFormats>? formats = _defaultBarcodeFormats,  
136 - bool isStarting = false; 112 + arguments['returnImage'] = true;
  113 + return arguments;
  114 + }
137 115
138 /// Start barcode scanning. This will first check if the required permissions 116 /// Start barcode scanning. This will first check if the required permissions
139 /// are set. 117 /// are set.
140 - Future<void> start() async {  
141 - ensure('startAsync'); 118 + Future<MobileScannerArguments?> start({
  119 + CameraFacing? cameraFacingOverride,
  120 + }) async {
  121 + debugPrint('Hashcode controller: $hashCode');
142 if (isStarting) { 122 if (isStarting) {
143 - throw Exception('mobile_scanner: Called start() while already starting.'); 123 + debugPrint("Called start() while starting.");
144 } 124 }
145 isStarting = true; 125 isStarting = true;
146 - // setAnalyzeMode(AnalyzeMode.barcode.index);  
147 126
148 // Check authorization status 127 // Check authorization status
149 if (!kIsWeb) { 128 if (!kIsWeb) {
150 - MobileScannerState state = MobileScannerState  
151 - .values[await methodChannel.invokeMethod('state') as int? ?? 0]; 129 + final MobileScannerState state = MobileScannerState
  130 + .values[await _methodChannel.invokeMethod('state') as int? ?? 0];
152 switch (state) { 131 switch (state) {
153 case MobileScannerState.undetermined: 132 case MobileScannerState.undetermined:
154 final bool result = 133 final bool result =
155 - await methodChannel.invokeMethod('request') as bool? ?? false;  
156 - state = result  
157 - ? MobileScannerState.authorized  
158 - : MobileScannerState.denied; 134 + await _methodChannel.invokeMethod('request') as bool? ?? false;
  135 + if (!result) {
  136 + isStarting = false;
159 onPermissionSet?.call(result); 137 onPermissionSet?.call(result);
  138 + throw MobileScannerException('User declined camera permission.');
  139 + }
160 break; 140 break;
161 case MobileScannerState.denied: 141 case MobileScannerState.denied:
162 isStarting = false; 142 isStarting = false;
163 onPermissionSet?.call(false); 143 onPermissionSet?.call(false);
164 - throw PlatformException(code: 'NO ACCESS'); 144 + throw MobileScannerException('User declined camera permission.');
165 case MobileScannerState.authorized: 145 case MobileScannerState.authorized:
166 onPermissionSet?.call(true); 146 onPermissionSet?.call(true);
167 break; 147 break;
168 } 148 }
169 } 149 }
170 150
171 - cameraFacingState.value = facing;  
172 -  
173 - // Set the starting arguments for the camera  
174 - final Map arguments = {};  
175 - arguments['facing'] = facing.index;  
176 - if (ratio != null) arguments['ratio'] = ratio;  
177 - if (torchEnabled != null) arguments['torch'] = torchEnabled;  
178 -  
179 - if (formats != null) {  
180 - if (Platform.isAndroid) {  
181 - arguments['formats'] = formats!.map((e) => e.index).toList();  
182 - } else if (Platform.isIOS || Platform.isMacOS) {  
183 - arguments['formats'] = formats!.map((e) => e.rawValue).toList();  
184 - }  
185 - }  
186 - arguments['returnImage'] = returnImage;  
187 -  
188 // Start the camera with arguments 151 // Start the camera with arguments
189 Map<String, dynamic>? startResult = {}; 152 Map<String, dynamic>? startResult = {};
190 try { 153 try {
191 - startResult = await methodChannel.invokeMapMethod<String, dynamic>( 154 + startResult = await _methodChannel.invokeMapMethod<String, dynamic>(
192 'start', 155 'start',
193 - arguments, 156 + _argumentsToMap(cameraFacingOverride: cameraFacingOverride),
194 ); 157 );
195 } on PlatformException catch (error) { 158 } on PlatformException catch (error) {
196 debugPrint('${error.code}: ${error.message}'); 159 debugPrint('${error.code}: ${error.message}');
@@ -198,85 +161,78 @@ class MobileScannerController { @@ -198,85 +161,78 @@ class MobileScannerController {
198 if (error.code == "MobileScannerWeb") { 161 if (error.code == "MobileScannerWeb") {
199 onPermissionSet?.call(false); 162 onPermissionSet?.call(false);
200 } 163 }
201 - // setAnalyzeMode(AnalyzeMode.none.index);  
202 - return; 164 + return null;
203 } 165 }
204 166
205 if (startResult == null) { 167 if (startResult == null) {
206 isStarting = false; 168 isStarting = false;
207 - throw PlatformException(code: 'INITIALIZATION ERROR'); 169 + throw MobileScannerException(
  170 + 'Failed to start mobileScanner, no response from platform side',
  171 + );
208 } 172 }
209 173
210 - hasTorch = startResult['torchable'] as bool? ?? false; 174 + _hasTorch = startResult['torchable'] as bool? ?? false;
  175 + if (_hasTorch! && torchEnabled) {
  176 + torchState.value = TorchState.on;
  177 + }
211 178
212 if (kIsWeb) { 179 if (kIsWeb) {
213 onPermissionSet?.call( 180 onPermissionSet?.call(
214 true, 181 true,
215 ); // If we reach this line, it means camera permission has been granted 182 ); // If we reach this line, it means camera permission has been granted
216 183
217 - args.value = MobileScannerArguments( 184 + startArguments.value = MobileScannerArguments(
218 webId: startResult['ViewID'] as String?, 185 webId: startResult['ViewID'] as String?,
219 size: Size( 186 size: Size(
220 startResult['videoWidth'] as double? ?? 0, 187 startResult['videoWidth'] as double? ?? 0,
221 startResult['videoHeight'] as double? ?? 0, 188 startResult['videoHeight'] as double? ?? 0,
222 ), 189 ),
223 - hasTorch: hasTorch, 190 + hasTorch: _hasTorch!,
224 ); 191 );
225 } else { 192 } else {
226 - args.value = MobileScannerArguments( 193 + startArguments.value = MobileScannerArguments(
227 textureId: startResult['textureId'] as int?, 194 textureId: startResult['textureId'] as int?,
228 size: toSize(startResult['size'] as Map? ?? {}), 195 size: toSize(startResult['size'] as Map? ?? {}),
229 - hasTorch: hasTorch, 196 + hasTorch: _hasTorch!,
230 ); 197 );
231 } 198 }
232 -  
233 isStarting = false; 199 isStarting = false;
  200 + return startArguments.value!;
234 } 201 }
235 202
  203 + /// Stops the camera, but does not dispose this controller.
236 Future<void> stop() async { 204 Future<void> stop() async {
237 - try {  
238 - await methodChannel.invokeMethod('stop');  
239 - } on PlatformException catch (error) {  
240 - debugPrint('${error.code}: ${error.message}');  
241 - } 205 + await _methodChannel.invokeMethod('stop');
242 } 206 }
243 207
244 /// Switches the torch on or off. 208 /// Switches the torch on or off.
245 /// 209 ///
246 /// Only works if torch is available. 210 /// Only works if torch is available.
247 Future<void> toggleTorch() async { 211 Future<void> toggleTorch() async {
248 - ensure('toggleTorch');  
249 - if (!hasTorch) {  
250 - debugPrint('Device has no torch/flash.');  
251 - return; 212 + if (_hasTorch == null) {
  213 + throw MobileScannerException(
  214 + 'Cannot toggle torch if start() has never been called',
  215 + );
  216 + } else if (!_hasTorch!) {
  217 + throw MobileScannerException('Device has no torch');
252 } 218 }
253 219
254 - final TorchState state = 220 + torchState.value =
255 torchState.value == TorchState.off ? TorchState.on : TorchState.off; 221 torchState.value == TorchState.off ? TorchState.on : TorchState.off;
256 222
257 - try {  
258 - await methodChannel.invokeMethod('torch', state.index);  
259 - } on PlatformException catch (error) {  
260 - debugPrint('${error.code}: ${error.message}');  
261 - } 223 + await _methodChannel.invokeMethod('torch', torchState.value.index);
262 } 224 }
263 225
264 /// Switches the torch on or off. 226 /// Switches the torch on or off.
265 /// 227 ///
266 /// Only works if torch is available. 228 /// Only works if torch is available.
267 Future<void> switchCamera() async { 229 Future<void> switchCamera() async {
268 - ensure('switchCamera');  
269 - try {  
270 - await methodChannel.invokeMethod('stop');  
271 - } on PlatformException catch (error) {  
272 - debugPrint(  
273 - '${error.code}: camera is stopped! Please start before switching camera.',  
274 - );  
275 - return;  
276 - }  
277 - facing =  
278 - facing == CameraFacing.back ? CameraFacing.front : CameraFacing.back;  
279 - await start(); 230 + await _methodChannel.invokeMethod('stop');
  231 + final CameraFacing facingToUse =
  232 + cameraFacingState.value == CameraFacing.back
  233 + ? CameraFacing.front
  234 + : CameraFacing.back;
  235 + await start(cameraFacingOverride: facingToUse);
280 } 236 }
281 237
282 /// Handles a local image file. 238 /// Handles a local image file.
@@ -285,28 +241,72 @@ class MobileScannerController { @@ -285,28 +241,72 @@ class MobileScannerController {
285 /// 241 ///
286 /// [path] The path of the image on the devices 242 /// [path] The path of the image on the devices
287 Future<bool> analyzeImage(String path) async { 243 Future<bool> analyzeImage(String path) async {
288 - return methodChannel 244 + return _methodChannel
289 .invokeMethod<bool>('analyzeImage', path) 245 .invokeMethod<bool>('analyzeImage', path)
290 .then<bool>((bool? value) => value ?? false); 246 .then<bool>((bool? value) => value ?? false);
291 } 247 }
292 248
293 /// Disposes the MobileScannerController and closes all listeners. 249 /// Disposes the MobileScannerController and closes all listeners.
  250 + ///
  251 + /// If you call this, you cannot use this controller object anymore.
294 void dispose() { 252 void dispose() {
295 - if (hashCode == _controllerHashcode) {  
296 stop(); 253 stop();
297 - events?.cancel();  
298 - events = null;  
299 - _controllerHashcode = null; 254 + events.cancel();
  255 + _barcodesController.close();
  256 + if (hashCode == controllerHashcode) {
  257 + controllerHashcode = null;
300 onPermissionSet = null; 258 onPermissionSet = null;
301 } 259 }
302 - barcodesController.close();  
303 } 260 }
304 261
305 - /// Checks if the MobileScannerController is bound to the correct MobileScanner object.  
306 - void ensure(String name) {  
307 - final message =  
308 - 'MobileScannerController.$name called after MobileScannerController.dispose\n'  
309 - 'MobileScannerController methods should not be used after calling dispose.';  
310 - assert(hashCode == _controllerHashcode, message); 262 + /// Handles a returning event from the platform side
  263 + void _handleEvent(Map event) {
  264 + final name = event['name'];
  265 + final data = event['data'];
  266 +
  267 + switch (name) {
  268 + case 'torchState':
  269 + final state = TorchState.values[data as int? ?? 0];
  270 + torchState.value = state;
  271 + break;
  272 + case 'barcode':
  273 + if (data == null) return;
  274 + final parsed = (data as List)
  275 + .map((value) => Barcode.fromNative(value as Map))
  276 + .toList();
  277 + _barcodesController.add(
  278 + BarcodeCapture(
  279 + barcodes: parsed,
  280 + image: event['image'] as Uint8List,
  281 + ),
  282 + );
  283 + break;
  284 + case 'barcodeMac':
  285 + _barcodesController.add(
  286 + BarcodeCapture(
  287 + barcodes: [
  288 + Barcode(
  289 + rawValue: (data as Map)['payload'] as String?,
  290 + )
  291 + ],
  292 + ),
  293 + );
  294 + break;
  295 + case 'barcodeWeb':
  296 + _barcodesController.add(
  297 + BarcodeCapture(
  298 + barcodes: [
  299 + Barcode(
  300 + rawValue: data as String?,
  301 + )
  302 + ],
  303 + ),
  304 + );
  305 + break;
  306 + case 'error':
  307 + throw MobileScannerException(data as String);
  308 + default:
  309 + throw UnimplementedError(name as String?);
  310 + }
311 } 311 }
312 } 312 }
  1 +class MobileScannerException implements Exception {
  2 + String message;
  3 + MobileScannerException(this.message);
  4 +}
@@ -14,6 +14,7 @@ dependencies: @@ -14,6 +14,7 @@ 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