Committed by
GitHub
Merge pull request #407 from navaronbracke/fix_android_permission_bug
fix: Android permission bug
Showing
14 changed files
with
492 additions
and
228 deletions
| 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; |
example/lib/scanner_error_widget.dart
0 → 100644
| 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 = | ||
| 174 | - await _methodChannel.invokeMethod('request') as bool? ?? false; | 173 | + bool result = false; |
| 174 | + | ||
| 175 | + try { | ||
| 176 | + result = | ||
| 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: |
-
Please register or login to post a comment