Julian Steenbakker

refactor: update android code to match ios implementation

... ... @@ -2,7 +2,7 @@ group 'dev.steenbakker.mobile_scanner'
version '1.0-SNAPSHOT'
buildscript {
ext.kotlin_version = '1.7.20'
ext.kotlin_version = '1.7.21'
repositories {
google()
mavenCentral()
... ...
package dev.steenbakker.mobile_scanner
import androidx.annotation.IntDef
@IntDef(AnalyzeMode.NONE, AnalyzeMode.BARCODE)
@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.SOURCE)
annotation class AnalyzeMode {
companion object {
const val NONE = 0
const val BARCODE = 1
}
}
\ No newline at end of file
package dev.steenbakker.mobile_scanner
import android.os.Handler
import android.os.Looper
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.EventChannel
class BarcodeHandler(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) : EventChannel.StreamHandler {
private var eventSink: EventChannel.EventSink? = null
private val eventChannel = EventChannel(
flutterPluginBinding.binaryMessenger,
"dev.steenbakker.mobile_scanner/scanner/event"
)
init {
eventChannel.setStreamHandler(this)
}
fun publishEvent(event: Map<String, Any>) {
Handler(Looper.getMainLooper()).post {
eventSink?.success(event)
}
}
override fun onListen(event: Any?, eventSink: EventChannel.EventSink?) {
this.eventSink = eventSink
}
override fun onCancel(event: Any?) {
this.eventSink = null
}
}
\ No newline at end of file
... ...
... ... @@ -3,16 +3,10 @@ package dev.steenbakker.mobile_scanner
import android.Manifest
import android.app.Activity
import android.content.pm.PackageManager
import android.graphics.ImageFormat
import android.graphics.Point
import android.graphics.Rect
import android.graphics.YuvImage
import android.media.Image
import android.net.Uri
import android.util.Log
import android.util.Size
import android.os.Handler
import android.os.Looper
import android.view.Surface
import androidx.annotation.NonNull
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.app.ActivityCompat
... ... @@ -20,19 +14,32 @@ import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.barcode.common.Barcode
import com.google.mlkit.vision.common.InputImage
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import dev.steenbakker.mobile_scanner.objects.DetectionSpeed
import dev.steenbakker.mobile_scanner.objects.MobileScannerStartParameters
import io.flutter.plugin.common.PluginRegistry
import io.flutter.view.TextureRegistry
import java.io.ByteArrayOutputStream
import java.io.File
class MobileScanner(private val activity: Activity, private val textureRegistry: TextureRegistry) :
MethodChannel.MethodCallHandler, EventChannel.StreamHandler,
typealias PermissionCallback = (permissionGranted: Boolean) -> Unit
typealias MobileScannerCallback = (barcodes: List<Map<String, Any?>>, image: ByteArray?) -> Unit
typealias AnalyzerCallback = (barcodes: List<Map<String, Any?>>?) -> Unit
typealias MobileScannerErrorCallback = (error: String) -> Unit
typealias TorchStateCallback = (state: Int) -> Unit
typealias MobileScannerStartedCallback = (parameters: MobileScannerStartParameters) -> Unit
class NoCamera : Exception()
class AlreadyStarted : Exception()
class AlreadyStopped : Exception()
class TorchError : Exception()
class CameraError : Exception()
class TorchWhenStopped : Exception()
class MobileScanner(
private val activity: Activity,
private val textureRegistry: TextureRegistry,
private val mobileScannerCallback: MobileScannerCallback,
private val mobileScannerErrorCallback: MobileScannerErrorCallback
) :
PluginRegistry.RequestPermissionsResultListener {
companion object {
/**
... ... @@ -40,10 +47,8 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
* @see androidx.fragment.app.FragmentActivity.validateRequestPermissionsRequestCode
*/
private const val REQUEST_CODE = 0x0786
private val TAG = MobileScanner::class.java.simpleName
}
private var sink: EventChannel.EventSink? = null
private var listener: PluginRegistry.RequestPermissionsResultListener? = null
private var cameraProvider: ProcessCameraProvider? = null
... ... @@ -51,31 +56,54 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
private var preview: Preview? = null
private var textureEntry: TextureRegistry.SurfaceTextureEntry? = null
// @AnalyzeMode
// private var analyzeMode: Int = AnalyzeMode.NONE
private var detectionSpeed: DetectionSpeed = DetectionSpeed.NO_DUPLICATES
private var detectionTimeout: Long = 250
private var lastScanned: List<String?>? = null
@ExperimentalGetImage
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: MethodChannel.Result) {
when (call.method) {
"state" -> checkPermission(result)
"request" -> requestPermission(result)
"start" -> start(call, result)
"torch" -> toggleTorch(call, result)
// "analyze" -> switchAnalyzeMode(call, result)
"stop" -> stop(result)
"analyzeImage" -> analyzeImage(call, result)
else -> result.notImplemented()
}
}
private var scannerTimeout = false
private var returnImage = false
private var scanner = BarcodeScanning.getClient()
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
this.sink = events
/**
* Check if we already have camera permission.
*/
fun hasCameraPermission(): Int {
// Can't get exact denied or not_determined state without request. Just return not_determined when state isn't authorized
val hasPermission = ContextCompat.checkSelfPermission(
activity,
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED
return if (hasPermission) {
1
} else {
0
}
}
override fun onCancel(arguments: Any?) {
sink = null
/**
* Request camera permissions.
*/
fun requestPermission(permissionCallback: PermissionCallback) {
listener
?: PluginRegistry.RequestPermissionsResultListener { requestCode, _, grantResults ->
if (requestCode != REQUEST_CODE) {
false
} else {
val authorized = grantResults[0] == PackageManager.PERMISSION_GRANTED
permissionCallback(authorized)
true
}
}
val permissions = arrayOf(Manifest.permission.CAMERA)
ActivityCompat.requestPermissions(activity, permissions, REQUEST_CODE)
}
/**
* Calls the callback after permissions are requested.
*/
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
... ... @@ -84,282 +112,161 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
return listener?.onRequestPermissionsResult(requestCode, permissions, grantResults) ?: false
}
private fun checkPermission(result: MethodChannel.Result) {
// Can't get exact denied or not_determined state without request. Just return not_determined when state isn't authorized
val state =
if (ContextCompat.checkSelfPermission(
activity,
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED
) 1
else 0
result.success(state)
}
private fun requestPermission(result: MethodChannel.Result) {
listener = PluginRegistry.RequestPermissionsResultListener { requestCode, _, grantResults ->
if (requestCode != REQUEST_CODE) {
false
} else {
val authorized = grantResults[0] == PackageManager.PERMISSION_GRANTED
result.success(authorized)
listener = null
true
}
}
val permissions = arrayOf(Manifest.permission.CAMERA)
ActivityCompat.requestPermissions(activity, permissions, REQUEST_CODE)
}
// var lastScanned: List<Barcode>? = null
// var isAnalyzing: Boolean = false
/**
* callback for the camera. Every frame is passed through this function.
*/
@ExperimentalGetImage
val analyzer = ImageAnalysis.Analyzer { imageProxy -> // YUV_420_888 format
val captureOutput = ImageAnalysis.Analyzer { imageProxy -> // YUV_420_888 format
val mediaImage = imageProxy.image ?: return@Analyzer
val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
if (detectionSpeed == DetectionSpeed.NORMAL && scannerTimeout) {
imageProxy.close()
return@Analyzer
} else if (detectionSpeed == DetectionSpeed.NORMAL) {
scannerTimeout = true
}
scanner.process(inputImage)
.addOnSuccessListener { barcodes ->
// if (isAnalyzing) {
// Log.d("scanner", "SKIPPING" )
// return@addOnSuccessListener
// }
// isAnalyzing = true
if (detectionSpeed == DetectionSpeed.NO_DUPLICATES) {
val newScannedBarcodes = barcodes.map { barcode -> barcode.rawValue }
if (newScannedBarcodes == lastScanned) {
// New scanned is duplicate, returning
return@addOnSuccessListener
}
lastScanned = newScannedBarcodes
}
val barcodeMap = barcodes.map { barcode -> barcode.data }
if (barcodeMap.isNotEmpty()) {
sink?.success(mapOf(
"name" to "barcode",
"data" to barcodeMap,
"image" to mediaImage.toByteArray()
))
mobileScannerCallback(
barcodeMap,
if (returnImage) mediaImage.toByteArray() else null
)
}
// for (barcode in barcodes) {
//// if (lastScanned?.contains(barcodes.first) == true) continue;
// if (lastScanned == null) {
// lastScanned = barcodes
// } else if (lastScanned!!.contains(barcode)) {
// // Duplicate, don't send image
// sink?.success(mapOf(
// "name" to "barcode",
// "data" to barcode.data,
// ))
// } else {
// if (byteArray.isEmpty()) {
// Log.d("scanner", "EMPTY" )
// return@addOnSuccessListener
// }
//
// Log.d("scanner", "SCANNED IMAGE: $byteArray")
// lastScanned = barcodes;
//
//
// }
//
// }
// isAnalyzing = false
}
.addOnFailureListener { e -> sink?.success(mapOf(
"name" to "error",
"data" to e.localizedMessage
)) }
.addOnFailureListener { e ->
mobileScannerErrorCallback(
e.localizedMessage ?: e.toString()
)
}
.addOnCompleteListener { imageProxy.close() }
}
private fun Image.toByteArray(): ByteArray {
val yBuffer = planes[0].buffer // Y
val vuBuffer = planes[2].buffer // VU
val ySize = yBuffer.remaining()
val vuSize = vuBuffer.remaining()
val nv21 = ByteArray(ySize + vuSize)
yBuffer.get(nv21, 0, ySize)
vuBuffer.get(nv21, ySize, vuSize)
val yuvImage = YuvImage(nv21, ImageFormat.NV21, this.width, this.height, null)
val out = ByteArrayOutputStream()
yuvImage.compressToJpeg(Rect(0, 0, yuvImage.width, yuvImage.height), 50, out)
return out.toByteArray()
if (detectionSpeed == DetectionSpeed.NORMAL) {
// Set timer and continue
Handler(Looper.getMainLooper()).postDelayed({
scannerTimeout = false
}, detectionTimeout)
}
}
private var scanner = BarcodeScanning.getClient()
/**
* Start barcode scanning by initializing the camera and barcode scanner.
*/
@ExperimentalGetImage
private fun start(call: MethodCall, result: MethodChannel.Result) {
fun start(
barcodeScannerOptions: BarcodeScannerOptions?,
returnImage: Boolean,
cameraPosition: CameraSelector,
torch: Boolean,
detectionSpeed: DetectionSpeed,
torchStateCallback: TorchStateCallback,
mobileScannerStartedCallback: MobileScannerStartedCallback,
detectionTimeout: Long
) {
this.detectionSpeed = detectionSpeed
this.detectionTimeout = detectionTimeout
this.returnImage = returnImage
if (camera?.cameraInfo != null && preview != null && textureEntry != null) {
val resolution = preview!!.resolutionInfo!!.resolution
val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0
val width = resolution.width.toDouble()
val height = resolution.height.toDouble()
val size = if (portrait) mapOf(
"width" to width,
"height" to height
) else mapOf("width" to height, "height" to width)
val answer = mapOf(
"textureId" to textureEntry!!.id(),
"size" to size,
"torchable" to camera!!.cameraInfo.hasFlashUnit()
)
result.success(answer)
throw AlreadyStarted()
}
scanner = if (barcodeScannerOptions != null) {
BarcodeScanning.getClient(barcodeScannerOptions)
} else {
val facing: Int = call.argument<Int>("facing") ?: 0
val ratio: Int? = call.argument<Int>("ratio")
val torch: Boolean = call.argument<Boolean>("torch") ?: false
val formats: List<Int>? = call.argument<List<Int>>("formats")
// val analyzerWidth = call.argument<Int>("ratio")
// val analyzeRHEIG = call.argument<Int>("ratio")
if (formats != null) {
val formatsList: MutableList<Int> = mutableListOf()
for (index in formats) {
formatsList.add(BarcodeFormats.values()[index].intValue)
}
scanner = if (formatsList.size == 1) {
BarcodeScanning.getClient(
BarcodeScannerOptions.Builder().setBarcodeFormats(formatsList.first())
.build()
)
} else {
BarcodeScanning.getClient(
BarcodeScannerOptions.Builder().setBarcodeFormats(
formatsList.first(),
*formatsList.subList(1, formatsList.size).toIntArray()
).build()
)
}
}
BarcodeScanning.getClient()
}
val future = ProcessCameraProvider.getInstance(activity)
val executor = ContextCompat.getMainExecutor(activity)
val cameraProviderFuture = ProcessCameraProvider.getInstance(activity)
val executor = ContextCompat.getMainExecutor(activity)
future.addListener({
cameraProvider = future.get()
if (cameraProvider == null) {
result.error("cameraProvider", "cameraProvider is null", null)
return@addListener
}
cameraProvider!!.unbindAll()
textureEntry = textureRegistry.createSurfaceTexture()
if (textureEntry == null) {
result.error("textureEntry", "textureEntry is null", null)
return@addListener
}
// Preview
val surfaceProvider = Preview.SurfaceProvider { request ->
val texture = textureEntry!!.surfaceTexture()
texture.setDefaultBufferSize(
request.resolution.width,
request.resolution.height
)
val surface = Surface(texture)
request.provideSurface(surface, executor) { }
}
cameraProviderFuture.addListener({
cameraProvider = cameraProviderFuture.get()
if (cameraProvider == null) {
throw CameraError()
}
cameraProvider!!.unbindAll()
textureEntry = textureRegistry.createSurfaceTexture()
// Preview
val surfaceProvider = Preview.SurfaceProvider { request ->
val texture = textureEntry!!.surfaceTexture()
texture.setDefaultBufferSize(
request.resolution.width,
request.resolution.height
)
// Build the preview to be shown on the Flutter texture
val previewBuilder = Preview.Builder()
if (ratio != null) {
previewBuilder.setTargetAspectRatio(ratio)
}
preview = previewBuilder.build().apply { setSurfaceProvider(surfaceProvider) }
val surface = Surface(texture)
request.provideSurface(surface, executor) { }
}
// Build the analyzer to be passed on to MLKit
val analysisBuilder = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
if (ratio != null) {
analysisBuilder.setTargetAspectRatio(ratio)
}
// Build the preview to be shown on the Flutter texture
val previewBuilder = Preview.Builder()
preview = previewBuilder.build().apply { setSurfaceProvider(surfaceProvider) }
// Build the analyzer to be passed on to MLKit
val analysisBuilder = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
// analysisBuilder.setTargetResolution(Size(1440, 1920))
val analysis = analysisBuilder.build().apply { setAnalyzer(executor, analyzer) }
val analysis = analysisBuilder.build().apply { setAnalyzer(executor, captureOutput) }
// Select the correct camera
val selector =
if (facing == 0) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA
camera = cameraProvider!!.bindToLifecycle(
activity as LifecycleOwner,
cameraPosition,
preview,
analysis
)
camera = cameraProvider!!.bindToLifecycle(
activity as LifecycleOwner,
selector,
preview,
analysis
)
// Register the torch listener
camera!!.cameraInfo.torchState.observe(activity) { state ->
// TorchState.OFF = 0; TorchState.ON = 1
torchStateCallback(state)
}
val analysisSize = analysis.resolutionInfo?.resolution ?: Size(0, 0)
val previewSize = preview!!.resolutionInfo?.resolution ?: Size(0, 0)
Log.i("LOG", "Analyzer: $analysisSize")
Log.i("LOG", "Preview: $previewSize")
// val analysisSize = analysis.resolutionInfo?.resolution ?: Size(0, 0)
// val previewSize = preview!!.resolutionInfo?.resolution ?: Size(0, 0)
// Log.i("LOG", "Analyzer: $analysisSize")
// Log.i("LOG", "Preview: $previewSize")
if (camera == null) {
result.error("camera", "camera is null", null)
return@addListener
}
// Enable torch if provided
camera!!.cameraControl.enableTorch(torch)
// Register the torch listener
camera!!.cameraInfo.torchState.observe(activity) { state ->
// TorchState.OFF = 0; TorchState.ON = 1
sink?.success(mapOf("name" to "torchState", "data" to state))
}
val resolution = preview!!.resolutionInfo!!.resolution
val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0
val width = resolution.width.toDouble()
val height = resolution.height.toDouble()
// Enable torch if provided
camera!!.cameraControl.enableTorch(torch)
val resolution = preview!!.resolutionInfo!!.resolution
val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0
val width = resolution.width.toDouble()
val height = resolution.height.toDouble()
val size = if (portrait) mapOf(
"width" to width,
"height" to height
) else mapOf("width" to height, "height" to width)
val answer = mapOf(
"textureId" to textureEntry!!.id(),
"size" to size,
"torchable" to camera!!.cameraInfo.hasFlashUnit()
mobileScannerStartedCallback(
MobileScannerStartParameters(
if (portrait) width else height,
if (portrait) height else width,
camera!!.cameraInfo.hasFlashUnit(),
textureEntry!!.id()
)
result.success(answer)
}, executor)
}
}
private fun toggleTorch(call: MethodCall, result: MethodChannel.Result) {
if (camera == null) {
result.error(TAG, "Called toggleTorch() while stopped!", null)
return
}
camera!!.cameraControl.enableTorch(call.arguments == 1)
result.success(null)
}
// private fun switchAnalyzeMode(call: MethodCall, result: MethodChannel.Result) {
// analyzeMode = call.arguments as Int
// result.success(null)
// }
private fun analyzeImage(call: MethodCall, result: MethodChannel.Result) {
val uri = Uri.fromFile(File(call.arguments.toString()))
val inputImage = InputImage.fromFilePath(activity, uri)
var barcodeFound = false
scanner.process(inputImage)
.addOnSuccessListener { barcodes ->
for (barcode in barcodes) {
barcodeFound = true
sink?.success(mapOf("name" to "barcode", "data" to barcode.data))
}
}
.addOnFailureListener { e ->
Log.e(TAG, e.message, e)
result.error(TAG, e.message, e)
}
.addOnCompleteListener { result.success(barcodeFound) }
)
}, executor)
}
private fun stop(result: MethodChannel.Result) {
/**
* Stop barcode scanning.
*/
fun stop() {
if (camera == null && preview == null) {
result.error(TAG, "Called stop() while already stopped!", null)
return
throw AlreadyStopped()
}
val owner = activity as LifecycleOwner
... ... @@ -367,81 +274,43 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
cameraProvider?.unbindAll()
textureEntry?.release()
// analyzeMode = AnalyzeMode.NONE
camera = null
preview = null
textureEntry = null
cameraProvider = null
}
result.success(null)
/**
* Toggles the flash light on or off.
*/
fun toggleTorch(enableTorch: Boolean) {
if (camera == null) {
throw TorchWhenStopped()
}
camera!!.cameraControl.enableTorch(enableTorch)
}
/**
* Analyze a single image.
*/
fun analyzeImage(image: Uri, analyzerCallback: AnalyzerCallback) {
val inputImage = InputImage.fromFilePath(activity, image)
scanner.process(inputImage)
.addOnSuccessListener { barcodes ->
val barcodeMap = barcodes.map { barcode -> barcode.data }
if (barcodeMap.isNotEmpty()) {
analyzerCallback(barcodeMap)
} else {
analyzerCallback(null)
}
}
.addOnFailureListener { e ->
mobileScannerErrorCallback(
e.localizedMessage ?: e.toString()
)
}
}
private val Barcode.data: Map<String, Any?>
get() = mapOf(
"corners" to cornerPoints?.map { corner -> corner.data }, "format" to format,
"rawBytes" to rawBytes, "rawValue" to rawValue, "type" to valueType,
"calendarEvent" to calendarEvent?.data, "contactInfo" to contactInfo?.data,
"driverLicense" to driverLicense?.data, "email" to email?.data,
"geoPoint" to geoPoint?.data, "phone" to phone?.data, "sms" to sms?.data,
"url" to url?.data, "wifi" to wifi?.data, "displayValue" to displayValue
)
private val Point.data: Map<String, Double>
get() = mapOf("x" to x.toDouble(), "y" to y.toDouble())
private val Barcode.CalendarEvent.data: Map<String, Any?>
get() = mapOf(
"description" to description, "end" to end?.rawValue, "location" to location,
"organizer" to organizer, "start" to start?.rawValue, "status" to status,
"summary" to summary
)
private val Barcode.ContactInfo.data: Map<String, Any?>
get() = mapOf(
"addresses" to addresses.map { address -> address.data },
"emails" to emails.map { email -> email.data }, "name" to name?.data,
"organization" to organization, "phones" to phones.map { phone -> phone.data },
"title" to title, "urls" to urls
)
private val Barcode.Address.data: Map<String, Any?>
get() = mapOf(
"addressLines" to addressLines.map { addressLine -> addressLine.toString() },
"type" to type
)
private val Barcode.PersonName.data: Map<String, Any?>
get() = mapOf(
"first" to first, "formattedName" to formattedName, "last" to last,
"middle" to middle, "prefix" to prefix, "pronunciation" to pronunciation,
"suffix" to suffix
)
private val Barcode.DriverLicense.data: Map<String, Any?>
get() = mapOf(
"addressCity" to addressCity, "addressState" to addressState,
"addressStreet" to addressStreet, "addressZip" to addressZip, "birthDate" to birthDate,
"documentType" to documentType, "expiryDate" to expiryDate, "firstName" to firstName,
"gender" to gender, "issueDate" to issueDate, "issuingCountry" to issuingCountry,
"lastName" to lastName, "licenseNumber" to licenseNumber, "middleName" to middleName
)
private val Barcode.Email.data: Map<String, Any?>
get() = mapOf("address" to address, "body" to body, "subject" to subject, "type" to type)
private val Barcode.GeoPoint.data: Map<String, Any?>
get() = mapOf("latitude" to lat, "longitude" to lng)
private val Barcode.Phone.data: Map<String, Any?>
get() = mapOf("number" to number, "type" to type)
private val Barcode.Sms.data: Map<String, Any?>
get() = mapOf("message" to message, "phoneNumber" to phoneNumber)
private val Barcode.UrlBookmark.data: Map<String, Any?>
get() = mapOf("title" to title, "url" to url)
private val Barcode.WiFi.data: Map<String, Any?>
get() = mapOf("encryptionType" to encryptionType, "password" to password, "ssid" to ssid)
}
\ No newline at end of file
}
... ...
package dev.steenbakker.mobile_scanner
import androidx.annotation.NonNull
import android.net.Uri
import androidx.camera.core.CameraSelector
import androidx.camera.core.ExperimentalGetImage
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
import dev.steenbakker.mobile_scanner.objects.BarcodeFormats
import dev.steenbakker.mobile_scanner.objects.DetectionSpeed
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import java.io.File
/** MobileScannerPlugin */
class MobileScannerPlugin : FlutterPlugin, ActivityAware {
private var flutter: FlutterPlugin.FlutterPluginBinding? = null
private var activity: ActivityPluginBinding? = null
class MobileScannerPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCallHandler {
private var flutterPluginBinding: FlutterPlugin.FlutterPluginBinding? = null
private var activityPluginBinding: ActivityPluginBinding? = null
private var handler: MobileScanner? = null
private var method: MethodChannel? = null
private var event: EventChannel? = null
override fun onAttachedToEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
this.flutter = binding
private lateinit var barcodeHandler: BarcodeHandler
private var permissionResult: MethodChannel.Result? = null
private var analyzerResult: MethodChannel.Result? = null
private val permissionCallback: PermissionCallback = {hasPermission: Boolean ->
permissionResult?.success(hasPermission)
permissionResult = null
}
private val callback: MobileScannerCallback = { barcodes: List<Map<String, Any?>>, image: ByteArray? ->
if (image != null) {
barcodeHandler.publishEvent(mapOf(
"name" to "barcode",
"data" to barcodes,
"image" to image
))
} else {
barcodeHandler.publishEvent(mapOf(
"name" to "barcode",
"data" to barcodes
))
}
}
private val analyzerCallback: AnalyzerCallback = { barcodes: List<Map<String, Any?>>?->
if (barcodes != null) {
barcodeHandler.publishEvent(mapOf(
"name" to "barcode",
"data" to barcodes
))
analyzerResult?.success(true)
} else {
analyzerResult?.success(false)
}
analyzerResult = null
}
private val errorCallback: MobileScannerErrorCallback = {error: String ->
barcodeHandler.publishEvent(mapOf(
"name" to "error",
"data" to error,
))
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
this.flutter = null
private val torchStateCallback: TorchStateCallback = {state: Int ->
barcodeHandler.publishEvent(mapOf("name" to "torchState", "data" to state))
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding
handler = MobileScanner(activity!!.activity, flutter!!.textureRegistry)
method = MethodChannel(flutter!!.binaryMessenger, "dev.steenbakker.mobile_scanner/scanner/method")
event = EventChannel(flutter!!.binaryMessenger, "dev.steenbakker.mobile_scanner/scanner/event")
method!!.setMethodCallHandler(handler)
event!!.setStreamHandler(handler)
activity!!.addRequestPermissionsResultListener(handler!!)
@ExperimentalGetImage
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
if (handler == null) {
result.error("MobileScanner", "Called ${call.method} before initializing.", null)
return
}
when (call.method) {
"state" -> result.success(handler!!.hasCameraPermission())
"request" -> requestPermission(result)
"start" -> start(call, result)
"torch" -> toggleTorch(call, result)
"stop" -> stop(result)
"analyzeImage" -> analyzeImage(call, result)
else -> result.notImplemented()
}
}
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
method = MethodChannel(binding.binaryMessenger, "dev.steenbakker.mobile_scanner/scanner/method")
method!!.setMethodCallHandler(this)
barcodeHandler = BarcodeHandler(binding)
this.flutterPluginBinding = binding
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
this.flutterPluginBinding = null
}
override fun onAttachedToActivity(activityPluginBinding: ActivityPluginBinding) {
handler = MobileScanner(activityPluginBinding.activity, flutterPluginBinding!!.textureRegistry, callback, errorCallback
)
activityPluginBinding.addRequestPermissionsResultListener(handler!!)
this.activityPluginBinding = activityPluginBinding
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
... ... @@ -38,16 +113,117 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware {
}
override fun onDetachedFromActivity() {
activity!!.removeRequestPermissionsResultListener(handler!!)
event!!.setStreamHandler(null)
activityPluginBinding!!.removeRequestPermissionsResultListener(handler!!)
method!!.setMethodCallHandler(null)
event = null
method = null
handler = null
activity = null
activityPluginBinding = null
}
override fun onDetachedFromActivityForConfigChanges() {
onDetachedFromActivity()
}
private fun requestPermission(result: MethodChannel.Result) {
permissionResult = result
handler!!.requestPermission(permissionCallback)
}
@ExperimentalGetImage
private fun start(call: MethodCall, result: MethodChannel.Result) {
val torch: Boolean = call.argument<Boolean>("torch") ?: false
val facing: Int = call.argument<Int>("facing") ?: 0
val formats: List<Int>? = call.argument<List<Int>>("formats")
val returnImage: Boolean = call.argument<Boolean>("returnImage") ?: false
val speed: Int = call.argument<Int>("speed") ?: 1
val timeout: Int = call.argument<Int>("timeout") ?: 250
var barcodeScannerOptions: BarcodeScannerOptions? = null
if (formats != null) {
val formatsList: MutableList<Int> = mutableListOf()
for (index in formats) {
formatsList.add(BarcodeFormats.values()[index].intValue)
}
barcodeScannerOptions = if (formatsList.size == 1) {
BarcodeScannerOptions.Builder().setBarcodeFormats(formatsList.first())
.build()
} else {
BarcodeScannerOptions.Builder().setBarcodeFormats(
formatsList.first(),
*formatsList.subList(1, formatsList.size).toIntArray()
).build()
}
}
val position =
if (facing == 0) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA
val detectionSpeed: DetectionSpeed = DetectionSpeed.values().first { it.intValue == speed}
try {
handler!!.start(barcodeScannerOptions, returnImage, position, torch, detectionSpeed, torchStateCallback, mobileScannerStartedCallback = {
result.success(mapOf(
"textureId" to it.id,
"size" to mapOf("width" to it.width, "height" to it.height),
"torchable" to it.hasFlashUnit
))
},
timeout.toLong())
} catch (e: AlreadyStarted) {
result.error(
"MobileScanner",
"Called start() while already started",
null
)
} catch (e: NoCamera) {
result.error(
"MobileScanner",
"No camera found or failed to open camera!",
null
)
} catch (e: TorchError) {
result.error(
"MobileScanner",
"Error occurred when setting torch!",
null
)
} catch (e: CameraError) {
result.error(
"MobileScanner",
"Error occurred when setting up camera!",
null
)
} catch (e: Exception) {
result.error(
"MobileScanner",
"Unknown error occurred..",
null
)
}
}
private fun stop(result: MethodChannel.Result) {
try {
handler!!.stop()
result.success(null)
} catch (e: AlreadyStopped) {
result.error("MobileScanner", "Called stop() while already stopped!", null)
}
}
private fun analyzeImage(call: MethodCall, result: MethodChannel.Result) {
analyzerResult = result
val uri = Uri.fromFile(File(call.arguments.toString()))
handler!!.analyzeImage(uri, analyzerCallback)
}
private fun toggleTorch(call: MethodCall, result: MethodChannel.Result) {
try {
handler!!.toggleTorch(call.arguments == 1)
result.success(null)
} catch (e: AlreadyStopped) {
result.error("MobileScanner", "Called toggleTorch() while stopped!", null)
}
}
}
... ...
package dev.steenbakker.mobile_scanner
import android.graphics.ImageFormat
import android.graphics.Point
import android.graphics.Rect
import android.graphics.YuvImage
import android.media.Image
import com.google.mlkit.vision.barcode.common.Barcode
import java.io.ByteArrayOutputStream
fun Image.toByteArray(): ByteArray {
val yBuffer = planes[0].buffer // Y
val vuBuffer = planes[2].buffer // VU
val ySize = yBuffer.remaining()
val vuSize = vuBuffer.remaining()
val nv21 = ByteArray(ySize + vuSize)
yBuffer.get(nv21, 0, ySize)
vuBuffer.get(nv21, ySize, vuSize)
val yuvImage = YuvImage(nv21, ImageFormat.NV21, this.width, this.height, null)
val out = ByteArrayOutputStream()
yuvImage.compressToJpeg(Rect(0, 0, yuvImage.width, yuvImage.height), 50, out)
return out.toByteArray()
}
val Barcode.data: Map<String, Any?>
get() = mapOf(
"corners" to cornerPoints?.map { corner -> corner.data }, "format" to format,
"rawBytes" to rawBytes, "rawValue" to rawValue, "type" to valueType,
"calendarEvent" to calendarEvent?.data, "contactInfo" to contactInfo?.data,
"driverLicense" to driverLicense?.data, "email" to email?.data,
"geoPoint" to geoPoint?.data, "phone" to phone?.data, "sms" to sms?.data,
"url" to url?.data, "wifi" to wifi?.data, "displayValue" to displayValue
)
private val Point.data: Map<String, Double>
get() = mapOf("x" to x.toDouble(), "y" to y.toDouble())
private val Barcode.CalendarEvent.data: Map<String, Any?>
get() = mapOf(
"description" to description, "end" to end?.rawValue, "location" to location,
"organizer" to organizer, "start" to start?.rawValue, "status" to status,
"summary" to summary
)
private val Barcode.ContactInfo.data: Map<String, Any?>
get() = mapOf(
"addresses" to addresses.map { address -> address.data },
"emails" to emails.map { email -> email.data }, "name" to name?.data,
"organization" to organization, "phones" to phones.map { phone -> phone.data },
"title" to title, "urls" to urls
)
private val Barcode.Address.data: Map<String, Any?>
get() = mapOf(
"addressLines" to addressLines.map { addressLine -> addressLine.toString() },
"type" to type
)
private val Barcode.PersonName.data: Map<String, Any?>
get() = mapOf(
"first" to first, "formattedName" to formattedName, "last" to last,
"middle" to middle, "prefix" to prefix, "pronunciation" to pronunciation,
"suffix" to suffix
)
private val Barcode.DriverLicense.data: Map<String, Any?>
get() = mapOf(
"addressCity" to addressCity, "addressState" to addressState,
"addressStreet" to addressStreet, "addressZip" to addressZip, "birthDate" to birthDate,
"documentType" to documentType, "expiryDate" to expiryDate, "firstName" to firstName,
"gender" to gender, "issueDate" to issueDate, "issuingCountry" to issuingCountry,
"lastName" to lastName, "licenseNumber" to licenseNumber, "middleName" to middleName
)
private val Barcode.Email.data: Map<String, Any?>
get() = mapOf("address" to address, "body" to body, "subject" to subject, "type" to type)
private val Barcode.GeoPoint.data: Map<String, Any?>
get() = mapOf("latitude" to lat, "longitude" to lng)
private val Barcode.Phone.data: Map<String, Any?>
get() = mapOf("number" to number, "type" to type)
private val Barcode.Sms.data: Map<String, Any?>
get() = mapOf("message" to message, "phoneNumber" to phoneNumber)
private val Barcode.UrlBookmark.data: Map<String, Any?>
get() = mapOf("title" to title, "url" to url)
private val Barcode.WiFi.data: Map<String, Any?>
get() = mapOf("encryptionType" to encryptionType, "password" to password, "ssid" to ssid)
\ No newline at end of file
... ...
package dev.steenbakker.mobile_scanner.exceptions
internal class NoPermissionException : RuntimeException()
//internal class Exception(val reason: Reason) :
// java.lang.Exception("Mobile Scanner failed because $reason") {
//
// internal enum class Reason {
// noHardware, noPermissions, noBackCamera
// }
//}
\ No newline at end of file
package dev.steenbakker.mobile_scanner
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
package dev.steenbakker.mobile_scanner.objects
enum class BarcodeFormats(val intValue: Int) {
UNKNOWN(com.google.mlkit.vision.barcode.common.Barcode.FORMAT_UNKNOWN),
... ...
package dev.steenbakker.mobile_scanner.objects
enum class DetectionSpeed(val intValue: Int) {
NO_DUPLICATES(0),
NORMAL(1),
UNRESTRICTED(2)
}
\ No newline at end of file
... ...
package dev.steenbakker.mobile_scanner.objects
class MobileScannerStartParameters(
val width: Double = 0.0,
val height: Double,
val hasFlashUnit: Boolean,
val id: Long
)
\ No newline at end of file
... ...
buildscript {
ext.kotlin_version = '1.7.20'
ext.kotlin_version = '1.7.21'
repositories {
google()
mavenCentral()
... ...
... ... @@ -8,7 +8,7 @@ environment:
dependencies:
flutter:
sdk: flutter
image_picker: ^0.8.5+3
image_picker: ^0.8.6
mobile_scanner:
path: ../
... ...
//
// DetectionSpeed.swift
// mobile_scanner
//
// Created by Julian Steenbakker on 11/11/2022.
//
enum DetectionSpeed: Int {
case noDuplicates = 0
case normal = 1
case unrestricted = 2
}
... ...
... ... @@ -49,7 +49,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
super.init()
}
/// Check permissions for video
/// Check if we already have camera permission.
func checkPermission() -> Int {
let status = AVCaptureDevice.authorizationStatus(for: .video)
switch status {
... ... @@ -66,6 +66,44 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
func requestPermission(_ result: @escaping FlutterResult) {
AVCaptureDevice.requestAccess(for: .video, completionHandler: { result($0) })
}
/// Gets called when a new image is added to the buffer
public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
print("Failed to get image buffer from sample buffer.")
return
}
latestBuffer = imageBuffer
registry?.textureFrameAvailable(textureId)
if ((detectionSpeed == DetectionSpeed.normal || detectionSpeed == DetectionSpeed.noDuplicates) && i > 10 || detectionSpeed == DetectionSpeed.unrestricted) {
i = 0
let ciImage = latestBuffer.image
let image = VisionImage(image: ciImage)
image.orientation = imageOrientation(
deviceOrientation: UIDevice.current.orientation,
defaultOrientation: .portrait,
position: videoPosition
)
scanner.process(image) { [self] barcodes, error in
if (detectionSpeed == DetectionSpeed.noDuplicates) {
let newScannedBarcodes = barcodes?.map { barcode in
return barcode.rawValue
}
if (error == nil && barcodesString != nil && newScannedBarcodes != nil && barcodesString!.elementsEqual(newScannedBarcodes!)) {
return
} else {
barcodesString = newScannedBarcodes
}
}
mobileScannerCallback(barcodes, error, ciImage)
}
} else {
i+=1
}
}
/// Start scanning for barcodes
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
return MobileScannerStartParameters(width: Double(dimensions.height), height: Double(dimensions.width), hasTorch: device.hasTorch, textureId: textureId)
}
struct MobileScannerStartParameters {
var width: Double = 0.0
var height: Double = 0.0
var hasTorch = false
var textureId: Int64 = 0
}
/// Stop scanning for barcodes
func stop() throws {
if (device == nil) {
... ... @@ -192,43 +223,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
var barcodesString: Array<String?>?
/// Gets called when a new image is added to the buffer
public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
print("Failed to get image buffer from sample buffer.")
return
}
latestBuffer = imageBuffer
registry?.textureFrameAvailable(textureId)
if ((detectionSpeed == DetectionSpeed.normal || detectionSpeed == DetectionSpeed.noDuplicates) && i > 10 || detectionSpeed == DetectionSpeed.unrestricted) {
i = 0
let ciImage = latestBuffer.image
let image = VisionImage(image: ciImage)
image.orientation = imageOrientation(
deviceOrientation: UIDevice.current.orientation,
defaultOrientation: .portrait,
position: videoPosition
)
scanner.process(image) { [self] barcodes, error in
if (detectionSpeed == DetectionSpeed.noDuplicates) {
let newScannedBarcodes = barcodes?.map { barcode in
return barcode.rawValue
}
if (error == nil && barcodesString != nil && newScannedBarcodes != nil && barcodesString!.elementsEqual(newScannedBarcodes!)) {
return
} else {
barcodesString = newScannedBarcodes
}
}
mobileScannerCallback(barcodes, error, ciImage)
}
} else {
i+=1
}
}
// /// Convert image buffer to jpeg
// private func ciImageToJpeg(ciImage: CIImage) -> Data {
... ... @@ -270,6 +265,12 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
}
return Unmanaged<CVPixelBuffer>.passRetained(latestBuffer)
}
struct MobileScannerStartParameters {
var width: Double = 0.0
var height: Double = 0.0
var hasTorch = false
var textureId: Int64 = 0
}
}
... ...
... ... @@ -56,11 +56,11 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
/// Parses all parameters and starts the mobileScanner
private func start(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
// let ratio: Int = (call.arguments as! Dictionary<String, Any?>)["ratio"] as! Int
let torch: Bool = (call.arguments as! Dictionary<String, Any?>)["torch"] as? Bool ?? false
let facing: Int = (call.arguments as! Dictionary<String, Any?>)["facing"] as? Int ?? 1
let formats: Array<Int> = (call.arguments as! Dictionary<String, Any?>)["formats"] as? Array ?? []
let returnImage: Bool = (call.arguments as! Dictionary<String, Any?>)["returnImage"] as? Bool ?? false
let speed: Int = (call.arguments as! Dictionary<String, Any?>)["speed"] as? Int ?? 0
let formatList = formats.map { format in return BarcodeFormat(rawValue: format)}
var barcodeOptions: BarcodeScannerOptions? = nil
... ... @@ -75,10 +75,10 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
let position = facing == 0 ? AVCaptureDevice.Position.front : .back
let speed: DetectionSpeed = DetectionSpeed(rawValue: (call.arguments as! Dictionary<String, Any?>)["speed"] as? Int ?? 0)!
let detectionSpeed: DetectionSpeed = DetectionSpeed(rawValue: speed)!
do {
let parameters = try mobileScanner.start(barcodeScannerOptions: barcodeOptions, returnImage: returnImage, cameraPosition: position, torch: torch ? .on : .off, detectionSpeed: speed)
let parameters = try mobileScanner.start(barcodeScannerOptions: barcodeOptions, returnImage: returnImage, cameraPosition: position, torch: torch ? .on : .off, detectionSpeed: detectionSpeed)
result(["textureId": parameters.textureId, "size": ["width": parameters.width, "height": parameters.height], "torchable": parameters.hasTorch])
} catch MobileScannerError.alreadyStarted {
result(FlutterError(code: "MobileScanner",
... ... @@ -90,7 +90,7 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
details: nil))
} catch MobileScannerError.torchError(let error) {
result(FlutterError(code: "MobileScanner",
message: "Error occured when setting toch!",
message: "Error occured when setting torch!",
details: error))
} catch MobileScannerError.cameraError(let error) {
result(FlutterError(code: "MobileScanner",
... ... @@ -162,9 +162,3 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
}
}
}
enum DetectionSpeed: Int {
case noDuplicates = 0
case normal = 1
case unrestricted = 2
}
... ...
... ... @@ -4,7 +4,7 @@ enum DetectionSpeed {
/// barcode has been scanned.
noDuplicates,
/// Front facing camera.
/// The barcode scanner will wait
normal,
/// Back facing camera.
... ...
... ... @@ -13,11 +13,10 @@ import 'package:mobile_scanner/src/mobile_scanner_exception.dart';
class MobileScannerController {
MobileScannerController({
this.facing = CameraFacing.back,
this.detectionSpeed = DetectionSpeed.noDuplicates,
// this.ratio,
this.detectionSpeed = DetectionSpeed.normal,
this.detectionTimeoutMs = 250,
this.torchEnabled = false,
this.formats,
// this.autoResume = true,
this.returnImage = false,
this.onPermissionSet,
}) {
... ... @@ -39,11 +38,6 @@ class MobileScannerController {
/// Default: CameraFacing.back
final CameraFacing facing;
// /// Analyze the image in 4:3 or 16:9
// ///
// /// Only on Android
// final Ratio? ratio;
/// Enable or disable the torch (Flash) on start
///
/// Default: disabled
... ... @@ -62,6 +56,8 @@ class MobileScannerController {
/// WARNING: DetectionSpeed.unrestricted can cause memory issues on some devices
final DetectionSpeed detectionSpeed;
final int detectionTimeoutMs;
/// Sets the barcode stream
final StreamController<BarcodeCapture> _barcodesController =
StreamController.broadcast();
... ... @@ -97,10 +93,9 @@ class MobileScannerController {
cameraFacingState.value = cameraFacingOverride ?? facing;
arguments['facing'] = cameraFacingState.value.index;
// if (ratio != null) arguments['ratio'] = ratio;
arguments['torch'] = torchEnabled;
arguments['speed'] = detectionSpeed.index;
arguments['timeout'] = detectionTimeoutMs;
if (formats != null) {
if (Platform.isAndroid) {
... ... @@ -281,7 +276,7 @@ class MobileScannerController {
_barcodesController.add(
BarcodeCapture(
barcodes: parsed,
image: event['image'] as Uint8List,
image: event['image'] as Uint8List?,
),
);
break;
... ...