From c4907c718302cd4d233d28d20dbee7cfc8d1437f Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 19 Mar 2026 15:42:08 +0100 Subject: [PATCH 1/3] fix(gallery): video thumbnail and overlay Signed-off-by: alperozturk96 --- .../jobs/gallery/GalleryImageGenerationJob.kt | 66 ++++++++++++++----- .../datamodel/ThumbnailsCacheManager.java | 8 +++ .../android/ui/adapter/OCFileListDelegate.kt | 15 +++-- 3 files changed, 68 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/gallery/GalleryImageGenerationJob.kt b/app/src/main/java/com/nextcloud/client/jobs/gallery/GalleryImageGenerationJob.kt index bc1be300a434..f48c913c762a 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/gallery/GalleryImageGenerationJob.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/gallery/GalleryImageGenerationJob.kt @@ -8,6 +8,8 @@ package com.nextcloud.client.jobs.gallery import android.graphics.Bitmap +import android.media.ThumbnailUtils +import android.provider.MediaStore import android.widget.ImageView import androidx.core.content.ContextCompat import com.nextcloud.client.account.User @@ -117,22 +119,56 @@ class GalleryImageGenerationJob(private val user: User, private val storageManag private suspend fun getBitmap(file: OCFile, onThumbnailGeneration: () -> Unit): Bitmap? = withContext(Dispatchers.IO) { - val key = file.remoteId - val cachedThumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache( + if (MimeTypeUtil.isVideo(file)) { + getVideoBitmap(file, onThumbnailGeneration) + } else { + getResizedImageBitmap(file, onThumbnailGeneration) + } + } + + private fun getVideoBitmap(file: OCFile, onThumbnailGeneration: () -> Unit): Bitmap? { + val key = ThumbnailsCacheManager.PREFIX_THUMBNAIL + file.remoteId + val cached = ThumbnailsCacheManager.getBitmapFromDiskCache(key) + + if (cached != null && !file.isUpdateThumbnailNeeded) { + return ThumbnailsCacheManager.addVideoOverlay(cached, MainApp.getAppContext()) + } + + onThumbnailGeneration() + var bitmap: Bitmap? = null + if (file.isDown) { + bitmap = ThumbnailUtils.createVideoThumbnail( + file.storagePath, + MediaStore.Images.Thumbnails.MINI_KIND + ) + } + + if (bitmap == null) { + bitmap = ThumbnailsCacheManager.getBitmapFromDiskCache( ThumbnailsCacheManager.PREFIX_RESIZED_IMAGE + file.remoteId ) - if (cachedThumbnail != null && !file.isUpdateThumbnailNeeded) { - Log_OC.d(TAG, "cached thumbnail is used for: ${file.fileName}") - return@withContext getThumbnailFromCache(file, cachedThumbnail, key) - } + } - Log_OC.d(TAG, "generating new thumbnail for: ${file.fileName}") + if (bitmap != null) { + ThumbnailsCacheManager.addBitmapToCache(key, bitmap) + return ThumbnailsCacheManager.addVideoOverlay(bitmap, MainApp.getAppContext()) + } + return null + } - onThumbnailGeneration() - semaphore.withPermit { - return@withContext getThumbnailFromServerAndAddToCache(file, cachedThumbnail) - } + private suspend fun getResizedImageBitmap(file: OCFile, onThumbnailGeneration: () -> Unit): Bitmap? { + val key = file.remoteId + val cachedThumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache( + ThumbnailsCacheManager.PREFIX_RESIZED_IMAGE + key + ) + if (cachedThumbnail != null && !file.isUpdateThumbnailNeeded) { + return getThumbnailFromCache(file, cachedThumbnail, key) + } + onThumbnailGeneration() + semaphore.withPermit { + return getThumbnailFromServerAndAddToCache(file, cachedThumbnail) } + } private suspend fun setThumbnail( bitmap: Bitmap, @@ -168,14 +204,12 @@ class GalleryImageGenerationJob(private val user: User, private val storageManag private fun getThumbnailFromCache(file: OCFile, thumbnail: Bitmap, key: String): Bitmap { var result = thumbnail - if (MimeTypeUtil.isVideo(file)) { - result = ThumbnailsCacheManager.addVideoOverlay(thumbnail, MainApp.getAppContext()) - } - if (thumbnail.allocationKilobyte() > ThumbnailsCacheManager.THUMBNAIL_SIZE_IN_KB) { result = ThumbnailsCacheManager.getScaledThumbnailAfterSave(result, key) } - + if (MimeTypeUtil.isVideo(file)) { + result = ThumbnailsCacheManager.addVideoOverlay(thumbnail, MainApp.getAppContext()) + } return result } diff --git a/app/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java b/app/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java index 72cfadc35f03..35a51bf69582 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java @@ -1325,4 +1325,12 @@ public static Bitmap doResizedImageInBackground(OCFile file, FileDataStorageMana return thumbnail; } + + public static String getCacheKey(@NonNull OCFile file) { + if (MimeTypeUtil.isVideo(file)) { + return ThumbnailsCacheManager.PREFIX_THUMBNAIL + file.getRemoteId(); + } else { + return ThumbnailsCacheManager.PREFIX_RESIZED_IMAGE + file.getRemoteId(); + } + } } diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt index 35c50edea2bb..1b2f2c936a84 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt @@ -36,6 +36,7 @@ import com.owncloud.android.ui.fragment.SearchType import com.owncloud.android.ui.interfaces.OCFileListFragmentInterface import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.EncryptionUtils +import com.owncloud.android.utils.MimeTypeUtil import com.owncloud.android.utils.overlay.OverlayManager import com.owncloud.android.utils.theme.ViewThemeUtils import kotlinx.coroutines.CoroutineScope @@ -113,11 +114,15 @@ class OCFileListDelegate( imageView.tag = file.fileId // set placeholder before async job - val cached = ThumbnailsCacheManager.getBitmapFromDiskCache( - ThumbnailsCacheManager.PREFIX_RESIZED_IMAGE + file.remoteId - ) - if (cached != null) { - imageView.setImageBitmap(cached) + val cacheKey = ThumbnailsCacheManager.getCacheKey(file) + val cachedBitmap = ThumbnailsCacheManager.getBitmapFromDiskCache(cacheKey) + if (cachedBitmap != null) { + val overlay = if (MimeTypeUtil.isVideo(file)) { + ThumbnailsCacheManager.addVideoOverlay(cachedBitmap, context) + } else { + cachedBitmap + } + imageView.setImageBitmap(overlay) } else { imageView.setImageDrawable(OCFileUtils.getMediaPlaceholder(file, imageDimension)) } From c17803e867332e6605f80be7c6e6b4d997ba803b Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 19 Mar 2026 15:44:58 +0100 Subject: [PATCH 2/3] fix(gallery): video thumbnail and overlay Signed-off-by: alperozturk96 --- .../jobs/gallery/GalleryImageGenerationJob.kt | 24 +++++++++++++++---- .../datamodel/ThumbnailsCacheManager.java | 6 +++-- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/gallery/GalleryImageGenerationJob.kt b/app/src/main/java/com/nextcloud/client/jobs/gallery/GalleryImageGenerationJob.kt index f48c913c762a..d5908aa57371 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/gallery/GalleryImageGenerationJob.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/gallery/GalleryImageGenerationJob.kt @@ -9,12 +9,15 @@ package com.nextcloud.client.jobs.gallery import android.graphics.Bitmap import android.media.ThumbnailUtils +import android.os.Build import android.provider.MediaStore +import android.util.Size import android.widget.ImageView import androidx.core.content.ContextCompat import com.nextcloud.client.account.User import com.nextcloud.utils.allocationKilobyte import com.nextcloud.utils.extensions.isPNG +import com.nextcloud.utils.extensions.toFile import com.owncloud.android.MainApp import com.owncloud.android.R import com.owncloud.android.datamodel.FileDataStorageManager @@ -137,10 +140,7 @@ class GalleryImageGenerationJob(private val user: User, private val storageManag onThumbnailGeneration() var bitmap: Bitmap? = null if (file.isDown) { - bitmap = ThumbnailUtils.createVideoThumbnail( - file.storagePath, - MediaStore.Images.Thumbnails.MINI_KIND - ) + bitmap = createVideoThumbnail(file.storagePath) } if (bitmap == null) { @@ -156,6 +156,22 @@ class GalleryImageGenerationJob(private val user: User, private val storageManag return null } + private fun createVideoThumbnail(storagePath: String): Bitmap? { + val file = storagePath.toFile() ?: return null + val size = ThumbnailsCacheManager.getThumbnailDimension() + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + try { + ThumbnailUtils.createVideoThumbnail(file, Size(size, size), null) + } catch (e: Exception) { + Log_OC.e(TAG, "Failed to create video thumbnail: ${e.message}") + null + } + } else { + @Suppress("DEPRECATION") + ThumbnailUtils.createVideoThumbnail(storagePath, MediaStore.Images.Thumbnails.MINI_KIND) + } + } + private suspend fun getResizedImageBitmap(file: OCFile, onThumbnailGeneration: () -> Unit): Bitmap? { val key = file.remoteId val cachedThumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache( diff --git a/app/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java b/app/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java index 35a51bf69582..e56f023ed7c8 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java @@ -1092,10 +1092,12 @@ public static Bitmap addVideoOverlay(Bitmap thumbnail, Context context) { c.drawBitmap(thumbnail, 0, 0, null); + float left = (thumbnail.getWidth() - px) / 2f; + float top = (thumbnail.getHeight() - px) / 2f; + Paint p = new Paint(); p.setAlpha(230); - - c.drawBitmap(resizedPlayButton, px, px, p); + c.drawBitmap(resizedPlayButton, left, top, p); return resultBitmap; } From bf835b425e5373b8f82dc1aabfbddd24a0fbd81c Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 19 Mar 2026 15:46:52 +0100 Subject: [PATCH 3/3] fix(gallery): video thumbnail and overlay Signed-off-by: alperozturk96 --- .../nextcloud/client/jobs/gallery/GalleryImageGenerationJob.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/gallery/GalleryImageGenerationJob.kt b/app/src/main/java/com/nextcloud/client/jobs/gallery/GalleryImageGenerationJob.kt index d5908aa57371..b37dd94e5fe9 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/gallery/GalleryImageGenerationJob.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/gallery/GalleryImageGenerationJob.kt @@ -34,6 +34,7 @@ import kotlinx.coroutines.withContext import java.util.Collections import java.util.WeakHashMap +@Suppress("DEPRECATION", "TooGenericExceptionCaught", "ReturnCount") class GalleryImageGenerationJob(private val user: User, private val storageManager: FileDataStorageManager) { companion object { private const val TAG = "GalleryImageGenerationJob" @@ -88,7 +89,6 @@ class GalleryImageGenerationJob(private val user: User, private val storageManag } } - @Suppress("TooGenericExceptionCaught") suspend fun run(file: OCFile, imageView: ImageView, listener: GalleryImageGenerationListener) { try { var newImage = false @@ -229,7 +229,6 @@ class GalleryImageGenerationJob(private val user: User, private val storageManag return result } - @Suppress("DEPRECATION", "TooGenericExceptionCaught") private suspend fun getThumbnailFromServerAndAddToCache(file: OCFile, thumbnail: Bitmap?): Bitmap? { var thumbnail = thumbnail try {