Julian Steenbakker

refactor: update android code to match ios implementation

@@ -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.20' 5 + ext.kotlin_version = '1.7.21'
6 repositories { 6 repositories {
7 google() 7 google()
8 mavenCentral() 8 mavenCentral()
1 -package dev.steenbakker.mobile_scanner  
2 -  
3 -import androidx.annotation.IntDef  
4 -  
5 -@IntDef(AnalyzeMode.NONE, AnalyzeMode.BARCODE)  
6 -@Target(AnnotationTarget.FIELD)  
7 -@Retention(AnnotationRetention.SOURCE)  
8 -annotation class AnalyzeMode {  
9 - companion object {  
10 - const val NONE = 0  
11 - const val BARCODE = 1  
12 - }  
13 -}  
  1 +package dev.steenbakker.mobile_scanner
  2 +
  3 +import android.os.Handler
  4 +import android.os.Looper
  5 +import io.flutter.embedding.engine.plugins.FlutterPlugin
  6 +import io.flutter.plugin.common.EventChannel
  7 +
  8 +class BarcodeHandler(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) : EventChannel.StreamHandler {
  9 +
  10 + private var eventSink: EventChannel.EventSink? = null
  11 +
  12 + private val eventChannel = EventChannel(
  13 + flutterPluginBinding.binaryMessenger,
  14 + "dev.steenbakker.mobile_scanner/scanner/event"
  15 + )
  16 +
  17 + init {
  18 + eventChannel.setStreamHandler(this)
  19 + }
  20 +
  21 + fun publishEvent(event: Map<String, Any>) {
  22 + Handler(Looper.getMainLooper()).post {
  23 + eventSink?.success(event)
  24 + }
  25 + }
  26 +
  27 + override fun onListen(event: Any?, eventSink: EventChannel.EventSink?) {
  28 + this.eventSink = eventSink
  29 + }
  30 +
  31 + override fun onCancel(event: Any?) {
  32 + this.eventSink = null
  33 + }
  34 +}
@@ -3,16 +3,10 @@ package dev.steenbakker.mobile_scanner @@ -3,16 +3,10 @@ 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  
7 -import android.graphics.Point  
8 -import android.graphics.Rect  
9 -import android.graphics.YuvImage  
10 -import android.media.Image  
11 import android.net.Uri 6 import android.net.Uri
12 -import android.util.Log  
13 -import android.util.Size 7 +import android.os.Handler
  8 +import android.os.Looper
14 import android.view.Surface 9 import android.view.Surface
15 -import androidx.annotation.NonNull  
16 import androidx.camera.core.* 10 import androidx.camera.core.*
17 import androidx.camera.lifecycle.ProcessCameraProvider 11 import androidx.camera.lifecycle.ProcessCameraProvider
18 import androidx.core.app.ActivityCompat 12 import androidx.core.app.ActivityCompat
@@ -20,19 +14,32 @@ import androidx.core.content.ContextCompat @@ -20,19 +14,32 @@ import androidx.core.content.ContextCompat
20 import androidx.lifecycle.LifecycleOwner 14 import androidx.lifecycle.LifecycleOwner
21 import com.google.mlkit.vision.barcode.BarcodeScannerOptions 15 import com.google.mlkit.vision.barcode.BarcodeScannerOptions
22 import com.google.mlkit.vision.barcode.BarcodeScanning 16 import com.google.mlkit.vision.barcode.BarcodeScanning
23 -import com.google.mlkit.vision.barcode.common.Barcode  
24 import com.google.mlkit.vision.common.InputImage 17 import com.google.mlkit.vision.common.InputImage
25 -import io.flutter.plugin.common.EventChannel  
26 -import io.flutter.plugin.common.MethodCall  
27 -import io.flutter.plugin.common.MethodChannel 18 +import dev.steenbakker.mobile_scanner.objects.DetectionSpeed
  19 +import dev.steenbakker.mobile_scanner.objects.MobileScannerStartParameters
28 import io.flutter.plugin.common.PluginRegistry 20 import io.flutter.plugin.common.PluginRegistry
29 import io.flutter.view.TextureRegistry 21 import io.flutter.view.TextureRegistry
30 -import java.io.ByteArrayOutputStream  
31 -import java.io.File  
32 22
33 -  
34 -class MobileScanner(private val activity: Activity, private val textureRegistry: TextureRegistry) :  
35 - MethodChannel.MethodCallHandler, EventChannel.StreamHandler, 23 +typealias PermissionCallback = (permissionGranted: Boolean) -> Unit
  24 +typealias MobileScannerCallback = (barcodes: List<Map<String, Any?>>, image: ByteArray?) -> Unit
  25 +typealias AnalyzerCallback = (barcodes: List<Map<String, Any?>>?) -> Unit
  26 +typealias MobileScannerErrorCallback = (error: String) -> Unit
  27 +typealias TorchStateCallback = (state: Int) -> Unit
  28 +typealias MobileScannerStartedCallback = (parameters: MobileScannerStartParameters) -> Unit
  29 +
  30 +class NoCamera : Exception()
  31 +class AlreadyStarted : Exception()
  32 +class AlreadyStopped : Exception()
  33 +class TorchError : Exception()
  34 +class CameraError : Exception()
  35 +class TorchWhenStopped : Exception()
  36 +
  37 +class MobileScanner(
  38 + private val activity: Activity,
  39 + private val textureRegistry: TextureRegistry,
  40 + private val mobileScannerCallback: MobileScannerCallback,
  41 + private val mobileScannerErrorCallback: MobileScannerErrorCallback
  42 +) :
36 PluginRegistry.RequestPermissionsResultListener { 43 PluginRegistry.RequestPermissionsResultListener {
37 companion object { 44 companion object {
38 /** 45 /**
@@ -40,10 +47,8 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: @@ -40,10 +47,8 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
40 * @see androidx.fragment.app.FragmentActivity.validateRequestPermissionsRequestCode 47 * @see androidx.fragment.app.FragmentActivity.validateRequestPermissionsRequestCode
41 */ 48 */
42 private const val REQUEST_CODE = 0x0786 49 private const val REQUEST_CODE = 0x0786
43 - private val TAG = MobileScanner::class.java.simpleName  
44 } 50 }
45 51
46 - private var sink: EventChannel.EventSink? = null  
47 private var listener: PluginRegistry.RequestPermissionsResultListener? = null 52 private var listener: PluginRegistry.RequestPermissionsResultListener? = null
48 53
49 private var cameraProvider: ProcessCameraProvider? = null 54 private var cameraProvider: ProcessCameraProvider? = null
@@ -51,31 +56,54 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: @@ -51,31 +56,54 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
51 private var preview: Preview? = null 56 private var preview: Preview? = null
52 private var textureEntry: TextureRegistry.SurfaceTextureEntry? = null 57 private var textureEntry: TextureRegistry.SurfaceTextureEntry? = null
53 58
54 -// @AnalyzeMode  
55 -// private var analyzeMode: Int = AnalyzeMode.NONE 59 + private var detectionSpeed: DetectionSpeed = DetectionSpeed.NO_DUPLICATES
  60 + private var detectionTimeout: Long = 250
  61 + private var lastScanned: List<String?>? = null
56 62
57 - @ExperimentalGetImage  
58 - override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: MethodChannel.Result) {  
59 - when (call.method) {  
60 - "state" -> checkPermission(result)  
61 - "request" -> requestPermission(result)  
62 - "start" -> start(call, result)  
63 - "torch" -> toggleTorch(call, result)  
64 -// "analyze" -> switchAnalyzeMode(call, result)  
65 - "stop" -> stop(result)  
66 - "analyzeImage" -> analyzeImage(call, result)  
67 - else -> result.notImplemented()  
68 - }  
69 - } 63 + private var scannerTimeout = false
  64 +
  65 + private var returnImage = false
  66 +
  67 + private var scanner = BarcodeScanning.getClient()
70 68
71 - override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {  
72 - this.sink = events 69 + /**
  70 + * Check if we already have camera permission.
  71 + */
  72 + fun hasCameraPermission(): Int {
  73 + // Can't get exact denied or not_determined state without request. Just return not_determined when state isn't authorized
  74 + val hasPermission = ContextCompat.checkSelfPermission(
  75 + activity,
  76 + Manifest.permission.CAMERA
  77 + ) == PackageManager.PERMISSION_GRANTED
  78 +
  79 + return if (hasPermission) {
  80 + 1
  81 + } else {
  82 + 0
  83 + }
73 } 84 }
74 85
75 - override fun onCancel(arguments: Any?) {  
76 - sink = null 86 + /**
  87 + * Request camera permissions.
  88 + */
  89 + fun requestPermission(permissionCallback: PermissionCallback) {
  90 + listener
  91 + ?: PluginRegistry.RequestPermissionsResultListener { requestCode, _, grantResults ->
  92 + if (requestCode != REQUEST_CODE) {
  93 + false
  94 + } else {
  95 + val authorized = grantResults[0] == PackageManager.PERMISSION_GRANTED
  96 + permissionCallback(authorized)
  97 + true
  98 + }
  99 + }
  100 + val permissions = arrayOf(Manifest.permission.CAMERA)
  101 + ActivityCompat.requestPermissions(activity, permissions, REQUEST_CODE)
77 } 102 }
78 103
  104 + /**
  105 + * Calls the callback after permissions are requested.
  106 + */
79 override fun onRequestPermissionsResult( 107 override fun onRequestPermissionsResult(
80 requestCode: Int, 108 requestCode: Int,
81 permissions: Array<out String>, 109 permissions: Array<out String>,
@@ -84,282 +112,161 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: @@ -84,282 +112,161 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
84 return listener?.onRequestPermissionsResult(requestCode, permissions, grantResults) ?: false 112 return listener?.onRequestPermissionsResult(requestCode, permissions, grantResults) ?: false
85 } 113 }
86 114
87 - private fun checkPermission(result: MethodChannel.Result) {  
88 - // Can't get exact denied or not_determined state without request. Just return not_determined when state isn't authorized  
89 - val state =  
90 - if (ContextCompat.checkSelfPermission(  
91 - activity,  
92 - Manifest.permission.CAMERA  
93 - ) == PackageManager.PERMISSION_GRANTED  
94 - ) 1  
95 - else 0  
96 - result.success(state)  
97 - }  
98 -  
99 - private fun requestPermission(result: MethodChannel.Result) {  
100 - listener = PluginRegistry.RequestPermissionsResultListener { requestCode, _, grantResults ->  
101 - if (requestCode != REQUEST_CODE) {  
102 - false  
103 - } else {  
104 - val authorized = grantResults[0] == PackageManager.PERMISSION_GRANTED  
105 - result.success(authorized)  
106 - listener = null  
107 - true  
108 - }  
109 - }  
110 - val permissions = arrayOf(Manifest.permission.CAMERA)  
111 - ActivityCompat.requestPermissions(activity, permissions, REQUEST_CODE)  
112 - }  
113 -// var lastScanned: List<Barcode>? = null  
114 -// var isAnalyzing: Boolean = false  
115 - 115 + /**
  116 + * callback for the camera. Every frame is passed through this function.
  117 + */
116 @ExperimentalGetImage 118 @ExperimentalGetImage
117 - val analyzer = ImageAnalysis.Analyzer { imageProxy -> // YUV_420_888 format  
118 - 119 + val captureOutput = ImageAnalysis.Analyzer { imageProxy -> // YUV_420_888 format
119 val mediaImage = imageProxy.image ?: return@Analyzer 120 val mediaImage = imageProxy.image ?: return@Analyzer
120 val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees) 121 val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
121 122
  123 + if (detectionSpeed == DetectionSpeed.NORMAL && scannerTimeout) {
  124 + imageProxy.close()
  125 + return@Analyzer
  126 + } else if (detectionSpeed == DetectionSpeed.NORMAL) {
  127 + scannerTimeout = true
  128 + }
  129 +
122 scanner.process(inputImage) 130 scanner.process(inputImage)
123 .addOnSuccessListener { barcodes -> 131 .addOnSuccessListener { barcodes ->
124 -// if (isAnalyzing) {  
125 -// Log.d("scanner", "SKIPPING" )  
126 -// return@addOnSuccessListener  
127 -// }  
128 -// isAnalyzing = true 132 + if (detectionSpeed == DetectionSpeed.NO_DUPLICATES) {
  133 + val newScannedBarcodes = barcodes.map { barcode -> barcode.rawValue }
  134 + if (newScannedBarcodes == lastScanned) {
  135 + // New scanned is duplicate, returning
  136 + return@addOnSuccessListener
  137 + }
  138 + lastScanned = newScannedBarcodes
  139 + }
  140 +
129 val barcodeMap = barcodes.map { barcode -> barcode.data } 141 val barcodeMap = barcodes.map { barcode -> barcode.data }
  142 +
130 if (barcodeMap.isNotEmpty()) { 143 if (barcodeMap.isNotEmpty()) {
131 - sink?.success(mapOf(  
132 - "name" to "barcode",  
133 - "data" to barcodeMap,  
134 - "image" to mediaImage.toByteArray()  
135 - )) 144 + mobileScannerCallback(
  145 + barcodeMap,
  146 + if (returnImage) mediaImage.toByteArray() else null
  147 + )
136 } 148 }
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  
151 -// }  
152 -//  
153 -// Log.d("scanner", "SCANNED IMAGE: $byteArray")  
154 -// lastScanned = barcodes;  
155 -//  
156 -//  
157 -// }  
158 -//  
159 -// }  
160 -// isAnalyzing = false  
161 } 149 }
162 - .addOnFailureListener { e -> sink?.success(mapOf(  
163 - "name" to "error",  
164 - "data" to e.localizedMessage  
165 - )) } 150 + .addOnFailureListener { e ->
  151 + mobileScannerErrorCallback(
  152 + e.localizedMessage ?: e.toString()
  153 + )
  154 + }
166 .addOnCompleteListener { imageProxy.close() } 155 .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)  
177 156
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() 157 + if (detectionSpeed == DetectionSpeed.NORMAL) {
  158 + // Set timer and continue
  159 + Handler(Looper.getMainLooper()).postDelayed({
  160 + scannerTimeout = false
  161 + }, detectionTimeout)
  162 + }
185 } 163 }
186 164
187 - private var scanner = BarcodeScanning.getClient()  
188 -  
189 - 165 + /**
  166 + * Start barcode scanning by initializing the camera and barcode scanner.
  167 + */
190 @ExperimentalGetImage 168 @ExperimentalGetImage
191 - private fun start(call: MethodCall, result: MethodChannel.Result) { 169 + fun start(
  170 + barcodeScannerOptions: BarcodeScannerOptions?,
  171 + returnImage: Boolean,
  172 + cameraPosition: CameraSelector,
  173 + torch: Boolean,
  174 + detectionSpeed: DetectionSpeed,
  175 + torchStateCallback: TorchStateCallback,
  176 + mobileScannerStartedCallback: MobileScannerStartedCallback,
  177 + detectionTimeout: Long
  178 + ) {
  179 + this.detectionSpeed = detectionSpeed
  180 + this.detectionTimeout = detectionTimeout
  181 + this.returnImage = returnImage
  182 +
192 if (camera?.cameraInfo != null && preview != null && textureEntry != null) { 183 if (camera?.cameraInfo != null && preview != null && textureEntry != null) {
193 - val resolution = preview!!.resolutionInfo!!.resolution  
194 - val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0  
195 - val width = resolution.width.toDouble()  
196 - val height = resolution.height.toDouble()  
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 - )  
206 - result.success(answer) 184 + throw AlreadyStarted()
  185 + }
  186 +
  187 + scanner = if (barcodeScannerOptions != null) {
  188 + BarcodeScanning.getClient(barcodeScannerOptions)
207 } else { 189 } else {
208 - val facing: Int = call.argument<Int>("facing") ?: 0  
209 - val ratio: Int? = call.argument<Int>("ratio")  
210 - val torch: Boolean = call.argument<Boolean>("torch") ?: false  
211 - val formats: List<Int>? = call.argument<List<Int>>("formats")  
212 -// val analyzerWidth = call.argument<Int>("ratio")  
213 -// val analyzeRHEIG = call.argument<Int>("ratio")  
214 -  
215 - if (formats != null) {  
216 - val formatsList: MutableList<Int> = mutableListOf()  
217 - for (index in formats) {  
218 - formatsList.add(BarcodeFormats.values()[index].intValue)  
219 - }  
220 - scanner = if (formatsList.size == 1) {  
221 - BarcodeScanning.getClient(  
222 - BarcodeScannerOptions.Builder().setBarcodeFormats(formatsList.first())  
223 - .build()  
224 - )  
225 - } else {  
226 - BarcodeScanning.getClient(  
227 - BarcodeScannerOptions.Builder().setBarcodeFormats(  
228 - formatsList.first(),  
229 - *formatsList.subList(1, formatsList.size).toIntArray()  
230 - ).build()  
231 - )  
232 - }  
233 - } 190 + BarcodeScanning.getClient()
  191 + }
234 192
235 - val future = ProcessCameraProvider.getInstance(activity)  
236 - val executor = ContextCompat.getMainExecutor(activity) 193 + val cameraProviderFuture = ProcessCameraProvider.getInstance(activity)
  194 + val executor = ContextCompat.getMainExecutor(activity)
237 195
238 - future.addListener({  
239 - cameraProvider = future.get()  
240 - if (cameraProvider == null) {  
241 - result.error("cameraProvider", "cameraProvider is null", null)  
242 - return@addListener  
243 - }  
244 - cameraProvider!!.unbindAll()  
245 - textureEntry = textureRegistry.createSurfaceTexture()  
246 - if (textureEntry == null) {  
247 - result.error("textureEntry", "textureEntry is null", null)  
248 - return@addListener  
249 - }  
250 - // Preview  
251 - val surfaceProvider = Preview.SurfaceProvider { request ->  
252 - val texture = textureEntry!!.surfaceTexture()  
253 - texture.setDefaultBufferSize(  
254 - request.resolution.width,  
255 - request.resolution.height  
256 - )  
257 - val surface = Surface(texture)  
258 - request.provideSurface(surface, executor) { }  
259 - } 196 + cameraProviderFuture.addListener({
  197 + cameraProvider = cameraProviderFuture.get()
  198 + if (cameraProvider == null) {
  199 + throw CameraError()
  200 + }
  201 + cameraProvider!!.unbindAll()
  202 + textureEntry = textureRegistry.createSurfaceTexture()
  203 +
  204 + // Preview
  205 + val surfaceProvider = Preview.SurfaceProvider { request ->
  206 + val texture = textureEntry!!.surfaceTexture()
  207 + texture.setDefaultBufferSize(
  208 + request.resolution.width,
  209 + request.resolution.height
  210 + )
260 211
261 - // Build the preview to be shown on the Flutter texture  
262 - val previewBuilder = Preview.Builder()  
263 - if (ratio != null) {  
264 - previewBuilder.setTargetAspectRatio(ratio)  
265 - }  
266 - preview = previewBuilder.build().apply { setSurfaceProvider(surfaceProvider) } 212 + val surface = Surface(texture)
  213 + request.provideSurface(surface, executor) { }
  214 + }
267 215
268 - // Build the analyzer to be passed on to MLKit  
269 - val analysisBuilder = ImageAnalysis.Builder()  
270 - .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)  
271 - if (ratio != null) {  
272 - analysisBuilder.setTargetAspectRatio(ratio)  
273 - } 216 + // Build the preview to be shown on the Flutter texture
  217 + val previewBuilder = Preview.Builder()
  218 + preview = previewBuilder.build().apply { setSurfaceProvider(surfaceProvider) }
  219 +
  220 + // Build the analyzer to be passed on to MLKit
  221 + val analysisBuilder = ImageAnalysis.Builder()
  222 + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
274 // analysisBuilder.setTargetResolution(Size(1440, 1920)) 223 // analysisBuilder.setTargetResolution(Size(1440, 1920))
275 - val analysis = analysisBuilder.build().apply { setAnalyzer(executor, analyzer) } 224 + val analysis = analysisBuilder.build().apply { setAnalyzer(executor, captureOutput) }
276 225
277 - // Select the correct camera  
278 - val selector =  
279 - if (facing == 0) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA 226 + camera = cameraProvider!!.bindToLifecycle(
  227 + activity as LifecycleOwner,
  228 + cameraPosition,
  229 + preview,
  230 + analysis
  231 + )
280 232
281 - camera = cameraProvider!!.bindToLifecycle(  
282 - activity as LifecycleOwner,  
283 - selector,  
284 - preview,  
285 - analysis  
286 - ) 233 + // Register the torch listener
  234 + camera!!.cameraInfo.torchState.observe(activity) { state ->
  235 + // TorchState.OFF = 0; TorchState.ON = 1
  236 + torchStateCallback(state)
  237 + }
287 238
288 - val analysisSize = analysis.resolutionInfo?.resolution ?: Size(0, 0)  
289 - val previewSize = preview!!.resolutionInfo?.resolution ?: Size(0, 0)  
290 - Log.i("LOG", "Analyzer: $analysisSize")  
291 - Log.i("LOG", "Preview: $previewSize") 239 +// val analysisSize = analysis.resolutionInfo?.resolution ?: Size(0, 0)
  240 +// val previewSize = preview!!.resolutionInfo?.resolution ?: Size(0, 0)
  241 +// Log.i("LOG", "Analyzer: $analysisSize")
  242 +// Log.i("LOG", "Preview: $previewSize")
292 243
293 - if (camera == null) {  
294 - result.error("camera", "camera is null", null)  
295 - return@addListener  
296 - } 244 + // Enable torch if provided
  245 + camera!!.cameraControl.enableTorch(torch)
297 246
298 - // Register the torch listener  
299 - camera!!.cameraInfo.torchState.observe(activity) { state ->  
300 - // TorchState.OFF = 0; TorchState.ON = 1  
301 - sink?.success(mapOf("name" to "torchState", "data" to state))  
302 - } 247 + val resolution = preview!!.resolutionInfo!!.resolution
  248 + val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0
  249 + val width = resolution.width.toDouble()
  250 + val height = resolution.height.toDouble()
303 251
304 - // Enable torch if provided  
305 - camera!!.cameraControl.enableTorch(torch)  
306 -  
307 - val resolution = preview!!.resolutionInfo!!.resolution  
308 - val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0  
309 - val width = resolution.width.toDouble()  
310 - val height = resolution.height.toDouble()  
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() 252 + mobileScannerStartedCallback(
  253 + MobileScannerStartParameters(
  254 + if (portrait) width else height,
  255 + if (portrait) height else width,
  256 + camera!!.cameraInfo.hasFlashUnit(),
  257 + textureEntry!!.id()
319 ) 258 )
320 - result.success(answer)  
321 - }, executor)  
322 - }  
323 - }  
324 -  
325 - private fun toggleTorch(call: MethodCall, result: MethodChannel.Result) {  
326 - if (camera == null) {  
327 - result.error(TAG, "Called toggleTorch() while stopped!", null)  
328 - return  
329 - }  
330 - camera!!.cameraControl.enableTorch(call.arguments == 1)  
331 - result.success(null)  
332 - }  
333 -  
334 -// private fun switchAnalyzeMode(call: MethodCall, result: MethodChannel.Result) {  
335 -// analyzeMode = call.arguments as Int  
336 -// result.success(null)  
337 -// }  
338 -  
339 - private fun analyzeImage(call: MethodCall, result: MethodChannel.Result) {  
340 - val uri = Uri.fromFile(File(call.arguments.toString()))  
341 - val inputImage = InputImage.fromFilePath(activity, uri)  
342 -  
343 - var barcodeFound = false  
344 - scanner.process(inputImage)  
345 - .addOnSuccessListener { barcodes ->  
346 - for (barcode in barcodes) {  
347 - barcodeFound = true  
348 - sink?.success(mapOf("name" to "barcode", "data" to barcode.data))  
349 - }  
350 - }  
351 - .addOnFailureListener { e ->  
352 - Log.e(TAG, e.message, e)  
353 - result.error(TAG, e.message, e)  
354 - }  
355 - .addOnCompleteListener { result.success(barcodeFound) } 259 + )
  260 + }, executor)
356 261
357 } 262 }
358 263
359 - private fun stop(result: MethodChannel.Result) { 264 + /**
  265 + * Stop barcode scanning.
  266 + */
  267 + fun stop() {
360 if (camera == null && preview == null) { 268 if (camera == null && preview == null) {
361 - result.error(TAG, "Called stop() while already stopped!", null)  
362 - return 269 + throw AlreadyStopped()
363 } 270 }
364 271
365 val owner = activity as LifecycleOwner 272 val owner = activity as LifecycleOwner
@@ -367,81 +274,43 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: @@ -367,81 +274,43 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
367 cameraProvider?.unbindAll() 274 cameraProvider?.unbindAll()
368 textureEntry?.release() 275 textureEntry?.release()
369 276
370 -// analyzeMode = AnalyzeMode.NONE  
371 camera = null 277 camera = null
372 preview = null 278 preview = null
373 textureEntry = null 279 textureEntry = null
374 cameraProvider = null 280 cameraProvider = null
  281 + }
375 282
376 - result.success(null) 283 + /**
  284 + * Toggles the flash light on or off.
  285 + */
  286 + fun toggleTorch(enableTorch: Boolean) {
  287 + if (camera == null) {
  288 + throw TorchWhenStopped()
  289 + }
  290 + camera!!.cameraControl.enableTorch(enableTorch)
377 } 291 }
378 292
  293 + /**
  294 + * Analyze a single image.
  295 + */
  296 + fun analyzeImage(image: Uri, analyzerCallback: AnalyzerCallback) {
  297 + val inputImage = InputImage.fromFilePath(activity, image)
  298 +
  299 + scanner.process(inputImage)
  300 + .addOnSuccessListener { barcodes ->
  301 + val barcodeMap = barcodes.map { barcode -> barcode.data }
  302 +
  303 + if (barcodeMap.isNotEmpty()) {
  304 + analyzerCallback(barcodeMap)
  305 + } else {
  306 + analyzerCallback(null)
  307 + }
  308 + }
  309 + .addOnFailureListener { e ->
  310 + mobileScannerErrorCallback(
  311 + e.localizedMessage ?: e.toString()
  312 + )
  313 + }
  314 + }
379 315
380 - private val Barcode.data: Map<String, Any?>  
381 - get() = mapOf(  
382 - "corners" to cornerPoints?.map { corner -> corner.data }, "format" to format,  
383 - "rawBytes" to rawBytes, "rawValue" to rawValue, "type" to valueType,  
384 - "calendarEvent" to calendarEvent?.data, "contactInfo" to contactInfo?.data,  
385 - "driverLicense" to driverLicense?.data, "email" to email?.data,  
386 - "geoPoint" to geoPoint?.data, "phone" to phone?.data, "sms" to sms?.data,  
387 - "url" to url?.data, "wifi" to wifi?.data, "displayValue" to displayValue  
388 - )  
389 -  
390 - private val Point.data: Map<String, Double>  
391 - get() = mapOf("x" to x.toDouble(), "y" to y.toDouble())  
392 -  
393 - private val Barcode.CalendarEvent.data: Map<String, Any?>  
394 - get() = mapOf(  
395 - "description" to description, "end" to end?.rawValue, "location" to location,  
396 - "organizer" to organizer, "start" to start?.rawValue, "status" to status,  
397 - "summary" to summary  
398 - )  
399 -  
400 - private val Barcode.ContactInfo.data: Map<String, Any?>  
401 - get() = mapOf(  
402 - "addresses" to addresses.map { address -> address.data },  
403 - "emails" to emails.map { email -> email.data }, "name" to name?.data,  
404 - "organization" to organization, "phones" to phones.map { phone -> phone.data },  
405 - "title" to title, "urls" to urls  
406 - )  
407 -  
408 - private val Barcode.Address.data: Map<String, Any?>  
409 - get() = mapOf(  
410 - "addressLines" to addressLines.map { addressLine -> addressLine.toString() },  
411 - "type" to type  
412 - )  
413 -  
414 - private val Barcode.PersonName.data: Map<String, Any?>  
415 - get() = mapOf(  
416 - "first" to first, "formattedName" to formattedName, "last" to last,  
417 - "middle" to middle, "prefix" to prefix, "pronunciation" to pronunciation,  
418 - "suffix" to suffix  
419 - )  
420 -  
421 - private val Barcode.DriverLicense.data: Map<String, Any?>  
422 - get() = mapOf(  
423 - "addressCity" to addressCity, "addressState" to addressState,  
424 - "addressStreet" to addressStreet, "addressZip" to addressZip, "birthDate" to birthDate,  
425 - "documentType" to documentType, "expiryDate" to expiryDate, "firstName" to firstName,  
426 - "gender" to gender, "issueDate" to issueDate, "issuingCountry" to issuingCountry,  
427 - "lastName" to lastName, "licenseNumber" to licenseNumber, "middleName" to middleName  
428 - )  
429 -  
430 - private val Barcode.Email.data: Map<String, Any?>  
431 - get() = mapOf("address" to address, "body" to body, "subject" to subject, "type" to type)  
432 -  
433 - private val Barcode.GeoPoint.data: Map<String, Any?>  
434 - get() = mapOf("latitude" to lat, "longitude" to lng)  
435 -  
436 - private val Barcode.Phone.data: Map<String, Any?>  
437 - get() = mapOf("number" to number, "type" to type)  
438 -  
439 - private val Barcode.Sms.data: Map<String, Any?>  
440 - get() = mapOf("message" to message, "phoneNumber" to phoneNumber)  
441 -  
442 - private val Barcode.UrlBookmark.data: Map<String, Any?>  
443 - get() = mapOf("title" to title, "url" to url)  
444 -  
445 - private val Barcode.WiFi.data: Map<String, Any?>  
446 - get() = mapOf("encryptionType" to encryptionType, "password" to password, "ssid" to ssid)  
447 -}  
  316 +}
1 package dev.steenbakker.mobile_scanner 1 package dev.steenbakker.mobile_scanner
2 2
3 -import androidx.annotation.NonNull 3 +import android.net.Uri
  4 +import androidx.camera.core.CameraSelector
  5 +import androidx.camera.core.ExperimentalGetImage
  6 +import com.google.mlkit.vision.barcode.BarcodeScannerOptions
  7 +import dev.steenbakker.mobile_scanner.objects.BarcodeFormats
  8 +import dev.steenbakker.mobile_scanner.objects.DetectionSpeed
4 import io.flutter.embedding.engine.plugins.FlutterPlugin 9 import io.flutter.embedding.engine.plugins.FlutterPlugin
5 import io.flutter.embedding.engine.plugins.activity.ActivityAware 10 import io.flutter.embedding.engine.plugins.activity.ActivityAware
6 import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding 11 import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
7 -import io.flutter.plugin.common.EventChannel 12 +import io.flutter.plugin.common.MethodCall
8 import io.flutter.plugin.common.MethodChannel 13 import io.flutter.plugin.common.MethodChannel
  14 +import java.io.File
9 15
10 /** MobileScannerPlugin */ 16 /** MobileScannerPlugin */
11 -class MobileScannerPlugin : FlutterPlugin, ActivityAware {  
12 - private var flutter: FlutterPlugin.FlutterPluginBinding? = null  
13 - private var activity: ActivityPluginBinding? = null 17 +class MobileScannerPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCallHandler {
  18 +
  19 + private var flutterPluginBinding: FlutterPlugin.FlutterPluginBinding? = null
  20 + private var activityPluginBinding: ActivityPluginBinding? = null
14 private var handler: MobileScanner? = null 21 private var handler: MobileScanner? = null
15 private var method: MethodChannel? = null 22 private var method: MethodChannel? = null
16 - private var event: EventChannel? = null  
17 23
18 - override fun onAttachedToEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {  
19 - this.flutter = binding 24 + private lateinit var barcodeHandler: BarcodeHandler
  25 +
  26 + private var permissionResult: MethodChannel.Result? = null
  27 + private var analyzerResult: MethodChannel.Result? = null
  28 +
  29 + private val permissionCallback: PermissionCallback = {hasPermission: Boolean ->
  30 + permissionResult?.success(hasPermission)
  31 + permissionResult = null
  32 + }
  33 +
  34 + private val callback: MobileScannerCallback = { barcodes: List<Map<String, Any?>>, image: ByteArray? ->
  35 + if (image != null) {
  36 + barcodeHandler.publishEvent(mapOf(
  37 + "name" to "barcode",
  38 + "data" to barcodes,
  39 + "image" to image
  40 + ))
  41 + } else {
  42 + barcodeHandler.publishEvent(mapOf(
  43 + "name" to "barcode",
  44 + "data" to barcodes
  45 + ))
  46 + }
  47 + }
  48 +
  49 + private val analyzerCallback: AnalyzerCallback = { barcodes: List<Map<String, Any?>>?->
  50 + if (barcodes != null) {
  51 + barcodeHandler.publishEvent(mapOf(
  52 + "name" to "barcode",
  53 + "data" to barcodes
  54 + ))
  55 + analyzerResult?.success(true)
  56 + } else {
  57 + analyzerResult?.success(false)
  58 + }
  59 + analyzerResult = null
  60 + }
  61 +
  62 + private val errorCallback: MobileScannerErrorCallback = {error: String ->
  63 + barcodeHandler.publishEvent(mapOf(
  64 + "name" to "error",
  65 + "data" to error,
  66 + ))
20 } 67 }
21 68
22 - override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {  
23 - this.flutter = null 69 + private val torchStateCallback: TorchStateCallback = {state: Int ->
  70 + barcodeHandler.publishEvent(mapOf("name" to "torchState", "data" to state))
24 } 71 }
25 72
26 - override fun onAttachedToActivity(binding: ActivityPluginBinding) {  
27 - activity = binding  
28 - handler = MobileScanner(activity!!.activity, flutter!!.textureRegistry)  
29 - method = MethodChannel(flutter!!.binaryMessenger, "dev.steenbakker.mobile_scanner/scanner/method")  
30 - event = EventChannel(flutter!!.binaryMessenger, "dev.steenbakker.mobile_scanner/scanner/event")  
31 - method!!.setMethodCallHandler(handler)  
32 - event!!.setStreamHandler(handler)  
33 - activity!!.addRequestPermissionsResultListener(handler!!) 73 + @ExperimentalGetImage
  74 + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
  75 + if (handler == null) {
  76 + result.error("MobileScanner", "Called ${call.method} before initializing.", null)
  77 + return
  78 + }
  79 + when (call.method) {
  80 + "state" -> result.success(handler!!.hasCameraPermission())
  81 + "request" -> requestPermission(result)
  82 + "start" -> start(call, result)
  83 + "torch" -> toggleTorch(call, result)
  84 + "stop" -> stop(result)
  85 + "analyzeImage" -> analyzeImage(call, result)
  86 + else -> result.notImplemented()
  87 + }
  88 + }
  89 +
  90 + override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
  91 + method = MethodChannel(binding.binaryMessenger, "dev.steenbakker.mobile_scanner/scanner/method")
  92 + method!!.setMethodCallHandler(this)
  93 +
  94 + barcodeHandler = BarcodeHandler(binding)
  95 +
  96 + this.flutterPluginBinding = binding
  97 + }
  98 +
  99 + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
  100 + this.flutterPluginBinding = null
  101 + }
  102 +
  103 + override fun onAttachedToActivity(activityPluginBinding: ActivityPluginBinding) {
  104 + handler = MobileScanner(activityPluginBinding.activity, flutterPluginBinding!!.textureRegistry, callback, errorCallback
  105 + )
  106 + activityPluginBinding.addRequestPermissionsResultListener(handler!!)
  107 +
  108 + this.activityPluginBinding = activityPluginBinding
34 } 109 }
35 110
36 override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { 111 override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
@@ -38,16 +113,117 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware { @@ -38,16 +113,117 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware {
38 } 113 }
39 114
40 override fun onDetachedFromActivity() { 115 override fun onDetachedFromActivity() {
41 - activity!!.removeRequestPermissionsResultListener(handler!!)  
42 - event!!.setStreamHandler(null) 116 + activityPluginBinding!!.removeRequestPermissionsResultListener(handler!!)
43 method!!.setMethodCallHandler(null) 117 method!!.setMethodCallHandler(null)
44 - event = null  
45 method = null 118 method = null
46 handler = null 119 handler = null
47 - activity = null 120 + activityPluginBinding = null
48 } 121 }
49 122
50 override fun onDetachedFromActivityForConfigChanges() { 123 override fun onDetachedFromActivityForConfigChanges() {
51 onDetachedFromActivity() 124 onDetachedFromActivity()
52 } 125 }
  126 +
  127 + private fun requestPermission(result: MethodChannel.Result) {
  128 + permissionResult = result
  129 + handler!!.requestPermission(permissionCallback)
  130 + }
  131 +
  132 + @ExperimentalGetImage
  133 + private fun start(call: MethodCall, result: MethodChannel.Result) {
  134 + val torch: Boolean = call.argument<Boolean>("torch") ?: false
  135 + val facing: Int = call.argument<Int>("facing") ?: 0
  136 + val formats: List<Int>? = call.argument<List<Int>>("formats")
  137 + val returnImage: Boolean = call.argument<Boolean>("returnImage") ?: false
  138 + val speed: Int = call.argument<Int>("speed") ?: 1
  139 + val timeout: Int = call.argument<Int>("timeout") ?: 250
  140 +
  141 + var barcodeScannerOptions: BarcodeScannerOptions? = null
  142 + if (formats != null) {
  143 + val formatsList: MutableList<Int> = mutableListOf()
  144 + for (index in formats) {
  145 + formatsList.add(BarcodeFormats.values()[index].intValue)
  146 + }
  147 + barcodeScannerOptions = if (formatsList.size == 1) {
  148 + BarcodeScannerOptions.Builder().setBarcodeFormats(formatsList.first())
  149 + .build()
  150 + } else {
  151 + BarcodeScannerOptions.Builder().setBarcodeFormats(
  152 + formatsList.first(),
  153 + *formatsList.subList(1, formatsList.size).toIntArray()
  154 + ).build()
  155 + }
  156 + }
  157 +
  158 + val position =
  159 + if (facing == 0) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA
  160 +
  161 + val detectionSpeed: DetectionSpeed = DetectionSpeed.values().first { it.intValue == speed}
  162 +
  163 + try {
  164 + handler!!.start(barcodeScannerOptions, returnImage, position, torch, detectionSpeed, torchStateCallback, mobileScannerStartedCallback = {
  165 + result.success(mapOf(
  166 + "textureId" to it.id,
  167 + "size" to mapOf("width" to it.width, "height" to it.height),
  168 + "torchable" to it.hasFlashUnit
  169 + ))
  170 + },
  171 + timeout.toLong())
  172 +
  173 + } catch (e: AlreadyStarted) {
  174 + result.error(
  175 + "MobileScanner",
  176 + "Called start() while already started",
  177 + null
  178 + )
  179 + } catch (e: NoCamera) {
  180 + result.error(
  181 + "MobileScanner",
  182 + "No camera found or failed to open camera!",
  183 + null
  184 + )
  185 + } catch (e: TorchError) {
  186 + result.error(
  187 + "MobileScanner",
  188 + "Error occurred when setting torch!",
  189 + null
  190 + )
  191 + } catch (e: CameraError) {
  192 + result.error(
  193 + "MobileScanner",
  194 + "Error occurred when setting up camera!",
  195 + null
  196 + )
  197 + } catch (e: Exception) {
  198 + result.error(
  199 + "MobileScanner",
  200 + "Unknown error occurred..",
  201 + null
  202 + )
  203 + }
  204 + }
  205 +
  206 + private fun stop(result: MethodChannel.Result) {
  207 + try {
  208 + handler!!.stop()
  209 + result.success(null)
  210 + } catch (e: AlreadyStopped) {
  211 + result.error("MobileScanner", "Called stop() while already stopped!", null)
  212 + }
  213 + }
  214 +
  215 + private fun analyzeImage(call: MethodCall, result: MethodChannel.Result) {
  216 + analyzerResult = result
  217 + val uri = Uri.fromFile(File(call.arguments.toString()))
  218 + handler!!.analyzeImage(uri, analyzerCallback)
  219 + }
  220 +
  221 + private fun toggleTorch(call: MethodCall, result: MethodChannel.Result) {
  222 + try {
  223 + handler!!.toggleTorch(call.arguments == 1)
  224 + result.success(null)
  225 + } catch (e: AlreadyStopped) {
  226 + result.error("MobileScanner", "Called toggleTorch() while stopped!", null)
  227 + }
  228 + }
53 } 229 }
  1 +package dev.steenbakker.mobile_scanner
  2 +
  3 +import android.graphics.ImageFormat
  4 +import android.graphics.Point
  5 +import android.graphics.Rect
  6 +import android.graphics.YuvImage
  7 +import android.media.Image
  8 +import com.google.mlkit.vision.barcode.common.Barcode
  9 +import java.io.ByteArrayOutputStream
  10 +
  11 +fun Image.toByteArray(): ByteArray {
  12 + val yBuffer = planes[0].buffer // Y
  13 + val vuBuffer = planes[2].buffer // VU
  14 +
  15 + val ySize = yBuffer.remaining()
  16 + val vuSize = vuBuffer.remaining()
  17 +
  18 + val nv21 = ByteArray(ySize + vuSize)
  19 +
  20 + yBuffer.get(nv21, 0, ySize)
  21 + vuBuffer.get(nv21, ySize, vuSize)
  22 +
  23 + val yuvImage = YuvImage(nv21, ImageFormat.NV21, this.width, this.height, null)
  24 + val out = ByteArrayOutputStream()
  25 + yuvImage.compressToJpeg(Rect(0, 0, yuvImage.width, yuvImage.height), 50, out)
  26 + return out.toByteArray()
  27 +}
  28 +
  29 +val Barcode.data: Map<String, Any?>
  30 + get() = mapOf(
  31 + "corners" to cornerPoints?.map { corner -> corner.data }, "format" to format,
  32 + "rawBytes" to rawBytes, "rawValue" to rawValue, "type" to valueType,
  33 + "calendarEvent" to calendarEvent?.data, "contactInfo" to contactInfo?.data,
  34 + "driverLicense" to driverLicense?.data, "email" to email?.data,
  35 + "geoPoint" to geoPoint?.data, "phone" to phone?.data, "sms" to sms?.data,
  36 + "url" to url?.data, "wifi" to wifi?.data, "displayValue" to displayValue
  37 + )
  38 +
  39 +private val Point.data: Map<String, Double>
  40 + get() = mapOf("x" to x.toDouble(), "y" to y.toDouble())
  41 +
  42 +private val Barcode.CalendarEvent.data: Map<String, Any?>
  43 + get() = mapOf(
  44 + "description" to description, "end" to end?.rawValue, "location" to location,
  45 + "organizer" to organizer, "start" to start?.rawValue, "status" to status,
  46 + "summary" to summary
  47 + )
  48 +
  49 +private val Barcode.ContactInfo.data: Map<String, Any?>
  50 + get() = mapOf(
  51 + "addresses" to addresses.map { address -> address.data },
  52 + "emails" to emails.map { email -> email.data }, "name" to name?.data,
  53 + "organization" to organization, "phones" to phones.map { phone -> phone.data },
  54 + "title" to title, "urls" to urls
  55 + )
  56 +
  57 +private val Barcode.Address.data: Map<String, Any?>
  58 + get() = mapOf(
  59 + "addressLines" to addressLines.map { addressLine -> addressLine.toString() },
  60 + "type" to type
  61 + )
  62 +
  63 +private val Barcode.PersonName.data: Map<String, Any?>
  64 + get() = mapOf(
  65 + "first" to first, "formattedName" to formattedName, "last" to last,
  66 + "middle" to middle, "prefix" to prefix, "pronunciation" to pronunciation,
  67 + "suffix" to suffix
  68 + )
  69 +
  70 +private val Barcode.DriverLicense.data: Map<String, Any?>
  71 + get() = mapOf(
  72 + "addressCity" to addressCity, "addressState" to addressState,
  73 + "addressStreet" to addressStreet, "addressZip" to addressZip, "birthDate" to birthDate,
  74 + "documentType" to documentType, "expiryDate" to expiryDate, "firstName" to firstName,
  75 + "gender" to gender, "issueDate" to issueDate, "issuingCountry" to issuingCountry,
  76 + "lastName" to lastName, "licenseNumber" to licenseNumber, "middleName" to middleName
  77 + )
  78 +
  79 +private val Barcode.Email.data: Map<String, Any?>
  80 + get() = mapOf("address" to address, "body" to body, "subject" to subject, "type" to type)
  81 +
  82 +private val Barcode.GeoPoint.data: Map<String, Any?>
  83 + get() = mapOf("latitude" to lat, "longitude" to lng)
  84 +
  85 +private val Barcode.Phone.data: Map<String, Any?>
  86 + get() = mapOf("number" to number, "type" to type)
  87 +
  88 +private val Barcode.Sms.data: Map<String, Any?>
  89 + get() = mapOf("message" to message, "phoneNumber" to phoneNumber)
  90 +
  91 +private val Barcode.UrlBookmark.data: Map<String, Any?>
  92 + get() = mapOf("title" to title, "url" to url)
  93 +
  94 +private val Barcode.WiFi.data: Map<String, Any?>
  95 + get() = mapOf("encryptionType" to encryptionType, "password" to password, "ssid" to ssid)
1 -package dev.steenbakker.mobile_scanner.exceptions  
2 -  
3 -internal class NoPermissionException : RuntimeException()  
4 -  
5 -//internal class Exception(val reason: Reason) :  
6 -// java.lang.Exception("Mobile Scanner failed because $reason") {  
7 -//  
8 -// internal enum class Reason {  
9 -// noHardware, noPermissions, noBackCamera  
10 -// }  
11 -//}  
1 -package dev.steenbakker.mobile_scanner  
2 -  
3 -import com.google.mlkit.vision.barcode.BarcodeScannerOptions 1 +package dev.steenbakker.mobile_scanner.objects
4 2
5 enum class BarcodeFormats(val intValue: Int) { 3 enum class BarcodeFormats(val intValue: Int) {
6 UNKNOWN(com.google.mlkit.vision.barcode.common.Barcode.FORMAT_UNKNOWN), 4 UNKNOWN(com.google.mlkit.vision.barcode.common.Barcode.FORMAT_UNKNOWN),
  1 +package dev.steenbakker.mobile_scanner.objects
  2 +
  3 +enum class DetectionSpeed(val intValue: Int) {
  4 + NO_DUPLICATES(0),
  5 + NORMAL(1),
  6 + UNRESTRICTED(2)
  7 +}
  1 +package dev.steenbakker.mobile_scanner.objects
  2 +
  3 +class MobileScannerStartParameters(
  4 + val width: Double = 0.0,
  5 + val height: Double,
  6 + val hasFlashUnit: Boolean,
  7 + val id: Long
  8 +)
1 buildscript { 1 buildscript {
2 - ext.kotlin_version = '1.7.20' 2 + ext.kotlin_version = '1.7.21'
3 repositories { 3 repositories {
4 google() 4 google()
5 mavenCentral() 5 mavenCentral()
@@ -8,7 +8,7 @@ environment: @@ -8,7 +8,7 @@ environment:
8 dependencies: 8 dependencies:
9 flutter: 9 flutter:
10 sdk: flutter 10 sdk: flutter
11 - image_picker: ^0.8.5+3 11 + image_picker: ^0.8.6
12 12
13 mobile_scanner: 13 mobile_scanner:
14 path: ../ 14 path: ../
  1 +//
  2 +// DetectionSpeed.swift
  3 +// mobile_scanner
  4 +//
  5 +// Created by Julian Steenbakker on 11/11/2022.
  6 +//
  7 +
  8 +enum DetectionSpeed: Int {
  9 + case noDuplicates = 0
  10 + case normal = 1
  11 + case unrestricted = 2
  12 +}
@@ -49,7 +49,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -49,7 +49,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
49 super.init() 49 super.init()
50 } 50 }
51 51
52 - /// Check permissions for video 52 + /// Check if we already have camera permission.
53 func checkPermission() -> Int { 53 func checkPermission() -> Int {
54 let status = AVCaptureDevice.authorizationStatus(for: .video) 54 let status = AVCaptureDevice.authorizationStatus(for: .video)
55 switch status { 55 switch status {
@@ -66,6 +66,44 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -66,6 +66,44 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
66 func requestPermission(_ result: @escaping FlutterResult) { 66 func requestPermission(_ result: @escaping FlutterResult) {
67 AVCaptureDevice.requestAccess(for: .video, completionHandler: { result($0) }) 67 AVCaptureDevice.requestAccess(for: .video, completionHandler: { result($0) })
68 } 68 }
  69 +
  70 + /// Gets called when a new image is added to the buffer
  71 + public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
  72 + guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
  73 + print("Failed to get image buffer from sample buffer.")
  74 + return
  75 + }
  76 + latestBuffer = imageBuffer
  77 + registry?.textureFrameAvailable(textureId)
  78 + if ((detectionSpeed == DetectionSpeed.normal || detectionSpeed == DetectionSpeed.noDuplicates) && i > 10 || detectionSpeed == DetectionSpeed.unrestricted) {
  79 + i = 0
  80 + let ciImage = latestBuffer.image
  81 +
  82 + let image = VisionImage(image: ciImage)
  83 + image.orientation = imageOrientation(
  84 + deviceOrientation: UIDevice.current.orientation,
  85 + defaultOrientation: .portrait,
  86 + position: videoPosition
  87 + )
  88 +
  89 + scanner.process(image) { [self] barcodes, error in
  90 + if (detectionSpeed == DetectionSpeed.noDuplicates) {
  91 + let newScannedBarcodes = barcodes?.map { barcode in
  92 + return barcode.rawValue
  93 + }
  94 + if (error == nil && barcodesString != nil && newScannedBarcodes != nil && barcodesString!.elementsEqual(newScannedBarcodes!)) {
  95 + return
  96 + } else {
  97 + barcodesString = newScannedBarcodes
  98 + }
  99 + }
  100 +
  101 + mobileScannerCallback(barcodes, error, ciImage)
  102 + }
  103 + } else {
  104 + i+=1
  105 + }
  106 + }
69 107
70 /// Start scanning for barcodes 108 /// Start scanning for barcodes
71 func start(barcodeScannerOptions: BarcodeScannerOptions?, returnImage: Bool, cameraPosition: AVCaptureDevice.Position, torch: AVCaptureDevice.TorchMode, detectionSpeed: DetectionSpeed) throws -> MobileScannerStartParameters { 109 func start(barcodeScannerOptions: BarcodeScannerOptions?, returnImage: Bool, cameraPosition: AVCaptureDevice.Position, torch: AVCaptureDevice.TorchMode, detectionSpeed: DetectionSpeed) throws -> MobileScannerStartParameters {
@@ -136,13 +174,6 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -136,13 +174,6 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
136 return MobileScannerStartParameters(width: Double(dimensions.height), height: Double(dimensions.width), hasTorch: device.hasTorch, textureId: textureId) 174 return MobileScannerStartParameters(width: Double(dimensions.height), height: Double(dimensions.width), hasTorch: device.hasTorch, textureId: textureId)
137 } 175 }
138 176
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 177 /// Stop scanning for barcodes
147 func stop() throws { 178 func stop() throws {
148 if (device == nil) { 179 if (device == nil) {
@@ -192,43 +223,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -192,43 +223,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
192 223
193 var barcodesString: Array<String?>? 224 var barcodesString: Array<String?>?
194 225
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 226
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 227
233 // /// Convert image buffer to jpeg 228 // /// Convert image buffer to jpeg
234 // private func ciImageToJpeg(ciImage: CIImage) -> Data { 229 // private func ciImageToJpeg(ciImage: CIImage) -> Data {
@@ -270,6 +265,12 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -270,6 +265,12 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
270 } 265 }
271 return Unmanaged<CVPixelBuffer>.passRetained(latestBuffer) 266 return Unmanaged<CVPixelBuffer>.passRetained(latestBuffer)
272 } 267 }
273 - 268 +
  269 + struct MobileScannerStartParameters {
  270 + var width: Double = 0.0
  271 + var height: Double = 0.0
  272 + var hasTorch = false
  273 + var textureId: Int64 = 0
  274 + }
274 } 275 }
275 276
@@ -56,11 +56,11 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { @@ -56,11 +56,11 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
56 56
57 /// Parses all parameters and starts the mobileScanner 57 /// Parses all parameters and starts the mobileScanner
58 private func start(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { 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 59 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 60 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 ?? [] 61 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 62 let returnImage: Bool = (call.arguments as! Dictionary<String, Any?>)["returnImage"] as? Bool ?? false
  63 + let speed: Int = (call.arguments as! Dictionary<String, Any?>)["speed"] as? Int ?? 0
64 64
65 let formatList = formats.map { format in return BarcodeFormat(rawValue: format)} 65 let formatList = formats.map { format in return BarcodeFormat(rawValue: format)}
66 var barcodeOptions: BarcodeScannerOptions? = nil 66 var barcodeOptions: BarcodeScannerOptions? = nil
@@ -75,10 +75,10 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { @@ -75,10 +75,10 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
75 75
76 76
77 let position = facing == 0 ? AVCaptureDevice.Position.front : .back 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)! 78 + let detectionSpeed: DetectionSpeed = DetectionSpeed(rawValue: speed)!
79 79
80 do { 80 do {
81 - let parameters = try mobileScanner.start(barcodeScannerOptions: barcodeOptions, returnImage: returnImage, cameraPosition: position, torch: torch ? .on : .off, detectionSpeed: speed) 81 + let parameters = try mobileScanner.start(barcodeScannerOptions: barcodeOptions, returnImage: returnImage, cameraPosition: position, torch: torch ? .on : .off, detectionSpeed: detectionSpeed)
82 result(["textureId": parameters.textureId, "size": ["width": parameters.width, "height": parameters.height], "torchable": parameters.hasTorch]) 82 result(["textureId": parameters.textureId, "size": ["width": parameters.width, "height": parameters.height], "torchable": parameters.hasTorch])
83 } catch MobileScannerError.alreadyStarted { 83 } catch MobileScannerError.alreadyStarted {
84 result(FlutterError(code: "MobileScanner", 84 result(FlutterError(code: "MobileScanner",
@@ -90,7 +90,7 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { @@ -90,7 +90,7 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
90 details: nil)) 90 details: nil))
91 } catch MobileScannerError.torchError(let error) { 91 } catch MobileScannerError.torchError(let error) {
92 result(FlutterError(code: "MobileScanner", 92 result(FlutterError(code: "MobileScanner",
93 - message: "Error occured when setting toch!", 93 + message: "Error occured when setting torch!",
94 details: error)) 94 details: error))
95 } catch MobileScannerError.cameraError(let error) { 95 } catch MobileScannerError.cameraError(let error) {
96 result(FlutterError(code: "MobileScanner", 96 result(FlutterError(code: "MobileScanner",
@@ -162,9 +162,3 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { @@ -162,9 +162,3 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
162 } 162 }
163 } 163 }
164 } 164 }
165 -  
166 -enum DetectionSpeed: Int {  
167 - case noDuplicates = 0  
168 - case normal = 1  
169 - case unrestricted = 2  
170 -}  
@@ -4,7 +4,7 @@ enum DetectionSpeed { @@ -4,7 +4,7 @@ enum DetectionSpeed {
4 /// barcode has been scanned. 4 /// barcode has been scanned.
5 noDuplicates, 5 noDuplicates,
6 6
7 - /// Front facing camera. 7 + /// The barcode scanner will wait
8 normal, 8 normal,
9 9
10 /// Back facing camera. 10 /// Back facing camera.
@@ -13,11 +13,10 @@ import 'package:mobile_scanner/src/mobile_scanner_exception.dart'; @@ -13,11 +13,10 @@ import 'package:mobile_scanner/src/mobile_scanner_exception.dart';
13 class MobileScannerController { 13 class MobileScannerController {
14 MobileScannerController({ 14 MobileScannerController({
15 this.facing = CameraFacing.back, 15 this.facing = CameraFacing.back,
16 - this.detectionSpeed = DetectionSpeed.noDuplicates,  
17 - // this.ratio, 16 + this.detectionSpeed = DetectionSpeed.normal,
  17 + this.detectionTimeoutMs = 250,
18 this.torchEnabled = false, 18 this.torchEnabled = false,
19 this.formats, 19 this.formats,
20 - // this.autoResume = true,  
21 this.returnImage = false, 20 this.returnImage = false,
22 this.onPermissionSet, 21 this.onPermissionSet,
23 }) { 22 }) {
@@ -39,11 +38,6 @@ class MobileScannerController { @@ -39,11 +38,6 @@ class MobileScannerController {
39 /// Default: CameraFacing.back 38 /// Default: CameraFacing.back
40 final CameraFacing facing; 39 final CameraFacing facing;
41 40
42 - // /// Analyze the image in 4:3 or 16:9  
43 - // ///  
44 - // /// Only on Android  
45 - // final Ratio? ratio;  
46 -  
47 /// Enable or disable the torch (Flash) on start 41 /// Enable or disable the torch (Flash) on start
48 /// 42 ///
49 /// Default: disabled 43 /// Default: disabled
@@ -62,6 +56,8 @@ class MobileScannerController { @@ -62,6 +56,8 @@ class MobileScannerController {
62 /// WARNING: DetectionSpeed.unrestricted can cause memory issues on some devices 56 /// WARNING: DetectionSpeed.unrestricted can cause memory issues on some devices
63 final DetectionSpeed detectionSpeed; 57 final DetectionSpeed detectionSpeed;
64 58
  59 + final int detectionTimeoutMs;
  60 +
65 /// Sets the barcode stream 61 /// Sets the barcode stream
66 final StreamController<BarcodeCapture> _barcodesController = 62 final StreamController<BarcodeCapture> _barcodesController =
67 StreamController.broadcast(); 63 StreamController.broadcast();
@@ -97,10 +93,9 @@ class MobileScannerController { @@ -97,10 +93,9 @@ class MobileScannerController {
97 93
98 cameraFacingState.value = cameraFacingOverride ?? facing; 94 cameraFacingState.value = cameraFacingOverride ?? facing;
99 arguments['facing'] = cameraFacingState.value.index; 95 arguments['facing'] = cameraFacingState.value.index;
100 -  
101 - // if (ratio != null) arguments['ratio'] = ratio;  
102 arguments['torch'] = torchEnabled; 96 arguments['torch'] = torchEnabled;
103 arguments['speed'] = detectionSpeed.index; 97 arguments['speed'] = detectionSpeed.index;
  98 + arguments['timeout'] = detectionTimeoutMs;
104 99
105 if (formats != null) { 100 if (formats != null) {
106 if (Platform.isAndroid) { 101 if (Platform.isAndroid) {
@@ -281,7 +276,7 @@ class MobileScannerController { @@ -281,7 +276,7 @@ class MobileScannerController {
281 _barcodesController.add( 276 _barcodesController.add(
282 BarcodeCapture( 277 BarcodeCapture(
283 barcodes: parsed, 278 barcodes: parsed,
284 - image: event['image'] as Uint8List, 279 + image: event['image'] as Uint8List?,
285 ), 280 ),
286 ); 281 );
287 break; 282 break;