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.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.graphics.Canvas
|
|
||||||
import android.graphics.Matrix
|
import android.graphics.Matrix
|
||||||
import android.graphics.Paint
|
|
||||||
import android.location.Location
|
import android.location.Location
|
||||||
import androidx.camera.core.ImageCapture
|
import androidx.camera.core.ImageCapture
|
||||||
import androidx.camera.core.ImageCaptureException
|
import androidx.camera.core.ImageCaptureException
|
||||||
|
|
@ -30,8 +28,21 @@ class ImageCaptureHandler(
|
||||||
private val photoSaver: PhotoSaver
|
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.
|
* 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(
|
suspend fun capturePhoto(
|
||||||
imageCapture: ImageCapture,
|
imageCapture: ImageCapture,
|
||||||
|
|
@ -40,55 +51,64 @@ class ImageCaptureHandler(
|
||||||
deviceRotation: Int,
|
deviceRotation: Int,
|
||||||
location: Location?,
|
location: Location?,
|
||||||
isFrontCamera: Boolean
|
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(
|
var bitmap = imageProxyToBitmap(imageProxy)
|
||||||
executor,
|
imageProxy.close()
|
||||||
object : ImageCapture.OnImageCapturedCallback() {
|
|
||||||
override fun onCaptureSuccess(imageProxy: ImageProxy) {
|
|
||||||
try {
|
|
||||||
// Get rotation from ImageProxy (sensor orientation)
|
|
||||||
val imageRotation = imageProxy.imageInfo.rotationDegrees
|
|
||||||
|
|
||||||
// Convert ImageProxy to Bitmap
|
if (bitmap == null) {
|
||||||
var bitmap = imageProxyToBitmap(imageProxy)
|
continuation.resume(
|
||||||
imageProxy.close()
|
SaveResult.Error("Failed to convert image") as Any
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (bitmap == null) {
|
bitmap = rotateBitmap(bitmap, imageRotation, isFrontCamera)
|
||||||
continuation.resume(SaveResult.Error("Failed to convert image"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rotate bitmap to correct orientation
|
val processedBitmap = applyTiltShiftEffect(bitmap, blurParams)
|
||||||
// Camera sensor is landscape, we need to rotate for portrait
|
bitmap.recycle()
|
||||||
bitmap = rotateBitmap(bitmap, imageRotation, isFrontCamera)
|
|
||||||
|
|
||||||
// Apply tilt-shift effect to the correctly oriented image
|
continuation.resume(ProcessedCapture(processedBitmap))
|
||||||
val processedBitmap = applyTiltShiftEffect(bitmap, blurParams)
|
} catch (e: Exception) {
|
||||||
bitmap.recycle()
|
continuation.resume(
|
||||||
|
SaveResult.Error("Capture failed: ${e.message}", e) as Any
|
||||||
// Save with EXIF orientation as NORMAL (bitmap is already rotated)
|
|
||||||
kotlinx.coroutines.runBlocking {
|
|
||||||
val result = photoSaver.saveBitmap(
|
|
||||||
processedBitmap,
|
|
||||||
ExifInterface.ORIENTATION_NORMAL,
|
|
||||||
location
|
|
||||||
)
|
)
|
||||||
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) {
|
// Phase 2: save to disk in the caller's coroutine context (suspend-safe)
|
||||||
continuation.resume(
|
if (captureResult is ProcessedCapture) {
|
||||||
SaveResult.Error("Capture failed: ${exception.message}", exception)
|
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