From a8df762ca02d2a3a6c58ca1945f713f28e84fb78 Mon Sep 17 00:00:00 2001 From: Lewin Pauli Date: Sat, 16 May 2026 19:10:34 +0200 Subject: [PATCH 1/3] Add image thumbnails in file browser #41 --- .idea/deploymentTargetSelector.xml | 11 ++ .idea/deviceManager.xml | 13 ++ .idea/markdown.xml | 8 + .idea/migrations.xml | 10 ++ .../di/component/ApplicationComponent.java | 3 + .../ui/adapter/BrowseFilesAdapter.kt | 18 +- .../presentation/util/ThumbnailUtil.kt | 154 ++++++++++++++++++ .../res/layout/item_browse_files_node.xml | 4 +- presentation/src/main/res/values/dimens.xml | 2 +- .../cryptomator/util/file/LruFileCacheUtil.kt | 3 +- 10 files changed, 220 insertions(+), 6 deletions(-) create mode 100644 .idea/deploymentTargetSelector.xml create mode 100644 .idea/deviceManager.xml create mode 100644 .idea/markdown.xml create mode 100644 .idea/migrations.xml create mode 100644 presentation/src/main/java/org/cryptomator/presentation/util/ThumbnailUtil.kt diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 000000000..83f083d5c --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,11 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml new file mode 100644 index 000000000..91f95584d --- /dev/null +++ b/.idea/deviceManager.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/.idea/markdown.xml b/.idea/markdown.xml new file mode 100644 index 000000000..c61ea3346 --- /dev/null +++ b/.idea/markdown.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 000000000..f8051a6f9 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/presentation/src/main/java/org/cryptomator/presentation/di/component/ApplicationComponent.java b/presentation/src/main/java/org/cryptomator/presentation/di/component/ApplicationComponent.java index ca0232028..edd4721c8 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/di/component/ApplicationComponent.java +++ b/presentation/src/main/java/org/cryptomator/presentation/di/component/ApplicationComponent.java @@ -16,6 +16,7 @@ import org.cryptomator.presentation.di.module.ThreadModule; import org.cryptomator.presentation.util.ContentResolverUtil; import org.cryptomator.presentation.util.FileUtil; +import org.cryptomator.presentation.util.ThumbnailUtil; import javax.inject.Singleton; @@ -45,6 +46,8 @@ public interface ApplicationComponent { ContentResolverUtil contentResolverUtil(); + ThumbnailUtil thumbnailUtil(); + NetworkConnectionCheck networkConnectionCheck(); } diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt index 5585d8848..7cfd58416 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt @@ -29,6 +29,7 @@ import org.cryptomator.presentation.util.FileIcon import org.cryptomator.presentation.util.FileSizeHelper import org.cryptomator.presentation.util.FileUtil import org.cryptomator.presentation.util.ResourceHelper.Companion.getDrawable +import org.cryptomator.presentation.util.ThumbnailUtil import org.cryptomator.util.SharedPreferencesHandler import javax.inject.Inject @@ -37,7 +38,8 @@ constructor( private val dateHelper: DateHelper, // private val fileSizeHelper: FileSizeHelper, // private val fileUtil: FileUtil, // - private val sharedPreferencesHandler: SharedPreferencesHandler + private val sharedPreferencesHandler: SharedPreferencesHandler, + private val thumbnailUtil: ThumbnailUtil ) : RecyclerViewBaseAdapter, BrowseFilesAdapter.ItemClickListener, VaultContentViewHolder, ItemBrowseFilesNodeBinding>(CloudNodeModelNameAZComparator()), FastScrollRecyclerView.SectionedAdapter { private var chooseCloudNodeSettings: ChooseCloudNodeSettings? = null @@ -54,6 +56,11 @@ constructor( return ItemBrowseFilesNodeBinding.inflate(inflater, parent, false) } + override fun onViewRecycled(holder: VaultContentViewHolder) { + super.onViewRecycled(holder) + thumbnailUtil.cancelLoad(holder.binding.cloudNodeImage) + } + fun addOrReplaceCloudNode(cloudNodeModel: CloudNodeModel<*>) { if (contains(cloudNodeModel)) { replaceItem(cloudNodeModel) @@ -114,7 +121,7 @@ constructor( } } - inner class VaultContentViewHolder internal constructor(private val binding: ItemBrowseFilesNodeBinding) : RecyclerViewBaseAdapter, BrowseFilesAdapter.ItemClickListener, VaultContentViewHolder, ItemBrowseFilesNodeBinding>.ItemViewHolder(binding.root) { + inner class VaultContentViewHolder internal constructor(internal val binding: ItemBrowseFilesNodeBinding) : RecyclerViewBaseAdapter, BrowseFilesAdapter.ItemClickListener, VaultContentViewHolder, ItemBrowseFilesNodeBinding>.ItemViewHolder(binding.root) { private var uiState: UiStateTest? = null @@ -135,7 +142,12 @@ constructor( } private fun bindNodeImage(node: CloudNodeModel<*>) { - binding.cloudNodeImage.setImageResource(bindCloudNodeImage(node)) + if (node is CloudFileModel && FileIcon.fileIconFor(node.name, fileUtil) == FileIcon.IMAGE) { + binding.cloudNodeImage.setImageResource(FileIcon.IMAGE.iconResource) + thumbnailUtil.loadThumbnail(node, binding.cloudNodeImage) + } else { + binding.cloudNodeImage.setImageResource(bindCloudNodeImage(node)) + } } private fun bindCloudNodeImage(cloudNodeModel: CloudNodeModel<*>): Int { diff --git a/presentation/src/main/java/org/cryptomator/presentation/util/ThumbnailUtil.kt b/presentation/src/main/java/org/cryptomator/presentation/util/ThumbnailUtil.kt new file mode 100644 index 000000000..7f3280ccb --- /dev/null +++ b/presentation/src/main/java/org/cryptomator/presentation/util/ThumbnailUtil.kt @@ -0,0 +1,154 @@ +package org.cryptomator.presentation.util + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.media.MediaMetadataRetriever +import android.os.Handler +import android.os.Looper +import android.widget.ImageView +import org.cryptomator.data.repository.DispatchingCloudContentRepository +import org.cryptomator.domain.usecases.ProgressAware +import org.cryptomator.presentation.model.CloudFileModel +import org.cryptomator.util.file.LruFileCacheUtil +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.util.concurrent.Executors +import javax.inject.Inject +import javax.inject.Singleton +import timber.log.Timber + +@Singleton +class ThumbnailUtil @Inject constructor( + context: Context, + private val cloudContentRepository: DispatchingCloudContentRepository, +) { + + private val executor = Executors.newFixedThreadPool(3) + private val mainHandler = Handler(Looper.getMainLooper()) + private val thumbnailDir: File = LruFileCacheUtil(context).resolve(LruFileCacheUtil.Cache.THUMBNAILS) + + init { + thumbnailDir.mkdirs() + } + + fun loadThumbnail(file: CloudFileModel, target: ImageView) { + val cacheKey = cacheKey(file) + target.tag = cacheKey + + val cachedFile = File(thumbnailDir, "$cacheKey.jpg") + if (cachedFile.exists()) { + BitmapFactory.decodeFile(cachedFile.absolutePath)?.let { + target.setImageBitmap(it) + return + } + } + + executor.execute { + runCatching { + val bitmap = when (file.icon) { + FileIcon.MOVIE -> generateVideoThumbnail(file, cachedFile) + else -> generateImageThumbnail(file, cachedFile) + } ?: return@execute + + mainHandler.post { + if (target.tag == cacheKey) { + target.setImageBitmap(bitmap) + } + } + }.onFailure { e -> + Timber.tag("ThumbnailUtil").w(e, "Failed to generate thumbnail for ${file.name}") + } + } + } + + fun cancelLoad(target: ImageView) { + target.tag = null + } + + private fun generateImageThumbnail(file: CloudFileModel, cachedFile: File): Bitmap? { + val bytes = downloadToByteArray(file) ?: return null + val bitmap = decodeScaledBitmap(bytes) ?: return null + saveThumbnail(bitmap, cachedFile) + return bitmap + } + + private fun generateVideoThumbnail(file: CloudFileModel, cachedFile: File): Bitmap? { + val tmpFile = File(thumbnailDir, "${cacheKey(file)}.tmp") + return try { + downloadToFile(file, tmpFile) ?: return null + val retriever = MediaMetadataRetriever() + retriever.use { + it.setDataSource(tmpFile.absolutePath) + val frame = it.getFrameAtTime(0, MediaMetadataRetriever.OPTION_CLOSEST_SYNC) ?: return null + val bitmap = scaleBitmap(frame) ?: frame + saveThumbnail(bitmap, cachedFile) + bitmap + } + } finally { + tmpFile.delete() + } + } + + private fun downloadToByteArray(file: CloudFileModel): ByteArray? { + return try { + ByteArrayOutputStream().also { out -> + cloudContentRepository.read(file.toCloudNode(), null, out, ProgressAware.NO_OP_PROGRESS_AWARE_DOWNLOAD) + }.toByteArray() + } catch (e: Exception) { + Timber.tag("ThumbnailUtil").w(e, "Download failed for ${file.name}") + null + } + } + + private fun downloadToFile(file: CloudFileModel, dest: File): File? { + return try { + FileOutputStream(dest).use { out -> + cloudContentRepository.read(file.toCloudNode(), null, out, ProgressAware.NO_OP_PROGRESS_AWARE_DOWNLOAD) + } + dest + } catch (e: Exception) { + Timber.tag("ThumbnailUtil").w(e, "Download failed for ${file.name}") + null + } + } + + private fun decodeScaledBitmap(bytes: ByteArray): Bitmap? { + val opts = BitmapFactory.Options().apply { inJustDecodeBounds = true } + BitmapFactory.decodeByteArray(bytes, 0, bytes.size, opts) + opts.inSampleSize = computeSampleSize(opts.outWidth, opts.outHeight) + opts.inJustDecodeBounds = false + return BitmapFactory.decodeByteArray(bytes, 0, bytes.size, opts) + } + + private fun scaleBitmap(src: Bitmap): Bitmap? { + val sampleSize = computeSampleSize(src.width, src.height) + if (sampleSize == 1) return src + val w = src.width / sampleSize + val h = src.height / sampleSize + return Bitmap.createScaledBitmap(src, w, h, true) + } + + private fun computeSampleSize(width: Int, height: Int): Int { + var n = 1 + while (minOf(width, height) / (n * 2) >= THUMBNAIL_PX) n *= 2 + return n + } + + private fun saveThumbnail(bitmap: Bitmap, dest: File) { + try { + FileOutputStream(dest).use { bitmap.compress(Bitmap.CompressFormat.JPEG, 80, it) } + } catch (e: IOException) { + Timber.tag("ThumbnailUtil").w(e, "Could not save thumbnail to disk") + } + } + + private fun cacheKey(file: CloudFileModel): String = + "${file.path}_${file.size}".hashCode().toString().replace('-', 'n') + + companion object { + private const val THUMBNAIL_PX = 144 + } +} diff --git a/presentation/src/main/res/layout/item_browse_files_node.xml b/presentation/src/main/res/layout/item_browse_files_node.xml index a79841409..b8db915d5 100644 --- a/presentation/src/main/res/layout/item_browse_files_node.xml +++ b/presentation/src/main/res/layout/item_browse_files_node.xml @@ -10,7 +10,9 @@ android:layout_width="@dimen/thumbnail_size" android:layout_height="@dimen/thumbnail_size" android:layout_centerVertical="true" - android:layout_marginStart="16dp" /> + android:layout_marginStart="16dp" + android:scaleType="centerCrop" + android:clipToOutline="true" /> 16dp 16dp 16dp - 36dp + 72dp 16dp 420dp diff --git a/util/src/main/java/org/cryptomator/util/file/LruFileCacheUtil.kt b/util/src/main/java/org/cryptomator/util/file/LruFileCacheUtil.kt index b3d2fbee3..a3691d5d6 100644 --- a/util/src/main/java/org/cryptomator/util/file/LruFileCacheUtil.kt +++ b/util/src/main/java/org/cryptomator/util/file/LruFileCacheUtil.kt @@ -20,7 +20,7 @@ class LruFileCacheUtil(context: Context) { private val parent: File = context.cacheDir enum class Cache { - DROPBOX, WEBDAV, PCLOUD, S3, ONEDRIVE, GOOGLE_DRIVE + DROPBOX, WEBDAV, PCLOUD, S3, ONEDRIVE, GOOGLE_DRIVE, THUMBNAILS } fun resolve(cache: Cache?): File { @@ -31,6 +31,7 @@ class LruFileCacheUtil(context: Context) { Cache.S3 -> File(parent, "LruCacheS3") Cache.ONEDRIVE -> File(parent, "LruCacheOneDrive") Cache.GOOGLE_DRIVE -> File(parent, "LruCacheGoogleDrive") + Cache.THUMBNAILS -> File(parent, "thumbnails") else -> throw IllegalStateException() } } From 9ef3d2498a8ef9d87286b21ea06d8aadaec0dbdc Mon Sep 17 00:00:00 2001 From: Lewin Pauli Date: Sun, 17 May 2026 00:08:16 +0200 Subject: [PATCH 2/3] Add video thumbnails in file browser and clean up .gitignore --- .gitignore | 5 ++ .idea/deploymentTargetSelector.xml | 11 ---- .idea/deviceManager.xml | 13 ---- .idea/markdown.xml | 8 --- .idea/migrations.xml | 10 --- .idea/misc.xml | 63 ------------------- .../ui/adapter/BrowseFilesAdapter.kt | 5 +- .../presentation/util/ThumbnailUtil.kt | 16 +++-- 8 files changed, 15 insertions(+), 116 deletions(-) delete mode 100644 .idea/deploymentTargetSelector.xml delete mode 100644 .idea/deviceManager.xml delete mode 100644 .idea/markdown.xml delete mode 100644 .idea/migrations.xml delete mode 100755 .idea/misc.xml diff --git a/.gitignore b/.gitignore index ea7379898..1b0f1c72f 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,11 @@ .idea/encodings.xml .idea/compiler.xml .idea/jarRepositories.xml +.idea/misc.xml +.idea/deploymentTargetSelector.xml +.idea/deviceManager.xml +.idea/markdown.xml +.idea/migrations.xml ###Android### diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml deleted file mode 100644 index 83f083d5c..000000000 --- a/.idea/deploymentTargetSelector.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml deleted file mode 100644 index 91f95584d..000000000 --- a/.idea/deviceManager.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/markdown.xml b/.idea/markdown.xml deleted file mode 100644 index c61ea3346..000000000 --- a/.idea/markdown.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml deleted file mode 100644 index f8051a6f9..000000000 --- a/.idea/migrations.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100755 index 1d3e3ba73..000000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,63 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt index 7cfd58416..e79b266e8 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt @@ -142,8 +142,9 @@ constructor( } private fun bindNodeImage(node: CloudNodeModel<*>) { - if (node is CloudFileModel && FileIcon.fileIconFor(node.name, fileUtil) == FileIcon.IMAGE) { - binding.cloudNodeImage.setImageResource(FileIcon.IMAGE.iconResource) + val icon = if (node is CloudFileModel) FileIcon.fileIconFor(node.name, fileUtil) else null + if (node is CloudFileModel && (icon == FileIcon.IMAGE || icon == FileIcon.MOVIE)) { + binding.cloudNodeImage.setImageResource(icon!!.iconResource) thumbnailUtil.loadThumbnail(node, binding.cloudNodeImage) } else { binding.cloudNodeImage.setImageResource(bindCloudNodeImage(node)) diff --git a/presentation/src/main/java/org/cryptomator/presentation/util/ThumbnailUtil.kt b/presentation/src/main/java/org/cryptomator/presentation/util/ThumbnailUtil.kt index 7f3280ccb..7b7157b5f 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/util/ThumbnailUtil.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/util/ThumbnailUtil.kt @@ -79,11 +79,11 @@ class ThumbnailUtil @Inject constructor( val tmpFile = File(thumbnailDir, "${cacheKey(file)}.tmp") return try { downloadToFile(file, tmpFile) ?: return null - val retriever = MediaMetadataRetriever() - retriever.use { - it.setDataSource(tmpFile.absolutePath) - val frame = it.getFrameAtTime(0, MediaMetadataRetriever.OPTION_CLOSEST_SYNC) ?: return null - val bitmap = scaleBitmap(frame) ?: frame + MediaMetadataRetriever().use { retriever -> + retriever.setDataSource(tmpFile.absolutePath) + val frame = retriever.getFrameAtTime(0, MediaMetadataRetriever.OPTION_CLOSEST_SYNC) + ?: return null + val bitmap = scaleBitmap(frame) saveThumbnail(bitmap, cachedFile) bitmap } @@ -123,12 +123,10 @@ class ThumbnailUtil @Inject constructor( return BitmapFactory.decodeByteArray(bytes, 0, bytes.size, opts) } - private fun scaleBitmap(src: Bitmap): Bitmap? { + private fun scaleBitmap(src: Bitmap): Bitmap { val sampleSize = computeSampleSize(src.width, src.height) if (sampleSize == 1) return src - val w = src.width / sampleSize - val h = src.height / sampleSize - return Bitmap.createScaledBitmap(src, w, h, true) + return Bitmap.createScaledBitmap(src, src.width / sampleSize, src.height / sampleSize, true) } private fun computeSampleSize(width: Int, height: Int): Int { From 03be2074cb04c379fed42b515c4469c0f710a759 Mon Sep 17 00:00:00 2001 From: Lewin Pauli Date: Sun, 17 May 2026 00:40:55 +0200 Subject: [PATCH 3/3] Fix thumbnail review issues: off-thread cache decode, MediaMetadataRetriever API 26 compat and stale tag cleanup on rebind --- .../ui/adapter/BrowseFilesAdapter.kt | 5 +-- .../presentation/util/ThumbnailUtil.kt | 33 +++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt index e79b266e8..88e3dfd00 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt @@ -121,7 +121,7 @@ constructor( } } - inner class VaultContentViewHolder internal constructor(internal val binding: ItemBrowseFilesNodeBinding) : RecyclerViewBaseAdapter, BrowseFilesAdapter.ItemClickListener, VaultContentViewHolder, ItemBrowseFilesNodeBinding>.ItemViewHolder(binding.root) { + inner class VaultContentViewHolder internal constructor(internal val binding: ItemBrowseFilesNodeBinding) : RecyclerViewBaseAdapter, ItemClickListener, VaultContentViewHolder, ItemBrowseFilesNodeBinding>.ItemViewHolder(binding.root) { private var uiState: UiStateTest? = null @@ -144,9 +144,10 @@ constructor( private fun bindNodeImage(node: CloudNodeModel<*>) { val icon = if (node is CloudFileModel) FileIcon.fileIconFor(node.name, fileUtil) else null if (node is CloudFileModel && (icon == FileIcon.IMAGE || icon == FileIcon.MOVIE)) { - binding.cloudNodeImage.setImageResource(icon!!.iconResource) + binding.cloudNodeImage.setImageResource(icon.iconResource) thumbnailUtil.loadThumbnail(node, binding.cloudNodeImage) } else { + thumbnailUtil.cancelLoad(binding.cloudNodeImage) binding.cloudNodeImage.setImageResource(bindCloudNodeImage(node)) } } diff --git a/presentation/src/main/java/org/cryptomator/presentation/util/ThumbnailUtil.kt b/presentation/src/main/java/org/cryptomator/presentation/util/ThumbnailUtil.kt index 7b7157b5f..eac8fc646 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/util/ThumbnailUtil.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/util/ThumbnailUtil.kt @@ -39,18 +39,16 @@ class ThumbnailUtil @Inject constructor( target.tag = cacheKey val cachedFile = File(thumbnailDir, "$cacheKey.jpg") - if (cachedFile.exists()) { - BitmapFactory.decodeFile(cachedFile.absolutePath)?.let { - target.setImageBitmap(it) - return - } - } executor.execute { runCatching { - val bitmap = when (file.icon) { - FileIcon.MOVIE -> generateVideoThumbnail(file, cachedFile) - else -> generateImageThumbnail(file, cachedFile) + val bitmap = if (cachedFile.exists()) { + BitmapFactory.decodeFile(cachedFile.absolutePath) + } else { + when (file.icon) { + FileIcon.MOVIE -> generateVideoThumbnail(file, cachedFile) + else -> generateImageThumbnail(file, cachedFile) + } } ?: return@execute mainHandler.post { @@ -77,17 +75,17 @@ class ThumbnailUtil @Inject constructor( private fun generateVideoThumbnail(file: CloudFileModel, cachedFile: File): Bitmap? { val tmpFile = File(thumbnailDir, "${cacheKey(file)}.tmp") + val retriever = MediaMetadataRetriever() return try { downloadToFile(file, tmpFile) ?: return null - MediaMetadataRetriever().use { retriever -> - retriever.setDataSource(tmpFile.absolutePath) - val frame = retriever.getFrameAtTime(0, MediaMetadataRetriever.OPTION_CLOSEST_SYNC) - ?: return null - val bitmap = scaleBitmap(frame) - saveThumbnail(bitmap, cachedFile) - bitmap - } + retriever.setDataSource(tmpFile.absolutePath) + val frame = retriever.getFrameAtTime(0, MediaMetadataRetriever.OPTION_CLOSEST_SYNC) + ?: return null + val bitmap = scaleBitmap(frame) + saveThumbnail(bitmap, cachedFile) + bitmap } finally { + retriever.release() tmpFile.delete() } } @@ -118,6 +116,7 @@ class ThumbnailUtil @Inject constructor( private fun decodeScaledBitmap(bytes: ByteArray): Bitmap? { val opts = BitmapFactory.Options().apply { inJustDecodeBounds = true } BitmapFactory.decodeByteArray(bytes, 0, bytes.size, opts) + if (opts.outWidth <= 0 || opts.outHeight <= 0) return null // unsupported format e.g. SVG opts.inSampleSize = computeSampleSize(opts.outWidth, opts.outHeight) opts.inJustDecodeBounds = false return BitmapFactory.decodeByteArray(bytes, 0, bytes.size, opts)