From 9a5ac00ecc2b4879fedd096f59ed8953d70435bb Mon Sep 17 00:00:00 2001 From: alperozturk Date: Thu, 20 Nov 2025 10:24:44 +0100 Subject: [PATCH 01/12] perf: convert gallery image generation task to coroutine Signed-off-by: alperozturk --- .../jobs/gallery/GalleryImageGenerationJob.kt | 125 ++++++++++++++++++ .../gallery/GalleryImageGenerationListener.kt | 14 ++ .../datamodel/ThumbnailsCacheManager.java | 21 ++- .../android/ui/adapter/OCFileListDelegate.kt | 82 ++++++++++-- 4 files changed, 232 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/client/jobs/gallery/GalleryImageGenerationJob.kt create mode 100644 app/src/main/java/com/nextcloud/client/jobs/gallery/GalleryImageGenerationListener.kt 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 new file mode 100644 index 000000000000..6e4a4774bb71 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/jobs/gallery/GalleryImageGenerationJob.kt @@ -0,0 +1,125 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.jobs.gallery + +import android.graphics.Bitmap +import android.widget.ImageView +import com.nextcloud.client.account.User +import com.nextcloud.utils.allocationKilobyte +import com.owncloud.android.MainApp +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.datamodel.ThumbnailsCacheManager +import com.owncloud.android.lib.common.OwnCloudClientManagerFactory +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.files.model.ImageDimension +import com.owncloud.android.utils.MimeTypeUtil +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class GalleryImageGenerationJob( + private val user: User, + private val storageManager: FileDataStorageManager, + private val imageView: ImageView, + private val file: OCFile, + private val key: String, + private val listener: GalleryImageGenerationListener, + private val backgroundColor: Int +) { + companion object { + private const val TAG = "GalleryImageGenerationJob" + } + + suspend fun execute() { + var newImage = false + val bitmap: Bitmap? = withContext(Dispatchers.IO) { + var thumbnail: Bitmap? + if (file.remoteId != null || file.isPreviewAvailable) { + thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache( + ThumbnailsCacheManager.PREFIX_RESIZED_IMAGE + file.remoteId + ) + + if (thumbnail != null && !file.isUpdateThumbnailNeeded) { + return@withContext getThumbnailFromCache(thumbnail) + } + + newImage = true + return@withContext getThumbnailFromServerAndAddToCache(thumbnail) + } + return@withContext null + } + + withContext(Dispatchers.Main) { + if (bitmap != null) { + val tagId = file.fileId.toString() + if (imageView.tag?.toString() == tagId) { + if ("image/png".equals(file.mimeType, ignoreCase = true)) { + imageView.setBackgroundColor(backgroundColor) + } + + if (newImage) { + listener.onNewGalleryImage() + } + + imageView.setImageBitmap(bitmap) + imageView.invalidate() + } + listener.onSuccess() + } else { + listener.onError() + } + } + } + + private fun getThumbnailFromCache(thumbnail: Bitmap): Bitmap { + val size = ThumbnailsCacheManager.getThumbnailDimension().toFloat() + + val imageDimension = file.imageDimension + if (imageDimension == null || imageDimension.width != size || imageDimension.height != size) { + val newDimension = ImageDimension( + thumbnail.getWidth().toFloat(), + thumbnail.getHeight().toFloat() + ) + file.imageDimension = newDimension + storageManager.saveFile(file) + } + + 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) + } + + return result + } + + private suspend fun getThumbnailFromServerAndAddToCache(thumbnail: Bitmap?): Bitmap? { + var thumbnail = thumbnail + try { + val client = withContext(Dispatchers.IO) { + OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor( + user.toOwnCloudAccount(), + MainApp.getAppContext() + ) + } + ThumbnailsCacheManager.setClient(client) + thumbnail = ThumbnailsCacheManager.doResizedImageInBackground(file, storageManager) + + if (MimeTypeUtil.isVideo(file) && thumbnail != null) { + thumbnail = ThumbnailsCacheManager.addVideoOverlay(thumbnail, MainApp.getAppContext()) + } + } catch (t: Throwable) { + Log_OC.e(TAG, "Generation of gallery image for $file failed", t) + } + + return thumbnail + } +} diff --git a/app/src/main/java/com/nextcloud/client/jobs/gallery/GalleryImageGenerationListener.kt b/app/src/main/java/com/nextcloud/client/jobs/gallery/GalleryImageGenerationListener.kt new file mode 100644 index 000000000000..e7ca171a1151 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/jobs/gallery/GalleryImageGenerationListener.kt @@ -0,0 +1,14 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.jobs.gallery + +interface GalleryImageGenerationListener { + fun onSuccess() + fun onNewGalleryImage() + fun onError() +} 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 96ab32fea907..f0e1be5b5d45 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java @@ -106,7 +106,7 @@ public final class ThumbnailsCacheManager { private static final CompressFormat mCompressFormat = CompressFormat.JPEG; private static final int mCompressQuality = 70; private static OwnCloudClient mClient; - private static final int THUMBNAIL_SIZE_IN_KB = 512; + public static final int THUMBNAIL_SIZE_IN_KB = 512; private static final int RESIZED_IMAGE_SIZE_IN_KB = 10240; public static final Bitmap mDefaultImg = BitmapFactory.decodeResource(MainApp.getAppContext().getResources(), @@ -460,6 +460,19 @@ public interface GalleryListener { } } + public static Bitmap getScaledThumbnailAfterSave(Bitmap thumbnail, String imageKey) { + Bitmap result = BitmapExtensionsKt.scaleUntil(thumbnail, THUMBNAIL_SIZE_IN_KB); + + synchronized (mThumbnailsDiskCacheLock) { + if (mThumbnailCache != null) { + Log_OC.d(TAG, "Scaling bitmap before caching: " + imageKey); + mThumbnailCache.put(imageKey, result); + } + } + + return result; + } + public static class ResizedImageGenerationTask extends AsyncTask { private final FileFragment fileFragment; private final FileDataStorageManager storageManager; @@ -1416,7 +1429,11 @@ public static void clearCache() { mThumbnailCache = null; } - private static Bitmap doResizedImageInBackground(OCFile file, FileDataStorageManager storageManager) { + public static void setClient(OwnCloudClient client) { + mClient = client; + } + + public static Bitmap doResizedImageInBackground(OCFile file, FileDataStorageManager storageManager) { Bitmap thumbnail; String imageKey = 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 10e1ddb5e026..fbb38876c8b1 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 @@ -10,7 +10,7 @@ package com.owncloud.android.ui.adapter import android.content.Context import android.content.res.Configuration import android.graphics.Color -import android.os.AsyncTask +import android.graphics.drawable.BitmapDrawable import android.view.View import android.widget.ImageView import androidx.core.content.ContextCompat @@ -20,6 +20,8 @@ import com.elyeproj.loaderviewlibrary.LoaderImageView import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.client.account.User import com.nextcloud.client.jobs.download.FileDownloadHelper +import com.nextcloud.client.jobs.gallery.GalleryImageGenerationJob +import com.nextcloud.client.jobs.gallery.GalleryImageGenerationListener import com.nextcloud.client.jobs.upload.FileUploadHelper import com.nextcloud.client.preferences.AppPreferences import com.nextcloud.utils.extensions.getSubfiles @@ -31,7 +33,6 @@ import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.OCFile import com.owncloud.android.datamodel.SyncedFolderProvider import com.owncloud.android.datamodel.ThumbnailsCacheManager -import com.owncloud.android.datamodel.ThumbnailsCacheManager.GalleryImageGenerationTask.GalleryListener import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.ui.activity.ComponentsGetter import com.owncloud.android.ui.activity.FolderPickerActivity @@ -140,19 +141,82 @@ class OCFileListDelegate( private fun getGalleryDrawable( file: OCFile, - width: Int, - task: ThumbnailsCacheManager.GalleryImageGenerationTask - ): ThumbnailsCacheManager.AsyncGalleryImageDrawable { - val drawable = MimeTypeUtil.getFileTypeIcon(file.mimeType, file.fileName, context, viewThemeUtils) + width: Int + ): BitmapDrawable { + val placeholder = MimeTypeUtil.getFileTypeIcon( + file.mimeType, + file.fileName, + context, + viewThemeUtils + ) ?: ResourcesCompat.getDrawable(context.resources, R.drawable.file_image, null) ?: Color.GRAY.toDrawable() - val thumbnail = BitmapUtils.drawableToBitmap(drawable, width / 2, width / 2) + val bitmap = BitmapUtils.drawableToBitmap( + placeholder, + width / 2, + width / 2 + ) - return ThumbnailsCacheManager.AsyncGalleryImageDrawable(context.resources, thumbnail, task) + return BitmapDrawable(context.resources, bitmap) } + private val scope = CoroutineScope(Dispatchers.IO) + @Suppress("ComplexMethod") + private fun setGalleryImage( + file: OCFile, + thumbnailView: ImageView, + shimmerThumbnail: LoaderImageView?, + galleryRowHolder: GalleryRowHolder, + width: Int + ) { + scope.launch { + try { + withContext(Dispatchers.Main) { + val asyncDrawable = getGalleryDrawable(file, width) + + if (shimmerThumbnail != null) { + Log_OC.d(tag, "setGalleryImage.startShimmer()") + DisplayUtils.startShimmer(shimmerThumbnail, thumbnailView) + } + + thumbnailView.setImageDrawable(asyncDrawable) + } + + GalleryImageGenerationJob( + user, + storageManager, + thumbnailView, + file, + file.remoteId, + object: GalleryImageGenerationListener { + override fun onSuccess() { + galleryRowHolder.binding.rowLayout.invalidate() + Log_OC.d(tag, "setGalleryImage.onSuccess()") + DisplayUtils.stopShimmer(shimmerThumbnail, thumbnailView) + } + + override fun onNewGalleryImage() { + Log_OC.d(tag, "setGalleryImage.redraw()") + galleryRowHolder.redraw() + } + + override fun onError() { + Log_OC.d(tag, "setGalleryImage.onError()") + DisplayUtils.stopShimmer(shimmerThumbnail, thumbnailView) + } + }, + ContextCompat.getColor(context, R.color.bg_default) + ).execute() + } catch (e: IllegalArgumentException) { + Log_OC.d(tag, "ThumbnailGenerationTask : " + e.message) + } + } + } + + /* + @Suppress("ComplexMethod") private fun setGalleryImage( file: OCFile, thumbnailView: ImageView, @@ -214,6 +278,8 @@ class OCFileListDelegate( Log_OC.d(tag, "ThumbnailGenerationTask : " + e.message) } } + */ + fun setThumbnail(thumbnail: ImageView, shimmerThumbnail: LoaderImageView?, file: OCFile) { DisplayUtils.setThumbnail( From 4d3955d9d877a6d8fc1138169fdb884ab13f074b Mon Sep 17 00:00:00 2001 From: alperozturk Date: Thu, 20 Nov 2025 10:43:04 +0100 Subject: [PATCH 02/12] perf: use cache if exists Signed-off-by: alperozturk --- .../jobs/gallery/GalleryImageGenerationJob.kt | 11 ++- .../android/ui/adapter/OCFileListDelegate.kt | 73 +++++++++++-------- 2 files changed, 53 insertions(+), 31 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 6e4a4774bb71..096251a3243e 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 @@ -20,6 +20,8 @@ import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.lib.resources.files.model.ImageDimension import com.owncloud.android.utils.MimeTypeUtil import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.withContext class GalleryImageGenerationJob( @@ -33,9 +35,16 @@ class GalleryImageGenerationJob( ) { companion object { private const val TAG = "GalleryImageGenerationJob" + private val semaphore = Semaphore(3) } - suspend fun execute() { + suspend fun run() { + semaphore.withPermit { + execute() + } + } + + private suspend fun execute() { var newImage = false val bitmap: Bitmap? = withContext(Dispatchers.IO) { var thumbnail: Bitmap? 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 fbb38876c8b1..a4bd26b1d89f 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 @@ -173,42 +173,55 @@ class OCFileListDelegate( ) { scope.launch { try { - withContext(Dispatchers.Main) { - val asyncDrawable = getGalleryDrawable(file, width) + val cachedBitmap = withContext(Dispatchers.IO) { + ThumbnailsCacheManager.getBitmapFromDiskCache( + ThumbnailsCacheManager.PREFIX_RESIZED_IMAGE + file.remoteId + ) + } - if (shimmerThumbnail != null) { - Log_OC.d(tag, "setGalleryImage.startShimmer()") - DisplayUtils.startShimmer(shimmerThumbnail, thumbnailView) + if (cachedBitmap != null && !file.isUpdateThumbnailNeeded) { + withContext(Dispatchers.Main) { + thumbnailView.setImageBitmap(cachedBitmap) + DisplayUtils.stopShimmer(shimmerThumbnail, thumbnailView) } + } else { + withContext(Dispatchers.Main) { + val asyncDrawable = getGalleryDrawable(file, width) - thumbnailView.setImageDrawable(asyncDrawable) - } - - GalleryImageGenerationJob( - user, - storageManager, - thumbnailView, - file, - file.remoteId, - object: GalleryImageGenerationListener { - override fun onSuccess() { - galleryRowHolder.binding.rowLayout.invalidate() - Log_OC.d(tag, "setGalleryImage.onSuccess()") - DisplayUtils.stopShimmer(shimmerThumbnail, thumbnailView) + if (shimmerThumbnail != null) { + Log_OC.d(tag, "setGalleryImage.startShimmer()") + DisplayUtils.startShimmer(shimmerThumbnail, thumbnailView) } - override fun onNewGalleryImage() { - Log_OC.d(tag, "setGalleryImage.redraw()") - galleryRowHolder.redraw() - } + thumbnailView.setImageDrawable(asyncDrawable) + } - override fun onError() { - Log_OC.d(tag, "setGalleryImage.onError()") - DisplayUtils.stopShimmer(shimmerThumbnail, thumbnailView) - } - }, - ContextCompat.getColor(context, R.color.bg_default) - ).execute() + GalleryImageGenerationJob( + user, + storageManager, + thumbnailView, + file, + file.remoteId, + object: GalleryImageGenerationListener { + override fun onSuccess() { + galleryRowHolder.binding.rowLayout.invalidate() + Log_OC.d(tag, "setGalleryImage.onSuccess()") + DisplayUtils.stopShimmer(shimmerThumbnail, thumbnailView) + } + + override fun onNewGalleryImage() { + Log_OC.d(tag, "setGalleryImage.redraw()") + galleryRowHolder.redraw() + } + + override fun onError() { + Log_OC.d(tag, "setGalleryImage.onError()") + DisplayUtils.stopShimmer(shimmerThumbnail, thumbnailView) + } + }, + ContextCompat.getColor(context, R.color.bg_default) + ).run() + } } catch (e: IllegalArgumentException) { Log_OC.d(tag, "ThumbnailGenerationTask : " + e.message) } From f86b2968223e5a32f29d5007dc65f56db65c86ed Mon Sep 17 00:00:00 2001 From: alperozturk Date: Thu, 20 Nov 2025 10:50:08 +0100 Subject: [PATCH 03/12] perf: use cache if exists Signed-off-by: alperozturk --- .../jobs/gallery/GalleryImageGenerationJob.kt | 31 ++++++++------- .../android/ui/adapter/OCFileListDelegate.kt | 39 ++++++++----------- 2 files changed, 33 insertions(+), 37 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 096251a3243e..9975b6c704aa 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,9 +9,11 @@ package com.nextcloud.client.jobs.gallery import android.graphics.Bitmap import android.widget.ImageView +import androidx.core.content.ContextCompat import com.nextcloud.client.account.User import com.nextcloud.utils.allocationKilobyte import com.owncloud.android.MainApp +import com.owncloud.android.R import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.OCFile import com.owncloud.android.datamodel.ThumbnailsCacheManager @@ -26,25 +28,21 @@ import kotlinx.coroutines.withContext class GalleryImageGenerationJob( private val user: User, - private val storageManager: FileDataStorageManager, - private val imageView: ImageView, - private val file: OCFile, - private val key: String, - private val listener: GalleryImageGenerationListener, - private val backgroundColor: Int + private val storageManager: FileDataStorageManager ) { companion object { private const val TAG = "GalleryImageGenerationJob" private val semaphore = Semaphore(3) } - suspend fun run() { + suspend fun run(file: OCFile, imageView: ImageView, listener: GalleryImageGenerationListener) { semaphore.withPermit { - execute() + execute(file, imageView, listener) } } - private suspend fun execute() { + private suspend fun execute(file: OCFile, imageView: ImageView, listener: GalleryImageGenerationListener) { + val key = file.remoteId var newImage = false val bitmap: Bitmap? = withContext(Dispatchers.IO) { var thumbnail: Bitmap? @@ -54,11 +52,11 @@ class GalleryImageGenerationJob( ) if (thumbnail != null && !file.isUpdateThumbnailNeeded) { - return@withContext getThumbnailFromCache(thumbnail) + return@withContext getThumbnailFromCache(file, thumbnail, key) } newImage = true - return@withContext getThumbnailFromServerAndAddToCache(thumbnail) + return@withContext getThumbnailFromServerAndAddToCache(file, thumbnail) } return@withContext null } @@ -68,7 +66,12 @@ class GalleryImageGenerationJob( val tagId = file.fileId.toString() if (imageView.tag?.toString() == tagId) { if ("image/png".equals(file.mimeType, ignoreCase = true)) { - imageView.setBackgroundColor(backgroundColor) + imageView.setBackgroundColor( + ContextCompat.getColor( + MainApp.getAppContext(), + R.color.bg_default + ) + ) } if (newImage) { @@ -85,7 +88,7 @@ class GalleryImageGenerationJob( } } - private fun getThumbnailFromCache(thumbnail: Bitmap): Bitmap { + private fun getThumbnailFromCache(file: OCFile, thumbnail: Bitmap, key: String): Bitmap { val size = ThumbnailsCacheManager.getThumbnailDimension().toFloat() val imageDimension = file.imageDimension @@ -110,7 +113,7 @@ class GalleryImageGenerationJob( return result } - private suspend fun getThumbnailFromServerAndAddToCache(thumbnail: Bitmap?): Bitmap? { + private suspend fun getThumbnailFromServerAndAddToCache(file: OCFile, thumbnail: Bitmap?): Bitmap? { var thumbnail = thumbnail try { val client = withContext(Dispatchers.IO) { 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 a4bd26b1d89f..3e35c52960f1 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 @@ -162,6 +162,7 @@ class OCFileListDelegate( } private val scope = CoroutineScope(Dispatchers.IO) + private val galleryImageGenerationJob = GalleryImageGenerationJob(user, storageManager) @Suppress("ComplexMethod") private fun setGalleryImage( @@ -196,31 +197,23 @@ class OCFileListDelegate( thumbnailView.setImageDrawable(asyncDrawable) } - GalleryImageGenerationJob( - user, - storageManager, - thumbnailView, - file, - file.remoteId, - object: GalleryImageGenerationListener { - override fun onSuccess() { - galleryRowHolder.binding.rowLayout.invalidate() - Log_OC.d(tag, "setGalleryImage.onSuccess()") - DisplayUtils.stopShimmer(shimmerThumbnail, thumbnailView) - } + galleryImageGenerationJob.run(file, thumbnailView, object : GalleryImageGenerationListener { + override fun onSuccess() { + galleryRowHolder.binding.rowLayout.invalidate() + Log_OC.d(tag, "setGalleryImage.onSuccess()") + DisplayUtils.stopShimmer(shimmerThumbnail, thumbnailView) + } - override fun onNewGalleryImage() { - Log_OC.d(tag, "setGalleryImage.redraw()") - galleryRowHolder.redraw() - } + override fun onNewGalleryImage() { + Log_OC.d(tag, "setGalleryImage.redraw()") + galleryRowHolder.redraw() + } - override fun onError() { - Log_OC.d(tag, "setGalleryImage.onError()") - DisplayUtils.stopShimmer(shimmerThumbnail, thumbnailView) - } - }, - ContextCompat.getColor(context, R.color.bg_default) - ).run() + override fun onError() { + Log_OC.d(tag, "setGalleryImage.onError()") + DisplayUtils.stopShimmer(shimmerThumbnail, thumbnailView) + } + }) } } catch (e: IllegalArgumentException) { Log_OC.d(tag, "ThumbnailGenerationTask : " + e.message) From ac0e9780a6a19090a1ecec84fde80ec075bacde1 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Thu, 20 Nov 2025 10:59:48 +0100 Subject: [PATCH 04/12] perf: simplify Signed-off-by: alperozturk --- .../jobs/gallery/GalleryImageGenerationJob.kt | 88 +++++++++----- .../android/ui/adapter/OCFileListDelegate.kt | 109 ++++-------------- 2 files changed, 81 insertions(+), 116 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 9975b6c704aa..bad96582f4c7 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 @@ -37,15 +37,42 @@ class GalleryImageGenerationJob( suspend fun run(file: OCFile, imageView: ImageView, listener: GalleryImageGenerationListener) { semaphore.withPermit { - execute(file, imageView, listener) + try { + execute(file, imageView, listener) + } catch (e: Exception) { + Log_OC.e(TAG, "gallery image generation job: ", e) + withContext(Dispatchers.Main) { + listener.onError() + } + } } } - private suspend fun execute(file: OCFile, imageView: ImageView, listener: GalleryImageGenerationListener) { - val key = file.remoteId + private suspend fun execute(file: OCFile, imageView: ImageView, listener: GalleryImageGenerationListener) { var newImage = false - val bitmap: Bitmap? = withContext(Dispatchers.IO) { + + if (file.remoteId == null && !file.isPreviewAvailable) { + listener.onError() + return + } + + val bitmap: Bitmap? = getBitmap(file, onThumbnailGeneration = { + newImage = true + }) + + if (bitmap == null) { + listener.onError() + return + } + + setThumbnail(bitmap, file, imageView, newImage, listener) + } + + private suspend fun getBitmap(file: OCFile, onThumbnailGeneration: () -> Unit): Bitmap? = + withContext(Dispatchers.IO) { + val key = file.remoteId var thumbnail: Bitmap? + if (file.remoteId != null || file.isPreviewAvailable) { thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache( ThumbnailsCacheManager.PREFIX_RESIZED_IMAGE + file.remoteId @@ -55,37 +82,39 @@ class GalleryImageGenerationJob( return@withContext getThumbnailFromCache(file, thumbnail, key) } - newImage = true + onThumbnailGeneration() return@withContext getThumbnailFromServerAndAddToCache(file, thumbnail) } + return@withContext null } - withContext(Dispatchers.Main) { - if (bitmap != null) { - val tagId = file.fileId.toString() - if (imageView.tag?.toString() == tagId) { - if ("image/png".equals(file.mimeType, ignoreCase = true)) { - imageView.setBackgroundColor( - ContextCompat.getColor( - MainApp.getAppContext(), - R.color.bg_default - ) - ) - } - - if (newImage) { - listener.onNewGalleryImage() - } - - imageView.setImageBitmap(bitmap) - imageView.invalidate() - } - listener.onSuccess() - } else { - listener.onError() - } + private suspend fun setThumbnail( + bitmap: Bitmap, + file: OCFile, + imageView: ImageView, + newImage: Boolean, + listener: GalleryImageGenerationListener + ) = withContext(Dispatchers.Main) { + val tagId = file.fileId.toString() + if (imageView.tag?.toString() != tagId) return@withContext + + if ("image/png".equals(file.mimeType, ignoreCase = true)) { + imageView.setBackgroundColor( + ContextCompat.getColor( + MainApp.getAppContext(), + R.color.bg_default + ) + ) + } + + if (newImage) { + listener.onNewGalleryImage() } + + imageView.setImageBitmap(bitmap) + imageView.invalidate() + listener.onSuccess() } private fun getThumbnailFromCache(file: OCFile, thumbnail: Bitmap, key: String): Bitmap { @@ -113,6 +142,7 @@ class GalleryImageGenerationJob( return result } + @Suppress("DEPRECATION") private suspend fun getThumbnailFromServerAndAddToCache(file: OCFile, thumbnail: Bitmap?): Bitmap? { var thumbnail = thumbnail try { 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 3e35c52960f1..b425c1b8a0a6 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 @@ -171,94 +171,38 @@ class OCFileListDelegate( shimmerThumbnail: LoaderImageView?, galleryRowHolder: GalleryRowHolder, width: Int - ) { + ) { scope.launch { - try { - val cachedBitmap = withContext(Dispatchers.IO) { - ThumbnailsCacheManager.getBitmapFromDiskCache( - ThumbnailsCacheManager.PREFIX_RESIZED_IMAGE + file.remoteId - ) - } - - if (cachedBitmap != null && !file.isUpdateThumbnailNeeded) { - withContext(Dispatchers.Main) { - thumbnailView.setImageBitmap(cachedBitmap) - DisplayUtils.stopShimmer(shimmerThumbnail, thumbnailView) - } - } else { - withContext(Dispatchers.Main) { - val asyncDrawable = getGalleryDrawable(file, width) - - if (shimmerThumbnail != null) { - Log_OC.d(tag, "setGalleryImage.startShimmer()") - DisplayUtils.startShimmer(shimmerThumbnail, thumbnailView) - } - - thumbnailView.setImageDrawable(asyncDrawable) - } - - galleryImageGenerationJob.run(file, thumbnailView, object : GalleryImageGenerationListener { - override fun onSuccess() { - galleryRowHolder.binding.rowLayout.invalidate() - Log_OC.d(tag, "setGalleryImage.onSuccess()") - DisplayUtils.stopShimmer(shimmerThumbnail, thumbnailView) - } - - override fun onNewGalleryImage() { - Log_OC.d(tag, "setGalleryImage.redraw()") - galleryRowHolder.redraw() - } + val cachedBitmap = withContext(Dispatchers.IO) { + ThumbnailsCacheManager.getBitmapFromDiskCache( + ThumbnailsCacheManager.PREFIX_RESIZED_IMAGE + file.remoteId + ) + } - override fun onError() { - Log_OC.d(tag, "setGalleryImage.onError()") - DisplayUtils.stopShimmer(shimmerThumbnail, thumbnailView) - } - }) + if (cachedBitmap != null && !file.isUpdateThumbnailNeeded) { + withContext(Dispatchers.Main) { + thumbnailView.setImageBitmap(cachedBitmap) + DisplayUtils.stopShimmer(shimmerThumbnail, thumbnailView) } - } catch (e: IllegalArgumentException) { - Log_OC.d(tag, "ThumbnailGenerationTask : " + e.message) + Log_OC.d(TAG, "thumbnail generation skipped, using cache: ${file.fileName}") + return@launch } - } - } - /* - @Suppress("ComplexMethod") - private fun setGalleryImage( - file: OCFile, - thumbnailView: ImageView, - shimmerThumbnail: LoaderImageView?, - galleryRowHolder: GalleryRowHolder, - width: Int - ) { - if (!ThumbnailsCacheManager.cancelPotentialThumbnailWork(file, thumbnailView)) { - Log_OC.d(tag, "setGalleryImage.cancelPotentialThumbnailWork()") - return - } + Log_OC.d(TAG, "generating thumbnail: ${file.fileName}") - for (task in asyncTasks) { - if (file.remoteId != null && task.imageKey != null && file.remoteId == task.imageKey) { - return - } - } - - try { - val task = ThumbnailsCacheManager.GalleryImageGenerationTask( - thumbnailView, - user, - storageManager, - asyncGalleryTasks, - file.remoteId, - ContextCompat.getColor(context, R.color.bg_default) - ) + // adds placeholder first + withContext(Dispatchers.Main) { + val asyncDrawable = getGalleryDrawable(file, width) - val asyncDrawable = getGalleryDrawable(file, width, task) + if (shimmerThumbnail != null) { + Log_OC.d(tag, "setGalleryImage.startShimmer()") + DisplayUtils.startShimmer(shimmerThumbnail, thumbnailView) + } - if (shimmerThumbnail != null) { - Log_OC.d(tag, "setGalleryImage.startShimmer()") - DisplayUtils.startShimmer(shimmerThumbnail, thumbnailView) + thumbnailView.setImageDrawable(asyncDrawable) } - task.setListener(object : GalleryListener { + galleryImageGenerationJob.run(file, thumbnailView, object : GalleryImageGenerationListener { override fun onSuccess() { galleryRowHolder.binding.rowLayout.invalidate() Log_OC.d(tag, "setGalleryImage.onSuccess()") @@ -275,17 +219,8 @@ class OCFileListDelegate( DisplayUtils.stopShimmer(shimmerThumbnail, thumbnailView) } }) - - thumbnailView.setImageDrawable(asyncDrawable) - - asyncGalleryTasks.add(task) - task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, file) - } catch (e: IllegalArgumentException) { - Log_OC.d(tag, "ThumbnailGenerationTask : " + e.message) } } - */ - fun setThumbnail(thumbnail: ImageView, shimmerThumbnail: LoaderImageView?, file: OCFile) { DisplayUtils.setThumbnail( From e7762e438e20223549d5bacc80adbafcfcdb248f Mon Sep 17 00:00:00 2001 From: alperozturk Date: Thu, 20 Nov 2025 11:16:57 +0100 Subject: [PATCH 05/12] perf: simplify Signed-off-by: alperozturk # Conflicts: # app/src/main/java/com/owncloud/android/ui/adapter/GalleryRowHolder.kt --- .../jobs/gallery/GalleryImageGenerationJob.kt | 44 ++--- .../datamodel/ThumbnailsCacheManager.java | 183 ------------------ .../android/ui/adapter/OCFileListDelegate.kt | 20 +- 3 files changed, 23 insertions(+), 224 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 bad96582f4c7..4f9791bed665 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 @@ -36,14 +36,12 @@ class GalleryImageGenerationJob( } suspend fun run(file: OCFile, imageView: ImageView, listener: GalleryImageGenerationListener) { - semaphore.withPermit { - try { - execute(file, imageView, listener) - } catch (e: Exception) { - Log_OC.e(TAG, "gallery image generation job: ", e) - withContext(Dispatchers.Main) { - listener.onError() - } + try { + execute(file, imageView, listener) + } catch (e: Exception) { + Log_OC.e(TAG, "gallery image generation job: ", e) + withContext(Dispatchers.Main) { + listener.onError() } } } @@ -70,23 +68,25 @@ class GalleryImageGenerationJob( private suspend fun getBitmap(file: OCFile, onThumbnailGeneration: () -> Unit): Bitmap? = withContext(Dispatchers.IO) { - val key = file.remoteId - var thumbnail: Bitmap? - - if (file.remoteId != null || file.isPreviewAvailable) { - thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache( - ThumbnailsCacheManager.PREFIX_RESIZED_IMAGE + file.remoteId - ) - - if (thumbnail != null && !file.isUpdateThumbnailNeeded) { - return@withContext getThumbnailFromCache(file, thumbnail, key) - } + if (file.remoteId == null && !file.isPreviewAvailable) { + Log_OC.w(TAG, "file has no remoteId and no preview") + return@withContext null + } - onThumbnailGeneration() - return@withContext getThumbnailFromServerAndAddToCache(file, thumbnail) + val key = file.remoteId + val cachedThumbnail = 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) } - return@withContext null + Log_OC.d(TAG, "generating new thumbnail for: ${file.fileName}") + onThumbnailGeneration() + semaphore.withPermit { + return@withContext getThumbnailFromServerAndAddToCache(file, cachedThumbnail) + } } private suspend fun setThumbnail( 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 f0e1be5b5d45..240185d2cc18 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java @@ -300,166 +300,6 @@ public static Bitmap getBitmapFromDiskCache(String key) { return null; } - public static class GalleryImageGenerationTask extends AsyncTask { - private final User user; - private final FileDataStorageManager storageManager; - private final WeakReference imageViewReference; - private OCFile file; - private final String imageKey; - private GalleryListener listener; - private final List asyncTasks; - private final int backgroundColor; - private boolean newImage = false; - - public GalleryImageGenerationTask( - ImageView imageView, - User user, - FileDataStorageManager storageManager, - List asyncTasks, - String imageKey, - int backgroundColor) { - this.user = user; - this.storageManager = storageManager; - imageViewReference = new WeakReference<>(imageView); - this.asyncTasks = asyncTasks; - this.imageKey = imageKey; - this.backgroundColor = backgroundColor; - } - - public void setListener(GalleryListener listener) { - this.listener = listener; - } - - @Override - protected Bitmap doInBackground(Object... params) { - Bitmap thumbnail; - - if (params == null || params.length == 0 || !(params[0] instanceof OCFile)) { - Log_OC.d(TAG, "Downloaded file is null or is not an instance of OCFile"); - return null; - } - - file = (OCFile) params[0]; - - if (file.getRemoteId() != null || file.isPreviewAvailable()) { - // Thumbnail in cache? - thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache( - ThumbnailsCacheManager.PREFIX_RESIZED_IMAGE + file.getRemoteId()); - - if (thumbnail != null && !file.isUpdateThumbnailNeeded()) - return getThumbnailFromCache(thumbnail); - - return getThumbnailFromServerAndAddToCache(thumbnail); - } - - Log_OC.d(TAG, "File cannot be previewed"); - return null; - } - - @Nullable - private Bitmap getThumbnailFromServerAndAddToCache(Bitmap thumbnail) { - try { - mClient = OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(user.toOwnCloudAccount(), - MainApp.getAppContext()); - - thumbnail = doResizedImageInBackground(file, storageManager); - newImage = true; - - if (MimeTypeUtil.isVideo(file) && thumbnail != null) { - thumbnail = addVideoOverlay(thumbnail, MainApp.getAppContext()); - } - - } catch (OutOfMemoryError oome) { - Log_OC.e(TAG, "Out of memory"); - } catch (Throwable t) { - // the app should never break due to a problem with thumbnails - Log_OC.e(TAG, "Generation of gallery image for " + file + " failed", t); - } - - return thumbnail; - } - - private Bitmap getThumbnailFromCache(Bitmap thumbnail) { - float size = (float) ThumbnailsCacheManager.getThumbnailDimension(); - - // resized dimensions - ImageDimension imageDimension = file.getImageDimension(); - if (imageDimension == null || - imageDimension.getWidth() != size || - imageDimension.getHeight() != size) { - file.setImageDimension(new ImageDimension(thumbnail.getWidth(), thumbnail.getHeight())); - storageManager.saveFile(file); - } - - Bitmap result = thumbnail; - if (MimeTypeUtil.isVideo(file)) { - result = ThumbnailsCacheManager.addVideoOverlay(thumbnail, MainApp.getAppContext()); - } - - if (BitmapExtensionsKt.allocationKilobyte(thumbnail) > THUMBNAIL_SIZE_IN_KB) { - result = getScaledThumbnailAfterSave(result); - } - - return result; - } - - private Bitmap getScaledThumbnailAfterSave(Bitmap thumbnail) { - Bitmap result = BitmapExtensionsKt.scaleUntil(thumbnail, THUMBNAIL_SIZE_IN_KB); - - synchronized (mThumbnailsDiskCacheLock) { - if (mThumbnailCache != null) { - Log_OC.d(TAG, "Scaling bitmap before caching: " + imageKey); - mThumbnailCache.put(imageKey, result); - } - } - - return result; - } - - protected void onPostExecute(Bitmap bitmap) { - if (bitmap != null && imageViewReference.get() != null) { - final ImageView imageView = imageViewReference.get(); - final GalleryImageGenerationTask bitmapWorkerTask = getGalleryImageGenerationTask(imageView); - - if (this == bitmapWorkerTask) { - String tagId = String.valueOf(file.getFileId()); - - if (String.valueOf(imageView.getTag()).equals(tagId)) { - if ("image/png".equalsIgnoreCase(file.getMimeType())) { - imageView.setBackgroundColor(backgroundColor); - } - - if (newImage && listener != null) { - listener.onNewGalleryImage(); - } - imageView.setImageBitmap(bitmap); - imageView.invalidate(); - } - } - - if (listener != null) { - listener.onSuccess(); - } - } else { - if (listener != null) { - listener.onError(); - } - } - - if (asyncTasks != null) { - asyncTasks.remove(this); - } - } - - public interface GalleryListener { - void onSuccess(); - - void onNewGalleryImage(); - - void onError(); - } - } - public static Bitmap getScaledThumbnailAfterSave(Bitmap thumbnail, String imageKey) { Bitmap result = BitmapExtensionsKt.scaleUntil(thumbnail, THUMBNAIL_SIZE_IN_KB); @@ -1230,16 +1070,6 @@ private static ResizedImageGenerationTask getResizedImageGenerationWorkerTask(Im return null; } - private static GalleryImageGenerationTask getGalleryImageGenerationTask(ImageView imageView) { - if (imageView != null) { - final Drawable drawable = imageView.getDrawable(); - if (drawable instanceof AsyncGalleryImageDrawable asyncDrawable) { - return asyncDrawable.getBitmapWorkerTask(); - } - } - return null; - } - public static Bitmap addVideoOverlay(Bitmap thumbnail, Context context) { Drawable playButtonDrawable = ResourcesCompat.getDrawable(MainApp.getAppContext().getResources(), @@ -1298,19 +1128,6 @@ private ResizedImageGenerationTask getBitmapWorkerTask() { } } - public static class AsyncGalleryImageDrawable extends BitmapDrawable { - private final WeakReference bitmapWorkerTaskReference; - - public AsyncGalleryImageDrawable(Resources res, Bitmap bitmap, GalleryImageGenerationTask bitmapWorkerTask) { - super(res, bitmap); - bitmapWorkerTaskReference = new WeakReference<>(bitmapWorkerTask); - } - - private GalleryImageGenerationTask getBitmapWorkerTask() { - return bitmapWorkerTaskReference.get(); - } - } - public static class AsyncMediaThumbnailDrawable extends BitmapDrawable { public AsyncMediaThumbnailDrawable(Resources res, Bitmap bitmap) { 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 b425c1b8a0a6..fe90a24ab345 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 @@ -71,7 +71,6 @@ class OCFileListDelegate( private var highlightedItem: OCFile? = null var isMultiSelect = false private val asyncTasks: MutableList = ArrayList() - private val asyncGalleryTasks: MutableList = ArrayList() private val ioScope = CoroutineScope(Dispatchers.IO) fun setHighlightedItem(highlightedItem: OCFile?) { @@ -173,24 +172,7 @@ class OCFileListDelegate( width: Int ) { scope.launch { - val cachedBitmap = withContext(Dispatchers.IO) { - ThumbnailsCacheManager.getBitmapFromDiskCache( - ThumbnailsCacheManager.PREFIX_RESIZED_IMAGE + file.remoteId - ) - } - - if (cachedBitmap != null && !file.isUpdateThumbnailNeeded) { - withContext(Dispatchers.Main) { - thumbnailView.setImageBitmap(cachedBitmap) - DisplayUtils.stopShimmer(shimmerThumbnail, thumbnailView) - } - Log_OC.d(TAG, "thumbnail generation skipped, using cache: ${file.fileName}") - return@launch - } - - Log_OC.d(TAG, "generating thumbnail: ${file.fileName}") - - // adds placeholder first + // add placeholder first withContext(Dispatchers.Main) { val asyncDrawable = getGalleryDrawable(file, width) From e16bc82fee3ee9916ed23cb12b26461c30c79817 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Thu, 20 Nov 2025 11:25:40 +0100 Subject: [PATCH 06/12] perf: simplify Signed-off-by: alperozturk --- .../jobs/gallery/GalleryImageGenerationJob.kt | 88 ++++++++++++++----- 1 file changed, 67 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 4f9791bed665..dc2b0f6afa12 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,8 +8,12 @@ package com.nextcloud.client.jobs.gallery import android.graphics.Bitmap +import android.graphics.Color +import android.graphics.drawable.BitmapDrawable import android.widget.ImageView import androidx.core.content.ContextCompat +import androidx.core.content.res.ResourcesCompat +import androidx.core.graphics.drawable.toDrawable import com.nextcloud.client.account.User import com.nextcloud.utils.allocationKilobyte import com.owncloud.android.MainApp @@ -20,7 +24,9 @@ import com.owncloud.android.datamodel.ThumbnailsCacheManager import com.owncloud.android.lib.common.OwnCloudClientManagerFactory import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.lib.resources.files.model.ImageDimension +import com.owncloud.android.utils.BitmapUtils import com.owncloud.android.utils.MimeTypeUtil +import com.owncloud.android.utils.theme.ViewThemeUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit @@ -35,9 +41,31 @@ class GalleryImageGenerationJob( private val semaphore = Semaphore(3) } - suspend fun run(file: OCFile, imageView: ImageView, listener: GalleryImageGenerationListener) { + suspend fun run( + file: OCFile, + imageView: ImageView, + width: Int, + viewThemeUtils: ViewThemeUtils, + listener: GalleryImageGenerationListener + ) { try { - execute(file, imageView, listener) + var newImage = false + + if (file.remoteId == null && !file.isPreviewAvailable) { + listener.onError() + return + } + + val bitmap: Bitmap? = getBitmap(imageView, file, width, viewThemeUtils, onThumbnailGeneration = { + newImage = true + }) + + if (bitmap == null) { + listener.onError() + return + } + + setThumbnail(bitmap, file, imageView, newImage, listener) } catch (e: Exception) { Log_OC.e(TAG, "gallery image generation job: ", e) withContext(Dispatchers.Main) { @@ -46,27 +74,38 @@ class GalleryImageGenerationJob( } } - private suspend fun execute(file: OCFile, imageView: ImageView, listener: GalleryImageGenerationListener) { - var newImage = false - - if (file.remoteId == null && !file.isPreviewAvailable) { - listener.onError() - return - } - - val bitmap: Bitmap? = getBitmap(file, onThumbnailGeneration = { - newImage = true - }) - - if (bitmap == null) { - listener.onError() - return - } - - setThumbnail(bitmap, file, imageView, newImage, listener) + private fun getPlaceholder( + file: OCFile, + width: Int, + viewThemeUtils: ViewThemeUtils + ): BitmapDrawable { + val context = MainApp.getAppContext() + + val placeholder = MimeTypeUtil.getFileTypeIcon( + file.mimeType, + file.fileName, + context, + viewThemeUtils + ) + ?: ResourcesCompat.getDrawable(context.resources, R.drawable.file_image, null) + ?: Color.GRAY.toDrawable() + + val bitmap = BitmapUtils.drawableToBitmap( + placeholder, + width / 2, + width / 2 + ) + + return BitmapDrawable(context.resources, bitmap) } - private suspend fun getBitmap(file: OCFile, onThumbnailGeneration: () -> Unit): Bitmap? = + private suspend fun getBitmap( + imageView: ImageView, + file: OCFile, + width: Int, + viewThemeUtils: ViewThemeUtils, + onThumbnailGeneration: () -> Unit + ): Bitmap? = withContext(Dispatchers.IO) { if (file.remoteId == null && !file.isPreviewAvailable) { Log_OC.w(TAG, "file has no remoteId and no preview") @@ -83,6 +122,13 @@ class GalleryImageGenerationJob( } Log_OC.d(TAG, "generating new thumbnail for: ${file.fileName}") + + // only add placeholder if new thumbnail will be generated because cached image will appear so quickly + withContext(Dispatchers.Main) { + val placeholderDrawable = getPlaceholder(file, width, viewThemeUtils) + imageView.setImageDrawable(placeholderDrawable) + } + onThumbnailGeneration() semaphore.withPermit { return@withContext getThumbnailFromServerAndAddToCache(file, cachedThumbnail) From fd784e9c01ed1b80bb9897b26d0a235fd9d87f0d Mon Sep 17 00:00:00 2001 From: alperozturk Date: Thu, 20 Nov 2025 11:49:06 +0100 Subject: [PATCH 07/12] perf: simplify Signed-off-by: alperozturk --- .../client/jobs/gallery/GalleryImageGenerationJob.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 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 dc2b0f6afa12..ab3f10de6176 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 @@ -38,9 +38,15 @@ class GalleryImageGenerationJob( ) { companion object { private const val TAG = "GalleryImageGenerationJob" - private val semaphore = Semaphore(3) + private val semaphore = Semaphore( + maxOf( + 3, + Runtime.getRuntime().availableProcessors() / 2 + ) + ) } + @Suppress("TooGenericExceptionCaught") suspend fun run( file: OCFile, imageView: ImageView, @@ -188,7 +194,7 @@ class GalleryImageGenerationJob( return result } - @Suppress("DEPRECATION") + @Suppress("DEPRECATION", "TooGenericExceptionCaught") private suspend fun getThumbnailFromServerAndAddToCache(file: OCFile, thumbnail: Bitmap?): Bitmap? { var thumbnail = thumbnail try { From cd9ba70e5818a8d0e63f6049794aa72400747ef9 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Thu, 20 Nov 2025 12:01:39 +0100 Subject: [PATCH 08/12] perf: simplify Signed-off-by: alperozturk --- .../jobs/gallery/GalleryImageGenerationJob.kt | 58 ++++++++----------- .../android/ui/adapter/GalleryRowHolder.kt | 1 - 2 files changed, 25 insertions(+), 34 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 ab3f10de6176..994256305272 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 @@ -32,10 +32,7 @@ import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.withContext -class GalleryImageGenerationJob( - private val user: User, - private val storageManager: FileDataStorageManager -) { +class GalleryImageGenerationJob(private val user: User, private val storageManager: FileDataStorageManager) { companion object { private const val TAG = "GalleryImageGenerationJob" private val semaphore = Semaphore( @@ -80,11 +77,7 @@ class GalleryImageGenerationJob( } } - private fun getPlaceholder( - file: OCFile, - width: Int, - viewThemeUtils: ViewThemeUtils - ): BitmapDrawable { + private fun getPlaceholder(file: OCFile, width: Int, viewThemeUtils: ViewThemeUtils): BitmapDrawable { val context = MainApp.getAppContext() val placeholder = MimeTypeUtil.getFileTypeIcon( @@ -111,35 +104,34 @@ class GalleryImageGenerationJob( width: Int, viewThemeUtils: ViewThemeUtils, onThumbnailGeneration: () -> Unit - ): Bitmap? = - withContext(Dispatchers.IO) { - if (file.remoteId == null && !file.isPreviewAvailable) { - Log_OC.w(TAG, "file has no remoteId and no preview") - return@withContext null - } + ): Bitmap? = withContext(Dispatchers.IO) { + if (file.remoteId == null && !file.isPreviewAvailable) { + Log_OC.w(TAG, "file has no remoteId and no preview") + return@withContext null + } - val key = file.remoteId - val cachedThumbnail = 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) - } + val key = file.remoteId + val cachedThumbnail = 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}") + Log_OC.d(TAG, "generating new thumbnail for: ${file.fileName}") - // only add placeholder if new thumbnail will be generated because cached image will appear so quickly - withContext(Dispatchers.Main) { - val placeholderDrawable = getPlaceholder(file, width, viewThemeUtils) - imageView.setImageDrawable(placeholderDrawable) - } + // only add placeholder if new thumbnail will be generated because cached image will appear so quickly + withContext(Dispatchers.Main) { + val placeholderDrawable = getPlaceholder(file, width, viewThemeUtils) + imageView.setImageDrawable(placeholderDrawable) + } - onThumbnailGeneration() - semaphore.withPermit { - return@withContext getThumbnailFromServerAndAddToCache(file, cachedThumbnail) - } + onThumbnailGeneration() + semaphore.withPermit { + return@withContext getThumbnailFromServerAndAddToCache(file, cachedThumbnail) } + } private suspend fun setThumbnail( bitmap: Bitmap, diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/GalleryRowHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/GalleryRowHolder.kt index 124c3b8f13d2..c6057c1b4a7f 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/GalleryRowHolder.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/GalleryRowHolder.kt @@ -27,7 +27,6 @@ import com.owncloud.android.databinding.GalleryRowBinding import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.GalleryRow import com.owncloud.android.datamodel.OCFile -import com.owncloud.android.datamodel.ThumbnailsCacheManager import com.owncloud.android.lib.resources.files.model.ImageDimension import com.owncloud.android.utils.BitmapUtils import com.owncloud.android.utils.DisplayUtils From 926a11a3b5a24e45519c89326b87d13447e0640b Mon Sep 17 00:00:00 2001 From: alperozturk Date: Mon, 1 Dec 2025 15:23:46 +0100 Subject: [PATCH 09/12] fix git conflict Signed-off-by: alperozturk --- .../android/ui/adapter/GalleryRowHolder.kt | 9 +- .../android/ui/adapter/OCFileListDelegate.kt | 107 +++++------------- 2 files changed, 29 insertions(+), 87 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/GalleryRowHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/GalleryRowHolder.kt index c6057c1b4a7f..8902cb9415b4 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/GalleryRowHolder.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/GalleryRowHolder.kt @@ -112,13 +112,8 @@ class GalleryRowHolder( invalidate() } - val drawable = ThumbnailsCacheManager.AsyncGalleryImageDrawable( - context.resources, - defaultBitmap, - null - ) val rowCellImageView = ImageView(context).apply { - setImageDrawable(drawable) + setImageBitmap(defaultBitmap) adjustViewBounds = true scaleType = ImageView.ScaleType.FIT_XY } @@ -180,7 +175,7 @@ class GalleryRowHolder( adjustRowCell(thumbnail, isChecked) adjustCheckBox(checkBoxImageView, isChecked) - ocFileListDelegate.bindGalleryRowThumbnail( + ocFileListDelegate.bindGalleryRow( shimmer, thumbnail, file, 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 fe90a24ab345..6c61cdafe996 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 @@ -9,13 +9,9 @@ package com.owncloud.android.ui.adapter import android.content.Context import android.content.res.Configuration -import android.graphics.Color -import android.graphics.drawable.BitmapDrawable import android.view.View import android.widget.ImageView import androidx.core.content.ContextCompat -import androidx.core.content.res.ResourcesCompat -import androidx.core.graphics.drawable.toDrawable import com.elyeproj.loaderviewlibrary.LoaderImageView import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.client.account.User @@ -39,10 +35,8 @@ import com.owncloud.android.ui.activity.FolderPickerActivity import com.owncloud.android.ui.fragment.GalleryFragment import com.owncloud.android.ui.fragment.SearchType import com.owncloud.android.ui.interfaces.OCFileListFragmentInterface -import com.owncloud.android.utils.BitmapUtils import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.EncryptionUtils -import com.owncloud.android.utils.MimeTypeUtil import com.owncloud.android.utils.theme.ViewThemeUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -72,6 +66,7 @@ class OCFileListDelegate( var isMultiSelect = false private val asyncTasks: MutableList = ArrayList() private val ioScope = CoroutineScope(Dispatchers.IO) + private val galleryImageGenerationJob = GalleryImageGenerationJob(user, storageManager) fun setHighlightedItem(highlightedItem: OCFile?) { this.highlightedItem = highlightedItem @@ -104,7 +99,7 @@ class OCFileListDelegate( checkedFiles.clear() } - fun bindGalleryRowThumbnail( + fun bindGalleryRow( shimmer: LoaderImageView?, imageView: ImageView, file: OCFile, @@ -113,13 +108,31 @@ class OCFileListDelegate( ) { imageView.tag = file.fileId - setGalleryImage( - file, - imageView, - shimmer, - galleryRowHolder, - width - ) + ioScope.launch { + galleryImageGenerationJob.run( + file, + imageView, + width, + viewThemeUtils, + object : GalleryImageGenerationListener { + override fun onSuccess() { + galleryRowHolder.binding.rowLayout.invalidate() + Log_OC.d(tag, "setGalleryImage.onSuccess()") + DisplayUtils.stopShimmer(shimmer, imageView) + } + + override fun onNewGalleryImage() { + Log_OC.d(tag, "setGalleryImage.updateRowVisuals()") + galleryRowHolder.updateRowVisuals() + } + + override fun onError() { + Log_OC.d(tag, "setGalleryImage.onError()") + DisplayUtils.stopShimmer(shimmer, imageView) + } + } + ) + } imageView.setOnClickListener { ocFileListFragmentInterface.onItemClicked(file) @@ -138,72 +151,6 @@ class OCFileListDelegate( } } - private fun getGalleryDrawable( - file: OCFile, - width: Int - ): BitmapDrawable { - val placeholder = MimeTypeUtil.getFileTypeIcon( - file.mimeType, - file.fileName, - context, - viewThemeUtils - ) - ?: ResourcesCompat.getDrawable(context.resources, R.drawable.file_image, null) - ?: Color.GRAY.toDrawable() - - val bitmap = BitmapUtils.drawableToBitmap( - placeholder, - width / 2, - width / 2 - ) - - return BitmapDrawable(context.resources, bitmap) - } - - private val scope = CoroutineScope(Dispatchers.IO) - private val galleryImageGenerationJob = GalleryImageGenerationJob(user, storageManager) - - @Suppress("ComplexMethod") - private fun setGalleryImage( - file: OCFile, - thumbnailView: ImageView, - shimmerThumbnail: LoaderImageView?, - galleryRowHolder: GalleryRowHolder, - width: Int - ) { - scope.launch { - // add placeholder first - withContext(Dispatchers.Main) { - val asyncDrawable = getGalleryDrawable(file, width) - - if (shimmerThumbnail != null) { - Log_OC.d(tag, "setGalleryImage.startShimmer()") - DisplayUtils.startShimmer(shimmerThumbnail, thumbnailView) - } - - thumbnailView.setImageDrawable(asyncDrawable) - } - - galleryImageGenerationJob.run(file, thumbnailView, object : GalleryImageGenerationListener { - override fun onSuccess() { - galleryRowHolder.binding.rowLayout.invalidate() - Log_OC.d(tag, "setGalleryImage.onSuccess()") - DisplayUtils.stopShimmer(shimmerThumbnail, thumbnailView) - } - - override fun onNewGalleryImage() { - Log_OC.d(tag, "updateRowVisuals") - galleryRowHolder.updateRowVisuals() - } - - override fun onError() { - Log_OC.d(tag, "setGalleryImage.onError()") - DisplayUtils.stopShimmer(shimmerThumbnail, thumbnailView) - } - }) - } - } - fun setThumbnail(thumbnail: ImageView, shimmerThumbnail: LoaderImageView?, file: OCFile) { DisplayUtils.setThumbnail( file, From a73ebf81168f6831e3778aabf56d6d6a4acf04e7 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Mon, 1 Dec 2025 16:10:45 +0100 Subject: [PATCH 10/12] use correct mime type Signed-off-by: alperozturk --- .../jobs/gallery/GalleryImageGenerationJob.kt | 38 +++---------------- .../java/com/nextcloud/utils/OCFileUtils.kt | 35 +++++++++++++++++ .../android/ui/adapter/GalleryRowHolder.kt | 24 ++++++------ .../android/ui/adapter/OCFileListDelegate.kt | 5 +-- 4 files changed, 54 insertions(+), 48 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 994256305272..bcbac42755c2 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,13 +8,10 @@ package com.nextcloud.client.jobs.gallery import android.graphics.Bitmap -import android.graphics.Color -import android.graphics.drawable.BitmapDrawable import android.widget.ImageView import androidx.core.content.ContextCompat -import androidx.core.content.res.ResourcesCompat -import androidx.core.graphics.drawable.toDrawable import com.nextcloud.client.account.User +import com.nextcloud.utils.OCFileUtils import com.nextcloud.utils.allocationKilobyte import com.owncloud.android.MainApp import com.owncloud.android.R @@ -24,9 +21,7 @@ import com.owncloud.android.datamodel.ThumbnailsCacheManager import com.owncloud.android.lib.common.OwnCloudClientManagerFactory import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.lib.resources.files.model.ImageDimension -import com.owncloud.android.utils.BitmapUtils import com.owncloud.android.utils.MimeTypeUtil -import com.owncloud.android.utils.theme.ViewThemeUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit @@ -47,8 +42,7 @@ class GalleryImageGenerationJob(private val user: User, private val storageManag suspend fun run( file: OCFile, imageView: ImageView, - width: Int, - viewThemeUtils: ViewThemeUtils, + imageDimension: Pair, listener: GalleryImageGenerationListener ) { try { @@ -59,7 +53,7 @@ class GalleryImageGenerationJob(private val user: User, private val storageManag return } - val bitmap: Bitmap? = getBitmap(imageView, file, width, viewThemeUtils, onThumbnailGeneration = { + val bitmap: Bitmap? = getBitmap(imageView, file, imageDimension, onThumbnailGeneration = { newImage = true }) @@ -77,32 +71,10 @@ class GalleryImageGenerationJob(private val user: User, private val storageManag } } - private fun getPlaceholder(file: OCFile, width: Int, viewThemeUtils: ViewThemeUtils): BitmapDrawable { - val context = MainApp.getAppContext() - - val placeholder = MimeTypeUtil.getFileTypeIcon( - file.mimeType, - file.fileName, - context, - viewThemeUtils - ) - ?: ResourcesCompat.getDrawable(context.resources, R.drawable.file_image, null) - ?: Color.GRAY.toDrawable() - - val bitmap = BitmapUtils.drawableToBitmap( - placeholder, - width / 2, - width / 2 - ) - - return BitmapDrawable(context.resources, bitmap) - } - private suspend fun getBitmap( imageView: ImageView, file: OCFile, - width: Int, - viewThemeUtils: ViewThemeUtils, + imageDimension: Pair, onThumbnailGeneration: () -> Unit ): Bitmap? = withContext(Dispatchers.IO) { if (file.remoteId == null && !file.isPreviewAvailable) { @@ -123,7 +95,7 @@ class GalleryImageGenerationJob(private val user: User, private val storageManag // only add placeholder if new thumbnail will be generated because cached image will appear so quickly withContext(Dispatchers.Main) { - val placeholderDrawable = getPlaceholder(file, width, viewThemeUtils) + val placeholderDrawable = OCFileUtils.getMediaPlaceholder(file, imageDimension) imageView.setImageDrawable(placeholderDrawable) } diff --git a/app/src/main/java/com/nextcloud/utils/OCFileUtils.kt b/app/src/main/java/com/nextcloud/utils/OCFileUtils.kt index 429de5a23bed..8245a4cf4255 100644 --- a/app/src/main/java/com/nextcloud/utils/OCFileUtils.kt +++ b/app/src/main/java/com/nextcloud/utils/OCFileUtils.kt @@ -6,10 +6,18 @@ */ package com.nextcloud.utils +import android.graphics.Color +import android.graphics.drawable.BitmapDrawable +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.toBitmap +import androidx.core.graphics.drawable.toDrawable import androidx.exifinterface.media.ExifInterface +import com.owncloud.android.MainApp +import com.owncloud.android.R import com.owncloud.android.datamodel.OCFile import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.utils.BitmapUtils +import com.owncloud.android.utils.MimeTypeUtil object OCFileUtils { private const val TAG = "OCFileUtils" @@ -22,9 +30,12 @@ object OCFileUtils { if (!ocFile.exists()) { ocFile.imageDimension?.width?.let { w -> ocFile.imageDimension?.height?.let { h -> + Log_OC.d(TAG, "Image dimensions are used width: $w and height: $h") return w.toInt() to h.toInt() } } + + Log_OC.d(TAG, "Default size is used: $defaultThumbnailSize") val size = defaultThumbnailSize.toInt().coerceAtLeast(1) return size to size } @@ -53,4 +64,28 @@ object OCFileUtils { Log_OC.d(TAG, "-----------------------------") } } + + fun getMediaPlaceholder(file: OCFile, imageDimension: Pair): BitmapDrawable { + val context = MainApp.getAppContext() + + val drawableId = if (MimeTypeUtil.isImage(file)) { + R.drawable.file_image + } else if (MimeTypeUtil.isVideo(file)) { + R.drawable.file_movie + } else { + R.drawable.file + } + + val drawable = ContextCompat.getDrawable(context, drawableId) + ?: return Color.GRAY.toDrawable().toBitmap(imageDimension.first, imageDimension.second) + .toDrawable(context.resources) + + val bitmap = BitmapUtils.drawableToBitmap( + drawable, + imageDimension.first, + imageDimension.second + ) + + return bitmap.toDrawable(context.resources) + } } diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/GalleryRowHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/GalleryRowHolder.kt index 8902cb9415b4..a349a5356a83 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/GalleryRowHolder.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/GalleryRowHolder.kt @@ -14,7 +14,6 @@ import android.view.ViewGroup import android.widget.FrameLayout import android.widget.ImageView import androidx.core.content.ContextCompat -import androidx.core.content.res.ResourcesCompat import androidx.core.view.get import com.afollestad.sectionedrecyclerview.SectionedViewHolder import com.elyeproj.loaderviewlibrary.LoaderImageView @@ -28,7 +27,6 @@ import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.GalleryRow import com.owncloud.android.datamodel.OCFile import com.owncloud.android.lib.resources.files.model.ImageDimension -import com.owncloud.android.utils.BitmapUtils import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.theme.ViewThemeUtils @@ -52,12 +50,6 @@ class GalleryRowHolder( private val standardMargin by lazy { context.resources.getDimension(R.dimen.standard_margin) } private val checkBoxMargin by lazy { context.resources.getDimension(R.dimen.standard_quarter_padding) } - private val defaultBitmap by lazy { - val fileDrawable = ResourcesCompat.getDrawable(context.resources, R.drawable.file_image, null) - val thumbnailSize = defaultThumbnailSize.toInt() - BitmapUtils.drawableToBitmap(fileDrawable, thumbnailSize, thumbnailSize) - } - private val checkedDrawable by lazy { ContextCompat.getDrawable(context, R.drawable.ic_checkbox_marked)?.also { viewThemeUtils.platform.tintDrawable(context, it, ColorRole.PRIMARY) @@ -78,7 +70,10 @@ class GalleryRowHolder( // Only rebuild if file count changed if (lastFileCount != requiredCount) { binding.rowLayout.removeAllViews() - repeat(requiredCount) { binding.rowLayout.addView(getRowLayout()) } + for (file in row.files) { + val rowLayout = getRowLayout(file) + binding.rowLayout.addView(rowLayout) + } lastFileCount = requiredCount } @@ -93,7 +88,7 @@ class GalleryRowHolder( bind(currentRow) } - private fun getRowLayout(): FrameLayout { + private fun getRowLayout(file: OCFile): FrameLayout { val checkbox = ImageView(context).apply { visibility = View.GONE layoutParams = FrameLayout.LayoutParams( @@ -112,8 +107,13 @@ class GalleryRowHolder( invalidate() } + // FIXME using max height for row calculation is not always producing correct row ratio + + // FIXME check from webdav --- image dimension must be available there thus no need to use default size + val mediaSize = defaultThumbnailSize.toInt() to defaultThumbnailSize.toInt() + val drawable = OCFileUtils.getMediaPlaceholder(file, mediaSize) val rowCellImageView = ImageView(context).apply { - setImageBitmap(defaultBitmap) + setImageDrawable(drawable) adjustViewBounds = true scaleType = ImageView.ScaleType.FIT_XY } @@ -180,7 +180,7 @@ class GalleryRowHolder( thumbnail, file, this, - width + width to height ) // Update layout params only if they differ 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 6c61cdafe996..aaab266e3679 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 @@ -104,7 +104,7 @@ class OCFileListDelegate( imageView: ImageView, file: OCFile, galleryRowHolder: GalleryRowHolder, - width: Int + imageDimension: Pair ) { imageView.tag = file.fileId @@ -112,8 +112,7 @@ class OCFileListDelegate( galleryImageGenerationJob.run( file, imageView, - width, - viewThemeUtils, + imageDimension, object : GalleryImageGenerationListener { override fun onSuccess() { galleryRowHolder.binding.rowLayout.invalidate() From 812f9717ac75fe4f4bd21a3db64e011d124f74e8 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Tue, 2 Dec 2025 09:14:55 +0100 Subject: [PATCH 11/12] use correct placeholder and image size Signed-off-by: alperozturk --- .../jobs/gallery/GalleryImageGenerationJob.kt | 15 +---- .../java/com/nextcloud/utils/OCFileUtils.kt | 56 ++++++++++--------- .../utils/extensions/OCFileExtensions.kt | 6 ++ .../android/ui/adapter/GalleryRowHolder.kt | 11 ++-- 4 files changed, 42 insertions(+), 46 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 bcbac42755c2..eb8eae1f0630 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 @@ -20,7 +20,6 @@ import com.owncloud.android.datamodel.OCFile import com.owncloud.android.datamodel.ThumbnailsCacheManager import com.owncloud.android.lib.common.OwnCloudClientManagerFactory import com.owncloud.android.lib.common.utils.Log_OC -import com.owncloud.android.lib.resources.files.model.ImageDimension import com.owncloud.android.utils.MimeTypeUtil import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.sync.Semaphore @@ -129,23 +128,11 @@ class GalleryImageGenerationJob(private val user: User, private val storageManag } imageView.setImageBitmap(bitmap) - imageView.invalidate() + // imageView.invalidate() listener.onSuccess() } private fun getThumbnailFromCache(file: OCFile, thumbnail: Bitmap, key: String): Bitmap { - val size = ThumbnailsCacheManager.getThumbnailDimension().toFloat() - - val imageDimension = file.imageDimension - if (imageDimension == null || imageDimension.width != size || imageDimension.height != size) { - val newDimension = ImageDimension( - thumbnail.getWidth().toFloat(), - thumbnail.getHeight().toFloat() - ) - file.imageDimension = newDimension - storageManager.saveFile(file) - } - var result = thumbnail if (MimeTypeUtil.isVideo(file)) { result = ThumbnailsCacheManager.addVideoOverlay(thumbnail, MainApp.getAppContext()) diff --git a/app/src/main/java/com/nextcloud/utils/OCFileUtils.kt b/app/src/main/java/com/nextcloud/utils/OCFileUtils.kt index 8245a4cf4255..142c6edfa009 100644 --- a/app/src/main/java/com/nextcloud/utils/OCFileUtils.kt +++ b/app/src/main/java/com/nextcloud/utils/OCFileUtils.kt @@ -27,39 +27,41 @@ object OCFileUtils { try { Log_OC.d(TAG, "Getting image size for: ${ocFile.fileName}") - if (!ocFile.exists()) { - ocFile.imageDimension?.width?.let { w -> - ocFile.imageDimension?.height?.let { h -> - Log_OC.d(TAG, "Image dimensions are used width: $w and height: $h") - return w.toInt() to h.toInt() - } - } - - Log_OC.d(TAG, "Default size is used: $defaultThumbnailSize") - val size = defaultThumbnailSize.toInt().coerceAtLeast(1) - return size to size + val widthFromDimension = ocFile.imageDimension?.width + val heightFromDimension = ocFile.imageDimension?.height + if (widthFromDimension != null && heightFromDimension != null) { + val width = widthFromDimension.toInt() + val height = heightFromDimension.toInt() + Log_OC.d(TAG, "Image dimensions are used, width: $width, height: $height") + return width to height } - val exif = ExifInterface(ocFile.storagePath) - val width = exif.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, 0) - val height = exif.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, 0) + return if (ocFile.exists()) { + val exif = ExifInterface(ocFile.storagePath) + val width = exif.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, 0) + val height = exif.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, 0) - if (width > 0 && height > 0) { - Log_OC.d(TAG, "Exif used width: $width and height: $height") - return width to height - } + if (width > 0 && height > 0) { + Log_OC.d(TAG, "Exif used width: $width and height: $height") + width to height + } - val (bitmapWidth, bitmapHeight) = BitmapUtils.getImageResolution(ocFile.storagePath) - .let { it[0] to it[1] } + val (bitmapWidth, bitmapHeight) = BitmapUtils.getImageResolution(ocFile.storagePath) + .let { it[0] to it[1] } - if (bitmapWidth > 0 && bitmapHeight > 0) { - Log_OC.d(TAG, "BitmapUtils.getImageResolution used width: $bitmapWidth and height: $bitmapHeight") - return bitmapWidth to bitmapHeight - } + if (bitmapWidth > 0 && bitmapHeight > 0) { + Log_OC.d(TAG, "BitmapUtils.getImageResolution used width: $bitmapWidth and height: $bitmapHeight") + bitmapWidth to bitmapHeight + } - val fallback = defaultThumbnailSize.toInt().coerceAtLeast(1) - Log_OC.d(TAG, "Default size used width: $fallback and height: $fallback") - return fallback to fallback + val fallback = defaultThumbnailSize.toInt().coerceAtLeast(1) + Log_OC.d(TAG, "Default size used width: $fallback and height: $fallback") + fallback to fallback + } else { + Log_OC.d(TAG, "Default size is used: $defaultThumbnailSize") + val size = defaultThumbnailSize.toInt().coerceAtLeast(1) + size to size + } } finally { Log_OC.d(TAG, "-----------------------------") } diff --git a/app/src/main/java/com/nextcloud/utils/extensions/OCFileExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/OCFileExtensions.kt index 1e327395f7c7..34f46ab66922 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/OCFileExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/OCFileExtensions.kt @@ -31,3 +31,9 @@ fun List.limitToPersonalFiles(userId: String): List = filter { f ownerId == userId && !file.isSharedWithMe && !file.mounted() } == true } + +fun OCFile.mediaSize(defaultThumbnailSize: Float): Pair { + val width = (imageDimension?.width?.toInt() ?: defaultThumbnailSize.toInt()) + val height = (imageDimension?.height?.toInt() ?: defaultThumbnailSize.toInt()) + return width to height +} diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/GalleryRowHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/GalleryRowHolder.kt index a349a5356a83..877ed965f7e0 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/GalleryRowHolder.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/GalleryRowHolder.kt @@ -20,6 +20,7 @@ import com.elyeproj.loaderviewlibrary.LoaderImageView import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.utils.OCFileUtils import com.nextcloud.utils.extensions.makeRounded +import com.nextcloud.utils.extensions.mediaSize import com.nextcloud.utils.extensions.setVisibleIf import com.owncloud.android.R import com.owncloud.android.databinding.GalleryRowBinding @@ -101,21 +102,21 @@ class GalleryRowHolder( } } + val mediaSize = file.mediaSize(defaultThumbnailSize) + val (width, height) = mediaSize + val shimmer = LoaderImageView(context).apply { setImageResource(R.drawable.background) resetLoader() - invalidate() + layoutParams = FrameLayout.LayoutParams(width, height) } - // FIXME using max height for row calculation is not always producing correct row ratio - - // FIXME check from webdav --- image dimension must be available there thus no need to use default size - val mediaSize = defaultThumbnailSize.toInt() to defaultThumbnailSize.toInt() val drawable = OCFileUtils.getMediaPlaceholder(file, mediaSize) val rowCellImageView = ImageView(context).apply { setImageDrawable(drawable) adjustViewBounds = true scaleType = ImageView.ScaleType.FIT_XY + layoutParams = FrameLayout.LayoutParams(width, height) } return FrameLayout(context).apply { From 2757665a73fb44770fcdf1a4cd01aa412f6b62cd Mon Sep 17 00:00:00 2001 From: alperozturk Date: Tue, 2 Dec 2025 09:47:27 +0100 Subject: [PATCH 12/12] use correct placeholder and image size Signed-off-by: alperozturk --- .../nextcloud/client/jobs/gallery/GalleryImageGenerationJob.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 eb8eae1f0630..a18dadecb94e 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 @@ -128,7 +128,7 @@ class GalleryImageGenerationJob(private val user: User, private val storageManag } imageView.setImageBitmap(bitmap) - // imageView.invalidate() + imageView.invalidate() listener.onSuccess() }