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 a17e690653e4..bb5817108a77 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 @@ -84,7 +84,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; @@ -102,7 +105,6 @@ public class OCFileListAdapter extends RecyclerView.Adapter mFiles = new ArrayList<>(); private final List mFilesAll = new ArrayList<>(); private final boolean hideItemOptions; private boolean gridView; @@ -137,6 +139,34 @@ public class OCFileListAdapter extends RecyclerView.Adapter differ = new AsyncListDiffer<>( + listUpdateCallback, + new AsyncDifferConfig.Builder<>(new OCFileDiffCallback()).build()); + public OCFileListAdapter( Activity activity, @NonNull User user, @@ -210,14 +240,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; @@ -226,12 +256,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); @@ -256,22 +319,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; @@ -323,10 +393,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 @@ -335,12 +405,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; } @@ -358,11 +428,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 @@ -371,8 +441,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; } @@ -389,7 +459,7 @@ public int getItemViewType(int position) { } public boolean isEmpty() { - return mFiles.isEmpty(); + return mFiles().isEmpty(); } @NonNull @@ -715,6 +785,7 @@ private void setVisibilityOfMoreOption(Object holder) { public void updateRecommendedFiles(@NonNull List value) { recommendedFiles.clear(); recommendedFiles.addAll(value); + headerDisplayed = shouldShowHeader(); notifyDataSetChanged(); } @@ -769,11 +840,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 { @@ -877,17 +948,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) { @@ -927,18 +998,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; } @@ -957,11 +1026,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 @@ -978,12 +1059,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 @@ -1060,7 +1139,7 @@ public void setShowShareAvatar(boolean bool) { @Override public int getFilesCount() { - return mFiles.size(); + return mFiles().size(); } @Override @@ -1076,6 +1155,7 @@ public void notifyItemChanged(@NonNull OCFile file) { @VisibleForTesting public void setCurrentDirectory(OCFile folder) { currentDirectory = folder; + headerDisplayed = shouldShowHeader(); } // payload only for local file indicator @@ -1105,69 +1185,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 61d8df3fff7a..4f757da15e60 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 @@ -1600,15 +1600,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() {