tobiasKaminsky 3 жил өмнө
parent
commit
97d71d0383
29 өөрчлөгдсөн 1636 нэмэгдсэн , 775 устгасан
  1. 1 0
      app/src/main/java/com/owncloud/android/MainApp.java
  2. 25 0
      app/src/main/java/com/owncloud/android/datamodel/GalleryItems.kt
  3. 52 0
      app/src/main/java/com/owncloud/android/ui/adapter/CommonOCFileListAdapterInterface.kt
  4. 55 0
      app/src/main/java/com/owncloud/android/ui/adapter/FilesFilter.kt
  5. 248 0
      app/src/main/java/com/owncloud/android/ui/adapter/GalleryAdapter.kt
  6. 28 0
      app/src/main/java/com/owncloud/android/ui/adapter/GalleryHeaderViewHolder.kt
  7. 56 0
      app/src/main/java/com/owncloud/android/ui/adapter/GalleryItemViewHolder.kt
  8. 37 0
      app/src/main/java/com/owncloud/android/ui/adapter/ListGridImageViewHolder.kt
  9. 28 0
      app/src/main/java/com/owncloud/android/ui/adapter/ListGridItemViewHolder.kt
  10. 35 0
      app/src/main/java/com/owncloud/android/ui/adapter/ListItemViewHolder.kt
  11. 175 714
      app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java
  12. 230 0
      app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt
  13. 35 0
      app/src/main/java/com/owncloud/android/ui/adapter/OCFileListFooterViewHolder.kt
  14. 53 0
      app/src/main/java/com/owncloud/android/ui/adapter/OCFileListGridImageViewHolder.kt
  15. 56 0
      app/src/main/java/com/owncloud/android/ui/adapter/OCFileListGridItemViewHolder.kt
  16. 35 0
      app/src/main/java/com/owncloud/android/ui/adapter/OCFileListHeaderViewHolder.kt
  17. 67 0
      app/src/main/java/com/owncloud/android/ui/adapter/OCFileListItemViewHolder.kt
  18. 1 1
      app/src/main/java/com/owncloud/android/ui/asynctasks/GallerySearchTask.java
  19. 9 10
      app/src/main/java/com/owncloud/android/ui/dialog/ConflictsResolveDialog.java
  20. 41 7
      app/src/main/java/com/owncloud/android/ui/fragment/GalleryFragment.java
  21. 51 35
      app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java
  22. 2 2
      app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchAsyncTask.kt
  23. 1 1
      app/src/main/java/com/owncloud/android/ui/fragment/SharedListFragment.kt
  24. 0 4
      app/src/main/java/com/owncloud/android/ui/interfaces/OCFileListFragmentInterface.java
  25. 3 0
      app/src/main/java/com/owncloud/android/utils/DataHolderUtil.java
  26. 187 1
      app/src/main/java/com/owncloud/android/utils/DisplayUtils.java
  27. 48 0
      app/src/main/res/layout/gallery_header.xml
  28. 76 0
      app/src/test/java/com/owncloud/android/ui/adapter/GalleryAdapterTest.kt
  29. 1 0
      spotbugs-filter.xml

+ 1 - 0
app/src/main/java/com/owncloud/android/MainApp.java

@@ -231,6 +231,7 @@ public class MainApp extends MultiDexApplication implements HasAndroidInjector {
         // we don't want to handle crashes occurring inside crash reporter activity/process;
         // let the platform deal with those
         final boolean isCrashReportingProcess = getAppProcessName().endsWith(":crash");
+        final boolean useExceptionHandler = !appInfo.isDebugBuild();
 
         if (!isCrashReportingProcess && !appInfo.isDebugBuild()) {
             Thread.UncaughtExceptionHandler defaultPlatformHandler = Thread.getDefaultUncaughtExceptionHandler();

+ 25 - 0
app/src/main/java/com/owncloud/android/datamodel/GalleryItems.kt

@@ -0,0 +1,25 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2022 Tobias Kaminsky
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.datamodel
+
+data class GalleryItems(val date: Long, val files: List<OCFile>)

+ 52 - 0
app/src/main/java/com/owncloud/android/ui/adapter/CommonOCFileListAdapterInterface.kt

@@ -0,0 +1,52 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2022 Tobias Kaminsky
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.ui.adapter
+
+import com.nextcloud.client.account.User
+import com.owncloud.android.datamodel.FileDataStorageManager
+import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.utils.FileSortOrder
+
+interface CommonOCFileListAdapterInterface {
+    fun isMultiSelect(): Boolean
+    fun cancelAllPendingTasks()
+    fun getItemPosition(file: OCFile): Int
+    fun swapDirectory(
+        user: User,
+        directory: OCFile,
+        storageManager: FileDataStorageManager,
+        onlyOnDevice: Boolean,
+        mLimitToMimeType: String
+    )
+
+    fun setHighlightedItem(file: OCFile)
+    fun setSortOrder(mFile: OCFile, sortOrder: FileSortOrder)
+    fun addCheckedFile(file: OCFile)
+    fun isCheckedFile(file: OCFile): Boolean
+    fun getCheckedItems(): Set<OCFile>
+    fun removeCheckedFile(file: OCFile)
+    fun notifyItemChanged(file: OCFile)
+    fun getFilesCount(): Int
+    fun setMultiSelect(boolean: Boolean)
+    fun clearCheckedItems()
+}

+ 55 - 0
app/src/main/java/com/owncloud/android/ui/adapter/FilesFilter.kt

@@ -0,0 +1,55 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2022 Tobias Kaminsky
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package com.owncloud.android.ui.adapter
+
+import android.text.TextUtils
+import android.widget.Filter
+import com.owncloud.android.datamodel.OCFile
+import java.util.Locale
+import java.util.Vector
+
+internal class FilesFilter(private val ocFileListAdapter: OCFileListAdapter) : Filter() {
+    override fun performFiltering(constraint: CharSequence): FilterResults {
+        val results = FilterResults()
+        val filteredFiles = Vector<OCFile>()
+        if (!TextUtils.isEmpty(constraint)) {
+            for (file in ocFileListAdapter.allFiles) {
+                if (file.parentRemotePath == ocFileListAdapter.currentDirectory.remotePath &&
+                    file.fileName.lowercase(Locale.getDefault()).contains(
+                        constraint.toString().lowercase(Locale.getDefault())
+                    ) &&
+                    !filteredFiles.contains(file)
+                ) {
+                    filteredFiles.add(file)
+                }
+            }
+        }
+        results.values = filteredFiles
+        results.count = filteredFiles.size
+        return results
+    }
+
+    override fun publishResults(constraint: CharSequence, results: FilterResults) {
+        val ocFiles = results.values as Vector<OCFile>
+        ocFileListAdapter.updateFilteredResults(ocFiles)
+    }
+}

+ 248 - 0
app/src/main/java/com/owncloud/android/ui/adapter/GalleryAdapter.kt

@@ -0,0 +1,248 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2022 Tobias Kaminsky
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.ui.adapter
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.os.Handler
+import android.os.Looper
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.annotation.VisibleForTesting
+import com.afollestad.sectionedrecyclerview.SectionedRecyclerViewAdapter
+import com.afollestad.sectionedrecyclerview.SectionedViewHolder
+import com.nextcloud.client.account.User
+import com.nextcloud.client.preferences.AppPreferences
+import com.owncloud.android.databinding.GalleryHeaderBinding
+import com.owncloud.android.databinding.GridImageBinding
+import com.owncloud.android.datamodel.FileDataStorageManager
+import com.owncloud.android.datamodel.GalleryItems
+import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.ui.activity.ComponentsGetter
+import com.owncloud.android.ui.interfaces.OCFileListFragmentInterface
+import com.owncloud.android.utils.DisplayUtils
+import com.owncloud.android.utils.FileSortOrder
+import com.owncloud.android.utils.FileStorageUtils
+import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView.SectionedAdapter
+import java.util.Calendar
+import java.util.Date
+
+class GalleryAdapter(
+    val context: Context,
+    user: User,
+    ocFileListFragmentInterface: OCFileListFragmentInterface,
+    preferences: AppPreferences,
+    transferServiceGetter: ComponentsGetter
+) : SectionedRecyclerViewAdapter<SectionedViewHolder>(), CommonOCFileListAdapterInterface, SectionedAdapter {
+    private var files: List<GalleryItems> = mutableListOf()
+    private var ocFileListDelegate: OCFileListDelegate
+    private var storageManager: FileDataStorageManager
+
+    init {
+        shouldShowFooters(false)
+        storageManager = transferServiceGetter.storageManager
+
+        ocFileListDelegate = OCFileListDelegate(
+            context,
+            ocFileListFragmentInterface,
+            user,
+            storageManager,
+            false,
+            preferences,
+            true,
+            transferServiceGetter,
+            showMetadata = false,
+            showShareAvatar = false
+        )
+    }
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SectionedViewHolder {
+        return if (viewType == VIEW_TYPE_HEADER) {
+            GalleryHeaderViewHolder(
+                GalleryHeaderBinding.inflate(
+                    LayoutInflater.from(parent.context),
+                    parent,
+                    false
+                )
+            )
+        } else {
+            GalleryItemViewHolder(
+                GridImageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+            )
+        }
+    }
+
+    override fun onBindViewHolder(
+        holder: SectionedViewHolder?,
+        section: Int,
+        relativePosition: Int,
+        absolutePosition: Int
+    ) {
+        if (holder != null) {
+            val itemViewHolder = holder as GalleryItemViewHolder
+            val ocFile = files[section].files[relativePosition]
+
+            ocFileListDelegate.bindGridViewHolder(itemViewHolder, ocFile)
+        }
+    }
+
+    override fun getItemCount(section: Int): Int {
+        return files[section].files.size
+    }
+
+    override fun getSectionCount(): Int {
+        return files.size
+    }
+
+    override fun getSectionName(position: Int): String {
+        return DisplayUtils.getDateByPattern(
+            files[getRelativePosition(position).section()].date,
+            context,
+            DisplayUtils.MONTH_YEAR_PATTERN
+        )
+    }
+
+    override fun onBindHeaderViewHolder(holder: SectionedViewHolder?, section: Int, expanded: Boolean) {
+        if (holder != null) {
+            val headerViewHolder = holder as GalleryHeaderViewHolder
+            val galleryItem = files[section]
+
+            headerViewHolder.binding.month.text = DisplayUtils.getDateByPattern(
+                galleryItem.date,
+                context,
+                DisplayUtils.MONTH_PATTERN
+            )
+            headerViewHolder.binding.year.text = DisplayUtils.getDateByPattern(
+                galleryItem.date,
+                context, DisplayUtils.YEAR_PATTERN
+            )
+        }
+    }
+
+    override fun onBindFooterViewHolder(holder: SectionedViewHolder?, section: Int) {
+        TODO("Not yet implemented")
+    }
+
+    @SuppressLint("NotifyDataSetChanged")
+    fun showAllGalleryItems(storageManager: FileDataStorageManager) {
+        val items = storageManager.allGalleryItems
+
+        files = items
+            .groupBy { firstOfMonth(it.modificationTimestamp) }
+            .map { GalleryItems(it.key, FileStorageUtils.sortOcFolderDescDateModifiedWithoutFavoritesFirst(it.value)) }
+            .sortedBy { it.date }.reversed()
+
+        Handler(Looper.getMainLooper()).post { notifyDataSetChanged() }
+    }
+
+    private fun firstOfMonth(timestamp: Long): Long {
+        val cal = Calendar.getInstance()
+        cal.time = Date(timestamp)
+        cal.set(Calendar.DAY_OF_MONTH, cal.getActualMinimum(Calendar.DAY_OF_MONTH))
+        cal.set(Calendar.HOUR_OF_DAY, 0)
+        cal.set(Calendar.MINUTE, 0)
+        cal.set(Calendar.SECOND, 0)
+
+        return cal.timeInMillis
+    }
+
+    fun isEmpty(): Boolean {
+        return files.isEmpty()
+    }
+
+    fun getItem(position: Int): OCFile {
+        val itemCoord = getRelativePosition(position)
+
+        return files[itemCoord.section()].files[itemCoord.relativePos()]
+    }
+
+    override fun isMultiSelect(): Boolean {
+        return ocFileListDelegate.isMultiSelect
+    }
+
+    override fun cancelAllPendingTasks() {
+        ocFileListDelegate.cancelAllPendingTasks()
+    }
+
+    override fun getItemPosition(file: OCFile): Int {
+        val item = files.find { it.files.contains(file) }
+        return getAbsolutePosition(files.indexOf(item), item?.files?.indexOf(file) ?: 0)
+    }
+
+    override fun swapDirectory(
+        user: User,
+        directory: OCFile,
+        storageManager: FileDataStorageManager,
+        onlyOnDevice: Boolean,
+        mLimitToMimeType: String
+    ) {
+        TODO("Not yet implemented")
+    }
+
+    override fun setHighlightedItem(file: OCFile) {
+        TODO("Not yet implemented")
+    }
+
+    override fun setSortOrder(mFile: OCFile, sortOrder: FileSortOrder) {
+        TODO("Not yet implemented")
+    }
+
+    override fun addCheckedFile(file: OCFile) {
+        ocFileListDelegate.addCheckedFile(file)
+    }
+
+    override fun isCheckedFile(file: OCFile): Boolean {
+        return ocFileListDelegate.isCheckedFile(file)
+    }
+
+    override fun getCheckedItems(): Set<OCFile> {
+        return ocFileListDelegate.checkedItems
+    }
+
+    override fun removeCheckedFile(file: OCFile) {
+        ocFileListDelegate.removeCheckedFile(file)
+    }
+
+    override fun notifyItemChanged(file: OCFile) {
+        notifyItemChanged(getItemPosition(file))
+    }
+
+    override fun getFilesCount(): Int {
+        return files.fold(0) { acc, item -> acc + item.files.size }
+    }
+
+    @SuppressLint("NotifyDataSetChanged")
+    override fun setMultiSelect(boolean: Boolean) {
+        ocFileListDelegate.isMultiSelect = boolean
+        notifyDataSetChanged()
+    }
+
+    override fun clearCheckedItems() {
+        ocFileListDelegate.clearCheckedItems()
+    }
+
+    @VisibleForTesting
+    fun addFiles(items: List<GalleryItems>) {
+        files = items
+    }
+}

+ 28 - 0
app/src/main/java/com/owncloud/android/ui/adapter/GalleryHeaderViewHolder.kt

@@ -0,0 +1,28 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2022 Tobias Kaminsky
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.ui.adapter
+
+import com.afollestad.sectionedrecyclerview.SectionedViewHolder
+import com.owncloud.android.databinding.GalleryHeaderBinding
+
+class GalleryHeaderViewHolder(val binding: GalleryHeaderBinding) : SectionedViewHolder(binding.root)

+ 56 - 0
app/src/main/java/com/owncloud/android/ui/adapter/GalleryItemViewHolder.kt

@@ -0,0 +1,56 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2022 Tobias Kaminsky
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.ui.adapter
+
+import android.view.View
+import android.widget.ImageView
+import com.afollestad.sectionedrecyclerview.SectionedViewHolder
+import com.elyeproj.loaderviewlibrary.LoaderImageView
+import com.owncloud.android.databinding.GridImageBinding
+
+class GalleryItemViewHolder(val binding: GridImageBinding) :
+    SectionedViewHolder(binding.root), ListGridImageViewHolder {
+    override val thumbnail: ImageView
+        get() = binding.thumbnail
+
+    override val shimmerThumbnail: LoaderImageView
+        get() = binding.thumbnailShimmer
+
+    override val favorite: ImageView
+        get() = binding.favoriteAction
+
+    override val localFileIndicator: ImageView
+        get() = binding.localFileIndicator
+
+    override val shared: ImageView
+        get() = binding.sharedIcon
+
+    override val checkbox: ImageView
+        get() = binding.customCheckbox
+
+    override val itemLayout: View
+        get() = binding.ListItemLayout
+
+    override val unreadComments: ImageView
+        get() = binding.unreadComments
+}

+ 37 - 0
app/src/main/java/com/owncloud/android/ui/adapter/ListGridImageViewHolder.kt

@@ -0,0 +1,37 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2022 Tobias Kaminsky
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package com.owncloud.android.ui.adapter
+
+import android.view.View
+import android.widget.ImageView
+import com.elyeproj.loaderviewlibrary.LoaderImageView
+
+interface ListGridImageViewHolder {
+    val thumbnail: ImageView
+    val shimmerThumbnail: LoaderImageView
+    val favorite: ImageView
+    val localFileIndicator: ImageView
+    val shared: ImageView
+    val checkbox: ImageView
+    val itemLayout: View
+    val unreadComments: ImageView
+}

+ 28 - 0
app/src/main/java/com/owncloud/android/ui/adapter/ListGridItemViewHolder.kt

@@ -0,0 +1,28 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2022 Tobias Kaminsky
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package com.owncloud.android.ui.adapter
+
+import android.widget.TextView
+
+internal interface ListGridItemViewHolder : ListGridImageViewHolder {
+    val fileName: TextView
+}

+ 35 - 0
app/src/main/java/com/owncloud/android/ui/adapter/ListItemViewHolder.kt

@@ -0,0 +1,35 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2022 Tobias Kaminsky
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package com.owncloud.android.ui.adapter
+
+import android.view.View
+import android.widget.ImageView
+import android.widget.TextView
+import com.owncloud.android.ui.AvatarGroupLayout
+
+internal interface ListItemViewHolder : ListGridItemViewHolder {
+    val fileSize: TextView
+    val fileSizeSeparator: View
+    val lastModification: TextView
+    val overflowMenu: ImageView
+    val sharedAvatars: AvatarGroupLayout
+}

+ 175 - 714
app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java

@@ -25,12 +25,10 @@
 package com.owncloud.android.ui.adapter;
 
 import android.accounts.AccountManager;
+import android.annotation.SuppressLint;
 import android.app.Activity;
 import android.content.ContentValues;
-import android.content.Context;
 import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.Point;
 import android.graphics.PorterDuff;
 import android.graphics.drawable.Drawable;
 import android.os.Handler;
@@ -39,11 +37,8 @@ import android.text.TextUtils;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
-import android.view.WindowManager;
 import android.widget.Filter;
-import android.widget.FrameLayout;
 import android.widget.ImageView;
-import android.widget.TextView;
 
 import com.elyeproj.loaderviewlibrary.LoaderImageView;
 import com.nextcloud.client.account.User;
@@ -60,8 +55,6 @@ import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.datamodel.ThumbnailsCacheManager;
 import com.owncloud.android.datamodel.VirtualFolderType;
 import com.owncloud.android.db.ProviderMeta;
-import com.owncloud.android.files.services.FileDownloader;
-import com.owncloud.android.files.services.FileUploader;
 import com.owncloud.android.lib.common.operations.RemoteOperation;
 import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.lib.resources.files.model.RemoteFile;
@@ -70,20 +63,16 @@ import com.owncloud.android.lib.resources.shares.ShareType;
 import com.owncloud.android.lib.resources.shares.ShareeUser;
 import com.owncloud.android.operations.RefreshFolderOperation;
 import com.owncloud.android.operations.RemoteOperationFailedException;
-import com.owncloud.android.services.OperationsService;
-import com.owncloud.android.ui.AvatarGroupLayout;
 import com.owncloud.android.ui.activity.ComponentsGetter;
 import com.owncloud.android.ui.fragment.SearchType;
 import com.owncloud.android.ui.interfaces.OCFileListFragmentInterface;
 import com.owncloud.android.ui.preview.PreviewTextFragment;
-import com.owncloud.android.utils.BitmapUtils;
 import com.owncloud.android.utils.DisplayUtils;
 import com.owncloud.android.utils.FileSortOrder;
 import com.owncloud.android.utils.FileStorageUtils;
 import com.owncloud.android.utils.MimeTypeUtil;
 import com.owncloud.android.utils.theme.CapabilityUtils;
 import com.owncloud.android.utils.theme.ThemeColorUtils;
-import com.owncloud.android.utils.theme.ThemeDrawableUtils;
 import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView;
 
 import java.io.File;
@@ -91,7 +80,6 @@ import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Date;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Set;
@@ -100,17 +88,18 @@ import java.util.Vector;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
-import androidx.core.content.res.ResourcesCompat;
 import androidx.recyclerview.widget.RecyclerView;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 
 /**
  * This Adapter populates a RecyclerView with all files and folders in a Nextcloud instance.
  */
 public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
-    implements DisplayUtils.AvatarGenerationListener, FastScrollRecyclerView.SectionedAdapter {
+    implements DisplayUtils.AvatarGenerationListener,
+    CommonOCFileListAdapterInterface,
+    FastScrollRecyclerView.SectionedAdapter {
 
     private static final int showFilenameColumnThreshold = 4;
-    private final ComponentsGetter transferServiceGetter;
     private final String userId;
     private Activity activity;
     private AppPreferences preferences;
@@ -119,8 +108,6 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
     private boolean hideItemOptions;
     private long lastTimestamp;
     private boolean gridView;
-    private boolean multiSelect;
-    private Set<OCFile> checkedFiles;
 
     private FileDataStorageManager mStorageManager;
     private User user;
@@ -135,18 +122,15 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
     private static final int VIEWTYPE_IMAGE = 2;
     private static final int VIEWTYPE_HEADER = 3;
 
-    private List<ThumbnailsCacheManager.ThumbnailGenerationTask> asyncTasks = new ArrayList<>();
     private boolean onlyOnDevice;
-    private boolean showShareAvatar = false;
-    private OCFile highlightedItem;
-    private boolean showMetadata = true;
+    private final OCFileListDelegate ocFileListDelegate;
     private FileSortOrder sortOrder;
 
     private final SimpleDateFormat dateFormat = new SimpleDateFormat("MMMM yyyy", Locale.getDefault());
 
     public OCFileListAdapter(
         Activity activity,
-        User user,
+        @NonNull User user,
         AppPreferences preferences,
         ComponentsGetter transferServiceGetter,
         OCFileListFragmentInterface ocFileListFragmentInterface,
@@ -159,53 +143,54 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
         this.user = user;
         hideItemOptions = argHideItemOptions;
         this.gridView = gridView;
-        checkedFiles = new HashSet<>();
+        mStorageManager = transferServiceGetter.getStorageManager();
 
-        this.transferServiceGetter = transferServiceGetter;
-
-        if (this.user != null) {
-            AccountManager platformAccountManager = AccountManager.get(this.activity);
-            userId = platformAccountManager.getUserData(this.user.toPlatformAccount(),
-                                                        com.owncloud.android.lib.common.accounts.AccountUtils.Constants.KEY_USER_ID);
-        } else {
-            userId = "";
+        if (mStorageManager == null) {
+            mStorageManager = new FileDataStorageManager(user, activity.getContentResolver());
         }
 
+        userId = AccountManager
+            .get(activity)
+            .getUserData(this.user.toPlatformAccount(),
+                         com.owncloud.android.lib.common.accounts.AccountUtils.Constants.KEY_USER_ID);
+
+        ocFileListDelegate = new OCFileListDelegate(activity,
+                                                    ocFileListFragmentInterface,
+                                                    user,
+                                                    mStorageManager,
+                                                    hideItemOptions,
+                                                    preferences,
+                                                    gridView,
+                                                    transferServiceGetter,
+                                                    true,
+                                                    CapabilityUtils
+                                                        .getCapability(activity)
+                                                        .getVersion()
+                                                        .isShareesOnDavSupported());
+
         // initialise thumbnails cache on background thread
         new ThumbnailsCacheManager.InitDiskCacheTask().execute();
     }
 
     public boolean isMultiSelect() {
-        return multiSelect;
+        return ocFileListDelegate.isMultiSelect();
     }
 
+    @SuppressLint("NotifyDataSetChanged")
     public void setMultiSelect(boolean bool) {
-        multiSelect = bool;
+        ocFileListDelegate.setMultiSelect(bool);
         notifyDataSetChanged();
     }
 
-    public boolean isCheckedFile(OCFile file) {
-        return checkedFiles.contains(file);
-    }
-
-    public void removeCheckedFile(OCFile file) {
-        checkedFiles.remove(file);
-    }
-
-    public void addCheckedFile(OCFile file) {
-        checkedFiles.add(file);
-        highlightedItem = null;
+    public void removeCheckedFile(@NonNull OCFile file) {
+        ocFileListDelegate.removeCheckedFile(file);
     }
 
     public void addAllFilesToCheckedFiles() {
-        checkedFiles.addAll(mFiles);
+        ocFileListDelegate.addToCheckedFiles(mFiles);
     }
 
-    public void removeAllFilesFromCheckedFiles() {
-        checkedFiles.clear();
-    }
-
-    public int getItemPosition(OCFile file) {
+    public int getItemPosition(@NonNull OCFile file) {
         int position = mFiles.indexOf(file);
 
         if (shouldShowHeader()) {
@@ -357,23 +342,23 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
     }
 
     @Override
+    @SuppressFBWarnings("ITC_INHERITANCE_TYPE_CHECKING")
     public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
         if (holder instanceof OCFileListFooterViewHolder) {
             OCFileListFooterViewHolder footerViewHolder = (OCFileListFooterViewHolder) holder;
-            footerViewHolder.binding.footerText.setText(getFooterText());
-            footerViewHolder.binding.loadingProgressBar.getIndeterminateDrawable()
+            footerViewHolder.getFooterText().setText(getFooterText());
+            footerViewHolder.getLoadingProgressBar().getIndeterminateDrawable()
                 .setColorFilter(ThemeColorUtils.primaryColor(activity), PorterDuff.Mode.SRC_IN);
-            footerViewHolder.binding.loadingProgressBar.setVisibility(
+            footerViewHolder.getLoadingProgressBar().setVisibility(
                 ocFileListFragmentInterface.isLoading() ? View.VISIBLE : View.GONE);
         } else if (holder instanceof OCFileListHeaderViewHolder) {
             OCFileListHeaderViewHolder headerViewHolder = (OCFileListHeaderViewHolder) holder;
             String text = currentDirectory.getRichWorkspace();
 
-            PreviewTextFragment.setText(headerViewHolder.binding.headerText, text, null, activity, true, true);
-            headerViewHolder.binding.headerView.setOnClickListener(v -> ocFileListFragmentInterface.onHeaderClicked());
+            PreviewTextFragment.setText(headerViewHolder.getHeaderText(), text, null, activity, true, true);
+            headerViewHolder.getHeaderView().setOnClickListener(v -> ocFileListFragmentInterface.onHeaderClicked());
         } else {
             ListGridImageViewHolder gridViewHolder = (ListGridImageViewHolder) holder;
-
             OCFile file = getItem(position);
 
             if (file == null) {
@@ -381,311 +366,113 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
                 return;
             }
 
-            boolean gridImage = MimeTypeUtil.isImage(file) || MimeTypeUtil.isVideo(file);
-
-            gridViewHolder.getThumbnail().setTag(file.getFileId());
-            setThumbnail(file,
-                         gridViewHolder.getThumbnail(),
-                         user,
-                         mStorageManager,
-                         asyncTasks,
-                         gridView,
-                         activity,
-                         gridViewHolder.getShimmerThumbnail(), preferences);
-
-            if (highlightedItem != null && file.getFileId() == highlightedItem.getFileId()) {
-                gridViewHolder.getItemLayout().setBackgroundColor(activity.getResources()
-                                                                      .getColor(R.color.selected_item_background));
-            } else if (isCheckedFile(file)) {
-                gridViewHolder.getItemLayout().setBackgroundColor(activity.getResources()
-                                                                      .getColor(R.color.selected_item_background));
-                gridViewHolder.getCheckbox().setImageDrawable(
-                    ThemeDrawableUtils.tintDrawable(R.drawable.ic_checkbox_marked,
-                                                    ThemeColorUtils.primaryColor(activity)));
-            } else {
-                gridViewHolder.getItemLayout().setBackgroundColor(activity.getResources().getColor(R.color.bg_default));
-                gridViewHolder.getCheckbox().setImageResource(R.drawable.ic_checkbox_blank_outline);
-            }
+            ocFileListDelegate.bindGridViewHolder(gridViewHolder, file);
 
-            gridViewHolder.getItemLayout().setOnClickListener(v -> ocFileListFragmentInterface.onItemClicked(file));
-
-            if (!hideItemOptions) {
-                gridViewHolder.getItemLayout().setLongClickable(true);
-                gridViewHolder.getItemLayout().setOnLongClickListener(v ->
-                                                                          ocFileListFragmentInterface.onLongItemClicked(file));
+            if (holder instanceof ListItemViewHolder) {
+                bindListItemViewHolder((ListItemViewHolder) gridViewHolder, file);
             }
 
-            // unread comments
-            if (file.getUnreadCommentsCount() > 0) {
-                gridViewHolder.getUnreadComments().setVisibility(View.VISIBLE);
-                gridViewHolder.getUnreadComments().setOnClickListener(view -> ocFileListFragmentInterface
-                    .showActivityDetailView(file));
-            } else {
-                gridViewHolder.getUnreadComments().setVisibility(View.GONE);
+            if (holder instanceof ListGridItemViewHolder) {
+                bindListGridItemViewHolder((ListGridItemViewHolder) holder, file);
             }
+        }
+    }
 
-            if (holder instanceof ListItemViewHolder) {
-                ListItemViewHolder itemViewHolder = (ListItemViewHolder) holder;
-
-                if ((file.isSharedWithMe() || file.isSharedWithSharee()) && !multiSelect && !gridView &&
-                    !hideItemOptions) {
-                    itemViewHolder.getSharedAvatars().setVisibility(View.VISIBLE);
-                    itemViewHolder.getSharedAvatars().removeAllViews();
-
-                    String fileOwner = file.getOwnerId();
-                    List<ShareeUser> sharees = file.getSharees();
-
-                    // use fileOwner if not oneself, then add at first
-                    ShareeUser fileOwnerSharee = new ShareeUser(fileOwner, file.getOwnerDisplayName(), ShareType.USER);
-                    if (!TextUtils.isEmpty(fileOwner) &&
-                        !fileOwner.equals(userId) &&
-                        !sharees.contains(fileOwnerSharee)) {
-                        sharees.add(fileOwnerSharee);
-                    }
-
-                    Collections.reverse(sharees);
-
-                    Log_OC.d(this, "sharees of " + file.getFileName() + ": " + sharees);
-
-                    itemViewHolder.getSharedAvatars().setAvatars(user, sharees);
-                    itemViewHolder.getSharedAvatars().setOnClickListener(
-                        view -> ocFileListFragmentInterface.onShareIconClick(file));
-                } else {
-                    itemViewHolder.getSharedAvatars().setVisibility(View.GONE);
-                    itemViewHolder.getSharedAvatars().removeAllViews();
-                }
-
-                // npe fix: looks like file without local storage path somehow get here
-                final String storagePath = file.getStoragePath();
-                if (onlyOnDevice && storagePath != null) {
-                    File localFile = new File(storagePath);
-                    long localSize;
-                    if (localFile.isDirectory()) {
-                        localSize = FileStorageUtils.getFolderSize(localFile);
-                    } else {
-                        localSize = localFile.length();
-                    }
+    private void bindListItemViewHolder(ListItemViewHolder holder, OCFile file) {
+        if ((file.isSharedWithMe() || file.isSharedWithSharee()) && !isMultiSelect() && !gridView &&
+            !hideItemOptions) {
+            holder.getSharedAvatars().setVisibility(View.VISIBLE);
+            holder.getSharedAvatars().removeAllViews();
 
-                    itemViewHolder.getFileSize().setText(DisplayUtils.bytesToHumanReadable(localSize));
-                    itemViewHolder.getFileSize().setVisibility(View.VISIBLE);
-                    itemViewHolder.getFileSizeSeparator().setVisibility(View.VISIBLE);
-                } else {
-                    final long fileLength = file.getFileLength();
-                    if (fileLength >= 0) {
-                        itemViewHolder.getFileSize().setText(DisplayUtils.bytesToHumanReadable(fileLength));
-                        itemViewHolder.getFileSize().setVisibility(View.VISIBLE);
-                        itemViewHolder.getFileSizeSeparator().setVisibility(View.VISIBLE);
-                    } else {
-                        itemViewHolder.getFileSize().setVisibility(View.GONE);
-                        itemViewHolder.getFileSizeSeparator().setVisibility(View.GONE);
-                    }
-                }
+            String fileOwner = file.getOwnerId();
+            List<ShareeUser> sharees = file.getSharees();
 
-                final long modificationTimestamp = file.getModificationTimestamp();
-                if (modificationTimestamp > 0) {
-                    itemViewHolder.getLastModification().setText(DisplayUtils.getRelativeTimestamp(activity,
-                                                                                                   modificationTimestamp));
-                    itemViewHolder.getLastModification().setVisibility(View.VISIBLE);
-                } else if (file.getFirstShareTimestamp() > 0) {
-                    itemViewHolder.getLastModification().setText(
-                        DisplayUtils.getRelativeTimestamp(activity, file.getFirstShareTimestamp())
-                                                                );
-                    itemViewHolder.getLastModification().setVisibility(View.VISIBLE);
-                } else {
-                    itemViewHolder.getLastModification().setVisibility(View.GONE);
-                }
+            // use fileOwner if not oneself, then add at first
+            ShareeUser fileOwnerSharee = new ShareeUser(fileOwner, file.getOwnerDisplayName(), ShareType.USER);
+            if (!TextUtils.isEmpty(fileOwner) &&
+                !fileOwner.equals(userId) &&
+                !sharees.contains(fileOwnerSharee)) {
+                sharees.add(fileOwnerSharee);
+            }
 
+            Collections.reverse(sharees);
 
-                if (multiSelect || gridView || hideItemOptions) {
-                    itemViewHolder.getOverflowMenu().setVisibility(View.GONE);
-                } else {
-                    itemViewHolder.getOverflowMenu().setVisibility(View.VISIBLE);
-                    itemViewHolder.getOverflowMenu().setOnClickListener(view -> ocFileListFragmentInterface
-                        .onOverflowIconClicked(file, view));
-                }
-            }
+            Log_OC.d(this, "sharees of " + file.getFileName() + ": " + sharees);
 
-            gridViewHolder.getLocalFileIndicator().setVisibility(View.INVISIBLE);   // default first
-
-            if (showMetadata) {
-                OperationsService.OperationsServiceBinder operationsServiceBinder = transferServiceGetter.getOperationsServiceBinder();
-                FileDownloader.FileDownloaderBinder fileDownloaderBinder = transferServiceGetter.getFileDownloaderBinder();
-                FileUploader.FileUploaderBinder fileUploaderBinder = transferServiceGetter.getFileUploaderBinder();
-                if (operationsServiceBinder != null && operationsServiceBinder.isSynchronizing(user, file)) {
-                    //synchronizing
-                    gridViewHolder.getLocalFileIndicator().setImageResource(R.drawable.ic_synchronizing);
-                    gridViewHolder.getLocalFileIndicator().setVisibility(View.VISIBLE);
-
-                } else if (fileDownloaderBinder != null && fileDownloaderBinder.isDownloading(user, file)) {
-                    // downloading
-                    gridViewHolder.getLocalFileIndicator().setImageResource(R.drawable.ic_synchronizing);
-                    gridViewHolder.getLocalFileIndicator().setVisibility(View.VISIBLE);
-
-                } else if (fileUploaderBinder != null && fileUploaderBinder.isUploading(user, file)) {
-                    //uploading
-                    gridViewHolder.getLocalFileIndicator().setImageResource(R.drawable.ic_synchronizing);
-                    gridViewHolder.getLocalFileIndicator().setVisibility(View.VISIBLE);
-
-                } else if (file.getEtagInConflict() != null) {
-                    // conflict
-                    gridViewHolder.getLocalFileIndicator().setImageResource(R.drawable.ic_synchronizing_error);
-                    gridViewHolder.getLocalFileIndicator().setVisibility(View.VISIBLE);
-
-                } else if (file.isDown()) {
-                    gridViewHolder.getLocalFileIndicator().setImageResource(R.drawable.ic_synced);
-                    gridViewHolder.getLocalFileIndicator().setVisibility(View.VISIBLE);
-                }
+            holder.getSharedAvatars().setAvatars(user, sharees);
+            holder.getSharedAvatars().setOnClickListener(
+                view -> ocFileListFragmentInterface.onShareIconClick(file));
+        } else {
+            holder.getSharedAvatars().setVisibility(View.GONE);
+            holder.getSharedAvatars().removeAllViews();
+        }
 
-                gridViewHolder.getFavorite().setVisibility(file.isFavorite() ? View.VISIBLE : View.GONE);
+        // npe fix: looks like file without local storage path somehow get here
+        final String storagePath = file.getStoragePath();
+        if (onlyOnDevice && storagePath != null) {
+            File localFile = new File(storagePath);
+            long localSize;
+            if (localFile.isDirectory()) {
+                localSize = FileStorageUtils.getFolderSize(localFile);
             } else {
-                gridViewHolder.getLocalFileIndicator().setVisibility(View.GONE);
-                gridViewHolder.getFavorite().setVisibility(View.GONE);
+                localSize = localFile.length();
             }
 
-            if (multiSelect) {
-                gridViewHolder.getCheckbox().setVisibility(View.VISIBLE);
+            holder.getFileSize().setText(DisplayUtils.bytesToHumanReadable(localSize));
+            holder.getFileSize().setVisibility(View.VISIBLE);
+            holder.getFileSizeSeparator().setVisibility(View.VISIBLE);
+        } else {
+            final long fileLength = file.getFileLength();
+            if (fileLength >= 0) {
+                holder.getFileSize().setText(DisplayUtils.bytesToHumanReadable(fileLength));
+                holder.getFileSize().setVisibility(View.VISIBLE);
+                holder.getFileSizeSeparator().setVisibility(View.VISIBLE);
             } else {
-                gridViewHolder.getCheckbox().setVisibility(View.GONE);
+                holder.getFileSize().setVisibility(View.GONE);
+                holder.getFileSizeSeparator().setVisibility(View.GONE);
             }
+        }
 
-            if (holder instanceof ListGridItemViewHolder) {
-                ListGridItemViewHolder gridItemViewHolder = (ListGridItemViewHolder) holder;
+        final long modificationTimestamp = file.getModificationTimestamp();
+        if (modificationTimestamp > 0) {
+            holder.getLastModification().setText(DisplayUtils.getRelativeTimestamp(activity,
+                                                                                   modificationTimestamp));
+            holder.getLastModification().setVisibility(View.VISIBLE);
+        } else if (file.getFirstShareTimestamp() > 0) {
+            holder.getLastModification().setText(
+                DisplayUtils.getRelativeTimestamp(activity, file.getFirstShareTimestamp())
+                                                );
+            holder.getLastModification().setVisibility(View.VISIBLE);
+        } else {
+            holder.getLastModification().setVisibility(View.GONE);
+        }
 
-                gridItemViewHolder.getFileName().setText(file.getDecryptedFileName());
 
-                if (gridView && gridImage) {
-                    gridItemViewHolder.getFileName().setVisibility(View.GONE);
-                } else {
-                    if (gridView && ocFileListFragmentInterface.getColumnsCount() > showFilenameColumnThreshold) {
-                        gridItemViewHolder.getFileName().setVisibility(View.GONE);
-                    } else {
-                        gridItemViewHolder.getFileName().setVisibility(View.VISIBLE);
-                    }
-                }
-            }
-
-            if (gridView || hideItemOptions || (file.isFolder() && !file.canReshare())) {
-                gridViewHolder.getShared().setVisibility(View.GONE);
-            } else {
-                showShareIcon(gridViewHolder, file);
-            }
+        if (isMultiSelect() || gridView || hideItemOptions) {
+            holder.getOverflowMenu().setVisibility(View.GONE);
+        } else {
+            holder.getOverflowMenu().setVisibility(View.VISIBLE);
+            holder.getOverflowMenu().setOnClickListener(view -> ocFileListFragmentInterface
+                .onOverflowIconClicked(file, view));
         }
     }
 
-    public static void setThumbnail(OCFile file,
-                                    ImageView thumbnailView,
-                                    User user,
-                                    FileDataStorageManager storageManager,
-                                    List<ThumbnailsCacheManager.ThumbnailGenerationTask> asyncTasks,
-                                    boolean gridView,
-                                    Context context) {
-        setThumbnail(file, thumbnailView, user, storageManager, asyncTasks, gridView, context, null, null);
-    }
+    private void bindListGridItemViewHolder(ListGridItemViewHolder holder, OCFile file) {
+        holder.getFileName().setText(file.getDecryptedFileName());
 
-    private static void setThumbnail(OCFile file,
-                                     ImageView thumbnailView,
-                                     User user,
-                                     FileDataStorageManager storageManager,
-                                     List<ThumbnailsCacheManager.ThumbnailGenerationTask> asyncTasks,
-                                     boolean gridView,
-                                     Context context,
-                                     LoaderImageView shimmerThumbnail,
-                                     AppPreferences preferences) {
-        if (file.isFolder()) {
-            stopShimmer(shimmerThumbnail, thumbnailView);
-            thumbnailView.setImageDrawable(MimeTypeUtil
-                                               .getFolderTypeIcon(file.isSharedWithMe() || file.isSharedWithSharee(),
-                                                                  file.isSharedViaLink(), file.isEncrypted(),
-                                                                  file.getMountType(), context));
+        boolean gridImage = MimeTypeUtil.isImage(file) || MimeTypeUtil.isVideo(file);
+        if (gridView && gridImage) {
+            holder.getFileName().setVisibility(View.GONE);
         } else {
-            if (file.getRemoteId() != null && file.isPreviewAvailable()) {
-                // Thumbnail in cache?
-                Bitmap thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache(
-                    ThumbnailsCacheManager.PREFIX_THUMBNAIL + file.getRemoteId()
-                                                                                );
-
-                if (thumbnail != null && !file.isUpdateThumbnailNeeded()) {
-                    stopShimmer(shimmerThumbnail, thumbnailView);
-
-                    if (MimeTypeUtil.isVideo(file)) {
-                        Bitmap withOverlay = ThumbnailsCacheManager.addVideoOverlay(thumbnail);
-                        thumbnailView.setImageBitmap(withOverlay);
-                    } else {
-                        if (gridView) {
-                            BitmapUtils.setRoundedBitmapForGridMode(thumbnail, thumbnailView);
-                        } else {
-                            BitmapUtils.setRoundedBitmap(thumbnail, thumbnailView);
-                        }
-                    }
-                } else {
-                    // generate new thumbnail
-                    if (ThumbnailsCacheManager.cancelPotentialThumbnailWork(file, thumbnailView)) {
-                        try {
-                            final ThumbnailsCacheManager.ThumbnailGenerationTask task =
-                                new ThumbnailsCacheManager.ThumbnailGenerationTask(thumbnailView,
-                                                                                   storageManager,
-                                                                                   user,
-                                                                                   asyncTasks,
-                                                                                   gridView);
-                            if (thumbnail == null) {
-                                Drawable drawable = MimeTypeUtil.getFileTypeIcon(file.getMimeType(),
-                                                                                 file.getFileName(),
-                                                                                 user,
-                                                                                 context);
-                                if (drawable == null) {
-                                    drawable = ResourcesCompat.getDrawable(context.getResources(),
-                                                                           R.drawable.file_image,
-                                                                           null);
-                                }
-                                int px = ThumbnailsCacheManager.getThumbnailDimension();
-                                thumbnail = BitmapUtils.drawableToBitmap(drawable, px, px);
-                            }
-                            final ThumbnailsCacheManager.AsyncThumbnailDrawable asyncDrawable =
-                                new ThumbnailsCacheManager.AsyncThumbnailDrawable(context.getResources(),
-                                                                                  thumbnail, task);
-
-                            if (shimmerThumbnail != null && shimmerThumbnail.getVisibility() == View.GONE) {
-                                if (gridView) {
-                                    configShimmerGridImageSize(shimmerThumbnail, preferences.getGridColumns());
-                                }
-                                startShimmer(shimmerThumbnail, thumbnailView);
-                            }
-
-                            task.setListener(new ThumbnailsCacheManager.ThumbnailGenerationTask.Listener() {
-                                @Override
-                                public void onSuccess() {
-                                    stopShimmer(shimmerThumbnail, thumbnailView);
-                                }
-
-                                @Override
-                                public void onError() {
-                                    stopShimmer(shimmerThumbnail, thumbnailView);
-                                }
-                            });
-
-                            thumbnailView.setImageDrawable(asyncDrawable);
-                            asyncTasks.add(task);
-                            task.execute(new ThumbnailsCacheManager.ThumbnailGenerationTaskObject(file,
-                                                                                                  file.getRemoteId()));
-                        } catch (IllegalArgumentException e) {
-                            Log_OC.d(TAG, "ThumbnailGenerationTask : " + e.getMessage());
-                        }
-                    }
-                }
-
-                if ("image/png".equalsIgnoreCase(file.getMimeType())) {
-                    thumbnailView.setBackgroundColor(context.getResources().getColor(R.color.bg_default));
-                }
+            if (gridView && ocFileListFragmentInterface.getColumnsCount() > showFilenameColumnThreshold) {
+                holder.getFileName().setVisibility(View.GONE);
             } else {
-                stopShimmer(shimmerThumbnail, thumbnailView);
-                thumbnailView.setImageDrawable(MimeTypeUtil.getFileTypeIcon(file.getMimeType(),
-                                                                            file.getFileName(),
-                                                                            user,
-                                                                            context));
+                holder.getFileName().setVisibility(View.VISIBLE);
             }
         }
     }
 
+
     @Override
     public void onViewAttachedToWindow(@NonNull RecyclerView.ViewHolder holder) {
         if (holder instanceof ListGridImageViewHolder) {
@@ -697,51 +484,6 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
         }
     }
 
-    private static Point getScreenSize(Context context) throws Exception {
-        final WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
-        if (windowManager != null) {
-            final Point displaySize = new Point();
-            windowManager.getDefaultDisplay().getSize(displaySize);
-            return displaySize;
-        } else {
-            throw new Exception("WindowManager not found");
-        }
-    }
-
-    private static void configShimmerGridImageSize(LoaderImageView thumbnailShimmer, float gridColumns) {
-        FrameLayout.LayoutParams targetLayoutParams = (FrameLayout.LayoutParams) thumbnailShimmer.getLayoutParams();
-
-        try {
-            final Point screenSize = getScreenSize(thumbnailShimmer.getContext());
-            final int marginLeftAndRight = targetLayoutParams.leftMargin + targetLayoutParams.rightMargin;
-            final int size = Math.round(screenSize.x / gridColumns - marginLeftAndRight);
-
-            FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(size, size);
-            params.setMargins(targetLayoutParams.leftMargin,
-                              targetLayoutParams.topMargin,
-                              targetLayoutParams.rightMargin,
-                              targetLayoutParams.bottomMargin);
-            thumbnailShimmer.setLayoutParams(params);
-        } catch (Exception exception) {
-            Log_OC.e("ConfigShimmer", exception.getMessage());
-        }
-    }
-
-    private static void startShimmer(LoaderImageView thumbnailShimmer, ImageView thumbnailView) {
-        thumbnailShimmer.setImageResource(R.drawable.background);
-        thumbnailShimmer.resetLoader();
-        thumbnailView.setVisibility(View.GONE);
-        thumbnailShimmer.setVisibility(View.VISIBLE);
-    }
-
-    private static void stopShimmer(@Nullable LoaderImageView thumbnailShimmer, ImageView thumbnailView) {
-        if (thumbnailShimmer != null) {
-            thumbnailShimmer.setVisibility(View.GONE);
-        }
-
-        thumbnailView.setVisibility(View.VISIBLE);
-    }
-
     private String getFooterText() {
         int filesCount = 0;
         int foldersCount = 0;
@@ -801,7 +543,7 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
             return false;
         }
 
-        if (MainApp.isOnlyOnDevice() || ocFileListFragmentInterface.isSearchFragment()) {
+        if (MainApp.isOnlyOnDevice()) {
             return false;
         }
 
@@ -840,33 +582,6 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
         }
     }
 
-    private void showShareIcon(ListGridImageViewHolder gridViewHolder, OCFile file) {
-        ImageView sharedIconView = gridViewHolder.getShared();
-
-        if (gridViewHolder instanceof OCFileListItemViewHolder || file.getUnreadCommentsCount() == 0) {
-            sharedIconView.setVisibility(View.VISIBLE);
-
-            if (file.isSharedWithSharee() || file.isSharedWithMe()) {
-                if (showShareAvatar) {
-                    sharedIconView.setVisibility(View.GONE);
-                } else {
-                    sharedIconView.setVisibility(View.VISIBLE);
-                    sharedIconView.setImageResource(R.drawable.shared_via_users);
-                    sharedIconView.setContentDescription(activity.getString(R.string.shared_icon_shared));
-                }
-            } else if (file.isSharedViaLink()) {
-                sharedIconView.setImageResource(R.drawable.shared_via_link);
-                sharedIconView.setContentDescription(activity.getString(R.string.shared_icon_shared_via_link));
-            } else {
-                sharedIconView.setImageResource(R.drawable.ic_unshared);
-                sharedIconView.setContentDescription(activity.getString(R.string.shared_icon_share));
-            }
-            sharedIconView.setOnClickListener(view -> ocFileListFragmentInterface.onShareIconClick(file));
-        } else {
-            sharedIconView.setVisibility(View.GONE);
-        }
-    }
-
     /**
      * Change the adapted directory for a new one
      *
@@ -874,17 +589,18 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
      * @param updatedStorageManager Optional updated storage manager; used to replace
      * @param limitToMimeType       show only files of this mimeType
      */
+    @SuppressLint("NotifyDataSetChanged")
     public void swapDirectory(
-        User account,
-        OCFile directory,
-        FileDataStorageManager updatedStorageManager,
-        boolean onlyOnDevice, String limitToMimeType
+        @NonNull User account,
+        @NonNull OCFile directory,
+        @NonNull FileDataStorageManager updatedStorageManager,
+        boolean onlyOnDevice, @NonNull String limitToMimeType
                              ) {
         this.onlyOnDevice = onlyOnDevice;
 
-        if (updatedStorageManager != null && !updatedStorageManager.equals(mStorageManager)) {
+        if (!updatedStorageManager.equals(mStorageManager)) {
             mStorageManager = updatedStorageManager;
-            showShareAvatar = CapabilityUtils.getCapability(account, activity).getVersion().isShareesOnDavSupported();
+            ocFileListDelegate.setShowShareAvatar(CapabilityUtils.getCapability(account, activity).getVersion().isShareesOnDavSupported());
             this.user = account;
         }
         if (mStorageManager != null) {
@@ -917,7 +633,10 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
                         boolean clear) {
         if (storageManager != null && mStorageManager == null) {
             mStorageManager = storageManager;
-            showShareAvatar = mStorageManager.getCapability(user.getAccountName()).getVersion().isShareesOnDavSupported();
+            ocFileListDelegate.setShowShareAvatar(mStorageManager
+                                                      .getCapability(user.getAccountName())
+                                                      .getVersion()
+                                                      .isShareesOnDavSupported());
         }
 
         if (mStorageManager == null) {
@@ -1057,19 +776,6 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
         mStorageManager.saveVirtuals(contentValues);
     }
 
-    public void showAllGalleryItems(FileDataStorageManager storageManager) {
-        if (mStorageManager == null) {
-            mStorageManager = storageManager;
-        }
-        mFiles = mStorageManager.getAllGalleryItems();
-        FileStorageUtils.sortOcFolderDescDateModifiedWithoutFavoritesFirst(mFiles);
-
-        mFilesAll.clear();
-        mFilesAll.addAll(mFiles);
-
-        new Handler(Looper.getMainLooper()).post(this::notifyDataSetChanged);
-    }
-
     public void showVirtuals(VirtualFolderType type, boolean onlyImages, FileDataStorageManager storageManager) {
         mFiles = storageManager.getVirtualFolderContent(type, onlyImages);
 
@@ -1093,16 +799,19 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
     }
 
     public Set<OCFile> getCheckedItems() {
-        return checkedFiles;
+        return ocFileListDelegate.getCheckedItems();
     }
 
     public void setCheckedItem(Set<OCFile> files) {
-        checkedFiles.clear();
-        checkedFiles.addAll(files);
+        ocFileListDelegate.setCheckedItem(files);
     }
 
     public void clearCheckedItems() {
-        checkedFiles.clear();
+        ocFileListDelegate.clearCheckedItems();
+    }
+
+    public void setFiles(List<OCFile> files) {
+        mFiles = files;
     }
 
     public List<OCFile> getFiles() {
@@ -1111,7 +820,7 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
 
     public Filter getFilter() {
         if (mFilesFilter == null) {
-            mFilesFilter = new FilesFilter();
+            mFilesFilter = new FilesFilter(this);
         }
         return mFilesFilter;
     }
@@ -1124,6 +833,20 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
         return lastTimestamp;
     }
 
+    public void updateFilteredResults(Vector<OCFile> results) {
+        mFiles.clear();
+        if (results != null && results.size() > 0) {
+            mFiles.addAll(results);
+            if (!preferences.isShowHiddenFilesEnabled()) {
+                mFiles = filterHiddenFiles(mFiles);
+            }
+            FileSortOrder sortOrder = preferences.getSortOrderByFolder(currentDirectory);
+            mFiles = sortOrder.sortCloudFiles(mFiles);
+        }
+
+        notifyDataSetChanged();
+    }
+
     @Override
     public void avatarGenerated(Drawable avatarDrawable, Object callContext) {
         ((ImageView) callContext).setImageDrawable(avatarDrawable);
@@ -1134,50 +857,16 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
         return ((ImageView) callContext).getTag().equals(tag);
     }
 
-    public void setHighlightedItem(OCFile highlightedItem) {
-        this.highlightedItem = highlightedItem;
+    public boolean isCheckedFile(OCFile file) {
+        return ocFileListDelegate.isCheckedFile(file);
     }
 
-    private class FilesFilter extends Filter {
-        @Override
-        protected FilterResults performFiltering(CharSequence constraint) {
-            FilterResults results = new FilterResults();
-            Vector<OCFile> filteredFiles = new Vector<>();
-
-            if (!TextUtils.isEmpty(constraint)) {
-                for (OCFile file : mFilesAll) {
-                    if (file.getParentRemotePath().equals(currentDirectory.getRemotePath()) &&
-                        file.getFileName().toLowerCase(Locale.getDefault()).contains(
-                            constraint.toString().toLowerCase(Locale.getDefault())) &&
-                        !filteredFiles.contains(file)) {
-                        filteredFiles.add(file);
-                    }
-                }
-            }
-
-            results.values = filteredFiles;
-            results.count = filteredFiles.size();
-
-            return results;
-        }
-
-        @SuppressWarnings("unchecked")
-        @Override
-        protected void publishResults(CharSequence constraint, Filter.FilterResults results) {
-
-            Vector<OCFile> ocFiles = (Vector<OCFile>) results.values;
-            mFiles.clear();
-            if (ocFiles != null && ocFiles.size() > 0) {
-                mFiles.addAll(ocFiles);
-                if (!preferences.isShowHiddenFilesEnabled()) {
-                    mFiles = filterHiddenFiles(mFiles);
-                }
-                sortOrder = preferences.getSortOrderByFolder(currentDirectory);
-                mFiles = sortOrder.sortCloudFiles(mFiles);
-            }
+    public void addCheckedFile(OCFile file) {
+        ocFileListDelegate.addCheckedFile(file);
+    }
 
-            notifyDataSetChanged();
-        }
+    public void setHighlightedItem(OCFile file) {
+        ocFileListDelegate.setHighlightedItem(file);
     }
 
     /**
@@ -1211,17 +900,7 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
     }
 
     public void cancelAllPendingTasks() {
-        for (ThumbnailsCacheManager.ThumbnailGenerationTask task : asyncTasks) {
-            if (task != null) {
-                task.cancel(true);
-                if (task.getGetMethod() != null) {
-                    Log_OC.d(TAG, "cancel: abort get method directly");
-                    task.getGetMethod().abort();
-                }
-            }
-        }
-
-        asyncTasks.clear();
+        ocFileListDelegate.cancelAllPendingTasks();
     }
 
     public void setGridView(boolean bool) {
@@ -1229,7 +908,7 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
     }
 
     public void setShowMetadata(boolean bool) {
-        showMetadata = bool;
+        ocFileListDelegate.setMultiSelect(bool);
     }
 
     @NonNull
@@ -1255,7 +934,7 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
 
     @VisibleForTesting
     public void setShowShareAvatar(boolean bool) {
-        showShareAvatar = bool;
+        ocFileListDelegate.setShowShareAvatar(bool);
     }
 
     @VisibleForTesting
@@ -1263,240 +942,22 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
         currentDirectory = folder;
     }
 
-    static class OCFileListItemViewHolder extends RecyclerView.ViewHolder implements ListItemViewHolder {
-        protected ListItemBinding binding;
-
-        private OCFileListItemViewHolder(ListItemBinding binding) {
-            super(binding.getRoot());
-            this.binding = binding;
-            this.binding.favoriteAction.getDrawable().mutate();
-        }
-
-        @Override
-        public TextView getFileSize() {
-            return binding.fileSize;
-        }
-
-        @Override
-        public View getFileSizeSeparator() {
-            return binding.fileSeparator;
-        }
-
-        @Override
-        public TextView getLastModification() {
-            return binding.lastMod;
-        }
-
-        @Override
-        public ImageView getOverflowMenu() {
-            return binding.overflowMenu;
-        }
-
-        @Override
-        public AvatarGroupLayout getSharedAvatars() {
-            return binding.sharedAvatars;
-        }
-
-        @Override
-        public TextView getFileName() {
-            return binding.Filename;
-        }
-
-        @Override
-        public ImageView getThumbnail() {
-            return binding.thumbnail;
-        }
-
-        @Override
-        public LoaderImageView getShimmerThumbnail() {
-            return binding.thumbnailShimmer;
-        }
-
-        @Override
-        public ImageView getFavorite() {
-            return binding.favoriteAction;
-        }
-
-        @Override
-        public ImageView getLocalFileIndicator() {
-            return binding.localFileIndicator;
-        }
-
-        @Override
-        public ImageView getShared() {
-            return binding.sharedIcon;
-        }
-
-        @Override
-        public ImageView getCheckbox() {
-            return binding.customCheckbox;
-        }
-
-        @Override
-        public View getItemLayout() {
-            return binding.ListItemLayout;
-        }
-
-        @Override
-        public ImageView getUnreadComments() {
-            return binding.unreadComments;
-        }
-    }
-
-    static class OCFileListGridItemViewHolder extends RecyclerView.ViewHolder implements ListGridItemViewHolder {
-        protected GridItemBinding binding;
-
-        private OCFileListGridItemViewHolder(GridItemBinding binding) {
-            super(binding.getRoot());
-            this.binding = binding;
-            this.binding.favoriteAction.getDrawable().mutate();
-        }
-
-        @Override
-        public TextView getFileName() {
-            return binding.Filename;
-        }
-
-        @Override
-        public ImageView getThumbnail() {
-            return binding.thumbnail;
-        }
-
-        @Override
-        public LoaderImageView getShimmerThumbnail() {
-            return binding.thumbnailShimmer;
-        }
-
-        @Override
-        public ImageView getFavorite() {
-            return binding.favoriteAction;
-        }
-
-        @Override
-        public ImageView getLocalFileIndicator() {
-            return binding.localFileIndicator;
-        }
-
-        @Override
-        public ImageView getShared() {
-            return binding.sharedIcon;
-        }
-
-        @Override
-        public ImageView getCheckbox() {
-            return binding.customCheckbox;
-        }
-
-        @Override
-        public View getItemLayout() {
-            return binding.ListItemLayout;
-        }
-
-        @Override
-        public ImageView getUnreadComments() {
-            return binding.unreadComments;
-        }
-    }
-
-    static class OCFileListGridImageViewHolder extends RecyclerView.ViewHolder implements ListGridImageViewHolder {
-        protected GridImageBinding binding;
-
-        private OCFileListGridImageViewHolder(GridImageBinding binding) {
-            super(binding.getRoot());
-            this.binding = binding;
-            this.binding.favoriteAction.getDrawable().mutate();
-        }
-
-        @Override
-        public ImageView getThumbnail() {
-            return binding.thumbnail;
-        }
-
-        @Override
-        public LoaderImageView getShimmerThumbnail() {
-            return binding.thumbnailShimmer;
-        }
-
-        @Override
-        public ImageView getFavorite() {
-            return binding.favoriteAction;
-        }
-
-        @Override
-        public ImageView getLocalFileIndicator() {
-            return binding.localFileIndicator;
-        }
-
-        @Override
-        public ImageView getShared() {
-            return binding.sharedIcon;
-        }
-
-        @Override
-        public ImageView getCheckbox() {
-            return binding.customCheckbox;
-        }
-
-        @Override
-        public View getItemLayout() {
-            return binding.ListItemLayout;
-        }
-
-        @Override
-        public ImageView getUnreadComments() {
-            return binding.unreadComments;
-        }
-    }
-
-    static class OCFileListFooterViewHolder extends RecyclerView.ViewHolder {
-        protected ListFooterBinding binding;
-
-        private OCFileListFooterViewHolder(ListFooterBinding binding) {
-            super(binding.getRoot());
-            this.binding = binding;
-        }
+    @SuppressFBWarnings("EI_EXPOSE_REP")
+    public List<OCFile> getAllFiles() {
+        return mFilesAll;
     }
 
-    static class OCFileListHeaderViewHolder extends RecyclerView.ViewHolder {
-        protected ListHeaderBinding binding;
-
-        private OCFileListHeaderViewHolder(ListHeaderBinding binding) {
-            super(binding.getRoot());
-            this.binding = binding;
-        }
+    public OCFile getCurrentDirectory() {
+        return currentDirectory;
     }
 
-    interface ListGridImageViewHolder {
-        ImageView getThumbnail();
-
-        LoaderImageView getShimmerThumbnail();
-
-        ImageView getFavorite();
-
-        ImageView getLocalFileIndicator();
-
-        ImageView getShared();
-
-        ImageView getCheckbox();
-
-        View getItemLayout();
-
-        ImageView getUnreadComments();
-    }
-
-    interface ListGridItemViewHolder extends ListGridImageViewHolder {
-        TextView getFileName();
+    @Override
+    public int getFilesCount() {
+        return mFiles.size();
     }
 
-    interface ListItemViewHolder extends ListGridItemViewHolder {
-        TextView getFileSize();
-
-        View getFileSizeSeparator();
-
-        TextView getLastModification();
-
-        ImageView getOverflowMenu();
-
-        AvatarGroupLayout getSharedAvatars();
+    @Override
+    public void notifyItemChanged(@NonNull OCFile file) {
+        notifyItemChanged(getItemPosition(file));
     }
 }

+ 230 - 0
app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt

@@ -0,0 +1,230 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2022 Tobias Kaminsky
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package com.owncloud.android.ui.adapter
+
+import android.content.Context
+import android.view.View
+import com.nextcloud.client.account.User
+import com.nextcloud.client.preferences.AppPreferences
+import com.owncloud.android.R
+import com.owncloud.android.datamodel.FileDataStorageManager
+import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.datamodel.ThumbnailsCacheManager.ThumbnailGenerationTask
+import com.owncloud.android.lib.common.utils.Log_OC
+import com.owncloud.android.ui.activity.ComponentsGetter
+import com.owncloud.android.ui.interfaces.OCFileListFragmentInterface
+import com.owncloud.android.utils.DisplayUtils
+import com.owncloud.android.utils.theme.ThemeColorUtils
+import com.owncloud.android.utils.theme.ThemeDrawableUtils
+
+class OCFileListDelegate(
+    private val context: Context,
+    private val ocFileListFragmentInterface: OCFileListFragmentInterface,
+    private val user: User,
+    private val storageManager: FileDataStorageManager,
+    private val hideItemOptions: Boolean,
+    private val preferences: AppPreferences,
+    private val gridView: Boolean,
+    private val transferServiceGetter: ComponentsGetter,
+    private val showMetadata: Boolean,
+    private var showShareAvatar: Boolean
+) {
+    private val checkedFiles: MutableSet<OCFile> = HashSet()
+    private var highlightedItem: OCFile? = null
+    var isMultiSelect = false
+    private val asyncTasks: MutableList<ThumbnailGenerationTask> = ArrayList()
+    fun setHighlightedItem(highlightedItem: OCFile?) {
+        this.highlightedItem = highlightedItem
+    }
+
+    fun isCheckedFile(file: OCFile): Boolean {
+        return checkedFiles.contains(file)
+    }
+
+    fun addCheckedFile(file: OCFile) {
+        checkedFiles.add(file)
+        highlightedItem = null
+    }
+
+    fun removeCheckedFile(file: OCFile) {
+        checkedFiles.remove(file)
+    }
+
+    fun addToCheckedFiles(files: List<OCFile>?) {
+        checkedFiles.addAll(files!!)
+    }
+
+    val checkedItems: Set<OCFile>
+        get() = checkedFiles
+
+    fun setCheckedItem(files: Set<OCFile>?) {
+        checkedFiles.clear()
+        checkedFiles.addAll(files!!)
+    }
+
+    fun clearCheckedItems() {
+        checkedFiles.clear()
+    }
+
+    fun bindGridViewHolder(gridViewHolder: ListGridImageViewHolder, file: OCFile) {
+        gridViewHolder.thumbnail.tag = file.fileId
+        DisplayUtils.setThumbnail(
+            file,
+            gridViewHolder.thumbnail,
+            user,
+            storageManager,
+            asyncTasks,
+            gridView,
+            context,
+            gridViewHolder.shimmerThumbnail,
+            preferences
+        )
+        if (highlightedItem != null && file.fileId == highlightedItem!!.fileId) {
+            gridViewHolder.itemLayout.setBackgroundColor(
+                context.resources
+                    .getColor(R.color.selected_item_background)
+            )
+        } else if (isCheckedFile(file)) {
+            gridViewHolder.itemLayout.setBackgroundColor(
+                context.resources
+                    .getColor(R.color.selected_item_background)
+            )
+            gridViewHolder.checkbox.setImageDrawable(
+                ThemeDrawableUtils.tintDrawable(
+                    R.drawable.ic_checkbox_marked,
+                    ThemeColorUtils.primaryColor(context)
+                )
+            )
+        } else {
+            gridViewHolder.itemLayout.setBackgroundColor(context.resources.getColor(R.color.bg_default))
+            gridViewHolder.checkbox.setImageResource(R.drawable.ic_checkbox_blank_outline)
+        }
+        gridViewHolder.itemLayout.setOnClickListener { ocFileListFragmentInterface.onItemClicked(file) }
+        if (!hideItemOptions) {
+            gridViewHolder.itemLayout.isLongClickable = true
+            gridViewHolder.itemLayout.setOnLongClickListener {
+                ocFileListFragmentInterface.onLongItemClicked(
+                    file
+                )
+            }
+        }
+
+        // unread comments
+        if (file.unreadCommentsCount > 0) {
+            gridViewHolder.unreadComments.visibility = View.VISIBLE
+            gridViewHolder.unreadComments.setOnClickListener {
+                ocFileListFragmentInterface
+                    .showActivityDetailView(file)
+            }
+        } else {
+            gridViewHolder.unreadComments.visibility = View.GONE
+        }
+
+        // multiSelect (Checkbox)
+        if (isMultiSelect) {
+            gridViewHolder.checkbox.visibility = View.VISIBLE
+        } else {
+            gridViewHolder.checkbox.visibility = View.GONE
+        }
+
+        // download state
+        gridViewHolder.localFileIndicator.visibility = View.INVISIBLE // default first
+        if (showMetadata) {
+            val operationsServiceBinder = transferServiceGetter.operationsServiceBinder
+            val fileDownloaderBinder = transferServiceGetter.fileDownloaderBinder
+            val fileUploaderBinder = transferServiceGetter.fileUploaderBinder
+            if (operationsServiceBinder != null && operationsServiceBinder.isSynchronizing(user, file)) {
+                //synchronizing
+                gridViewHolder.localFileIndicator.setImageResource(R.drawable.ic_synchronizing)
+                gridViewHolder.localFileIndicator.visibility = View.VISIBLE
+            } else if (fileDownloaderBinder != null && fileDownloaderBinder.isDownloading(user, file)) {
+                // downloading
+                gridViewHolder.localFileIndicator.setImageResource(R.drawable.ic_synchronizing)
+                gridViewHolder.localFileIndicator.visibility = View.VISIBLE
+            } else if (fileUploaderBinder != null && fileUploaderBinder.isUploading(user, file)) {
+                //uploading
+                gridViewHolder.localFileIndicator.setImageResource(R.drawable.ic_synchronizing)
+                gridViewHolder.localFileIndicator.visibility = View.VISIBLE
+            } else if (file.etagInConflict != null) {
+                // conflict
+                gridViewHolder.localFileIndicator.setImageResource(R.drawable.ic_synchronizing_error)
+                gridViewHolder.localFileIndicator.visibility = View.VISIBLE
+            } else if (file.isDown) {
+                gridViewHolder.localFileIndicator.setImageResource(R.drawable.ic_synced)
+                gridViewHolder.localFileIndicator.visibility = View.VISIBLE
+            }
+            gridViewHolder.favorite.visibility = if (file.isFavorite) View.VISIBLE else View.GONE
+        } else {
+            gridViewHolder.localFileIndicator.visibility = View.GONE
+            gridViewHolder.favorite.visibility = View.GONE
+        }
+        if (gridView || hideItemOptions || file.isFolder && !file.canReshare()) {
+            gridViewHolder.shared.visibility = View.GONE
+        } else {
+            showShareIcon(gridViewHolder, file)
+        }
+    }
+
+    private fun showShareIcon(gridViewHolder: ListGridImageViewHolder, file: OCFile) {
+        val sharedIconView = gridViewHolder.shared
+        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)
+            }
+            sharedIconView.setOnClickListener { ocFileListFragmentInterface.onShareIconClick(file) }
+        } else {
+            sharedIconView.visibility = View.GONE
+        }
+    }
+
+    fun cancelAllPendingTasks() {
+        for (task in asyncTasks) {
+            task.cancel(true)
+            if (task.getMethod != null) {
+                Log_OC.d(TAG, "cancel: abort get method directly")
+                task.getMethod.abort()
+            }
+        }
+        asyncTasks.clear()
+    }
+
+    fun setShowShareAvatar(bool: Boolean) {
+        showShareAvatar = bool
+    }
+
+    companion object {
+        private val TAG = OCFileListDelegate::class.java.simpleName
+    }
+}

+ 35 - 0
app/src/main/java/com/owncloud/android/ui/adapter/OCFileListFooterViewHolder.kt

@@ -0,0 +1,35 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2022 Tobias Kaminsky
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package com.owncloud.android.ui.adapter
+
+import androidx.recyclerview.widget.RecyclerView
+import com.owncloud.android.databinding.ListFooterBinding
+
+internal class OCFileListFooterViewHolder(var binding: ListFooterBinding) : RecyclerView.ViewHolder(
+    binding.root
+) {
+    val footerText
+        get() = binding.footerText
+
+    val loadingProgressBar
+        get() = binding.loadingProgressBar
+}

+ 53 - 0
app/src/main/java/com/owncloud/android/ui/adapter/OCFileListGridImageViewHolder.kt

@@ -0,0 +1,53 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2022 Tobias Kaminsky
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package com.owncloud.android.ui.adapter
+
+import android.view.View
+import android.widget.ImageView
+import androidx.recyclerview.widget.RecyclerView
+import com.elyeproj.loaderviewlibrary.LoaderImageView
+import com.owncloud.android.databinding.GridImageBinding
+
+internal class OCFileListGridImageViewHolder(var binding: GridImageBinding) : RecyclerView.ViewHolder(
+    binding.root
+), ListGridImageViewHolder {
+    override val thumbnail: ImageView
+        get() = binding.thumbnail
+    override val shimmerThumbnail: LoaderImageView
+        get() = binding.thumbnailShimmer
+    override val favorite: ImageView
+        get() = binding.favoriteAction
+    override val localFileIndicator: ImageView
+        get() = binding.localFileIndicator
+    override val shared: ImageView
+        get() = binding.sharedIcon
+    override val checkbox: ImageView
+        get() = binding.customCheckbox
+    override val itemLayout: View
+        get() = binding.ListItemLayout
+    override val unreadComments: ImageView
+        get() = binding.unreadComments
+
+    init {
+        binding.favoriteAction.drawable.mutate()
+    }
+}

+ 56 - 0
app/src/main/java/com/owncloud/android/ui/adapter/OCFileListGridItemViewHolder.kt

@@ -0,0 +1,56 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2022 Tobias Kaminsky
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package com.owncloud.android.ui.adapter
+
+import android.view.View
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import com.elyeproj.loaderviewlibrary.LoaderImageView
+import com.owncloud.android.databinding.GridItemBinding
+
+internal class OCFileListGridItemViewHolder(var binding: GridItemBinding) : RecyclerView.ViewHolder(
+    binding.root
+), ListGridItemViewHolder {
+    override val fileName: TextView
+        get() = binding.Filename
+    override val thumbnail: ImageView
+        get() = binding.thumbnail
+    override val shimmerThumbnail: LoaderImageView
+        get() = binding.thumbnailShimmer
+    override val favorite: ImageView
+        get() = binding.favoriteAction
+    override val localFileIndicator: ImageView
+        get() = binding.localFileIndicator
+    override val shared: ImageView
+        get() = binding.sharedIcon
+    override val checkbox: ImageView
+        get() = binding.customCheckbox
+    override val itemLayout: View
+        get() = binding.ListItemLayout
+    override val unreadComments: ImageView
+        get() = binding.unreadComments
+
+    init {
+        binding.favoriteAction.drawable.mutate()
+    }
+}

+ 35 - 0
app/src/main/java/com/owncloud/android/ui/adapter/OCFileListHeaderViewHolder.kt

@@ -0,0 +1,35 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2022 Tobias Kaminsky
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package com.owncloud.android.ui.adapter
+
+import androidx.recyclerview.widget.RecyclerView
+import com.owncloud.android.databinding.ListHeaderBinding
+
+internal class OCFileListHeaderViewHolder(var binding: ListHeaderBinding) : RecyclerView.ViewHolder(
+    binding.root
+) {
+    val headerText
+        get() = binding.headerText
+
+    val headerView
+        get() = binding.headerView
+}

+ 67 - 0
app/src/main/java/com/owncloud/android/ui/adapter/OCFileListItemViewHolder.kt

@@ -0,0 +1,67 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2022 Tobias Kaminsky
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package com.owncloud.android.ui.adapter
+
+import android.view.View
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import com.elyeproj.loaderviewlibrary.LoaderImageView
+import com.owncloud.android.databinding.ListItemBinding
+import com.owncloud.android.ui.AvatarGroupLayout
+
+internal class OCFileListItemViewHolder(private var binding: ListItemBinding) : RecyclerView.ViewHolder(
+    binding.root
+), ListItemViewHolder {
+    override val fileSize: TextView
+        get() = binding.fileSize
+    override val fileSizeSeparator: View
+        get() = binding.fileSeparator
+    override val lastModification: TextView
+        get() = binding.lastMod
+    override val overflowMenu: ImageView
+        get() = binding.overflowMenu
+    override val sharedAvatars: AvatarGroupLayout
+        get() = binding.sharedAvatars
+    override val fileName: TextView
+        get() = binding.Filename
+    override val thumbnail: ImageView
+        get() = binding.thumbnail
+    override val shimmerThumbnail: LoaderImageView
+        get() = binding.thumbnailShimmer
+    override val favorite: ImageView
+        get() = binding.favoriteAction
+    override val localFileIndicator: ImageView
+        get() = binding.localFileIndicator
+    override val shared: ImageView
+        get() = binding.sharedIcon
+    override val checkbox: ImageView
+        get() = binding.customCheckbox
+    override val itemLayout: View
+        get() = binding.ListItemLayout
+    override val unreadComments: ImageView
+        get() = binding.unreadComments
+
+    init {
+        binding.favoriteAction.drawable.mutate()
+    }
+}

+ 1 - 1
app/src/main/java/com/owncloud/android/ui/asynctasks/GallerySearchTask.java

@@ -100,7 +100,7 @@ public class GallerySearchTask extends AsyncTask<Void, Void, GallerySearchTask.R
                 boolean emptySearch = parseMedia(startDate, endDate, result.getData());
                 long lastTimeStamp = findLastTimestamp(result.getData());
 
-                photoFragment.getAdapter().showAllGalleryItems(storageManager);
+                photoFragment.showAllGalleryItems();
 
                 return new Result(result.isSuccess(), emptySearch, lastTimeStamp);
             } else {

+ 9 - 10
app/src/main/java/com/owncloud/android/ui/dialog/ConflictsResolveDialog.java

@@ -37,7 +37,6 @@ import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.datamodel.ThumbnailsCacheManager;
 import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.ui.adapter.LocalFileListAdapter;
-import com.owncloud.android.ui.adapter.OCFileListAdapter;
 import com.owncloud.android.utils.DisplayUtils;
 import com.owncloud.android.utils.theme.ThemeButtonUtils;
 import com.owncloud.android.utils.theme.ThemeCheckableUtils;
@@ -194,17 +193,17 @@ public class ConflictsResolveDialog extends DialogFragment {
         // set info for existing file
         binding.existingSize.setText(DisplayUtils.bytesToHumanReadable(existingFile.getFileLength()));
         binding.existingTimestamp.setText(DisplayUtils.getRelativeTimestamp(getContext(),
-                                                                    existingFile.getModificationTimestamp()));
+                                                                            existingFile.getModificationTimestamp()));
 
         binding.existingThumbnail.setTag(existingFile.getFileId());
-        OCFileListAdapter.setThumbnail(existingFile,
-                                       binding.existingThumbnail,
-                                       user,
-                                       new FileDataStorageManager(user,
-                                                                  requireContext().getContentResolver()),
-                                       asyncTasks,
-                                       false,
-                                       getContext());
+        DisplayUtils.setThumbnail(existingFile,
+                                  binding.existingThumbnail,
+                                  user,
+                                  new FileDataStorageManager(user,
+                                                             requireContext().getContentResolver()),
+                                  asyncTasks,
+                                  false,
+                                  getContext());
 
         View.OnClickListener checkBoxClickListener = v -> {
             positiveButton.setEnabled(binding.newCheckbox.isChecked() || binding.existingCheckbox.isChecked());

+ 41 - 7
app/src/main/java/com/owncloud/android/ui/fragment/GalleryFragment.java

@@ -31,6 +31,8 @@ import com.owncloud.android.R;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.ui.activity.FileDisplayActivity;
+import com.owncloud.android.ui.adapter.CommonOCFileListAdapterInterface;
+import com.owncloud.android.ui.adapter.GalleryAdapter;
 import com.owncloud.android.ui.asynctasks.GallerySearchTask;
 import com.owncloud.android.ui.events.ChangeMenuEvent;
 
@@ -50,6 +52,7 @@ public class GalleryFragment extends OCFileListFragment {
     private long endDate;
     private long daySpan = 30;
     private int limit = 300;
+    private GalleryAdapter mAdapter;
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
@@ -87,18 +90,45 @@ public class GalleryFragment extends OCFileListFragment {
     @Override
     public void onActivityCreated(Bundle savedInstanceState) {
         super.onActivityCreated(savedInstanceState);
-        mAdapter.setShowMetadata(false);
 
         currentSearchType = SearchType.GALLERY_SEARCH;
 
-        switchToGridView();
-
         menuItemAddRemoveValue = MenuItemAddRemove.REMOVE_GRID_AND_SORT;
         requireActivity().invalidateOptionsMenu();
 
         handleSearchEvent();
     }
 
+    @Override
+    protected void setAdapter(Bundle args) {
+        mAdapter = new GalleryAdapter(requireContext(),
+                                      accountManager.getUser(),
+                                      this,
+                                      preferences,
+                                      mContainerActivity);
+
+//        val spacing = resources.getDimensionPixelSize(R.dimen.media_grid_spacing)
+//        binding.list.addItemDecoration(MediaGridItemDecoration(spacing))
+        setRecyclerViewAdapter(mAdapter);
+
+
+        GridLayoutManager layoutManager = new GridLayoutManager(getContext(), getColumnsCount());
+//        ((GridLayoutManager) layoutManager).setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
+//            @Override
+//            public int getSpanSize(int position) {
+//                if (position == getAdapter().getItemCount() - 1 ||
+//                    position == 0 && getAdapter().shouldShowHeader()) {
+//                    return ((GridLayoutManager) layoutManager).getSpanCount();
+//                } else {
+//                    return 1;
+//                }
+//            }
+//        });
+
+        mAdapter.setLayoutManager(layoutManager);
+        getRecyclerView().setLayoutManager(layoutManager);
+    }
+
     @Override
     public void onRefresh() {
         super.onRefresh();
@@ -106,6 +136,11 @@ public class GalleryFragment extends OCFileListFragment {
         handleSearchEvent();
     }
 
+    @Override
+    public CommonOCFileListAdapterInterface getCommonAdapter() {
+        return mAdapter;
+    }
+
     @Override
     public void onResume() {
         super.onResume();
@@ -161,7 +196,7 @@ public class GalleryFragment extends OCFileListFragment {
             setEmptyListMessage(SearchType.GALLERY_SEARCH);
         }
 
-        if (emptySearch && getAdapter().getItemCount() > 0) {
+        if (emptySearch && mAdapter.getItemCount() > 0) {
             Log_OC.d(this, "End gallery search");
             return;
         }
@@ -234,8 +269,7 @@ public class GalleryFragment extends OCFileListFragment {
         }
     }
 
-    @Override
-    public boolean isGalleryFragment() {
-        return true;
+    public void showAllGalleryItems() {
+        mAdapter.showAllGalleryItems(mContainerActivity.getStorageManager());
     }
 }

+ 51 - 35
app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java

@@ -78,6 +78,7 @@ import com.owncloud.android.ui.activity.FileDisplayActivity;
 import com.owncloud.android.ui.activity.FolderPickerActivity;
 import com.owncloud.android.ui.activity.OnEnforceableRefreshListener;
 import com.owncloud.android.ui.activity.UploadFilesActivity;
+import com.owncloud.android.ui.adapter.CommonOCFileListAdapterInterface;
 import com.owncloud.android.ui.adapter.OCFileListAdapter;
 import com.owncloud.android.ui.dialog.ChooseRichDocumentsTemplateDialogFragment;
 import com.owncloud.android.ui.dialog.ChooseTemplateDialogFragment;
@@ -189,7 +190,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
     protected FileFragment.ContainerActivity mContainerActivity;
 
     protected OCFile mFile;
-    protected OCFileListAdapter mAdapter;
+    private OCFileListAdapter mAdapter;
     protected boolean mOnlyFoldersClickable;
     protected boolean mFileSelectable;
 
@@ -326,7 +327,9 @@ public class OCFileListFragment extends ExtendedListFragment implements
     @Override
     public void onPause() {
         super.onPause();
-        mAdapter.cancelAllPendingTasks();
+        if (mAdapter != null) {
+            mAdapter.cancelAllPendingTasks();
+        }
 
         if (getActivity() != null) {
             getActivity().getIntent().removeExtra(OCFileListFragment.SEARCH_EVENT);
@@ -349,18 +352,8 @@ public class OCFileListFragment extends ExtendedListFragment implements
         mOnlyFoldersClickable = args != null && args.getBoolean(ARG_ONLY_FOLDERS_CLICKABLE, false);
         mFileSelectable = args != null && args.getBoolean(ARG_FILE_SELECTABLE, false);
         mLimitToMimeType = args != null ? args.getString(ARG_MIMETYPE, "") : "";
-        boolean hideItemOptions = args != null && args.getBoolean(ARG_HIDE_ITEM_OPTIONS, false);
 
-        mAdapter = new OCFileListAdapter(
-            getActivity(),
-            accountManager.getUser(),
-            preferences,
-            mContainerActivity,
-            this,
-            hideItemOptions,
-            isGridViewPreferred(mFile)
-        );
-        setRecyclerViewAdapter(mAdapter);
+        setAdapter(args);
 
         mHideFab = args != null && args.getBoolean(ARG_HIDE_FAB, false);
 
@@ -406,6 +399,22 @@ public class OCFileListFragment extends ExtendedListFragment implements
         listDirectory(false, false);
     }
 
+    protected void setAdapter(Bundle args) {
+        boolean hideItemOptions = args != null && args.getBoolean(ARG_HIDE_ITEM_OPTIONS, false);
+
+        mAdapter = new OCFileListAdapter(
+            getActivity(),
+            accountManager.getUser(),
+            preferences,
+            mContainerActivity,
+            this,
+            hideItemOptions,
+            isGridViewPreferred(mFile)
+        );
+
+        setRecyclerViewAdapter(mAdapter);
+    }
+
     protected void prepareCurrentSearch(SearchEvent event) {
         if (isSearchEventSet(event)) {
 
@@ -691,7 +700,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
             // hide FAB in multi selection mode
             setFabVisible(false);
 
-            mAdapter.setMultiSelect(true);
+            getCommonAdapter().setMultiSelect(true);
             return true;
         }
 
@@ -700,12 +709,12 @@ public class OCFileListFragment extends ExtendedListFragment implements
          */
         @Override
         public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
-            final int checkedCount = mAdapter.getCheckedItems().size();
-            Set<OCFile> checkedFiles = mAdapter.getCheckedItems();
+            Set<OCFile> checkedFiles = getCommonAdapter().getCheckedItems();
+            final int checkedCount = checkedFiles.size();
             String title = getResources().getQuantityString(R.plurals.items_selected_count, checkedCount, checkedCount);
             mode.setTitle(title);
             FileMenuFilter mf = new FileMenuFilter(
-                mAdapter.getFiles().size(),
+                getCommonAdapter().getFilesCount(),
                 checkedFiles,
                 mContainerActivity,
                 getActivity(),
@@ -728,7 +737,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
          */
         @Override
         public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
-            Set<OCFile> checkedFiles = mAdapter.getCheckedItems();
+            Set<OCFile> checkedFiles = getCommonAdapter().getCheckedItems();
             return onFileActionChosen(item, checkedFiles);
         }
 
@@ -738,14 +747,14 @@ public class OCFileListFragment extends ExtendedListFragment implements
         @Override
         public void onDestroyActionMode(ActionMode mode) {
             mActiveActionMode = null;
-            
+
             // show FAB on multi selection mode exit
             if (!mHideFab && !searchFragment) {
                 setFabVisible(true);
             }
 
-            mAdapter.setMultiSelect(false);
-            mAdapter.clearCheckedItems();
+            getCommonAdapter().setMultiSelect(false);
+            getCommonAdapter().clearCheckedItems();
         }
 
         public void storeStateIn(Bundle outState) {
@@ -865,10 +874,10 @@ public class OCFileListFragment extends ExtendedListFragment implements
      * @param file The concerned OCFile by the selection/deselection
      */
     private void toggleItemToCheckedList(OCFile file) {
-        if (getAdapter().isCheckedFile(file)) {
-            getAdapter().removeCheckedFile(file);
+        if (getCommonAdapter().isCheckedFile(file)) {
+            getCommonAdapter().removeCheckedFile(file);
         } else {
-            getAdapter().addCheckedFile(file);
+            getCommonAdapter().addCheckedFile(file);
         }
         updateActionModeFile(file);
     }
@@ -881,7 +890,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
         mIsActionModeNew = false;
         if (mActiveActionMode != null) {
             mActiveActionMode.invalidate();
-            mAdapter.notifyItemChanged(getAdapter().getItemPosition(file));
+            getCommonAdapter().notifyItemChanged(file);
         }
     }
 
@@ -894,7 +903,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
                 toggleItemToCheckedList(file);
             } else {
                 actionBarActivity.startActionMode(mMultiChoiceModeListener);
-                getAdapter().addCheckedFile(file);
+                getCommonAdapter().addCheckedFile(file);
             }
             updateActionModeFile(file);
         }
@@ -904,11 +913,11 @@ public class OCFileListFragment extends ExtendedListFragment implements
 
     @Override
     public void onItemClicked(OCFile file) {
-        if (getAdapter().isMultiSelect()) {
+        if (getCommonAdapter().isMultiSelect()) {
             toggleItemToCheckedList(file);
         } else {
             if (file != null) {
-                int position = mAdapter.getItemPosition(file);
+                int position = getCommonAdapter().getItemPosition(file);
 
                 if (file.isFolder()) {
                     resetHeaderScrollingState();
@@ -1229,6 +1238,10 @@ public class OCFileListFragment extends ExtendedListFragment implements
                 if (!directory.isFolder()) {
                     Log_OC.w(TAG, "You see, that is not a directory -> " + directory);
                     directory = storageManager.getFileById(directory.getParentId());
+
+                    if (directory == null) {
+                        return; // no files, wait for sync
+                    }
                 }
 
                 if (searchView != null && !searchView.isIconified() && !fromSearch) {
@@ -1379,6 +1392,10 @@ public class OCFileListFragment extends ExtendedListFragment implements
         getAdapter().notifyDataSetChanged();
     }
 
+    public CommonOCFileListAdapterInterface getCommonAdapter() {
+        return mAdapter;
+    }
+
     public OCFileListAdapter getAdapter() {
         return mAdapter;
     }
@@ -1587,7 +1604,11 @@ public class OCFileListFragment extends ExtendedListFragment implements
 
 
                     FileDataStorageManager storageManager = mContainerActivity.getStorageManager();
-                    int position = mAdapter.getItemPosition(storageManager.getFileByRemoteId(event.remoteId));
+                    OCFile file = storageManager.getFileByRemoteId(event.remoteId);
+                    int position = -1;
+                    if (file != null) {
+                        position = mAdapter.getItemPosition(file);
+                    }
                     SetupEncryptionDialogFragment dialog = SetupEncryptionDialogFragment.newInstance(user, position);
                     dialog.setTargetFragment(this, SetupEncryptionDialogFragment.SETUP_ENCRYPTION_REQUEST_CODE);
                     dialog.show(getParentFragmentManager(), SetupEncryptionDialogFragment.SETUP_ENCRYPTION_DIALOG_TAG);
@@ -1656,11 +1677,6 @@ public class OCFileListFragment extends ExtendedListFragment implements
         return searchFragment;
     }
 
-    @Override
-    public boolean isGalleryFragment() {
-        return false;
-    }
-
     /**
      * De-/select all elements in the current list view.
      *
@@ -1672,7 +1688,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
         if (select) {
             ocFileListAdapter.addAllFilesToCheckedFiles();
         } else {
-            ocFileListAdapter.removeAllFilesFromCheckedFiles();
+            ocFileListAdapter.clearCheckedItems();
         }
 
         for (int i = 0; i < mAdapter.getItemCount(); i++) {

+ 2 - 2
app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchAsyncTask.kt

@@ -69,7 +69,7 @@ class OCFileListSearchAsyncTask(
             if (remoteOperationResult.resultData.isNullOrEmpty()) {
                 fragment.setEmptyView(event)
             } else {
-                fragment.mAdapter.setData(
+                fragment.adapter.setData(
                     remoteOperationResult.resultData,
                     fragment.currentSearchType,
                     fileDataStorageManager,
@@ -85,7 +85,7 @@ class OCFileListSearchAsyncTask(
         fragmentReference.get()?.let { fragment ->
             fragment.isLoading = false
             if (!isCancelled) {
-                fragment.mAdapter.notifyDataSetChanged()
+                fragment.adapter.notifyDataSetChanged()
             }
         }
     }

+ 1 - 1
app/src/main/java/com/owncloud/android/ui/fragment/SharedListFragment.kt

@@ -59,7 +59,7 @@ class SharedListFragment : OCFileListFragment(), Injectable {
 
     override fun onActivityCreated(savedInstanceState: Bundle?) {
         super.onActivityCreated(savedInstanceState)
-        mAdapter.setShowMetadata(false)
+        adapter.setShowMetadata(false)
         currentSearchType = SearchType.SHARED_FILTER
         searchEvent = SearchEvent("", SearchRemoteOperation.SearchType.SHARED_FILTER)
         menuItemAddRemoveValue = MenuItemAddRemove.REMOVE_GRID_AND_SORT

+ 0 - 4
app/src/main/java/com/owncloud/android/ui/interfaces/OCFileListFragmentInterface.java

@@ -48,8 +48,4 @@ public interface OCFileListFragmentInterface {
     boolean isLoading();
 
     void onHeaderClicked();
-
-    boolean isSearchFragment();
-
-    boolean isGalleryFragment();
 }

+ 3 - 0
app/src/main/java/com/owncloud/android/utils/DataHolderUtil.java

@@ -27,6 +27,8 @@ import java.security.SecureRandom;
 import java.util.HashMap;
 import java.util.Map;
 
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
 /**
  * Data holder utility to store & retrieve stuff
  */
@@ -39,6 +41,7 @@ public class DataHolderUtil {
     @SuppressLint("TrulyRandom")
     private SecureRandom random = new SecureRandom();
 
+    @SuppressFBWarnings("MS_EXPOSE_REP")
     public static synchronized DataHolderUtil getInstance() {
         if (instance == null) {
             instance = new DataHolderUtil();

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

@@ -46,6 +46,9 @@ import android.text.style.StyleSpan;
 import android.util.DisplayMetrics;
 import android.util.Log;
 import android.view.View;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
 import android.widget.Toast;
 
 import com.bumptech.glide.GenericRequestBuilder;
@@ -56,13 +59,16 @@ import com.bumptech.glide.load.resource.file.FileToStreamDecoder;
 import com.bumptech.glide.request.target.SimpleTarget;
 import com.bumptech.glide.request.target.Target;
 import com.caverock.androidsvg.SVG;
+import com.elyeproj.loaderviewlibrary.LoaderImageView;
 import com.google.android.material.snackbar.Snackbar;
 import com.nextcloud.client.account.CurrentAccountProvider;
 import com.nextcloud.client.account.User;
 import com.nextcloud.client.network.ClientFactory;
+import com.nextcloud.client.preferences.AppPreferences;
 import com.owncloud.android.MainApp;
 import com.owncloud.android.R;
 import com.owncloud.android.datamodel.ArbitraryDataProvider;
+import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.datamodel.ThumbnailsCacheManager;
 import com.owncloud.android.lib.common.OwnCloudAccount;
@@ -89,17 +95,19 @@ import java.math.BigDecimal;
 import java.net.IDN;
 import java.nio.charset.Charset;
 import java.text.DateFormat;
+import java.text.SimpleDateFormat;
 import java.util.Date;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.TimeZone;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.StringRes;
 import androidx.appcompat.widget.AppCompatDrawableManager;
 import androidx.core.content.res.ResourcesCompat;
-import androidx.fragment.app.Fragment;
 import androidx.fragment.app.FragmentManager;
 import androidx.fragment.app.FragmentTransaction;
 
@@ -130,6 +138,10 @@ public final class DisplayUtils {
     private static final double BYTE_SIZE_DIVIDER_DOUBLE = 1024.0;
     private static final int DATE_TIME_PARTS_SIZE = 2;
 
+    public static final String MONTH_YEAR_PATTERN = "MMMM yyyy";
+    public static final String MONTH_PATTERN = "MMMM";
+    public static final String YEAR_PATTERN = "yyyy";
+
     private static Map<String, String> mimeType2HumanReadable;
 
     static {
@@ -820,4 +832,178 @@ public final class DisplayUtils {
                 return R.string.menu_item_sort_by_name_a_z;
         }
     }
+
+    public static String getDateByPattern(long timestamp, Context context, String pattern) {
+        DateFormat df = new SimpleDateFormat(pattern, context.getResources().getConfiguration().locale);
+        df.setTimeZone(TimeZone.getTimeZone(TimeZone.getDefault().getID()));
+
+        return df.format(timestamp);
+    }
+
+    public static void setThumbnail(OCFile file,
+                                    ImageView thumbnailView,
+                                    User user,
+                                    FileDataStorageManager storageManager,
+                                    List<ThumbnailsCacheManager.ThumbnailGenerationTask> asyncTasks,
+                                    boolean gridView,
+                                    Context context) {
+        setThumbnail(file,
+                     thumbnailView,
+                     user,
+                     storageManager,
+                     asyncTasks,
+                     gridView,
+                     context,
+                     null,
+                     null);
+    }
+
+    public static void setThumbnail(OCFile file,
+                                    ImageView thumbnailView,
+                                    User user,
+                                    FileDataStorageManager storageManager,
+                                    List<ThumbnailsCacheManager.ThumbnailGenerationTask> asyncTasks,
+                                    boolean gridView,
+                                    Context context,
+                                    LoaderImageView shimmerThumbnail,
+                                    AppPreferences preferences) {
+        if (file.isFolder()) {
+            stopShimmer(shimmerThumbnail, thumbnailView);
+            thumbnailView.setImageDrawable(MimeTypeUtil
+                                               .getFolderTypeIcon(file.isSharedWithMe() || file.isSharedWithSharee(),
+                                                                  file.isSharedViaLink(), file.isEncrypted(),
+                                                                  file.getMountType(), context));
+        } else {
+            if (file.getRemoteId() != null && file.isPreviewAvailable()) {
+                // Thumbnail in cache?
+                Bitmap thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache(
+                    ThumbnailsCacheManager.PREFIX_THUMBNAIL + file.getRemoteId()
+                                                                                );
+
+                if (thumbnail != null && !file.isUpdateThumbnailNeeded()) {
+                    stopShimmer(shimmerThumbnail, thumbnailView);
+
+                    if (MimeTypeUtil.isVideo(file)) {
+                        Bitmap withOverlay = ThumbnailsCacheManager.addVideoOverlay(thumbnail);
+                        thumbnailView.setImageBitmap(withOverlay);
+                    } else {
+                        if (gridView) {
+                            BitmapUtils.setRoundedBitmapForGridMode(thumbnail, thumbnailView);
+                        } else {
+                            BitmapUtils.setRoundedBitmap(thumbnail, thumbnailView);
+                        }
+                    }
+                } else {
+                    // generate new thumbnail
+                    if (ThumbnailsCacheManager.cancelPotentialThumbnailWork(file, thumbnailView)) {
+                        try {
+                            final ThumbnailsCacheManager.ThumbnailGenerationTask task =
+                                new ThumbnailsCacheManager.ThumbnailGenerationTask(thumbnailView,
+                                                                                   storageManager,
+                                                                                   user,
+                                                                                   asyncTasks,
+                                                                                   gridView);
+                            if (thumbnail == null) {
+                                Drawable drawable = MimeTypeUtil.getFileTypeIcon(file.getMimeType(),
+                                                                                 file.getFileName(),
+                                                                                 user,
+                                                                                 context);
+                                if (drawable == null) {
+                                    drawable = ResourcesCompat.getDrawable(context.getResources(),
+                                                                           R.drawable.file_image,
+                                                                           null);
+                                }
+                                int px = ThumbnailsCacheManager.getThumbnailDimension();
+                                thumbnail = BitmapUtils.drawableToBitmap(drawable, px, px);
+                            }
+                            final ThumbnailsCacheManager.AsyncThumbnailDrawable asyncDrawable =
+                                new ThumbnailsCacheManager.AsyncThumbnailDrawable(context.getResources(),
+                                                                                  thumbnail, task);
+
+                            if (shimmerThumbnail != null && shimmerThumbnail.getVisibility() == View.GONE) {
+                                if (gridView) {
+                                    configShimmerGridImageSize(shimmerThumbnail, preferences.getGridColumns());
+                                }
+                                startShimmer(shimmerThumbnail, thumbnailView);
+                            }
+
+                            task.setListener(new ThumbnailsCacheManager.ThumbnailGenerationTask.Listener() {
+                                @Override
+                                public void onSuccess() {
+                                    stopShimmer(shimmerThumbnail, thumbnailView);
+                                }
+
+                                @Override
+                                public void onError() {
+                                    stopShimmer(shimmerThumbnail, thumbnailView);
+                                }
+                            });
+
+                            thumbnailView.setImageDrawable(asyncDrawable);
+                            asyncTasks.add(task);
+                            task.execute(new ThumbnailsCacheManager.ThumbnailGenerationTaskObject(file,
+                                                                                                  file.getRemoteId()));
+                        } catch (IllegalArgumentException e) {
+                            Log_OC.d(TAG, "ThumbnailGenerationTask : " + e.getMessage());
+                        }
+                    }
+                }
+
+                if ("image/png".equalsIgnoreCase(file.getMimeType())) {
+                    thumbnailView.setBackgroundColor(context.getResources().getColor(R.color.bg_default));
+                }
+            } else {
+                stopShimmer(shimmerThumbnail, thumbnailView);
+                thumbnailView.setImageDrawable(MimeTypeUtil.getFileTypeIcon(file.getMimeType(),
+                                                                            file.getFileName(),
+                                                                            user,
+                                                                            context));
+            }
+        }
+    }
+
+    private static void startShimmer(LoaderImageView thumbnailShimmer, ImageView thumbnailView) {
+        thumbnailShimmer.setImageResource(R.drawable.background);
+        thumbnailShimmer.resetLoader();
+        thumbnailView.setVisibility(View.GONE);
+        thumbnailShimmer.setVisibility(View.VISIBLE);
+    }
+
+    private static void stopShimmer(@Nullable LoaderImageView thumbnailShimmer, ImageView thumbnailView) {
+        if (thumbnailShimmer != null) {
+            thumbnailShimmer.setVisibility(View.GONE);
+        }
+
+        thumbnailView.setVisibility(View.VISIBLE);
+    }
+
+    private static void configShimmerGridImageSize(LoaderImageView thumbnailShimmer, float gridColumns) {
+        FrameLayout.LayoutParams targetLayoutParams = (FrameLayout.LayoutParams) thumbnailShimmer.getLayoutParams();
+
+        try {
+            final Point screenSize = getScreenSize(thumbnailShimmer.getContext());
+            final int marginLeftAndRight = targetLayoutParams.leftMargin + targetLayoutParams.rightMargin;
+            final int size = Math.round(screenSize.x / gridColumns - marginLeftAndRight);
+
+            FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(size, size);
+            params.setMargins(targetLayoutParams.leftMargin,
+                              targetLayoutParams.topMargin,
+                              targetLayoutParams.rightMargin,
+                              targetLayoutParams.bottomMargin);
+            thumbnailShimmer.setLayoutParams(params);
+        } catch (Exception exception) {
+            Log_OC.e("ConfigShimmer", exception.getMessage());
+        }
+    }
+
+    private static Point getScreenSize(Context context) throws Exception {
+        final WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+        if (windowManager != null) {
+            final Point displaySize = new Point();
+            windowManager.getDefaultDisplay().getSize(displaySize);
+            return displaySize;
+        } else {
+            throw new Exception("WindowManager not found");
+        }
+    }
 }

+ 48 - 0
app/src/main/res/layout/gallery_header.xml

@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~
+  ~ Nextcloud Android client application
+  ~
+  ~ @author Tobias Kaminsky
+  ~ Copyright (C) 2022 Tobias Kaminsky
+  ~ Copyright (C) 2022 Nextcloud GmbH
+  ~
+  ~ This program is free software: you can redistribute it and/or modify
+  ~ it under the terms of the GNU Affero General Public License as published by
+  ~ the Free Software Foundation, either version 3 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  ~ GNU Affero General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU Affero General Public License
+  ~ along with this program. If not, see <https://www.gnu.org/licenses/>.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:paddingBottom="@dimen/alternate_half_padding"
+    android:paddingTop="@dimen/alternate_half_padding"
+    xmlns:tools="http://schemas.android.com/tools">
+
+    <TextView
+        android:id="@+id/month"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="@dimen/standard_margin"
+        android:layout_marginTop="@dimen/standard_margin"
+        android:layout_marginEnd="@dimen/standard_half_margin"
+        android:textAppearance="?android:attr/textAppearanceLarge"
+        android:textStyle="bold"
+        tools:text="July" />
+
+    <TextView
+        android:id="@+id/year"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/standard_margin"
+        android:textAppearance="?android:attr/textAppearanceLarge"
+        tools:text="2016" />
+</LinearLayout>

+ 76 - 0
app/src/test/java/com/owncloud/android/ui/adapter/GalleryAdapterTest.kt

@@ -0,0 +1,76 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2022 Tobias Kaminsky
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.ui.adapter
+
+import android.content.Context
+import com.nextcloud.client.account.User
+import com.nextcloud.client.preferences.AppPreferences
+import com.owncloud.android.datamodel.FileDataStorageManager
+import com.owncloud.android.datamodel.GalleryItems
+import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.ui.activity.ComponentsGetter
+import com.owncloud.android.ui.interfaces.OCFileListFragmentInterface
+import junit.framework.Assert.assertEquals
+import org.junit.Test
+import org.mockito.Mock
+
+class GalleryAdapterTest {
+    @Mock
+    lateinit var context: Context
+
+    @Mock
+    lateinit var user: User
+
+    @Mock
+    lateinit var ocFileListFragmentInterface: OCFileListFragmentInterface
+
+    @Mock
+    lateinit var preferences: AppPreferences
+
+    @Mock
+    lateinit var storageManager: FileDataStorageManager
+
+    @Mock
+    lateinit var transferServiceGetter: ComponentsGetter
+
+    @Test
+    fun testItemCount() {
+        val sut = GalleryAdapter(
+            context,
+            user,
+            ocFileListFragmentInterface,
+            preferences,
+            storageManager,
+            transferServiceGetter
+        )
+
+        val list = listOf(
+            GalleryItems(1649317247, listOf(OCFile("/1.md"), OCFile("/2.md"))),
+            GalleryItems(1649317247, listOf(OCFile("/1.md"), OCFile("/2.md")))
+        )
+
+        sut.addFiles(list)
+
+        assertEquals(4, sut.getFilesCount())
+    }
+}

+ 1 - 0
spotbugs-filter.xml

@@ -56,6 +56,7 @@
     <Bug pattern="ANDROID_EXTERNAL_FILE_ACCESS" />
     <Bug pattern="BAS_BLOATED_ASSIGNMENT_SCOPE" />
     <Bug pattern="IMC_IMMATURE_CLASS_BAD_SERIALVERSIONUID" />
+    <Bug pattern="EI_EXPOSE_REP" />
     <Bug pattern="EI_EXPOSE_REP2" />
 
     <!-- This is unmanageable for now due to large amount of interconnected static state -->