From e8a220d5df9ac110388af525c145bfd341bdef43 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 7 Aug 2026 10:33:26 -0400 Subject: [PATCH 1/2] feat: improve thumbnail generation Signed-off-by: alex --- .../datamodel/ThumbnailsCacheManager.java | 5 +- .../files/filelist/FileListAdapter.kt | 58 ++++++------------- .../thumbnails/ThumbnailsRequester.kt | 38 ++++++++---- 3 files changed, 48 insertions(+), 53 deletions(-) diff --git a/owncloudApp/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java b/owncloudApp/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java index 7899af41e9d..bd43bf8472a 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java +++ b/owncloudApp/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java @@ -72,7 +72,7 @@ public class ThumbnailsCacheManager { private static DiskLruImageCache mThumbnailCache = null; private static boolean mThumbnailCacheStarting = true; - private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB + private static final int DISK_CACHE_SIZE = 1024 * 1024 * 250; // 250MB private static final CompressFormat mCompressFormat = CompressFormat.JPEG; private static final int mCompressQuality = 70; private static OwnCloudClient mClient = null; @@ -447,8 +447,7 @@ public static boolean cancelPotentialThumbnailWork(Object file, ImageView imageV private static ThumbnailGenerationTask getBitmapWorkerTask(ImageView imageView) { if (imageView != null) { final Drawable drawable = imageView.getDrawable(); - if (drawable instanceof AsyncThumbnailDrawable) { - final AsyncThumbnailDrawable asyncDrawable = (AsyncThumbnailDrawable) drawable; + if (drawable instanceof AsyncThumbnailDrawable asyncDrawable) { return asyncDrawable.getBitmapWorkerTask(); } } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/files/filelist/FileListAdapter.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/files/filelist/FileListAdapter.kt index fd0cf999b3f..569c5aa8c1c 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/files/filelist/FileListAdapter.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/files/filelist/FileListAdapter.kt @@ -26,7 +26,6 @@ package com.owncloud.android.presentation.files.filelist import android.accounts.Account import android.content.Context -import android.graphics.Bitmap import android.graphics.Color import android.view.LayoutInflater import android.view.View @@ -42,7 +41,8 @@ import com.owncloud.android.R import com.owncloud.android.databinding.GridItemBinding import com.owncloud.android.databinding.ItemFileListBinding import com.owncloud.android.databinding.ListFooterBinding -import com.owncloud.android.datamodel.ThumbnailsCacheManager +import com.owncloud.android.presentation.thumbnails.ThumbnailsRequester +import coil.load import com.owncloud.android.domain.files.model.FileListOption import com.owncloud.android.domain.files.model.OCFileWithSyncInfo import com.owncloud.android.domain.files.model.OCFooterFile @@ -188,7 +188,6 @@ class FileListAdapter( val fileIcon = holder.itemView.findViewById(R.id.thumbnail).apply { tag = file.id } - val thumbnail: Bitmap? = file.remoteId?.let { ThumbnailsCacheManager.getBitmapFromDiskCache(file.remoteId) } holder.itemView.findViewById(R.id.ListItemLayout)?.apply { contentDescription = "LinearLayout-$name" @@ -203,7 +202,7 @@ class FileListAdapter( holder.itemView.findViewById(R.id.shared_via_users_icon).isVisible = file.sharedWithSharee == true || file.isSharedWithMe - setSpecificViewHolder(viewType, holder, fileWithSyncInfo, thumbnail) + setSpecificViewHolder(viewType, holder, fileWithSyncInfo) setIconPinAccordingToFilesLocalState(holder.itemView.findViewById(R.id.localFileIndicator), fileWithSyncInfo) @@ -237,22 +236,12 @@ class FileListAdapter( // Folder fileIcon.setImageResource(R.drawable.ic_menu_archive) } else { - // Set file icon depending on its mimetype. Ask for thumbnail later. - fileIcon.setImageResource(MimetypeIconUtil.getFileTypeIconId(file.mimeType, file.fileName)) - - if (thumbnail != null) { - fileIcon.setImageBitmap(thumbnail) - } - if (file.needsToUpdateThumbnail && ThumbnailsCacheManager.cancelPotentialThumbnailWork(file, fileIcon)) { - // generate new Thumbnail - val task = ThumbnailsCacheManager.ThumbnailGenerationTask(fileIcon, account) - val asyncDrawable = ThumbnailsCacheManager.AsyncThumbnailDrawable(context.resources, thumbnail, task) - - // If drawable is not visible, do not update it. - if (asyncDrawable.minimumHeight > 0 && asyncDrawable.minimumWidth > 0) { - fileIcon.setImageDrawable(asyncDrawable) - } - task.execute(file) + fileIcon.load( + ThumbnailsRequester.getPreviewUriForFile(fileWithSyncInfo, account!!), + ThumbnailsRequester.getCoilImageLoader() + ) { + placeholder(MimetypeIconUtil.getFileTypeIconId(file.mimeType, file.fileName)) + error(MimetypeIconUtil.getFileTypeIconId(file.mimeType, file.fileName)) } if (file.mimeType == "image/png") { @@ -272,7 +261,7 @@ class FileListAdapter( } } - private fun setSpecificViewHolder(viewType: Int, holder: RecyclerView.ViewHolder, fileWithSyncInfo: OCFileWithSyncInfo, thumbnail: Bitmap?) { + private fun setSpecificViewHolder(viewType: Int, holder: RecyclerView.ViewHolder, fileWithSyncInfo: OCFileWithSyncInfo) { val file = fileWithSyncInfo.file when (viewType) { @@ -327,23 +316,14 @@ class FileListAdapter( val fileIcon = holder.itemView.findViewById(R.id.thumbnail) val layoutParams = fileIcon.layoutParams as ViewGroup.MarginLayoutParams - if (thumbnail == null) { - view.binding.Filename.text = file.fileName - // Reset layout params values default - manageGridLayoutParams( - layoutParams = layoutParams, - marginVertical = 0, - height = context.resources.getDimensionPixelSize(R.dimen.item_file_grid_height), - width = context.resources.getDimensionPixelSize(R.dimen.item_file_grid_width), - ) - } else { - manageGridLayoutParams( - layoutParams = layoutParams, - marginVertical = context.resources.getDimensionPixelSize(R.dimen.item_file_image_grid_margin), - height = ViewGroup.LayoutParams.MATCH_PARENT, - width = ViewGroup.LayoutParams.MATCH_PARENT, - ) - } + view.binding.Filename.text = file.fileName + // Reset layout params values default + manageGridLayoutParams( + layoutParams = layoutParams, + marginVertical = 0, + height = context.resources.getDimensionPixelSize(R.dimen.item_file_grid_height), + width = context.resources.getDimensionPixelSize(R.dimen.item_file_grid_width), + ) } } } @@ -412,7 +392,7 @@ class FileListAdapter( } filesCount == 1 -> { - when { + when { foldersCount <= 0 -> { context.getString(R.string.file_list__footer__file) } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/thumbnails/ThumbnailsRequester.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/thumbnails/ThumbnailsRequester.kt index e1441fbed39..a999ac22e0a 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/thumbnails/ThumbnailsRequester.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/thumbnails/ThumbnailsRequester.kt @@ -38,6 +38,7 @@ import com.owncloud.android.lib.common.http.HttpConstants.AUTHORIZATION_HEADER import com.owncloud.android.lib.common.http.HttpConstants.OC_X_REQUEST_ID import com.owncloud.android.lib.common.http.HttpConstants.USER_AGENT_HEADER import com.owncloud.android.lib.common.utils.RandomUtils +import com.owncloud.android.lib.common.accounts.AccountUtils as AppAccountUtils import com.owncloud.android.presentation.authentication.AccountUtils import okhttp3.Headers.Companion.toHeaders import okhttp3.Interceptor @@ -52,23 +53,37 @@ object ThumbnailsRequester : KoinComponent { private val clientManager: ClientManager by inject() private const val SPACE_SPECIAL_PREVIEW_URI = "%s?scalingup=0&a=1&x=%d&y=%d&c=%s&preview=1" - private const val FILE_PREVIEW_URI = "%s%s?x=%d&y=%d&c=%s&preview=1&id=%s" + private const val FILE_PREVIEW_URI = "%s/%s?x=%d&y=%d&c=%s&preview=1&id=%s" - private const val DISK_CACHE_SIZE: Long = 1024 * 1024 * 10 // 10MB + private const val DISK_CACHE_SIZE: Long = 1024 * 1024 * 250 // 250MB + private var imageLoader: ImageLoader? = null + private var lastAccountName: String? = null + + @Synchronized fun getCoilImageLoader(): ImageLoader { - val ownCloudClient = getOwnCloudClient() + val currentAccount = AccountUtils.getCurrentOwnCloudAccount(appContext) + val currentAccountName = currentAccount?.name + + if (imageLoader != null && lastAccountName == currentAccountName) { + return imageLoader!! + } + + val ownCloudClient = clientManager.getClientForCoilThumbnails( + accountName = currentAccountName ?: "" + ) val coilRequestHeaderInterceptor = CoilRequestHeaderInterceptor( requestHeaders = hashMapOf( - AUTHORIZATION_HEADER to ownCloudClient.credentials.headerAuth, + AUTHORIZATION_HEADER to (ownCloudClient.credentials?.headerAuth ?: ""), ACCEPT_ENCODING_HEADER to ACCEPT_ENCODING_IDENTITY, USER_AGENT_HEADER to SingleSessionManager.getUserAgent(), OC_X_REQUEST_ID to RandomUtils.generateRandomUUID(), ) ) - return ImageLoader(appContext).newBuilder().okHttpClient( + lastAccountName = currentAccountName + imageLoader = ImageLoader(appContext).newBuilder().okHttpClient( okHttpClient = ownCloudClient.okHttpClient.newBuilder().addNetworkInterceptor(coilRequestHeaderInterceptor).build() ).logger(DebugLogger()) .memoryCache { @@ -83,6 +98,8 @@ object ThumbnailsRequester : KoinComponent { .build() } .build() + + return imageLoader!! } fun getPreviewUriForSpaceSpecial(spaceSpecial: SpaceSpecial): String { @@ -99,9 +116,8 @@ object ThumbnailsRequester : KoinComponent { } fun getPreviewUriForFile(ocFile: OCFileWithSyncInfo, account: Account): String { - var baseUrl = getOwnCloudClient().baseUri.toString() + "/remote.php/dav/files/" + account.name.split("@".toRegex()) - .dropLastWhile { it.isEmpty() } - .toTypedArray()[0] + var baseUrl = getOwnCloudClient().baseUri.toString() + "/remote.php/dav/files/" + + AppAccountUtils.getUserId(account, appContext) ocFile.space?.getSpaceSpecialImage()?.let { baseUrl = it.webDavUrl } @@ -111,8 +127,8 @@ object ThumbnailsRequester : KoinComponent { return String.format( Locale.ROOT, FILE_PREVIEW_URI, - baseUrl, - Uri.encode(ocFile.file.remotePath, "/"), + baseUrl.removeSuffix("/"), + Uri.encode(ocFile.file.remotePath, "/").removePrefix("/"), fileThumbnailSize, fileThumbnailSize, ocFile.file.etag, @@ -121,7 +137,7 @@ object ThumbnailsRequester : KoinComponent { } private fun getOwnCloudClient() = clientManager.getClientForCoilThumbnails( - accountName = AccountUtils.getCurrentOwnCloudAccount(appContext).name + accountName = AccountUtils.getCurrentOwnCloudAccount(appContext)?.name ?: "" ) private class CoilRequestHeaderInterceptor( From a484d2a8db721315b7008b4b20cc457d1ea3c2a1 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 7 Aug 2026 10:33:49 -0400 Subject: [PATCH 2/2] chore: add calens file Signed-off-by: alex --- changelog/unreleased/4946 | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelog/unreleased/4946 diff --git a/changelog/unreleased/4946 b/changelog/unreleased/4946 new file mode 100644 index 00000000000..b0ce5b7d2ee --- /dev/null +++ b/changelog/unreleased/4946 @@ -0,0 +1,5 @@ +Enhancement: Thumbnail caching + +Thumbnail caching system has been improved for a better experience and performance. + +https://github.com/owncloud/android/pull/4946