Skip to content

Commit

Permalink
Merge pull request #1139 from navaronbracke/fix_barcode_scanner_leak
Browse files Browse the repository at this point in the history
fix: fix barcode scanner leak on Android
  • Loading branch information
navaronbracke authored Aug 8, 2024
2 parents b3e5b58 + 5bf6bb3 commit 35ce2fa
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 74 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
## NEXT
* Fixed a leak of the barcode scanner on Android.

## 5.1.1
* This release fixes an issue with automatic starts in the examples.

Expand Down
180 changes: 107 additions & 73 deletions android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,20 @@ import android.os.Looper
import android.util.Size
import android.view.Surface
import android.view.WindowManager
import androidx.annotation.VisibleForTesting
import androidx.camera.core.Camera
import androidx.camera.core.CameraSelector
import androidx.camera.core.ExperimentalGetImage
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import androidx.camera.core.Preview
import androidx.camera.core.TorchState
import androidx.camera.core.resolutionselector.AspectRatioStrategy
import androidx.camera.core.resolutionselector.ResolutionSelector
import androidx.camera.core.resolutionselector.ResolutionStrategy
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import com.google.mlkit.vision.barcode.BarcodeScanner
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.barcode.common.Barcode
Expand All @@ -37,20 +38,20 @@ import io.flutter.view.TextureRegistry
import java.io.ByteArrayOutputStream
import kotlin.math.roundToInt


class MobileScanner(
private val activity: Activity,
private val textureRegistry: TextureRegistry,
private val mobileScannerCallback: MobileScannerCallback,
private val mobileScannerErrorCallback: MobileScannerErrorCallback
private val mobileScannerErrorCallback: MobileScannerErrorCallback,
private val barcodeScannerFactory: (options: BarcodeScannerOptions?) -> BarcodeScanner = ::defaultBarcodeScannerFactory,
) {

/// Internal variables
private var cameraProvider: ProcessCameraProvider? = null
private var camera: Camera? = null
private var preview: Preview? = null
private var textureEntry: TextureRegistry.SurfaceTextureEntry? = null
private var scanner = BarcodeScanning.getClient()
private var scanner: BarcodeScanner? = null
private var lastScanned: List<String?>? = null
private var scannerTimeout = false
private var displayListener: DisplayManager.DisplayListener? = null
Expand All @@ -61,6 +62,15 @@ class MobileScanner(
private var detectionTimeout: Long = 250
private var returnImage = false

companion object {
/**
* Create a barcode scanner from the given options.
*/
fun defaultBarcodeScannerFactory(options: BarcodeScannerOptions?) : BarcodeScanner {
return if (options == null) BarcodeScanning.getClient() else BarcodeScanning.getClient(options)
}
}

/**
* callback for the camera. Every frame is passed through this function.
*/
Expand All @@ -76,76 +86,75 @@ class MobileScanner(
scannerTimeout = true
}

scanner.process(inputImage)
.addOnSuccessListener { barcodes ->
scanner?.let {
it.process(inputImage).addOnSuccessListener { barcodes ->
if (detectionSpeed == DetectionSpeed.NO_DUPLICATES) {
val newScannedBarcodes = barcodes.mapNotNull { barcode -> barcode.rawValue }.sorted()
val newScannedBarcodes = barcodes.mapNotNull {
barcode -> barcode.rawValue
}.sorted()

if (newScannedBarcodes == lastScanned) {
// New scanned is duplicate, returning
return@addOnSuccessListener
}
if (newScannedBarcodes.isNotEmpty()) lastScanned = newScannedBarcodes
if (newScannedBarcodes.isNotEmpty()) {
lastScanned = newScannedBarcodes
}
}

val barcodeMap: MutableList<Map<String, Any?>> = mutableListOf()

for (barcode in barcodes) {
if (scanWindow != null) {
val match = isBarcodeInScanWindow(scanWindow!!, barcode, imageProxy)
if (!match) {
continue
} else {
barcodeMap.add(barcode.data)
}
} else {
if (scanWindow == null) {
barcodeMap.add(barcode.data)
continue
}
}


if (barcodeMap.isNotEmpty()) {
if (returnImage) {

val bitmap = Bitmap.createBitmap(mediaImage.width, mediaImage.height, Bitmap.Config.ARGB_8888)

val imageFormat = YuvToRgbConverter(activity.applicationContext)
if (isBarcodeInScanWindow(scanWindow!!, barcode, imageProxy)) {
barcodeMap.add(barcode.data)
}
}

imageFormat.yuvToRgb(mediaImage, bitmap)
if (barcodeMap.isEmpty()) {
return@addOnSuccessListener
}

val bmResult = rotateBitmap(bitmap, camera?.cameraInfo?.sensorRotationDegrees?.toFloat() ?: 90f)
if (!returnImage) {
mobileScannerCallback(
barcodeMap,
null,
null,
null
)
return@addOnSuccessListener
}

val stream = ByteArrayOutputStream()
bmResult.compress(Bitmap.CompressFormat.PNG, 100, stream)
val byteArray = stream.toByteArray()
val bmWidth = bmResult.width
val bmHeight = bmResult.height
bmResult.recycle()
val bitmap = Bitmap.createBitmap(mediaImage.width, mediaImage.height, Bitmap.Config.ARGB_8888)
val imageFormat = YuvToRgbConverter(activity.applicationContext)

imageFormat.yuvToRgb(mediaImage, bitmap)

mobileScannerCallback(
barcodeMap,
byteArray,
bmWidth,
bmHeight
)
val bmResult = rotateBitmap(bitmap, camera?.cameraInfo?.sensorRotationDegrees?.toFloat() ?: 90f)

} else {
val stream = ByteArrayOutputStream()
bmResult.compress(Bitmap.CompressFormat.PNG, 100, stream)
val byteArray = stream.toByteArray()
val bmWidth = bmResult.width
val bmHeight = bmResult.height
bmResult.recycle()

mobileScannerCallback(
barcodeMap,
null,
null,
null
)
}
}
}
.addOnFailureListener { e ->
mobileScannerCallback(
barcodeMap,
byteArray,
bmWidth,
bmHeight
)
}.addOnFailureListener { e ->
mobileScannerErrorCallback(
e.localizedMessage ?: e.toString()
)
}
.addOnCompleteListener { imageProxy.close() }
}.addOnCompleteListener { imageProxy.close() }
}

if (detectionSpeed == DetectionSpeed.NORMAL) {
// Set timer and continue
Expand All @@ -161,7 +170,6 @@ class MobileScanner(
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
}


// scales the scanWindow to the provided inputImage and checks if that scaled
// scanWindow contains the barcode
private fun isBarcodeInScanWindow(
Expand Down Expand Up @@ -240,11 +248,7 @@ class MobileScanner(
}

lastScanned = null
scanner = if (barcodeScannerOptions != null) {
BarcodeScanning.getClient(barcodeScannerOptions)
} else {
BarcodeScanning.getClient()
}
scanner = barcodeScannerFactory(barcodeScannerOptions)

val cameraProviderFuture = ProcessCameraProvider.getInstance(activity)
val executor = ContextCompat.getMainExecutor(activity)
Expand Down Expand Up @@ -408,14 +412,27 @@ class MobileScanner(
}

val owner = activity as LifecycleOwner
camera?.cameraInfo?.torchState?.removeObservers(owner)
// Release the camera observers first.
camera?.cameraInfo?.let {
it.torchState.removeObservers(owner)
it.zoomState.removeObservers(owner)
it.cameraState.removeObservers(owner)
}
// Unbind the camera use cases, the preview is a use case.
// The camera will be closed when the last use case is unbound.
cameraProvider?.unbindAll()
textureEntry?.release()

cameraProvider = null
camera = null
preview = null

// Release the texture for the preview.
textureEntry?.release()
textureEntry = null
cameraProvider = null

// Release the scanner.
scanner?.close()
scanner = null
lastScanned = null
}

private fun isStopped() = camera == null && preview == null
Expand All @@ -439,22 +456,29 @@ class MobileScanner(
/**
* Analyze a single image.
*/
fun analyzeImage(image: Uri, onSuccess: AnalyzerSuccessCallback, onError: AnalyzerErrorCallback) {
fun analyzeImage(
image: Uri,
scannerOptions: BarcodeScannerOptions?,
onSuccess: AnalyzerSuccessCallback,
onError: AnalyzerErrorCallback) {
val inputImage = InputImage.fromFilePath(activity, image)

scanner.process(inputImage)
.addOnSuccessListener { barcodes ->
val barcodeMap = barcodes.map { barcode -> barcode.data }
// Use a short lived scanner instance, which is closed when the analysis is done.
val barcodeScanner: BarcodeScanner = barcodeScannerFactory(scannerOptions)

if (barcodeMap.isNotEmpty()) {
onSuccess(barcodeMap)
} else {
onSuccess(null)
}
}
.addOnFailureListener { e ->
onError(e.localizedMessage ?: e.toString())
barcodeScanner.process(inputImage).addOnSuccessListener { barcodes ->
val barcodeMap = barcodes.map { barcode -> barcode.data }

if (barcodeMap.isEmpty()) {
onSuccess(null)
} else {
onSuccess(barcodeMap)
}
}.addOnFailureListener { e ->
onError(e.localizedMessage ?: e.toString())
}.addOnCompleteListener {
barcodeScanner.close()
}
}

/**
Expand All @@ -474,4 +498,14 @@ class MobileScanner(
camera?.cameraControl?.setZoomRatio(1f)
}

/**
* Dispose of this scanner instance.
*/
fun dispose() {
if (isStopped()) {
return
}

stop() // Defer to the stop method, which disposes all resources anyway.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ class MobileScannerHandler(
fun dispose(activityPluginBinding: ActivityPluginBinding) {
methodChannel?.setMethodCallHandler(null)
methodChannel = null
mobileScanner?.dispose()
mobileScanner = null

val listener: RequestPermissionsResultListener? = permissions.getPermissionListener()
Expand Down Expand Up @@ -242,7 +243,13 @@ class MobileScannerHandler(
analyzerResult = result
val uri = Uri.fromFile(File(call.arguments.toString()))

mobileScanner!!.analyzeImage(uri, analyzeImageSuccessCallback, analyzeImageErrorCallback)
// TODO: parse options from the method call
// See https://github.com/juliansteenbakker/mobile_scanner/issues/1069
mobileScanner!!.analyzeImage(
uri,
null,
analyzeImageSuccessCallback,
analyzeImageErrorCallback)
}

private fun toggleTorch(result: MethodChannel.Result) {
Expand Down

0 comments on commit 35ce2fa

Please sign in to comment.