Try Finally blocks to ensure cleanup and prevent OOM crashes

This commit is contained in:
ca-ke 2026-01-26 20:18:06 -04:00
parent f5c90f0e5c
commit 578e77421a

View File

@ -91,49 +91,73 @@ class CameraCaptureManager(private val context: Context) {
val (bytes, orientation) = capture.takeJpegWithExif(context.mainExecutor()) val (bytes, orientation) = capture.takeJpegWithExif(context.mainExecutor())
val decoded = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) val decoded = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
?: throw IllegalStateException("UNAVAILABLE: failed to decode captured image") ?: throw IllegalStateException("UNAVAILABLE: failed to decode captured image")
val rotated = rotateBitmapByExif(decoded, orientation)
val scaled = // Wrap bitmap operations in try-finally to guarantee cleanup on all paths
val rotated = try {
rotateBitmapByExif(decoded, orientation)
} catch (e: Exception) {
decoded.recycle()
throw e
}
// Note: if rotation was applied, decoded is already recycled; otherwise rotated === decoded
val scaled: Bitmap
val recycleRotatedSeparately: Boolean
try {
if (maxWidth != null && maxWidth > 0 && rotated.width > maxWidth) { if (maxWidth != null && maxWidth > 0 && rotated.width > maxWidth) {
val h = val h =
(rotated.height.toDouble() * (maxWidth.toDouble() / rotated.width.toDouble())) (rotated.height.toDouble() * (maxWidth.toDouble() / rotated.width.toDouble()))
.toInt() .toInt()
.coerceAtLeast(1) .coerceAtLeast(1)
rotated.scale(maxWidth, h) scaled = rotated.scale(maxWidth, h)
recycleRotatedSeparately = true
} else { } else {
rotated scaled = rotated
recycleRotatedSeparately = false
} }
} catch (e: Exception) {
rotated.recycle()
throw e
}
val maxPayloadBytes = 5 * 1024 * 1024 try {
// Base64 inflates payloads by ~4/3; cap encoded bytes so the payload stays under 5MB (API limit). val maxPayloadBytes = 5 * 1024 * 1024
val maxEncodedBytes = (maxPayloadBytes / 4) * 3 // Base64 inflates payloads by ~4/3; cap encoded bytes so the payload stays under 5MB (API limit).
val result = val maxEncodedBytes = (maxPayloadBytes / 4) * 3
JpegSizeLimiter.compressToLimit( val result =
initialWidth = scaled.width, JpegSizeLimiter.compressToLimit(
initialHeight = scaled.height, initialWidth = scaled.width,
startQuality = (quality * 100.0).roundToInt().coerceIn(10, 100), initialHeight = scaled.height,
maxBytes = maxEncodedBytes, startQuality = (quality * 100.0).roundToInt().coerceIn(10, 100),
encode = { width, height, q -> maxBytes = maxEncodedBytes,
val bitmap = encode = { width, height, q ->
if (width == scaled.width && height == scaled.height) { val bitmap =
scaled if (width == scaled.width && height == scaled.height) {
} else { scaled
scaled.scale(width, height) } else {
scaled.scale(width, height)
}
val out = ByteArrayOutputStream()
if (!bitmap.compress(Bitmap.CompressFormat.JPEG, q, out)) {
if (bitmap !== scaled) bitmap.recycle()
throw IllegalStateException("UNAVAILABLE: failed to encode JPEG")
} }
val out = ByteArrayOutputStream() if (bitmap !== scaled) {
if (!bitmap.compress(Bitmap.CompressFormat.JPEG, q, out)) { bitmap.recycle()
if (bitmap !== scaled) bitmap.recycle() }
throw IllegalStateException("UNAVAILABLE: failed to encode JPEG") out.toByteArray()
} },
if (bitmap !== scaled) { )
bitmap.recycle() val base64 = Base64.encodeToString(result.bytes, Base64.NO_WRAP)
} Payload(
out.toByteArray() """{"format":"jpg","base64":"$base64","width":${result.width},"height":${result.height}}""",
},
) )
val base64 = Base64.encodeToString(result.bytes, Base64.NO_WRAP) } finally {
Payload( scaled.recycle()
"""{"format":"jpg","base64":"$base64","width":${result.width},"height":${result.height}}""", if (recycleRotatedSeparately) {
) rotated.recycle()
}
}
} }
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")