Fix bitmap recycle race condition and startActivity crash

- Null the Compose state reference before recycling bitmaps to prevent
  the renderer from drawing a recycled bitmap between recycle() and
  the state update
- Wrap ACTION_VIEW startActivity in try-catch for devices without
  an image viewer installed

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2026-03-05 11:56:29 +01:00
commit 5e08fb9c13

View file

@ -6,6 +6,7 @@ import android.graphics.SurfaceTexture
import android.location.Location import android.location.Location
import android.net.Uri import android.net.Uri
import android.opengl.GLSurfaceView import android.opengl.GLSurfaceView
import android.util.Log
import android.view.Surface import android.view.Surface
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
@ -359,9 +360,10 @@ fun CameraScreen(
// Cancel button // Cancel button
IconButton( IconButton(
onClick = { onClick = {
galleryBitmap?.recycle() val oldBitmap = galleryBitmap
galleryBitmap = null galleryBitmap = null
galleryImageUri = null galleryImageUri = null
oldBitmap?.recycle()
}, },
modifier = Modifier modifier = Modifier
.size(56.dp) .size(56.dp)
@ -392,9 +394,10 @@ fun CameraScreen(
when (result) { when (result) {
is SaveResult.Success -> { is SaveResult.Success -> {
haptics.success() haptics.success()
lastThumbnailBitmap?.recycle() val oldThumb = lastThumbnailBitmap
lastThumbnailBitmap = result.thumbnail lastThumbnailBitmap = result.thumbnail
lastSavedUri = result.uri lastSavedUri = result.uri
oldThumb?.recycle()
showSaveSuccess = true showSaveSuccess = true
delay(1500) delay(1500)
showSaveSuccess = false showSaveSuccess = false
@ -406,9 +409,10 @@ fun CameraScreen(
showSaveError = null showSaveError = null
} }
} }
galleryBitmap?.recycle() val oldGalleryBitmap = galleryBitmap
galleryBitmap = null galleryBitmap = null
galleryImageUri = null galleryImageUri = null
oldGalleryBitmap?.recycle()
isCapturing = false isCapturing = false
} }
} }
@ -492,9 +496,10 @@ fun CameraScreen(
when (result) { when (result) {
is SaveResult.Success -> { is SaveResult.Success -> {
haptics.success() haptics.success()
lastThumbnailBitmap?.recycle() val oldThumb = lastThumbnailBitmap
lastThumbnailBitmap = result.thumbnail lastThumbnailBitmap = result.thumbnail
lastSavedUri = result.uri lastSavedUri = result.uri
oldThumb?.recycle()
showSaveSuccess = true showSaveSuccess = true
delay(1500) delay(1500)
showSaveSuccess = false showSaveSuccess = false
@ -524,11 +529,15 @@ fun CameraScreen(
thumbnail = lastThumbnailBitmap, thumbnail = lastThumbnailBitmap,
onTap = { onTap = {
lastSavedUri?.let { uri -> lastSavedUri?.let { uri ->
try {
val intent = Intent(Intent.ACTION_VIEW).apply { val intent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(uri, "image/jpeg") setDataAndType(uri, "image/jpeg")
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
} }
context.startActivity(intent) context.startActivity(intent)
} catch (e: android.content.ActivityNotFoundException) {
Log.w("CameraScreen", "No activity found to view image", e)
}
} }
}, },
modifier = Modifier modifier = Modifier