Skip to content

Commit bf31a52

Browse files
committed
Avoid crash on missing file
1 parent 6b92d1f commit bf31a52

File tree

4 files changed

+138
-60
lines changed

4 files changed

+138
-60
lines changed

app/src/main/java/com/ismartcoding/plain/helpers/ImageHelper.kt

Lines changed: 74 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -51,57 +51,67 @@ object ImageHelper {
5151
return ImageType.JPG
5252
}
5353

54-
File(path).inputStream().use {
55-
val totalBytes = it.available()
56-
val bytes = ByteArray(256)
57-
it.read(bytes)
58-
59-
if (bytes[0].toInt() == -1 && bytes[1].toInt() == -40) {
60-
// jpg
61-
return ImageType.JPG
62-
}
54+
val file = File(path)
55+
if (!file.exists()) {
56+
return ImageType.UNKNOWN
57+
}
6358

64-
if (bytes[0].toInt() == -119 && bytes[1].toInt() == 80) {
65-
// png
66-
return ImageType.PNG
67-
}
59+
try {
60+
file.inputStream().use {
61+
val totalBytes = it.available()
62+
val bytes = ByteArray(256)
63+
it.read(bytes)
64+
65+
if (bytes[0].toInt() == -1 && bytes[1].toInt() == -40) {
66+
// jpg
67+
return ImageType.JPG
68+
}
6869

69-
val info = bytes.decodeToString()
70-
if (info.substring(0, 4) == WEBP_HEADER_RIFF && info.substring(8, 12) == WEBP_HEADER_WEBP) {
71-
// webp
72-
if (info.substring(12, 16) == WEBP_HEADER_VPX8 &&
73-
totalBytes > 17 &&
74-
(bytes[16] and 0b00000010) > 0
75-
) {
76-
// 动态 webp
77-
return ImageType.WEBP_ANIMATE
70+
if (bytes[0].toInt() == -119 && bytes[1].toInt() == 80) {
71+
// png
72+
return ImageType.PNG
7873
}
79-
return ImageType.WEBP
80-
}
8174

82-
val gifInfo = info.substring(0, 6)
83-
if (gifInfo == GIF_HEADER_89A || gifInfo == GIF_HEADER_87A) {
84-
// gif
85-
return ImageType.GIF
86-
}
75+
val info = bytes.decodeToString()
76+
if (info.substring(0, 4) == WEBP_HEADER_RIFF && info.substring(8, 12) == WEBP_HEADER_WEBP) {
77+
// webp
78+
if (info.substring(12, 16) == WEBP_HEADER_VPX8 &&
79+
totalBytes > 17 &&
80+
(bytes[16] and 0b00000010) > 0
81+
) {
82+
// 动态 webp
83+
return ImageType.WEBP_ANIMATE
84+
}
85+
return ImageType.WEBP
86+
}
8787

88-
if (info.substring(4, 8) == HEIF_HEADER_FTYP) {
89-
// heif
90-
val heifAnimateInfo = info.substring(8, 12)
91-
if (heifAnimateInfo == HEIF_HEADER_MSF1 ||
92-
heifAnimateInfo == HEIF_HEADER_HEVC ||
93-
heifAnimateInfo == HEIF_HEADER_HEVX
94-
) {
95-
// 动态 heif
96-
return ImageType.HEIF_ANIMATED
88+
val gifInfo = info.substring(0, 6)
89+
if (gifInfo == GIF_HEADER_89A || gifInfo == GIF_HEADER_87A) {
90+
// gif
91+
return ImageType.GIF
92+
}
93+
94+
if (info.substring(4, 8) == HEIF_HEADER_FTYP) {
95+
// heif
96+
val heifAnimateInfo = info.substring(8, 12)
97+
if (heifAnimateInfo == HEIF_HEADER_MSF1 ||
98+
heifAnimateInfo == HEIF_HEADER_HEVC ||
99+
heifAnimateInfo == HEIF_HEADER_HEVX
100+
) {
101+
// 动态 heif
102+
return ImageType.HEIF_ANIMATED
103+
}
104+
return ImageType.HEIF
97105
}
98-
return ImageType.HEIF
99-
}
100106

101-
if (info.contains(SVG_TAG)) {
102-
// svg
103-
return ImageType.SVG
107+
if (info.contains(SVG_TAG)) {
108+
// svg
109+
return ImageType.SVG
110+
}
104111
}
112+
} catch (e: Exception) {
113+
LogCat.e(e.toString())
114+
return ImageType.UNKNOWN
105115
}
106116

107117
return ImageType.UNKNOWN
@@ -111,6 +121,12 @@ object ImageHelper {
111121
if (path.endsWith(".svg", true)) {
112122
return 0
113123
}
124+
125+
val file = File(path)
126+
if (!file.exists()) {
127+
return 0
128+
}
129+
114130
try {
115131
val exif = ExifInterface(path)
116132
val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
@@ -120,21 +136,30 @@ object ImageHelper {
120136
ExifInterface.ORIENTATION_ROTATE_270 -> 270
121137
else -> 0
122138
}
123-
} catch (e: IOException) {
124-
e.printStackTrace()
139+
} catch (e: Exception) {
125140
LogCat.e(e.toString())
126141
}
127142
return 0
128143
}
129144

130145
fun getIntrinsicSize(path: String, rotation: Int): IntSize {
146+
val file = File(path)
147+
if (!file.exists()) {
148+
return IntSize.Zero
149+
}
150+
131151
val size = if (path.endsWith(".svg", true)) {
132152
SvgHelper.getSize(path)
133153
} else {
134-
val options = BitmapFactory.Options()
135-
options.inJustDecodeBounds = true
136-
BitmapFactory.decodeFile(path, options)
137-
IntSize(options.outWidth, options.outHeight)
154+
try {
155+
val options = BitmapFactory.Options()
156+
options.inJustDecodeBounds = true
157+
BitmapFactory.decodeFile(path, options)
158+
IntSize(options.outWidth, options.outHeight)
159+
} catch (e: Exception) {
160+
LogCat.e(e.toString())
161+
IntSize.Zero
162+
}
138163
}
139164

140165
if (rotation == 90 || rotation == 270) {

app/src/main/java/com/ismartcoding/plain/helpers/SvgHelper.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,13 @@ import com.ismartcoding.lib.logcat.LogCat
77

88
object SvgHelper {
99
fun getSize(path:String): IntSize {
10+
val file = File(path)
11+
if (!file.exists()) {
12+
return IntSize(150, 150)
13+
}
14+
1015
try {
11-
val svg = SVG.getFromInputStream(File(path).inputStream())
16+
val svg = SVG.getFromInputStream(file.inputStream())
1217
var width = svg.documentWidth.toInt()
1318
var height = svg.documentHeight.toInt()
1419
if (width <= 0) {

app/src/main/java/com/ismartcoding/plain/ui/components/mediaviewer/previewer/MediaPreviewer.kt

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import com.ismartcoding.plain.ui.models.TagsViewModel
4646
import com.ismartcoding.plain.ui.components.mediaviewer.PreviewItem
4747
import kotlinx.coroutines.launch
4848
import java.io.File
49+
import java.util.concurrent.ConcurrentHashMap
4950

5051

5152
val DEFAULT_SOFT_ANIMATION_SPEC = tween<Float>(320)
@@ -75,6 +76,9 @@ val DEFAULT_PREVIEWER_PLACEHOLDER_CONTENT = @Composable {
7576
}
7677
}
7778

79+
// 文件存在性缓存,避免重复检查
80+
private val fileExistsCache = ConcurrentHashMap<String, Boolean>()
81+
7882
// 加载时的占位内容
7983
class PreviewerPlaceholder(
8084
var enterTransition: EnterTransition = DEFAULT_PLACEHOLDER_ENTER_TRANSITION,
@@ -229,12 +233,40 @@ fun getModel(item: PreviewItem): Any? {
229233
}
230234
item.rotation
231235
}
232-
val inputStream = remember(item.path) { File(item.path).inputStream() }
233-
val decoder = rememberDecoderImagePainter(inputStream = inputStream, rotation = rotation)
234-
if (decoder != null) {
235-
item.intrinsicSize = IntSize(decoder.decoderWidth, decoder.decoderHeight)
236+
237+
// 使用缓存检查文件是否存在,避免重复文件系统调用
238+
val fileExists = remember(item.path) {
239+
fileExistsCache.getOrPut(item.path) {
240+
File(item.path).exists()
241+
}
242+
}
243+
244+
if (!fileExists) {
245+
// If file doesn't exist, return the item itself to handle gracefully
246+
model = item
247+
} else {
248+
val inputStream = remember(item.path) {
249+
try {
250+
File(item.path).inputStream()
251+
} catch (e: Exception) {
252+
// If there's any error opening the file, return null to handle gracefully
253+
// 如果文件访问失败,从缓存中移除该条目
254+
fileExistsCache.remove(item.path)
255+
null
256+
}
257+
}
258+
259+
val decoder = if (inputStream != null) {
260+
rememberDecoderImagePainter(inputStream = inputStream, rotation = rotation)
261+
} else {
262+
null
263+
}
264+
265+
if (decoder != null) {
266+
item.intrinsicSize = IntSize(decoder.decoderWidth, decoder.decoderHeight)
267+
}
268+
model = decoder ?: item
236269
}
237-
model = decoder
238270
}
239271
}
240272
return model

app/src/main/java/com/ismartcoding/plain/ui/models/MediaFoldersViewModel.kt

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ import kotlinx.coroutines.flow.SharingStarted
1818
import kotlinx.coroutines.flow.StateFlow
1919
import kotlinx.coroutines.flow.map
2020
import kotlinx.coroutines.flow.stateIn
21+
import kotlinx.coroutines.Dispatchers
22+
import kotlinx.coroutines.withContext
23+
import java.io.File
2124

2225
@OptIn(androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi::class)
2326
class MediaFoldersViewModel : ViewModel() {
@@ -63,8 +66,9 @@ class MediaFoldersViewModel : ViewModel() {
6366
sizeValue += bucket.size
6467

6568
// Add the first item from each folder's topItems if available
66-
if (bucket.topItems.isNotEmpty()) {
67-
subItems.add(bucket.topItems.first())
69+
val validTopItems = bucket.topItems.filter { File(it).exists() }
70+
if (validTopItems.isNotEmpty()) {
71+
subItems.add(validTopItems.first())
6872
}
6973

7074
// Stop if we've collected 4 items
@@ -77,11 +81,12 @@ class MediaFoldersViewModel : ViewModel() {
7781
// take additional items from the first folder that has multiple items
7882
if (subItems.size < 4 && _itemsFlow.value.isNotEmpty()) {
7983
for (bucket in _itemsFlow.value) {
80-
if (bucket.topItems.size > 1) {
84+
val validTopItems = bucket.topItems.filter { File(it).exists() }
85+
if (validTopItems.size > 1) {
8186
// Start from the second item (index 1) since we've already added the first one
82-
for (i in 1 until bucket.topItems.size) {
87+
for (i in 1 until validTopItems.size) {
8388
if (subItems.size < 4) {
84-
subItems.add(bucket.topItems[i])
89+
subItems.add(validTopItems[i])
8590
} else {
8691
break
8792
}
@@ -102,4 +107,15 @@ class MediaFoldersViewModel : ViewModel() {
102107
updateLoadingState = { isLoading -> showLoading.value = isLoading }
103108
)
104109
}
110+
111+
/**
112+
* 异步预验证文件存在性,可以在后台线程中调用
113+
* 这样可以提前过滤掉不存在的文件,减少UI线程的负担
114+
*/
115+
suspend fun preValidateFilesAsync() = withContext(Dispatchers.IO) {
116+
_itemsFlow.value.forEach { bucket ->
117+
// 过滤掉不存在的文件
118+
bucket.topItems.removeAll { !File(it).exists() }
119+
}
120+
}
105121
}

0 commit comments

Comments
 (0)