Fix concurrency, lifecycle, performance, and config issues from audit
Concurrency & bitmap lifecycle: - Defer bitmap recycling by one cycle so Compose finishes drawing before native memory is freed (preview bitmaps, thumbnails) - Make galleryPreviewSource @Volatile for cross-thread visibility - Join preview job before recycling source bitmap in cancelGalleryPreview() to prevent use-after-free during CPU blur loop - Add @Volatile to TiltShiftRenderer.currentTexCoords (UI/GL thread race) - Fix error dismiss race with cancellable Job tracking Lifecycle & resource management: - Release GL resources via glSurfaceView.queueEvent (must run on GL thread) - Pause GLSurfaceView when entering gallery preview mode - Shut down captureExecutor in CameraManager.release() (thread leak) - Use WeakReference for lifecycleOwnerRef to avoid Activity GC delay - Fix thumbnail bitmap leak on coroutine cancellation (add to finally) - Guarantee imageProxy.close() in finally block Performance: - Compute gradient mask at 1/4 resolution with bilinear upscale (~93% less per-pixel trig work, ~75% less mask memory) - Precompute cos/sin on CPU, pass as uCosAngle/uSinAngle uniforms (eliminates per-fragment transcendental calls in GLSL) - Unroll 9-tap Gaussian blur kernel (avoids integer-branched weight lookup that de-optimizes on mobile GPUs) - Add 80ms debounce to preview recomputation during slider drags Silent failure fixes: - Check bitmap.compress() return value; report error on failure - Log all loadBitmapFromUri null paths (stream, dimensions, decode) - Surface preview computation errors and ActivityNotFoundException to user - Return boolean from writeExifToUri, log at ERROR level - Wrap gallery preview downscale in try-catch (OOM protection) Config: - Add ACCESS_MEDIA_LOCATION permission (GPS EXIF on Android 10+) - Accept coarse-only location grant for geotags - Remove dead adjustResize (no effect with edge-to-edge) - Set windowBackground to black (eliminates white flash on cold start) - Add values-night theme for dark mode - Remove overly broad ProGuard keeps (CameraX/GMS ship consumer rules) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
12051b2a83
commit
11a79076bc
13 changed files with 292 additions and 159 deletions
|
|
@ -18,7 +18,9 @@ import androidx.lifecycle.LifecycleOwner
|
|||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
/**
|
||||
|
|
@ -58,11 +60,12 @@ class CameraManager(private val context: Context) {
|
|||
val isFrontCamera: StateFlow<Boolean> = _isFrontCamera.asStateFlow()
|
||||
|
||||
/** Background executor for image capture callbacks to avoid blocking the main thread. */
|
||||
private val captureExecutor: Executor = Executors.newSingleThreadExecutor()
|
||||
private val captureExecutor: ExecutorService = Executors.newSingleThreadExecutor()
|
||||
|
||||
private var surfaceTextureProvider: (() -> SurfaceTexture?)? = null
|
||||
private var surfaceSize: Size = Size(1920, 1080)
|
||||
private var lifecycleOwnerRef: LifecycleOwner? = null
|
||||
/** Weak reference to avoid preventing Activity GC across config changes. */
|
||||
private var lifecycleOwnerRef: WeakReference<LifecycleOwner>? = null
|
||||
|
||||
/**
|
||||
* Starts the camera with the given lifecycle owner.
|
||||
|
|
@ -73,7 +76,7 @@ class CameraManager(private val context: Context) {
|
|||
surfaceTextureProvider: () -> SurfaceTexture?
|
||||
) {
|
||||
this.surfaceTextureProvider = surfaceTextureProvider
|
||||
this.lifecycleOwnerRef = lifecycleOwner
|
||||
this.lifecycleOwnerRef = WeakReference(lifecycleOwner)
|
||||
|
||||
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
|
||||
cameraProviderFuture.addListener({
|
||||
|
|
@ -89,7 +92,10 @@ class CameraManager(private val context: Context) {
|
|||
}
|
||||
|
||||
private fun bindCameraUseCases(lifecycleOwner: LifecycleOwner) {
|
||||
val provider = cameraProvider ?: return
|
||||
val provider = cameraProvider ?: run {
|
||||
Log.w(TAG, "bindCameraUseCases called before camera provider initialized")
|
||||
return
|
||||
}
|
||||
|
||||
// Unbind all use cases before rebinding
|
||||
provider.unbindAll()
|
||||
|
|
@ -164,12 +170,24 @@ class CameraManager(private val context: Context) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Sets the zoom ratio.
|
||||
* Sets the zoom ratio. Updates UI state only after the camera confirms the change.
|
||||
*/
|
||||
fun setZoom(ratio: Float) {
|
||||
val clamped = ratio.coerceIn(_minZoomRatio.value, _maxZoomRatio.value)
|
||||
camera?.cameraControl?.setZoomRatio(clamped)
|
||||
_zoomRatio.value = clamped
|
||||
val future = camera?.cameraControl?.setZoomRatio(clamped)
|
||||
if (future != null) {
|
||||
future.addListener({
|
||||
try {
|
||||
future.get()
|
||||
_zoomRatio.value = clamped
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Zoom operation failed", e)
|
||||
}
|
||||
}, ContextCompat.getMainExecutor(context))
|
||||
} else {
|
||||
// Optimistic update when camera not available (e.g. during init)
|
||||
_zoomRatio.value = clamped
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -185,7 +203,7 @@ class CameraManager(private val context: Context) {
|
|||
fun switchCamera() {
|
||||
_isFrontCamera.value = !_isFrontCamera.value
|
||||
_zoomRatio.value = 1.0f // Reset zoom when switching
|
||||
lifecycleOwnerRef?.let { bindCameraUseCases(it) }
|
||||
lifecycleOwnerRef?.get()?.let { bindCameraUseCases(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -207,10 +225,11 @@ class CameraManager(private val context: Context) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Releases camera resources.
|
||||
* Releases camera resources and shuts down the background executor.
|
||||
*/
|
||||
fun release() {
|
||||
cameraProvider?.unbindAll()
|
||||
captureExecutor.shutdown()
|
||||
camera = null
|
||||
preview = null
|
||||
imageCapture = null
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue