You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
413 lines
15 KiB
413 lines
15 KiB
package app.tauri.barcodescanner
|
|
|
|
import android.Manifest
|
|
import android.annotation.SuppressLint
|
|
import android.app.Activity
|
|
import android.content.Context
|
|
import android.content.Context.MODE_PRIVATE
|
|
import android.content.Intent
|
|
import android.content.SharedPreferences
|
|
import android.content.pm.PackageManager
|
|
import android.net.Uri
|
|
import android.os.Build
|
|
import android.os.VibrationEffect
|
|
import android.os.Vibrator
|
|
import android.provider.Settings
|
|
import android.util.Size
|
|
import android.view.ViewGroup
|
|
import android.webkit.WebView
|
|
import android.widget.FrameLayout
|
|
import androidx.activity.result.ActivityResult
|
|
import androidx.camera.core.Camera
|
|
import androidx.camera.core.CameraSelector
|
|
import androidx.camera.core.ImageAnalysis
|
|
import androidx.camera.core.ImageProxy
|
|
import androidx.camera.core.Preview
|
|
import androidx.camera.lifecycle.ProcessCameraProvider
|
|
import androidx.camera.view.PreviewView
|
|
import androidx.core.content.ContextCompat
|
|
import androidx.lifecycle.LifecycleOwner
|
|
import app.tauri.Logger
|
|
import app.tauri.PermissionState
|
|
import app.tauri.annotation.ActivityCallback
|
|
import app.tauri.annotation.Command
|
|
import app.tauri.annotation.Permission
|
|
import app.tauri.annotation.PermissionCallback
|
|
import app.tauri.annotation.TauriPlugin
|
|
import app.tauri.plugin.Invoke
|
|
import app.tauri.plugin.JSArray
|
|
import app.tauri.plugin.JSObject
|
|
import app.tauri.plugin.Plugin
|
|
import com.google.common.util.concurrent.ListenableFuture
|
|
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 org.json.JSONException
|
|
import java.util.Collections
|
|
import java.util.concurrent.ExecutionException
|
|
|
|
private const val PERMISSION_ALIAS_CAMERA = "camera"
|
|
private const val PERMISSION_NAME = Manifest.permission.CAMERA
|
|
private const val PREFS_PERMISSION_FIRST_TIME_ASKING = "PREFS_PERMISSION_FIRST_TIME_ASKING"
|
|
|
|
@TauriPlugin(
|
|
permissions = [
|
|
Permission(strings = [Manifest.permission.CAMERA], alias = "camera")
|
|
]
|
|
)
|
|
class BarcodeScannerPlugin(private val activity: Activity) : Plugin(activity),
|
|
ImageAnalysis.Analyzer {
|
|
private lateinit var webView: WebView
|
|
private var previewView: PreviewView? = null
|
|
private var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>? = null
|
|
private var cameraProvider: ProcessCameraProvider? = null
|
|
private var graphicOverlay: GraphicOverlay? = null
|
|
private var camera: Camera? = null
|
|
private var vibrator: Vibrator? = null
|
|
|
|
private var scannerOptions: BarcodeScannerOptions? = null
|
|
private var scanner: com.google.mlkit.vision.barcode.BarcodeScanner? = null
|
|
|
|
private var requestPermissionResponse: JSObject? = null
|
|
private var cameraReady = false
|
|
|
|
// declare a map constant for allowed barcode formats
|
|
private val supportedFormats = supportedFormats()
|
|
|
|
private var savedInvoke: Invoke? = null
|
|
|
|
override fun load(webView: WebView) {
|
|
super.load(webView)
|
|
this.webView = webView
|
|
}
|
|
|
|
private fun supportedFormats(): Map<String, Int> {
|
|
val map: MutableMap<String, Int> = HashMap()
|
|
map["UPC_A"] = Barcode.FORMAT_UPC_A
|
|
map["UPC_E"] = Barcode.FORMAT_UPC_E
|
|
map["EAN_8"] = Barcode.FORMAT_EAN_8
|
|
map["EAN_13"] = Barcode.FORMAT_EAN_13
|
|
map["CODE_39"] = Barcode.FORMAT_CODE_39
|
|
map["CODE_93"] = Barcode.FORMAT_CODE_93
|
|
map["CODE_128"] = Barcode.FORMAT_CODE_128
|
|
map["CODABAR"] = Barcode.FORMAT_CODABAR
|
|
map["ITF"] = Barcode.FORMAT_ITF
|
|
map["AZTEC"] = Barcode.FORMAT_AZTEC
|
|
map["DATA_MATRIX"] = Barcode.FORMAT_DATA_MATRIX
|
|
map["PDF_417"] = Barcode.FORMAT_PDF417
|
|
map["QR_CODE"] = Barcode.FORMAT_QR_CODE
|
|
return Collections.unmodifiableMap(map)
|
|
}
|
|
|
|
private fun hasCamera(): Boolean {
|
|
return activity.packageManager
|
|
.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)
|
|
}
|
|
|
|
private fun setupCamera(cameraDirection: String) {
|
|
activity
|
|
.runOnUiThread {
|
|
|
|
val previewView = PreviewView(activity)
|
|
previewView.layoutParams = FrameLayout.LayoutParams(
|
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
ViewGroup.LayoutParams.MATCH_PARENT
|
|
)
|
|
this.previewView = previewView
|
|
|
|
val graphicOverlay = GraphicOverlay(activity)
|
|
graphicOverlay.layoutParams = FrameLayout.LayoutParams(
|
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
ViewGroup.LayoutParams.MATCH_PARENT
|
|
)
|
|
this.graphicOverlay = graphicOverlay
|
|
|
|
val parent = webView.parent as ViewGroup
|
|
parent.addView(previewView)
|
|
parent.addView(graphicOverlay)
|
|
|
|
val cameraProviderFuture = ProcessCameraProvider.getInstance(activity)
|
|
cameraProviderFuture.addListener(
|
|
{
|
|
try {
|
|
val cameraProvider = cameraProviderFuture.get()
|
|
bindPreview(
|
|
cameraProvider,
|
|
if (cameraDirection == "front") CameraSelector.LENS_FACING_FRONT else CameraSelector.LENS_FACING_BACK
|
|
)
|
|
this.cameraProvider = cameraProvider
|
|
} catch (e: InterruptedException) {
|
|
// ignored
|
|
} catch (_: ExecutionException) {
|
|
// ignored
|
|
}
|
|
},
|
|
ContextCompat.getMainExecutor(activity)
|
|
)
|
|
this.cameraProviderFuture = cameraProviderFuture
|
|
}
|
|
}
|
|
|
|
private fun bindPreview(cameraProvider: ProcessCameraProvider, cameraDirection: Int) {
|
|
activity
|
|
.runOnUiThread {
|
|
val preview = Preview.Builder().build()
|
|
val cameraSelector =
|
|
CameraSelector.Builder().requireLensFacing(cameraDirection).build()
|
|
preview.setSurfaceProvider(previewView?.surfaceProvider)
|
|
val imageAnalysis = ImageAnalysis.Builder()
|
|
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
|
.setTargetResolution(Size(1280, 720))
|
|
.build()
|
|
imageAnalysis.setAnalyzer(
|
|
ContextCompat.getMainExecutor(activity),
|
|
this
|
|
)
|
|
camera = cameraProvider.bindToLifecycle(
|
|
activity as LifecycleOwner,
|
|
cameraSelector,
|
|
preview,
|
|
imageAnalysis
|
|
)
|
|
}
|
|
}
|
|
|
|
private fun dismantleCamera() {
|
|
// opposite of setupCamera
|
|
activity
|
|
.runOnUiThread {
|
|
if (cameraProvider != null) {
|
|
cameraProvider?.unbindAll()
|
|
val parent = webView.parent as ViewGroup
|
|
parent.removeView(previewView)
|
|
parent.removeView(graphicOverlay)
|
|
camera = null
|
|
previewView = null
|
|
graphicOverlay = null
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun getFormats(invoke: Invoke): List<Int> {
|
|
val jsFormats = invoke.getArray("formats", JSArray())
|
|
val formats = ArrayList<Int>()
|
|
for (i in 0 until jsFormats.length()) {
|
|
try {
|
|
val targetedFormat: String = jsFormats.getString(i)
|
|
val targetedBarcodeFormat =
|
|
supportedFormats[targetedFormat]
|
|
if (targetedBarcodeFormat != null) {
|
|
formats.add(targetedBarcodeFormat)
|
|
}
|
|
} catch (e: JSONException) {
|
|
e.printStackTrace()
|
|
}
|
|
}
|
|
return formats
|
|
}
|
|
|
|
private fun prepareInternal(direction: String) {
|
|
dismantleCamera()
|
|
setupCamera(direction)
|
|
}
|
|
|
|
private fun destroy() {
|
|
dismantleCamera()
|
|
savedInvoke = null
|
|
}
|
|
|
|
@Suppress("DEPRECATION")
|
|
private fun configureCamera(formats: List<Int>) {
|
|
activity
|
|
.runOnUiThread {
|
|
val vibrator =
|
|
activity.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
|
this.vibrator = vibrator
|
|
if (previewView == null) {
|
|
throw Exception("Something went wrong configuring the BarcodeScanner")
|
|
}
|
|
|
|
if (formats.isNotEmpty()) {
|
|
val mappedFormats = mapFormats(formats)
|
|
val options =
|
|
BarcodeScannerOptions.Builder()
|
|
.setBarcodeFormats(Barcode.FORMAT_QR_CODE, *mappedFormats).build()
|
|
scannerOptions = options
|
|
scanner = BarcodeScanning.getClient(options)
|
|
} else {
|
|
val options = BarcodeScannerOptions.Builder()
|
|
.setBarcodeFormats(Barcode.FORMAT_ALL_FORMATS).build()
|
|
scannerOptions = options
|
|
scanner = BarcodeScanning.getClient(options)
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun mapFormats(integers: List<Int>): IntArray {
|
|
val ret = IntArray(integers.size)
|
|
for (i in ret.indices) {
|
|
if (integers[i] != Barcode.FORMAT_QR_CODE) ret[i] = integers[i]
|
|
}
|
|
return ret
|
|
}
|
|
|
|
override fun analyze(image: ImageProxy) {
|
|
@SuppressLint("UnsafeOptInUsageError") val mediaImage = image.image
|
|
if (mediaImage != null) {
|
|
val inputImage =
|
|
InputImage.fromMediaImage(mediaImage, image.imageInfo.rotationDegrees)
|
|
scanner
|
|
?.process(inputImage)
|
|
?.addOnSuccessListener { barcodes ->
|
|
for (barcode in barcodes) {
|
|
val bounds = barcode.boundingBox
|
|
val rawValue = barcode.rawValue ?: ""
|
|
|
|
// add vibration logic here
|
|
val s = bounds?.flattenToString()
|
|
val jsObject = JSObject()
|
|
jsObject.put("content", rawValue)
|
|
jsObject.put("bounds", s)
|
|
|
|
savedInvoke?.resolve(jsObject)
|
|
destroy()
|
|
}
|
|
}
|
|
?.addOnFailureListener { e ->
|
|
Logger.error(e.message ?: e.toString())
|
|
}
|
|
?.addOnCompleteListener {
|
|
image.close()
|
|
mediaImage.close()
|
|
}
|
|
}
|
|
}
|
|
|
|
@Command
|
|
fun vibrate(invoke: Invoke) {
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
vibrator!!.vibrate(
|
|
VibrationEffect.createOneShot(
|
|
50,
|
|
VibrationEffect.DEFAULT_AMPLITUDE
|
|
)
|
|
)
|
|
}
|
|
invoke.resolve()
|
|
}
|
|
|
|
@Command
|
|
fun prepare(invoke: Invoke) {
|
|
prepareInternal(invoke.getString("cameraDirection", "back"))
|
|
cameraReady = true
|
|
invoke.resolve()
|
|
}
|
|
|
|
@Command
|
|
fun cancel(invoke: Invoke) {
|
|
savedInvoke?.reject("cancelled")
|
|
destroy()
|
|
invoke.resolve()
|
|
}
|
|
|
|
@Command
|
|
fun scan(invoke: Invoke) {
|
|
savedInvoke = invoke
|
|
if (hasCamera()) {
|
|
if (getPermissionState("camera") != PermissionState.GRANTED) {
|
|
throw Exception("No permission to use camera. Did you request it yet?")
|
|
} else {
|
|
if (!cameraReady) {
|
|
prepareInternal(invoke.getString("cameraDirection", "back"))
|
|
}
|
|
cameraReady = false
|
|
configureCamera(getFormats(invoke))
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun markFirstPermissionRequest() {
|
|
val sharedPreference: SharedPreferences =
|
|
activity.getSharedPreferences(PREFS_PERMISSION_FIRST_TIME_ASKING, MODE_PRIVATE)
|
|
sharedPreference.edit().putBoolean(PERMISSION_NAME, false).apply()
|
|
}
|
|
|
|
private fun firstPermissionRequest(): Boolean {
|
|
return activity.getSharedPreferences(PREFS_PERMISSION_FIRST_TIME_ASKING, MODE_PRIVATE)
|
|
.getBoolean(PERMISSION_NAME, true)
|
|
}
|
|
|
|
@SuppressLint("ObsoleteSdkInt")
|
|
@PermissionCallback
|
|
fun cameraPermissionCallback(invoke: Invoke) {
|
|
if (requestPermissionResponse == null) {
|
|
return
|
|
}
|
|
|
|
val requestPermissionResponse = requestPermissionResponse!!
|
|
|
|
val granted = getPermissionState(PERMISSION_ALIAS_CAMERA) === PermissionState.GRANTED
|
|
|
|
if (granted) {
|
|
requestPermissionResponse.put(PERMISSION_ALIAS_CAMERA, PermissionState.GRANTED)
|
|
} else {
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
if (!activity.shouldShowRequestPermissionRationale(PERMISSION_NAME)) {
|
|
requestPermissionResponse.put(PERMISSION_ALIAS_CAMERA, PermissionState.DENIED)
|
|
}
|
|
} else {
|
|
requestPermissionResponse.put(PERMISSION_ALIAS_CAMERA, PermissionState.GRANTED)
|
|
}
|
|
}
|
|
|
|
invoke.resolve(requestPermissionResponse)
|
|
this.requestPermissionResponse = null
|
|
}
|
|
|
|
@SuppressLint("ObsoleteSdkInt")
|
|
@Command
|
|
override fun requestPermissions(invoke: Invoke) {
|
|
val requestPermissionResponse = JSObject()
|
|
this.requestPermissionResponse = requestPermissionResponse
|
|
if (getPermissionState(PERMISSION_ALIAS_CAMERA) === PermissionState.GRANTED) {
|
|
requestPermissionResponse.put(PERMISSION_ALIAS_CAMERA, PermissionState.GRANTED)
|
|
} else {
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
if (firstPermissionRequest() || activity.shouldShowRequestPermissionRationale(
|
|
PERMISSION_NAME
|
|
)
|
|
) {
|
|
markFirstPermissionRequest()
|
|
requestPermissionForAlias(
|
|
PERMISSION_ALIAS_CAMERA,
|
|
invoke,
|
|
"cameraPermissionCallback"
|
|
)
|
|
return
|
|
} else {
|
|
requestPermissionResponse.put(PERMISSION_ALIAS_CAMERA, PermissionState.DENIED)
|
|
}
|
|
} else {
|
|
requestPermissionResponse.put(PERMISSION_ALIAS_CAMERA, PermissionState.GRANTED)
|
|
}
|
|
}
|
|
invoke.resolve(requestPermissionResponse)
|
|
}
|
|
|
|
@Command
|
|
fun openAppSettings(invoke: Invoke) {
|
|
val intent = Intent(
|
|
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
|
|
Uri.fromParts("package", activity.packageName, null)
|
|
)
|
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
startActivityForResult(invoke, intent, "openSettingsResult")
|
|
}
|
|
|
|
@ActivityCallback
|
|
private fun openSettingsResult(invoke: Invoke, result: ActivityResult) {
|
|
invoke.resolve()
|
|
}
|
|
}
|