Add bitmap safety in onCaptureSuccess callback

Track the current bitmap through the decode→rotate→effect pipeline
with a nullable variable. On exception, the in-flight bitmap is
recycled in the catch block to prevent native memory leaks. Errors
are now logged with Log.e and a proper companion TAG.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2026-02-27 15:20:57 +01:00
commit f0249fcd64

View file

@ -5,6 +5,7 @@ import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.graphics.Matrix import android.graphics.Matrix
import android.location.Location import android.location.Location
import android.util.Log
import androidx.camera.core.ImageCapture import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException import androidx.camera.core.ImageCaptureException
import androidx.camera.core.ImageProxy import androidx.camera.core.ImageProxy
@ -28,6 +29,10 @@ class ImageCaptureHandler(
private val photoSaver: PhotoSaver private val photoSaver: PhotoSaver
) { ) {
companion object {
private const val TAG = "ImageCaptureHandler"
}
/** /**
* Holds the processed bitmap ready for saving, produced inside the * Holds the processed bitmap ready for saving, produced inside the
* camera callback (synchronous CPU work) and consumed afterwards * camera callback (synchronous CPU work) and consumed afterwards
@ -58,26 +63,30 @@ class ImageCaptureHandler(
executor, executor,
object : ImageCapture.OnImageCapturedCallback() { object : ImageCapture.OnImageCapturedCallback() {
override fun onCaptureSuccess(imageProxy: ImageProxy) { override fun onCaptureSuccess(imageProxy: ImageProxy) {
var currentBitmap: Bitmap? = null
try { try {
val imageRotation = imageProxy.imageInfo.rotationDegrees val imageRotation = imageProxy.imageInfo.rotationDegrees
var bitmap = imageProxyToBitmap(imageProxy) currentBitmap = imageProxyToBitmap(imageProxy)
imageProxy.close() imageProxy.close()
if (bitmap == null) { if (currentBitmap == null) {
continuation.resume( continuation.resume(
SaveResult.Error("Failed to convert image") as Any SaveResult.Error("Failed to convert image") as Any
) )
return return
} }
bitmap = rotateBitmap(bitmap, imageRotation, isFrontCamera) currentBitmap = rotateBitmap(currentBitmap, imageRotation, isFrontCamera)
val processedBitmap = applyTiltShiftEffect(bitmap, blurParams) val processedBitmap = applyTiltShiftEffect(currentBitmap, blurParams)
bitmap.recycle() currentBitmap.recycle()
currentBitmap = null
continuation.resume(ProcessedCapture(processedBitmap)) continuation.resume(ProcessedCapture(processedBitmap))
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Image processing failed", e)
currentBitmap?.recycle()
continuation.resume( continuation.resume(
SaveResult.Error("Capture failed: ${e.message}", e) as Any SaveResult.Error("Capture failed: ${e.message}", e) as Any
) )