Navaron Bracke
Committed by GitHub

Merge pull request #1140 from navaronbracke/round_nan_bug

fix: do not crash when getting NaN on Android
1 ## NEXT 1 ## NEXT
2 -* Fixed a leak of the barcode scanner on Android. 2 +* [Android] Fixed a leak of the barcode scanner.
  3 +* [Android] Fixed a crash when encountering invalid numbers for the scan window.
3 4
4 ## 5.1.1 5 ## 5.1.1
5 * This release fixes an issue with automatic starts in the examples. 6 * This release fixes an issue with automatic starts in the examples.
@@ -170,25 +170,35 @@ class MobileScanner( @@ -170,25 +170,35 @@ class MobileScanner(
170 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)
171 } 171 }
172 172
173 - // scales the scanWindow to the provided inputImage and checks if that scaled  
174 - // scanWindow contains the barcode  
175 - 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(
176 scanWindow: List<Float>, 177 scanWindow: List<Float>,
177 barcode: Barcode, 178 barcode: Barcode,
178 inputImage: ImageProxy 179 inputImage: ImageProxy
179 ): 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.
180 val barcodeBoundingBox = barcode.boundingBox ?: return false 183 val barcodeBoundingBox = barcode.boundingBox ?: return false
181 184
182 - val imageWidth = inputImage.height  
183 - val imageHeight = inputImage.width 185 + try {
  186 + val imageWidth = inputImage.height
  187 + val imageHeight = inputImage.width
184 188
185 - val left = (scanWindow[0] * imageWidth).roundToInt()  
186 - val top = (scanWindow[1] * imageHeight).roundToInt()  
187 - val right = (scanWindow[2] * imageWidth).roundToInt()  
188 - 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()
189 193
190 - val scaledScanWindow = Rect(left, top, right, bottom)  
191 - 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 + }
192 } 202 }
193 203
194 // Return the best resolution for the actual device orientation. 204 // Return the best resolution for the actual device orientation.
  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 +}