Avoid a possible crash when deleting an image

This commit is contained in:
Pierre-Yves Nicolas
2025-06-21 17:34:31 +02:00
parent 60d5bc51ef
commit 8ed04238fb
4 changed files with 46 additions and 40 deletions

View File

@@ -39,12 +39,12 @@ class ImageRepository(appFilesDir: File) {
fileNames.add(fileName) fileNames.add(fileName)
} }
fun getContent(id: String): ByteArray { fun getContent(id: String): ByteArray? {
if (fileNames.contains(id)) { if (fileNames.contains(id)) {
val file = File(scanDir, id) val file = File(scanDir, id)
return file.readBytes() return file.readBytes()
} }
throw IllegalArgumentException("No image for id: $id") return null
} }
fun delete(id: String) { fun delete(id: String) {

View File

@@ -137,14 +137,15 @@ class MainViewModel(
fun pageCount(): Int = pageIds.value.size fun pageCount(): Int = pageIds.value.size
fun getBitmap(id: String): Bitmap { fun getBitmap(id: String): Bitmap? {
val bytes = imageRepository.getContent(id) val bytes = imageRepository.getContent(id)
return BitmapFactory.decodeByteArray(bytes, 0, bytes.size) return bytes?.let { BitmapFactory.decodeByteArray(it, 0, it.size) }
} }
fun createPdf(outputStream: OutputStream) { fun createPdf(outputStream: OutputStream) {
val jpegs = imageRepository.imageIds().asSequence() val jpegs = imageRepository.imageIds().asSequence()
.map { id -> imageRepository.getContent(id) } .map { id -> imageRepository.getContent(id) }
.filterNotNull()
writePdfFromJpegs(jpegs, outputStream) writePdfFromJpegs(jpegs, outputStream)
} }
} }

View File

@@ -74,7 +74,7 @@ import org.mydomain.myscan.ui.theme.MyScanTheme
@Composable @Composable
fun DocumentScreen( fun DocumentScreen(
pageIds: List<String>, pageIds: List<String>,
imageLoader: (String) -> Bitmap, imageLoader: (String) -> Bitmap?,
toCameraScreen: () -> Unit, toCameraScreen: () -> Unit,
onSavePressed: () -> Unit, onSavePressed: () -> Unit,
onSharePressed: () -> Unit, onSharePressed: () -> Unit,
@@ -135,7 +135,7 @@ fun DocumentScreen(
@Composable @Composable
private fun DocumentPreview( private fun DocumentPreview(
pageIds: List<String>, pageIds: List<String>,
imageLoader: (String) -> Bitmap, imageLoader: (String) -> Bitmap?,
currentPageIndex: MutableIntState, currentPageIndex: MutableIntState,
onDeleteImage: (String) -> Unit, onDeleteImage: (String) -> Unit,
padding: PaddingValues, padding: PaddingValues,
@@ -151,9 +151,11 @@ private fun DocumentPreview(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
val bitmap = imageLoader(imageId) val bitmap = imageLoader(imageId)
if (bitmap != null) {
val imageBitmap = bitmap.asImageBitmap() val imageBitmap = bitmap.asImageBitmap()
val zoomState = rememberZoomState( val zoomState = rememberZoomState(
contentSize = Size(bitmap.width.toFloat(), bitmap.height.toFloat())) contentSize = Size(bitmap.width.toFloat(), bitmap.height.toFloat())
)
LaunchedEffect(imageId) { LaunchedEffect(imageId) {
zoomState.reset() zoomState.reset()
@@ -166,6 +168,7 @@ private fun DocumentPreview(
.align(Alignment.Center) .align(Alignment.Center)
.zoomable(zoomState) .zoomable(zoomState)
) )
}
SmallFloatingActionButton( SmallFloatingActionButton(
onClick = { onDeleteImage(imageId) }, onClick = { onDeleteImage(imageId) },
modifier = Modifier.align(Alignment.TopEnd).padding(4.dp) modifier = Modifier.align(Alignment.TopEnd).padding(4.dp)
@@ -186,7 +189,7 @@ private fun DocumentPreview(
@Composable @Composable
private fun PageList( private fun PageList(
pageIds: List<String>, pageIds: List<String>,
imageLoader: (String) -> Bitmap, imageLoader: (String) -> Bitmap?,
currentPageIndex: MutableState<Int>, currentPageIndex: MutableState<Int>,
toCameraScreen: () -> Unit toCameraScreen: () -> Unit
) { ) {
@@ -202,9 +205,12 @@ private fun PageList(
) { ) {
itemsIndexed (pageIds) { index, id -> itemsIndexed (pageIds) { index, id ->
// TODO Use small images rather than big ones // TODO Use small images rather than big ones
val bitmap = imageLoader(id).asImageBitmap() val image = imageLoader(id)
if (image != null) {
val bitmap = image.asImageBitmap()
val isSelected = index == currentPageIndex.value val isSelected = index == currentPageIndex.value
val borderColor = if (isSelected) MaterialTheme.colorScheme.primary else Color.Transparent val borderColor =
if (isSelected) MaterialTheme.colorScheme.primary else Color.Transparent
val modifier = val modifier =
if (bitmap.height > bitmap.width) if (bitmap.height > bitmap.width)
Modifier.height(120.dp) Modifier.height(120.dp)
@@ -220,6 +226,7 @@ private fun PageList(
) )
} }
} }
}
SmallFloatingActionButton( SmallFloatingActionButton(
onClick = toCameraScreen, onClick = toCameraScreen,
modifier = Modifier modifier = Modifier

View File

@@ -14,7 +14,6 @@
*/ */
package org.mydomain.myscan package org.mydomain.myscan
import org.assertj.core.api.Assertions
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@@ -73,10 +72,9 @@ class ImageRepositoryTest {
} }
@Test @Test
fun `should throw on invalid id`() { fun `should return null on invalid id`() {
val repo = repo() val repo = repo()
assertThat(repo.imageIds()).isEmpty() assertThat(repo.imageIds()).isEmpty()
Assertions.assertThatThrownBy { repo.getContent("x") } assertThat(repo.getContent("x")).isNull()
.isInstanceOf(IllegalArgumentException::class.java)
} }
} }