Committed by
GitHub
Merge branch 'master' into pause_function
Showing
10 changed files
with
215 additions
and
112 deletions
| @@ -36,7 +36,7 @@ jobs: | @@ -36,7 +36,7 @@ jobs: | ||
| 36 | - uses: subosito/flutter-action@v2.12.0 | 36 | - uses: subosito/flutter-action@v2.12.0 |
| 37 | with: | 37 | with: |
| 38 | cache: true | 38 | cache: true |
| 39 | - flutter-version: '3.19' | 39 | + flutter-version: '3.22' |
| 40 | channel: 'stable' | 40 | channel: 'stable' |
| 41 | - name: Version | 41 | - name: Version |
| 42 | run: flutter doctor -v | 42 | run: flutter doctor -v |
| 1 | +## NEXT | ||
| 2 | +* This release requires Flutter 3.22.0 and Dart 3.4. | ||
| 3 | + | ||
| 4 | +* [Android] Fixed a leak of the barcode scanner. | ||
| 5 | +* [Android] Fixed a crash when encountering invalid numbers for the scan window. | ||
| 6 | +* [Web] Migrates `package:web` to 1.0.0. | ||
| 7 | + | ||
| 1 | ## 5.1.1 | 8 | ## 5.1.1 |
| 2 | * This release fixes an issue with automatic starts in the examples. | 9 | * This release fixes an issue with automatic starts in the examples. |
| 3 | 10 |
| @@ -13,6 +13,7 @@ import android.os.Looper | @@ -13,6 +13,7 @@ import android.os.Looper | ||
| 13 | import android.util.Size | 13 | import android.util.Size |
| 14 | import android.view.Surface | 14 | import android.view.Surface |
| 15 | import android.view.WindowManager | 15 | import android.view.WindowManager |
| 16 | +import androidx.annotation.VisibleForTesting | ||
| 16 | import androidx.camera.core.Camera | 17 | import androidx.camera.core.Camera |
| 17 | import androidx.camera.core.CameraSelector | 18 | import androidx.camera.core.CameraSelector |
| 18 | import androidx.camera.core.ExperimentalGetImage | 19 | import androidx.camera.core.ExperimentalGetImage |
| @@ -20,12 +21,12 @@ import androidx.camera.core.ImageAnalysis | @@ -20,12 +21,12 @@ import androidx.camera.core.ImageAnalysis | ||
| 20 | import androidx.camera.core.ImageProxy | 21 | import androidx.camera.core.ImageProxy |
| 21 | import androidx.camera.core.Preview | 22 | import androidx.camera.core.Preview |
| 22 | import androidx.camera.core.TorchState | 23 | import androidx.camera.core.TorchState |
| 23 | -import androidx.camera.core.resolutionselector.AspectRatioStrategy | ||
| 24 | import androidx.camera.core.resolutionselector.ResolutionSelector | 24 | import androidx.camera.core.resolutionselector.ResolutionSelector |
| 25 | import androidx.camera.core.resolutionselector.ResolutionStrategy | 25 | import androidx.camera.core.resolutionselector.ResolutionStrategy |
| 26 | import androidx.camera.lifecycle.ProcessCameraProvider | 26 | import androidx.camera.lifecycle.ProcessCameraProvider |
| 27 | import androidx.core.content.ContextCompat | 27 | import androidx.core.content.ContextCompat |
| 28 | import androidx.lifecycle.LifecycleOwner | 28 | import androidx.lifecycle.LifecycleOwner |
| 29 | +import com.google.mlkit.vision.barcode.BarcodeScanner | ||
| 29 | import com.google.mlkit.vision.barcode.BarcodeScannerOptions | 30 | import com.google.mlkit.vision.barcode.BarcodeScannerOptions |
| 30 | import com.google.mlkit.vision.barcode.BarcodeScanning | 31 | import com.google.mlkit.vision.barcode.BarcodeScanning |
| 31 | import com.google.mlkit.vision.barcode.common.Barcode | 32 | import com.google.mlkit.vision.barcode.common.Barcode |
| @@ -37,12 +38,12 @@ import io.flutter.view.TextureRegistry | @@ -37,12 +38,12 @@ import io.flutter.view.TextureRegistry | ||
| 37 | import java.io.ByteArrayOutputStream | 38 | import java.io.ByteArrayOutputStream |
| 38 | import kotlin.math.roundToInt | 39 | import kotlin.math.roundToInt |
| 39 | 40 | ||
| 40 | - | ||
| 41 | class MobileScanner( | 41 | class MobileScanner( |
| 42 | private val activity: Activity, | 42 | private val activity: Activity, |
| 43 | private val textureRegistry: TextureRegistry, | 43 | private val textureRegistry: TextureRegistry, |
| 44 | private val mobileScannerCallback: MobileScannerCallback, | 44 | private val mobileScannerCallback: MobileScannerCallback, |
| 45 | - private val mobileScannerErrorCallback: MobileScannerErrorCallback | 45 | + private val mobileScannerErrorCallback: MobileScannerErrorCallback, |
| 46 | + private val barcodeScannerFactory: (options: BarcodeScannerOptions?) -> BarcodeScanner = ::defaultBarcodeScannerFactory, | ||
| 46 | ) { | 47 | ) { |
| 47 | 48 | ||
| 48 | /// Internal variables | 49 | /// Internal variables |
| @@ -50,7 +51,7 @@ class MobileScanner( | @@ -50,7 +51,7 @@ class MobileScanner( | ||
| 50 | private var camera: Camera? = null | 51 | private var camera: Camera? = null |
| 51 | private var preview: Preview? = null | 52 | private var preview: Preview? = null |
| 52 | private var textureEntry: TextureRegistry.SurfaceTextureEntry? = null | 53 | private var textureEntry: TextureRegistry.SurfaceTextureEntry? = null |
| 53 | - private var scanner = BarcodeScanning.getClient() | 54 | + private var scanner: BarcodeScanner? = null |
| 54 | private var lastScanned: List<String?>? = null | 55 | private var lastScanned: List<String?>? = null |
| 55 | private var scannerTimeout = false | 56 | private var scannerTimeout = false |
| 56 | private var displayListener: DisplayManager.DisplayListener? = null | 57 | private var displayListener: DisplayManager.DisplayListener? = null |
| @@ -61,6 +62,15 @@ class MobileScanner( | @@ -61,6 +62,15 @@ class MobileScanner( | ||
| 61 | private var detectionTimeout: Long = 250 | 62 | private var detectionTimeout: Long = 250 |
| 62 | private var returnImage = false | 63 | private var returnImage = false |
| 63 | 64 | ||
| 65 | + companion object { | ||
| 66 | + /** | ||
| 67 | + * Create a barcode scanner from the given options. | ||
| 68 | + */ | ||
| 69 | + fun defaultBarcodeScannerFactory(options: BarcodeScannerOptions?) : BarcodeScanner { | ||
| 70 | + return if (options == null) BarcodeScanning.getClient() else BarcodeScanning.getClient(options) | ||
| 71 | + } | ||
| 72 | + } | ||
| 73 | + | ||
| 64 | /** | 74 | /** |
| 65 | * callback for the camera. Every frame is passed through this function. | 75 | * callback for the camera. Every frame is passed through this function. |
| 66 | */ | 76 | */ |
| @@ -76,76 +86,75 @@ class MobileScanner( | @@ -76,76 +86,75 @@ class MobileScanner( | ||
| 76 | scannerTimeout = true | 86 | scannerTimeout = true |
| 77 | } | 87 | } |
| 78 | 88 | ||
| 79 | - scanner.process(inputImage) | ||
| 80 | - .addOnSuccessListener { barcodes -> | 89 | + scanner?.let { |
| 90 | + it.process(inputImage).addOnSuccessListener { barcodes -> | ||
| 81 | if (detectionSpeed == DetectionSpeed.NO_DUPLICATES) { | 91 | if (detectionSpeed == DetectionSpeed.NO_DUPLICATES) { |
| 82 | - val newScannedBarcodes = barcodes.mapNotNull { barcode -> barcode.rawValue }.sorted() | 92 | + val newScannedBarcodes = barcodes.mapNotNull { |
| 93 | + barcode -> barcode.rawValue | ||
| 94 | + }.sorted() | ||
| 95 | + | ||
| 83 | if (newScannedBarcodes == lastScanned) { | 96 | if (newScannedBarcodes == lastScanned) { |
| 84 | // New scanned is duplicate, returning | 97 | // New scanned is duplicate, returning |
| 85 | return@addOnSuccessListener | 98 | return@addOnSuccessListener |
| 86 | } | 99 | } |
| 87 | - if (newScannedBarcodes.isNotEmpty()) lastScanned = newScannedBarcodes | 100 | + if (newScannedBarcodes.isNotEmpty()) { |
| 101 | + lastScanned = newScannedBarcodes | ||
| 102 | + } | ||
| 88 | } | 103 | } |
| 89 | 104 | ||
| 90 | val barcodeMap: MutableList<Map<String, Any?>> = mutableListOf() | 105 | val barcodeMap: MutableList<Map<String, Any?>> = mutableListOf() |
| 91 | 106 | ||
| 92 | for (barcode in barcodes) { | 107 | for (barcode in barcodes) { |
| 93 | - if (scanWindow != null) { | ||
| 94 | - val match = isBarcodeInScanWindow(scanWindow!!, barcode, imageProxy) | ||
| 95 | - if (!match) { | ||
| 96 | - continue | ||
| 97 | - } else { | ||
| 98 | - barcodeMap.add(barcode.data) | ||
| 99 | - } | ||
| 100 | - } else { | 108 | + if (scanWindow == null) { |
| 101 | barcodeMap.add(barcode.data) | 109 | barcodeMap.add(barcode.data) |
| 110 | + continue | ||
| 102 | } | 111 | } |
| 103 | - } | ||
| 104 | - | ||
| 105 | 112 | ||
| 106 | - if (barcodeMap.isNotEmpty()) { | ||
| 107 | - if (returnImage) { | ||
| 108 | - | ||
| 109 | - val bitmap = Bitmap.createBitmap(mediaImage.width, mediaImage.height, Bitmap.Config.ARGB_8888) | ||
| 110 | - | ||
| 111 | - val imageFormat = YuvToRgbConverter(activity.applicationContext) | 113 | + if (isBarcodeInScanWindow(scanWindow!!, barcode, imageProxy)) { |
| 114 | + barcodeMap.add(barcode.data) | ||
| 115 | + } | ||
| 116 | + } | ||
| 112 | 117 | ||
| 113 | - imageFormat.yuvToRgb(mediaImage, bitmap) | 118 | + if (barcodeMap.isEmpty()) { |
| 119 | + return@addOnSuccessListener | ||
| 120 | + } | ||
| 114 | 121 | ||
| 115 | - val bmResult = rotateBitmap(bitmap, camera?.cameraInfo?.sensorRotationDegrees?.toFloat() ?: 90f) | 122 | + if (!returnImage) { |
| 123 | + mobileScannerCallback( | ||
| 124 | + barcodeMap, | ||
| 125 | + null, | ||
| 126 | + null, | ||
| 127 | + null | ||
| 128 | + ) | ||
| 129 | + return@addOnSuccessListener | ||
| 130 | + } | ||
| 116 | 131 | ||
| 117 | - val stream = ByteArrayOutputStream() | ||
| 118 | - bmResult.compress(Bitmap.CompressFormat.PNG, 100, stream) | ||
| 119 | - val byteArray = stream.toByteArray() | ||
| 120 | - val bmWidth = bmResult.width | ||
| 121 | - val bmHeight = bmResult.height | ||
| 122 | - bmResult.recycle() | 132 | + val bitmap = Bitmap.createBitmap(mediaImage.width, mediaImage.height, Bitmap.Config.ARGB_8888) |
| 133 | + val imageFormat = YuvToRgbConverter(activity.applicationContext) | ||
| 123 | 134 | ||
| 135 | + imageFormat.yuvToRgb(mediaImage, bitmap) | ||
| 124 | 136 | ||
| 125 | - mobileScannerCallback( | ||
| 126 | - barcodeMap, | ||
| 127 | - byteArray, | ||
| 128 | - bmWidth, | ||
| 129 | - bmHeight | ||
| 130 | - ) | 137 | + val bmResult = rotateBitmap(bitmap, camera?.cameraInfo?.sensorRotationDegrees?.toFloat() ?: 90f) |
| 131 | 138 | ||
| 132 | - } else { | 139 | + val stream = ByteArrayOutputStream() |
| 140 | + bmResult.compress(Bitmap.CompressFormat.PNG, 100, stream) | ||
| 141 | + val byteArray = stream.toByteArray() | ||
| 142 | + val bmWidth = bmResult.width | ||
| 143 | + val bmHeight = bmResult.height | ||
| 144 | + bmResult.recycle() | ||
| 133 | 145 | ||
| 134 | - mobileScannerCallback( | ||
| 135 | - barcodeMap, | ||
| 136 | - null, | ||
| 137 | - null, | ||
| 138 | - null | ||
| 139 | - ) | ||
| 140 | - } | ||
| 141 | - } | ||
| 142 | - } | ||
| 143 | - .addOnFailureListener { e -> | 146 | + mobileScannerCallback( |
| 147 | + barcodeMap, | ||
| 148 | + byteArray, | ||
| 149 | + bmWidth, | ||
| 150 | + bmHeight | ||
| 151 | + ) | ||
| 152 | + }.addOnFailureListener { e -> | ||
| 144 | mobileScannerErrorCallback( | 153 | mobileScannerErrorCallback( |
| 145 | e.localizedMessage ?: e.toString() | 154 | e.localizedMessage ?: e.toString() |
| 146 | ) | 155 | ) |
| 147 | - } | ||
| 148 | - .addOnCompleteListener { imageProxy.close() } | 156 | + }.addOnCompleteListener { imageProxy.close() } |
| 157 | + } | ||
| 149 | 158 | ||
| 150 | if (detectionSpeed == DetectionSpeed.NORMAL) { | 159 | if (detectionSpeed == DetectionSpeed.NORMAL) { |
| 151 | // Set timer and continue | 160 | // Set timer and continue |
| @@ -161,26 +170,35 @@ class MobileScanner( | @@ -161,26 +170,35 @@ class MobileScanner( | ||
| 161 | return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true) | 170 | return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true) |
| 162 | } | 171 | } |
| 163 | 172 | ||
| 164 | - | ||
| 165 | - // scales the scanWindow to the provided inputImage and checks if that scaled | ||
| 166 | - // scanWindow contains the barcode | ||
| 167 | - private fun isBarcodeInScanWindow( | 173 | + // Scales the scanWindow to the provided inputImage and checks if that scaled |
| 174 | + // scanWindow contains the barcode. | ||
| 175 | + @VisibleForTesting | ||
| 176 | + fun isBarcodeInScanWindow( | ||
| 168 | scanWindow: List<Float>, | 177 | scanWindow: List<Float>, |
| 169 | barcode: Barcode, | 178 | barcode: Barcode, |
| 170 | inputImage: ImageProxy | 179 | inputImage: ImageProxy |
| 171 | ): Boolean { | 180 | ): Boolean { |
| 181 | + // TODO: use `cornerPoints` instead, since the bounding box is not bound to the coordinate system of the input image | ||
| 182 | + // On iOS we do this correctly, so the calculation should match that. | ||
| 172 | val barcodeBoundingBox = barcode.boundingBox ?: return false | 183 | val barcodeBoundingBox = barcode.boundingBox ?: return false |
| 173 | 184 | ||
| 174 | - val imageWidth = inputImage.height | ||
| 175 | - val imageHeight = inputImage.width | 185 | + try { |
| 186 | + val imageWidth = inputImage.height | ||
| 187 | + val imageHeight = inputImage.width | ||
| 176 | 188 | ||
| 177 | - val left = (scanWindow[0] * imageWidth).roundToInt() | ||
| 178 | - val top = (scanWindow[1] * imageHeight).roundToInt() | ||
| 179 | - val right = (scanWindow[2] * imageWidth).roundToInt() | ||
| 180 | - val bottom = (scanWindow[3] * imageHeight).roundToInt() | 189 | + val left = (scanWindow[0] * imageWidth).roundToInt() |
| 190 | + val top = (scanWindow[1] * imageHeight).roundToInt() | ||
| 191 | + val right = (scanWindow[2] * imageWidth).roundToInt() | ||
| 192 | + val bottom = (scanWindow[3] * imageHeight).roundToInt() | ||
| 181 | 193 | ||
| 182 | - val scaledScanWindow = Rect(left, top, right, bottom) | ||
| 183 | - return scaledScanWindow.contains(barcodeBoundingBox) | 194 | + val scaledScanWindow = Rect(left, top, right, bottom) |
| 195 | + | ||
| 196 | + return scaledScanWindow.contains(barcodeBoundingBox) | ||
| 197 | + } catch (exception: IllegalArgumentException) { | ||
| 198 | + // Rounding of the scan window dimensions can fail, due to encountering NaN. | ||
| 199 | + // If we get NaN, rather than give a false positive, just return false. | ||
| 200 | + return false | ||
| 201 | + } | ||
| 184 | } | 202 | } |
| 185 | 203 | ||
| 186 | // Return the best resolution for the actual device orientation. | 204 | // Return the best resolution for the actual device orientation. |
| @@ -240,11 +258,7 @@ class MobileScanner( | @@ -240,11 +258,7 @@ class MobileScanner( | ||
| 240 | } | 258 | } |
| 241 | 259 | ||
| 242 | lastScanned = null | 260 | lastScanned = null |
| 243 | - scanner = if (barcodeScannerOptions != null) { | ||
| 244 | - BarcodeScanning.getClient(barcodeScannerOptions) | ||
| 245 | - } else { | ||
| 246 | - BarcodeScanning.getClient() | ||
| 247 | - } | 261 | + scanner = barcodeScannerFactory(barcodeScannerOptions) |
| 248 | 262 | ||
| 249 | val cameraProviderFuture = ProcessCameraProvider.getInstance(activity) | 263 | val cameraProviderFuture = ProcessCameraProvider.getInstance(activity) |
| 250 | val executor = ContextCompat.getMainExecutor(activity) | 264 | val executor = ContextCompat.getMainExecutor(activity) |
| @@ -427,12 +441,24 @@ class MobileScanner( | @@ -427,12 +441,24 @@ class MobileScanner( | ||
| 427 | } | 441 | } |
| 428 | 442 | ||
| 429 | val owner = activity as LifecycleOwner | 443 | val owner = activity as LifecycleOwner |
| 430 | - camera?.cameraInfo?.torchState?.removeObservers(owner) | 444 | + // Release the camera observers first. |
| 445 | + camera?.cameraInfo?.let { | ||
| 446 | + it.torchState.removeObservers(owner) | ||
| 447 | + it.zoomState.removeObservers(owner) | ||
| 448 | + it.cameraState.removeObservers(owner) | ||
| 449 | + } | ||
| 450 | + // Unbind the camera use cases, the preview is a use case. | ||
| 451 | + // The camera will be closed when the last use case is unbound. | ||
| 431 | cameraProvider?.unbindAll() | 452 | cameraProvider?.unbindAll() |
| 432 | 453 | ||
| 433 | - camera = null | ||
| 434 | - preview = null | ||
| 435 | - cameraProvider = null | 454 | + // Release the texture for the preview. |
| 455 | + textureEntry?.release() | ||
| 456 | + textureEntry = null | ||
| 457 | + | ||
| 458 | + // Release the scanner. | ||
| 459 | + scanner?.close() | ||
| 460 | + scanner = null | ||
| 461 | + lastScanned = null | ||
| 436 | } | 462 | } |
| 437 | 463 | ||
| 438 | private fun releaseTexture() { | 464 | private fun releaseTexture() { |
| @@ -462,22 +488,29 @@ class MobileScanner( | @@ -462,22 +488,29 @@ class MobileScanner( | ||
| 462 | /** | 488 | /** |
| 463 | * Analyze a single image. | 489 | * Analyze a single image. |
| 464 | */ | 490 | */ |
| 465 | - fun analyzeImage(image: Uri, onSuccess: AnalyzerSuccessCallback, onError: AnalyzerErrorCallback) { | 491 | + fun analyzeImage( |
| 492 | + image: Uri, | ||
| 493 | + scannerOptions: BarcodeScannerOptions?, | ||
| 494 | + onSuccess: AnalyzerSuccessCallback, | ||
| 495 | + onError: AnalyzerErrorCallback) { | ||
| 466 | val inputImage = InputImage.fromFilePath(activity, image) | 496 | val inputImage = InputImage.fromFilePath(activity, image) |
| 467 | 497 | ||
| 468 | - scanner.process(inputImage) | ||
| 469 | - .addOnSuccessListener { barcodes -> | ||
| 470 | - val barcodeMap = barcodes.map { barcode -> barcode.data } | 498 | + // Use a short lived scanner instance, which is closed when the analysis is done. |
| 499 | + val barcodeScanner: BarcodeScanner = barcodeScannerFactory(scannerOptions) | ||
| 471 | 500 | ||
| 472 | - if (barcodeMap.isNotEmpty()) { | ||
| 473 | - onSuccess(barcodeMap) | ||
| 474 | - } else { | ||
| 475 | - onSuccess(null) | ||
| 476 | - } | ||
| 477 | - } | ||
| 478 | - .addOnFailureListener { e -> | ||
| 479 | - onError(e.localizedMessage ?: e.toString()) | 501 | + barcodeScanner.process(inputImage).addOnSuccessListener { barcodes -> |
| 502 | + val barcodeMap = barcodes.map { barcode -> barcode.data } | ||
| 503 | + | ||
| 504 | + if (barcodeMap.isEmpty()) { | ||
| 505 | + onSuccess(null) | ||
| 506 | + } else { | ||
| 507 | + onSuccess(barcodeMap) | ||
| 480 | } | 508 | } |
| 509 | + }.addOnFailureListener { e -> | ||
| 510 | + onError(e.localizedMessage ?: e.toString()) | ||
| 511 | + }.addOnCompleteListener { | ||
| 512 | + barcodeScanner.close() | ||
| 513 | + } | ||
| 481 | } | 514 | } |
| 482 | 515 | ||
| 483 | /** | 516 | /** |
| @@ -497,4 +530,14 @@ class MobileScanner( | @@ -497,4 +530,14 @@ class MobileScanner( | ||
| 497 | camera?.cameraControl?.setZoomRatio(1f) | 530 | camera?.cameraControl?.setZoomRatio(1f) |
| 498 | } | 531 | } |
| 499 | 532 | ||
| 533 | + /** | ||
| 534 | + * Dispose of this scanner instance. | ||
| 535 | + */ | ||
| 536 | + fun dispose() { | ||
| 537 | + if (isStopped()) { | ||
| 538 | + return | ||
| 539 | + } | ||
| 540 | + | ||
| 541 | + stop() // Defer to the stop method, which disposes all resources anyway. | ||
| 542 | + } | ||
| 500 | } | 543 | } |
| @@ -92,6 +92,7 @@ class MobileScannerHandler( | @@ -92,6 +92,7 @@ class MobileScannerHandler( | ||
| 92 | fun dispose(activityPluginBinding: ActivityPluginBinding) { | 92 | fun dispose(activityPluginBinding: ActivityPluginBinding) { |
| 93 | methodChannel?.setMethodCallHandler(null) | 93 | methodChannel?.setMethodCallHandler(null) |
| 94 | methodChannel = null | 94 | methodChannel = null |
| 95 | + mobileScanner?.dispose() | ||
| 95 | mobileScanner = null | 96 | mobileScanner = null |
| 96 | 97 | ||
| 97 | val listener: RequestPermissionsResultListener? = permissions.getPermissionListener() | 98 | val listener: RequestPermissionsResultListener? = permissions.getPermissionListener() |
| @@ -255,7 +256,13 @@ class MobileScannerHandler( | @@ -255,7 +256,13 @@ class MobileScannerHandler( | ||
| 255 | analyzerResult = result | 256 | analyzerResult = result |
| 256 | val uri = Uri.fromFile(File(call.arguments.toString())) | 257 | val uri = Uri.fromFile(File(call.arguments.toString())) |
| 257 | 258 | ||
| 258 | - mobileScanner!!.analyzeImage(uri, analyzeImageSuccessCallback, analyzeImageErrorCallback) | 259 | + // TODO: parse options from the method call |
| 260 | + // See https://github.com/juliansteenbakker/mobile_scanner/issues/1069 | ||
| 261 | + mobileScanner!!.analyzeImage( | ||
| 262 | + uri, | ||
| 263 | + null, | ||
| 264 | + analyzeImageSuccessCallback, | ||
| 265 | + analyzeImageErrorCallback) | ||
| 259 | } | 266 | } |
| 260 | 267 | ||
| 261 | private fun toggleTorch(result: MethodChannel.Result) { | 268 | private fun toggleTorch(result: MethodChannel.Result) { |
| 1 | +package dev.steenbakker.mobile_scanner | ||
| 2 | + | ||
| 3 | +import android.app.Activity | ||
| 4 | +import android.graphics.Rect | ||
| 5 | +import androidx.camera.core.ImageProxy | ||
| 6 | +import com.google.mlkit.vision.barcode.BarcodeScanner | ||
| 7 | +import com.google.mlkit.vision.barcode.BarcodeScannerOptions | ||
| 8 | +import com.google.mlkit.vision.barcode.common.Barcode | ||
| 9 | +import kotlin.test.Test | ||
| 10 | +import org.mockito.Mockito | ||
| 11 | +import io.flutter.view.TextureRegistry | ||
| 12 | +import kotlin.test.expect | ||
| 13 | + | ||
| 14 | +/* | ||
| 15 | + * This demonstrates a simple unit test of the Kotlin portion of this plugin's implementation. | ||
| 16 | + * | ||
| 17 | + * Once you have built the plugin's example app, you can run these tests from the command | ||
| 18 | + * line by running `./gradlew testDebugUnitTest` in the `example/android/` directory, or | ||
| 19 | + * you can run them directly from IDEs that support JUnit such as Android Studio. | ||
| 20 | + */ | ||
| 21 | + | ||
| 22 | +internal class MobileScannerTest { | ||
| 23 | + @Test | ||
| 24 | + fun isBarcodeInScanWindow_canHandleNaNValues() { | ||
| 25 | + val barcodeScannerMock = Mockito.mock(BarcodeScanner::class.java) | ||
| 26 | + | ||
| 27 | + val mobileScanner = MobileScanner( | ||
| 28 | + Mockito.mock(Activity::class.java), | ||
| 29 | + Mockito.mock(TextureRegistry::class.java), | ||
| 30 | + { _: List<Map<String, Any?>>, _: ByteArray?, _: Int?, _: Int? -> }, | ||
| 31 | + { _: String -> }, | ||
| 32 | + { _: BarcodeScannerOptions? -> barcodeScannerMock } | ||
| 33 | + ) | ||
| 34 | + | ||
| 35 | + // Intentional suppression for the mock value in the test, | ||
| 36 | + // since there is no NaN constant. | ||
| 37 | + @Suppress("DIVISION_BY_ZERO") | ||
| 38 | + val notANumber = 0.0f / 0.0f | ||
| 39 | + | ||
| 40 | + val barcodeMock: Barcode = Mockito.mock(Barcode::class.java) | ||
| 41 | + val imageMock: ImageProxy = Mockito.mock(ImageProxy::class.java) | ||
| 42 | + | ||
| 43 | + // TODO: use corner points instead of bounding box | ||
| 44 | + | ||
| 45 | + // Bounding box that is 100 pixels offset from the left and top, | ||
| 46 | + // and is 100 pixels in width and height. | ||
| 47 | + Mockito.`when`(barcodeMock.boundingBox).thenReturn( | ||
| 48 | + Rect(100, 100, 200, 300)) | ||
| 49 | + Mockito.`when`(imageMock.height).thenReturn(400) | ||
| 50 | + Mockito.`when`(imageMock.width).thenReturn(400) | ||
| 51 | + | ||
| 52 | + // Use a scan window that has an invalid value, but otherwise uses the entire image. | ||
| 53 | + val scanWindow: List<Float> = listOf(0f, notANumber, 100f, 100f) | ||
| 54 | + | ||
| 55 | + expect(false) { | ||
| 56 | + mobileScanner.isBarcodeInScanWindow(scanWindow, barcodeMock, imageMock) | ||
| 57 | + } | ||
| 58 | + } | ||
| 59 | +} |
| @@ -5,7 +5,7 @@ Demonstrates how to use the mobile_scanner plugin. | @@ -5,7 +5,7 @@ Demonstrates how to use the mobile_scanner plugin. | ||
| 5 | ## Run Examples | 5 | ## Run Examples |
| 6 | 6 | ||
| 7 | 1. `git clone https://github.com/juliansteenbakker/mobile_scanner.git` | 7 | 1. `git clone https://github.com/juliansteenbakker/mobile_scanner.git` |
| 8 | -2. `cd mobile_scanner/examples/lib` | 8 | +2. `cd mobile_scanner/example/lib` |
| 9 | 3. `flutter pub get` | 9 | 3. `flutter pub get` |
| 10 | 4. `flutter run` | 10 | 4. `flutter run` |
| 11 | 11 |
| @@ -31,29 +31,8 @@ | @@ -31,29 +31,8 @@ | ||
| 31 | 31 | ||
| 32 | <title>mobile_scanner_example</title> | 32 | <title>mobile_scanner_example</title> |
| 33 | <link rel="manifest" href="manifest.json"> | 33 | <link rel="manifest" href="manifest.json"> |
| 34 | - | ||
| 35 | - <script> | ||
| 36 | - // The value below is injected by flutter build, do not touch. | ||
| 37 | - const serviceWorkerVersion = null; | ||
| 38 | - </script> | ||
| 39 | - <!-- This script adds the flutter initialization JS code --> | ||
| 40 | - <script src="flutter.js" defer></script> | ||
| 41 | </head> | 34 | </head> |
| 42 | <body> | 35 | <body> |
| 43 | - <script> | ||
| 44 | - window.addEventListener('load', function(ev) { | ||
| 45 | - // Download main.dart.js | ||
| 46 | - _flutter.loader.loadEntrypoint({ | ||
| 47 | - serviceWorker: { | ||
| 48 | - serviceWorkerVersion: serviceWorkerVersion, | ||
| 49 | - }, | ||
| 50 | - onEntrypointLoaded: function(engineInitializer) { | ||
| 51 | - engineInitializer.initializeEngine().then(function(appRunner) { | ||
| 52 | - appRunner.runApp(); | ||
| 53 | - }); | ||
| 54 | - } | ||
| 55 | - }); | ||
| 56 | - }); | ||
| 57 | - </script> | 36 | + <script src="flutter_bootstrap.js" async></script> |
| 58 | </body> | 37 | </body> |
| 59 | </html> | 38 | </html> |
| @@ -292,6 +292,7 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { | @@ -292,6 +292,7 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { | ||
| 292 | formats: formats, | 292 | formats: formats, |
| 293 | returnImage: returnImage, | 293 | returnImage: returnImage, |
| 294 | torchEnabled: torchEnabled, | 294 | torchEnabled: torchEnabled, |
| 295 | + useNewCameraSelector: useNewCameraSelector, | ||
| 295 | ); | 296 | ); |
| 296 | 297 | ||
| 297 | try { | 298 | try { |
| @@ -14,6 +14,7 @@ class StartOptions { | @@ -14,6 +14,7 @@ class StartOptions { | ||
| 14 | required this.formats, | 14 | required this.formats, |
| 15 | required this.returnImage, | 15 | required this.returnImage, |
| 16 | required this.torchEnabled, | 16 | required this.torchEnabled, |
| 17 | + required this.useNewCameraSelector, | ||
| 17 | }); | 18 | }); |
| 18 | 19 | ||
| 19 | /// The direction for the camera. | 20 | /// The direction for the camera. |
| @@ -37,6 +38,11 @@ class StartOptions { | @@ -37,6 +38,11 @@ class StartOptions { | ||
| 37 | /// Whether the torch should be turned on when the scanner starts. | 38 | /// Whether the torch should be turned on when the scanner starts. |
| 38 | final bool torchEnabled; | 39 | final bool torchEnabled; |
| 39 | 40 | ||
| 41 | + /// Whether the new resolution selector should be used. | ||
| 42 | + /// | ||
| 43 | + /// This option is only supported on Android. Other platforms will ignore this option. | ||
| 44 | + final bool useNewCameraSelector; | ||
| 45 | + | ||
| 40 | Map<String, Object?> toMap() { | 46 | Map<String, Object?> toMap() { |
| 41 | return <String, Object?>{ | 47 | return <String, Object?>{ |
| 42 | if (cameraResolution != null) | 48 | if (cameraResolution != null) |
| @@ -51,6 +57,7 @@ class StartOptions { | @@ -51,6 +57,7 @@ class StartOptions { | ||
| 51 | 'speed': detectionSpeed.rawValue, | 57 | 'speed': detectionSpeed.rawValue, |
| 52 | 'timeout': detectionTimeoutMs, | 58 | 'timeout': detectionTimeoutMs, |
| 53 | 'torch': torchEnabled, | 59 | 'torch': torchEnabled, |
| 60 | + 'useNewCameraSelector': useNewCameraSelector, | ||
| 54 | }; | 61 | }; |
| 55 | } | 62 | } |
| 56 | } | 63 | } |
| @@ -16,8 +16,8 @@ screenshots: | @@ -16,8 +16,8 @@ screenshots: | ||
| 16 | path: example/screenshots/overlay.png | 16 | path: example/screenshots/overlay.png |
| 17 | 17 | ||
| 18 | environment: | 18 | environment: |
| 19 | - sdk: ">=3.3.0 <4.0.0" | ||
| 20 | - flutter: ">=3.19.0" | 19 | + sdk: ">=3.4.0 <4.0.0" |
| 20 | + flutter: ">=3.22.0" | ||
| 21 | 21 | ||
| 22 | dependencies: | 22 | dependencies: |
| 23 | flutter: | 23 | flutter: |
| @@ -25,7 +25,7 @@ dependencies: | @@ -25,7 +25,7 @@ dependencies: | ||
| 25 | flutter_web_plugins: | 25 | flutter_web_plugins: |
| 26 | sdk: flutter | 26 | sdk: flutter |
| 27 | plugin_platform_interface: ^2.0.2 | 27 | plugin_platform_interface: ^2.0.2 |
| 28 | - web: ^0.5.1 | 28 | + web: ^1.0.0 |
| 29 | 29 | ||
| 30 | dev_dependencies: | 30 | dev_dependencies: |
| 31 | flutter_test: | 31 | flutter_test: |
-
Please register or login to post a comment