Julian Steenbakker
Committed by GitHub

Merge pull request #407 from navaronbracke/fix_android_permission_bug

fix: Android permission bug
  1 +## 3.0.0-beta.4
  2 +Fixes:
  3 +* Fixes a permission bug on Android where denying the permission would cause an infinite loop of permission requests.
  4 +* Updates the example app to handle permission errors with the new builder parameter.
  5 + Now it no longer throws uncaught exceptions when the permission is denied.
  6 +
  7 +Features:
  8 +* Added a new `errorBuilder` to the `MobileScanner` widget that can be used to customize the error state of the preview.
  9 +
1 ## 3.0.0-beta.3 10 ## 3.0.0-beta.3
2 Deprecated: 11 Deprecated:
3 * The `onStart` method has been renamed to `onScannerStarted`. 12 * The `onStart` method has been renamed to `onScannerStarted`.
@@ -2,15 +2,15 @@ package dev.steenbakker.mobile_scanner @@ -2,15 +2,15 @@ package dev.steenbakker.mobile_scanner
2 2
3 import android.os.Handler 3 import android.os.Handler
4 import android.os.Looper 4 import android.os.Looper
5 -import io.flutter.embedding.engine.plugins.FlutterPlugin 5 +import io.flutter.plugin.common.BinaryMessenger
6 import io.flutter.plugin.common.EventChannel 6 import io.flutter.plugin.common.EventChannel
7 7
8 -class BarcodeHandler(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) : EventChannel.StreamHandler { 8 +class BarcodeHandler(binaryMessenger: BinaryMessenger) : EventChannel.StreamHandler {
9 9
10 private var eventSink: EventChannel.EventSink? = null 10 private var eventSink: EventChannel.EventSink? = null
11 11
12 private val eventChannel = EventChannel( 12 private val eventChannel = EventChannel(
13 - flutterPluginBinding.binaryMessenger, 13 + binaryMessenger,
14 "dev.steenbakker.mobile_scanner/scanner/event" 14 "dev.steenbakker.mobile_scanner/scanner/event"
15 ) 15 )
16 16
  1 +package dev.steenbakker.mobile_scanner
  2 +
  3 +import android.app.Activity
  4 +import android.net.Uri
  5 +import androidx.camera.core.CameraSelector
  6 +import androidx.camera.core.ExperimentalGetImage
  7 +import com.google.mlkit.vision.barcode.BarcodeScannerOptions
  8 +import dev.steenbakker.mobile_scanner.objects.BarcodeFormats
  9 +import dev.steenbakker.mobile_scanner.objects.DetectionSpeed
  10 +import io.flutter.plugin.common.BinaryMessenger
  11 +import io.flutter.plugin.common.MethodCall
  12 +import io.flutter.plugin.common.MethodChannel
  13 +import io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener
  14 +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
  15 +import io.flutter.view.TextureRegistry
  16 +import java.io.File
  17 +
  18 +class MethodCallHandlerImpl(
  19 + private val activity: Activity,
  20 + private val barcodeHandler: BarcodeHandler,
  21 + binaryMessenger: BinaryMessenger,
  22 + private val permissions: MobileScannerPermissions,
  23 + private val addPermissionListener: (RequestPermissionsResultListener) -> Unit,
  24 + textureRegistry: TextureRegistry): MethodChannel.MethodCallHandler {
  25 +
  26 + private val analyzerCallback: AnalyzerCallback = { barcodes: List<Map<String, Any?>>?->
  27 + if (barcodes != null) {
  28 + barcodeHandler.publishEvent(mapOf(
  29 + "name" to "barcode",
  30 + "data" to barcodes
  31 + ))
  32 + analyzerResult?.success(true)
  33 + } else {
  34 + analyzerResult?.success(false)
  35 + }
  36 + analyzerResult = null
  37 + }
  38 +
  39 + private var analyzerResult: MethodChannel.Result? = null
  40 +
  41 + private val callback: MobileScannerCallback = { barcodes: List<Map<String, Any?>>, image: ByteArray? ->
  42 + if (image != null) {
  43 + barcodeHandler.publishEvent(mapOf(
  44 + "name" to "barcode",
  45 + "data" to barcodes,
  46 + "image" to image
  47 + ))
  48 + } else {
  49 + barcodeHandler.publishEvent(mapOf(
  50 + "name" to "barcode",
  51 + "data" to barcodes
  52 + ))
  53 + }
  54 + }
  55 +
  56 + private val errorCallback: MobileScannerErrorCallback = {error: String ->
  57 + barcodeHandler.publishEvent(mapOf(
  58 + "name" to "error",
  59 + "data" to error,
  60 + ))
  61 + }
  62 +
  63 + private var methodChannel: MethodChannel? = null
  64 +
  65 + private var mobileScanner: MobileScanner? = null
  66 +
  67 + private val torchStateCallback: TorchStateCallback = {state: Int ->
  68 + barcodeHandler.publishEvent(mapOf("name" to "torchState", "data" to state))
  69 + }
  70 +
  71 + init {
  72 + methodChannel = MethodChannel(binaryMessenger,
  73 + "dev.steenbakker.mobile_scanner/scanner/method")
  74 + methodChannel!!.setMethodCallHandler(this)
  75 + mobileScanner = MobileScanner(activity, textureRegistry, callback, errorCallback)
  76 + }
  77 +
  78 + fun dispose(activityPluginBinding: ActivityPluginBinding) {
  79 + methodChannel?.setMethodCallHandler(null)
  80 + methodChannel = null
  81 + mobileScanner = null
  82 +
  83 + val listener: RequestPermissionsResultListener? = permissions.getPermissionListener()
  84 +
  85 + if(listener != null) {
  86 + activityPluginBinding.removeRequestPermissionsResultListener(listener)
  87 + }
  88 +
  89 + }
  90 +
  91 + @ExperimentalGetImage
  92 + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
  93 + if (mobileScanner == null) {
  94 + result.error("MobileScanner", "Called ${call.method} before initializing.", null)
  95 + return
  96 + }
  97 + when (call.method) {
  98 + "state" -> result.success(permissions.hasCameraPermission(activity))
  99 + "request" -> permissions.requestPermission(
  100 + activity,
  101 + addPermissionListener,
  102 + object: MobileScannerPermissions.ResultCallback {
  103 + override fun onResult(errorCode: String?, errorDescription: String?) {
  104 + when(errorCode) {
  105 + null -> result.success(true)
  106 + MobileScannerPermissions.CAMERA_ACCESS_DENIED -> result.success(false)
  107 + else -> result.error(errorCode, errorDescription, null)
  108 + }
  109 + }
  110 + })
  111 + "start" -> start(call, result)
  112 + "torch" -> toggleTorch(call, result)
  113 + "stop" -> stop(result)
  114 + "analyzeImage" -> analyzeImage(call, result)
  115 + else -> result.notImplemented()
  116 + }
  117 + }
  118 +
  119 + @ExperimentalGetImage
  120 + private fun start(call: MethodCall, result: MethodChannel.Result) {
  121 + val torch: Boolean = call.argument<Boolean>("torch") ?: false
  122 + val facing: Int = call.argument<Int>("facing") ?: 0
  123 + val formats: List<Int>? = call.argument<List<Int>>("formats")
  124 + val returnImage: Boolean = call.argument<Boolean>("returnImage") ?: false
  125 + val speed: Int = call.argument<Int>("speed") ?: 1
  126 + val timeout: Int = call.argument<Int>("timeout") ?: 250
  127 +
  128 + var barcodeScannerOptions: BarcodeScannerOptions? = null
  129 + if (formats != null) {
  130 + val formatsList: MutableList<Int> = mutableListOf()
  131 + for (index in formats) {
  132 + formatsList.add(BarcodeFormats.values()[index].intValue)
  133 + }
  134 + barcodeScannerOptions = if (formatsList.size == 1) {
  135 + BarcodeScannerOptions.Builder().setBarcodeFormats(formatsList.first())
  136 + .build()
  137 + } else {
  138 + BarcodeScannerOptions.Builder().setBarcodeFormats(
  139 + formatsList.first(),
  140 + *formatsList.subList(1, formatsList.size).toIntArray()
  141 + ).build()
  142 + }
  143 + }
  144 +
  145 + val position =
  146 + if (facing == 0) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA
  147 +
  148 + val detectionSpeed: DetectionSpeed = DetectionSpeed.values().first { it.intValue == speed}
  149 +
  150 + try {
  151 + mobileScanner!!.start(barcodeScannerOptions, returnImage, position, torch, detectionSpeed, torchStateCallback, mobileScannerStartedCallback = {
  152 + result.success(mapOf(
  153 + "textureId" to it.id,
  154 + "size" to mapOf("width" to it.width, "height" to it.height),
  155 + "torchable" to it.hasFlashUnit
  156 + ))
  157 + },
  158 + timeout.toLong())
  159 +
  160 + } catch (e: AlreadyStarted) {
  161 + result.error(
  162 + "MobileScanner",
  163 + "Called start() while already started",
  164 + null
  165 + )
  166 + } catch (e: NoCamera) {
  167 + result.error(
  168 + "MobileScanner",
  169 + "No camera found or failed to open camera!",
  170 + null
  171 + )
  172 + } catch (e: TorchError) {
  173 + result.error(
  174 + "MobileScanner",
  175 + "Error occurred when setting torch!",
  176 + null
  177 + )
  178 + } catch (e: CameraError) {
  179 + result.error(
  180 + "MobileScanner",
  181 + "Error occurred when setting up camera!",
  182 + null
  183 + )
  184 + } catch (e: Exception) {
  185 + result.error(
  186 + "MobileScanner",
  187 + "Unknown error occurred..",
  188 + null
  189 + )
  190 + }
  191 + }
  192 +
  193 + private fun stop(result: MethodChannel.Result) {
  194 + try {
  195 + mobileScanner!!.stop()
  196 + result.success(null)
  197 + } catch (e: AlreadyStopped) {
  198 + result.success(null)
  199 + }
  200 + }
  201 +
  202 + private fun analyzeImage(call: MethodCall, result: MethodChannel.Result) {
  203 + analyzerResult = result
  204 + val uri = Uri.fromFile(File(call.arguments.toString()))
  205 + mobileScanner!!.analyzeImage(uri, analyzerCallback)
  206 + }
  207 +
  208 + private fun toggleTorch(call: MethodCall, result: MethodChannel.Result) {
  209 + try {
  210 + mobileScanner!!.toggleTorch(call.arguments == 1)
  211 + result.success(null)
  212 + } catch (e: AlreadyStopped) {
  213 + result.error("MobileScanner", "Called toggleTorch() while stopped!", null)
  214 + }
  215 + }
  216 +}
1 package dev.steenbakker.mobile_scanner 1 package dev.steenbakker.mobile_scanner
2 2
3 -import android.Manifest  
4 import android.app.Activity 3 import android.app.Activity
5 import android.content.pm.PackageManager 4 import android.content.pm.PackageManager
6 import android.graphics.Rect 5 import android.graphics.Rect
@@ -11,7 +10,6 @@ import android.util.Log @@ -11,7 +10,6 @@ import android.util.Log
11 import android.view.Surface 10 import android.view.Surface
12 import androidx.camera.core.* 11 import androidx.camera.core.*
13 import androidx.camera.lifecycle.ProcessCameraProvider 12 import androidx.camera.lifecycle.ProcessCameraProvider
14 -import androidx.core.app.ActivityCompat  
15 import androidx.core.content.ContextCompat 13 import androidx.core.content.ContextCompat
16 import androidx.lifecycle.LifecycleOwner 14 import androidx.lifecycle.LifecycleOwner
17 import com.google.mlkit.vision.barcode.BarcodeScannerOptions 15 import com.google.mlkit.vision.barcode.BarcodeScannerOptions
@@ -20,8 +18,6 @@ import com.google.mlkit.vision.barcode.common.Barcode @@ -20,8 +18,6 @@ import com.google.mlkit.vision.barcode.common.Barcode
20 import com.google.mlkit.vision.common.InputImage 18 import com.google.mlkit.vision.common.InputImage
21 import dev.steenbakker.mobile_scanner.objects.DetectionSpeed 19 import dev.steenbakker.mobile_scanner.objects.DetectionSpeed
22 import dev.steenbakker.mobile_scanner.objects.MobileScannerStartParameters 20 import dev.steenbakker.mobile_scanner.objects.MobileScannerStartParameters
23 -import io.flutter.plugin.common.MethodChannel  
24 -import io.flutter.plugin.common.PluginRegistry  
25 import io.flutter.view.TextureRegistry 21 import io.flutter.view.TextureRegistry
26 import kotlin.math.roundToInt 22 import kotlin.math.roundToInt
27 23
@@ -47,19 +43,10 @@ class MobileScanner( @@ -47,19 +43,10 @@ class MobileScanner(
47 private val textureRegistry: TextureRegistry, 43 private val textureRegistry: TextureRegistry,
48 private val mobileScannerCallback: MobileScannerCallback, 44 private val mobileScannerCallback: MobileScannerCallback,
49 private val mobileScannerErrorCallback: MobileScannerErrorCallback 45 private val mobileScannerErrorCallback: MobileScannerErrorCallback
50 -) :  
51 - PluginRegistry.RequestPermissionsResultListener {  
52 - companion object {  
53 - /**  
54 - * When the application's activity is [androidx.fragment.app.FragmentActivity], requestCode can only use the lower 16 bits.  
55 - * @see androidx.fragment.app.FragmentActivity.validateRequestPermissionsRequestCode  
56 - */  
57 - private const val REQUEST_CODE = 0x0786  
58 - } 46 +) {
59 47
60 private var cameraProvider: ProcessCameraProvider? = null 48 private var cameraProvider: ProcessCameraProvider? = null
61 private var camera: Camera? = null 49 private var camera: Camera? = null
62 - private var pendingPermissionResult: MethodChannel.Result? = null  
63 private var preview: Preview? = null 50 private var preview: Preview? = null
64 private var textureEntry: TextureRegistry.SurfaceTextureEntry? = null 51 private var textureEntry: TextureRegistry.SurfaceTextureEntry? = null
65 var scanWindow: List<Float>? = null 52 var scanWindow: List<Float>? = null
@@ -75,54 +62,6 @@ class MobileScanner( @@ -75,54 +62,6 @@ class MobileScanner(
75 private var scanner = BarcodeScanning.getClient() 62 private var scanner = BarcodeScanning.getClient()
76 63
77 /** 64 /**
78 - * Check if we already have camera permission.  
79 - */  
80 - fun hasCameraPermission(): Int {  
81 - // Can't get exact denied or not_determined state without request. Just return not_determined when state isn't authorized  
82 - val hasPermission = ContextCompat.checkSelfPermission(  
83 - activity,  
84 - Manifest.permission.CAMERA  
85 - ) == PackageManager.PERMISSION_GRANTED  
86 -  
87 - return if (hasPermission) {  
88 - 1  
89 - } else {  
90 - 0  
91 - }  
92 - }  
93 -  
94 - /**  
95 - * Request camera permissions.  
96 - */  
97 - fun requestPermission(result: MethodChannel.Result) {  
98 - if(pendingPermissionResult != null) {  
99 - return  
100 - }  
101 -  
102 - pendingPermissionResult = result  
103 - val permissions = arrayOf(Manifest.permission.CAMERA)  
104 - ActivityCompat.requestPermissions(activity, permissions, REQUEST_CODE)  
105 - }  
106 -  
107 - /**  
108 - * Calls the callback after permissions are requested.  
109 - */  
110 - override fun onRequestPermissionsResult(  
111 - requestCode: Int,  
112 - permissions: Array<out String>,  
113 - grantResults: IntArray  
114 - ): Boolean {  
115 - if (requestCode != REQUEST_CODE) {  
116 - return false  
117 - }  
118 -  
119 - pendingPermissionResult?.success(grantResults[0] == PackageManager.PERMISSION_GRANTED)  
120 - pendingPermissionResult = null  
121 -  
122 - return true  
123 - }  
124 -  
125 - /**  
126 * callback for the camera. Every frame is passed through this function. 65 * callback for the camera. Every frame is passed through this function.
127 */ 66 */
128 @ExperimentalGetImage 67 @ExperimentalGetImage
  1 +package dev.steenbakker.mobile_scanner
  2 +
  3 +import android.Manifest.permission
  4 +import android.app.Activity
  5 +import android.content.pm.PackageManager
  6 +import androidx.core.app.ActivityCompat
  7 +import androidx.core.content.ContextCompat
  8 +import io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener
  9 +
  10 +/**
  11 + * This class handles the camera permissions for the Mobile Scanner.
  12 + */
  13 +class MobileScannerPermissions {
  14 + companion object {
  15 + const val CAMERA_ACCESS_DENIED = "CameraAccessDenied"
  16 + const val CAMERA_ACCESS_DENIED_MESSAGE = "Camera access permission was denied."
  17 + const val CAMERA_PERMISSIONS_REQUEST_ONGOING = "CameraPermissionsRequestOngoing"
  18 + const val CAMERA_PERMISSIONS_REQUEST_ONGOING_MESSAGE = "Another request is ongoing and multiple requests cannot be handled at once."
  19 +
  20 + /**
  21 + * When the application's activity is [androidx.fragment.app.FragmentActivity], requestCode can only use the lower 16 bits.
  22 + * @see androidx.fragment.app.FragmentActivity.validateRequestPermissionsRequestCode
  23 + */
  24 + const val REQUEST_CODE = 0x0786
  25 + }
  26 +
  27 + interface ResultCallback {
  28 + fun onResult(errorCode: String?, errorDescription: String?)
  29 + }
  30 +
  31 + private var listener: RequestPermissionsResultListener? = null
  32 +
  33 + fun getPermissionListener(): RequestPermissionsResultListener? {
  34 + return listener
  35 + }
  36 +
  37 + private var ongoing: Boolean = false
  38 +
  39 + fun hasCameraPermission(activity: Activity) : Int {
  40 + val hasPermission = ContextCompat.checkSelfPermission(
  41 + activity,
  42 + permission.CAMERA,
  43 + ) == PackageManager.PERMISSION_GRANTED
  44 +
  45 + return if (hasPermission) {
  46 + 1
  47 + } else {
  48 + 0
  49 + }
  50 + }
  51 +
  52 + fun requestPermission(activity: Activity,
  53 + addPermissionListener: (RequestPermissionsResultListener) -> Unit,
  54 + callback: ResultCallback) {
  55 + if (ongoing) {
  56 + callback.onResult(
  57 + CAMERA_PERMISSIONS_REQUEST_ONGOING, CAMERA_PERMISSIONS_REQUEST_ONGOING_MESSAGE)
  58 + return
  59 + }
  60 +
  61 + if(hasCameraPermission(activity) == 1) {
  62 + // Permissions already exist. Call the callback with success.
  63 + callback.onResult(null, null)
  64 + return
  65 + }
  66 +
  67 + if(listener == null) {
  68 + // Keep track of the listener, so that it can be unregistered later.
  69 + listener = MobileScannerPermissionsListener(
  70 + object: ResultCallback {
  71 + override fun onResult(errorCode: String?, errorDescription: String?) {
  72 + ongoing = false
  73 + callback.onResult(errorCode, errorDescription)
  74 + }
  75 + }
  76 + )
  77 + listener?.let { listener -> addPermissionListener(listener) }
  78 + }
  79 +
  80 + ongoing = true
  81 + ActivityCompat.requestPermissions(
  82 + activity,
  83 + arrayOf(permission.CAMERA),
  84 + REQUEST_CODE
  85 + )
  86 + }
  87 +}
  88 +
  89 +/**
  90 + * This class handles incoming camera permission results.
  91 + */
  92 +@SuppressWarnings("deprecation")
  93 +private class MobileScannerPermissionsListener(
  94 + private val resultCallback: MobileScannerPermissions.ResultCallback,
  95 +): RequestPermissionsResultListener {
  96 + // There's no way to unregister permission listeners in the v1 embedding, so we'll be called
  97 + // duplicate times in cases where the user denies and then grants a permission. Keep track of if
  98 + // we've responded before and bail out of handling the callback manually if this is a repeat
  99 + // call.
  100 + private var alreadyCalled: Boolean = false
  101 +
  102 + override fun onRequestPermissionsResult(
  103 + requestCode: Int,
  104 + permissions: Array<out String>,
  105 + grantResults: IntArray
  106 + ): Boolean {
  107 + if (alreadyCalled || requestCode != MobileScannerPermissions.REQUEST_CODE) {
  108 + return false
  109 + }
  110 +
  111 + alreadyCalled = true
  112 +
  113 + // grantResults could be empty if the permissions request with the user is interrupted
  114 + // https://developer.android.com/reference/android/app/Activity#onRequestPermissionsResult(int,%20java.lang.String[],%20int[])
  115 + if (grantResults.isEmpty() || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
  116 + resultCallback.onResult(
  117 + MobileScannerPermissions.CAMERA_ACCESS_DENIED,
  118 + MobileScannerPermissions.CAMERA_ACCESS_DENIED_MESSAGE)
  119 + } else {
  120 + resultCallback.onResult(null, null)
  121 + }
  122 +
  123 + return true
  124 + }
  125 +}
1 package dev.steenbakker.mobile_scanner 1 package dev.steenbakker.mobile_scanner
2 2
3 -import android.net.Uri  
4 -import androidx.camera.core.CameraSelector  
5 -import androidx.camera.core.ExperimentalGetImage  
6 -import com.google.mlkit.vision.barcode.BarcodeScannerOptions  
7 -import dev.steenbakker.mobile_scanner.objects.BarcodeFormats  
8 -import dev.steenbakker.mobile_scanner.objects.DetectionSpeed  
9 import io.flutter.embedding.engine.plugins.FlutterPlugin 3 import io.flutter.embedding.engine.plugins.FlutterPlugin
10 import io.flutter.embedding.engine.plugins.activity.ActivityAware 4 import io.flutter.embedding.engine.plugins.activity.ActivityAware
11 import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding 5 import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
12 -import io.flutter.plugin.common.MethodCall  
13 -import io.flutter.plugin.common.MethodChannel  
14 -import java.io.File  
15 6
16 /** MobileScannerPlugin */ 7 /** MobileScannerPlugin */
17 -class MobileScannerPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCallHandler {  
18 -  
19 - private var flutterPluginBinding: FlutterPlugin.FlutterPluginBinding? = null 8 +class MobileScannerPlugin : FlutterPlugin, ActivityAware {
20 private var activityPluginBinding: ActivityPluginBinding? = null 9 private var activityPluginBinding: ActivityPluginBinding? = null
  10 + private var flutterPluginBinding: FlutterPlugin.FlutterPluginBinding? = null
  11 + private var methodCallHandler: MethodCallHandlerImpl? = null
21 private var handler: MobileScanner? = null 12 private var handler: MobileScanner? = null
22 private var method: MethodChannel? = null 13 private var method: MethodChannel? = null
23 14
@@ -86,11 +77,6 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCa @@ -86,11 +77,6 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCa
86 } 77 }
87 78
88 override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { 79 override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
89 - method = MethodChannel(binding.binaryMessenger, "dev.steenbakker.mobile_scanner/scanner/method")  
90 - method!!.setMethodCallHandler(this)  
91 -  
92 - barcodeHandler = BarcodeHandler(binding)  
93 -  
94 this.flutterPluginBinding = binding 80 this.flutterPluginBinding = binding
95 } 81 }
96 82
@@ -99,125 +85,32 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCa @@ -99,125 +85,32 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCa
99 } 85 }
100 86
101 override fun onAttachedToActivity(activityPluginBinding: ActivityPluginBinding) { 87 override fun onAttachedToActivity(activityPluginBinding: ActivityPluginBinding) {
102 - handler = MobileScanner(activityPluginBinding.activity, flutterPluginBinding!!.textureRegistry, callback, errorCallback 88 + val binaryMessenger = this.flutterPluginBinding!!.binaryMessenger
  89 +
  90 + methodCallHandler = MethodCallHandlerImpl(
  91 + activityPluginBinding.activity,
  92 + BarcodeHandler(binaryMessenger),
  93 + binaryMessenger,
  94 + MobileScannerPermissions(),
  95 + activityPluginBinding::addRequestPermissionsResultListener,
  96 + this.flutterPluginBinding!!.textureRegistry,
103 ) 97 )
104 - activityPluginBinding.addRequestPermissionsResultListener(handler!!)  
105 98
106 this.activityPluginBinding = activityPluginBinding 99 this.activityPluginBinding = activityPluginBinding
107 } 100 }
108 101
109 - override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {  
110 - onAttachedToActivity(binding)  
111 - }  
112 -  
113 override fun onDetachedFromActivity() { 102 override fun onDetachedFromActivity() {
114 - activityPluginBinding!!.removeRequestPermissionsResultListener(handler!!)  
115 - method!!.setMethodCallHandler(null)  
116 - method = null  
117 - handler = null 103 + methodCallHandler?.dispose(this.activityPluginBinding!!)
  104 + methodCallHandler = null
118 activityPluginBinding = null 105 activityPluginBinding = null
119 } 106 }
120 107
121 - override fun onDetachedFromActivityForConfigChanges() {  
122 - onDetachedFromActivity()  
123 - }  
124 -  
125 - @ExperimentalGetImage  
126 - private fun start(call: MethodCall, result: MethodChannel.Result) {  
127 - val torch: Boolean = call.argument<Boolean>("torch") ?: false  
128 - val facing: Int = call.argument<Int>("facing") ?: 0  
129 - val formats: List<Int>? = call.argument<List<Int>>("formats")  
130 - val returnImage: Boolean = call.argument<Boolean>("returnImage") ?: false  
131 - val speed: Int = call.argument<Int>("speed") ?: 1  
132 - val timeout: Int = call.argument<Int>("timeout") ?: 250  
133 -  
134 - var barcodeScannerOptions: BarcodeScannerOptions? = null  
135 - if (formats != null) {  
136 - val formatsList: MutableList<Int> = mutableListOf()  
137 - for (index in formats) {  
138 - formatsList.add(BarcodeFormats.values()[index].intValue)  
139 - }  
140 - barcodeScannerOptions = if (formatsList.size == 1) {  
141 - BarcodeScannerOptions.Builder().setBarcodeFormats(formatsList.first())  
142 - .build()  
143 - } else {  
144 - BarcodeScannerOptions.Builder().setBarcodeFormats(  
145 - formatsList.first(),  
146 - *formatsList.subList(1, formatsList.size).toIntArray()  
147 - ).build()  
148 - }  
149 - }  
150 -  
151 - val position =  
152 - if (facing == 0) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA  
153 -  
154 - val detectionSpeed: DetectionSpeed = DetectionSpeed.values().first { it.intValue == speed}  
155 -  
156 - try {  
157 - handler!!.start(barcodeScannerOptions, returnImage, position, torch, detectionSpeed, torchStateCallback, mobileScannerStartedCallback = {  
158 - result.success(mapOf(  
159 - "textureId" to it.id,  
160 - "size" to mapOf("width" to it.width, "height" to it.height),  
161 - "torchable" to it.hasFlashUnit  
162 - ))  
163 - },  
164 - timeout.toLong())  
165 -  
166 - } catch (e: AlreadyStarted) {  
167 - result.error(  
168 - "MobileScanner",  
169 - "Called start() while already started",  
170 - null  
171 - )  
172 - } catch (e: NoCamera) {  
173 - result.error(  
174 - "MobileScanner",  
175 - "No camera found or failed to open camera!",  
176 - null  
177 - )  
178 - } catch (e: TorchError) {  
179 - result.error(  
180 - "MobileScanner",  
181 - "Error occurred when setting torch!",  
182 - null  
183 - )  
184 - } catch (e: CameraError) {  
185 - result.error(  
186 - "MobileScanner",  
187 - "Error occurred when setting up camera!",  
188 - null  
189 - )  
190 - } catch (e: Exception) {  
191 - result.error(  
192 - "MobileScanner",  
193 - "Unknown error occurred..",  
194 - null  
195 - )  
196 - }  
197 - }  
198 -  
199 - private fun stop(result: MethodChannel.Result) {  
200 - try {  
201 - handler!!.stop()  
202 - result.success(null)  
203 - } catch (e: AlreadyStopped) {  
204 - result.success(null)  
205 - }  
206 - }  
207 -  
208 - private fun analyzeImage(call: MethodCall, result: MethodChannel.Result) {  
209 - analyzerResult = result  
210 - val uri = Uri.fromFile(File(call.arguments.toString()))  
211 - handler!!.analyzeImage(uri, analyzerCallback) 108 + override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
  109 + onAttachedToActivity(binding)
212 } 110 }
213 111
214 - private fun toggleTorch(call: MethodCall, result: MethodChannel.Result) {  
215 - try {  
216 - handler!!.toggleTorch(call.arguments == 1)  
217 - result.success(null)  
218 - } catch (e: AlreadyStopped) {  
219 - result.error("MobileScanner", "Called toggleTorch() while stopped!", null)  
220 - } 112 + override fun onDetachedFromActivityForConfigChanges() {
  113 + onDetachedFromActivity()
221 } 114 }
222 115
223 private fun setScale(call: MethodCall, result: MethodChannel.Result) { 116 private fun setScale(call: MethodCall, result: MethodChannel.Result) {
1 import 'package:flutter/material.dart'; 1 import 'package:flutter/material.dart';
2 import 'package:image_picker/image_picker.dart'; 2 import 'package:image_picker/image_picker.dart';
3 import 'package:mobile_scanner/mobile_scanner.dart'; 3 import 'package:mobile_scanner/mobile_scanner.dart';
  4 +import 'package:mobile_scanner_example/scanner_error_widget.dart';
4 5
5 class BarcodeListScannerWithController extends StatefulWidget { 6 class BarcodeListScannerWithController extends StatefulWidget {
6 const BarcodeListScannerWithController({Key? key}) : super(key: key); 7 const BarcodeListScannerWithController({Key? key}) : super(key: key);
@@ -31,17 +32,8 @@ class _BarcodeListScannerWithControllerState @@ -31,17 +32,8 @@ class _BarcodeListScannerWithControllerState
31 controller.stop(); 32 controller.stop();
32 } else { 33 } else {
33 controller.start().catchError((error) { 34 controller.start().catchError((error) {
34 - final exception = error as MobileScannerException;  
35 -  
36 - switch (exception.errorCode) {  
37 - case MobileScannerErrorCode.controllerUninitialized:  
38 - break; // This error code is not used by `start()`.  
39 - case MobileScannerErrorCode.genericError:  
40 - debugPrint('Scanner failed to start');  
41 - break;  
42 - case MobileScannerErrorCode.permissionDenied:  
43 - debugPrint('Camera permission denied');  
44 - break; 35 + if (mounted) {
  36 + setState(() {});
45 } 37 }
46 }); 38 });
47 } 39 }
@@ -61,6 +53,9 @@ class _BarcodeListScannerWithControllerState @@ -61,6 +53,9 @@ class _BarcodeListScannerWithControllerState
61 children: [ 53 children: [
62 MobileScanner( 54 MobileScanner(
63 controller: controller, 55 controller: controller,
  56 + errorBuilder: (context, error, child) {
  57 + return ScannerErrorWidget(error: error);
  58 + },
64 fit: BoxFit.contain, 59 fit: BoxFit.contain,
65 onDetect: (barcodeCapture) { 60 onDetect: (barcodeCapture) {
66 setState(() { 61 setState(() {
1 import 'package:flutter/material.dart'; 1 import 'package:flutter/material.dart';
2 import 'package:image_picker/image_picker.dart'; 2 import 'package:image_picker/image_picker.dart';
3 import 'package:mobile_scanner/mobile_scanner.dart'; 3 import 'package:mobile_scanner/mobile_scanner.dart';
  4 +import 'package:mobile_scanner_example/scanner_error_widget.dart';
4 5
5 class BarcodeScannerWithController extends StatefulWidget { 6 class BarcodeScannerWithController extends StatefulWidget {
6 const BarcodeScannerWithController({Key? key}) : super(key: key); 7 const BarcodeScannerWithController({Key? key}) : super(key: key);
@@ -31,17 +32,8 @@ class _BarcodeScannerWithControllerState @@ -31,17 +32,8 @@ class _BarcodeScannerWithControllerState
31 controller.stop(); 32 controller.stop();
32 } else { 33 } else {
33 controller.start().catchError((error) { 34 controller.start().catchError((error) {
34 - final exception = error as MobileScannerException;  
35 -  
36 - switch (exception.errorCode) {  
37 - case MobileScannerErrorCode.controllerUninitialized:  
38 - break; // This error code is not used by `start()`.  
39 - case MobileScannerErrorCode.genericError:  
40 - debugPrint('Scanner failed to start');  
41 - break;  
42 - case MobileScannerErrorCode.permissionDenied:  
43 - debugPrint('Camera permission denied');  
44 - break; 35 + if (mounted) {
  36 + setState(() {});
45 } 37 }
46 }); 38 });
47 } 39 }
@@ -61,6 +53,9 @@ class _BarcodeScannerWithControllerState @@ -61,6 +53,9 @@ class _BarcodeScannerWithControllerState
61 children: [ 53 children: [
62 MobileScanner( 54 MobileScanner(
63 controller: controller, 55 controller: controller,
  56 + errorBuilder: (context, error, child) {
  57 + return ScannerErrorWidget(error: error);
  58 + },
64 fit: BoxFit.contain, 59 fit: BoxFit.contain,
65 onDetect: (barcode) { 60 onDetect: (barcode) {
66 setState(() { 61 setState(() {
@@ -2,6 +2,7 @@ import 'dart:math'; @@ -2,6 +2,7 @@ import 'dart:math';
2 2
3 import 'package:flutter/material.dart'; 3 import 'package:flutter/material.dart';
4 import 'package:mobile_scanner/mobile_scanner.dart'; 4 import 'package:mobile_scanner/mobile_scanner.dart';
  5 +import 'package:mobile_scanner_example/scanner_error_widget.dart';
5 6
6 class BarcodeScannerReturningImage extends StatefulWidget { 7 class BarcodeScannerReturningImage extends StatefulWidget {
7 const BarcodeScannerReturningImage({Key? key}) : super(key: key); 8 const BarcodeScannerReturningImage({Key? key}) : super(key: key);
@@ -33,17 +34,8 @@ class _BarcodeScannerReturningImageState @@ -33,17 +34,8 @@ class _BarcodeScannerReturningImageState
33 controller.stop(); 34 controller.stop();
34 } else { 35 } else {
35 controller.start().catchError((error) { 36 controller.start().catchError((error) {
36 - final exception = error as MobileScannerException;  
37 -  
38 - switch (exception.errorCode) {  
39 - case MobileScannerErrorCode.controllerUninitialized:  
40 - break; // This error code is not used by `start()`.  
41 - case MobileScannerErrorCode.genericError:  
42 - debugPrint('Scanner failed to start');  
43 - break;  
44 - case MobileScannerErrorCode.permissionDenied:  
45 - debugPrint('Camera permission denied');  
46 - break; 37 + if (mounted) {
  38 + setState(() {});
47 } 39 }
48 }); 40 });
49 } 41 }
@@ -83,6 +75,9 @@ class _BarcodeScannerReturningImageState @@ -83,6 +75,9 @@ class _BarcodeScannerReturningImageState
83 children: [ 75 children: [
84 MobileScanner( 76 MobileScanner(
85 controller: controller, 77 controller: controller,
  78 + errorBuilder: (context, error, child) {
  79 + return ScannerErrorWidget(error: error);
  80 + },
86 fit: BoxFit.contain, 81 fit: BoxFit.contain,
87 onDetect: (barcode) { 82 onDetect: (barcode) {
88 setState(() { 83 setState(() {
1 import 'package:flutter/material.dart'; 1 import 'package:flutter/material.dart';
2 import 'package:mobile_scanner/mobile_scanner.dart'; 2 import 'package:mobile_scanner/mobile_scanner.dart';
  3 +import 'package:mobile_scanner_example/scanner_error_widget.dart';
3 4
4 class BarcodeScannerWithoutController extends StatefulWidget { 5 class BarcodeScannerWithoutController extends StatefulWidget {
5 const BarcodeScannerWithoutController({Key? key}) : super(key: key); 6 const BarcodeScannerWithoutController({Key? key}) : super(key: key);
@@ -24,6 +25,9 @@ class _BarcodeScannerWithoutControllerState @@ -24,6 +25,9 @@ class _BarcodeScannerWithoutControllerState
24 children: [ 25 children: [
25 MobileScanner( 26 MobileScanner(
26 fit: BoxFit.contain, 27 fit: BoxFit.contain,
  28 + errorBuilder: (context, error, child) {
  29 + return ScannerErrorWidget(error: error);
  30 + },
27 onDetect: (capture) { 31 onDetect: (capture) {
28 setState(() { 32 setState(() {
29 this.capture = capture; 33 this.capture = capture;
  1 +import 'package:flutter/material.dart';
  2 +import 'package:mobile_scanner/mobile_scanner.dart';
  3 +
  4 +class ScannerErrorWidget extends StatelessWidget {
  5 + const ScannerErrorWidget({Key? key, required this.error}) : super(key: key);
  6 +
  7 + final MobileScannerException error;
  8 +
  9 + @override
  10 + Widget build(BuildContext context) {
  11 + String errorMessage;
  12 +
  13 + switch (error.errorCode) {
  14 + case MobileScannerErrorCode.controllerUninitialized:
  15 + errorMessage = 'Controller not ready.';
  16 + break;
  17 + case MobileScannerErrorCode.permissionDenied:
  18 + errorMessage = 'Permission denied';
  19 + break;
  20 + default:
  21 + errorMessage = 'Generic Error';
  22 + break;
  23 + }
  24 +
  25 + return ColoredBox(
  26 + color: Colors.black,
  27 + child: Center(
  28 + child: Column(
  29 + mainAxisSize: MainAxisSize.min,
  30 + children: [
  31 + const Padding(
  32 + padding: EdgeInsets.only(bottom: 16),
  33 + child: Icon(Icons.error, color: Colors.white),
  34 + ),
  35 + Text(
  36 + errorMessage,
  37 + style: const TextStyle(color: Colors.white),
  38 + ),
  39 + ],
  40 + ),
  41 + ),
  42 + );
  43 + }
  44 +}
@@ -3,9 +3,17 @@ import 'dart:async'; @@ -3,9 +3,17 @@ import 'dart:async';
3 import 'package:flutter/foundation.dart'; 3 import 'package:flutter/foundation.dart';
4 import 'package:flutter/material.dart'; 4 import 'package:flutter/material.dart';
5 import 'package:mobile_scanner/src/mobile_scanner_controller.dart'; 5 import 'package:mobile_scanner/src/mobile_scanner_controller.dart';
  6 +import 'package:mobile_scanner/src/mobile_scanner_exception.dart';
6 import 'package:mobile_scanner/src/objects/barcode_capture.dart'; 7 import 'package:mobile_scanner/src/objects/barcode_capture.dart';
7 import 'package:mobile_scanner/src/objects/mobile_scanner_arguments.dart'; 8 import 'package:mobile_scanner/src/objects/mobile_scanner_arguments.dart';
8 9
  10 +/// The function signature for the error builder.
  11 +typedef MobileScannerErrorBuilder = Widget Function(
  12 + BuildContext,
  13 + MobileScannerException,
  14 + Widget?,
  15 +);
  16 +
9 /// The [MobileScanner] widget displays a live camera preview. 17 /// The [MobileScanner] widget displays a live camera preview.
10 class MobileScanner extends StatefulWidget { 18 class MobileScanner extends StatefulWidget {
11 /// The controller that manages the barcode scanner. 19 /// The controller that manages the barcode scanner.
@@ -13,6 +21,13 @@ class MobileScanner extends StatefulWidget { @@ -13,6 +21,13 @@ class MobileScanner extends StatefulWidget {
13 /// If this is null, the scanner will manage its own controller. 21 /// If this is null, the scanner will manage its own controller.
14 final MobileScannerController? controller; 22 final MobileScannerController? controller;
15 23
  24 + /// The function that builds an error widget when the scanner
  25 + /// could not be started.
  26 + ///
  27 + /// If this is null, defaults to a black [ColoredBox]
  28 + /// with a centered white [Icons.error] icon.
  29 + final MobileScannerErrorBuilder? errorBuilder;
  30 +
16 /// The [BoxFit] for the camera preview. 31 /// The [BoxFit] for the camera preview.
17 /// 32 ///
18 /// Defaults to [BoxFit.cover]. 33 /// Defaults to [BoxFit.cover].
@@ -45,6 +60,7 @@ class MobileScanner extends StatefulWidget { @@ -45,6 +60,7 @@ class MobileScanner extends StatefulWidget {
45 /// and [onBarcodeDetected] callback. 60 /// and [onBarcodeDetected] callback.
46 const MobileScanner({ 61 const MobileScanner({
47 this.controller, 62 this.controller,
  63 + this.errorBuilder,
48 this.fit = BoxFit.cover, 64 this.fit = BoxFit.cover,
49 required this.onDetect, 65 required this.onDetect,
50 @Deprecated('Use onScannerStarted() instead.') this.onStart, 66 @Deprecated('Use onScannerStarted() instead.') this.onStart,
@@ -70,6 +86,23 @@ class _MobileScannerState extends State<MobileScanner> @@ -70,6 +86,23 @@ class _MobileScannerState extends State<MobileScanner>
70 /// when the application comes back to the foreground. 86 /// when the application comes back to the foreground.
71 bool _resumeFromBackground = false; 87 bool _resumeFromBackground = false;
72 88
  89 + MobileScannerException? _startException;
  90 +
  91 + Widget __buildPlaceholderOrError(BuildContext context, Widget? child) {
  92 + final error = _startException;
  93 +
  94 + if (error != null) {
  95 + return widget.errorBuilder?.call(context, error, child) ??
  96 + const ColoredBox(
  97 + color: Colors.black,
  98 + child: Center(child: Icon(Icons.error, color: Colors.white)),
  99 + );
  100 + }
  101 +
  102 + return widget.placeholderBuilder?.call(context, child) ??
  103 + const ColoredBox(color: Colors.black);
  104 + }
  105 +
73 /// Start the given [scanner]. 106 /// Start the given [scanner].
74 void _startScanner(MobileScannerController scanner) { 107 void _startScanner(MobileScannerController scanner) {
75 if (!_controller.autoStart) { 108 if (!_controller.autoStart) {
@@ -82,6 +115,12 @@ class _MobileScannerState extends State<MobileScanner> @@ -82,6 +115,12 @@ class _MobileScannerState extends State<MobileScanner>
82 // ignore: deprecated_member_use_from_same_package 115 // ignore: deprecated_member_use_from_same_package
83 widget.onStart?.call(arguments); 116 widget.onStart?.call(arguments);
84 widget.onScannerStarted?.call(arguments); 117 widget.onScannerStarted?.call(arguments);
  118 + }).catchError((error) {
  119 + if (mounted) {
  120 + setState(() {
  121 + _startException = error as MobileScannerException;
  122 + });
  123 + }
85 }); 124 });
86 } 125 }
87 126
@@ -189,8 +228,7 @@ class _MobileScannerState extends State<MobileScanner> @@ -189,8 +228,7 @@ class _MobileScannerState extends State<MobileScanner>
189 valueListenable: _controller.startArguments, 228 valueListenable: _controller.startArguments,
190 builder: (context, value, child) { 229 builder: (context, value, child) {
191 if (value == null) { 230 if (value == null) {
192 - return widget.placeholderBuilder?.call(context, child) ??  
193 - const ColoredBox(color: Colors.black); 231 + return __buildPlaceholderOrError(context, child);
194 } 232 }
195 233
196 if (widget.scanWindow != null && scanWindow == null) { 234 if (widget.scanWindow != null && scanWindow == null) {
@@ -170,14 +170,25 @@ class MobileScannerController { @@ -170,14 +170,25 @@ class MobileScannerController {
170 .values[await _methodChannel.invokeMethod('state') as int? ?? 0]; 170 .values[await _methodChannel.invokeMethod('state') as int? ?? 0];
171 switch (state) { 171 switch (state) {
172 case MobileScannerState.undetermined: 172 case MobileScannerState.undetermined:
173 - final bool result = 173 + bool result = false;
  174 +
  175 + try {
  176 + result =
174 await _methodChannel.invokeMethod('request') as bool? ?? false; 177 await _methodChannel.invokeMethod('request') as bool? ?? false;
  178 + } catch (error) {
  179 + isStarting = false;
  180 + throw const MobileScannerException(
  181 + errorCode: MobileScannerErrorCode.genericError,
  182 + );
  183 + }
  184 +
175 if (!result) { 185 if (!result) {
176 isStarting = false; 186 isStarting = false;
177 throw const MobileScannerException( 187 throw const MobileScannerException(
178 errorCode: MobileScannerErrorCode.permissionDenied, 188 errorCode: MobileScannerErrorCode.permissionDenied,
179 ); 189 );
180 } 190 }
  191 +
181 break; 192 break;
182 case MobileScannerState.denied: 193 case MobileScannerState.denied:
183 isStarting = false; 194 isStarting = false;
1 name: mobile_scanner 1 name: mobile_scanner
2 description: A universal barcode and QR code scanner for Flutter based on MLKit. Uses CameraX on Android, AVFoundation on iOS and Apple Vision & AVFoundation on macOS. 2 description: A universal barcode and QR code scanner for Flutter based on MLKit. Uses CameraX on Android, AVFoundation on iOS and Apple Vision & AVFoundation on macOS.
3 -version: 3.0.0-beta.3 3 +version: 3.0.0-beta.4
4 repository: https://github.com/juliansteenbakker/mobile_scanner 4 repository: https://github.com/juliansteenbakker/mobile_scanner
5 5
6 environment: 6 environment: