From 2f061a4c28f0ba2e122abf0a29b23f5d179b4169 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 17 Sep 2026 12:13:15 +0200 Subject: [PATCH 1/4] perf(file-list): diff util Signed-off-by: alperozturk96 --- .../android/ui/adapter/OCFileDiffCallback.kt | 51 +++++ .../android/ui/adapter/OCFileListAdapter.java | 213 +++++++++++------- .../ui/fragment/OCFileListFragment.java | 10 +- 3 files changed, 186 insertions(+), 88 deletions(-) create mode 100644 app/src/main/java/com/owncloud/android/ui/adapter/OCFileDiffCallback.kt diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileDiffCallback.kt b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileDiffCallback.kt new file mode 100644 index 000000000000..a9e7678af11f --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/adapter/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 + +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/OCFileListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java index 0d1fcfe9e0fd..5f88e40fd233 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 @@ -89,7 +89,10 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import androidx.recyclerview.widget.AsyncDifferConfig; +import androidx.recyclerview.widget.AsyncListDiffer; import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.ListUpdateCallback; import androidx.recyclerview.widget.RecyclerView; import kotlin.Pair; import kotlin.Unit; @@ -107,7 +110,6 @@ public class OCFileListAdapter extends RecyclerView.Adapter mFiles = new ArrayList<>(); private final List mFilesAll = new ArrayList<>(); private final boolean hideItemOptions; private boolean gridView; @@ -144,6 +146,34 @@ public class OCFileListAdapter extends RecyclerView.Adapter differ = new AsyncListDiffer<>( + listUpdateCallback, + new AsyncDifferConfig.Builder<>(new OCFileDiffCallback()).build()); + public OCFileListAdapter( Activity activity, @NonNull User user, @@ -219,14 +249,14 @@ public void removeCheckedFile(@NonNull OCFile file) { @Override public void selectAll(boolean value) { if (value) { - ocFileListDelegate.addToCheckedFiles(mFiles); + ocFileListDelegate.addToCheckedFiles(mFiles()); } else { clearCheckedItems(); } } public int getItemPosition(@NonNull OCFile file) { - int position = mFiles.indexOf(file); + int position = mFiles().indexOf(file); if (shouldShowHeader()) { position = position + 1; @@ -235,12 +265,45 @@ public int getItemPosition(@NonNull OCFile file) { return position; } - @SuppressLint("NotifyDataSetChanged") + private List mFiles() { + return differ.getCurrentList(); + } + + private int headerOffset() { + return shouldShowHeader() ? 1 : 0; + } + + private void submitFiles(@NonNull List newFiles) { + differ.submitList(newFiles, () -> notifyItemChanged(getItemCount() - 1)); + } + + private void updateHeader() { + boolean shouldShowHeader = shouldShowHeader(); + + if (shouldShowHeader == headerDisplayed) { + if (shouldShowHeader) { + notifyItemChanged(0); + } + return; + } + + headerDisplayed = shouldShowHeader; + + if (shouldShowHeader) { + notifyItemInserted(0); + } else { + notifyItemRemoved(0); + } + } + public void setFavoriteAttributeForItemID(String remotePath, boolean favorite, boolean removeFromList) { List filesToDelete = new ArrayList<>(); - for (OCFile file : mFiles) { + List newFiles = new ArrayList<>(mFiles()); + OCFile changedFile = null; + for (OCFile file : newFiles) { if (file.getRemotePath().equals(remotePath)) { file.setFavorite(favorite); + changedFile = file; if (removeFromList) { filesToDelete.add(file); @@ -265,22 +328,29 @@ public void setFavoriteAttributeForItemID(String remotePath, boolean favorite, b } FileSortOrder sortOrder = preferences.getSortOrderByFolder(currentDirectory); + final List sortedFiles; if (searchType == SearchType.SHARED_FILTER) { - mFiles.sort((o1, o2) -> Long.compare(o2.getFirstShareTimestamp(), o1.getFirstShareTimestamp())); + newFiles.sort((o1, o2) -> Long.compare(o2.getFirstShareTimestamp(), o1.getFirstShareTimestamp())); + sortedFiles = newFiles; } else { boolean foldersBeforeFiles = preferences.isSortFoldersBeforeFiles(); boolean favoritesFirst = preferences.isSortFavoritesFirst(); - mFiles = sortOrder.sortCloudFiles(mFiles, foldersBeforeFiles, favoritesFirst); + sortedFiles = sortOrder.sortCloudFiles(newFiles, foldersBeforeFiles, favoritesFirst); } + final OCFile updatedFile = changedFile; new Handler(Looper.getMainLooper()).post(() -> { - mFiles.removeAll(filesToDelete); - notifyDataSetChanged(); + sortedFiles.removeAll(filesToDelete); + submitFiles(sortedFiles); + + if (updatedFile != null && !removeFromList) { + notifyItemChanged(getItemPosition(updatedFile)); + } }); } public void refreshCommentsCount(String fileId) { - for (OCFile file : mFiles) { + for (OCFile file : mFiles()) { if (file.getRemoteId().equals(fileId)) { file.setUnreadCommentsCount(0); break; @@ -332,10 +402,10 @@ public long getItemId(int position) { position--; } - if (position == mFiles.size()) { + if (position == mFiles().size()) { return footerId; - } if (position < mFiles.size()) { - return mFiles.get(position).getFileId(); + } if (position < mFiles().size()) { + return mFiles().get(position).getFileId(); } // fallback @@ -344,12 +414,12 @@ public long getItemId(int position) { @Override public int getItemCount() { - return mFiles.size() + (shouldShowHeader() ? 2 : 1); + return mFiles().size() + (shouldShowHeader() ? 2 : 1); } @Nullable public OCFile getItem(int position) { - if (mFiles == null || mFiles.isEmpty()) { + if (mFiles() == null || mFiles().isEmpty()) { return null; } @@ -367,11 +437,11 @@ public OCFile getItem(int position) { newPosition = position - 1; } - if (newPosition >= mFiles.size()) { + if (newPosition >= mFiles().size()) { return null; } - return mFiles.get(newPosition); + return mFiles().get(newPosition); } @Override @@ -380,8 +450,8 @@ public int getItemViewType(int position) { return VIEW_TYPE_HEADER; } - if (shouldShowHeader() && position == mFiles.size() + 1 || - (!shouldShowHeader() && position == mFiles.size())) { + if (shouldShowHeader() && position == mFiles().size() + 1 || + (!shouldShowHeader() && position == mFiles().size())) { return VIEW_TYPE_FOOTER; } @@ -398,7 +468,7 @@ public int getItemViewType(int position) { } public boolean isEmpty() { - return mFiles.isEmpty(); + return mFiles().isEmpty(); } @NonNull @@ -720,6 +790,7 @@ private void setVisibilityOfMoreOption(Object holder) { public void updateRecommendedFiles(@NonNull List value) { recommendedFiles.clear(); recommendedFiles.addAll(value); + headerDisplayed = shouldShowHeader(); notifyDataSetChanged(); } @@ -774,11 +845,11 @@ public void onViewAttachedToWindow(@NonNull RecyclerView.ViewHolder holder) { private String getFooterText() { int filesCount = 0; int foldersCount = 0; - int count = mFiles.size(); + int count = mFiles().size(); OCFile file; final boolean showHiddenFiles = preferences.isShowHiddenFilesEnabled(); for (int i = 0; i < count; i++) { - file = mFiles.get(i); + file = mFiles().get(i); if (file.isFolder()) { foldersCount++; } else { @@ -882,17 +953,17 @@ 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(() -> { + if (directory != null) { + currentDirectory = directory; + } - activity.runOnUiThread(this::notifyDataSetChanged); + updateHeader(); + submitFiles(new ArrayList<>(newFiles)); + }); } public void prepareForSearchData(FileDataStorageManager storageManager, SearchType searchType) { @@ -932,18 +1003,16 @@ 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(); + submitFiles(sortOrder.sortCloudFiles(new ArrayList<>(mFiles()), foldersBeforeFiles, favoritesFirst)); this.sortOrder = sortOrder; } @@ -962,11 +1031,23 @@ public void clearCheckedItems() { } public void setFiles(List files) { - mFiles = files; + submitFiles(new ArrayList<>(files)); } public List getFiles() { - return mFiles; + return mFiles(); + } + + public void replaceFile(@NonNull OCFile file) { + List newFiles = new ArrayList<>(mFiles()); + 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); + submitFiles(newFiles); } @Nullable @@ -983,12 +1064,10 @@ 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<>(mFiles()); + newFiles.add(file); + submitFiles(newFiles); } @Override @@ -1065,7 +1144,7 @@ public void setShowShareAvatar(boolean bool) { @Override public int getFilesCount() { - return mFiles.size(); + return mFiles().size(); } @Override @@ -1081,6 +1160,7 @@ public void notifyItemChanged(@NonNull OCFile file) { @VisibleForTesting public void setCurrentDirectory(OCFile folder) { currentDirectory = folder; + headerDisplayed = shouldShowHeader(); } // payload only for local file indicator @@ -1110,69 +1190,44 @@ public void cleanup() { helper.cleanup(); } - @SuppressLint("NotifyDataSetChanged") public void removeAllFiles() { - mFiles.clear(); mFilesAll.clear(); - notifyDataSetChanged(); + submitFiles(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<>(mFiles()); + newFiles.remove(file); + submitFiles(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<>(mFiles()); + int oldIndex = helper.indexOfSameRemoteFile(newFiles, updatedFile); if (oldIndex == -1) { return; } - long previousItemId = mFiles.get(oldIndex).getFileId(); + newFiles.remove(oldIndex); + newFiles.add(updatedFile); - 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())); + newFiles.sort((o1, o2) -> Long.compare(o2.getFirstShareTimestamp(), o1.getFirstShareTimestamp())); } else { + FileSortOrder currentSortOrder = preferences.getSortOrderByFolder(currentDirectory); boolean foldersBeforeFiles = preferences.isSortFoldersBeforeFiles(); boolean favoritesFirst = preferences.isSortFavoritesFirst(); - mFiles = currentSortOrder.sortCloudFiles(mFiles, foldersBeforeFiles, favoritesFirst); - } - - int newIndex = mFiles.indexOf(updatedFile); - if (newIndex == -1) { - notifyDataSetChanged(); - return; + newFiles = currentSortOrder.sortCloudFiles(newFiles, foldersBeforeFiles, favoritesFirst); } - int headerOffset = shouldShowHeader() ? 1 : 0; - int oldAdapterPos = oldIndex + headerOffset; - int newAdapterPos = newIndex + headerOffset; - - if (oldAdapterPos == newAdapterPos && previousItemId == updatedFile.getFileId()) { - notifyItemChanged(newAdapterPos); - } else { - notifyItemRemoved(oldAdapterPos); - notifyItemInserted(newAdapterPos); - } + submitFiles(newFiles); if (shouldShowRecommendedFiles() && recommendedFilesAdapter != null && updatedFile.isRecommendedFile()) { int pos = recommendedFilesAdapter.getItemPosition(updatedFile); 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() { From 65e3560b0c79109cff3b81a603bd34fc3ed66de6 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 7 Oct 2026 13:37:55 +0200 Subject: [PATCH 2/4] rename helper package to filelist Signed-off-by: alperozturk96 --- .../android/datamodel/OCFileListAdapterDataProviderImpl.kt | 2 +- .../com/owncloud/android/ui/adapter/OCFileListAdapter.java | 6 +++--- .../adapter/{helper => filelist}/AvatarShareesProvider.kt | 2 +- .../{helper => filelist}/OCFileListAdapterDataProvider.kt | 2 +- .../adapter/{helper => filelist}/OCFileListAdapterHelper.kt | 2 +- .../android/ui/adapter/MockOCFileListAdapterDataProvider.kt | 2 +- .../android/ui/adapter/OCFileListAdapterHelperTest.kt | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) rename app/src/main/java/com/owncloud/android/ui/adapter/{helper => filelist}/AvatarShareesProvider.kt (93%) rename app/src/main/java/com/owncloud/android/ui/adapter/{helper => filelist}/OCFileListAdapterDataProvider.kt (90%) rename app/src/main/java/com/owncloud/android/ui/adapter/{helper => filelist}/OCFileListAdapterHelper.kt (99%) 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 5f88e40fd233..3425331d811d 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 @@ -62,9 +62,9 @@ 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.fragment.OCFileListFragment; import com.owncloud.android.ui.fragment.SearchType; import com.owncloud.android.ui.interfaces.OCFileListFragmentInterface; 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/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/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 From a886b686929c5abaa635ba6affd909c068a5727f Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 7 Oct 2026 13:44:13 +0200 Subject: [PATCH 3/4] use OCFileListDiffer Signed-off-by: alperozturk96 --- .../android/ui/adapter/OCFileListAdapter.java | 131 +++++------------- .../{ => filelist}/OCFileDiffCallback.kt | 2 +- .../ui/adapter/filelist/OCFileListDiffer.kt | 73 ++++++++++ 3 files changed, 110 insertions(+), 96 deletions(-) rename app/src/main/java/com/owncloud/android/ui/adapter/{ => filelist}/OCFileDiffCallback.kt (98%) create mode 100644 app/src/main/java/com/owncloud/android/ui/adapter/filelist/OCFileListDiffer.kt 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 3425331d811d..64b2e9b03705 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 @@ -65,6 +65,7 @@ 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; @@ -89,10 +90,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; -import androidx.recyclerview.widget.AsyncDifferConfig; -import androidx.recyclerview.widget.AsyncListDiffer; import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.ListUpdateCallback; import androidx.recyclerview.widget.RecyclerView; import kotlin.Pair; import kotlin.Unit; @@ -146,33 +144,7 @@ public class OCFileListAdapter extends RecyclerView.Adapter differ = new AsyncListDiffer<>( - listUpdateCallback, - new AsyncDifferConfig.Builder<>(new OCFileDiffCallback()).build()); + private final OCFileListDiffer differ = new OCFileListDiffer(this, this::shouldShowHeader); public OCFileListAdapter( Activity activity, @@ -249,14 +221,14 @@ public void removeCheckedFile(@NonNull OCFile file) { @Override public void selectAll(boolean value) { if (value) { - ocFileListDelegate.addToCheckedFiles(mFiles()); + ocFileListDelegate.addToCheckedFiles(getFiles()); } else { clearCheckedItems(); } } public int getItemPosition(@NonNull OCFile file) { - int position = mFiles().indexOf(file); + int position = getFiles().indexOf(file); if (shouldShowHeader()) { position = position + 1; @@ -265,40 +237,9 @@ public int getItemPosition(@NonNull OCFile file) { return position; } - private List mFiles() { - return differ.getCurrentList(); - } - - private int headerOffset() { - return shouldShowHeader() ? 1 : 0; - } - - private void submitFiles(@NonNull List newFiles) { - differ.submitList(newFiles, () -> notifyItemChanged(getItemCount() - 1)); - } - - private void updateHeader() { - boolean shouldShowHeader = shouldShowHeader(); - - if (shouldShowHeader == headerDisplayed) { - if (shouldShowHeader) { - notifyItemChanged(0); - } - return; - } - - headerDisplayed = shouldShowHeader; - - if (shouldShowHeader) { - notifyItemInserted(0); - } else { - notifyItemRemoved(0); - } - } - public void setFavoriteAttributeForItemID(String remotePath, boolean favorite, boolean removeFromList) { List filesToDelete = new ArrayList<>(); - List newFiles = new ArrayList<>(mFiles()); + List newFiles = new ArrayList<>(getFiles()); OCFile changedFile = null; for (OCFile file : newFiles) { if (file.getRemotePath().equals(remotePath)) { @@ -341,7 +282,7 @@ public void setFavoriteAttributeForItemID(String remotePath, boolean favorite, b final OCFile updatedFile = changedFile; new Handler(Looper.getMainLooper()).post(() -> { sortedFiles.removeAll(filesToDelete); - submitFiles(sortedFiles); + differ.submit(sortedFiles); if (updatedFile != null && !removeFromList) { notifyItemChanged(getItemPosition(updatedFile)); @@ -350,7 +291,7 @@ public void setFavoriteAttributeForItemID(String remotePath, boolean favorite, b } public void refreshCommentsCount(String fileId) { - for (OCFile file : mFiles()) { + for (OCFile file : getFiles()) { if (file.getRemoteId().equals(fileId)) { file.setUnreadCommentsCount(0); break; @@ -402,10 +343,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 @@ -414,12 +355,12 @@ public long getItemId(int position) { @Override public int getItemCount() { - return mFiles().size() + (shouldShowHeader() ? 2 : 1); + return getFiles().size() + (shouldShowHeader() ? 2 : 1); } @Nullable public OCFile getItem(int position) { - if (mFiles() == null || mFiles().isEmpty()) { + if (getFiles() == null || getFiles().isEmpty()) { return null; } @@ -437,11 +378,11 @@ 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 @@ -450,8 +391,8 @@ public int getItemViewType(int position) { return VIEW_TYPE_HEADER; } - if (shouldShowHeader() && position == mFiles().size() + 1 || - (!shouldShowHeader() && position == mFiles().size())) { + if (shouldShowHeader() && position == getFiles().size() + 1 || + (!shouldShowHeader() && position == getFiles().size())) { return VIEW_TYPE_FOOTER; } @@ -468,7 +409,7 @@ public int getItemViewType(int position) { } public boolean isEmpty() { - return mFiles().isEmpty(); + return getFiles().isEmpty(); } @NonNull @@ -790,7 +731,7 @@ private void setVisibilityOfMoreOption(Object holder) { public void updateRecommendedFiles(@NonNull List value) { recommendedFiles.clear(); recommendedFiles.addAll(value); - headerDisplayed = shouldShowHeader(); + differ.syncHeaderState(); notifyDataSetChanged(); } @@ -845,11 +786,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 { @@ -961,8 +902,8 @@ public void updateAdapter(List newFiles, OCFile directory) { currentDirectory = directory; } - updateHeader(); - submitFiles(new ArrayList<>(newFiles)); + differ.updateHeader(); + differ.submit(new ArrayList<>(newFiles)); }); } @@ -1012,7 +953,7 @@ public void setSortOrder(@Nullable OCFile folder, @NonNull FileSortOrder sortOrd boolean foldersBeforeFiles = preferences.isSortFoldersBeforeFiles(); boolean favoritesFirst = preferences.isSortFavoritesFirst(); - submitFiles(sortOrder.sortCloudFiles(new ArrayList<>(mFiles()), foldersBeforeFiles, favoritesFirst)); + differ.submit(sortOrder.sortCloudFiles(new ArrayList<>(getFiles()), foldersBeforeFiles, favoritesFirst)); this.sortOrder = sortOrder; } @@ -1031,15 +972,15 @@ public void clearCheckedItems() { } public void setFiles(List files) { - submitFiles(new ArrayList<>(files)); + differ.submit(new ArrayList<>(files)); } public List getFiles() { - return mFiles(); + return differ.getFiles(); } public void replaceFile(@NonNull OCFile file) { - List newFiles = new ArrayList<>(mFiles()); + List newFiles = new ArrayList<>(getFiles()); int index = newFiles.indexOf(file); if (index == -1) { Log_OC.d(TAG, "File cannot be found in adapter's files"); @@ -1047,7 +988,7 @@ public void replaceFile(@NonNull OCFile file) { } newFiles.set(index, file); - submitFiles(newFiles); + differ.submit(newFiles); } @Nullable @@ -1065,9 +1006,9 @@ public void insertFile(@Nullable OCFile file) { mFilesAll.add(file); - List newFiles = new ArrayList<>(mFiles()); + List newFiles = new ArrayList<>(getFiles()); newFiles.add(file); - submitFiles(newFiles); + differ.submit(newFiles); } @Override @@ -1144,7 +1085,7 @@ public void setShowShareAvatar(boolean bool) { @Override public int getFilesCount() { - return mFiles().size(); + return getFiles().size(); } @Override @@ -1160,7 +1101,7 @@ public void notifyItemChanged(@NonNull OCFile file) { @VisibleForTesting public void setCurrentDirectory(OCFile folder) { currentDirectory = folder; - headerDisplayed = shouldShowHeader(); + differ.syncHeaderState(); } // payload only for local file indicator @@ -1192,15 +1133,15 @@ public void cleanup() { public void removeAllFiles() { mFilesAll.clear(); - submitFiles(new ArrayList<>()); + differ.submit(new ArrayList<>()); } public void removeFile(@NonNull OCFile file) { mFilesAll.remove(file); - List newFiles = new ArrayList<>(mFiles()); + List newFiles = new ArrayList<>(getFiles()); newFiles.remove(file); - submitFiles(newFiles); + differ.submit(newFiles); } public void updateFile(@NonNull OCFile updatedFile) { @@ -1209,7 +1150,7 @@ public void updateFile(@NonNull OCFile updatedFile) { mFilesAll.set(allIndex, updatedFile); } - List newFiles = new ArrayList<>(mFiles()); + List newFiles = new ArrayList<>(getFiles()); int oldIndex = helper.indexOfSameRemoteFile(newFiles, updatedFile); if (oldIndex == -1) { return; @@ -1227,7 +1168,7 @@ public void updateFile(@NonNull OCFile updatedFile) { newFiles = currentSortOrder.sortCloudFiles(newFiles, foldersBeforeFiles, favoritesFirst); } - submitFiles(newFiles); + differ.submit(newFiles); if (shouldShowRecommendedFiles() && recommendedFilesAdapter != null && updatedFile.isRecommendedFile()) { int pos = recommendedFilesAdapter.getItemPosition(updatedFile); diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileDiffCallback.kt b/app/src/main/java/com/owncloud/android/ui/adapter/filelist/OCFileDiffCallback.kt similarity index 98% rename from app/src/main/java/com/owncloud/android/ui/adapter/OCFileDiffCallback.kt rename to app/src/main/java/com/owncloud/android/ui/adapter/filelist/OCFileDiffCallback.kt index a9e7678af11f..16b421d3465c 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileDiffCallback.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/filelist/OCFileDiffCallback.kt @@ -4,7 +4,7 @@ * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-License-Identifier: AGPL-3.0-or-later */ -package com.owncloud.android.ui.adapter +package com.owncloud.android.ui.adapter.filelist import androidx.recyclerview.widget.DiffUtil import com.owncloud.android.datamodel.OCFile 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..45da276330a6 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/adapter/filelist/OCFileListDiffer.kt @@ -0,0 +1,73 @@ +/* + * 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.AsyncDifferConfig +import androidx.recyclerview.widget.AsyncListDiffer +import androidx.recyclerview.widget.ListUpdateCallback +import androidx.recyclerview.widget.RecyclerView +import com.owncloud.android.datamodel.OCFile + +class OCFileListDiffer(private val adapter: RecyclerView.Adapter<*>, private val shouldShowHeader: () -> Boolean) { + + private var isHeaderDisplayed = false + + private val listUpdateCallback = object : ListUpdateCallback { + override fun onInserted(position: Int, count: Int) { + adapter.notifyItemRangeInserted(position + headerOffset(), count) + } + + override fun onRemoved(position: Int, count: Int) { + adapter.notifyItemRangeRemoved(position + headerOffset(), count) + } + + override fun onMoved(fromPosition: Int, toPosition: Int) { + adapter.notifyItemMoved(fromPosition + headerOffset(), toPosition + headerOffset()) + } + + override fun onChanged(position: Int, count: Int, payload: Any?) { + 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) { + differ.submitList(newFiles) { adapter.notifyItemChanged(adapter.itemCount - 1) } + } + + fun updateHeader() { + val shouldShowHeader = shouldShowHeader() + + if (shouldShowHeader == isHeaderDisplayed) { + if (shouldShowHeader) { + adapter.notifyItemChanged(0) + } + return + } + + isHeaderDisplayed = shouldShowHeader + + if (shouldShowHeader) { + adapter.notifyItemInserted(0) + } else { + adapter.notifyItemRemoved(0) + } + } + + fun syncHeaderState() { + isHeaderDisplayed = shouldShowHeader() + } + + private fun headerOffset(): Int = if (shouldShowHeader()) 1 else 0 +} From ede9591f72d17560b03de2de08c25c7535f72e38 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 7 Oct 2026 14:39:21 +0200 Subject: [PATCH 4/4] handle edge cases Signed-off-by: alperozturk96 --- .../android/ui/adapter/OCFileListAdapter.java | 115 ++++++++++-------- .../ui/adapter/filelist/OCFileListDiffer.kt | 103 ++++++++++++++-- .../ui/fragment/FileListLayoutManager.kt | 2 +- 3 files changed, 164 insertions(+), 56 deletions(-) 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 64b2e9b03705..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; @@ -74,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; @@ -119,6 +119,7 @@ public class OCFileListAdapter extends RecyclerView.Adapter filesToDelete = new ArrayList<>(); - List newFiles = new ArrayList<>(getFiles()); OCFile changedFile = null; - for (OCFile file : newFiles) { + for (OCFile file : differ.getLatestFiles()) { if (file.getRemotePath().equals(remotePath)) { file.setFavorite(favorite); changedFile = file; @@ -268,24 +268,21 @@ public void setFavoriteAttributeForItemID(String remotePath, boolean favorite, b } } - FileSortOrder sortOrder = preferences.getSortOrderByFolder(currentDirectory); - final List sortedFiles; - if (searchType == SearchType.SHARED_FILTER) { - newFiles.sort((o1, o2) -> Long.compare(o2.getFirstShareTimestamp(), o1.getFirstShareTimestamp())); - sortedFiles = newFiles; - } else { - boolean foldersBeforeFiles = preferences.isSortFoldersBeforeFiles(); - boolean favoritesFirst = preferences.isSortFavoritesFirst(); - sortedFiles = sortOrder.sortCloudFiles(newFiles, foldersBeforeFiles, favoritesFirst); - } - final OCFile updatedFile = changedFile; new Handler(Looper.getMainLooper()).post(() -> { + List sortedFiles = differ.sortForCurrentView(new ArrayList<>(differ.getLatestFiles()), + searchType, + currentDirectory); sortedFiles.removeAll(filesToDelete); differ.submit(sortedFiles); - if (updatedFile != null && !removeFromList) { - notifyItemChanged(getItemPosition(updatedFile)); + if (updatedFile == null || removeFromList) { + return; + } + + int position = getItemPosition(updatedFile); + if (position != -1) { + notifyItemChanged(position); } }); } @@ -333,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; } @@ -355,7 +352,7 @@ public long getItemId(int position) { @Override public int getItemCount() { - return getFiles().size() + (shouldShowHeader() ? 2 : 1); + return getFiles().size() + (isHeaderDisplayed() ? 2 : 1); } @Nullable @@ -370,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; @@ -387,12 +384,12 @@ public OCFile getItem(int position) { @Override public int getItemViewType(int position) { - if (shouldShowHeader() && position == 0) { + if (isHeaderDisplayed() && position == 0) { return VIEW_TYPE_HEADER; } - if (shouldShowHeader() && position == getFiles().size() + 1 || - (!shouldShowHeader() && position == getFiles().size())) { + if (isHeaderDisplayed() && position == getFiles().size() + 1 || + (!isHeaderDisplayed() && position == getFiles().size())) { return VIEW_TYPE_FOOTER; } @@ -409,7 +406,7 @@ public int getItemViewType(int position) { } public boolean isEmpty() { - return getFiles().isEmpty(); + return differ.getLatestFiles().isEmpty(); } @NonNull @@ -821,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; @@ -898,12 +899,21 @@ public void updateAdapter(List newFiles, OCFile directory) { mFilesAll.addAll(newFiles); activity.runOnUiThread(() -> { + boolean isSameDirectoryRefresh = directory != null && directory.equals(listedDirectory); + listedDirectory = directory; + if (directory != null) { currentDirectory = directory; } differ.updateHeader(); - differ.submit(new ArrayList<>(newFiles)); + + List files = new ArrayList<>(newFiles); + if (isSameDirectoryRefresh) { + differ.submitAndRebind(files); + } else { + differ.replace(files); + } }); } @@ -953,7 +963,8 @@ public void setSortOrder(@Nullable OCFile folder, @NonNull FileSortOrder sortOrd boolean foldersBeforeFiles = preferences.isSortFoldersBeforeFiles(); boolean favoritesFirst = preferences.isSortFavoritesFirst(); - differ.submit(sortOrder.sortCloudFiles(new ArrayList<>(getFiles()), foldersBeforeFiles, favoritesFirst)); + List files = new ArrayList<>(differ.getLatestFiles()); + differ.replace(sortOrder.sortCloudFiles(files, foldersBeforeFiles, favoritesFirst)); this.sortOrder = sortOrder; } @@ -980,7 +991,7 @@ public List getFiles() { } public void replaceFile(@NonNull OCFile file) { - List newFiles = new ArrayList<>(getFiles()); + 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"); @@ -989,6 +1000,7 @@ public void replaceFile(@NonNull OCFile file) { newFiles.set(index, file); differ.submit(newFiles); + notifyItemChanged(file); } @Nullable @@ -1006,11 +1018,23 @@ public void insertFile(@Nullable OCFile file) { mFilesAll.add(file); - List newFiles = new ArrayList<>(getFiles()); + 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 public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) { super.onViewRecycled(holder); @@ -1093,8 +1117,12 @@ 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); } } @@ -1104,10 +1132,10 @@ public void setCurrentDirectory(OCFile 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); @@ -1139,7 +1167,7 @@ public void removeAllFiles() { public void removeFile(@NonNull OCFile file) { mFilesAll.remove(file); - List newFiles = new ArrayList<>(getFiles()); + List newFiles = new ArrayList<>(differ.getLatestFiles()); newFiles.remove(file); differ.submit(newFiles); } @@ -1150,7 +1178,7 @@ public void updateFile(@NonNull OCFile updatedFile) { mFilesAll.set(allIndex, updatedFile); } - List newFiles = new ArrayList<>(getFiles()); + List newFiles = new ArrayList<>(differ.getLatestFiles()); int oldIndex = helper.indexOfSameRemoteFile(newFiles, updatedFile); if (oldIndex == -1) { return; @@ -1159,16 +1187,7 @@ public void updateFile(@NonNull OCFile updatedFile) { newFiles.remove(oldIndex); newFiles.add(updatedFile); - if (searchType == SearchType.SHARED_FILTER) { - newFiles.sort((o1, o2) -> Long.compare(o2.getFirstShareTimestamp(), o1.getFirstShareTimestamp())); - } else { - FileSortOrder currentSortOrder = preferences.getSortOrderByFolder(currentDirectory); - boolean foldersBeforeFiles = preferences.isSortFoldersBeforeFiles(); - boolean favoritesFirst = preferences.isSortFavoritesFirst(); - newFiles = currentSortOrder.sortCloudFiles(newFiles, foldersBeforeFiles, favoritesFirst); - } - - differ.submit(newFiles); + 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/filelist/OCFileListDiffer.kt b/app/src/main/java/com/owncloud/android/ui/adapter/filelist/OCFileListDiffer.kt index 45da276330a6..568948238254 100644 --- 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 @@ -6,30 +6,58 @@ */ 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 shouldShowHeader: () -> Boolean) { +class OCFileListDiffer( + private val adapter: RecyclerView.Adapter<*>, + private val preferences: AppPreferences, + private val shouldShowHeader: () -> Boolean +) { - private var isHeaderDisplayed = false + 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) { - adapter.notifyItemRangeInserted(position + headerOffset(), count) + 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) { - adapter.notifyItemMoved(fromPosition + headerOffset(), toPosition + headerOffset()) + 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) } } @@ -43,7 +71,41 @@ class OCFileListDiffer(private val adapter: RecyclerView.Adapter<*>, private val get() = differ.currentList fun submit(newFiles: List) { - differ.submitList(newFiles) { adapter.notifyItemChanged(adapter.itemCount - 1) } + 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() { @@ -59,7 +121,7 @@ class OCFileListDiffer(private val adapter: RecyclerView.Adapter<*>, private val isHeaderDisplayed = shouldShowHeader if (shouldShowHeader) { - adapter.notifyItemInserted(0) + keepTopVisibleWhile(0) { adapter.notifyItemInserted(0) } } else { adapter.notifyItemRemoved(0) } @@ -69,5 +131,32 @@ class OCFileListDiffer(private val adapter: RecyclerView.Adapter<*>, private val isHeaderDisplayed = shouldShowHeader() } - private fun headerOffset(): Int = if (shouldShowHeader()) 1 else 0 + 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 {