Julian Steenbakker
Committed by GitHub

Merge pull request #407 from navaronbracke/fix_android_permission_bug

fix: Android permission bug
## 3.0.0-beta.4
Fixes:
* Fixes a permission bug on Android where denying the permission would cause an infinite loop of permission requests.
* Updates the example app to handle permission errors with the new builder parameter.
Now it no longer throws uncaught exceptions when the permission is denied.
Features:
* Added a new `errorBuilder` to the `MobileScanner` widget that can be used to customize the error state of the preview.
## 3.0.0-beta.3
Deprecated:
* The `onStart` method has been renamed to `onScannerStarted`.
... ...
... ... @@ -2,15 +2,15 @@ package dev.steenbakker.mobile_scanner
import android.os.Handler
import android.os.Looper
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.EventChannel
class BarcodeHandler(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) : EventChannel.StreamHandler {
class BarcodeHandler(binaryMessenger: BinaryMessenger) : EventChannel.StreamHandler {
private var eventSink: EventChannel.EventSink? = null
private val eventChannel = EventChannel(
flutterPluginBinding.binaryMessenger,
binaryMessenger,
"dev.steenbakker.mobile_scanner/scanner/event"
)
... ...
package dev.steenbakker.mobile_scanner
import android.app.Activity
import android.net.Uri
import androidx.camera.core.CameraSelector
import androidx.camera.core.ExperimentalGetImage
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
import dev.steenbakker.mobile_scanner.objects.BarcodeFormats
import dev.steenbakker.mobile_scanner.objects.DetectionSpeed
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.view.TextureRegistry
import java.io.File
class MethodCallHandlerImpl(
private val activity: Activity,
private val barcodeHandler: BarcodeHandler,
binaryMessenger: BinaryMessenger,
private val permissions: MobileScannerPermissions,
private val addPermissionListener: (RequestPermissionsResultListener) -> Unit,
textureRegistry: TextureRegistry): MethodChannel.MethodCallHandler {
private val analyzerCallback: AnalyzerCallback = { barcodes: List<Map<String, Any?>>?->
if (barcodes != null) {
barcodeHandler.publishEvent(mapOf(
"name" to "barcode",
"data" to barcodes
))
analyzerResult?.success(true)
} else {
analyzerResult?.success(false)
}
analyzerResult = null
}
private var analyzerResult: MethodChannel.Result? = null
private val callback: MobileScannerCallback = { barcodes: List<Map<String, Any?>>, image: ByteArray? ->
if (image != null) {
barcodeHandler.publishEvent(mapOf(
"name" to "barcode",
"data" to barcodes,
"image" to image
))
} else {
barcodeHandler.publishEvent(mapOf(
"name" to "barcode",
"data" to barcodes
))
}
}
private val errorCallback: MobileScannerErrorCallback = {error: String ->
barcodeHandler.publishEvent(mapOf(
"name" to "error",
"data" to error,
))
}
private var methodChannel: MethodChannel? = null
private var mobileScanner: MobileScanner? = null
private val torchStateCallback: TorchStateCallback = {state: Int ->
barcodeHandler.publishEvent(mapOf("name" to "torchState", "data" to state))
}
init {
methodChannel = MethodChannel(binaryMessenger,
"dev.steenbakker.mobile_scanner/scanner/method")
methodChannel!!.setMethodCallHandler(this)
mobileScanner = MobileScanner(activity, textureRegistry, callback, errorCallback)
}
fun dispose(activityPluginBinding: ActivityPluginBinding) {
methodChannel?.setMethodCallHandler(null)
methodChannel = null
mobileScanner = null
val listener: RequestPermissionsResultListener? = permissions.getPermissionListener()
if(listener != null) {
activityPluginBinding.removeRequestPermissionsResultListener(listener)
}
}
@ExperimentalGetImage
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
if (mobileScanner == null) {
result.error("MobileScanner", "Called ${call.method} before initializing.", null)
return
}
when (call.method) {
"state" -> result.success(permissions.hasCameraPermission(activity))
"request" -> permissions.requestPermission(
activity,
addPermissionListener,
object: MobileScannerPermissions.ResultCallback {
override fun onResult(errorCode: String?, errorDescription: String?) {
when(errorCode) {
null -> result.success(true)
MobileScannerPermissions.CAMERA_ACCESS_DENIED -> result.success(false)
else -> result.error(errorCode, errorDescription, null)
}
}
})
"start" -> start(call, result)
"torch" -> toggleTorch(call, result)
"stop" -> stop(result)
"analyzeImage" -> analyzeImage(call, result)
else -> result.notImplemented()
}
}
@ExperimentalGetImage
private fun start(call: MethodCall, result: MethodChannel.Result) {
val torch: Boolean = call.argument<Boolean>("torch") ?: false
val facing: Int = call.argument<Int>("facing") ?: 0
val formats: List<Int>? = call.argument<List<Int>>("formats")
val returnImage: Boolean = call.argument<Boolean>("returnImage") ?: false
val speed: Int = call.argument<Int>("speed") ?: 1
val timeout: Int = call.argument<Int>("timeout") ?: 250
var barcodeScannerOptions: BarcodeScannerOptions? = null
if (formats != null) {
val formatsList: MutableList<Int> = mutableListOf()
for (index in formats) {
formatsList.add(BarcodeFormats.values()[index].intValue)
}
barcodeScannerOptions = if (formatsList.size == 1) {
BarcodeScannerOptions.Builder().setBarcodeFormats(formatsList.first())
.build()
} else {
BarcodeScannerOptions.Builder().setBarcodeFormats(
formatsList.first(),
*formatsList.subList(1, formatsList.size).toIntArray()
).build()
}
}
val position =
if (facing == 0) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA
val detectionSpeed: DetectionSpeed = DetectionSpeed.values().first { it.intValue == speed}
try {
mobileScanner!!.start(barcodeScannerOptions, returnImage, position, torch, detectionSpeed, torchStateCallback, mobileScannerStartedCallback = {
result.success(mapOf(
"textureId" to it.id,
"size" to mapOf("width" to it.width, "height" to it.height),
"torchable" to it.hasFlashUnit
))
},
timeout.toLong())
} catch (e: AlreadyStarted) {
result.error(
"MobileScanner",
"Called start() while already started",
null
)
} catch (e: NoCamera) {
result.error(
"MobileScanner",
"No camera found or failed to open camera!",
null
)
} catch (e: TorchError) {
result.error(
"MobileScanner",
"Error occurred when setting torch!",
null
)
} catch (e: CameraError) {
result.error(
"MobileScanner",
"Error occurred when setting up camera!",
null
)
} catch (e: Exception) {
result.error(
"MobileScanner",
"Unknown error occurred..",
null
)
}
}
private fun stop(result: MethodChannel.Result) {
try {
mobileScanner!!.stop()
result.success(null)
} catch (e: AlreadyStopped) {
result.success(null)
}
}
private fun analyzeImage(call: MethodCall, result: MethodChannel.Result) {
analyzerResult = result
val uri = Uri.fromFile(File(call.arguments.toString()))
mobileScanner!!.analyzeImage(uri, analyzerCallback)
}
private fun toggleTorch(call: MethodCall, result: MethodChannel.Result) {
try {
mobileScanner!!.toggleTorch(call.arguments == 1)
result.success(null)
} catch (e: AlreadyStopped) {
result.error("MobileScanner", "Called toggleTorch() while stopped!", null)
}
}
}
\ No newline at end of file
... ...
package dev.steenbakker.mobile_scanner
import android.Manifest
import android.app.Activity
import android.content.pm.PackageManager
import android.graphics.Rect
... ... @@ -11,7 +10,6 @@ import android.util.Log
import android.view.Surface
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
... ... @@ -20,8 +18,6 @@ import com.google.mlkit.vision.barcode.common.Barcode
import com.google.mlkit.vision.common.InputImage
import dev.steenbakker.mobile_scanner.objects.DetectionSpeed
import dev.steenbakker.mobile_scanner.objects.MobileScannerStartParameters
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.PluginRegistry
import io.flutter.view.TextureRegistry
import kotlin.math.roundToInt
... ... @@ -47,19 +43,10 @@ class MobileScanner(
private val textureRegistry: TextureRegistry,
private val mobileScannerCallback: MobileScannerCallback,
private val mobileScannerErrorCallback: MobileScannerErrorCallback
) :
PluginRegistry.RequestPermissionsResultListener {
companion object {
/**
* When the application's activity is [androidx.fragment.app.FragmentActivity], requestCode can only use the lower 16 bits.
* @see androidx.fragment.app.FragmentActivity.validateRequestPermissionsRequestCode
*/
private const val REQUEST_CODE = 0x0786
}
) {
private var cameraProvider: ProcessCameraProvider? = null
private var camera: Camera? = null
private var pendingPermissionResult: MethodChannel.Result? = null
private var preview: Preview? = null
private var textureEntry: TextureRegistry.SurfaceTextureEntry? = null
var scanWindow: List<Float>? = null
... ... @@ -75,54 +62,6 @@ class MobileScanner(
private var scanner = BarcodeScanning.getClient()
/**
* Check if we already have camera permission.
*/
fun hasCameraPermission(): Int {
// Can't get exact denied or not_determined state without request. Just return not_determined when state isn't authorized
val hasPermission = ContextCompat.checkSelfPermission(
activity,
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED
return if (hasPermission) {
1
} else {
0
}
}
/**
* Request camera permissions.
*/
fun requestPermission(result: MethodChannel.Result) {
if(pendingPermissionResult != null) {
return
}
pendingPermissionResult = result
val permissions = arrayOf(Manifest.permission.CAMERA)
ActivityCompat.requestPermissions(activity, permissions, REQUEST_CODE)
}
/**
* Calls the callback after permissions are requested.
*/
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
): Boolean {
if (requestCode != REQUEST_CODE) {
return false
}
pendingPermissionResult?.success(grantResults[0] == PackageManager.PERMISSION_GRANTED)
pendingPermissionResult = null
return true
}
/**
* callback for the camera. Every frame is passed through this function.
*/
@ExperimentalGetImage
... ...
package dev.steenbakker.mobile_scanner
import android.Manifest.permission
import android.app.Activity
import android.content.pm.PackageManager
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener
/**
* This class handles the camera permissions for the Mobile Scanner.
*/
class MobileScannerPermissions {
companion object {
const val CAMERA_ACCESS_DENIED = "CameraAccessDenied"
const val CAMERA_ACCESS_DENIED_MESSAGE = "Camera access permission was denied."
const val CAMERA_PERMISSIONS_REQUEST_ONGOING = "CameraPermissionsRequestOngoing"
const val CAMERA_PERMISSIONS_REQUEST_ONGOING_MESSAGE = "Another request is ongoing and multiple requests cannot be handled at once."
/**
* When the application's activity is [androidx.fragment.app.FragmentActivity], requestCode can only use the lower 16 bits.
* @see androidx.fragment.app.FragmentActivity.validateRequestPermissionsRequestCode
*/
const val REQUEST_CODE = 0x0786
}
interface ResultCallback {
fun onResult(errorCode: String?, errorDescription: String?)
}
private var listener: RequestPermissionsResultListener? = null
fun getPermissionListener(): RequestPermissionsResultListener? {
return listener
}
private var ongoing: Boolean = false
fun hasCameraPermission(activity: Activity) : Int {
val hasPermission = ContextCompat.checkSelfPermission(
activity,
permission.CAMERA,
) == PackageManager.PERMISSION_GRANTED
return if (hasPermission) {
1
} else {
0
}
}
fun requestPermission(activity: Activity,
addPermissionListener: (RequestPermissionsResultListener) -> Unit,
callback: ResultCallback) {
if (ongoing) {
callback.onResult(
CAMERA_PERMISSIONS_REQUEST_ONGOING, CAMERA_PERMISSIONS_REQUEST_ONGOING_MESSAGE)
return
}
if(hasCameraPermission(activity) == 1) {
// Permissions already exist. Call the callback with success.
callback.onResult(null, null)
return
}
if(listener == null) {
// Keep track of the listener, so that it can be unregistered later.
listener = MobileScannerPermissionsListener(
object: ResultCallback {
override fun onResult(errorCode: String?, errorDescription: String?) {
ongoing = false
callback.onResult(errorCode, errorDescription)
}
}
)
listener?.let { listener -> addPermissionListener(listener) }
}
ongoing = true
ActivityCompat.requestPermissions(
activity,
arrayOf(permission.CAMERA),
REQUEST_CODE
)
}
}
/**
* This class handles incoming camera permission results.
*/
@SuppressWarnings("deprecation")
private class MobileScannerPermissionsListener(
private val resultCallback: MobileScannerPermissions.ResultCallback,
): RequestPermissionsResultListener {
// There's no way to unregister permission listeners in the v1 embedding, so we'll be called
// duplicate times in cases where the user denies and then grants a permission. Keep track of if
// we've responded before and bail out of handling the callback manually if this is a repeat
// call.
private var alreadyCalled: Boolean = false
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
): Boolean {
if (alreadyCalled || requestCode != MobileScannerPermissions.REQUEST_CODE) {
return false
}
alreadyCalled = true
// grantResults could be empty if the permissions request with the user is interrupted
// https://developer.android.com/reference/android/app/Activity#onRequestPermissionsResult(int,%20java.lang.String[],%20int[])
if (grantResults.isEmpty() || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
resultCallback.onResult(
MobileScannerPermissions.CAMERA_ACCESS_DENIED,
MobileScannerPermissions.CAMERA_ACCESS_DENIED_MESSAGE)
} else {
resultCallback.onResult(null, null)
}
return true
}
}
\ No newline at end of file
... ...
package dev.steenbakker.mobile_scanner
import android.net.Uri
import androidx.camera.core.CameraSelector
import androidx.camera.core.ExperimentalGetImage
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
import dev.steenbakker.mobile_scanner.objects.BarcodeFormats
import dev.steenbakker.mobile_scanner.objects.DetectionSpeed
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import java.io.File
/** MobileScannerPlugin */
class MobileScannerPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCallHandler {
private var flutterPluginBinding: FlutterPlugin.FlutterPluginBinding? = null
class MobileScannerPlugin : FlutterPlugin, ActivityAware {
private var activityPluginBinding: ActivityPluginBinding? = null
private var flutterPluginBinding: FlutterPlugin.FlutterPluginBinding? = null
private var methodCallHandler: MethodCallHandlerImpl? = null
private var handler: MobileScanner? = null
private var method: MethodChannel? = null
... ... @@ -86,11 +77,6 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCa
}
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
method = MethodChannel(binding.binaryMessenger, "dev.steenbakker.mobile_scanner/scanner/method")
method!!.setMethodCallHandler(this)
barcodeHandler = BarcodeHandler(binding)
this.flutterPluginBinding = binding
}
... ... @@ -99,125 +85,32 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCa
}
override fun onAttachedToActivity(activityPluginBinding: ActivityPluginBinding) {
handler = MobileScanner(activityPluginBinding.activity, flutterPluginBinding!!.textureRegistry, callback, errorCallback
val binaryMessenger = this.flutterPluginBinding!!.binaryMessenger
methodCallHandler = MethodCallHandlerImpl(
activityPluginBinding.activity,
BarcodeHandler(binaryMessenger),
binaryMessenger,
MobileScannerPermissions(),
activityPluginBinding::addRequestPermissionsResultListener,
this.flutterPluginBinding!!.textureRegistry,
)
activityPluginBinding.addRequestPermissionsResultListener(handler!!)
this.activityPluginBinding = activityPluginBinding
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
onAttachedToActivity(binding)
}
override fun onDetachedFromActivity() {
activityPluginBinding!!.removeRequestPermissionsResultListener(handler!!)
method!!.setMethodCallHandler(null)
method = null
handler = null
methodCallHandler?.dispose(this.activityPluginBinding!!)
methodCallHandler = null
activityPluginBinding = null
}
override fun onDetachedFromActivityForConfigChanges() {
onDetachedFromActivity()
}
@ExperimentalGetImage
private fun start(call: MethodCall, result: MethodChannel.Result) {
val torch: Boolean = call.argument<Boolean>("torch") ?: false
val facing: Int = call.argument<Int>("facing") ?: 0
val formats: List<Int>? = call.argument<List<Int>>("formats")
val returnImage: Boolean = call.argument<Boolean>("returnImage") ?: false
val speed: Int = call.argument<Int>("speed") ?: 1
val timeout: Int = call.argument<Int>("timeout") ?: 250
var barcodeScannerOptions: BarcodeScannerOptions? = null
if (formats != null) {
val formatsList: MutableList<Int> = mutableListOf()
for (index in formats) {
formatsList.add(BarcodeFormats.values()[index].intValue)
}
barcodeScannerOptions = if (formatsList.size == 1) {
BarcodeScannerOptions.Builder().setBarcodeFormats(formatsList.first())
.build()
} else {
BarcodeScannerOptions.Builder().setBarcodeFormats(
formatsList.first(),
*formatsList.subList(1, formatsList.size).toIntArray()
).build()
}
}
val position =
if (facing == 0) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA
val detectionSpeed: DetectionSpeed = DetectionSpeed.values().first { it.intValue == speed}
try {
handler!!.start(barcodeScannerOptions, returnImage, position, torch, detectionSpeed, torchStateCallback, mobileScannerStartedCallback = {
result.success(mapOf(
"textureId" to it.id,
"size" to mapOf("width" to it.width, "height" to it.height),
"torchable" to it.hasFlashUnit
))
},
timeout.toLong())
} catch (e: AlreadyStarted) {
result.error(
"MobileScanner",
"Called start() while already started",
null
)
} catch (e: NoCamera) {
result.error(
"MobileScanner",
"No camera found or failed to open camera!",
null
)
} catch (e: TorchError) {
result.error(
"MobileScanner",
"Error occurred when setting torch!",
null
)
} catch (e: CameraError) {
result.error(
"MobileScanner",
"Error occurred when setting up camera!",
null
)
} catch (e: Exception) {
result.error(
"MobileScanner",
"Unknown error occurred..",
null
)
}
}
private fun stop(result: MethodChannel.Result) {
try {
handler!!.stop()
result.success(null)
} catch (e: AlreadyStopped) {
result.success(null)
}
}
private fun analyzeImage(call: MethodCall, result: MethodChannel.Result) {
analyzerResult = result
val uri = Uri.fromFile(File(call.arguments.toString()))
handler!!.analyzeImage(uri, analyzerCallback)
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
onAttachedToActivity(binding)
}
private fun toggleTorch(call: MethodCall, result: MethodChannel.Result) {
try {
handler!!.toggleTorch(call.arguments == 1)
result.success(null)
} catch (e: AlreadyStopped) {
result.error("MobileScanner", "Called toggleTorch() while stopped!", null)
}
override fun onDetachedFromActivityForConfigChanges() {
onDetachedFromActivity()
}
private fun setScale(call: MethodCall, result: MethodChannel.Result) {
... ...
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:mobile_scanner_example/scanner_error_widget.dart';
class BarcodeListScannerWithController extends StatefulWidget {
const BarcodeListScannerWithController({Key? key}) : super(key: key);
... ... @@ -31,17 +32,8 @@ class _BarcodeListScannerWithControllerState
controller.stop();
} else {
controller.start().catchError((error) {
final exception = error as MobileScannerException;
switch (exception.errorCode) {
case MobileScannerErrorCode.controllerUninitialized:
break; // This error code is not used by `start()`.
case MobileScannerErrorCode.genericError:
debugPrint('Scanner failed to start');
break;
case MobileScannerErrorCode.permissionDenied:
debugPrint('Camera permission denied');
break;
if (mounted) {
setState(() {});
}
});
}
... ... @@ -61,6 +53,9 @@ class _BarcodeListScannerWithControllerState
children: [
MobileScanner(
controller: controller,
errorBuilder: (context, error, child) {
return ScannerErrorWidget(error: error);
},
fit: BoxFit.contain,
onDetect: (barcodeCapture) {
setState(() {
... ...
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:mobile_scanner_example/scanner_error_widget.dart';
class BarcodeScannerWithController extends StatefulWidget {
const BarcodeScannerWithController({Key? key}) : super(key: key);
... ... @@ -31,17 +32,8 @@ class _BarcodeScannerWithControllerState
controller.stop();
} else {
controller.start().catchError((error) {
final exception = error as MobileScannerException;
switch (exception.errorCode) {
case MobileScannerErrorCode.controllerUninitialized:
break; // This error code is not used by `start()`.
case MobileScannerErrorCode.genericError:
debugPrint('Scanner failed to start');
break;
case MobileScannerErrorCode.permissionDenied:
debugPrint('Camera permission denied');
break;
if (mounted) {
setState(() {});
}
});
}
... ... @@ -61,6 +53,9 @@ class _BarcodeScannerWithControllerState
children: [
MobileScanner(
controller: controller,
errorBuilder: (context, error, child) {
return ScannerErrorWidget(error: error);
},
fit: BoxFit.contain,
onDetect: (barcode) {
setState(() {
... ...
... ... @@ -2,6 +2,7 @@ import 'dart:math';
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:mobile_scanner_example/scanner_error_widget.dart';
class BarcodeScannerReturningImage extends StatefulWidget {
const BarcodeScannerReturningImage({Key? key}) : super(key: key);
... ... @@ -33,17 +34,8 @@ class _BarcodeScannerReturningImageState
controller.stop();
} else {
controller.start().catchError((error) {
final exception = error as MobileScannerException;
switch (exception.errorCode) {
case MobileScannerErrorCode.controllerUninitialized:
break; // This error code is not used by `start()`.
case MobileScannerErrorCode.genericError:
debugPrint('Scanner failed to start');
break;
case MobileScannerErrorCode.permissionDenied:
debugPrint('Camera permission denied');
break;
if (mounted) {
setState(() {});
}
});
}
... ... @@ -83,6 +75,9 @@ class _BarcodeScannerReturningImageState
children: [
MobileScanner(
controller: controller,
errorBuilder: (context, error, child) {
return ScannerErrorWidget(error: error);
},
fit: BoxFit.contain,
onDetect: (barcode) {
setState(() {
... ...
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:mobile_scanner_example/scanner_error_widget.dart';
class BarcodeScannerWithoutController extends StatefulWidget {
const BarcodeScannerWithoutController({Key? key}) : super(key: key);
... ... @@ -24,6 +25,9 @@ class _BarcodeScannerWithoutControllerState
children: [
MobileScanner(
fit: BoxFit.contain,
errorBuilder: (context, error, child) {
return ScannerErrorWidget(error: error);
},
onDetect: (capture) {
setState(() {
this.capture = capture;
... ...
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
class ScannerErrorWidget extends StatelessWidget {
const ScannerErrorWidget({Key? key, required this.error}) : super(key: key);
final MobileScannerException error;
@override
Widget build(BuildContext context) {
String errorMessage;
switch (error.errorCode) {
case MobileScannerErrorCode.controllerUninitialized:
errorMessage = 'Controller not ready.';
break;
case MobileScannerErrorCode.permissionDenied:
errorMessage = 'Permission denied';
break;
default:
errorMessage = 'Generic Error';
break;
}
return ColoredBox(
color: Colors.black,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Padding(
padding: EdgeInsets.only(bottom: 16),
child: Icon(Icons.error, color: Colors.white),
),
Text(
errorMessage,
style: const TextStyle(color: Colors.white),
),
],
),
),
);
}
}
... ...
... ... @@ -3,9 +3,17 @@ import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:mobile_scanner/src/mobile_scanner_controller.dart';
import 'package:mobile_scanner/src/mobile_scanner_exception.dart';
import 'package:mobile_scanner/src/objects/barcode_capture.dart';
import 'package:mobile_scanner/src/objects/mobile_scanner_arguments.dart';
/// The function signature for the error builder.
typedef MobileScannerErrorBuilder = Widget Function(
BuildContext,
MobileScannerException,
Widget?,
);
/// The [MobileScanner] widget displays a live camera preview.
class MobileScanner extends StatefulWidget {
/// The controller that manages the barcode scanner.
... ... @@ -13,6 +21,13 @@ class MobileScanner extends StatefulWidget {
/// If this is null, the scanner will manage its own controller.
final MobileScannerController? controller;
/// The function that builds an error widget when the scanner
/// could not be started.
///
/// If this is null, defaults to a black [ColoredBox]
/// with a centered white [Icons.error] icon.
final MobileScannerErrorBuilder? errorBuilder;
/// The [BoxFit] for the camera preview.
///
/// Defaults to [BoxFit.cover].
... ... @@ -45,6 +60,7 @@ class MobileScanner extends StatefulWidget {
/// and [onBarcodeDetected] callback.
const MobileScanner({
this.controller,
this.errorBuilder,
this.fit = BoxFit.cover,
required this.onDetect,
@Deprecated('Use onScannerStarted() instead.') this.onStart,
... ... @@ -70,6 +86,23 @@ class _MobileScannerState extends State<MobileScanner>
/// when the application comes back to the foreground.
bool _resumeFromBackground = false;
MobileScannerException? _startException;
Widget __buildPlaceholderOrError(BuildContext context, Widget? child) {
final error = _startException;
if (error != null) {
return widget.errorBuilder?.call(context, error, child) ??
const ColoredBox(
color: Colors.black,
child: Center(child: Icon(Icons.error, color: Colors.white)),
);
}
return widget.placeholderBuilder?.call(context, child) ??
const ColoredBox(color: Colors.black);
}
/// Start the given [scanner].
void _startScanner(MobileScannerController scanner) {
if (!_controller.autoStart) {
... ... @@ -82,6 +115,12 @@ class _MobileScannerState extends State<MobileScanner>
// ignore: deprecated_member_use_from_same_package
widget.onStart?.call(arguments);
widget.onScannerStarted?.call(arguments);
}).catchError((error) {
if (mounted) {
setState(() {
_startException = error as MobileScannerException;
});
}
});
}
... ... @@ -189,8 +228,7 @@ class _MobileScannerState extends State<MobileScanner>
valueListenable: _controller.startArguments,
builder: (context, value, child) {
if (value == null) {
return widget.placeholderBuilder?.call(context, child) ??
const ColoredBox(color: Colors.black);
return __buildPlaceholderOrError(context, child);
}
if (widget.scanWindow != null && scanWindow == null) {
... ...
... ... @@ -170,14 +170,25 @@ class MobileScannerController {
.values[await _methodChannel.invokeMethod('state') as int? ?? 0];
switch (state) {
case MobileScannerState.undetermined:
final bool result =
bool result = false;
try {
result =
await _methodChannel.invokeMethod('request') as bool? ?? false;
} catch (error) {
isStarting = false;
throw const MobileScannerException(
errorCode: MobileScannerErrorCode.genericError,
);
}
if (!result) {
isStarting = false;
throw const MobileScannerException(
errorCode: MobileScannerErrorCode.permissionDenied,
);
}
break;
case MobileScannerState.denied:
isStarting = false;
... ...
name: mobile_scanner
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.
version: 3.0.0-beta.3
version: 3.0.0-beta.4
repository: https://github.com/juliansteenbakker/mobile_scanner
environment:
... ...