Fix runBlocking deadlock in ImageCaptureHandler
runBlocking on the camera callback thread could deadlock when saveBitmap() needed the main thread. Split capturePhoto() into two phases: synchronous CPU work (decode/rotate/effect) inside the suspendCancellableCoroutine callback, and suspend-safe saveBitmap() after the continuation resumes in coroutine context. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b235231390
commit
f53d6f0b1b
1 changed files with 65 additions and 45 deletions
|
|
@ -3,9 +3,7 @@ package no.naiv.tiltshift.camera
|
|||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.Paint
|
||||
import android.location.Location
|
||||
import androidx.camera.core.ImageCapture
|
||||
import androidx.camera.core.ImageCaptureException
|
||||
|
|
@ -30,8 +28,21 @@ class ImageCaptureHandler(
|
|||
private val photoSaver: PhotoSaver
|
||||
) {
|
||||
|
||||
/**
|
||||
* Holds the processed bitmap ready for saving, produced inside the
|
||||
* camera callback (synchronous CPU work) and consumed afterwards
|
||||
* in the caller's coroutine context.
|
||||
*/
|
||||
private class ProcessedCapture(val bitmap: Bitmap)
|
||||
|
||||
/**
|
||||
* Captures a photo and applies the tilt-shift effect.
|
||||
*
|
||||
* Phase 1 (inside suspendCancellableCoroutine / camera callback):
|
||||
* decode → rotate → apply effect (synchronous CPU work only)
|
||||
*
|
||||
* Phase 2 (after continuation resumes, back in coroutine context):
|
||||
* save bitmap via PhotoSaver (suspend-safe)
|
||||
*/
|
||||
suspend fun capturePhoto(
|
||||
imageCapture: ImageCapture,
|
||||
|
|
@ -40,55 +51,64 @@ class ImageCaptureHandler(
|
|||
deviceRotation: Int,
|
||||
location: Location?,
|
||||
isFrontCamera: Boolean
|
||||
): SaveResult = suspendCancellableCoroutine { continuation ->
|
||||
): SaveResult {
|
||||
// Phase 1: capture and process the image synchronously in the callback
|
||||
val captureResult = suspendCancellableCoroutine { continuation ->
|
||||
imageCapture.takePicture(
|
||||
executor,
|
||||
object : ImageCapture.OnImageCapturedCallback() {
|
||||
override fun onCaptureSuccess(imageProxy: ImageProxy) {
|
||||
try {
|
||||
val imageRotation = imageProxy.imageInfo.rotationDegrees
|
||||
|
||||
imageCapture.takePicture(
|
||||
executor,
|
||||
object : ImageCapture.OnImageCapturedCallback() {
|
||||
override fun onCaptureSuccess(imageProxy: ImageProxy) {
|
||||
try {
|
||||
// Get rotation from ImageProxy (sensor orientation)
|
||||
val imageRotation = imageProxy.imageInfo.rotationDegrees
|
||||
var bitmap = imageProxyToBitmap(imageProxy)
|
||||
imageProxy.close()
|
||||
|
||||
// Convert ImageProxy to Bitmap
|
||||
var bitmap = imageProxyToBitmap(imageProxy)
|
||||
imageProxy.close()
|
||||
if (bitmap == null) {
|
||||
continuation.resume(
|
||||
SaveResult.Error("Failed to convert image") as Any
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (bitmap == null) {
|
||||
continuation.resume(SaveResult.Error("Failed to convert image"))
|
||||
return
|
||||
}
|
||||
bitmap = rotateBitmap(bitmap, imageRotation, isFrontCamera)
|
||||
|
||||
// Rotate bitmap to correct orientation
|
||||
// Camera sensor is landscape, we need to rotate for portrait
|
||||
bitmap = rotateBitmap(bitmap, imageRotation, isFrontCamera)
|
||||
val processedBitmap = applyTiltShiftEffect(bitmap, blurParams)
|
||||
bitmap.recycle()
|
||||
|
||||
// Apply tilt-shift effect to the correctly oriented image
|
||||
val processedBitmap = applyTiltShiftEffect(bitmap, blurParams)
|
||||
bitmap.recycle()
|
||||
|
||||
// Save with EXIF orientation as NORMAL (bitmap is already rotated)
|
||||
kotlinx.coroutines.runBlocking {
|
||||
val result = photoSaver.saveBitmap(
|
||||
processedBitmap,
|
||||
ExifInterface.ORIENTATION_NORMAL,
|
||||
location
|
||||
continuation.resume(ProcessedCapture(processedBitmap))
|
||||
} catch (e: Exception) {
|
||||
continuation.resume(
|
||||
SaveResult.Error("Capture failed: ${e.message}", e) as Any
|
||||
)
|
||||
processedBitmap.recycle()
|
||||
continuation.resume(result)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
continuation.resume(SaveResult.Error("Capture failed: ${e.message}", e))
|
||||
}
|
||||
|
||||
override fun onError(exception: ImageCaptureException) {
|
||||
continuation.resume(
|
||||
SaveResult.Error(
|
||||
"Capture failed: ${exception.message}", exception
|
||||
) as Any
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onError(exception: ImageCaptureException) {
|
||||
continuation.resume(
|
||||
SaveResult.Error("Capture failed: ${exception.message}", exception)
|
||||
)
|
||||
}
|
||||
// Phase 2: save to disk in the caller's coroutine context (suspend-safe)
|
||||
if (captureResult is ProcessedCapture) {
|
||||
return try {
|
||||
photoSaver.saveBitmap(
|
||||
captureResult.bitmap,
|
||||
ExifInterface.ORIENTATION_NORMAL,
|
||||
location
|
||||
)
|
||||
} finally {
|
||||
captureResult.bitmap.recycle()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return captureResult as SaveResult
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue