瀏覽代碼

Merge pull request #14030 from nextcloud/bugfix/icon-visibilities

BugFix - Icon Visibilities
Tobias Kaminsky 4 月之前
父節點
當前提交
d4b46893fd
共有 18 個文件被更改,包括 182 次插入109 次删除
  1. 二進制
      app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.GroupfolderListFragmentIT_showGroupfolders.png
  2. 二進制
      app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showFolderTypes.png
  3. 二進制
      app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showSharedFiles.png
  4. 二進制
      app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.SharedListFragmentIT_showSharedFiles.png
  5. 8 0
      app/src/androidTest/java/com/owncloud/android/ui/fragment/OCFileListFragmentStaticServerIT.kt
  6. 2 10
      app/src/androidTest/java/com/owncloud/android/utils/DrawableUtilTests.kt
  7. 1 1
      app/src/main/java/com/nextcloud/utils/ShortcutUtil.kt
  8. 9 0
      app/src/main/java/com/nextcloud/utils/extensions/ViewExtensions.kt
  9. 1 1
      app/src/main/java/com/owncloud/android/ui/activity/EditorWebView.java
  10. 1 1
      app/src/main/java/com/owncloud/android/ui/activity/ShareActivity.java
  11. 1 1
      app/src/main/java/com/owncloud/android/ui/adapter/GroupfolderListAdapter.kt
  12. 37 49
      app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java
  13. 31 28
      app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt
  14. 1 1
      app/src/main/java/com/owncloud/android/ui/adapter/ReceiveExternalFilesAdapter.kt
  15. 1 1
      app/src/main/java/com/owncloud/android/utils/DisplayUtils.java
  16. 24 10
      app/src/main/java/com/owncloud/android/utils/DrawableUtil.kt
  17. 49 6
      app/src/main/java/com/owncloud/android/utils/MimeTypeUtil.java
  18. 16 0
      app/src/main/res/drawable/ic_folder_offline.xml

二進制
app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.GroupfolderListFragmentIT_showGroupfolders.png


二進制
app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showFolderTypes.png


二進制
app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showSharedFiles.png


二進制
app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.SharedListFragmentIT_showSharedFiles.png


+ 8 - 0
app/src/androidTest/java/com/owncloud/android/ui/fragment/OCFileListFragmentStaticServerIT.kt

@@ -300,6 +300,14 @@ class OCFileListFragmentStaticServerIT : AbstractIT() {
             sut.storageManager.saveFile(this)
         }
 
+        OCFile("/offlineOperation/").apply {
+            mimeType = MimeType.DIRECTORY
+            decryptedRemotePath = "/offlineOperation/"
+            modificationTimestamp = System.currentTimeMillis()
+            parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId
+            sut.storageManager.saveFile(this)
+        }
+
         sut.addFragment(fragment)
 
         shortSleep()

+ 2 - 10
app/src/androidTest/java/com/owncloud/android/utils/DrawableUtilTests.kt

@@ -12,18 +12,15 @@ import android.graphics.Bitmap
 import android.graphics.drawable.BitmapDrawable
 import androidx.test.platform.app.InstrumentationRegistry
 import org.junit.After
-import org.junit.Assert.fail
 import org.junit.Before
 import org.junit.Test
 
 class DrawableUtilTests {
 
-    private var sut: DrawableUtil? = null
     private var context: Context? = null
 
     @Before
     fun setUp() {
-        sut = DrawableUtil()
         context = InstrumentationRegistry.getInstrumentation().context
     }
 
@@ -32,18 +29,13 @@ class DrawableUtilTests {
         val bitmap: Bitmap = Bitmap.createBitmap(2, 2, Bitmap.Config.ARGB_8888)
         val drawable = BitmapDrawable(context?.resources, bitmap)
 
-        val layerDrawable = sut?.addDrawableAsOverlay(drawable, drawable)
+        val layerDrawable = DrawableUtil.addDrawableAsOverlay(drawable, drawable)
 
-        if (layerDrawable == null) {
-            fail("Layer drawable expected to be not null")
-        }
-
-        assert(layerDrawable?.numberOfLayers == 2)
+        assert(layerDrawable.numberOfLayers == 2)
     }
 
     @After
     fun destroy() {
-        sut = null
         context = null
     }
 }

+ 1 - 1
app/src/main/java/com/nextcloud/utils/ShortcutUtil.kt

@@ -62,7 +62,7 @@ class ShortcutUtil @Inject constructor(private val mContext: Context) {
                 val isDarkModeActive = syncedFolderProvider.preferences.isDarkModeEnabled
 
                 val overlayIconId = file.getFileOverlayIconId(isAutoUploadFolder)
-                val drawable = MimeTypeUtil.getFileIcon(isDarkModeActive, overlayIconId, mContext, viewThemeUtils)
+                val drawable = MimeTypeUtil.getFolderIcon(isDarkModeActive, overlayIconId, mContext, viewThemeUtils)
                 val bitmapIcon = drawable.toBitmap()
                 icon = IconCompat.createWithBitmap(bitmapIcon)
             } else {

+ 9 - 0
app/src/main/java/com/nextcloud/utils/extensions/ViewExtensions.kt

@@ -18,6 +18,15 @@ fun View?.setVisibleIf(condition: Boolean) {
     visibility = if (condition) View.VISIBLE else View.GONE
 }
 
+fun View?.makeRounded(context: Context, cornerRadius: Float) {
+    this?.let {
+        it.apply {
+            outlineProvider = createRoundedOutline(context, cornerRadius)
+            clipToOutline = true
+        }
+    }
+}
+
 fun createRoundedOutline(context: Context, cornerRadiusValue: Float): ViewOutlineProvider {
     return object : ViewOutlineProvider() {
         override fun getOutline(view: View, outline: Outline) {

+ 1 - 1
app/src/main/java/com/owncloud/android/ui/activity/EditorWebView.java

@@ -250,7 +250,7 @@ public abstract class EditorWebView extends ExternalSiteWebView {
             boolean isAutoUploadFolder = SyncedFolderProvider.isAutoUploadFolder(syncedFolderProvider, file, user);
 
             Integer overlayIconId = file.getFileOverlayIconId(isAutoUploadFolder);
-            LayerDrawable drawable = MimeTypeUtil.getFileIcon(preferences.isDarkModeEnabled(), overlayIconId, this, viewThemeUtils);
+            LayerDrawable drawable = MimeTypeUtil.getFolderIcon(preferences.isDarkModeEnabled(), overlayIconId, this, viewThemeUtils);
             binding.thumbnail.setImageDrawable(drawable);
         } else {
             if ((MimeTypeUtil.isImage(file) || MimeTypeUtil.isVideo(file)) && file.getRemoteId() != null) {

+ 1 - 1
app/src/main/java/com/owncloud/android/ui/activity/ShareActivity.java

@@ -73,7 +73,7 @@ public class ShareActivity extends FileActivity {
             boolean isAutoUploadFolder = SyncedFolderProvider.isAutoUploadFolder(syncedFolderProvider, file, optionalUser.get());
 
             Integer overlayIconId = file.getFileOverlayIconId(isAutoUploadFolder);
-            LayerDrawable drawable = MimeTypeUtil.getFileIcon(preferences.isDarkModeEnabled(), overlayIconId, this, viewThemeUtils);
+            LayerDrawable drawable = MimeTypeUtil.getFolderIcon(preferences.isDarkModeEnabled(), overlayIconId, this, viewThemeUtils);
             binding.shareFileIcon.setImageDrawable(drawable);
         } else {
             binding.shareFileIcon.setImageDrawable(MimeTypeUtil.getFileTypeIcon(file.getMimeType(),

+ 1 - 1
app/src/main/java/com/owncloud/android/ui/adapter/GroupfolderListAdapter.kt

@@ -35,7 +35,7 @@ class GroupfolderListAdapter(
 
     private fun getFolderIcon(): LayerDrawable? {
         val overlayDrawableId = R.drawable.ic_folder_overlay_account_group
-        return MimeTypeUtil.getFileIcon(false, overlayDrawableId, context, viewThemeUtils)
+        return MimeTypeUtil.getFolderIcon(false, overlayDrawableId, context, viewThemeUtils)
     }
 
     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {

+ 37 - 49
app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java

@@ -18,7 +18,6 @@ import android.content.ContentValues;
 import android.content.res.Resources;
 import android.graphics.Bitmap;
 import android.graphics.Color;
-import android.graphics.PorterDuff;
 import android.graphics.drawable.Drawable;
 import android.os.Build;
 import android.os.Handler;
@@ -97,6 +96,7 @@ import java.util.stream.Collectors;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
+import androidx.core.content.ContextCompat;
 import androidx.recyclerview.widget.RecyclerView;
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 import me.zhanghai.android.fastscroll.PopupTextProvider;
@@ -141,6 +141,7 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
 
     private final long footerId = UUID.randomUUID().getLeastSignificantBits();
     private final long headerId = UUID.randomUUID().getLeastSignificantBits();
+    private final SyncedFolderProvider syncedFolderProvider;
 
     public OCFileListAdapter(
         Activity activity,
@@ -172,9 +173,8 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
             .get(activity)
             .getUserData(this.user.toPlatformAccount(),
                          AccountUtils.Constants.KEY_USER_ID);
-
+        this.syncedFolderProvider = syncedFolderProvider;
         this.viewThemeUtils = viewThemeUtils;
-
         ocFileListDelegate = new OCFileListDelegate(FileUploadHelper.Companion.instance(),
                                                     activity,
                                                     ocFileListFragmentInterface,
@@ -536,12 +536,7 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
             }
         }
 
-        ViewExtensionsKt.setVisibleIf(holder.getShared(), !file.isOfflineOperation());
-        if (file.isFolder()) {
-            setColorFilterForOfflineCreateFolderOperations(holder, file);
-        } else {
-            setColorFilterForOfflineCreateFileOperations(holder, file);
-        }
+        configureThumbnail(holder, file);
     }
 
     private void bindListItemViewHolder(ListItemViewHolder holder, OCFile file) {
@@ -651,75 +646,68 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
             holder.getOverflowMenu().setImageResource(R.drawable.ic_dots_vertical);
         }
 
-        applyVisualsForOfflineOperations(holder, file);
+        configureThumbnail(holder, file);
     }
 
     private void prepareFileSize(ListItemViewHolder holder, OCFile file, long size) {
         holder.getFileSize().setVisibility(View.VISIBLE);
-        ViewExtensionsKt.setVisibleIf(holder.getFileSizeSeparator(), !file.isOfflineOperation());
         String fileSizeText = getFileSizeText(file, size);
         holder.getFileSize().setText(fileSizeText);
     }
 
     private String getFileSizeText(OCFile file, long size) {
-        OfflineOperationEntity entity = mStorageManager.getOfflineEntityFromOCFile(file);
-        boolean isRemoveOperation = entity != null && entity.getType() instanceof OfflineOperationType.RemoveFile;
+        if (!file.isOfflineOperation()) {
+            return DisplayUtils.bytesToHumanReadable(size);
+        }
 
+        OfflineOperationEntity entity = mStorageManager.getOfflineEntityFromOCFile(file);
+        boolean isRemoveOperation = (entity != null && entity.getType() instanceof OfflineOperationType.RemoveFile);
         if (isRemoveOperation) {
             return activity.getString(R.string.oc_file_list_adapter_offline_operation_remove_description_text);
         }
 
-        if (file.isOfflineOperation()) {
-            return activity.getString(R.string.oc_file_list_adapter_offline_operation_description_text);
-        }
-
-        return DisplayUtils.bytesToHumanReadable(size);
-    }
-
-    private void applyVisualsForOfflineOperations(ListItemViewHolder holder, OCFile file) {
-        ViewExtensionsKt.setVisibleIf(holder.getShared(), !file.isOfflineOperation());
-
-        if (file.isFolder()) {
-            setColorFilterForOfflineCreateFolderOperations(holder, file);
-        } else {
-            setColorFilterForOfflineCreateFileOperations(holder, file);
-        }
+        return activity.getString(R.string.oc_file_list_adapter_offline_operation_description_text);
     }
 
     private final ExecutorService executorService = Executors.newCachedThreadPool();
     private final Handler mainHandler = new Handler(Looper.getMainLooper());
 
-    private void setColorFilterForOfflineCreateFileOperations(ListViewHolder holder, OCFile file) {
-        if (!file.isOfflineOperation()) {
-            return;
-        }
+    private void configureThumbnail(ListViewHolder holder, OCFile file) {
+        final var context = MainApp.getAppContext();
 
-        executorService.execute(() -> {
-            OfflineOperationEntity entity = mStorageManager.offlineOperationDao.getByPath(file.getDecryptedRemotePath());
+        if (file.isOfflineOperation()) {
+            if (file.isFolder()) {
+                Drawable icon = ContextCompat.getDrawable(context, R.drawable.ic_folder_offline);
+                holder.getThumbnail().setImageDrawable(icon);
+            } else {
+                executorService.execute(() -> {
+                    OfflineOperationEntity entity = mStorageManager.offlineOperationDao.getByPath(file.getDecryptedRemotePath());
 
-            if (entity != null && entity.getType() != null && entity.getType() instanceof OfflineOperationType.CreateFile createFileOperation) {
-                Bitmap bitmap = BitmapUtils.decodeSampledBitmapFromFile(createFileOperation.getLocalPath(), holder.getThumbnail().getWidth(), holder.getThumbnail().getHeight());
-                if (bitmap == null) return;
+                    if (entity != null && entity.getType() != null && entity.getType() instanceof OfflineOperationType.CreateFile createFileOperation) {
+                        Bitmap bitmap = BitmapUtils.decodeSampledBitmapFromFile(createFileOperation.getLocalPath(), holder.getThumbnail().getWidth(), holder.getThumbnail().getHeight());
+                        if (bitmap == null) return;
 
-                Bitmap thumbnail = BitmapUtils.addColorFilter(bitmap, Color.GRAY,100);
-                mainHandler.post(() -> holder.getThumbnail().setImageBitmap(thumbnail));
+                        Bitmap thumbnail = BitmapUtils.addColorFilter(bitmap, Color.GRAY,100);
+                        mainHandler.post(() -> holder.getThumbnail().setImageBitmap(thumbnail));
+                    }
+                });
             }
-        });
+        } else {
+            boolean isAutoUpload = SyncedFolderProvider.isAutoUploadFolder(syncedFolderProvider, file, user);
+            boolean isDarkModeActive = preferences.isDarkModeEnabled();
+            Drawable icon = MimeTypeUtil.getOCFileIcon(file, context, viewThemeUtils, isAutoUpload, isDarkModeActive);
+            holder.getThumbnail().setImageDrawable(icon);
+
+            if (!file.isFolder()) {
+                ViewExtensionsKt.makeRounded(holder.getThumbnail(), context, 4);
+            }
+        }
     }
 
     public void onDestroy() {
         executorService.shutdown();
     }
 
-    private void setColorFilterForOfflineCreateFolderOperations(ListViewHolder holder, OCFile file) {
-        if (file.isOfflineOperation()) {
-            holder.getThumbnail().setColorFilter(Color.GRAY, PorterDuff.Mode.SRC_IN);
-        } else {
-            Drawable drawable = viewThemeUtils.platform.tintDrawable(MainApp.getAppContext(), holder.getThumbnail().getDrawable(), ColorRole.PRIMARY);
-            holder.getThumbnail().setImageDrawable(drawable);
-        }
-    }
-
     @Override
     public void onViewAttachedToWindow(@NonNull RecyclerView.ViewHolder holder) {
         if (holder instanceof ListViewHolder) {

+ 31 - 28
app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt

@@ -21,7 +21,7 @@ import com.nextcloud.client.account.User
 import com.nextcloud.client.jobs.download.FileDownloadHelper
 import com.nextcloud.client.jobs.upload.FileUploadHelper
 import com.nextcloud.client.preferences.AppPreferences
-import com.nextcloud.utils.extensions.createRoundedOutline
+import com.nextcloud.utils.extensions.makeRounded
 import com.nextcloud.utils.mdm.MDMConfig
 import com.owncloud.android.R
 import com.owncloud.android.datamodel.FileDataStorageManager
@@ -250,7 +250,7 @@ class OCFileListDelegate(
         if (shouldHideShare) {
             gridViewHolder.shared.visibility = View.GONE
         } else {
-            showShareIcon(gridViewHolder, file)
+            configureSharedIconView(gridViewHolder, file)
         }
     }
 
@@ -307,9 +307,8 @@ class OCFileListDelegate(
             R.color.bg_default
         }
 
-        gridViewHolder.itemLayout.apply {
-            outlineProvider = createRoundedOutline(context, cornerRadius)
-            clipToOutline = true
+        gridViewHolder.itemLayout.run {
+            makeRounded(context, cornerRadius)
             setBackgroundColor(ContextCompat.getColor(context, itemLayoutBackgroundColorId))
         }
     }
@@ -367,34 +366,38 @@ class OCFileListDelegate(
         }
     }
 
-    private fun showShareIcon(gridViewHolder: ListViewHolder, file: OCFile) {
-        val sharedIconView = gridViewHolder.shared
+    private fun configureSharedIconView(gridViewHolder: ListViewHolder, file: OCFile) {
+        val result = getShareIconIdAndContentDescriptionId(gridViewHolder, file)
 
+        gridViewHolder.shared.run {
+            if (result == null) {
+                visibility = View.GONE
+                return
+            }
+
+            setImageResource(result.first)
+            contentDescription = context.getString(result.second)
+            visibility = View.VISIBLE
+            setOnClickListener { ocFileListFragmentInterface.onShareIconClick(file) }
+        }
+    }
+
+    @Suppress("ReturnCount")
+    private fun getShareIconIdAndContentDescriptionId(holder: ListViewHolder, file: OCFile): Pair<Int, Int>? {
         if (!MDMConfig.sharingSupport(context)) {
-            sharedIconView.visibility = View.GONE
-            return
+            return null
         }
 
-        if (gridViewHolder is OCFileListItemViewHolder || file.unreadCommentsCount == 0) {
-            sharedIconView.visibility = View.VISIBLE
-            if (file.isSharedWithSharee || file.isSharedWithMe) {
-                if (showShareAvatar) {
-                    sharedIconView.visibility = View.GONE
-                } else {
-                    sharedIconView.visibility = View.VISIBLE
-                    sharedIconView.setImageResource(R.drawable.shared_via_users)
-                    sharedIconView.contentDescription = context.getString(R.string.shared_icon_shared)
-                }
-            } else if (file.isSharedViaLink) {
-                sharedIconView.setImageResource(R.drawable.shared_via_link)
-                sharedIconView.contentDescription = context.getString(R.string.shared_icon_shared_via_link)
-            } else {
-                sharedIconView.setImageResource(R.drawable.ic_unshared)
-                sharedIconView.contentDescription = context.getString(R.string.shared_icon_share)
+        if (file.isOfflineOperation) return null
+
+        if (holder !is OCFileListItemViewHolder && file.unreadCommentsCount != 0) return null
+
+        return when {
+            file.isSharedWithSharee || file.isSharedWithMe -> {
+                if (showShareAvatar) null else R.drawable.shared_via_users to R.string.shared_icon_shared
             }
-            sharedIconView.setOnClickListener { ocFileListFragmentInterface.onShareIconClick(file) }
-        } else {
-            sharedIconView.visibility = View.GONE
+            file.isSharedViaLink -> R.drawable.shared_via_link to R.string.shared_icon_shared_via_link
+            else -> R.drawable.ic_unshared to R.string.shared_icon_share
         }
     }
 

+ 1 - 1
app/src/main/java/com/owncloud/android/ui/adapter/ReceiveExternalFilesAdapter.kt

@@ -115,7 +115,7 @@ class ReceiveExternalFilesAdapter(
         val isAutoUploadFolder = SyncedFolderProvider.isAutoUploadFolder(syncedFolderProvider, file, user)
         val isDarkModeActive = syncedFolderProvider.preferences.isDarkModeEnabled
         val overlayIconId = file.getFileOverlayIconId(isAutoUploadFolder)
-        val icon = MimeTypeUtil.getFileIcon(isDarkModeActive, overlayIconId, context, viewThemeUtils)
+        val icon = MimeTypeUtil.getFolderIcon(isDarkModeActive, overlayIconId, context, viewThemeUtils)
         thumbnailImageView.setImageDrawable(icon)
     }
 

+ 1 - 1
app/src/main/java/com/owncloud/android/utils/DisplayUtils.java

@@ -868,7 +868,7 @@ public final class DisplayUtils {
             boolean isDarkModeActive = preferences.isDarkModeEnabled();
 
             Integer overlayIconId = file.getFileOverlayIconId(isAutoUploadFolder);
-            LayerDrawable fileIcon = MimeTypeUtil.getFileIcon(isDarkModeActive, overlayIconId, context, viewThemeUtils);
+            LayerDrawable fileIcon = MimeTypeUtil.getFolderIcon(isDarkModeActive, overlayIconId, context, viewThemeUtils);
             thumbnailView.setImageDrawable(fileIcon);
         } else {
             if (file.getRemoteId() != null && file.isPreviewAvailable()) {

+ 24 - 10
app/src/main/java/com/owncloud/android/utils/DrawableUtil.kt

@@ -7,12 +7,12 @@
  */
 package com.owncloud.android.utils
 
-import android.graphics.Rect
+import android.graphics.drawable.BitmapDrawable
 import android.graphics.drawable.Drawable
 import android.graphics.drawable.LayerDrawable
 import androidx.core.graphics.drawable.DrawableCompat
 
-class DrawableUtil {
+object DrawableUtil {
 
     fun changeColor(source: Drawable, color: Int): Drawable {
         val drawable = DrawableCompat.wrap(source)
@@ -21,13 +21,27 @@ class DrawableUtil {
     }
 
     fun addDrawableAsOverlay(backgroundDrawable: Drawable, overlayDrawable: Drawable): LayerDrawable {
-        val overlayBounds = Rect()
-        val overlayIconSize = backgroundDrawable.intrinsicWidth / 2
-        val topMargin = overlayIconSize.div(2)
-        overlayBounds.set(overlayIconSize, overlayIconSize + topMargin, overlayIconSize, overlayIconSize)
-
-        val layerDrawable = LayerDrawable(arrayOf(backgroundDrawable, overlayDrawable))
-        layerDrawable.setLayerInset(1, overlayBounds.left, overlayBounds.top, overlayBounds.right, overlayBounds.bottom)
-        return layerDrawable
+        val containerDrawable = LayerDrawable(arrayOf(backgroundDrawable, overlayDrawable))
+
+        val overlayWidth = overlayDrawable.intrinsicWidth
+        val overlayHeight = overlayDrawable.intrinsicHeight
+        val backgroundWidth = backgroundDrawable.intrinsicWidth
+        val backgroundHeight = backgroundDrawable.intrinsicHeight
+
+        val scaleFactor = 2f / maxOf(overlayWidth, overlayHeight)
+        val scaledOverlayWidth = (overlayWidth * scaleFactor).toInt()
+        val scaledOverlayHeight = (overlayHeight * scaleFactor).toInt()
+
+        val left = (backgroundWidth - scaledOverlayWidth) / 2
+        val top = (backgroundHeight - scaledOverlayHeight) / 2
+
+        // Icons are centered on the folder icon. However, some icons take up more vertical space,
+        // so adding a top margin to all icons helps center the overlay icon better.
+        val topMargin = 2
+
+        containerDrawable.setLayerInset(1, left, top + topMargin, left, top)
+        (overlayDrawable as? BitmapDrawable)?.setBounds(0, 0, scaledOverlayWidth, scaledOverlayHeight)
+
+        return containerDrawable
     }
 }

+ 49 - 6
app/src/main/java/com/owncloud/android/utils/MimeTypeUtil.java

@@ -11,6 +11,8 @@
 package com.owncloud.android.utils;
 
 import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.LayerDrawable;
 import android.net.Uri;
@@ -19,6 +21,7 @@ import android.webkit.MimeTypeMap;
 import com.nextcloud.android.common.ui.theme.utils.ColorRole;
 import com.owncloud.android.R;
 import com.owncloud.android.datamodel.OCFile;
+import com.owncloud.android.datamodel.ThumbnailsCacheManager;
 import com.owncloud.android.lib.resources.files.model.ServerFileInterface;
 import com.owncloud.android.utils.theme.ViewThemeUtils;
 
@@ -95,9 +98,12 @@ public final class MimeTypeUtil {
         if (context != null) {
             int iconId = MimeTypeUtil.getFileTypeIconId(mimetype, filename);
             Drawable icon = ContextCompat.getDrawable(context, iconId);
+            if (icon == null) {
+                return null;
+            }
 
             if (R.drawable.file_zip == iconId) {
-                viewThemeUtils.platform.tintPrimaryDrawable(context, icon);
+                viewThemeUtils.platform.tintDrawable(context, icon, ColorRole.PRIMARY);
             }
 
             return icon;
@@ -141,6 +147,45 @@ public final class MimeTypeUtil {
         return determineIconIdByMimeTypeList(possibleMimeTypes);
     }
 
+    /**
+     * Returns a drawable representing a file or folder.
+     * <p>
+     *
+     * - For folders: Returns a folder icon. If an overlay is needed, it includes an overlay icon on the folder.
+     *
+     * <p>
+     * - For files: Returns the file's thumbnail if it exists. Otherwise, it provides a thumbnail based on the file's MIME type.
+     *
+     * @return A drawable for the file or folder.
+     */
+    public static Drawable getOCFileIcon(OCFile file, Context context, ViewThemeUtils viewThemeUtils, boolean isAutoUpload, boolean isDarkModeActive) {
+        if (file.isFolder()) {
+            Integer overlayIconId = file.getFileOverlayIconId(isAutoUpload);
+            return MimeTypeUtil.getFolderIcon(isDarkModeActive, overlayIconId, context, viewThemeUtils);
+        }
+
+        if (!(MimeTypeUtil.isImage(file) || MimeTypeUtil.isVideo(file)) || file.getRemoteId() == null) {
+            return MimeTypeUtil.getFileTypeIcon(file.getMimeType(), file.getFileName(), context, viewThemeUtils);
+        }
+
+        Bitmap thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache(ThumbnailsCacheManager.PREFIX_THUMBNAIL + file.getRemoteId());
+        if (thumbnail == null || file.isUpdateThumbnailNeeded()) {
+            return MimeTypeUtil.getFileTypeIcon(file.getMimeType(), file.getFileName(), context, viewThemeUtils);
+        }
+
+        Drawable background = new BitmapDrawable(context.getResources(), thumbnail);
+        if (!MimeTypeUtil.isVideo(file)) {
+            return background;
+        }
+
+        Drawable videoOverlay = ContextCompat.getDrawable(context, R.drawable.video_white);
+        if (videoOverlay == null) {
+            return background;
+        }
+
+        return DrawableUtil.INSTANCE.addDrawableAsOverlay(background, videoOverlay);
+    }
+
     public static Drawable getDefaultFolderIcon(Context context, ViewThemeUtils viewThemeUtils) {
         Drawable drawable = ContextCompat.getDrawable(context, R.drawable.folder);
         assert(drawable != null);
@@ -149,7 +194,7 @@ public final class MimeTypeUtil {
         return drawable;
     }
 
-    public static LayerDrawable getFileIcon(Boolean isDarkModeActive, Integer overlayIconId, Context context, ViewThemeUtils viewThemeUtils) {
+    public static LayerDrawable getFolderIcon(boolean isDarkModeActive, Integer overlayIconId, Context context, ViewThemeUtils viewThemeUtils) {
         Drawable folderDrawable = getDefaultFolderIcon(context, viewThemeUtils);
         assert(folderDrawable != null);
 
@@ -159,16 +204,14 @@ public final class MimeTypeUtil {
             return folderLayerDrawable;
         }
 
-        DrawableUtil drawableUtil = new DrawableUtil();
-
         Drawable overlayDrawable = ContextCompat.getDrawable(context, overlayIconId);
         assert(overlayDrawable != null);
 
         if (isDarkModeActive) {
-            overlayDrawable = drawableUtil.changeColor(overlayDrawable, R.color.dark);
+            overlayDrawable = DrawableUtil.INSTANCE.changeColor(overlayDrawable, R.color.dark);
         }
 
-        return drawableUtil.addDrawableAsOverlay(folderDrawable, overlayDrawable);
+        return DrawableUtil.INSTANCE.addDrawableAsOverlay(folderDrawable, overlayDrawable);
     }
 
     /**

+ 16 - 0
app/src/main/res/drawable/ic_folder_offline.xml

@@ -0,0 +1,16 @@
+<!--
+  ~ Nextcloud - Android Client
+  ~
+  ~ SPDX-FileCopyrightText: 2018-2024 Google LLC
+  ~ SPDX-License-Identifier: Apache-2.0
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="@color/grey_600"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0">
+    <path
+        android:fillColor="@color/grey_600"
+        android:pathData="M10,4H4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V8c0,-1.1 -0.9,-2 -2,-2h-8l-2,-2z" />
+</vector>