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,21 +151,24 @@ private fun DocumentPreview(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
val bitmap = imageLoader(imageId) val bitmap = imageLoader(imageId)
val imageBitmap = bitmap.asImageBitmap() if (bitmap != null) {
val zoomState = rememberZoomState( val imageBitmap = bitmap.asImageBitmap()
contentSize = Size(bitmap.width.toFloat(), bitmap.height.toFloat())) val zoomState = rememberZoomState(
contentSize = Size(bitmap.width.toFloat(), bitmap.height.toFloat())
)
LaunchedEffect(imageId) { LaunchedEffect(imageId) {
zoomState.reset() zoomState.reset()
}
Image(
bitmap = imageBitmap,
contentDescription = null,
modifier = Modifier
.padding(4.dp)
.align(Alignment.Center)
.zoomable(zoomState)
)
} }
Image(
bitmap = imageBitmap,
contentDescription = null,
modifier = Modifier
.padding(4.dp)
.align(Alignment.Center)
.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,22 +205,26 @@ 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)
val isSelected = index == currentPageIndex.value if (image != null) {
val borderColor = if (isSelected) MaterialTheme.colorScheme.primary else Color.Transparent val bitmap = image.asImageBitmap()
val modifier = val isSelected = index == currentPageIndex.value
if (bitmap.height > bitmap.width) val borderColor =
Modifier.height(120.dp) if (isSelected) MaterialTheme.colorScheme.primary else Color.Transparent
else val modifier =
Modifier.width(120.dp) if (bitmap.height > bitmap.width)
Image( Modifier.height(120.dp)
bitmap = bitmap, else
contentDescription = null, Modifier.width(120.dp)
modifier = modifier Image(
.padding(4.dp) bitmap = bitmap,
.border(2.dp, borderColor) contentDescription = null,
.clickable { currentPageIndex.value = index } modifier = modifier
) .padding(4.dp)
.border(2.dp, borderColor)
.clickable { currentPageIndex.value = index }
)
}
} }
} }
SmallFloatingActionButton( SmallFloatingActionButton(

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)
} }
} }