Julian Steenbakker

imp: apply camerax

... ... @@ -9,7 +9,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.0'
classpath 'com.android.tools.build:gradle:4.1.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
... ... @@ -41,10 +41,12 @@ android {
}
defaultConfig {
minSdkVersion 16
minSdkVersion 21
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.google.mlkit:barcode-scanning:17.0.2'
implementation 'com.google.mlkit:camera:16.0.0-beta3'
}
... ...
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="dev.steenbakker.mobile_scanner">
<uses-permission android:name="android.permission.CAMERA" />
</manifest>
... ...
package dev.steenbakker.mobile_scanner
import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
import android.content.pm.PackageManager
import android.util.Log
import android.view.Surface
import androidx.annotation.IntDef
import androidx.annotation.NonNull
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.common.InputImage
import io.flutter.plugin.common.*
import io.flutter.view.TextureRegistry
class MobileScanner(private val activity: Activity, private val textureRegistry: TextureRegistry)
: MethodChannel.MethodCallHandler, EventChannel.StreamHandler, PluginRegistry.RequestPermissionsResultListener {
companion object {
private const val REQUEST_CODE = 19930430
}
private var sink: EventChannel.EventSink? = null
private var listener: PluginRegistry.RequestPermissionsResultListener? = null
private var cameraProvider: ProcessCameraProvider? = null
private var camera: Camera? = null
private var textureEntry: TextureRegistry.SurfaceTextureEntry? = null
// @AnalyzeMode
// private var analyzeMode: Int = AnalyzeMode.NONE
@ExperimentalGetImage
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: MethodChannel.Result) {
when (call.method) {
"state" -> stateNative(result)
"request" -> requestNative(result)
"start" -> startNative(call, result)
"torch" -> torchNative(call, result)
"analyze" -> analyzeNative(call, result)
"stop" -> stopNative(result)
else -> result.notImplemented()
}
}
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
this.sink = events
}
override fun onCancel(arguments: Any?) {
sink = null
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>?, grantResults: IntArray?): Boolean {
return listener?.onRequestPermissionsResult(requestCode, permissions, grantResults) ?: false
}
private fun stateNative(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 requestNative(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)
}
@ExperimentalGetImage
private fun startNative(call: MethodCall, result: MethodChannel.Result) {
val future = ProcessCameraProvider.getInstance(activity)
val executor = ContextCompat.getMainExecutor(activity)
future.addListener({
cameraProvider = future.get()
textureEntry = textureRegistry.createSurfaceTexture()
val textureId = textureEntry!!.id()
// Preview
val surfaceProvider = Preview.SurfaceProvider { request ->
val resolution = request.resolution
val texture = textureEntry!!.surfaceTexture()
texture.setDefaultBufferSize(resolution.width, resolution.height)
val surface = Surface(texture)
request.provideSurface(surface, executor, { })
}
val preview = Preview.Builder().build().apply { setSurfaceProvider(surfaceProvider) }
// Analyzer
val analyzer = ImageAnalysis.Analyzer { imageProxy -> // YUV_420_888 format
// when (analyzeMode) {
// AnalyzeMode.BARCODE -> {
val mediaImage = imageProxy.image ?: return@Analyzer
val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
val scanner = BarcodeScanning.getClient()
scanner.process(inputImage)
.addOnSuccessListener { barcodes ->
for (barcode in barcodes) {
val event = mapOf("name" to "barcode", "data" to barcode.data)
sink?.success(event)
}
}
.addOnFailureListener { e -> Log.e(TAG, e.message, e) }
.addOnCompleteListener { imageProxy.close() }
// }
// else -> imageProxy.close()
// }
}
val analysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build().apply { setAnalyzer(executor, analyzer) }
// Bind to lifecycle.
val owner = activity as LifecycleOwner
val selector =
if (call.arguments == 0) CameraSelector.DEFAULT_FRONT_CAMERA
else CameraSelector.DEFAULT_BACK_CAMERA
camera = cameraProvider!!.bindToLifecycle(owner, selector, preview, analysis)
camera!!.cameraInfo.torchState.observe(owner, { state ->
// TorchState.OFF = 0; TorchState.ON = 1
val event = mapOf("name" to "torchState", "data" to state)
sink?.success(event)
})
// TODO: seems there's not a better way to get the final resolution
@SuppressLint("RestrictedApi")
val resolution = preview.attachedSurfaceResolution!!
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 textureId, "size" to size, "torchable" to camera!!.torchable)
result.success(answer)
}, executor)
}
private fun torchNative(call: MethodCall, result: MethodChannel.Result) {
val state = call.arguments == 1
camera!!.cameraControl.enableTorch(state)
result.success(null)
}
private fun analyzeNative(call: MethodCall, result: MethodChannel.Result) {
// analyzeMode = call.arguments as Int
result.success(null)
}
private fun stopNative(result: MethodChannel.Result) {
val owner = activity as LifecycleOwner
camera!!.cameraInfo.torchState.removeObservers(owner)
cameraProvider!!.unbindAll()
textureEntry!!.release()
camera = null
textureEntry = null
cameraProvider = null
result.success(null)
}
}
//
//
//@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 androidx.annotation.NonNull
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
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.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
/** MobileScannerPlugin */
class MobileScannerPlugin: FlutterPlugin, MethodCallHandler {
/// The MethodChannel that will the communication between Flutter and native Android
///
/// This local reference serves to register the plugin with the Flutter Engine and unregister it
/// when the Flutter Engine is detached from the Activity
private lateinit var channel : MethodChannel
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "mobile_scanner")
channel.setMethodCallHandler(this)
}
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
if (call.method == "getPlatformVersion") {
result.success("Android ${android.os.Build.VERSION.RELEASE}")
} else {
result.notImplemented()
/** CameraXPlugin */
class MobileScannerPlugin : FlutterPlugin, ActivityAware {
private var flutter: FlutterPlugin.FlutterPluginBinding? = null
private var activity: 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
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
this.flutter = null
}
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!!)
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
onAttachedToActivity(binding)
}
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
override fun onDetachedFromActivity() {
activity!!.removeRequestPermissionsResultListener(handler!!)
event!!.setStreamHandler(null)
method!!.setMethodCallHandler(null)
event = null
method = null
handler = null
activity = null
}
override fun onDetachedFromActivityForConfigChanges() {
onDetachedFromActivity()
}
}
... ...
package dev.steenbakker.mobile_scanner
import android.graphics.Point
import androidx.camera.core.Camera
import androidx.camera.core.ImageProxy
import com.google.mlkit.vision.barcode.common.Barcode
val Any.TAG: String
get() = javaClass.simpleName
val Camera.torchable: Boolean
get() = cameraInfo.hasFlashUnit()
//
//val ImageProxy.yuv: ByteArray
// get() {
// val ySize = y.buffer.remaining()
// val uSize = u.buffer.remaining()
// val vSize = v.buffer.remaining()
//
// val size = ySize + uSize + vSize
// val data = ByteArray(size)
//
// var offset = 0
// y.buffer.get(data, offset, ySize)
// offset += ySize
// u.buffer.get(data, offset, uSize)
// offset += uSize
// v.buffer.get(data, offset, vSize)
//
// return data
// }
//
//val ImageProxy.nv21: ByteArray
// get() {
// if (BuildConfig.DEBUG) {
// if (y.pixelStride != 1 || u.rowStride != v.rowStride || u.pixelStride != v.pixelStride) {
// error("Assertion failed")
// }
// }
//
// val ySize = width * height
// val uvSize = ySize / 2
// val size = ySize + uvSize
// val data = ByteArray(size)
//
// var offset = 0
// // Y Plane
// if (y.rowStride == width) {
// y.buffer.get(data, offset, ySize)
// offset += ySize
// } else {
// for (row in 0 until height) {
// y.buffer.get(data, offset, width)
// offset += width
// }
//
// if (BuildConfig.DEBUG && offset != ySize) {
// error("Assertion failed")
// }
// }
// // U,V Planes
// if (v.rowStride == width && v.pixelStride == 2) {
// if (BuildConfig.DEBUG && v.size != uvSize - 1) {
// error("Assertion failed")
// }
//
// v.buffer.get(data, offset, 1)
// offset += 1
// u.buffer.get(data, offset, u.size)
// if (BuildConfig.DEBUG) {
// val value = v.buffer.get()
// if (data[offset] != value) {
// error("Assertion failed")
// }
// }
// } else {
// for (row in 0 until height / 2)
// for (col in 0 until width / 2) {
// val index = row * v.rowStride + col * v.pixelStride
// data[offset++] = v.buffer.get(index)
// data[offset++] = u.buffer.get(index)
// }
//
// if (BuildConfig.DEBUG && offset != size) {
// error("Assertion failed")
// }
// }
//
// return data
// }
val ImageProxy.PlaneProxy.size
get() = buffer.remaining()
val ImageProxy.y: ImageProxy.PlaneProxy
get() = planes[0]
val ImageProxy.u: ImageProxy.PlaneProxy
get() = planes[1]
val ImageProxy.v: ImageProxy.PlaneProxy
get() = planes[2]
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)
val Point.data: Map<String, Double>
get() = mapOf("x" to x.toDouble(), "y" to y.toDouble())
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)
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)
val Barcode.Address.data: Map<String, Any?>
get() = mapOf("addressLines" to addressLines, "type" to type)
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)
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)
val Barcode.Email.data: Map<String, Any?>
get() = mapOf("address" to address, "body" to body, "subject" to subject, "type" to type)
val Barcode.GeoPoint.data: Map<String, Any?>
get() = mapOf("latitude" to lat, "longitude" to lng)
val Barcode.Phone.data: Map<String, Any?>
get() = mapOf("number" to number, "type" to type)
val Barcode.Sms.data: Map<String, Any?>
get() = mapOf("message" to message, "phoneNumber" to phoneNumber)
val Barcode.UrlBookmark.data: Map<String, Any?>
get() = mapOf("title" to title, "url" to url)
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()
\ No newline at end of file
... ...
package dev.steenbakker.mobile_scanner.old
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
import java.util.ArrayList
enum class BarcodeFormats(val intValue: Int) {
ALL_FORMATS(com.google.mlkit.vision.barcode.common.Barcode.FORMAT_ALL_FORMATS), CODE_128(com.google.mlkit.vision.barcode.common.Barcode.FORMAT_CODE_128), CODE_39(
com.google.mlkit.vision.barcode.common.Barcode.FORMAT_CODE_39
),
CODE_93(com.google.mlkit.vision.barcode.common.Barcode.FORMAT_CODE_93), CODABAR(com.google.mlkit.vision.barcode.common.Barcode.FORMAT_CODABAR), DATA_MATRIX(
com.google.mlkit.vision.barcode.common.Barcode.FORMAT_DATA_MATRIX
),
EAN_13(com.google.mlkit.vision.barcode.common.Barcode.FORMAT_EAN_13), EAN_8(com.google.mlkit.vision.barcode.common.Barcode.FORMAT_EAN_8), ITF(
com.google.mlkit.vision.barcode.common.Barcode.FORMAT_ITF
),
QR_CODE(com.google.mlkit.vision.barcode.common.Barcode.FORMAT_QR_CODE), UPC_A(com.google.mlkit.vision.barcode.common.Barcode.FORMAT_UPC_A), UPC_E(
com.google.mlkit.vision.barcode.common.Barcode.FORMAT_UPC_E
),
PDF417(com.google.mlkit.vision.barcode.common.Barcode.FORMAT_PDF417), AZTEC(com.google.mlkit.vision.barcode.common.Barcode.FORMAT_AZTEC);
companion object {
private var formatsMap: MutableMap<String, Int>? = null
/**
* Return the integer value resuling from OR-ing all of the values
* of the supplied strings.
*
*
* Note that if ALL_FORMATS is defined as well as other values, ALL_FORMATS
* will be ignored (following how it would work with just OR-ing the ints).
*
* @param strings - list of strings representing the various formats
* @return integer value corresponding to OR of all the values.
*/
fun intFromStringList(strings: List<String>?): Int {
if (strings == null) return ALL_FORMATS.intValue
var `val` = 0
for (string in strings) {
val asInt = formatsMap!![string]
if (asInt != null) {
`val` = `val` or asInt
}
}
return `val`
}
fun optionsFromStringList(strings: List<String>?): BarcodeScannerOptions {
if (strings == null) {
return BarcodeScannerOptions.Builder().setBarcodeFormats(ALL_FORMATS.intValue)
.build()
}
val ints: MutableList<Int> = ArrayList(strings.size)
run {
var i = 0
val l = strings.size
while (i < l) {
val integer =
formatsMap!![strings[i]]
if (integer != null) {
ints.add(integer)
}
++i
}
}
if (ints.size == 0) {
return BarcodeScannerOptions.Builder().setBarcodeFormats(ALL_FORMATS.intValue)
.build()
}
if (ints.size == 1) {
return BarcodeScannerOptions.Builder().setBarcodeFormats(ints[0]).build()
}
val first = ints[0]
val rest = IntArray(ints.size - 1)
var i = 0
for (e in ints.subList(1, ints.size)) {
rest[i++] = e
}
return BarcodeScannerOptions.Builder()
.setBarcodeFormats(first, *rest).build()
}
init {
val values = values()
// formatsMap =
// HashMap<String, Int>(com.github.rmtmckenzie.qrmobilevision.values.size * 4 / 3)
// for (value in com.github.rmtmckenzie.qrmobilevision.values) {
// formatsMap!![com.github.rmtmckenzie.qrmobilevision.value.name] =
// com.github.rmtmckenzie.qrmobilevision.value.intValue
// }
}
}
}
\ No newline at end of file
... ...
package dev.steenbakker.mobile_scanner.old
import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.pm.PackageManager
import android.util.Log
import android.view.Surface
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
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.common.InputImage
import dev.steenbakker.mobile_scanner.exceptions.NoPermissionException
import io.flutter.plugin.common.MethodChannel
import io.flutter.view.TextureRegistry
import java.io.IOException
internal class MobileScanner(
private val context: Activity,
private val texture: TextureRegistry
) {
private var cameraProvider: ProcessCameraProvider? = null
private var camera: Camera? = null
private var textureEntry: TextureRegistry.SurfaceTextureEntry? = null
@ExperimentalGetImage
@Throws(IOException::class, NoPermissionException::class, Exception::class)
fun start(
result: MethodChannel.Result,
options: BarcodeScannerOptions?,
channel: MethodChannel
) {
if (!hasCameraHardware(context)) {
throw Exception(Exception.Reason.noHardware)
}
if (!checkCameraPermission(context)) {
throw NoPermissionException()
}
textureEntry = texture.createSurfaceTexture()
val textureId = textureEntry!!.id()
val future = ProcessCameraProvider.getInstance(context)
val executor = ContextCompat.getMainExecutor(context)
future.addListener({
// Preview
val surfaceProvider = Preview.SurfaceProvider { request ->
val resolution = request.resolution
val texture = textureEntry!!.surfaceTexture()
texture.setDefaultBufferSize(resolution.width, resolution.height)
val surface = Surface(texture)
request.provideSurface(surface, executor, { })
}
val preview = Preview.Builder().build().apply { setSurfaceProvider(surfaceProvider) }
val analyzer = ImageAnalysis.Analyzer { imageProxy -> // YUV_420_888 format
val mediaImage = imageProxy.image ?: return@Analyzer
val inputImage =
InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
val scanner = if (options != null) {
BarcodeScanning.getClient(options)
} else {
BarcodeScanning.getClient()
}
scanner.process(inputImage)
.addOnSuccessListener { barcodes ->
val barcodeList: MutableList<Map<String, Any?>> = mutableListOf()
for (barcode in barcodes) {
barcodeList.add(
mapOf(
"value" to barcode.rawValue,
"bytes" to barcode.rawBytes
)
)
}
channel.invokeMethod("qrRead", barcodeList)
}
.addOnFailureListener { e -> Log.e("Camera", e.message, e) }
.addOnCompleteListener { imageProxy.close() }
}
val analysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build().apply { setAnalyzer(executor, analyzer) }
// Bind to lifecycle.
val owner = context as LifecycleOwner
// val selector =
// if (call.arguments == 0) CameraSelector.DEFAULT_FRONT_CAMERA
// else CameraSelector.DEFAULT_BACK_CAMERA
camera = cameraProvider!!.bindToLifecycle(
owner,
CameraSelector.DEFAULT_BACK_CAMERA,
preview,
analysis
)
camera!!.cameraInfo.torchState.observe(owner, { state ->
// TorchState.OFF = 0; TorchState.ON = 1
// val event = mapOf("name" to "torchState", "data" to state)
// sink?.success(event)
})
// TODO: seems there's not a better way to get the final resolution
@SuppressLint("RestrictedApi")
val resolution = preview.attachedSurfaceResolution!!
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)
result.success(mapOf("textureId" to textureId, "size" to size))
}, executor)
}
fun stop() {
val owner = context as LifecycleOwner
camera!!.cameraInfo.torchState.removeObservers(owner)
cameraProvider!!.unbindAll()
textureEntry!!.release()
camera = null
textureEntry = null
cameraProvider = null
}
private fun hasCameraHardware(context: Context): Boolean {
return context.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)
}
private fun checkCameraPermission(context: Context): Boolean {
val permissions = arrayOf(Manifest.permission.CAMERA)
val res = context.checkCallingOrSelfPermission(permissions[0])
return res == PackageManager.PERMISSION_GRANTED
}
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.old
import android.Manifest
import android.app.Activity
import android.content.pm.PackageManager
import android.util.Log
import androidx.annotation.NonNull
import androidx.camera.core.ExperimentalGetImage
import androidx.core.app.ActivityCompat
import dev.steenbakker.mobile_scanner.exceptions.NoPermissionException
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 io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener
import io.flutter.view.TextureRegistry
import java.io.IOException
/** MobileScannerPlugin */
class MobileScannerPlugin: FlutterPlugin, MethodCallHandler, ActivityAware,
RequestPermissionsResultListener, EventChannel.StreamHandler {
private lateinit var textures: TextureRegistry
private lateinit var channel : MethodChannel
private lateinit var event : EventChannel
private var activity: Activity? = null
private var waitingForPermissionResult = false
private var sink: EventChannel.EventSink? = null
private var mobileScanner: MobileScanner? = null
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity
}
override fun onDetachedFromActivityForConfigChanges() {
onDetachedFromActivity()
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
onAttachedToActivity(binding)
}
override fun onDetachedFromActivity() {
activity = null
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
textures = flutterPluginBinding.textureRegistry
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "dev.steenbakker.mobile_scanner/scanner/method")
event = EventChannel(flutterPluginBinding.binaryMessenger, "dev.steenbakker.mobile_scanner/scanner/event")
channel.setMethodCallHandler(this)
event.setStreamHandler(this)
}
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
this.sink = events
}
override fun onCancel(arguments: Any?) {
sink = null
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String?>?,
grantResults: IntArray
): Boolean {
if (requestCode == 105505) {
waitingForPermissionResult = false
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Log.i(
"mobile_scanner",
"Permissions request granted."
)
mobileScanner?.stop()
} else {
Log.i(
"mobile_scanner",
"Permissions request denied."
)
mobileScanner?.stop()
}
return true
}
return false
}
@ExperimentalGetImage
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) {
"start" -> {
// val targetWidth: Int? = call.argument<Int>("targetWidth")
// val targetHeight: Int? = call.argument<Int>("targetHeight")
// val formatStrings: List<String>? = call.argument<List<String>>("formats")
// if (targetWidth == null || targetHeight == null) {
// result.error(
// "INVALID_ARGUMENT",
// "Missing a required argument",
// "Expecting targetWidth, targetHeight"
// )
// return
// }
// val options: BarcodeScannerOptions = BarcodeFormats.optionsFromStringList(formatStrings)
// mobileScanner ?:
try {
MobileScanner(activity!!, textures).start(result, null, channel)
} catch (e: IOException) {
e.printStackTrace()
result.error(
"IOException",
"Error starting camera because of IOException: " + e.localizedMessage,
null
)
} catch (e: MobileScanner.Exception) {
e.printStackTrace()
result.error(
e.reason.name,
"Error starting camera for reason: " + e.reason.name,
null
)
} catch (e: NoPermissionException) {
waitingForPermissionResult = true
ActivityCompat.requestPermissions(
activity!!,
arrayOf(Manifest.permission.CAMERA),
105505
)
}
}
"stop" -> {
if (mobileScanner != null && !waitingForPermissionResult) {
mobileScanner!!.stop()
}
result.success(null)
}
else -> result.notImplemented()
}
}
}
... ...
... ... @@ -24,6 +24,8 @@ if (flutterVersionName == null) {
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
// Apply the Performance Monitoring plugin
apply plugin: 'com.google.firebase.firebase-perf'
android {
compileSdkVersion flutter.compileSdkVersion
... ... @@ -44,7 +46,7 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "dev.steenbakker.mobile_scanner_example"
minSdkVersion flutter.minSdkVersion
minSdkVersion 21
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
... ...
... ... @@ -6,7 +6,8 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.0'
classpath 'com.google.firebase:perf-plugin:1.4.0' // Performanc
classpath 'com.android.tools.build:gradle:7.0.4'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
... ...
#Fri Jun 23 08:50:38 CEST 2017
#Tue Feb 08 10:35:11 CET 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
zipStoreBase=GRADLE_USER_HOME
... ...
# Uncomment this line to define a global platform for your project
# platform :ios, '9.0'
platform :ios, '10.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
... ...
... ... @@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 50;
objectVersion = 51;
objects = {
/* Begin PBXBuildFile section */
... ... @@ -13,6 +13,7 @@
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
C80F46710D9B9F4F17AD4E3D /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5E133769572782C32D37D8AC /* Pods_Runner.framework */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
... ... @@ -31,7 +32,9 @@
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
1CD9C88F6BFEF6CB7CA6746B /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
5E133769572782C32D37D8AC /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
... ... @@ -42,6 +45,8 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
E29A089CD1D61281C49DBB79 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
E33BE6AC5C06F7A45470ADE0 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
... ... @@ -49,12 +54,31 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
C80F46710D9B9F4F17AD4E3D /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
0F766276E0F46921DEBF581B /* Frameworks */ = {
isa = PBXGroup;
children = (
5E133769572782C32D37D8AC /* Pods_Runner.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
203D5C95A734778D93D18369 /* Pods */ = {
isa = PBXGroup;
children = (
E33BE6AC5C06F7A45470ADE0 /* Pods-Runner.debug.xcconfig */,
1CD9C88F6BFEF6CB7CA6746B /* Pods-Runner.release.xcconfig */,
E29A089CD1D61281C49DBB79 /* Pods-Runner.profile.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
... ... @@ -72,6 +96,8 @@
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
203D5C95A734778D93D18369 /* Pods */,
0F766276E0F46921DEBF581B /* Frameworks */,
);
sourceTree = "<group>";
};
... ... @@ -105,12 +131,14 @@
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
1C759CA63421B131D22BB688 /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
EE97B31B239E017B5516C6AD /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
... ... @@ -169,6 +197,28 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
1C759CA63421B131D22BB688 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
... ... @@ -197,6 +247,23 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
EE97B31B239E017B5516C6AD /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
... ... @@ -288,7 +355,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 3K8Q7WKS3W;
DEVELOPMENT_TEAM = 75Y2P2WSQQ;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
... ... @@ -417,7 +484,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 3K8Q7WKS3W;
DEVELOPMENT_TEAM = 75Y2P2WSQQ;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
... ... @@ -440,7 +507,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 3K8Q7WKS3W;
DEVELOPMENT_TEAM = 75Y2P2WSQQ;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
... ...
... ... @@ -4,4 +4,7 @@
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>
... ...
... ... @@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSCameraUsageDescription</key>
<string>We use the camera to scan barcodes</string>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
... ...
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter/services.dart';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
void main() {
runApp(const MyApp());
runApp(const AnalyzeView());
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
class AnalyzeView extends StatefulWidget {
const AnalyzeView({Key? key}) : super(key: key);
@override
State<MyApp> createState() => _MyAppState();
_AnalyzeViewState createState() => _AnalyzeViewState();
}
class _MyAppState extends State<MyApp> {
String _platformVersion = 'Unknown';
class _AnalyzeViewState extends State<AnalyzeView>
with SingleTickerProviderStateMixin {
@override
void initState() {
super.initState();
initPlatformState();
}
List<Offset> points = [];
// Platform messages are asynchronous, so we initialize in an async method.
Future<void> initPlatformState() async {
String platformVersion;
// Platform messages may fail, so we use a try/catch PlatformException.
// We also handle the message potentially returning null.
try {
platformVersion =
await MobileScanner.platformVersion ?? 'Unknown platform version';
} on PlatformException {
platformVersion = 'Failed to get platform version.';
}
CameraController cameraController = CameraController();
// If the widget was removed from the tree while the asynchronous platform
// message was in flight, we want to discard the reply rather than calling
// setState to update our non-existent appearance.
if (!mounted) return;
String? barcode = null;
setState(() {
_platformVersion = platformVersion;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Plugin example app'),
),
body: Center(
child: Text('Running on: $_platformVersion\n'),
body: Builder(
builder: (context) {
return Stack(
children: [
CameraView(cameraController,
onDetect: (barcode, args) {
if (this.barcode != barcode.rawValue) {
this.barcode = barcode.rawValue;
if ( barcode.corners != null) {
debugPrint('Size: ${MediaQuery.of(context).size}');
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('${barcode.rawValue}'), duration: Duration(milliseconds: 200), animation: null,));
setState(() {
final List<Offset> points = [];
double factorWidth = args.size.width / 520;
double factorHeight = args.size.height / 640;
for (var point in barcode.corners!) {
points.add(Offset(point.dx * factorWidth, point.dy * factorHeight));
}
this.points = points;
});
}
}
// Default 640 x480
}),
Container(
// width: 400,
// height: 400,
child: CustomPaint(
painter: OpenPainter(points),
),
),
Container(
alignment: Alignment.bottomCenter,
margin: EdgeInsets.only(bottom: 80.0),
child: IconButton(
icon: ValueListenableBuilder(
valueListenable: cameraController.torchState,
builder: (context, state, child) {
final color =
state == TorchState.off ? Colors.grey : Colors.white;
return Icon(Icons.bolt, color: color);
},
),
iconSize: 32.0,
onPressed: () => cameraController.torch(),
),
),
],
);
}
),
),
);
}
@override
void dispose() {
cameraController.dispose();
super.dispose();
}
void display(Barcode barcode) {
Navigator.of(context).popAndPushNamed('display', arguments: barcode);
}
}
class OpenPainter extends CustomPainter {
final List<Offset> points;
OpenPainter(this.points);
@override
void paint(Canvas canvas, Size size) {
var paint1 = Paint()
..color = Color(0xff63aa65)
..strokeWidth = 10;
//draw points on canvas
canvas.drawPoints(PointMode.points, points, paint1);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
class OpacityCurve extends Curve {
@override
double transform(double t) {
if (t < 0.1) {
return t * 10;
} else if (t <= 0.9) {
return 1.0;
} else {
return (1.0 - t) * 10;
}
}
}
// import 'package:flutter/material.dart';
// import 'package:flutter/rendering.dart';
// import 'package:mobile_scanner/mobile_scanner.dart';
//
// void main() {
// debugPaintSizeEnabled = false;
// runApp(HomePage());
// }
//
// class HomePage extends StatefulWidget {
// @override
// HomeState createState() => HomeState();
// }
//
// class HomeState extends State<HomePage> {
// @override
// Widget build(BuildContext context) {
// return MaterialApp(home: MyApp());
// }
// }
//
// class MyApp extends StatefulWidget {
// @override
// _MyAppState createState() => _MyAppState();
// }
//
// class _MyAppState extends State<MyApp> {
// String? qr;
// bool camState = false;
//
// @override
// initState() {
// super.initState();
// }
//
// @override
// Widget build(BuildContext context) {
// return Scaffold(
// appBar: AppBar(
// title: Text('Plugin example app'),
// ),
// body: Center(
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.center,
// mainAxisAlignment: MainAxisAlignment.center,
// children: <Widget>[
// Expanded(
// child: camState
// ? Center(
// child: SizedBox(
// width: 300.0,
// height: 600.0,
// child: MobileScanner(
// onError: (context, error) => Text(
// error.toString(),
// style: TextStyle(color: Colors.red),
// ),
// qrCodeCallback: (code) {
// setState(() {
// qr = code;
// });
// },
// child: Container(
// decoration: BoxDecoration(
// color: Colors.transparent,
// border: Border.all(
// color: Colors.orange,
// width: 10.0,
// style: BorderStyle.solid),
// ),
// ),
// ),
// ),
// )
// : Center(child: Text("Camera inactive"))),
// Text("QRCODE: $qr"),
// ],
// ),
// ),
// floatingActionButton: FloatingActionButton(
// child: Text(
// "press me",
// textAlign: TextAlign.center,
// ),
// onPressed: () {
// setState(() {
// camState = !camState;
// });
// }),
// );
// }
// }
... ...
... ... @@ -13,15 +13,5 @@ import 'package:mobile_scanner_example/main.dart';
void main() {
testWidgets('Verify Platform version', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that platform version is retrieved.
expect(
find.byWidgetPredicate(
(Widget widget) => widget is Text &&
widget.data!.startsWith('Running on:'),
),
findsOneWidget,
);
});
}
... ...
import AVFoundation
import Flutter
import UIKit
import MLKitVision
import MLKitBarcodeScanning
public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "mobile_scanner", binaryMessenger: registrar.messenger())
let instance = SwiftMobileScannerPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
result("iOS " + UIDevice.current.systemVersion)
}
public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, FlutterTexture, AVCaptureVideoDataOutputSampleBufferDelegate {
public static func register(with registrar: FlutterPluginRegistrar) {
let instance = SwiftMobileScannerPlugin(registrar.textures())
let method = FlutterMethodChannel(name: "dev.steenbakker.mobile_scanner/scanner/method", binaryMessenger: registrar.messenger())
registrar.addMethodCallDelegate(instance, channel: method)
let event = FlutterEventChannel(name: "dev.steenbakker.mobile_scanner/scanner/event", binaryMessenger: registrar.messenger())
event.setStreamHandler(instance)
}
let registry: FlutterTextureRegistry
var sink: FlutterEventSink!
var textureId: Int64!
var captureSession: AVCaptureSession!
var device: AVCaptureDevice!
var latestBuffer: CVImageBuffer!
var analyzeMode: Int
var analyzing: Bool
init(_ registry: FlutterTextureRegistry) {
self.registry = registry
analyzeMode = 0
analyzing = false
super.init()
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "state":
stateNative(call, result)
case "request":
requestNative(call, result)
case "start":
startNative(call, result)
case "torch":
torchNative(call, result)
case "analyze":
analyzeNative(call, result)
case "stop":
stopNative(result)
default:
result(FlutterMethodNotImplemented)
}
}
public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
sink = events
return nil
}
public func onCancel(withArguments arguments: Any?) -> FlutterError? {
sink = nil
return nil
}
public func copyPixelBuffer() -> Unmanaged<CVPixelBuffer>? {
if latestBuffer == nil {
return nil
}
return Unmanaged<CVPixelBuffer>.passRetained(latestBuffer)
}
public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
latestBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)
registry.textureFrameAvailable(textureId)
switch analyzeMode {
case 1: // barcode
if analyzing {
break
}
analyzing = true
let buffer = CMSampleBufferGetImageBuffer(sampleBuffer)
let image = VisionImage(image: buffer!.image)
let scanner = BarcodeScanner.barcodeScanner()
scanner.process(image) { [self] barcodes, error in
if error == nil && barcodes != nil {
for barcode in barcodes! {
let event: [String: Any?] = ["name": "barcode", "data": barcode.data]
sink?(event)
}
}
analyzing = false
}
default: // none
break
}
}
func stateNative(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
let status = AVCaptureDevice.authorizationStatus(for: .video)
switch status {
case .notDetermined:
result(0)
case .authorized:
result(1)
default:
result(2)
}
}
func requestNative(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
AVCaptureDevice.requestAccess(for: .video, completionHandler: { result($0) })
}
func startNative(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
textureId = registry.register(self)
captureSession = AVCaptureSession()
let position = call.arguments as! Int == 0 ? AVCaptureDevice.Position.front : .back
if #available(iOS 10.0, *) {
device = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: position).devices.first
} else {
device = AVCaptureDevice.devices(for: .video).filter({$0.position == position}).first
}
device.addObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode), options: .new, context: nil)
captureSession.beginConfiguration()
// Add device input.
do {
let input = try AVCaptureDeviceInput(device: device)
captureSession.addInput(input)
} catch {
error.throwNative(result)
}
// Add video output.
let videoOutput = AVCaptureVideoDataOutput()
videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA]
videoOutput.alwaysDiscardsLateVideoFrames = true
videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue.main)
captureSession.addOutput(videoOutput)
for connection in videoOutput.connections {
connection.videoOrientation = .portrait
if position == .front && connection.isVideoMirroringSupported {
connection.isVideoMirrored = true
}
}
captureSession.commitConfiguration()
captureSession.startRunning()
let demensions = CMVideoFormatDescriptionGetDimensions(device.activeFormat.formatDescription)
let width = Double(demensions.height)
let height = Double(demensions.width)
let size = ["width": width, "height": height]
let answer: [String : Any?] = ["textureId": textureId, "size": size, "torchable": device.hasTorch]
result(answer)
}
func torchNative(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
do {
try device.lockForConfiguration()
device.torchMode = call.arguments as! Int == 1 ? .on : .off
device.unlockForConfiguration()
result(nil)
} catch {
error.throwNative(result)
}
}
func analyzeNative(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
analyzeMode = call.arguments as! Int
result(nil)
}
func stopNative(_ result: FlutterResult) {
captureSession.stopRunning()
for input in captureSession.inputs {
captureSession.removeInput(input)
}
for output in captureSession.outputs {
captureSession.removeOutput(output)
}
device.removeObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode))
registry.unregisterTexture(textureId)
analyzeMode = 0
latestBuffer = nil
captureSession = nil
device = nil
textureId = nil
result(nil)
}
public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
switch keyPath {
case "torchMode":
// off = 0; on = 1; auto = 2;
let state = change?[.newKey] as? Int
let event: [String: Any?] = ["name": "torchState", "data": state]
sink?(event)
default:
break
}
}
}
... ...
//
// Util.swift
// camerax
//
// Created by 闫守旺 on 2021/2/6.
//
import AVFoundation
import Flutter
import Foundation
import MLKitBarcodeScanning
extension Error {
func throwNative(_ result: FlutterResult) {
let error = FlutterError(code: localizedDescription, message: nil, details: nil)
result(error)
}
}
extension CVBuffer {
var image: UIImage {
let ciImage = CIImage(cvPixelBuffer: self)
let cgImage = CIContext().createCGImage(ciImage, from: ciImage.extent)
return UIImage(cgImage: cgImage!)
}
var image1: UIImage {
// Lock the base address of the pixel buffer
CVPixelBufferLockBaseAddress(self, CVPixelBufferLockFlags.readOnly)
// Get the number of bytes per row for the pixel buffer
let baseAddress = CVPixelBufferGetBaseAddress(self)
// Get the number of bytes per row for the pixel buffer
let bytesPerRow = CVPixelBufferGetBytesPerRow(self)
// Get the pixel buffer width and height
let width = CVPixelBufferGetWidth(self)
let height = CVPixelBufferGetHeight(self)
// Create a device-dependent RGB color space
let colorSpace = CGColorSpaceCreateDeviceRGB()
// Create a bitmap graphics context with the sample buffer data
var bitmapInfo = CGBitmapInfo.byteOrder32Little.rawValue
bitmapInfo |= CGImageAlphaInfo.premultipliedFirst.rawValue & CGBitmapInfo.alphaInfoMask.rawValue
//let bitmapInfo: UInt32 = CGBitmapInfo.alphaInfoMask.rawValue
let context = CGContext(data: baseAddress, width: width, height: height, bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo)
// Create a Quartz image from the pixel data in the bitmap graphics context
let quartzImage = context?.makeImage()
// Unlock the pixel buffer
CVPixelBufferUnlockBaseAddress(self, CVPixelBufferLockFlags.readOnly)
// Create an image object from the Quartz image
return UIImage(cgImage: quartzImage!)
}
}
extension UIDeviceOrientation {
func imageOrientation(position: AVCaptureDevice.Position) -> UIImage.Orientation {
switch self {
case .portrait:
return position == .front ? .leftMirrored : .right
case .landscapeLeft:
return position == .front ? .downMirrored : .up
case .portraitUpsideDown:
return position == .front ? .rightMirrored : .left
case .landscapeRight:
return position == .front ? .upMirrored : .down
default:
return .up
}
}
}
extension Barcode {
var data: [String: Any?] {
let corners = cornerPoints?.map({$0.cgPointValue.data})
return ["corners": corners, "format": format.rawValue, "rawBytes": rawData, "rawValue": rawValue, "type": valueType.rawValue, "calendarEvent": calendarEvent?.data, "contactInfo": contactInfo?.data, "driverLicense": driverLicense?.data, "email": email?.data, "geoPoint": geoPoint?.data, "phone": phone?.data, "sms": sms?.data, "url": url?.data, "wifi": wifi?.data]
}
}
extension CGPoint {
var data: [String: Any?] {
let x1 = NSNumber(value: x.native)
let y1 = NSNumber(value: y.native)
return ["x": x1, "y": y1]
}
}
extension BarcodeCalendarEvent {
var data: [String: Any?] {
return ["description": eventDescription, "end": end?.rawValue, "location": location, "organizer": organizer, "start": start?.rawValue, "status": status, "summary": summary]
}
}
extension Date {
var rawValue: String {
return ISO8601DateFormatter().string(from: self)
}
}
extension BarcodeContactInfo {
var data: [String: Any?] {
return ["addresses": addresses?.map({$0.data}), "emails": emails?.map({$0.data}), "name": name?.data, "organization": organization, "phones": phones?.map({$0.data}), "title": jobTitle, "urls": urls]
}
}
extension BarcodeAddress {
var data: [String: Any?] {
return ["addressLines": addressLines, "type": type.rawValue]
}
}
extension BarcodePersonName {
var data: [String: Any?] {
return ["first": first, "formattedName": formattedName, "last": last, "middle": middle, "prefix": prefix, "pronunciation": pronunciation, "suffix": suffix]
}
}
extension BarcodeDriverLicense {
var data: [String: Any?] {
return ["addressCity": addressCity, "addressState": addressState, "addressStreet": addressStreet, "addressZip": addressZip, "birthDate": birthDate, "documentType": documentType, "expiryDate": expiryDate, "firstName": firstName, "gender": gender, "issueDate": issuingDate, "issuingCountry": issuingCountry, "lastName": lastName, "licenseNumber": licenseNumber, "middleName": middleName]
}
}
extension BarcodeEmail {
var data: [String: Any?] {
return ["address": address, "body": body, "subject": subject, "type": type.rawValue]
}
}
extension BarcodeGeoPoint {
var data: [String: Any?] {
return ["latitude": latitude, "longitude": longitude]
}
}
extension BarcodePhone {
var data: [String: Any?] {
return ["number": number, "type": type.rawValue]
}
}
extension BarcodeSMS {
var data: [String: Any?] {
return ["message": message, "phoneNumber": phoneNumber]
}
}
extension BarcodeURLBookmark {
var data: [String: Any?] {
return ["title": title, "url": url]
}
}
extension BarcodeWifi {
var data: [String: Any?] {
return ["encryptionType": type.rawValue, "password": password, "ssid": ssid]
}
}
... ...
... ... @@ -15,8 +15,9 @@ An universal scanner for Flutter based on MLKit.
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'Flutter'
s.platform = :ios, '9.0'
s.dependency 'GoogleMLKit/BarcodeScanning'
s.platform = :ios, '10.0'
s.static_framework = true
# Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
s.swift_version = '5.0'
... ...
export 'src/mobile_scanner.dart';
\ No newline at end of file
library mobile_scanner;
export 'src/mobile_scanner.dart';
export 'src/camera_controller.dart';
export 'src/camera_view.dart';
export 'src/torch_state.dart';
export 'src/objects/barcode.dart';
\ No newline at end of file
... ...
import 'package:flutter/material.dart';
/// Camera args for [CameraView].
class CameraArgs {
/// The texture id.
final int textureId;
/// Size of the texture.
final Size size;
/// Create a [CameraArgs].
CameraArgs(this.textureId, this.size);
}
... ...
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'camera_args.dart';
import 'camera_facing.dart';
import 'objects/barcode.dart';
import 'torch_state.dart';
import 'util.dart';
/// A camera controller.
abstract class CameraController {
/// Arguments for [CameraView].
ValueNotifier<CameraArgs?> get args;
/// Torch state of the camera.
ValueNotifier<TorchState> get torchState;
/// A stream of barcodes.
Stream<Barcode> get barcodes;
/// Create a [CameraController].
///
/// [facing] target facing used to select camera.
///
/// [formats] the barcode formats for image analyzer.
factory CameraController([CameraFacing facing = CameraFacing.back]) =>
_CameraController(facing);
/// Start the camera asynchronously.
Future<void> startAsync();
/// Switch the torch's state.
void torch();
/// Release the resources of the camera.
void dispose();
}
class _CameraController implements CameraController {
static const MethodChannel method =
MethodChannel('dev.steenbakker.mobile_scanner/scanner/method');
static const EventChannel event =
EventChannel('dev.steenbakker.mobile_scanner/scanner/event');
static const undetermined = 0;
static const authorized = 1;
static const denied = 2;
static const analyze_none = 0;
static const analyze_barcode = 1;
static int? id;
static StreamSubscription? subscription;
final CameraFacing facing;
@override
final ValueNotifier<CameraArgs?> args;
@override
final ValueNotifier<TorchState> torchState;
bool torchable;
late StreamController<Barcode> barcodesController;
@override
Stream<Barcode> get barcodes => barcodesController.stream;
_CameraController(this.facing)
: args = ValueNotifier(null),
torchState = ValueNotifier(TorchState.off),
torchable = false {
// In case new instance before dispose.
if (id != null) {
stop();
}
id = hashCode;
// Create barcode stream controller.
barcodesController = StreamController.broadcast(
onListen: () => tryAnalyze(analyze_barcode),
onCancel: () => tryAnalyze(analyze_none),
);
startAsync();
// Listen event handler.
subscription =
event.receiveBroadcastStream().listen((data) => handleEvent(data));
}
void handleEvent(Map<dynamic, dynamic> event) {
final name = event['name'];
final data = event['data'];
switch (name) {
case 'torchState':
final state = TorchState.values[data];
torchState.value = state;
break;
case 'barcode':
final barcode = Barcode.fromNative(data);
barcodesController.add(barcode);
break;
default:
throw UnimplementedError();
}
}
void tryAnalyze(int mode) {
if (hashCode != id) {
return;
}
method.invokeMethod('analyze', mode);
}
@override
Future<void> startAsync() async {
ensure('startAsync');
// Check authorization state.
var state = await method.invokeMethod('state');
if (state == undetermined) {
final result = await method.invokeMethod('request');
state = result ? authorized : denied;
}
if (state != authorized) {
throw PlatformException(code: 'NO ACCESS');
}
// Start camera.
final answer =
await method.invokeMapMethod<String, dynamic>('start', facing.index);
final textureId = answer?['textureId'];
final size = toSize(answer?['size']);
args.value = CameraArgs(textureId, size);
torchable = answer?['torchable'];
}
@override
void torch() {
ensure('torch');
if (!torchable) {
return;
}
var state =
torchState.value == TorchState.off ? TorchState.on : TorchState.off;
method.invokeMethod('torch', state.index);
}
@override
void dispose() {
if (hashCode == id) {
stop();
subscription?.cancel();
subscription = null;
id = null;
}
barcodesController.close();
}
void stop() => method.invokeMethod('stop');
void ensure(String name) {
final message =
'CameraController.$name called after CameraController.dispose\n'
'CameraController methods should not be used after calling dispose.';
assert(hashCode == id, message);
}
}
... ...
/// The facing of a camera.
enum CameraFacing {
/// Front facing camera.
front,
/// Back facing camera.
back,
}
... ...
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'camera_args.dart';
import 'camera_controller.dart';
/// A widget showing a live camera preview.
class CameraView extends StatelessWidget {
/// The controller of the camera.
final CameraController controller;
final Function(Barcode barcode, CameraArgs args)? onDetect;
/// Create a [CameraView] with a [controller], the [controller] must has been initialized.
CameraView(this.controller, {this.onDetect});
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: controller.args,
builder: (context, value, child) => _build(context, value as CameraArgs?),
);
}
Widget _build(BuildContext context, CameraArgs? value) {
if (value == null) {
return Container(color: Colors.black);
} else {
controller.barcodes.listen((a) => onDetect!(a, value));
return ClipRect(
child: Transform.scale(
scale: value.size.fill(MediaQuery.of(context) .size),
child: Center(
child: AspectRatio(
aspectRatio: value.size.aspectRatio,
child: Texture(textureId: value.textureId),
),
),
),
);
}
}
}
extension on Size {
double fill(Size targetSize) {
if (targetSize.aspectRatio < aspectRatio) {
return targetSize.height * aspectRatio / targetSize.width;
} else {
return targetSize.width / aspectRatio / targetSize.height;
}
}
}
... ...
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:mobile_scanner/src/objects/preview_details.dart';
import 'package:native_device_orientation/native_device_orientation.dart';
import 'package:sensors_plus/sensors_plus.dart';
class Preview extends StatelessWidget {
class Preview extends StatefulWidget {
final double width, height;
final double targetWidth, targetHeight;
final int? textureId;
... ... @@ -18,47 +21,103 @@ class Preview extends StatelessWidget {
}) : textureId = previewDetails.textureId,
width = previewDetails.width!.toDouble(),
height = previewDetails.height!.toDouble(),
sensorOrientation = previewDetails.sensorOrientation as int?, super(key: key);
sensorOrientation = previewDetails.sensorOrientation as int?,
super(key: key);
@override
State<Preview> createState() => _PreviewState();
}
class _PreviewState extends State<Preview> {
final _streamSubscriptions = <StreamSubscription<dynamic>>[];
bool landscapeLeft = false;
@override
void initState() {
super.initState();
_streamSubscriptions.add(
magnetometerEvents.listen(
(MagnetometerEvent event) {
if (event.x <= 0) {
landscapeLeft = true;
} else {
landscapeLeft = false;
}
},
),
);
}
@override
void dispose() {
super.dispose();
for (final subscription in _streamSubscriptions) {
subscription.cancel();
}
}
int _getRotationCompensation(NativeDeviceOrientation nativeOrientation) {
int nativeRotation = 0;
switch (nativeOrientation) {
case NativeDeviceOrientation.portraitUp:
nativeRotation = 0;
break;
case NativeDeviceOrientation.landscapeRight:
nativeRotation = 90;
break;
case NativeDeviceOrientation.portraitDown:
nativeRotation = 180;
break;
case NativeDeviceOrientation.landscapeLeft:
nativeRotation = 270;
break;
case NativeDeviceOrientation.unknown:
default:
break;
}
return ((nativeRotation - widget.sensorOrientation! + 450) % 360) ~/ 90;
}
@override
Widget build(BuildContext context) {
final orientation = MediaQuery.of(context).orientation;
double frameHeight = widget.width;
double frameWidth = widget.height;
return ClipRect(
child: FittedBox(
fit: widget.fit,
child: RotatedBox(
quarterTurns: orientation == Orientation.landscape ? landscapeLeft ? 1 : 3 : 0,
child: SizedBox(
width: frameWidth,
height: frameHeight,
child: Texture(textureId: widget.textureId!),
),
),
),
);
return NativeDeviceOrientationReader(
builder: (context) {
var nativeOrientation = NativeDeviceOrientationReader.orientation(context);
int nativeRotation = 0;
switch (nativeOrientation) {
case NativeDeviceOrientation.portraitUp:
nativeRotation = 0;
break;
case NativeDeviceOrientation.landscapeRight:
nativeRotation = 90;
break;
case NativeDeviceOrientation.portraitDown:
nativeRotation = 180;
break;
case NativeDeviceOrientation.landscapeLeft:
nativeRotation = 270;
break;
case NativeDeviceOrientation.unknown:
default:
break;
}
int rotationCompensation = ((nativeRotation - sensorOrientation! + 450) % 360) ~/ 90;
double frameHeight = width;
double frameWidth = height;
var nativeOrientation =
NativeDeviceOrientationReader.orientation(context);
double frameHeight = widget.width;
double frameWidth = widget.height;
return ClipRect(
child: FittedBox(
fit: fit,
fit: widget.fit,
child: RotatedBox(
quarterTurns: rotationCompensation,
quarterTurns: _getRotationCompensation(nativeOrientation),
child: SizedBox(
width: frameWidth,
height: frameHeight,
child: Texture(textureId: textureId!),
child: Texture(textureId: widget.textureId!),
),
),
),
... ...
import 'dart:typed_data';
import 'dart:ui';
import '../util.dart';
/// Represents a single recognized barcode and its value.
class Barcode {
/// Returns four corner points in clockwise direction starting with top-left.
///
/// Due to the possible perspective distortions, this is not necessarily a rectangle.
///
/// Returns null if the corner points can not be determined.
final List<Offset>? corners;
/// Returns barcode format
final BarcodeFormat format;
/// Returns raw bytes as it was encoded in the barcode.
///
/// Returns null if the raw bytes can not be determined.
final Uint8List rawBytes;
/// Returns barcode value as it was encoded in the barcode. Structured values are not parsed, for example: 'MEBKM:TITLE:Google;URL://www.google.com;;'.
///
/// It's only available when the barcode is encoded in the UTF-8 format, and for non-UTF8 ones use [rawBytes] instead.
///
/// Returns null if the raw value can not be determined.
final String rawValue;
/// Returns format type of the barcode value.
///
/// For example, TYPE_TEXT, TYPE_PRODUCT, TYPE_URL, etc.
///
/// If the value structure cannot be parsed, TYPE_TEXT will be returned. If the recognized structure type is not defined in your current version of SDK, TYPE_UNKNOWN will be returned.
///
/// Note that the built-in parsers only recognize a few popular value structures. For your specific use case, you might want to directly consume rawValue and implement your own parsing logic.
final BarcodeType type;
/// Gets parsed calendar event details.
final CalendarEvent? calendarEvent;
/// Gets parsed contact details.
final ContactInfo? contactInfo;
/// Gets parsed driver license details.
final DriverLicense? driverLicense;
/// Gets parsed email details.
final Email? email;
/// Gets parsed geo coordinates.
final GeoPoint? geoPoint;
/// Gets parsed phone number details.
final Phone? phone;
/// Gets parsed SMS details.
final SMS? sms;
/// Gets parsed URL bookmark details.
final UrlBookmark? url;
/// Gets parsed WiFi AP details.
final WiFi? wifi;
/// Create a [Barcode] from native data.
Barcode.fromNative(Map<dynamic, dynamic> data)
: corners = toCorners(data['corners']),
format = toFormat(data['format']),
rawBytes = data['rawBytes'],
rawValue = data['rawValue'],
type = BarcodeType.values[data['type']],
calendarEvent = toCalendarEvent(data['calendarEvent']),
contactInfo = toContactInfo(data['contactInfo']),
driverLicense = toDriverLicense(data['driverLicense']),
email = toEmail(data['email']),
geoPoint = toGeoPoint(data['geoPoint']),
phone = toPhone(data['phone']),
sms = toSMS(data['sms']),
url = toUrl(data['url']),
wifi = toWiFi(data['wifi']);
}
/// A calendar event extracted from QRCode.
class CalendarEvent {
/// Gets the description of the calendar event.
///
/// Returns null if not available.
final String? description;
/// Gets the start date time of the calendar event.
///
/// Returns null if not available.
final DateTime? start;
/// Gets the end date time of the calendar event.
///
/// Returns null if not available.
final DateTime? end;
/// Gets the location of the calendar event.
///
/// Returns null if not available.
final String? location;
/// Gets the organizer of the calendar event.
///
/// Returns null if not available.
final String? organizer;
/// Gets the status of the calendar event.
///
/// Returns null if not available.
final String? status;
/// Gets the summary of the calendar event.
///
/// Returns null if not available.
final String? summary;
/// Create a [CalendarEvent] from native data.
CalendarEvent.fromNative(Map<dynamic, dynamic> data)
: description = data['description'],
start = DateTime.tryParse(data['start']),
end = DateTime.tryParse(data['end']),
location = data['location'],
organizer = data['organizer'],
status = data['status'],
summary = data['summary'];
}
/// A person's or organization's business card. For example a VCARD.
class ContactInfo {
/// Gets contact person's addresses.
///
/// Returns an empty list if nothing found.
final List<Address> addresses;
/// Gets contact person's emails.
///
/// Returns an empty list if nothing found.
final List<Email> emails;
/// Gets contact person's name.
///
/// Returns null if not available.
final PersonName? name;
/// Gets contact person's organization.
///
/// Returns null if not available.
final String organization;
/// Gets contact person's phones.
///
/// Returns an empty list if nothing found.
final List<Phone> phones;
/// Gets contact person's title.
///
/// Returns null if not available.
final String title;
/// Gets contact person's urls.
///
/// Returns an empty list if nothing found.
final List<String> urls;
/// Create a [ContactInfo] from native data.
ContactInfo.fromNative(Map<dynamic, dynamic> data)
: addresses = List.unmodifiable(
data['addresses'].map((e) => Address.fromNative(e))),
emails =
List.unmodifiable(data['emails'].map((e) => Email.fromNative(e))),
name = toName(data['name']),
organization = data['organization'],
phones =
List.unmodifiable(data['phones'].map((e) => Phone.fromNative(e))),
title = data['title'],
urls = List.unmodifiable(data['urls']);
}
/// An address.
class Address {
/// Gets formatted address, multiple lines when appropriate. This field always contains at least one line.
final List<String> addressLines;
/// Gets type of the address.
final AddressType type;
/// Create a [Address] from native data.
Address.fromNative(Map<dynamic, dynamic> data)
: addressLines = List.unmodifiable(data['addressLines']),
type = AddressType.values[data['type']];
}
/// A person's name, both formatted version and individual name components.
class PersonName {
/// Gets first name.
///
/// Returns null if not available.
final String first;
/// Gets middle name.
///
/// Returns null if not available.
final String middle;
/// Gets last name.
///
/// Returns null if not available.
final String last;
/// Gets prefix of the name.
///
/// Returns null if not available.
final String prefix;
/// Gets suffix of the person's name.
///
/// Returns null if not available.
final String suffix;
/// Gets the properly formatted name.
///
/// Returns null if not available.
final String formattedName;
/// Designates a text string to be set as the kana name in the phonebook. Used for Japanese contacts.
///
/// Returns null if not available.
final String pronunciation;
/// Create a [PersonName] from native data.
PersonName.fromNative(Map<dynamic, dynamic> data)
: first = data['first'],
middle = data['middle'],
last = data['last'],
prefix = data['prefix'],
suffix = data['suffix'],
formattedName = data['formattedName'],
pronunciation = data['pronunciation'];
}
/// A driver license or ID card.
class DriverLicense {
/// Gets city of holder's address.
///
/// Returns null if not available.
final String addressCity;
/// Gets state of holder's address.
///
/// Returns null if not available.
final String addressState;
/// Gets holder's street address.
///
/// Returns null if not available.
final String addressStreet;
/// Gets postal code of holder's address.
///
/// Returns null if not available.
final String addressZip;
/// Gets birth date of the holder.
///
/// Returns null if not available.
final String birthDate;
/// Gets "DL" for driver licenses, "ID" for ID cards.
///
/// Returns null if not available.
final String documentType;
/// Gets expiry date of the license.
///
/// Returns null if not available.
final String expiryDate;
/// Gets holder's first name.
///
/// Returns null if not available.
final String firstName;
/// Gets holder's gender. 1 - male, 2 - female.
///
/// Returns null if not available.
final String gender;
/// Gets issue date of the license.
///
/// The date format depends on the issuing country. MMDDYYYY for the US, YYYYMMDD for Canada.
///
/// Returns null if not available.
final String issueDate;
/// Gets the three-letter country code in which DL/ID was issued.
///
/// Returns null if not available.
final String issuingCountry;
/// Gets holder's last name.
///
/// Returns null if not available.
final String lastName;
/// Gets driver license ID number.
///
/// Returns null if not available.
final String licenseNumber;
/// Gets holder's middle name.
///
/// Returns null if not available.
final String middleName;
/// Create a [DriverLicense] from native data.
DriverLicense.fromNative(Map<dynamic, dynamic> data)
: addressCity = data['addressCity'],
addressState = data['addressState'],
addressStreet = data['addressStreet'],
addressZip = data['addressZip'],
birthDate = data['birthDate'],
documentType = data['documentType'],
expiryDate = data['expiryDate'],
firstName = data['firstName'],
gender = data['gender'],
issueDate = data['issueDate'],
issuingCountry = data['issuingCountry'],
lastName = data['lastName'],
licenseNumber = data['licenseNumber'],
middleName = data['middleName'];
}
/// An email message from a 'MAILTO:' or similar QRCode type.
class Email {
/// Gets email's address.
///
/// Returns null if not available.
final String address;
/// Gets email's body.
///
/// Returns null if not available.
final String body;
/// Gets email's subject.
///
/// Returns null if not available.
final String subject;
/// Gets type of the email.
///
/// See also [EmailType].
final EmailType type;
/// Create a [Email] from native data.
Email.fromNative(Map<dynamic, dynamic> data)
: address = data['address'],
body = data['body'],
subject = data['subject'],
type = EmailType.values[data['type']];
}
/// GPS coordinates from a 'GEO:' or similar QRCode type.
class GeoPoint {
/// Gets the latitude.
final double latitude;
/// Gets the longitude.
final double longitude;
/// Create a [GeoPoint] from native data.
GeoPoint.fromNative(Map<dynamic, dynamic> data)
: latitude = data['latitude'],
longitude = data['longitude'];
}
/// Phone number info.
class Phone {
/// Gets phone number.
///
/// Returns null if not available.
final String number;
/// Gets type of the phone number.
///
/// See also [PhoneType].
final PhoneType type;
/// Create a [Phone] from native data.
Phone.fromNative(Map<dynamic, dynamic> data)
: number = data['number'],
type = PhoneType.values[data['type']];
}
/// A sms message from a 'SMS:' or similar QRCode type.
class SMS {
/// Gets the message content of the sms.
///
/// Returns null if not available.
final String message;
/// Gets the phone number of the sms.
///
/// Returns null if not available.
final String phoneNumber;
/// Create a [SMS] from native data.
SMS.fromNative(Map<dynamic, dynamic> data)
: message = data['message'],
phoneNumber = data['phoneNumber'];
}
/// A URL and title from a 'MEBKM:' or similar QRCode type.
class UrlBookmark {
/// Gets the title of the bookmark.
///
/// Returns null if not available.
final String title;
/// Gets the url of the bookmark.
///
/// Returns null if not available.
final String url;
/// Create a [UrlBookmark] from native data.
UrlBookmark.fromNative(Map<dynamic, dynamic> data)
: title = data['title'],
url = data['url'];
}
/// A wifi network parameters from a 'WIFI:' or similar QRCode type.
class WiFi {
/// Gets the encryption type of the WIFI.
///
/// See all [EncryptionType].
final EncryptionType encryptionType;
/// Gets the ssid of the WIFI.
///
/// Returns null if not available.
final String ssid;
/// Gets the password of the WIFI.
///
/// Returns null if not available.
final String password;
/// Create a [WiFi] from native data.
WiFi.fromNative(Map<dynamic, dynamic> data)
: encryptionType = EncryptionType.values[data['encryptionType']],
ssid = data['ssid'],
password = data['password'];
}
enum BarcodeFormat {
/// Barcode format unknown to the current SDK.
///
/// Constant Value: -1
unknown,
/// Barcode format constant representing the union of all supported formats.
///
/// Constant Value: 0
all,
/// Barcode format constant for Code 128.
///
/// Constant Value: 1
code128,
/// Barcode format constant for Code 39.
///
/// Constant Value: 2
code39,
/// Barcode format constant for Code 93.
///
/// Constant Value: 4
code93,
/// Barcode format constant for Codabar.
///
/// Constant Value: 8
codebar,
/// Barcode format constant for Data Matrix.
///
/// Constant Value: 16
data_matrix,
/// Barcode format constant for EAN-13.
///
/// Constant Value: 32
ean13,
/// Barcode format constant for EAN-8.
///
/// Constant Value: 64
ean8,
/// Barcode format constant for ITF (Interleaved Two-of-Five).
///
/// Constant Value: 128
itf,
/// Barcode format constant for QR Code.
///
/// Constant Value: 256
qr_code,
/// Barcode format constant for UPC-A.
///
/// Constant Value: 512
upc_a,
/// Barcode format constant for UPC-E.
///
/// Constant Value: 1024
upc_e,
/// Barcode format constant for PDF-417.
///
/// Constant Value: 2048
pdf417,
/// Barcode format constant for AZTEC.
///
/// Constant Value: 4096
aztec,
}
/// Address type constants.
enum AddressType {
/// Unknown address type.
///
/// Constant Value: 0
unknown,
/// Work address.
///
/// Constant Value: 1
work,
/// Home address.
///
/// Constant Value: 2
home,
}
/// Barcode value type constants
enum BarcodeType {
/// Barcode value type unknown, which indicates the current version of SDK cannot recognize the structure of the barcode. Developers can inspect the raw value instead.
///
/// Constant Value: 0
unknown,
/// Barcode value type constant for contact information.
///
/// Constant Value: 1
contactInfo,
/// Barcode value type constant for email message details.
///
/// Constant Value: 2
email,
/// Barcode value type constant for ISBNs.
///
/// Constant Value: 3
isbn,
/// Barcode value type constant for phone numbers.
///
/// Constant Value: 4
phone,
/// Barcode value type constant for product codes.
///
/// Constant Value: 5
product,
/// Barcode value type constant for SMS details.
///
/// Constant Value: 6
sms,
/// Barcode value type constant for plain text.
///
///Constant Value: 7
text,
/// Barcode value type constant for URLs/bookmarks.
///
/// Constant Value: 8
url,
/// Barcode value type constant for WiFi access point details.
///
/// Constant Value: 9
wifi,
/// Barcode value type constant for geographic coordinates.
///
/// Constant Value: 10
geo,
/// Barcode value type constant for calendar events.
///
/// Constant Value: 11
calendarEvent,
/// Barcode value type constant for driver's license data.
///
/// Constant Value: 12
driverLicense,
}
/// Email format type constants.
enum EmailType {
/// Unknown email type.
///
/// Constant Value: 0
unknown,
/// Work email.
///
/// Constant Value: 1
work,
/// Home email.
///
/// Constant Value: 2
home,
}
/// Phone number format type constants.
enum PhoneType {
/// Unknown phone type.
///
/// Constant Value: 0
unknown,
/// Work phone.
///
/// Constant Value: 1
work,
/// Home phone.
///
/// Constant Value: 2
home,
/// Fax machine.
///
/// Constant Value: 3
fax,
/// Mobile phone.
///
/// Constant Value: 4
mobile,
}
/// Wifi encryption type constants.
enum EncryptionType {
/// Unknown encryption type.
///
/// Constant Value: 0
none,
/// Not encrypted.
///
/// Constant Value: 1
open,
/// WPA level encryption.
///
/// Constant Value: 2
wpa,
/// WEP level encryption.
///
/// Constant Value: 3
wep,
}
... ...
/// The state of torch.
enum TorchState {
/// Torch is off.
off,
/// Torch is on.
on,
}
... ...
import 'package:flutter/material.dart';
import 'objects/barcode.dart';
Size toSize(Map<dynamic, dynamic> data) {
final width = data['width'];
final height = data['height'];
return Size(width, height);
}
List<Offset>? toCorners(List<dynamic>? data) {
if (data != null) {
return List.unmodifiable(data.map((e) => Offset(e['x'], e['y'])));
} else {
return null;
}
}
BarcodeFormat toFormat(int value) {
switch (value) {
case 0:
return BarcodeFormat.all;
case 1:
return BarcodeFormat.code128;
case 2:
return BarcodeFormat.code39;
case 4:
return BarcodeFormat.code93;
case 8:
return BarcodeFormat.codebar;
case 16:
return BarcodeFormat.data_matrix;
case 32:
return BarcodeFormat.ean13;
case 64:
return BarcodeFormat.ean8;
case 128:
return BarcodeFormat.itf;
case 256:
return BarcodeFormat.qr_code;
case 512:
return BarcodeFormat.upc_a;
case 1024:
return BarcodeFormat.upc_e;
case 2048:
return BarcodeFormat.pdf417;
case 4096:
return BarcodeFormat.aztec;
default:
return BarcodeFormat.unknown;
}
}
CalendarEvent? toCalendarEvent(Map<dynamic, dynamic>? data) {
if (data != null) {
return CalendarEvent.fromNative(data);
} else {
return null;
}
}
DateTime? toDateTime(Map<dynamic, dynamic>? data) {
if (data != null) {
final year = data['year'];
final month = data['month'];
final day = data['day'];
final hour = data['hours'];
final minute = data['minutes'];
final second = data['seconds'];
return data['isUtc']
? DateTime.utc(year, month, day, hour, minute, second)
: DateTime(year, month, day, hour, minute, second);
} else {
return null;
}
}
ContactInfo? toContactInfo(Map<dynamic, dynamic>? data) {
if (data != null) {
return ContactInfo.fromNative(data);
} else {
return null;
}
}
PersonName? toName(Map<dynamic, dynamic>? data) {
if (data != null) {
return PersonName.fromNative(data);
} else {
return null;
}
}
DriverLicense? toDriverLicense(Map<dynamic, dynamic>? data) {
if (data != null) {
return DriverLicense.fromNative(data);
} else {
return null;
}
}
Email? toEmail(Map<dynamic, dynamic>? data) {
if (data != null) {
return Email.fromNative(data);
} else {
return null;
}
}
GeoPoint? toGeoPoint(Map<dynamic, dynamic>? data) {
if (data != null) {
return GeoPoint.fromNative(data);
} else {
return null;
}
}
Phone? toPhone(Map<dynamic, dynamic>? data) {
if (data != null) {
return Phone.fromNative(data);
} else {
return null;
}
}
SMS? toSMS(Map<dynamic, dynamic>? data) {
if (data != null) {
return SMS.fromNative(data);
} else {
return null;
}
}
UrlBookmark? toUrl(Map<dynamic, dynamic>? data) {
if (data != null) {
return UrlBookmark.fromNative(data);
} else {
return null;
}
}
WiFi? toWiFi(Map<dynamic, dynamic>? data) {
if (data != null) {
return WiFi.fromNative(data);
} else {
return null;
}
}
... ...
... ... @@ -9,6 +9,7 @@ environment:
dependencies:
native_device_orientation: ^1.0.0
sensors_plus: ^1.2.1
flutter:
sdk: flutter
... ...
... ... @@ -17,7 +17,4 @@ void main() {
channel.setMockMethodCallHandler(null);
});
test('getPlatformVersion', () async {
expect(await MobileScannerHandler.platformVersion, '42');
});
}
... ...