diff --git a/app/src/main/java/com/owncloud/android/datamodel/OCFileListAdapterDataProviderImpl.kt b/app/src/main/java/com/owncloud/android/datamodel/OCFileListAdapterDataProviderImpl.kt index 3f09d4d6e276..994695b13ffc 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/OCFileListAdapterDataProviderImpl.kt +++ b/app/src/main/java/com/owncloud/android/datamodel/OCFileListAdapterDataProviderImpl.kt @@ -8,7 +8,7 @@ package com.owncloud.android.datamodel import com.nextcloud.client.database.entity.FileEntity -import com.owncloud.android.ui.adapter.helper.OCFileListAdapterDataProvider +import com.owncloud.android.ui.adapter.filelist.OCFileListAdapterDataProvider @Suppress("ReturnCount") class OCFileListAdapterDataProviderImpl(private val storageManager: FileDataStorageManager) : diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java index 0d1fcfe9e0fd..4133be999632 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java @@ -42,6 +42,7 @@ import com.nextcloud.utils.extensions.ViewExtensionsKt; import com.nextcloud.utils.mdm.MDMConfig; import com.nextcloud.utils.text.DisplayTextFormatter; +import com.nextcloud.utils.thumbnail.ThumbnailGenerator; import com.nextcloud.utils.view.LocaleDirection; import com.owncloud.android.MainApp; import com.owncloud.android.R; @@ -62,9 +63,10 @@ import com.owncloud.android.ui.activity.ComponentsGetter; import com.owncloud.android.ui.activity.DrawerActivity; import com.owncloud.android.ui.activity.FileDisplayActivity; -import com.owncloud.android.ui.adapter.helper.AvatarShareesProvider; -import com.owncloud.android.ui.adapter.helper.OCFileListAdapterDataProvider; -import com.owncloud.android.ui.adapter.helper.OCFileListAdapterHelper; +import com.owncloud.android.ui.adapter.filelist.AvatarShareesProvider; +import com.owncloud.android.ui.adapter.filelist.OCFileListAdapterDataProvider; +import com.owncloud.android.ui.adapter.filelist.OCFileListAdapterHelper; +import com.owncloud.android.ui.adapter.filelist.OCFileListDiffer; import com.owncloud.android.ui.fragment.OCFileListFragment; import com.owncloud.android.ui.fragment.SearchType; import com.owncloud.android.ui.interfaces.OCFileListFragmentInterface; @@ -73,7 +75,6 @@ import com.owncloud.android.utils.FileSortOrder; import com.owncloud.android.utils.FileStorageUtils; import com.owncloud.android.utils.MimeTypeUtil; -import com.nextcloud.utils.thumbnail.ThumbnailGenerator; import com.owncloud.android.utils.theme.CapabilityUtils; import com.owncloud.android.utils.theme.ViewThemeUtils; @@ -107,7 +108,6 @@ public class OCFileListAdapter extends RecyclerView.Adapter mFiles = new ArrayList<>(); private final List mFilesAll = new ArrayList<>(); private final boolean hideItemOptions; private boolean gridView; @@ -119,6 +119,7 @@ public class OCFileListAdapter extends RecyclerView.Adapter filesToDelete = new ArrayList<>(); - for (OCFile file : mFiles) { + OCFile changedFile = null; + for (OCFile file : differ.getLatestFiles()) { if (file.getRemotePath().equals(remotePath)) { file.setFavorite(favorite); + changedFile = file; if (removeFromList) { filesToDelete.add(file); @@ -264,23 +268,27 @@ public void setFavoriteAttributeForItemID(String remotePath, boolean favorite, b } } - FileSortOrder sortOrder = preferences.getSortOrderByFolder(currentDirectory); - if (searchType == SearchType.SHARED_FILTER) { - mFiles.sort((o1, o2) -> Long.compare(o2.getFirstShareTimestamp(), o1.getFirstShareTimestamp())); - } else { - boolean foldersBeforeFiles = preferences.isSortFoldersBeforeFiles(); - boolean favoritesFirst = preferences.isSortFavoritesFirst(); - mFiles = sortOrder.sortCloudFiles(mFiles, foldersBeforeFiles, favoritesFirst); - } - + final OCFile updatedFile = changedFile; new Handler(Looper.getMainLooper()).post(() -> { - mFiles.removeAll(filesToDelete); - notifyDataSetChanged(); + List sortedFiles = differ.sortForCurrentView(new ArrayList<>(differ.getLatestFiles()), + searchType, + currentDirectory); + sortedFiles.removeAll(filesToDelete); + differ.submit(sortedFiles); + + if (updatedFile == null || removeFromList) { + return; + } + + int position = getItemPosition(updatedFile); + if (position != -1) { + notifyItemChanged(position); + } }); } public void refreshCommentsCount(String fileId) { - for (OCFile file : mFiles) { + for (OCFile file : getFiles()) { if (file.getRemoteId().equals(fileId)) { file.setUnreadCommentsCount(0); break; @@ -322,7 +330,7 @@ public void updateFileEncryptionById(String fileId, boolean encrypted) { @Override public long getItemId(int position) { - if (shouldShowHeader()) { + if (isHeaderDisplayed()) { if (position == 0) { return headerId; } @@ -332,10 +340,10 @@ public long getItemId(int position) { position--; } - if (position == mFiles.size()) { + if (position == getFiles().size()) { return footerId; - } if (position < mFiles.size()) { - return mFiles.get(position).getFileId(); + } if (position < getFiles().size()) { + return getFiles().get(position).getFileId(); } // fallback @@ -344,12 +352,12 @@ public long getItemId(int position) { @Override public int getItemCount() { - return mFiles.size() + (shouldShowHeader() ? 2 : 1); + return getFiles().size() + (isHeaderDisplayed() ? 2 : 1); } @Nullable public OCFile getItem(int position) { - if (mFiles == null || mFiles.isEmpty()) { + if (getFiles() == null || getFiles().isEmpty()) { return null; } @@ -359,7 +367,7 @@ public OCFile getItem(int position) { int newPosition = position; - if (shouldShowHeader()) { + if (isHeaderDisplayed()) { if (position == 0) { // Header position — no file here return null; @@ -367,21 +375,21 @@ public OCFile getItem(int position) { newPosition = position - 1; } - if (newPosition >= mFiles.size()) { + if (newPosition >= getFiles().size()) { return null; } - return mFiles.get(newPosition); + return getFiles().get(newPosition); } @Override public int getItemViewType(int position) { - if (shouldShowHeader() && position == 0) { + if (isHeaderDisplayed() && position == 0) { return VIEW_TYPE_HEADER; } - if (shouldShowHeader() && position == mFiles.size() + 1 || - (!shouldShowHeader() && position == mFiles.size())) { + if (isHeaderDisplayed() && position == getFiles().size() + 1 || + (!isHeaderDisplayed() && position == getFiles().size())) { return VIEW_TYPE_FOOTER; } @@ -398,7 +406,7 @@ public int getItemViewType(int position) { } public boolean isEmpty() { - return mFiles.isEmpty(); + return differ.getLatestFiles().isEmpty(); } @NonNull @@ -720,6 +728,7 @@ private void setVisibilityOfMoreOption(Object holder) { public void updateRecommendedFiles(@NonNull List value) { recommendedFiles.clear(); recommendedFiles.addAll(value); + differ.syncHeaderState(); notifyDataSetChanged(); } @@ -774,11 +783,11 @@ public void onViewAttachedToWindow(@NonNull RecyclerView.ViewHolder holder) { private String getFooterText() { int filesCount = 0; int foldersCount = 0; - int count = mFiles.size(); + int count = getFiles().size(); OCFile file; final boolean showHiddenFiles = preferences.isShowHiddenFilesEnabled(); for (int i = 0; i < count; i++) { - file = mFiles.get(i); + file = getFiles().get(i); if (file.isFolder()) { foldersCount++; } else { @@ -809,6 +818,10 @@ private String generateFooterText(int filesCount, int foldersCount) { return output; } + public boolean isHeaderDisplayed() { + return differ.isHeaderDisplayed(); + } + public boolean shouldShowHeader() { if (currentDirectory == null) { return false; @@ -882,17 +895,26 @@ public void swapDirectory( public void updateAdapter(List newFiles, OCFile directory) { Log_OC.d(TAG, "updating the adapter"); - mFiles.clear(); - mFiles.addAll(newFiles); - mFilesAll.clear(); - mFilesAll.addAll(mFiles); + mFilesAll.addAll(newFiles); - if (directory != null) { - currentDirectory = directory; - } + activity.runOnUiThread(() -> { + boolean isSameDirectoryRefresh = directory != null && directory.equals(listedDirectory); + listedDirectory = directory; + + if (directory != null) { + currentDirectory = directory; + } + + differ.updateHeader(); - activity.runOnUiThread(this::notifyDataSetChanged); + List files = new ArrayList<>(newFiles); + if (isSameDirectoryRefresh) { + differ.submitAndRebind(files); + } else { + differ.replace(files); + } + }); } public void prepareForSearchData(FileDataStorageManager storageManager, SearchType searchType) { @@ -932,18 +954,17 @@ public void setSortOrder(FileSortOrder newSortOrder) { sortOrder = newSortOrder; } - @SuppressLint("NotifyDataSetChanged") public void setSortOrder(@Nullable OCFile folder, @NonNull FileSortOrder sortOrder) { if (searchType == SearchType.FAVORITE_SEARCH) { - preferences.setSortOrder(FileSortOrder.Type.favoritesListView, sortOrder); + preferences.setSortOrder(FileSortOrder.Type.favoritesListView, sortOrder); } else { preferences.setSortOrder(folder, sortOrder); } boolean foldersBeforeFiles = preferences.isSortFoldersBeforeFiles(); boolean favoritesFirst = preferences.isSortFavoritesFirst(); - mFiles = sortOrder.sortCloudFiles(mFiles, foldersBeforeFiles, favoritesFirst); - notifyDataSetChanged(); + List files = new ArrayList<>(differ.getLatestFiles()); + differ.replace(sortOrder.sortCloudFiles(files, foldersBeforeFiles, favoritesFirst)); this.sortOrder = sortOrder; } @@ -962,11 +983,24 @@ public void clearCheckedItems() { } public void setFiles(List files) { - mFiles = files; + differ.submit(new ArrayList<>(files)); } public List getFiles() { - return mFiles; + return differ.getFiles(); + } + + public void replaceFile(@NonNull OCFile file) { + List newFiles = new ArrayList<>(differ.getLatestFiles()); + int index = newFiles.indexOf(file); + if (index == -1) { + Log_OC.d(TAG, "File cannot be found in adapter's files"); + return; + } + + newFiles.set(index, file); + differ.submit(newFiles); + notifyItemChanged(file); } @Nullable @@ -983,12 +1017,22 @@ public void insertFile(@Nullable OCFile file) { if (mFilesAll.contains(file)) return; mFilesAll.add(file); - mFiles.add(file); - int position = getItemPosition(file); - if (position != -1) { - notifyItemInserted(position); - } + List newFiles = new ArrayList<>(differ.getLatestFiles()); + newFiles.add(file); + differ.submit(newFiles); + } + + @Override + public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) { + super.onAttachedToRecyclerView(recyclerView); + differ.attach(recyclerView); + } + + @Override + public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) { + super.onDetachedFromRecyclerView(recyclerView); + differ.detach(); } @Override @@ -1065,7 +1109,7 @@ public void setShowShareAvatar(boolean bool) { @Override public int getFilesCount() { - return mFiles.size(); + return getFiles().size(); } @Override @@ -1073,20 +1117,25 @@ public void notifyItemChanged(@NonNull OCFile file) { if (shouldShowRecommendedFiles() && recommendedFilesAdapter != null && file.isRecommendedFile()) { final int position = recommendedFilesAdapter.getItemPosition(file); recommendedFilesAdapter.notifyItemChanged(position); - } else { - notifyItemChanged(getItemPosition(file)); + return; + } + + int position = getItemPosition(file); + if (position != -1) { + notifyItemChanged(position); } } @VisibleForTesting public void setCurrentDirectory(OCFile folder) { currentDirectory = folder; + differ.syncHeaderState(); } - // payload only for local file indicator @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List payloads) { - if (!payloads.isEmpty() && payloads.get(0) instanceof Integer iconId && + if (!payloads.isEmpty() && payloads.get(0) instanceof Integer iconId && + !payloads.contains(OCFileListDiffer.REBIND_PAYLOAD) && holder instanceof ListViewHolder listViewHolder) { listViewHolder.getLocalFileIndicator().setImageResource(iconId); listViewHolder.getLocalFileIndicator().setVisibility(View.VISIBLE); @@ -1110,69 +1159,35 @@ public void cleanup() { helper.cleanup(); } - @SuppressLint("NotifyDataSetChanged") public void removeAllFiles() { - mFiles.clear(); mFilesAll.clear(); - notifyDataSetChanged(); + differ.submit(new ArrayList<>()); } - @SuppressLint("NotifyDataSetChanged") public void removeFile(@NonNull OCFile file) { - int position = getItemPosition(file); - - mFiles.remove(file); mFilesAll.remove(file); - if (position != -1) { - notifyItemRemoved(position); - } else { - notifyDataSetChanged(); - } + List newFiles = new ArrayList<>(differ.getLatestFiles()); + newFiles.remove(file); + differ.submit(newFiles); } - @SuppressLint("NotifyDataSetChanged") public void updateFile(@NonNull OCFile updatedFile) { int allIndex = helper.indexOfSameRemoteFile(mFilesAll, updatedFile); if (allIndex != -1) { mFilesAll.set(allIndex, updatedFile); } - int oldIndex = helper.indexOfSameRemoteFile(mFiles, updatedFile); + List newFiles = new ArrayList<>(differ.getLatestFiles()); + int oldIndex = helper.indexOfSameRemoteFile(newFiles, updatedFile); if (oldIndex == -1) { return; } - long previousItemId = mFiles.get(oldIndex).getFileId(); - - mFiles.remove(oldIndex); - mFiles.add(updatedFile); - - FileSortOrder currentSortOrder = preferences.getSortOrderByFolder(currentDirectory); - if (searchType == SearchType.SHARED_FILTER) { - mFiles.sort((o1, o2) -> Long.compare(o2.getFirstShareTimestamp(), o1.getFirstShareTimestamp())); - } else { - boolean foldersBeforeFiles = preferences.isSortFoldersBeforeFiles(); - boolean favoritesFirst = preferences.isSortFavoritesFirst(); - mFiles = currentSortOrder.sortCloudFiles(mFiles, foldersBeforeFiles, favoritesFirst); - } - - int newIndex = mFiles.indexOf(updatedFile); - if (newIndex == -1) { - notifyDataSetChanged(); - return; - } - - int headerOffset = shouldShowHeader() ? 1 : 0; - int oldAdapterPos = oldIndex + headerOffset; - int newAdapterPos = newIndex + headerOffset; + newFiles.remove(oldIndex); + newFiles.add(updatedFile); - if (oldAdapterPos == newAdapterPos && previousItemId == updatedFile.getFileId()) { - notifyItemChanged(newAdapterPos); - } else { - notifyItemRemoved(oldAdapterPos); - notifyItemInserted(newAdapterPos); - } + differ.submit(differ.sortForCurrentView(newFiles, searchType, currentDirectory)); if (shouldShowRecommendedFiles() && recommendedFilesAdapter != null && updatedFile.isRecommendedFile()) { int pos = recommendedFilesAdapter.getItemPosition(updatedFile); diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/helper/AvatarShareesProvider.kt b/app/src/main/java/com/owncloud/android/ui/adapter/filelist/AvatarShareesProvider.kt similarity index 93% rename from app/src/main/java/com/owncloud/android/ui/adapter/helper/AvatarShareesProvider.kt rename to app/src/main/java/com/owncloud/android/ui/adapter/filelist/AvatarShareesProvider.kt index 8aaf99a6592b..25c6d2718f4a 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/helper/AvatarShareesProvider.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/filelist/AvatarShareesProvider.kt @@ -5,7 +5,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -package com.owncloud.android.ui.adapter.helper +package com.owncloud.android.ui.adapter.filelist import com.owncloud.android.datamodel.OCFile import com.owncloud.android.lib.resources.shares.ShareType diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/filelist/OCFileDiffCallback.kt b/app/src/main/java/com/owncloud/android/ui/adapter/filelist/OCFileDiffCallback.kt new file mode 100644 index 000000000000..16b421d3465c --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/adapter/filelist/OCFileDiffCallback.kt @@ -0,0 +1,51 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.adapter.filelist + +import androidx.recyclerview.widget.DiffUtil +import com.owncloud.android.datamodel.OCFile + +class OCFileDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: OCFile, newItem: OCFile): Boolean = + oldItem.fileId == newItem.fileId && oldItem.remotePath == newItem.remotePath + + override fun areContentsTheSame(oldItem: OCFile, newItem: OCFile): Boolean = hasSameMetadata(oldItem, newItem) && + hasSameSyncState(oldItem, newItem) && + hasSameSharing(oldItem, newItem) && + hasSameIndicators(oldItem, newItem) + + private fun hasSameMetadata(oldItem: OCFile, newItem: OCFile): Boolean = oldItem.fileName == newItem.fileName && + oldItem.mimeType == newItem.mimeType && + oldItem.isFolder == newItem.isFolder && + oldItem.fileLength == newItem.fileLength && + oldItem.modificationTimestamp == newItem.modificationTimestamp + + private fun hasSameSyncState(oldItem: OCFile, newItem: OCFile): Boolean = oldItem.etag == newItem.etag && + oldItem.etagInConflict == newItem.etagInConflict && + oldItem.storagePath == newItem.storagePath && + oldItem.isDown == newItem.isDown && + oldItem.isUpdateThumbnailNeeded == newItem.isUpdateThumbnailNeeded && + oldItem.isOfflineOperation == newItem.isOfflineOperation + + private fun hasSameSharing(oldItem: OCFile, newItem: OCFile): Boolean = oldItem.isShared == newItem.isShared && + oldItem.isSharedViaLink == newItem.isSharedViaLink && + oldItem.isSharedWithMe == newItem.isSharedWithMe && + oldItem.isSharedWithSharee == newItem.isSharedWithSharee && + oldItem.firstShareTimestamp == newItem.firstShareTimestamp && + oldItem.ownerId == newItem.ownerId && + oldItem.sharees == newItem.sharees + + private fun hasSameIndicators(oldItem: OCFile, newItem: OCFile): Boolean = + oldItem.isFavorite == newItem.isFavorite && + oldItem.isHidden == newItem.isHidden && + oldItem.isEncrypted == newItem.isEncrypted && + oldItem.isLocked == newItem.isLocked && + oldItem.isRecommendedFile == newItem.isRecommendedFile && + oldItem.unreadCommentsCount == newItem.unreadCommentsCount && + oldItem.tags == newItem.tags && + oldItem.linkedFileIdForLivePhoto == newItem.linkedFileIdForLivePhoto +} diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterDataProvider.kt b/app/src/main/java/com/owncloud/android/ui/adapter/filelist/OCFileListAdapterDataProvider.kt similarity index 90% rename from app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterDataProvider.kt rename to app/src/main/java/com/owncloud/android/ui/adapter/filelist/OCFileListAdapterDataProvider.kt index 191b7ce666e3..4ae74c333dbc 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterDataProvider.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/filelist/OCFileListAdapterDataProvider.kt @@ -5,7 +5,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -package com.owncloud.android.ui.adapter.helper +package com.owncloud.android.ui.adapter.filelist import com.nextcloud.client.database.entity.FileEntity import com.owncloud.android.datamodel.OCFile diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterHelper.kt b/app/src/main/java/com/owncloud/android/ui/adapter/filelist/OCFileListAdapterHelper.kt similarity index 99% rename from app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterHelper.kt rename to app/src/main/java/com/owncloud/android/ui/adapter/filelist/OCFileListAdapterHelper.kt index 84aaa80c90da..d096c0db5ec4 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterHelper.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/filelist/OCFileListAdapterHelper.kt @@ -5,7 +5,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -package com.owncloud.android.ui.adapter.helper +package com.owncloud.android.ui.adapter.filelist import com.nextcloud.client.database.entity.FileEntity import com.nextcloud.client.preferences.AppPreferences diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/filelist/OCFileListDiffer.kt b/app/src/main/java/com/owncloud/android/ui/adapter/filelist/OCFileListDiffer.kt new file mode 100644 index 000000000000..568948238254 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/adapter/filelist/OCFileListDiffer.kt @@ -0,0 +1,162 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.adapter.filelist + +import android.annotation.SuppressLint +import androidx.recyclerview.widget.AsyncDifferConfig +import androidx.recyclerview.widget.AsyncListDiffer +import androidx.recyclerview.widget.ListUpdateCallback +import androidx.recyclerview.widget.RecyclerView +import com.nextcloud.client.preferences.AppPreferences +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.ui.fragment.SearchType + +class OCFileListDiffer( + private val adapter: RecyclerView.Adapter<*>, + private val preferences: AppPreferences, + private val shouldShowHeader: () -> Boolean +) { + + companion object { + @JvmField + val REBIND_PAYLOAD = Any() + } + + var isHeaderDisplayed = false + private set + + var latestFiles: List = emptyList() + private set + + private var isRebindPending = false + private var isReplacing = false + private var recyclerView: RecyclerView? = null + + private val listUpdateCallback = object : ListUpdateCallback { + override fun onInserted(position: Int, count: Int) { + if (isReplacing) return + val adapterPosition = position + headerOffset() + keepTopVisibleWhile(adapterPosition) { adapter.notifyItemRangeInserted(adapterPosition, count) } + } + + override fun onRemoved(position: Int, count: Int) { + if (isReplacing) return + adapter.notifyItemRangeRemoved(position + headerOffset(), count) + } + + override fun onMoved(fromPosition: Int, toPosition: Int) { + if (isReplacing) return + val adapterToPosition = toPosition + headerOffset() + keepTopVisibleWhile(adapterToPosition) { + adapter.notifyItemMoved(fromPosition + headerOffset(), adapterToPosition) + } + } + + override fun onChanged(position: Int, count: Int, payload: Any?) { + if (isReplacing) return + adapter.notifyItemRangeChanged(position + headerOffset(), count, payload) + } + } + + private val differ = AsyncListDiffer( + listUpdateCallback, + AsyncDifferConfig.Builder(OCFileDiffCallback()).build() + ) + + val files: List + get() = differ.currentList + + fun submit(newFiles: List) { + latestFiles = newFiles + differ.submitList(newFiles) { onListCommitted() } + } + + fun submitAndRebind(newFiles: List) { + isRebindPending = true + submit(newFiles) + } + + @SuppressLint("NotifyDataSetChanged") + fun replace(newFiles: List) { + latestFiles = newFiles + isRebindPending = false + isReplacing = true + try { + differ.submitList(null) + differ.submitList(newFiles) + } finally { + isReplacing = false + } + adapter.notifyDataSetChanged() + } + + fun sortForCurrentView( + files: MutableList, + searchType: SearchType?, + currentDirectory: OCFile? + ): MutableList { + if (searchType == SearchType.SHARED_FILTER) { + files.sortByDescending { it.firstShareTimestamp } + return files + } + + val sortOrder = preferences.getSortOrderByFolder(currentDirectory) + return sortOrder.sortCloudFiles(files, preferences.isSortFoldersBeforeFiles, preferences.isSortFavoritesFirst) + } + + fun updateHeader() { + val shouldShowHeader = shouldShowHeader() + + if (shouldShowHeader == isHeaderDisplayed) { + if (shouldShowHeader) { + adapter.notifyItemChanged(0) + } + return + } + + isHeaderDisplayed = shouldShowHeader + + if (shouldShowHeader) { + keepTopVisibleWhile(0) { adapter.notifyItemInserted(0) } + } else { + adapter.notifyItemRemoved(0) + } + } + + fun syncHeaderState() { + isHeaderDisplayed = shouldShowHeader() + } + + fun headerOffset(): Int = if (isHeaderDisplayed) 1 else 0 + + fun attach(recyclerView: RecyclerView) { + this.recyclerView = recyclerView + } + + fun detach() { + recyclerView = null + } + + private fun keepTopVisibleWhile(adapterPosition: Int, notify: () -> Unit) { + val wasAtTop = adapterPosition == 0 && recyclerView?.canScrollVertically(-1) == false + notify() + if (wasAtTop) { + recyclerView?.scrollToPosition(0) + } + } + + private fun onListCommitted() { + adapter.notifyItemChanged(adapter.itemCount - 1) + + if (!isRebindPending) return + isRebindPending = false + + if (files.isNotEmpty()) { + adapter.notifyItemRangeChanged(headerOffset(), files.size, REBIND_PAYLOAD) + } + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileListLayoutManager.kt b/app/src/main/java/com/owncloud/android/ui/fragment/FileListLayoutManager.kt index c859dd460538..d33d4cd797c0 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileListLayoutManager.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileListLayoutManager.kt @@ -91,7 +91,7 @@ class FileListLayoutManager(private val fragment: OCFileListFragment, private va layoutManager = GridLayoutManager(context, fragment.columnsCount) layoutManager.spanSizeLookup = object : SpanSizeLookup() { override fun getSpanSize(position: Int): Int = if (position == fragment.adapter.itemCount - 1 || - (position == 0 && fragment.adapter.shouldShowHeader()) + (position == 0 && fragment.adapter.isHeaderDisplayed) ) { layoutManager.spanCount } else { diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java index 31feb5d8df8c..41f802839c49 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java @@ -1611,15 +1611,7 @@ public List getAdapterFiles() { } public void updateOCFile(@NonNull OCFile file) { - List mFiles = mAdapter.getFiles(); - int index = mFiles.indexOf(file); - if (index == -1) { - Log_OC.d(TAG, "File cannot be found in adapter's files"); - return; - } - - mFiles.set(index, file); - mAdapter.notifyItemChanged(file); + mAdapter.replaceFile(file); } private void updateLayout() { diff --git a/app/src/test/java/com/owncloud/android/ui/adapter/MockOCFileListAdapterDataProvider.kt b/app/src/test/java/com/owncloud/android/ui/adapter/MockOCFileListAdapterDataProvider.kt index a053c9e6649a..e6ba7cf0f122 100644 --- a/app/src/test/java/com/owncloud/android/ui/adapter/MockOCFileListAdapterDataProvider.kt +++ b/app/src/test/java/com/owncloud/android/ui/adapter/MockOCFileListAdapterDataProvider.kt @@ -9,7 +9,7 @@ package com.owncloud.android.ui.adapter import com.nextcloud.client.database.entity.FileEntity import com.owncloud.android.datamodel.OCFile -import com.owncloud.android.ui.adapter.helper.OCFileListAdapterDataProvider +import com.owncloud.android.ui.adapter.filelist.OCFileListAdapterDataProvider import com.owncloud.android.utils.MimeType @Suppress("LongParameterList", "MagicNumber") diff --git a/app/src/test/java/com/owncloud/android/ui/adapter/OCFileListAdapterHelperTest.kt b/app/src/test/java/com/owncloud/android/ui/adapter/OCFileListAdapterHelperTest.kt index edd0be902d65..0b4252c5daf4 100644 --- a/app/src/test/java/com/owncloud/android/ui/adapter/OCFileListAdapterHelperTest.kt +++ b/app/src/test/java/com/owncloud/android/ui/adapter/OCFileListAdapterHelperTest.kt @@ -11,7 +11,7 @@ import android.content.Context import com.nextcloud.client.preferences.AppPreferences import com.owncloud.android.MainApp import com.owncloud.android.datamodel.OCFile -import com.owncloud.android.ui.adapter.helper.OCFileListAdapterHelper +import com.owncloud.android.ui.adapter.filelist.OCFileListAdapterHelper import com.owncloud.android.utils.FileSortOrder import com.owncloud.android.utils.MimeType import io.mockk.every