From d105cc148f41b6cf5f5aec5ca388d6f2fd449005 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Thu, 20 Nov 2025 13:45:44 +0100 Subject: [PATCH 01/32] feat: cache content of different tabs Signed-off-by: alperozturk --- .../nextcloud/client/database/dao/FileDao.kt | 23 +++++++++++ .../android/ui/adapter/OCFileListAdapter.java | 32 +++++++++++---- .../ui/fragment/OCFileListFragment.java | 9 ----- .../ui/fragment/OCFileListSearchTask.kt | 40 ++++++++++++++----- 4 files changed, 78 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt b/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt index 3356d69d0be5..dbbf7e493372 100644 --- a/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt +++ b/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt @@ -111,4 +111,27 @@ interface FileDao { """ ) fun searchFilesInFolder(parentId: Long, fileOwner: String, query: String): List + + @Query( + """ + SELECT * + FROM filelist + WHERE file_owner = :fileOwner + AND share_by_link = 1 + ORDER BY ${ProviderTableMeta.FILE_DEFAULT_SORT_ORDER} + """ + ) + suspend fun getSharedFiles(fileOwner: String): List + + @Query( + """ + SELECT * + FROM filelist + WHERE file_owner = :fileOwner + AND favorite = 1 + ORDER BY ${ProviderTableMeta.FILE_DEFAULT_SORT_ORDER} + """ + ) + suspend fun getFavoriteFiles(fileOwner: String): List + } 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 9aa3702d5b12..ce0d0f6e1204 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 @@ -1001,17 +1001,35 @@ public void setData(List objects, } } - // early exit - if (!objects.isEmpty() && mStorageManager != null) { - if (searchType == SearchType.SHARED_FILTER) { - parseShares(objects); - } else { - if (searchType != SearchType.GALLERY_SEARCH) { - parseVirtuals(objects, searchType); + boolean areObjectsOCFile = false; + for (Object item : objects) { + if (item instanceof OCFile) { + areObjectsOCFile = true; + break; + } + } + + if (areObjectsOCFile) { + List files = objects.stream() + .filter(OCFile.class::isInstance) + .map(OCFile.class::cast) + .toList(); + + mFiles.addAll(files); + } else { + if (!objects.isEmpty() && mStorageManager != null) { + if (searchType == SearchType.SHARED_FILTER) { + parseShares(objects); + } else { + if (searchType != SearchType.GALLERY_SEARCH) { + parseVirtuals(objects, searchType); + } } } } + + if (searchType == SearchType.GALLERY_SEARCH || searchType == SearchType.RECENTLY_MODIFIED_SEARCH) { mFiles = FileStorageUtils.sortOcFolderDescDateModifiedWithoutFavoritesFirst(mFiles); 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 524594e90ee8..07288264b7f0 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 @@ -1915,13 +1915,6 @@ protected void handleSearchEvent(SearchEvent event) { prepareCurrentSearch(event); searchFragment = true; - setEmptyListMessage(EmptyListState.LOADING); - mAdapter.setData(new ArrayList<>(), - NO_SEARCH, - mContainerActivity.getStorageManager(), - mFile, - true); - setFabVisible(false); Runnable switchViewsRunnable = () -> { @@ -1937,9 +1930,7 @@ protected void handleSearchEvent(SearchEvent event) { new Handler(Looper.getMainLooper()).post(switchViewsRunnable); final User currentUser = accountManager.getUser(); - final var remoteOperation = getSearchRemoteOperation(currentUser, event); - searchTask = new OCFileListSearchTask(mContainerActivity, this, remoteOperation, currentUser, event, SharedListFragment.TASK_TIMEOUT); searchTask.execute(); } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt index 20d545a60bbf..6ebba66b3ed8 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt @@ -16,6 +16,7 @@ import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.lib.common.operations.RemoteOperation import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.files.SearchRemoteOperation import com.owncloud.android.ui.events.SearchEvent import com.owncloud.android.utils.DisplayUtils import kotlinx.coroutines.Dispatchers @@ -53,21 +54,41 @@ class OCFileListSearchTask( val fragment = fragmentReference.get() ?: return val context = fragment.context ?: return - job = fragment.lifecycleScope.launch { - val result: RemoteOperationResult>? = withContext(Dispatchers.IO) { - try { - withTimeoutOrNull(taskTimeout) { - remoteOperation.execute(currentUser, context) - } ?: remoteOperation.executeNextcloudClient(currentUser, context) - } catch (e: Exception) { - Log_OC.e(TAG, "exception execute: ", e) + job = fragment.lifecycleScope.launch(Dispatchers.IO) { + val filesInDb = when (event.searchType) { + SearchRemoteOperation.SearchType.SHARED_FILTER -> { + fileDataStorageManager?.fileDao?.getSharedFiles(currentUser.accountName) + } + SearchRemoteOperation.SearchType.FAVORITE_SEARCH -> { + fileDataStorageManager?.fileDao?.getFavoriteFiles(currentUser.accountName) + } + else -> { null } + }?.map { fileDataStorageManager?.createFileInstance(it) } + + withContext(Dispatchers.Main) { + fragment.adapter.setData( + filesInDb, + fragment.currentSearchType, + fileDataStorageManager, + fragment.mFile, + true + ) + } + + val result: RemoteOperationResult>? = try { + withTimeoutOrNull(taskTimeout) { + remoteOperation.execute(currentUser, context) + } ?: remoteOperation.executeNextcloudClient(currentUser, context) + } catch (e: Exception) { + Log_OC.e(TAG, "exception execute: ", e) + null } withContext(Dispatchers.Main) { if (!fragment.isAdded || !fragment.searchFragment) { - Log_OC.e(TAG, "cannot fetch sharees fragment is not ready") + Log_OC.e(TAG, "cannot search, fragment is not ready") return@withContext } @@ -85,7 +106,6 @@ class OCFileListSearchTask( fragment.mFile, true ) - return@withContext } From 144d63952808e99687d4f222f31bd2e55dc8a023 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Thu, 20 Nov 2025 15:00:26 +0100 Subject: [PATCH 02/32] perf: implement diff util for adapter Signed-off-by: alperozturk --- .../extensions/OCFileListAdapterExtensions.kt | 124 +++++++ .../android/ui/adapter/OCFileListAdapter.java | 316 +++++++----------- .../ui/fragment/OCFileListSearchTask.kt | 16 +- 3 files changed, 252 insertions(+), 204 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/utils/extensions/OCFileListAdapterExtensions.kt diff --git a/app/src/main/java/com/nextcloud/utils/extensions/OCFileListAdapterExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/OCFileListAdapterExtensions.kt new file mode 100644 index 000000000000..795972062f67 --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/extensions/OCFileListAdapterExtensions.kt @@ -0,0 +1,124 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils.extensions + +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFolderMetadataFileV1 +import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFolderMetadataFile +import com.owncloud.android.db.ProviderMeta +import com.owncloud.android.lib.common.OwnCloudClientFactory +import com.owncloud.android.lib.resources.files.model.RemoteFile +import com.owncloud.android.lib.resources.shares.OCShare +import com.owncloud.android.ui.adapter.OCFileListAdapter +import com.owncloud.android.ui.adapter.OCShareToOCFileConverter +import com.owncloud.android.ui.fragment.SearchType +import com.owncloud.android.utils.FileStorageUtils +import android.content.ContentValues +import com.owncloud.android.operations.RefreshFolderOperation +import com.owncloud.android.utils.MimeTypeUtil +import com.owncloud.android.datamodel.VirtualFolderType + +fun OCFileListAdapter.parseAndSaveShares(data: List): List { + val shares = data.filterIsInstance() + val files: List = OCShareToOCFileConverter.buildOCFilesFromShares(shares) + for (file in files) { + FileStorageUtils.searchForLocalFileInDefaultPath(file, user.accountName) + } + mStorageManager.saveShares(shares) + return files +} + +fun OCFileListAdapter.parseCachedFiles(data: List): List { + return data.filterIsInstance() +} + +fun OCFileListAdapter.parseAndSaveVirtuals( + data: List, + searchType: SearchType +): List { + val (type, onlyMedia) = when (searchType) { + SearchType.FAVORITE_SEARCH -> VirtualFolderType.FAVORITE to false + SearchType.GALLERY_SEARCH -> VirtualFolderType.GALLERY to true + else -> VirtualFolderType.NONE to false + } + + val contentValuesList = mutableListOf() + val resultFiles = mutableListOf() + + for (obj in data) { + try { + val remoteFile = obj as RemoteFile + + var ocFile = FileStorageUtils.fillOCFile(remoteFile) + FileStorageUtils.searchForLocalFileInDefaultPath(ocFile, user.accountName) + + ocFile = mStorageManager.saveFileWithParent(ocFile, activity) + + val parent = mStorageManager.getFileById(ocFile.parentId) + if (parent != null && (ocFile.isEncrypted || parent.isEncrypted)) { + val metadata = RefreshFolderOperation.getDecryptedFolderMetadata( + true, + parent, + OwnCloudClientFactory.createOwnCloudClient(user.toPlatformAccount(), activity), + user, + activity + ) ?: throw IllegalStateException("metadata is null") + + when (metadata) { + is DecryptedFolderMetadataFileV1 -> + RefreshFolderOperation.updateFileNameForEncryptedFileV1( + mStorageManager, metadata, ocFile + ) + + is DecryptedFolderMetadataFile -> + RefreshFolderOperation.updateFileNameForEncryptedFile( + mStorageManager, metadata, ocFile + ) + } + + ocFile = mStorageManager.saveFileWithParent(ocFile, activity) + } + + if (searchType != SearchType.GALLERY_SEARCH && ocFile.isFolder) { + val op = RefreshFolderOperation( + ocFile, + System.currentTimeMillis(), + true, + false, + mStorageManager, + user, + activity + ) + op.execute(user, activity) + } + + val passesMediaFilter = + !onlyMedia || MimeTypeUtil.isImage(ocFile) || MimeTypeUtil.isVideo(ocFile) + + if (passesMediaFilter) { + if (resultFiles.isEmpty() || !resultFiles.contains(ocFile)) { + resultFiles.add(ocFile) + } + } + + val cv = ContentValues().apply { + put(ProviderMeta.ProviderTableMeta.VIRTUAL_TYPE, type.toString()) + put(ProviderMeta.ProviderTableMeta.VIRTUAL_OCFILE_ID, ocFile.fileId) + } + contentValuesList.add(cv) + + } catch (_: Exception) { + } + } + + // Save timestamp + virtual entries + preferences.setPhotoSearchTimestamp(System.currentTimeMillis()) + mStorageManager.saveVirtuals(contentValuesList) + + return resultFiles +} 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 ce0d0f6e1204..4c56d1243ba2 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 @@ -10,12 +10,9 @@ package com.owncloud.android.ui.adapter; import android.accounts.AccountManager; -import android.accounts.AuthenticatorException; -import android.accounts.OperationCanceledException; import android.annotation.SuppressLint; import android.app.Activity; import android.content.ComponentCallbacks; -import android.content.ContentValues; import android.content.res.ColorStateList; import android.content.res.Configuration; import android.content.res.Resources; @@ -42,6 +39,7 @@ import com.nextcloud.model.OfflineOperationType; import com.nextcloud.utils.LinkHelper; import com.nextcloud.utils.extensions.OCFileExtensionsKt; +import com.nextcloud.utils.extensions.OCFileListAdapterExtensionsKt; import com.nextcloud.utils.extensions.ViewExtensionsKt; import com.nextcloud.utils.mdm.MDMConfig; import com.owncloud.android.MainApp; @@ -55,21 +53,13 @@ import com.owncloud.android.datamodel.SyncedFolderProvider; import com.owncloud.android.datamodel.ThumbnailsCacheManager; import com.owncloud.android.datamodel.VirtualFolderType; -import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFolderMetadataFileV1; -import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFolderMetadataFile; -import com.owncloud.android.db.ProviderMeta; -import com.owncloud.android.lib.common.OwnCloudClientFactory; import com.owncloud.android.lib.common.accounts.AccountUtils; -import com.owncloud.android.lib.common.operations.RemoteOperation; import com.owncloud.android.lib.common.utils.Log_OC; -import com.owncloud.android.lib.resources.files.model.RemoteFile; -import com.owncloud.android.lib.resources.shares.OCShare; import com.owncloud.android.lib.resources.shares.ShareType; import com.owncloud.android.lib.resources.shares.ShareeUser; import com.owncloud.android.lib.resources.status.OCCapability; import com.owncloud.android.lib.resources.tags.Tag; import com.owncloud.android.operations.RefreshFolderOperation; -import com.owncloud.android.operations.RemoteOperationFailedException; import com.owncloud.android.ui.activity.ComponentsGetter; import com.owncloud.android.ui.activity.DrawerActivity; import com.owncloud.android.ui.activity.FileDisplayActivity; @@ -85,7 +75,6 @@ import com.owncloud.android.utils.theme.ViewThemeUtils; import java.io.File; -import java.io.IOException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; @@ -95,11 +84,14 @@ import java.util.Objects; import java.util.Set; import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.stream.Collectors; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import kotlin.Pair; @@ -114,16 +106,16 @@ public class OCFileListAdapter extends RecyclerView.Adapter mFiles = new ArrayList<>(); private final List mFilesAll = new ArrayList<>(); private final boolean hideItemOptions; private boolean gridView; public ArrayList listOfHiddenFiles = new ArrayList<>(); - private FileDataStorageManager mStorageManager; - private User user; + public FileDataStorageManager mStorageManager; + public User user; private final OCFileListFragmentInterface ocFileListFragmentInterface; private final boolean isRTL; @@ -148,6 +140,7 @@ public class OCFileListAdapter extends RecyclerView.Adapter recommendedFiles = new ArrayList<>(); private RecommendedFilesAdapter recommendedFilesAdapter; + private final ExecutorService executorService = Executors.newSingleThreadExecutor(); public OCFileListAdapter( Activity activity, @@ -896,44 +889,49 @@ public void swapDirectory( } if (mStorageManager != null) { - // TODO refactor filtering mechanism for mFiles - mFiles = mStorageManager.getFolderContent(directory, onlyOnDevice); + // Create a new local list to avoid concurrent modification + List files = mStorageManager.getFolderContent(directory, onlyOnDevice); + if (!preferences.isShowHiddenFilesEnabled()) { - mFiles = OCFileExtensionsKt.filterHiddenFiles(mFiles); + files = OCFileExtensionsKt.filterHiddenFiles(files); } if (!limitToMimeType.isEmpty()) { - mFiles = OCFileExtensionsKt.filterByMimeType(mFiles, limitToMimeType); + files = OCFileExtensionsKt.filterByMimeType(files, limitToMimeType); } if (OCFile.ROOT_PATH.equals(directory.getRemotePath()) && MainApp.isOnlyPersonFiles()) { - mFiles = OCFileExtensionsKt.limitToPersonalFiles(mFiles, userId); + files = OCFileExtensionsKt.limitToPersonalFiles(files, userId); } // TODO refactor add DrawerState instead of using static menuItemId if (DrawerActivity.menuItemId == R.id.nav_shared && currentDirectory != null) { - mFiles = updatedStorageManager.filter(currentDirectory, OCFileFilterType.Shared); + files = updatedStorageManager.filter(currentDirectory, OCFileFilterType.Shared); } if (DrawerActivity.menuItemId == R.id.nav_favorites && currentDirectory != null) { - mFiles = updatedStorageManager.filter(currentDirectory, OCFileFilterType.Favorite); + files = updatedStorageManager.filter(currentDirectory, OCFileFilterType.Favorite); } // Filter out temp files from the list to prevent duplication - mFiles = OCFileExtensionsKt.filterTempFilter(mFiles); - - mFiles = OCFileExtensionsKt.filterFilenames(mFiles); + files = OCFileExtensionsKt.filterTempFilter(files); + files = OCFileExtensionsKt.filterFilenames(files); sortOrder = preferences.getSortOrderByFolder(directory); boolean foldersBeforeFiles = preferences.isSortFoldersBeforeFiles(); boolean favoritesFirst = preferences.isSortFavoritesFirst(); - mFiles = sortOrder.sortCloudFiles(mFiles, foldersBeforeFiles, favoritesFirst); + files = sortOrder.sortCloudFiles(files, foldersBeforeFiles, favoritesFirst); + + // Create new list for mFiles to avoid sharing references + mFiles = new ArrayList<>(files); + prepareListOfHiddenFiles(); mergeOCFilesForLivePhoto(); + mFilesAll.clear(); addOfflineOperations(directory.getFileId()); - mFilesAll.addAll(mFiles); + currentDirectory = directory; } else { - mFiles.clear(); + mFiles = new ArrayList<>(); // Create new instance instead of clear mFilesAll.clear(); } @@ -971,205 +969,128 @@ private void addOfflineOperations(long fileId) { mFilesAll.addAll(newFiles); } - public void setData(List objects, + public synchronized void setData(List objects, SearchType searchType, FileDataStorageManager storageManager, @Nullable OCFile folder, boolean clear) { - if (storageManager != null && mStorageManager == null) { - mStorageManager = storageManager; - ocFileListDelegate.setShowShareAvatar(true); - } + executorService.execute(() -> { + if (storageManager != null && mStorageManager == null) { + mStorageManager = storageManager; + ocFileListDelegate.setShowShareAvatar(true); + } + if (mStorageManager == null) { + mStorageManager = new FileDataStorageManager(user, activity.getContentResolver()); + } - if (mStorageManager == null) { - mStorageManager = new FileDataStorageManager(user, activity.getContentResolver()); - } + List newList = new ArrayList<>(); - if (clear) { - mFiles.clear(); - preferences.setPhotoSearchTimestamp(0); + if (clear) { + preferences.setPhotoSearchTimestamp(0); - VirtualFolderType type = switch (searchType) { - case FAVORITE_SEARCH -> VirtualFolderType.FAVORITE; - case GALLERY_SEARCH -> VirtualFolderType.GALLERY; - default -> VirtualFolderType.NONE; - }; + VirtualFolderType type = switch (searchType) { + case FAVORITE_SEARCH -> VirtualFolderType.FAVORITE; + case GALLERY_SEARCH -> VirtualFolderType.GALLERY; + default -> VirtualFolderType.NONE; + }; - if (type != VirtualFolderType.GALLERY) { - mStorageManager.deleteVirtuals(type); + if (type != VirtualFolderType.GALLERY) { + mStorageManager.deleteVirtuals(type); + } } - } - boolean areObjectsOCFile = false; - for (Object item : objects) { - if (item instanceof OCFile) { - areObjectsOCFile = true; - break; + boolean containsOCFiles = false; + for (Object item : objects) { + if (item instanceof OCFile) { + containsOCFiles = true; + break; + } } - } - if (areObjectsOCFile) { - List files = objects.stream() - .filter(OCFile.class::isInstance) - .map(OCFile.class::cast) - .toList(); - - mFiles.addAll(files); - } else { - if (!objects.isEmpty() && mStorageManager != null) { + if (containsOCFiles) { + newList = OCFileListAdapterExtensionsKt.parseCachedFiles(this, objects); + } else if (!objects.isEmpty()) { if (searchType == SearchType.SHARED_FILTER) { - parseShares(objects); + newList = OCFileListAdapterExtensionsKt.parseAndSaveShares(this, objects); } else { - if (searchType != SearchType.GALLERY_SEARCH) { - parseVirtuals(objects, searchType); - } + newList = OCFileListAdapterExtensionsKt.parseAndSaveVirtuals(this, objects, searchType); } } - } + if (searchType == SearchType.GALLERY_SEARCH || + searchType == SearchType.RECENTLY_MODIFIED_SEARCH) { + newList = FileStorageUtils.sortOcFolderDescDateModifiedWithoutFavoritesFirst(newList); + } else if (searchType != SearchType.SHARED_FILTER) { + boolean foldersBeforeFiles = preferences.isSortFoldersBeforeFiles(); + boolean favoritesFirst = preferences.isSortFavoritesFirst(); - - if (searchType == SearchType.GALLERY_SEARCH || - searchType == SearchType.RECENTLY_MODIFIED_SEARCH) { - mFiles = FileStorageUtils.sortOcFolderDescDateModifiedWithoutFavoritesFirst(mFiles); - } else if (searchType != SearchType.SHARED_FILTER) { - boolean foldersBeforeFiles = preferences.isSortFoldersBeforeFiles(); - boolean favoritesFirst = preferences.isSortFavoritesFirst(); - - if (searchType == SearchType.FAVORITE_SEARCH) { - sortOrder = preferences.getSortOrderByType(FileSortOrder.Type.favoritesListView); - } else { - sortOrder = preferences.getSortOrderByFolder(folder); + if (searchType == SearchType.FAVORITE_SEARCH) { + sortOrder = preferences.getSortOrderByType(FileSortOrder.Type.favoritesListView); + } else { + sortOrder = preferences.getSortOrderByFolder(folder); + } + newList = sortOrder.sortCloudFiles(newList, foldersBeforeFiles, favoritesFirst); } - mFiles = sortOrder.sortCloudFiles(mFiles, foldersBeforeFiles, favoritesFirst); - } - - - mFilesAll.clear(); - mFilesAll.addAll(mFiles); - - new Handler(Looper.getMainLooper()).post(this::notifyDataSetChanged); + List finalNewList = newList; + new Handler(Looper.getMainLooper()).post(() -> updateData(finalNewList)); + }); } - private void parseShares(List objects) { - List shares = new ArrayList<>(); - - for (Object shareObject : objects) { - // check type before cast as of long running data fetch it is possible that old result is filled - if (shareObject instanceof OCShare ocShare) { - shares.add(ocShare); - } - } - - // create partial OCFile from OCShares - List files = OCShareToOCFileConverter.buildOCFilesFromShares(shares); - - // set localPath of individual files iff present on device - for (OCFile file : files) { - FileStorageUtils.searchForLocalFileInDefaultPath(file, user.getAccountName()); + public void updateData(List newList) { + if (mFiles.isEmpty() && newList.isEmpty()) { + return; } - - mFiles.clear(); - mFiles.addAll(files); - mStorageManager.saveShares(shares); - } - - private void parseVirtuals(List objects, SearchType searchType) { - VirtualFolderType type; - boolean onlyMedia = false; - - switch (searchType) { - case FAVORITE_SEARCH: - type = VirtualFolderType.FAVORITE; - break; - case GALLERY_SEARCH: - type = VirtualFolderType.GALLERY; - onlyMedia = true; - break; - default: - type = VirtualFolderType.NONE; - break; + if (mFiles == newList) { + return; } - List contentValues = new ArrayList<>(); - - for (Object remoteFile : objects) { - OCFile ocFile = FileStorageUtils.fillOCFile((RemoteFile) remoteFile); - FileStorageUtils.searchForLocalFileInDefaultPath(ocFile, user.getAccountName()); - - try { - ocFile = mStorageManager.saveFileWithParent(ocFile, activity); - - OCFile parentFolder = mStorageManager.getFileById(ocFile.getParentId()); - if (parentFolder != null && (ocFile.isEncrypted() || parentFolder.isEncrypted())) { - Object object = RefreshFolderOperation.getDecryptedFolderMetadata( - true, - parentFolder, - OwnCloudClientFactory.createOwnCloudClient(user.toPlatformAccount(), activity), - user, - activity); - - if (object == null) { - throw new IllegalStateException("metadata is null!"); - } - - if (object instanceof DecryptedFolderMetadataFileV1) { - // update ocFile - RefreshFolderOperation.updateFileNameForEncryptedFileV1(mStorageManager, - (DecryptedFolderMetadataFileV1) object, - ocFile); - } else { - // update ocFile - RefreshFolderOperation.updateFileNameForEncryptedFile(mStorageManager, - (DecryptedFolderMetadataFile) object, - ocFile); - } - - ocFile = mStorageManager.saveFileWithParent(ocFile, activity); - } + DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffUtil.Callback() { + @Override + public int getOldListSize() { + return mFiles.size(); + } - if (SearchType.GALLERY_SEARCH != searchType) { - // also sync folder content - if (ocFile.isFolder()) { - long currentSyncTime = System.currentTimeMillis(); - RemoteOperation refreshFolderOperation = new RefreshFolderOperation(ocFile, - currentSyncTime, - true, - false, - mStorageManager, - user, - activity); - refreshFolderOperation.execute(user, activity); - } - } + @Override + public int getNewListSize() { + return newList.size(); + } - if (!onlyMedia || MimeTypeUtil.isImage(ocFile) || MimeTypeUtil.isVideo(ocFile)) { - //handling duplicates for favorites section - if (mFiles.isEmpty() || !mFiles.contains(ocFile)) { - mFiles.add(ocFile); - } - } + @Override + public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { + if (oldItemPosition >= mFiles.size() || newItemPosition >= newList.size()) return false; + return mFiles.get(oldItemPosition).equals(newList.get(newItemPosition)); + } - ContentValues cv = new ContentValues(); - cv.put(ProviderMeta.ProviderTableMeta.VIRTUAL_TYPE, type.toString()); - cv.put(ProviderMeta.ProviderTableMeta.VIRTUAL_OCFILE_ID, ocFile.getFileId()); - - contentValues.add(cv); - } catch ( - RemoteOperationFailedException | - OperationCanceledException | - AuthenticatorException | - IOException | - AccountUtils.AccountNotFoundException | - IllegalStateException e) { - Log_OC.e(TAG, "Error saving file with parent" + e.getMessage(), e); + @Override + public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { + if (oldItemPosition >= mFiles.size() || newItemPosition >= newList.size()) return false; + + OCFile oldFile = mFiles.get(oldItemPosition); + OCFile newFile = newList.get(newItemPosition); + + return oldFile.getModificationTimestamp() == newFile.getModificationTimestamp() && + oldFile.getFileLength() == newFile.getFileLength() && + Objects.equals(oldFile.getFileName(), newFile.getFileName()) && + oldFile.isFavorite() == newFile.isFavorite() && + oldFile.isEncrypted() == newFile.isEncrypted() && + oldFile.isSharedWithMe() == newFile.isSharedWithMe() && + oldFile.isSharedWithSharee() == newFile.isSharedWithSharee() && + oldFile.isSharedViaLink() == newFile.isSharedViaLink() && + oldFile.isLocked() == newFile.isLocked() && + oldFile.getUnreadCommentsCount() == newFile.getUnreadCommentsCount() && + Objects.equals(oldFile.getEtag(), newFile.getEtag()) && + Objects.equals(oldFile.getLinkedFileIdForLivePhoto(), newFile.getLinkedFileIdForLivePhoto()) && + oldFile.getTags().equals(newFile.getTags()) && + oldFile.isOfflineOperation() == newFile.isOfflineOperation(); } - } + }); + + mFiles.clear(); + mFiles.addAll(newList); - preferences.setPhotoSearchTimestamp(System.currentTimeMillis()); - mStorageManager.saveVirtuals(contentValues); + diffResult.dispatchUpdatesTo(this); } @SuppressLint("NotifyDataSetChanged") @@ -1312,5 +1233,6 @@ public void setCurrentDirectory(OCFile folder) { public void cleanup() { ocFileListDelegate.cleanup(); + executorService.shutdown(); } } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt index 6ebba66b3ed8..b79b14946f56 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt @@ -68,13 +68,15 @@ class OCFileListSearchTask( }?.map { fileDataStorageManager?.createFileInstance(it) } withContext(Dispatchers.Main) { - fragment.adapter.setData( - filesInDb, - fragment.currentSearchType, - fileDataStorageManager, - fragment.mFile, - true - ) + if (fragment.isAdded && fragment.searchFragment) { + fragment.adapter.setData( + filesInDb, + fragment.currentSearchType, + fileDataStorageManager, + fragment.mFile, + false + ) + } } val result: RemoteOperationResult>? = try { From 2d6bf25c0de74f9f83db40ebeab092b08cf87eab Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 21 Nov 2025 09:11:19 +0100 Subject: [PATCH 03/32] offload jobs from adapter Signed-off-by: alperozturk --- .../ui/fragment/SharedListFragmentIT.kt | 24 +- .../extensions/OCFileListAdapterExtensions.kt | 124 -------- .../android/ui/adapter/OCFileListAdapter.java | 98 ++----- .../ui/fragment/OCFileListFragment.java | 2 +- .../ui/fragment/OCFileListSearchTask.kt | 272 +++++++++++++++--- 5 files changed, 278 insertions(+), 242 deletions(-) delete mode 100644 app/src/main/java/com/nextcloud/utils/extensions/OCFileListAdapterExtensions.kt diff --git a/app/src/androidTest/java/com/owncloud/android/ui/fragment/SharedListFragmentIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/fragment/SharedListFragmentIT.kt index 9eabce775330..6cae3fd36282 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/fragment/SharedListFragmentIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/fragment/SharedListFragmentIT.kt @@ -21,8 +21,13 @@ import com.owncloud.android.AbstractIT import com.owncloud.android.datamodel.OCFile import com.owncloud.android.lib.resources.shares.OCShare import com.owncloud.android.lib.resources.shares.ShareType +import com.owncloud.android.ui.adapter.OCShareToOCFileConverter import com.owncloud.android.utils.EspressoIdlingResource +import com.owncloud.android.utils.FileStorageUtils import com.owncloud.android.utils.ScreenshotTest +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext import org.junit.After import org.junit.Before import org.junit.Rule @@ -165,11 +170,14 @@ internal class SharedListFragmentIT : AbstractIT() { fragment.isLoading = false fragment.mEmptyListContainer?.visibility = View.GONE - fragment.adapter.setData( - shares, + + val newList = runBlocking { + parseAndSaveShares(shares) + } + fragment.adapter.setSearchData( + newList, SearchType.SHARED_FILTER, storageManager, - null, true ) @@ -182,4 +190,14 @@ internal class SharedListFragmentIT : AbstractIT() { } } } + + private suspend fun parseAndSaveShares(data: List): List = withContext(Dispatchers.IO) { + val shares = data.filterIsInstance() + val files: List = OCShareToOCFileConverter.buildOCFilesFromShares(shares) + for (file in files) { + FileStorageUtils.searchForLocalFileInDefaultPath(file, user.accountName) + } + fileDataStorageManager?.saveShares(shares) + return@withContext files + } } diff --git a/app/src/main/java/com/nextcloud/utils/extensions/OCFileListAdapterExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/OCFileListAdapterExtensions.kt deleted file mode 100644 index 795972062f67..000000000000 --- a/app/src/main/java/com/nextcloud/utils/extensions/OCFileListAdapterExtensions.kt +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2025 Alper Ozturk - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -package com.nextcloud.utils.extensions - -import com.owncloud.android.datamodel.OCFile -import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFolderMetadataFileV1 -import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFolderMetadataFile -import com.owncloud.android.db.ProviderMeta -import com.owncloud.android.lib.common.OwnCloudClientFactory -import com.owncloud.android.lib.resources.files.model.RemoteFile -import com.owncloud.android.lib.resources.shares.OCShare -import com.owncloud.android.ui.adapter.OCFileListAdapter -import com.owncloud.android.ui.adapter.OCShareToOCFileConverter -import com.owncloud.android.ui.fragment.SearchType -import com.owncloud.android.utils.FileStorageUtils -import android.content.ContentValues -import com.owncloud.android.operations.RefreshFolderOperation -import com.owncloud.android.utils.MimeTypeUtil -import com.owncloud.android.datamodel.VirtualFolderType - -fun OCFileListAdapter.parseAndSaveShares(data: List): List { - val shares = data.filterIsInstance() - val files: List = OCShareToOCFileConverter.buildOCFilesFromShares(shares) - for (file in files) { - FileStorageUtils.searchForLocalFileInDefaultPath(file, user.accountName) - } - mStorageManager.saveShares(shares) - return files -} - -fun OCFileListAdapter.parseCachedFiles(data: List): List { - return data.filterIsInstance() -} - -fun OCFileListAdapter.parseAndSaveVirtuals( - data: List, - searchType: SearchType -): List { - val (type, onlyMedia) = when (searchType) { - SearchType.FAVORITE_SEARCH -> VirtualFolderType.FAVORITE to false - SearchType.GALLERY_SEARCH -> VirtualFolderType.GALLERY to true - else -> VirtualFolderType.NONE to false - } - - val contentValuesList = mutableListOf() - val resultFiles = mutableListOf() - - for (obj in data) { - try { - val remoteFile = obj as RemoteFile - - var ocFile = FileStorageUtils.fillOCFile(remoteFile) - FileStorageUtils.searchForLocalFileInDefaultPath(ocFile, user.accountName) - - ocFile = mStorageManager.saveFileWithParent(ocFile, activity) - - val parent = mStorageManager.getFileById(ocFile.parentId) - if (parent != null && (ocFile.isEncrypted || parent.isEncrypted)) { - val metadata = RefreshFolderOperation.getDecryptedFolderMetadata( - true, - parent, - OwnCloudClientFactory.createOwnCloudClient(user.toPlatformAccount(), activity), - user, - activity - ) ?: throw IllegalStateException("metadata is null") - - when (metadata) { - is DecryptedFolderMetadataFileV1 -> - RefreshFolderOperation.updateFileNameForEncryptedFileV1( - mStorageManager, metadata, ocFile - ) - - is DecryptedFolderMetadataFile -> - RefreshFolderOperation.updateFileNameForEncryptedFile( - mStorageManager, metadata, ocFile - ) - } - - ocFile = mStorageManager.saveFileWithParent(ocFile, activity) - } - - if (searchType != SearchType.GALLERY_SEARCH && ocFile.isFolder) { - val op = RefreshFolderOperation( - ocFile, - System.currentTimeMillis(), - true, - false, - mStorageManager, - user, - activity - ) - op.execute(user, activity) - } - - val passesMediaFilter = - !onlyMedia || MimeTypeUtil.isImage(ocFile) || MimeTypeUtil.isVideo(ocFile) - - if (passesMediaFilter) { - if (resultFiles.isEmpty() || !resultFiles.contains(ocFile)) { - resultFiles.add(ocFile) - } - } - - val cv = ContentValues().apply { - put(ProviderMeta.ProviderTableMeta.VIRTUAL_TYPE, type.toString()) - put(ProviderMeta.ProviderTableMeta.VIRTUAL_OCFILE_ID, ocFile.fileId) - } - contentValuesList.add(cv) - - } catch (_: Exception) { - } - } - - // Save timestamp + virtual entries - preferences.setPhotoSearchTimestamp(System.currentTimeMillis()) - mStorageManager.saveVirtuals(contentValuesList) - - return resultFiles -} 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 4c56d1243ba2..4dbba6451c1f 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 @@ -39,7 +39,6 @@ import com.nextcloud.model.OfflineOperationType; import com.nextcloud.utils.LinkHelper; import com.nextcloud.utils.extensions.OCFileExtensionsKt; -import com.nextcloud.utils.extensions.OCFileListAdapterExtensionsKt; import com.nextcloud.utils.extensions.ViewExtensionsKt; import com.nextcloud.utils.mdm.MDMConfig; import com.owncloud.android.MainApp; @@ -84,8 +83,6 @@ import java.util.Objects; import java.util.Set; import java.util.UUID; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.stream.Collectors; import androidx.annotation.NonNull; @@ -140,7 +137,6 @@ public class OCFileListAdapter extends RecyclerView.Adapter recommendedFiles = new ArrayList<>(); private RecommendedFilesAdapter recommendedFilesAdapter; - private final ExecutorService executorService = Executors.newSingleThreadExecutor(); public OCFileListAdapter( Activity activity, @@ -969,73 +965,42 @@ private void addOfflineOperations(long fileId) { mFilesAll.addAll(newFiles); } - public synchronized void setData(List objects, - SearchType searchType, - FileDataStorageManager storageManager, - @Nullable OCFile folder, - boolean clear) { - executorService.execute(() -> { - if (storageManager != null && mStorageManager == null) { - mStorageManager = storageManager; - ocFileListDelegate.setShowShareAvatar(true); - } - - if (mStorageManager == null) { - mStorageManager = new FileDataStorageManager(user, activity.getContentResolver()); - } - - List newList = new ArrayList<>(); - - if (clear) { - preferences.setPhotoSearchTimestamp(0); - - VirtualFolderType type = switch (searchType) { - case FAVORITE_SEARCH -> VirtualFolderType.FAVORITE; - case GALLERY_SEARCH -> VirtualFolderType.GALLERY; - default -> VirtualFolderType.NONE; - }; + public void setSearchData(List newList, SearchType searchType, FileDataStorageManager storageManager, boolean clear) { + initStorageManagerShowShareAvatar(storageManager); + if (clear) { + clearSearchData(searchType); + } + new Handler(Looper.getMainLooper()).post(() -> updateData(newList)); + } - if (type != VirtualFolderType.GALLERY) { - mStorageManager.deleteVirtuals(type); - } - } + private void initStorageManagerShowShareAvatar(FileDataStorageManager storageManager) { + if (mStorageManager == null) { + mStorageManager = (storageManager != null) + ? storageManager + : new FileDataStorageManager(user, activity.getContentResolver()); - boolean containsOCFiles = false; - for (Object item : objects) { - if (item instanceof OCFile) { - containsOCFiles = true; - break; - } + if (storageManager != null) { + ocFileListDelegate.setShowShareAvatar(true); } + } + } - if (containsOCFiles) { - newList = OCFileListAdapterExtensionsKt.parseCachedFiles(this, objects); - } else if (!objects.isEmpty()) { - if (searchType == SearchType.SHARED_FILTER) { - newList = OCFileListAdapterExtensionsKt.parseAndSaveShares(this, objects); - } else { - newList = OCFileListAdapterExtensionsKt.parseAndSaveVirtuals(this, objects, searchType); - } - } + private void clearSearchData(SearchType searchType) { + preferences.setPhotoSearchTimestamp(0); - if (searchType == SearchType.GALLERY_SEARCH || - searchType == SearchType.RECENTLY_MODIFIED_SEARCH) { - newList = FileStorageUtils.sortOcFolderDescDateModifiedWithoutFavoritesFirst(newList); - } else if (searchType != SearchType.SHARED_FILTER) { - boolean foldersBeforeFiles = preferences.isSortFoldersBeforeFiles(); - boolean favoritesFirst = preferences.isSortFavoritesFirst(); + VirtualFolderType type = switch (searchType) { + case FAVORITE_SEARCH -> VirtualFolderType.FAVORITE; + case GALLERY_SEARCH -> VirtualFolderType.GALLERY; + default -> VirtualFolderType.NONE; + }; - if (searchType == SearchType.FAVORITE_SEARCH) { - sortOrder = preferences.getSortOrderByType(FileSortOrder.Type.favoritesListView); - } else { - sortOrder = preferences.getSortOrderByFolder(folder); - } - newList = sortOrder.sortCloudFiles(newList, foldersBeforeFiles, favoritesFirst); - } + if (type != VirtualFolderType.GALLERY) { + mStorageManager.deleteVirtuals(type); + } + } - List finalNewList = newList; - new Handler(Looper.getMainLooper()).post(() -> updateData(finalNewList)); - }); + public void setSortOrder(FileSortOrder newSortOrder) { + sortOrder = newSortOrder; } public void updateData(List newList) { @@ -1049,12 +1014,12 @@ public void updateData(List newList) { DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffUtil.Callback() { @Override public int getOldListSize() { - return mFiles.size(); + return mFiles.size() + (shouldShowHeader() ? 2 : 1); } @Override public int getNewListSize() { - return newList.size(); + return newList.size() + (shouldShowHeader() ? 2 : 1); } @Override @@ -1233,6 +1198,5 @@ public void setCurrentDirectory(OCFile folder) { public void cleanup() { ocFileListDelegate.cleanup(); - executorService.shutdown(); } } 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 07288264b7f0..196f66c96577 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 @@ -1931,7 +1931,7 @@ protected void handleSearchEvent(SearchEvent event) { final User currentUser = accountManager.getUser(); final var remoteOperation = getSearchRemoteOperation(currentUser, event); - searchTask = new OCFileListSearchTask(mContainerActivity, this, remoteOperation, currentUser, event, SharedListFragment.TASK_TIMEOUT); + searchTask = new OCFileListSearchTask(mContainerActivity, this, remoteOperation, currentUser, event, SharedListFragment.TASK_TIMEOUT, preferences); searchTask.execute(); } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt index b79b14946f56..9f05723ab33f 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt @@ -9,16 +9,32 @@ package com.owncloud.android.ui.fragment import android.annotation.SuppressLint +import android.app.Activity +import android.content.ContentValues import androidx.lifecycle.lifecycleScope import com.nextcloud.client.account.User +import com.nextcloud.client.preferences.AppPreferences import com.owncloud.android.R import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.datamodel.VirtualFolderType +import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFolderMetadataFileV1 +import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFolderMetadataFile +import com.owncloud.android.db.ProviderMeta +import com.owncloud.android.lib.common.OwnCloudClientFactory import com.owncloud.android.lib.common.operations.RemoteOperation import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.lib.resources.files.SearchRemoteOperation +import com.owncloud.android.lib.resources.files.model.RemoteFile +import com.owncloud.android.lib.resources.shares.OCShare +import com.owncloud.android.operations.RefreshFolderOperation +import com.owncloud.android.ui.adapter.OCShareToOCFileConverter import com.owncloud.android.ui.events.SearchEvent import com.owncloud.android.utils.DisplayUtils +import com.owncloud.android.utils.FileSortOrder +import com.owncloud.android.utils.FileStorageUtils +import com.owncloud.android.utils.MimeTypeUtil import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch @@ -34,7 +50,8 @@ class OCFileListSearchTask( private val remoteOperation: RemoteOperation>, private val currentUser: User, private val event: SearchEvent, - private val taskTimeout: Long + private val taskTimeout: Long, + private val preferences: AppPreferences ) { companion object { private const val TAG = "OCFileListSearchTask" @@ -52,73 +69,234 @@ class OCFileListSearchTask( fun execute() { Log_OC.d(TAG, "search task running, query: ${event.searchType}") val fragment = fragmentReference.get() ?: return - val context = fragment.context ?: return job = fragment.lifecycleScope.launch(Dispatchers.IO) { - val filesInDb = when (event.searchType) { - SearchRemoteOperation.SearchType.SHARED_FILTER -> { - fileDataStorageManager?.fileDao?.getSharedFiles(currentUser.accountName) - } - SearchRemoteOperation.SearchType.FAVORITE_SEARCH -> { - fileDataStorageManager?.fileDao?.getFavoriteFiles(currentUser.accountName) + val searchType = fragment.currentSearchType + + // using cached data + val filesInDb = loadCachedDbFiles(event.searchType) + val sortedFilesInDb = sortSearchData(filesInDb, searchType, null, setNewSortOrder = { + fragment.adapter.setSortOrder(it) + }) + updateAdapterData(fragment, sortedFilesInDb, false) + + // updating cache and refreshing adapter + val result = fetchRemoteResults() + if (result?.isSuccess == true) { + if (result.resultData?.isEmpty() == true) { + withContext(Dispatchers.Main) { + fragment.setEmptyListMessage(SearchType.NO_SEARCH) + return@withContext + } + + return@launch } - else -> { - null + + val newList = if (searchType == SearchType.SHARED_FILTER) { + parseAndSaveShares(result.resultData ?: listOf()) + } else { + parseAndSaveVirtuals(result.resultData ?: listOf(), fragment) } - }?.map { fileDataStorageManager?.createFileInstance(it) } + + val sortedNewList = sortSearchData(newList, searchType, null, setNewSortOrder = { + fragment.adapter.setSortOrder(it) + }) + + updateAdapterData(fragment, sortedNewList, true) + + return@launch + } withContext(Dispatchers.Main) { - if (fragment.isAdded && fragment.searchFragment) { - fragment.adapter.setData( - filesInDb, - fragment.currentSearchType, - fileDataStorageManager, - fragment.mFile, - false - ) + fragment.activity?.let { + DisplayUtils.showSnackMessage(it, R.string.error_fetching_sharees) } } + } + } - val result: RemoteOperationResult>? = try { - withTimeoutOrNull(taskTimeout) { - remoteOperation.execute(currentUser, context) - } ?: remoteOperation.executeNextcloudClient(currentUser, context) - } catch (e: Exception) { - Log_OC.e(TAG, "exception execute: ", e) - null + fun cancel() = job?.cancel(null) + + fun isFinished(): Boolean = job?.isCompleted == true + + private suspend fun loadCachedDbFiles(searchType: SearchRemoteOperation.SearchType): List { + val storage = fileDataStorageManager ?: return emptyList() + + val rows = when (searchType) { + SearchRemoteOperation.SearchType.SHARED_FILTER -> + storage.fileDao.getSharedFiles(currentUser.accountName) + + SearchRemoteOperation.SearchType.FAVORITE_SEARCH -> + storage.fileDao.getFavoriteFiles(currentUser.accountName) + + else -> null + } ?: return emptyList() + + return rows.mapNotNull { storage.createFileInstance(it) } + } + + private suspend fun fetchRemoteResults(): RemoteOperationResult>? { + val fragment = fragmentReference.get() ?: return null + val context = fragment.context ?: return null + + return try { + withTimeoutOrNull(taskTimeout) { + remoteOperation.execute(currentUser, context) + } ?: remoteOperation.executeNextcloudClient(currentUser, context) + } catch (e: Exception) { + Log_OC.e(TAG, "exception execute: ", e) + null + } + } + + private suspend fun updateAdapterData(fragment: OCFileListFragment, newList: List, clear: Boolean) = + withContext(Dispatchers.Main) { + if (!fragment.isAdded || !fragment.searchFragment) { + Log_OC.e(TAG, "cannot update adapter data, fragment is not ready") + return@withContext } - withContext(Dispatchers.Main) { - if (!fragment.isAdded || !fragment.searchFragment) { - Log_OC.e(TAG, "cannot search, fragment is not ready") - return@withContext + fragment.adapter.setSearchData(newList, fragment.currentSearchType, fileDataStorageManager, clear) + } + + private suspend fun sortSearchData( + list: List, + searchType: SearchType, + folder: OCFile?, + setNewSortOrder: (FileSortOrder) -> Unit + ): List = withContext(Dispatchers.IO) { + var newList = list.toMutableList() + + if (searchType == SearchType.GALLERY_SEARCH || + searchType == SearchType.RECENTLY_MODIFIED_SEARCH + ) { + return@withContext FileStorageUtils.sortOcFolderDescDateModifiedWithoutFavoritesFirst(newList) + } + + if (searchType != SearchType.SHARED_FILTER) { + val foldersBeforeFiles = preferences.isSortFoldersBeforeFiles() + val favoritesFirst = preferences.isSortFavoritesFirst() + + val sortOrder = + if (searchType == SearchType.FAVORITE_SEARCH) { + preferences.getSortOrderByType(FileSortOrder.Type.favoritesListView) + } else { + preferences.getSortOrderByFolder(folder) } - if (result?.isSuccess == true) { - if (result.resultData.isEmpty()) { - fragment.setEmptyListMessage(SearchType.NO_SEARCH) - return@withContext - } + setNewSortOrder(sortOrder) + newList = sortOrder.sortCloudFiles(newList, foldersBeforeFiles, favoritesFirst) + } + + return@withContext newList + } + + private suspend fun parseAndSaveShares(data: List): List = withContext(Dispatchers.IO) { + val shares = data.filterIsInstance() + val files: List = OCShareToOCFileConverter.buildOCFilesFromShares(shares) + for (file in files) { + FileStorageUtils.searchForLocalFileInDefaultPath(file, currentUser.accountName) + } + fileDataStorageManager?.saveShares(shares) + return@withContext files + } + + private suspend fun parseAndSaveVirtuals( + data: List, + fragment: OCFileListFragment + ): List = withContext(Dispatchers.IO) { + val fileDataStorageManager = fileDataStorageManager ?: return@withContext listOf() + val activity = fragment.activity ?: return@withContext listOf() + val now = System.currentTimeMillis() + + val (virtualType, onlyMedia) = when (fragment.currentSearchType) { + SearchType.FAVORITE_SEARCH -> VirtualFolderType.FAVORITE to false + SearchType.GALLERY_SEARCH -> VirtualFolderType.GALLERY to true + else -> VirtualFolderType.NONE to false + } + + val contentValuesList = ArrayList(data.size) + val resultFiles = LinkedHashSet() - fragment.searchEvent = event - fragment.adapter.setData( - result.resultData, - fragment.currentSearchType, + for (obj in data) { + try { + val remoteFile = obj as? RemoteFile ?: continue + var ocFile = FileStorageUtils.fillOCFile(remoteFile).apply { + FileStorageUtils.searchForLocalFileInDefaultPath(this, currentUser.accountName) + } + ocFile = fileDataStorageManager.saveFileWithParent(ocFile, activity) + ocFile = handleEncryptionIfNeeded(ocFile, fileDataStorageManager, activity) + + if (fragment.currentSearchType != SearchType.GALLERY_SEARCH && ocFile.isFolder) { + RefreshFolderOperation( + ocFile, + now, + true, + false, fileDataStorageManager, - fragment.mFile, - true - ) - return@withContext + currentUser, + activity + ).execute(currentUser, activity) } - fragment.activity?.let { - DisplayUtils.showSnackMessage(it, R.string.error_fetching_sharees) + val isMediaAllowed = + !onlyMedia || MimeTypeUtil.isImage(ocFile) || MimeTypeUtil.isVideo(ocFile) + + if (isMediaAllowed) { + resultFiles.add(ocFile) } + + contentValuesList.add( + ContentValues(2).apply { + put(ProviderMeta.ProviderTableMeta.VIRTUAL_TYPE, virtualType.toString()) + put(ProviderMeta.ProviderTableMeta.VIRTUAL_OCFILE_ID, ocFile.fileId) + } + ) + } catch (_: Exception) { } } + + // Save timestamp + virtual entries + preferences.setPhotoSearchTimestamp(System.currentTimeMillis()) + fileDataStorageManager.saveVirtuals(contentValuesList) + + resultFiles.toList() } - fun cancel() = job?.cancel(null) + private fun handleEncryptionIfNeeded( + ocFile: OCFile, + fileDataStorage: FileDataStorageManager, + activity: Activity + ): OCFile { + val parent = fileDataStorage.getFileById(ocFile.parentId) + ?: return ocFile - fun isFinished(): Boolean = job?.isCompleted == true + if (!ocFile.isEncrypted && !parent.isEncrypted) return ocFile + + val client = OwnCloudClientFactory.createOwnCloudClient( + currentUser.toPlatformAccount(), + activity + ) + + val metadata = RefreshFolderOperation.getDecryptedFolderMetadata( + true, + parent, + client, + currentUser, + activity + ) ?: throw IllegalStateException("metadata is null") + + when (metadata) { + is DecryptedFolderMetadataFileV1 -> + RefreshFolderOperation.updateFileNameForEncryptedFileV1( + fileDataStorage, metadata, ocFile + ) + is DecryptedFolderMetadataFile -> + RefreshFolderOperation.updateFileNameForEncryptedFile( + fileDataStorage, metadata, ocFile + ) + } + + return fileDataStorage.saveFileWithParent(ocFile, activity) + } } From c375e21cc0e7b2c7748895e52eb1d8c0de701059 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 21 Nov 2025 09:16:15 +0100 Subject: [PATCH 04/32] offload jobs from adapter Signed-off-by: alperozturk --- .../ui/fragment/SharedListFragmentIT.kt | 15 +---------- .../ui/adapter/OCShareToOCFileConverter.kt | 25 +++++++++++++++++++ .../ui/fragment/OCFileListSearchTask.kt | 17 ++++--------- 3 files changed, 31 insertions(+), 26 deletions(-) diff --git a/app/src/androidTest/java/com/owncloud/android/ui/fragment/SharedListFragmentIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/fragment/SharedListFragmentIT.kt index 6cae3fd36282..fbc2dbfe3fac 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/fragment/SharedListFragmentIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/fragment/SharedListFragmentIT.kt @@ -23,11 +23,8 @@ import com.owncloud.android.lib.resources.shares.OCShare import com.owncloud.android.lib.resources.shares.ShareType import com.owncloud.android.ui.adapter.OCShareToOCFileConverter import com.owncloud.android.utils.EspressoIdlingResource -import com.owncloud.android.utils.FileStorageUtils import com.owncloud.android.utils.ScreenshotTest -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext import org.junit.After import org.junit.Before import org.junit.Rule @@ -172,7 +169,7 @@ internal class SharedListFragmentIT : AbstractIT() { fragment.mEmptyListContainer?.visibility = View.GONE val newList = runBlocking { - parseAndSaveShares(shares) + OCShareToOCFileConverter.parseAndSaveShares(shares, storageManager, user.accountName) } fragment.adapter.setSearchData( newList, @@ -190,14 +187,4 @@ internal class SharedListFragmentIT : AbstractIT() { } } } - - private suspend fun parseAndSaveShares(data: List): List = withContext(Dispatchers.IO) { - val shares = data.filterIsInstance() - val files: List = OCShareToOCFileConverter.buildOCFilesFromShares(shares) - for (file in files) { - FileStorageUtils.searchForLocalFileInDefaultPath(file, user.accountName) - } - fileDataStorageManager?.saveShares(shares) - return@withContext files - } } diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt index affc513870b7..8cff475db99c 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt @@ -7,10 +7,14 @@ */ package com.owncloud.android.ui.adapter +import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.OCFile import com.owncloud.android.lib.resources.shares.OCShare import com.owncloud.android.lib.resources.shares.ShareType import com.owncloud.android.lib.resources.shares.ShareeUser +import com.owncloud.android.utils.FileStorageUtils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext object OCShareToOCFileConverter { private const val MILLIS_PER_SECOND = 1000 @@ -36,6 +40,27 @@ object OCShareToOCFileConverter { .sortedByDescending { it.firstShareTimestamp } } + suspend fun parseAndSaveShares( + data: List, + storageManager: FileDataStorageManager?, + accountName: String + ): List = withContext(Dispatchers.IO) { + if (data.isEmpty()) { + return@withContext listOf() + } + + val shares = data.filterIsInstance() + if (shares.isEmpty()) { + return@withContext listOf() + } + + val files = buildOCFilesFromShares(shares).onEach { file -> + FileStorageUtils.searchForLocalFileInDefaultPath(file, accountName) + } + storageManager?.saveShares(shares) + files + } + private fun buildOcFile(path: String, shares: List): OCFile { require(shares.all { it.path == path }) // common attributes diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt index 9f05723ab33f..3d4a352c2814 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt @@ -27,7 +27,6 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.lib.resources.files.SearchRemoteOperation import com.owncloud.android.lib.resources.files.model.RemoteFile -import com.owncloud.android.lib.resources.shares.OCShare import com.owncloud.android.operations.RefreshFolderOperation import com.owncloud.android.ui.adapter.OCShareToOCFileConverter import com.owncloud.android.ui.events.SearchEvent @@ -93,7 +92,11 @@ class OCFileListSearchTask( } val newList = if (searchType == SearchType.SHARED_FILTER) { - parseAndSaveShares(result.resultData ?: listOf()) + OCShareToOCFileConverter.parseAndSaveShares( + result.resultData ?: listOf(), + fileDataStorageManager, + currentUser.accountName + ) } else { parseAndSaveVirtuals(result.resultData ?: listOf(), fragment) } @@ -191,16 +194,6 @@ class OCFileListSearchTask( return@withContext newList } - private suspend fun parseAndSaveShares(data: List): List = withContext(Dispatchers.IO) { - val shares = data.filterIsInstance() - val files: List = OCShareToOCFileConverter.buildOCFilesFromShares(shares) - for (file in files) { - FileStorageUtils.searchForLocalFileInDefaultPath(file, currentUser.accountName) - } - fileDataStorageManager?.saveShares(shares) - return@withContext files - } - private suspend fun parseAndSaveVirtuals( data: List, fragment: OCFileListFragment From 68dd4044b29eea039211fce67717b4b444cb0459 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 21 Nov 2025 11:06:05 +0100 Subject: [PATCH 05/32] extract diff util to new class Signed-off-by: alperozturk --- .../android/ui/adapter/OCFileListAdapter.java | 44 ++------------ .../ui/adapter/diffUtil/OCFileListDiffUtil.kt | 58 +++++++++++++++++++ 2 files changed, 62 insertions(+), 40 deletions(-) create mode 100644 app/src/main/java/com/owncloud/android/ui/adapter/diffUtil/OCFileListDiffUtil.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 4dbba6451c1f..9ba6c06b8e8f 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,6 +62,7 @@ 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.diffUtil.OCFileListDiffUtil; import com.owncloud.android.ui.fragment.OCFileListFragment; import com.owncloud.android.ui.fragment.SearchType; import com.owncloud.android.ui.interfaces.OCFileListFragmentInterface; @@ -137,6 +138,7 @@ public class OCFileListAdapter extends RecyclerView.Adapter recommendedFiles = new ArrayList<>(); private RecommendedFilesAdapter recommendedFilesAdapter; + private final OCFileListDiffUtil diffUtil = new OCFileListDiffUtil(); public OCFileListAdapter( Activity activity, @@ -1011,46 +1013,8 @@ public void updateData(List newList) { return; } - DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffUtil.Callback() { - @Override - public int getOldListSize() { - return mFiles.size() + (shouldShowHeader() ? 2 : 1); - } - - @Override - public int getNewListSize() { - return newList.size() + (shouldShowHeader() ? 2 : 1); - } - - @Override - public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { - if (oldItemPosition >= mFiles.size() || newItemPosition >= newList.size()) return false; - return mFiles.get(oldItemPosition).equals(newList.get(newItemPosition)); - } - - @Override - public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { - if (oldItemPosition >= mFiles.size() || newItemPosition >= newList.size()) return false; - - OCFile oldFile = mFiles.get(oldItemPosition); - OCFile newFile = newList.get(newItemPosition); - - return oldFile.getModificationTimestamp() == newFile.getModificationTimestamp() && - oldFile.getFileLength() == newFile.getFileLength() && - Objects.equals(oldFile.getFileName(), newFile.getFileName()) && - oldFile.isFavorite() == newFile.isFavorite() && - oldFile.isEncrypted() == newFile.isEncrypted() && - oldFile.isSharedWithMe() == newFile.isSharedWithMe() && - oldFile.isSharedWithSharee() == newFile.isSharedWithSharee() && - oldFile.isSharedViaLink() == newFile.isSharedViaLink() && - oldFile.isLocked() == newFile.isLocked() && - oldFile.getUnreadCommentsCount() == newFile.getUnreadCommentsCount() && - Objects.equals(oldFile.getEtag(), newFile.getEtag()) && - Objects.equals(oldFile.getLinkedFileIdForLivePhoto(), newFile.getLinkedFileIdForLivePhoto()) && - oldFile.getTags().equals(newFile.getTags()) && - oldFile.isOfflineOperation() == newFile.isOfflineOperation(); - } - }); + diffUtil.updateLists(mFiles, newList, shouldShowHeader()); + final var diffResult = DiffUtil.calculateDiff(diffUtil); mFiles.clear(); mFiles.addAll(newList); diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/diffUtil/OCFileListDiffUtil.kt b/app/src/main/java/com/owncloud/android/ui/adapter/diffUtil/OCFileListDiffUtil.kt new file mode 100644 index 000000000000..dbb9a12e1b77 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/adapter/diffUtil/OCFileListDiffUtil.kt @@ -0,0 +1,58 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.ui.adapter.diffUtil + +import androidx.recyclerview.widget.DiffUtil +import com.owncloud.android.datamodel.OCFile + +class OCFileListDiffUtil() : DiffUtil.Callback() { + private var oldList: List = listOf() + private var newList: List = listOf() + private var showHeader: Boolean = false + + fun updateLists(oldList: List, newList: List, showHeader: Boolean) { + this.oldList = oldList + this.newList = newList + this.showHeader = showHeader + } + + override fun getOldListSize(): Int { + return oldList.size + if (showHeader) 2 else 1 + } + + override fun getNewListSize(): Int { + return newList.size + if (showHeader) 2 else 1 + } + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + if (oldItemPosition >= oldList.size || newItemPosition >= newList.size) return false + return oldList[oldItemPosition] == newList[newItemPosition] + } + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + if (oldItemPosition >= oldList.size || newItemPosition >= newList.size) return false + + val oldFile = oldList[oldItemPosition] + val newFile = newList[newItemPosition] + + return oldFile.modificationTimestamp == newFile.modificationTimestamp && + oldFile.fileLength == newFile.fileLength && + oldFile.fileName == newFile.fileName && + oldFile.isFavorite == newFile.isFavorite && + oldFile.isEncrypted == newFile.isEncrypted && + oldFile.isSharedWithMe == newFile.isSharedWithMe && + oldFile.isSharedWithSharee == newFile.isSharedWithSharee && + oldFile.isSharedViaLink == newFile.isSharedViaLink && + oldFile.isLocked == newFile.isLocked && + oldFile.unreadCommentsCount == newFile.unreadCommentsCount && + oldFile.etag == newFile.etag && + oldFile.linkedFileIdForLivePhoto == newFile.linkedFileIdForLivePhoto && + oldFile.tags == newFile.tags && + oldFile.isOfflineOperation == newFile.isOfflineOperation + } +} From 8b9cb5fe290b5891e1a422c2b2fe82374a88efd2 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 21 Nov 2025 11:59:42 +0100 Subject: [PATCH 06/32] offload swapDirectory io jobs from main thread. Signed-off-by: alperozturk --- .../nextcloud/client/database/dao/FileDao.kt | 3 + .../android/ui/adapter/OCFileListAdapter.java | 126 +++------------- .../adapter/helper/OCFileListAdapterHelper.kt | 140 ++++++++++++++++++ 3 files changed, 161 insertions(+), 108 deletions(-) create mode 100644 app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterHelper.kt diff --git a/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt b/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt index dbbf7e493372..9ef59f078294 100644 --- a/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt +++ b/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt @@ -41,6 +41,9 @@ interface FileDao { @Query("SELECT * FROM filelist WHERE parent = :parentId ORDER BY ${ProviderTableMeta.FILE_DEFAULT_SORT_ORDER}") fun getFolderContent(parentId: Long): List + @Query("SELECT * FROM filelist WHERE parent = :parentId ORDER BY ${ProviderTableMeta.FILE_DEFAULT_SORT_ORDER}") + suspend fun getFolderContentSuspended(parentId: Long): List + @Query( "SELECT * FROM filelist WHERE modified >= :startDate" + " AND modified < :endDate" + 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 9ba6c06b8e8f..2929836e4d62 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 @@ -18,7 +18,6 @@ import android.content.res.Resources; import android.graphics.Color; import android.graphics.drawable.Drawable; -import android.os.Build; import android.os.Handler; import android.os.Looper; import android.text.TextUtils; @@ -35,10 +34,8 @@ import com.nextcloud.client.database.entity.OfflineOperationEntity; import com.nextcloud.client.jobs.upload.FileUploadHelper; import com.nextcloud.client.preferences.AppPreferences; -import com.nextcloud.model.OCFileFilterType; import com.nextcloud.model.OfflineOperationType; import com.nextcloud.utils.LinkHelper; -import com.nextcloud.utils.extensions.OCFileExtensionsKt; import com.nextcloud.utils.extensions.ViewExtensionsKt; import com.nextcloud.utils.mdm.MDMConfig; import com.owncloud.android.MainApp; @@ -58,11 +55,10 @@ import com.owncloud.android.lib.resources.shares.ShareeUser; import com.owncloud.android.lib.resources.status.OCCapability; import com.owncloud.android.lib.resources.tags.Tag; -import com.owncloud.android.operations.RefreshFolderOperation; 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.diffUtil.OCFileListDiffUtil; +import com.owncloud.android.ui.adapter.helper.OCFileListAdapterHelper; import com.owncloud.android.ui.fragment.OCFileListFragment; import com.owncloud.android.ui.fragment.SearchType; import com.owncloud.android.ui.interfaces.OCFileListFragmentInterface; @@ -81,10 +77,8 @@ import java.util.Date; import java.util.List; import java.util.Locale; -import java.util.Objects; import java.util.Set; import java.util.UUID; -import java.util.stream.Collectors; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -93,6 +87,8 @@ import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import kotlin.Pair; +import kotlin.Unit; +import kotlin.jvm.functions.Function2; import me.zhanghai.android.fastscroll.PopupTextProvider; /** @@ -139,6 +135,7 @@ public class OCFileListAdapter extends RecyclerView.Adapter recommendedFiles = new ArrayList<>(); private RecommendedFilesAdapter recommendedFilesAdapter; private final OCFileListDiffUtil diffUtil = new OCFileListDiffUtil(); + private final OCFileListAdapterHelper helper = new OCFileListAdapterHelper(); public OCFileListAdapter( Activity activity, @@ -537,33 +534,6 @@ private void checkVisibilityOfFileFeaturesLayout(ListViewHolder holder) { fileFeaturesLayout.setVisibility(fileFeaturesVisibility); } - private void mergeOCFilesForLivePhoto() { - List filesToRemove = new ArrayList<>(); - - for (int i = 0; i < mFiles.size(); i++) { - OCFile file = mFiles.get(i); - - for (int j = i + 1; j < mFiles.size(); j++) { - OCFile nextFile = mFiles.get(j); - String fileLocalId = String.valueOf(file.getLocalId()); - String nextFileLinkedLocalId = nextFile.getLinkedFileIdForLivePhoto(); - - if (fileLocalId.equals(nextFileLinkedLocalId)) { - if (MimeTypeUtil.isVideo(file.getMimeType())) { - nextFile.livePhotoVideo = file; - filesToRemove.add(file); - } else if (MimeTypeUtil.isVideo(nextFile.getMimeType())) { - file.livePhotoVideo = nextFile; - filesToRemove.add(nextFile); - } - } - } - } - - mFiles.removeAll(filesToRemove); - filesToRemove.clear(); - } - private void updateLivePhotoIndicators(ListViewHolder holder, OCFile file) { boolean isLivePhoto = file.getLinkedFileIdForLivePhoto() != null; @@ -887,84 +857,24 @@ public void swapDirectory( } if (mStorageManager != null) { - // Create a new local list to avoid concurrent modification - List files = mStorageManager.getFolderContent(directory, onlyOnDevice); - - if (!preferences.isShowHiddenFilesEnabled()) { - files = OCFileExtensionsKt.filterHiddenFiles(files); - } - if (!limitToMimeType.isEmpty()) { - files = OCFileExtensionsKt.filterByMimeType(files, limitToMimeType); - } - if (OCFile.ROOT_PATH.equals(directory.getRemotePath()) && MainApp.isOnlyPersonFiles()) { - files = OCFileExtensionsKt.limitToPersonalFiles(files, userId); - } - - // TODO refactor add DrawerState instead of using static menuItemId - if (DrawerActivity.menuItemId == R.id.nav_shared && currentDirectory != null) { - files = updatedStorageManager.filter(currentDirectory, OCFileFilterType.Shared); - } - if (DrawerActivity.menuItemId == R.id.nav_favorites && currentDirectory != null) { - files = updatedStorageManager.filter(currentDirectory, OCFileFilterType.Favorite); - } - - // Filter out temp files from the list to prevent duplication - files = OCFileExtensionsKt.filterTempFilter(files); - files = OCFileExtensionsKt.filterFilenames(files); - - sortOrder = preferences.getSortOrderByFolder(directory); - boolean foldersBeforeFiles = preferences.isSortFoldersBeforeFiles(); - boolean favoritesFirst = preferences.isSortFavoritesFirst(); - files = sortOrder.sortCloudFiles(files, foldersBeforeFiles, favoritesFirst); - - // Create new list for mFiles to avoid sharing references - mFiles = new ArrayList<>(files); - - prepareListOfHiddenFiles(); - mergeOCFilesForLivePhoto(); - - mFilesAll.clear(); - addOfflineOperations(directory.getFileId()); - mFilesAll.addAll(mFiles); - - currentDirectory = directory; + helper.prepareFileList(directory, updatedStorageManager, onlyOnDevice, limitToMimeType, preferences, userId, new Function2<>() { + @Override + public Unit invoke(List newList, FileSortOrder fileSortOrder) { + mFiles = new ArrayList<>(newList); + mFilesAll.clear(); + mFilesAll.addAll(mFiles); + currentDirectory = directory; + searchType = null; + activity.runOnUiThread(() -> notifyDataSetChanged()); + return Unit.INSTANCE; + } + }); } else { mFiles = new ArrayList<>(); // Create new instance instead of clear mFilesAll.clear(); + searchType = null; + activity.runOnUiThread(this::notifyDataSetChanged); } - - searchType = null; - activity.runOnUiThread(this::notifyDataSetChanged); - } - - /** - * Converts Offline Operations to OCFiles and adds them to the adapter for visual feedback. - * This function creates pending OCFiles, but they may not consistently appear in the UI. - * The issue arises when {@link RefreshFolderOperation} deletes pending Offline Operations, while some may still exist in the table. - * If only this function is used, it cause crash in {@link FileDisplayActivity mSyncBroadcastReceiver.onReceive}. - *

- * These function also need to be used: {@link FileDataStorageManager#createPendingDirectory(String, long, long)}, {@link FileDataStorageManager#createPendingFile(String, String, long, long)}. - */ - private void addOfflineOperations(long fileId) { - List offlineOperations = mStorageManager.offlineOperationsRepository.convertToOCFiles(fileId); - if (offlineOperations.isEmpty()) { - return; - } - - List newFiles; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - newFiles = offlineOperations.stream() - .filter(offlineFile -> mFilesAll.stream() - .noneMatch(file -> Objects.equals(file.getDecryptedRemotePath(), offlineFile.getDecryptedRemotePath()))) - .toList(); - } else { - newFiles = offlineOperations.stream() - .filter(offlineFile -> mFilesAll.stream() - .noneMatch(file -> Objects.equals(file.getDecryptedRemotePath(), offlineFile.getDecryptedRemotePath()))) - .collect(Collectors.toList()); - } - - mFilesAll.addAll(newFiles); } public void setSearchData(List newList, SearchType searchType, FileDataStorageManager storageManager, boolean clear) { 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/helper/OCFileListAdapterHelper.kt new file mode 100644 index 000000000000..865825ff9a8d --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterHelper.kt @@ -0,0 +1,140 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.ui.adapter.helper + +import com.nextcloud.client.database.entity.FileEntity +import com.nextcloud.client.preferences.AppPreferences +import com.nextcloud.utils.extensions.filterByMimeType +import com.nextcloud.utils.extensions.filterFilenames +import com.nextcloud.utils.extensions.filterHiddenFiles +import com.nextcloud.utils.extensions.filterTempFilter +import com.nextcloud.utils.extensions.limitToPersonalFiles +import com.owncloud.android.MainApp +import com.owncloud.android.R +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.ui.activity.DrawerActivity +import com.owncloud.android.utils.FileSortOrder +import com.owncloud.android.utils.MimeTypeUtil +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class OCFileListAdapterHelper { + private val scope = CoroutineScope(Dispatchers.IO) + + fun prepareFileList( + directory: OCFile, + storageManager: FileDataStorageManager, + onlyOnDevice: Boolean, + limitToMimeType: String, + preferences: AppPreferences, + userId: String, + onComplete: (List, FileSortOrder) -> Unit + ) { + scope.launch { + var result = getFolderContent(directory, storageManager, onlyOnDevice) + + if (!preferences.isShowHiddenFilesEnabled()) { + result = result.filterHiddenFiles() + } + + if (!limitToMimeType.isEmpty()) { + result = result.filterByMimeType(limitToMimeType) + } + + if (OCFile.ROOT_PATH == directory.remotePath && MainApp.isOnlyPersonFiles()) { + result = result.limitToPersonalFiles(userId) + } + + if (DrawerActivity.menuItemId == R.id.nav_shared) { + result = result.filter { it.isShared } + } + + if (DrawerActivity.menuItemId == R.id.nav_favorites) { + result = result.filter { it.isFavorite } + } + + result = result.filterTempFilter() + result = result.filterFilenames() + result = mergeOCFilesForLivePhoto(result) + result = addOfflineOperations(result, directory.fileId, storageManager) + val (newList, newSortOrder) = sortData(directory, result, preferences) + + withContext(Dispatchers.Main) { + onComplete(newList, newSortOrder) + } + } + } + + private fun addOfflineOperations(files: List, fileId: Long, storageManager: FileDataStorageManager): List { + val offlineOperations = storageManager.offlineOperationsRepository.convertToOCFiles(fileId) + if (offlineOperations.isEmpty()) return files + + val newFiles = offlineOperations.filter { offlineFile -> + files.none { it.decryptedRemotePath == offlineFile.decryptedRemotePath } + } + + return files + newFiles + } + + fun mergeOCFilesForLivePhoto(files: List): List { + val filesToRemove = mutableSetOf() + + for (i in files.indices) { + val file = files[i] + + for (j in i + 1 until files.size) { + val nextFile = files[j] + val fileLocalId = file.localId.toString() + val nextFileLinkedLocalId = nextFile.linkedFileIdForLivePhoto + + if (fileLocalId == nextFileLinkedLocalId) { + when { + MimeTypeUtil.isVideo(file.mimeType) -> { + nextFile.livePhotoVideo = file + filesToRemove.add(file) + } + MimeTypeUtil.isVideo(nextFile.mimeType) -> { + file.livePhotoVideo = nextFile + filesToRemove.add(nextFile) + } + } + } + } + } + + return files.filter { it !in filesToRemove } + } + + private suspend fun sortData(directory: OCFile, files: List, preferences: AppPreferences): Pair, FileSortOrder> = withContext( + Dispatchers.IO) { + val sortOrder = preferences.getSortOrderByFolder(directory) + val foldersBeforeFiles: Boolean = preferences.isSortFoldersBeforeFiles() + val favoritesFirst: Boolean = preferences.isSortFavoritesFirst() + return@withContext sortOrder.sortCloudFiles(files.toMutableList(), foldersBeforeFiles, favoritesFirst).toList() to sortOrder + } + + private suspend fun getFolderContent(ocFile: OCFile, storageManager: FileDataStorageManager, onlyOnDevice: Boolean): List = withContext(Dispatchers.IO) { + if (!ocFile.isFolder || !ocFile.fileExists()) { + return@withContext emptyList() + } + + val fileEntities: List = storageManager.fileDao.getFolderContentSuspended(ocFile.fileId) + + return@withContext fileEntities.mapNotNull { fileEntity -> + val file = storageManager.createFileInstance(fileEntity) + if (!onlyOnDevice || file.existsOnDevice()) { + file + } else { + null + } + } + } +} From 90f5a1e19695e592ae11c647c4d82e27c937864f Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 21 Nov 2025 12:22:28 +0100 Subject: [PATCH 07/32] remove diff util Signed-off-by: alperozturk --- .../nextcloud/client/database/dao/FileDao.kt | 1 - .../android/ui/adapter/OCFileListAdapter.java | 57 ++++----- .../ui/adapter/diffUtil/OCFileListDiffUtil.kt | 58 --------- .../adapter/helper/OCFileListAdapterHelper.kt | 38 ++++-- .../ui/fragment/OCFileListSearchTask.kt | 114 +++++++++--------- 5 files changed, 114 insertions(+), 154 deletions(-) delete mode 100644 app/src/main/java/com/owncloud/android/ui/adapter/diffUtil/OCFileListDiffUtil.kt diff --git a/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt b/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt index 9ef59f078294..d75a021099e2 100644 --- a/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt +++ b/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt @@ -136,5 +136,4 @@ interface FileDao { """ ) suspend fun getFavoriteFiles(fileOwner: String): List - } 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 2929836e4d62..5eced7ce3cef 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 @@ -57,7 +57,6 @@ import com.owncloud.android.lib.resources.tags.Tag; import com.owncloud.android.ui.activity.ComponentsGetter; import com.owncloud.android.ui.activity.FileDisplayActivity; -import com.owncloud.android.ui.adapter.diffUtil.OCFileListDiffUtil; import com.owncloud.android.ui.adapter.helper.OCFileListAdapterHelper; import com.owncloud.android.ui.fragment.OCFileListFragment; import com.owncloud.android.ui.fragment.SearchType; @@ -83,7 +82,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; -import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import kotlin.Pair; @@ -134,7 +132,6 @@ public class OCFileListAdapter extends RecyclerView.Adapter recommendedFiles = new ArrayList<>(); private RecommendedFilesAdapter recommendedFilesAdapter; - private final OCFileListDiffUtil diffUtil = new OCFileListDiffUtil(); private final OCFileListAdapterHelper helper = new OCFileListAdapterHelper(); public OCFileListAdapter( @@ -860,21 +857,28 @@ public void swapDirectory( helper.prepareFileList(directory, updatedStorageManager, onlyOnDevice, limitToMimeType, preferences, userId, new Function2<>() { @Override public Unit invoke(List newList, FileSortOrder fileSortOrder) { - mFiles = new ArrayList<>(newList); - mFilesAll.clear(); - mFilesAll.addAll(mFiles); - currentDirectory = directory; - searchType = null; - activity.runOnUiThread(() -> notifyDataSetChanged()); + updateAdapter((List) newList, directory); return Unit.INSTANCE; } }); - } else { - mFiles = new ArrayList<>(); // Create new instance instead of clear - mFilesAll.clear(); - searchType = null; - activity.runOnUiThread(this::notifyDataSetChanged); + return; } + + updateAdapter(new ArrayList<>(), null); + } + + private void updateAdapter(List newFiles, OCFile directory) { + mFiles = new ArrayList<>(newFiles); + mFilesAll.clear(); + mFilesAll.addAll(mFiles); + + if (directory != null) { + currentDirectory = directory; + } + + searchType = null; + + activity.runOnUiThread(this::notifyDataSetChanged); } public void setSearchData(List newList, SearchType searchType, FileDataStorageManager storageManager, boolean clear) { @@ -882,7 +886,12 @@ public void setSearchData(List newList, SearchType searchType, FileDataS if (clear) { clearSearchData(searchType); } - new Handler(Looper.getMainLooper()).post(() -> updateData(newList)); + + activity.runOnUiThread(() -> { + mFiles.clear(); + mFiles.addAll(newList); + notifyDataSetChanged(); + }); } private void initStorageManagerShowShareAvatar(FileDataStorageManager storageManager) { @@ -915,23 +924,6 @@ public void setSortOrder(FileSortOrder newSortOrder) { sortOrder = newSortOrder; } - public void updateData(List newList) { - if (mFiles.isEmpty() && newList.isEmpty()) { - return; - } - if (mFiles == newList) { - return; - } - - diffUtil.updateLists(mFiles, newList, shouldShowHeader()); - final var diffResult = DiffUtil.calculateDiff(diffUtil); - - mFiles.clear(); - mFiles.addAll(newList); - - diffResult.dispatchUpdatesTo(this); - } - @SuppressLint("NotifyDataSetChanged") public void setSortOrder(@Nullable OCFile folder, @NonNull FileSortOrder sortOrder) { if (searchType == SearchType.FAVORITE_SEARCH) { @@ -1072,5 +1064,6 @@ public void setCurrentDirectory(OCFile folder) { public void cleanup() { ocFileListDelegate.cleanup(); + helper.cleanup(); } } diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/diffUtil/OCFileListDiffUtil.kt b/app/src/main/java/com/owncloud/android/ui/adapter/diffUtil/OCFileListDiffUtil.kt deleted file mode 100644 index dbb9a12e1b77..000000000000 --- a/app/src/main/java/com/owncloud/android/ui/adapter/diffUtil/OCFileListDiffUtil.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2025 Alper Ozturk - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -package com.owncloud.android.ui.adapter.diffUtil - -import androidx.recyclerview.widget.DiffUtil -import com.owncloud.android.datamodel.OCFile - -class OCFileListDiffUtil() : DiffUtil.Callback() { - private var oldList: List = listOf() - private var newList: List = listOf() - private var showHeader: Boolean = false - - fun updateLists(oldList: List, newList: List, showHeader: Boolean) { - this.oldList = oldList - this.newList = newList - this.showHeader = showHeader - } - - override fun getOldListSize(): Int { - return oldList.size + if (showHeader) 2 else 1 - } - - override fun getNewListSize(): Int { - return newList.size + if (showHeader) 2 else 1 - } - - override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - if (oldItemPosition >= oldList.size || newItemPosition >= newList.size) return false - return oldList[oldItemPosition] == newList[newItemPosition] - } - - override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - if (oldItemPosition >= oldList.size || newItemPosition >= newList.size) return false - - val oldFile = oldList[oldItemPosition] - val newFile = newList[newItemPosition] - - return oldFile.modificationTimestamp == newFile.modificationTimestamp && - oldFile.fileLength == newFile.fileLength && - oldFile.fileName == newFile.fileName && - oldFile.isFavorite == newFile.isFavorite && - oldFile.isEncrypted == newFile.isEncrypted && - oldFile.isSharedWithMe == newFile.isSharedWithMe && - oldFile.isSharedWithSharee == newFile.isSharedWithSharee && - oldFile.isSharedViaLink == newFile.isSharedViaLink && - oldFile.isLocked == newFile.isLocked && - oldFile.unreadCommentsCount == newFile.unreadCommentsCount && - oldFile.etag == newFile.etag && - oldFile.linkedFileIdForLivePhoto == newFile.linkedFileIdForLivePhoto && - oldFile.tags == newFile.tags && - oldFile.isOfflineOperation == newFile.isOfflineOperation - } -} 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/helper/OCFileListAdapterHelper.kt index 865825ff9a8d..a9343af2a82c 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterHelper.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterHelper.kt @@ -23,12 +23,16 @@ import com.owncloud.android.utils.FileSortOrder import com.owncloud.android.utils.MimeTypeUtil import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class OCFileListAdapterHelper { - private val scope = CoroutineScope(Dispatchers.IO) + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + private var job: Job? = null + @Suppress("LongParameterList") fun prepareFileList( directory: OCFile, storageManager: FileDataStorageManager, @@ -38,7 +42,7 @@ class OCFileListAdapterHelper { userId: String, onComplete: (List, FileSortOrder) -> Unit ) { - scope.launch { + job = scope.launch { var result = getFolderContent(directory, storageManager, onlyOnDevice) if (!preferences.isShowHiddenFilesEnabled()) { @@ -73,7 +77,11 @@ class OCFileListAdapterHelper { } } - private fun addOfflineOperations(files: List, fileId: Long, storageManager: FileDataStorageManager): List { + private fun addOfflineOperations( + files: List, + fileId: Long, + storageManager: FileDataStorageManager + ): List { val offlineOperations = storageManager.offlineOperationsRepository.convertToOCFiles(fileId) if (offlineOperations.isEmpty()) return files @@ -84,6 +92,7 @@ class OCFileListAdapterHelper { return files + newFiles } + @Suppress("NestedBlockDepth") fun mergeOCFilesForLivePhoto(files: List): List { val filesToRemove = mutableSetOf() @@ -113,15 +122,25 @@ class OCFileListAdapterHelper { return files.filter { it !in filesToRemove } } - private suspend fun sortData(directory: OCFile, files: List, preferences: AppPreferences): Pair, FileSortOrder> = withContext( - Dispatchers.IO) { + private suspend fun sortData( + directory: OCFile, + files: List, + preferences: AppPreferences + ): Pair, FileSortOrder> = withContext( + Dispatchers.IO + ) { val sortOrder = preferences.getSortOrderByFolder(directory) val foldersBeforeFiles: Boolean = preferences.isSortFoldersBeforeFiles() val favoritesFirst: Boolean = preferences.isSortFavoritesFirst() - return@withContext sortOrder.sortCloudFiles(files.toMutableList(), foldersBeforeFiles, favoritesFirst).toList() to sortOrder + return@withContext sortOrder.sortCloudFiles(files.toMutableList(), foldersBeforeFiles, favoritesFirst) + .toList() to sortOrder } - private suspend fun getFolderContent(ocFile: OCFile, storageManager: FileDataStorageManager, onlyOnDevice: Boolean): List = withContext(Dispatchers.IO) { + private suspend fun getFolderContent( + ocFile: OCFile, + storageManager: FileDataStorageManager, + onlyOnDevice: Boolean + ): List = withContext(Dispatchers.IO) { if (!ocFile.isFolder || !ocFile.fileExists()) { return@withContext emptyList() } @@ -137,4 +156,9 @@ class OCFileListAdapterHelper { } } } + + fun cleanup() { + job?.cancel() + job = null + } } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt index 3d4a352c2814..8aff6ff429d7 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt @@ -41,7 +41,7 @@ import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeoutOrNull import java.lang.ref.WeakReference -@Suppress("LongParameterList") +@Suppress("LongParameterList", "ReturnCount", "TooGenericExceptionCaught") @SuppressLint("NotifyDataSetChanged") class OCFileListSearchTask( containerActivity: FileFragment.ContainerActivity, @@ -74,7 +74,7 @@ class OCFileListSearchTask( // using cached data val filesInDb = loadCachedDbFiles(event.searchType) - val sortedFilesInDb = sortSearchData(filesInDb, searchType, null, setNewSortOrder = { + val sortedFilesInDb = sortSearchData(filesInDb, searchType, null, setNewSortOrder = { fragment.adapter.setSortOrder(it) }) updateAdapterData(fragment, sortedFilesInDb, false) @@ -194,67 +194,65 @@ class OCFileListSearchTask( return@withContext newList } - private suspend fun parseAndSaveVirtuals( - data: List, - fragment: OCFileListFragment - ): List = withContext(Dispatchers.IO) { - val fileDataStorageManager = fileDataStorageManager ?: return@withContext listOf() - val activity = fragment.activity ?: return@withContext listOf() - val now = System.currentTimeMillis() - - val (virtualType, onlyMedia) = when (fragment.currentSearchType) { - SearchType.FAVORITE_SEARCH -> VirtualFolderType.FAVORITE to false - SearchType.GALLERY_SEARCH -> VirtualFolderType.GALLERY to true - else -> VirtualFolderType.NONE to false - } + private suspend fun parseAndSaveVirtuals(data: List, fragment: OCFileListFragment): List = + withContext(Dispatchers.IO) { + val fileDataStorageManager = fileDataStorageManager ?: return@withContext listOf() + val activity = fragment.activity ?: return@withContext listOf() + val now = System.currentTimeMillis() - val contentValuesList = ArrayList(data.size) - val resultFiles = LinkedHashSet() + val (virtualType, onlyMedia) = when (fragment.currentSearchType) { + SearchType.FAVORITE_SEARCH -> VirtualFolderType.FAVORITE to false + SearchType.GALLERY_SEARCH -> VirtualFolderType.GALLERY to true + else -> VirtualFolderType.NONE to false + } - for (obj in data) { - try { - val remoteFile = obj as? RemoteFile ?: continue - var ocFile = FileStorageUtils.fillOCFile(remoteFile).apply { - FileStorageUtils.searchForLocalFileInDefaultPath(this, currentUser.accountName) - } - ocFile = fileDataStorageManager.saveFileWithParent(ocFile, activity) - ocFile = handleEncryptionIfNeeded(ocFile, fileDataStorageManager, activity) - - if (fragment.currentSearchType != SearchType.GALLERY_SEARCH && ocFile.isFolder) { - RefreshFolderOperation( - ocFile, - now, - true, - false, - fileDataStorageManager, - currentUser, - activity - ).execute(currentUser, activity) - } + val contentValuesList = ArrayList(data.size) + val resultFiles = LinkedHashSet() - val isMediaAllowed = - !onlyMedia || MimeTypeUtil.isImage(ocFile) || MimeTypeUtil.isVideo(ocFile) + for (obj in data) { + try { + val remoteFile = obj as? RemoteFile ?: continue + var ocFile = FileStorageUtils.fillOCFile(remoteFile).apply { + FileStorageUtils.searchForLocalFileInDefaultPath(this, currentUser.accountName) + } + ocFile = fileDataStorageManager.saveFileWithParent(ocFile, activity) + ocFile = handleEncryptionIfNeeded(ocFile, fileDataStorageManager, activity) + + if (fragment.currentSearchType != SearchType.GALLERY_SEARCH && ocFile.isFolder) { + RefreshFolderOperation( + ocFile, + now, + true, + false, + fileDataStorageManager, + currentUser, + activity + ).execute(currentUser, activity) + } - if (isMediaAllowed) { - resultFiles.add(ocFile) - } + val isMediaAllowed = + !onlyMedia || MimeTypeUtil.isImage(ocFile) || MimeTypeUtil.isVideo(ocFile) - contentValuesList.add( - ContentValues(2).apply { - put(ProviderMeta.ProviderTableMeta.VIRTUAL_TYPE, virtualType.toString()) - put(ProviderMeta.ProviderTableMeta.VIRTUAL_OCFILE_ID, ocFile.fileId) + if (isMediaAllowed) { + resultFiles.add(ocFile) } - ) - } catch (_: Exception) { + + contentValuesList.add( + ContentValues(2).apply { + put(ProviderMeta.ProviderTableMeta.VIRTUAL_TYPE, virtualType.toString()) + put(ProviderMeta.ProviderTableMeta.VIRTUAL_OCFILE_ID, ocFile.fileId) + } + ) + } catch (_: Exception) { + } } - } - // Save timestamp + virtual entries - preferences.setPhotoSearchTimestamp(System.currentTimeMillis()) - fileDataStorageManager.saveVirtuals(contentValuesList) + // Save timestamp + virtual entries + preferences.setPhotoSearchTimestamp(System.currentTimeMillis()) + fileDataStorageManager.saveVirtuals(contentValuesList) - resultFiles.toList() - } + resultFiles.toList() + } private fun handleEncryptionIfNeeded( ocFile: OCFile, @@ -282,11 +280,15 @@ class OCFileListSearchTask( when (metadata) { is DecryptedFolderMetadataFileV1 -> RefreshFolderOperation.updateFileNameForEncryptedFileV1( - fileDataStorage, metadata, ocFile + fileDataStorage, + metadata, + ocFile ) is DecryptedFolderMetadataFile -> RefreshFolderOperation.updateFileNameForEncryptedFile( - fileDataStorage, metadata, ocFile + fileDataStorage, + metadata, + ocFile ) } From 1e87a96da3f159092cb3687d3daf25e6f5ce9764 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 21 Nov 2025 12:51:34 +0100 Subject: [PATCH 08/32] fix lint Signed-off-by: alperozturk --- .../com/owncloud/android/ui/adapter/OCFileListAdapter.java | 4 ++-- 1 file changed, 2 insertions(+), 2 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 5eced7ce3cef..58857f6374fd 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 @@ -106,8 +106,8 @@ public class OCFileListAdapter extends RecyclerView.Adapter listOfHiddenFiles = new ArrayList<>(); - public FileDataStorageManager mStorageManager; - public User user; + private FileDataStorageManager mStorageManager; + private User user; private final OCFileListFragmentInterface ocFileListFragmentInterface; private final boolean isRTL; From 949251af935340a06896451ded8b2dc2fc261643 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 21 Nov 2025 12:54:04 +0100 Subject: [PATCH 09/32] fix lint Signed-off-by: alperozturk --- .../owncloud/android/ui/adapter/OCFileListAdapter.java | 10 ---------- 1 file changed, 10 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 58857f6374fd..6029379b749b 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 @@ -960,16 +960,6 @@ public List getFiles() { return mFiles; } - private void prepareListOfHiddenFiles() { - listOfHiddenFiles.clear(); - - mFiles.forEach(file -> { - if (file.shouldHide()) { - listOfHiddenFiles.add(file.getFileName()); - } - }); - } - @Override public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) { super.onViewRecycled(holder); From e13343b3bc1c9b53dfba8cc9f5d1c0d9c93b5105 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 21 Nov 2025 13:01:43 +0100 Subject: [PATCH 10/32] fix lint Signed-off-by: alperozturk --- .../com/owncloud/android/ui/adapter/OCFileListAdapter.java | 4 ++-- 1 file changed, 2 insertions(+), 2 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 6029379b749b..f00fa1352a3c 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 @@ -98,8 +98,8 @@ public class OCFileListAdapter extends RecyclerView.Adapter mFiles = new ArrayList<>(); private final List mFilesAll = new ArrayList<>(); From a5ce31a4e7576b9b71d10c61c9a1a52806ec94ed Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 21 Nov 2025 13:12:02 +0100 Subject: [PATCH 11/32] fix lint Signed-off-by: alperozturk --- .../android/ui/adapter/OCFileListAdapter.java | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 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 f00fa1352a3c..f76821989008 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 @@ -845,6 +845,7 @@ public void swapDirectory( @NonNull FileDataStorageManager updatedStorageManager, boolean onlyOnDevice, @NonNull String limitToMimeType) { + this.onlyOnDevice = onlyOnDevice; if (!updatedStorageManager.equals(mStorageManager)) { @@ -853,18 +854,22 @@ public void swapDirectory( this.user = account; } - if (mStorageManager != null) { - helper.prepareFileList(directory, updatedStorageManager, onlyOnDevice, limitToMimeType, preferences, userId, new Function2<>() { - @Override - public Unit invoke(List newList, FileSortOrder fileSortOrder) { - updateAdapter((List) newList, directory); - return Unit.INSTANCE; - } - }); + if (mStorageManager == null) { + updateAdapter(new ArrayList<>(), null); return; } - updateAdapter(new ArrayList<>(), null); + helper.prepareFileList(directory, + updatedStorageManager, + onlyOnDevice, + limitToMimeType, + preferences, + userId, + (newList, fileSortOrder) -> + { + updateAdapter((List) newList, directory); + return Unit.INSTANCE; + }); } private void updateAdapter(List newFiles, OCFile directory) { @@ -887,11 +892,7 @@ public void setSearchData(List newList, SearchType searchType, FileDataS clearSearchData(searchType); } - activity.runOnUiThread(() -> { - mFiles.clear(); - mFiles.addAll(newList); - notifyDataSetChanged(); - }); + updateAdapter(newList, null); } private void initStorageManagerShowShareAvatar(FileDataStorageManager storageManager) { From cab8728fde1b44544a3cba9c400d2566db01ab38 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 21 Nov 2025 13:29:49 +0100 Subject: [PATCH 12/32] only update if new data passed Signed-off-by: alperozturk --- .../utils/extensions/OCFileExtensions.kt | 23 +++++++++++++++++++ .../android/ui/adapter/OCFileListAdapter.java | 11 +++++++++ 2 files changed, 34 insertions(+) diff --git a/app/src/main/java/com/nextcloud/utils/extensions/OCFileExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/OCFileExtensions.kt index 34f46ab66922..9a7ba34629ce 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/OCFileExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/OCFileExtensions.kt @@ -11,6 +11,29 @@ import com.owncloud.android.MainApp import com.owncloud.android.datamodel.OCFile import com.owncloud.android.utils.FileStorageUtils +fun List.hasSameContentAs(other: List): Boolean { + if (this.size != other.size) return false + + if (this === other) return true + + for (i in this.indices) { + val a = this[i] + val b = other[i] + + if (a != b) return false + if (a.fileId != b.fileId) return false + if (a.etag != b.etag) return false + if (a.modificationTimestamp != b.modificationTimestamp) return false + + if (a.fileLength != b.fileLength) return false + if (a.isFavorite != b.isFavorite) return false + + if (a.fileName != b.fileName) return false + } + + return true +} + fun List.filterFilenames(): List = distinctBy { it.fileName } fun List.filterTempFilter(): List = filterNot { it.isTempFile() } 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 f76821989008..c871f53150bc 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 @@ -36,6 +36,7 @@ import com.nextcloud.client.preferences.AppPreferences; import com.nextcloud.model.OfflineOperationType; import com.nextcloud.utils.LinkHelper; +import com.nextcloud.utils.extensions.OCFileExtensionsKt; import com.nextcloud.utils.extensions.ViewExtensionsKt; import com.nextcloud.utils.mdm.MDMConfig; import com.owncloud.android.MainApp; @@ -78,6 +79,7 @@ import java.util.Locale; import java.util.Set; import java.util.UUID; +import java.util.stream.Collectors; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -873,6 +875,15 @@ public void swapDirectory( } private void updateAdapter(List newFiles, OCFile directory) { + boolean hasSameContent = OCFileExtensionsKt.hasSameContentAs(mFiles, newFiles); + + if (hasSameContent) { + Log_OC.d(TAG, "same data passed skipping update"); + return; + } + + Log_OC.d(TAG, "updating the adapter"); + mFiles = new ArrayList<>(newFiles); mFilesAll.clear(); mFilesAll.addAll(mFiles); From fdf77da0bea1187d310a7f511f001bfeea90de6b Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 21 Nov 2025 14:51:19 +0100 Subject: [PATCH 13/32] only update if new data passed Signed-off-by: alperozturk --- .../utils/extensions/OCFileExtensions.kt | 1 + .../android/ui/adapter/OCFileListAdapter.java | 6 ++++ .../ui/fragment/OCFileListSearchTask.kt | 34 +++++++++---------- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/nextcloud/utils/extensions/OCFileExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/OCFileExtensions.kt index 9a7ba34629ce..5ed33a0cf57a 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/OCFileExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/OCFileExtensions.kt @@ -11,6 +11,7 @@ import com.owncloud.android.MainApp import com.owncloud.android.datamodel.OCFile import com.owncloud.android.utils.FileStorageUtils +@Suppress("ReturnCount") fun List.hasSameContentAs(other: List): Boolean { if (this.size != other.size) return false 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 c871f53150bc..71d6ec294bd4 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 @@ -972,6 +972,12 @@ public List getFiles() { return mFiles; } + public void addVirtualFile(@NonNull OCFile file) { + if (mFiles.isEmpty() || !mFiles.contains(file)) { + mFiles.add(file); + } + } + @Override public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) { super.onViewRecycled(holder); diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt index 8aff6ff429d7..9bc58f63930c 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt @@ -99,6 +99,7 @@ class OCFileListSearchTask( ) } else { parseAndSaveVirtuals(result.resultData ?: listOf(), fragment) + fragment.adapter.files } val sortedNewList = sortSearchData(newList, searchType, null, setNewSortOrder = { @@ -138,6 +139,7 @@ class OCFileListSearchTask( return rows.mapNotNull { storage.createFileInstance(it) } } + @Suppress("DEPRECATION") private suspend fun fetchRemoteResults(): RemoteOperationResult>? { val fragment = fragmentReference.get() ?: return null val context = fragment.context ?: return null @@ -194,10 +196,10 @@ class OCFileListSearchTask( return@withContext newList } - private suspend fun parseAndSaveVirtuals(data: List, fragment: OCFileListFragment): List = - withContext(Dispatchers.IO) { - val fileDataStorageManager = fileDataStorageManager ?: return@withContext listOf() - val activity = fragment.activity ?: return@withContext listOf() + @Suppress("DEPRECATION") + private suspend fun parseAndSaveVirtuals(data: List, fragment: OCFileListFragment) = withContext(Dispatchers.IO) { + val fileDataStorageManager = fileDataStorageManager ?: return@withContext + val activity = fragment.activity ?: return@withContext val now = System.currentTimeMillis() val (virtualType, onlyMedia) = when (fragment.currentSearchType) { @@ -206,15 +208,13 @@ class OCFileListSearchTask( else -> VirtualFolderType.NONE to false } - val contentValuesList = ArrayList(data.size) - val resultFiles = LinkedHashSet() + val contentValuesList = ArrayList() for (obj in data) { try { val remoteFile = obj as? RemoteFile ?: continue - var ocFile = FileStorageUtils.fillOCFile(remoteFile).apply { - FileStorageUtils.searchForLocalFileInDefaultPath(this, currentUser.accountName) - } + var ocFile = FileStorageUtils.fillOCFile(remoteFile) + FileStorageUtils.searchForLocalFileInDefaultPath(ocFile, currentUser.accountName) ocFile = fileDataStorageManager.saveFileWithParent(ocFile, activity) ocFile = handleEncryptionIfNeeded(ocFile, fileDataStorageManager, activity) @@ -234,15 +234,14 @@ class OCFileListSearchTask( !onlyMedia || MimeTypeUtil.isImage(ocFile) || MimeTypeUtil.isVideo(ocFile) if (isMediaAllowed) { - resultFiles.add(ocFile) + fragment.adapter.addVirtualFile(ocFile) } - contentValuesList.add( - ContentValues(2).apply { - put(ProviderMeta.ProviderTableMeta.VIRTUAL_TYPE, virtualType.toString()) - put(ProviderMeta.ProviderTableMeta.VIRTUAL_OCFILE_ID, ocFile.fileId) - } - ) + val cv = ContentValues().apply { + put(ProviderMeta.ProviderTableMeta.VIRTUAL_TYPE, virtualType.toString()) + put(ProviderMeta.ProviderTableMeta.VIRTUAL_OCFILE_ID, ocFile.fileId) + } + contentValuesList.add(cv) } catch (_: Exception) { } } @@ -250,10 +249,9 @@ class OCFileListSearchTask( // Save timestamp + virtual entries preferences.setPhotoSearchTimestamp(System.currentTimeMillis()) fileDataStorageManager.saveVirtuals(contentValuesList) - - resultFiles.toList() } + @Suppress("DEPRECATION") private fun handleEncryptionIfNeeded( ocFile: OCFile, fileDataStorage: FileDataStorageManager, From 54a07493c434d7fc2a6770904e8db2456dbcf256 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 21 Nov 2025 15:12:29 +0100 Subject: [PATCH 14/32] fix state management Signed-off-by: alperozturk --- .../android/ui/fragment/SharedListFragmentIT.kt | 11 ++++------- .../android/ui/adapter/OCFileListAdapter.java | 14 +++++--------- .../android/ui/fragment/OCFileListFragment.java | 5 +++++ .../android/ui/fragment/OCFileListSearchTask.kt | 10 ++++++---- .../ui/interfaces/OCFileListFragmentInterface.java | 2 ++ 5 files changed, 22 insertions(+), 20 deletions(-) diff --git a/app/src/androidTest/java/com/owncloud/android/ui/fragment/SharedListFragmentIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/fragment/SharedListFragmentIT.kt index fbc2dbfe3fac..2dbb2c370ad1 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/fragment/SharedListFragmentIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/fragment/SharedListFragmentIT.kt @@ -171,13 +171,10 @@ internal class SharedListFragmentIT : AbstractIT() { val newList = runBlocking { OCShareToOCFileConverter.parseAndSaveShares(shares, storageManager, user.accountName) } - fragment.adapter.setSearchData( - newList, - SearchType.SHARED_FILTER, - storageManager, - true - ) - + fragment.adapter.run { + prepareForSearchData(storageManager, SearchType.SHARED_FILTER) + updateAdapter(newList, null) + } EspressoIdlingResource.decrement() val screenShotName = createName(testClassName + "_" + "showSharedFiles", "") 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 71d6ec294bd4..d3783dca6685 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 @@ -79,7 +79,6 @@ import java.util.Locale; import java.util.Set; import java.util.UUID; -import java.util.stream.Collectors; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -88,7 +87,6 @@ import androidx.recyclerview.widget.RecyclerView; import kotlin.Pair; import kotlin.Unit; -import kotlin.jvm.functions.Function2; import me.zhanghai.android.fastscroll.PopupTextProvider; /** @@ -861,6 +859,8 @@ public void swapDirectory( return; } + ocFileListFragmentInterface.setLoadingEmptyListState(); + helper.prepareFileList(directory, updatedStorageManager, onlyOnDevice, @@ -874,7 +874,7 @@ public void swapDirectory( }); } - private void updateAdapter(List newFiles, OCFile directory) { + public void updateAdapter(List newFiles, OCFile directory) { boolean hasSameContent = OCFileExtensionsKt.hasSameContentAs(mFiles, newFiles); if (hasSameContent) { @@ -897,13 +897,9 @@ private void updateAdapter(List newFiles, OCFile directory) { activity.runOnUiThread(this::notifyDataSetChanged); } - public void setSearchData(List newList, SearchType searchType, FileDataStorageManager storageManager, boolean clear) { + public void prepareForSearchData(FileDataStorageManager storageManager, SearchType searchType) { initStorageManagerShowShareAvatar(storageManager); - if (clear) { - clearSearchData(searchType); - } - - updateAdapter(newList, null); + clearSearchData(searchType); } private void initStorageManagerShowShareAvatar(FileDataStorageManager storageManager) { 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 196f66c96577..0ac28b890208 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 @@ -715,6 +715,11 @@ public void onHeaderClicked() { fda.startRichWorkspacePreview(file); } + @Override + public void setLoadingEmptyListState() { + setEmptyListMessage(EmptyListState.LOADING); + } + @Override public void showTemplate(@NonNull Creator creator, @NonNull String headline) { ChooseTemplateDialogFragment.newInstance(mFile, creator, headline).show(requireActivity().getSupportFragmentManager(), diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt index 9bc58f63930c..5c2176fc727c 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt @@ -77,7 +77,7 @@ class OCFileListSearchTask( val sortedFilesInDb = sortSearchData(filesInDb, searchType, null, setNewSortOrder = { fragment.adapter.setSortOrder(it) }) - updateAdapterData(fragment, sortedFilesInDb, false) + updateAdapterData(fragment, sortedFilesInDb) // updating cache and refreshing adapter val result = fetchRemoteResults() @@ -91,6 +91,8 @@ class OCFileListSearchTask( return@launch } + fragment.adapter.prepareForSearchData(fileDataStorageManager, fragment.currentSearchType) + val newList = if (searchType == SearchType.SHARED_FILTER) { OCShareToOCFileConverter.parseAndSaveShares( result.resultData ?: listOf(), @@ -106,7 +108,7 @@ class OCFileListSearchTask( fragment.adapter.setSortOrder(it) }) - updateAdapterData(fragment, sortedNewList, true) + updateAdapterData(fragment, sortedNewList) return@launch } @@ -154,14 +156,14 @@ class OCFileListSearchTask( } } - private suspend fun updateAdapterData(fragment: OCFileListFragment, newList: List, clear: Boolean) = + private suspend fun updateAdapterData(fragment: OCFileListFragment, newList: List) = withContext(Dispatchers.Main) { if (!fragment.isAdded || !fragment.searchFragment) { Log_OC.e(TAG, "cannot update adapter data, fragment is not ready") return@withContext } - fragment.adapter.setSearchData(newList, fragment.currentSearchType, fileDataStorageManager, clear) + fragment.adapter.updateAdapter(newList, null) } private suspend fun sortSearchData( diff --git a/app/src/main/java/com/owncloud/android/ui/interfaces/OCFileListFragmentInterface.java b/app/src/main/java/com/owncloud/android/ui/interfaces/OCFileListFragmentInterface.java index 2d47d841299e..5a45e185d634 100644 --- a/app/src/main/java/com/owncloud/android/ui/interfaces/OCFileListFragmentInterface.java +++ b/app/src/main/java/com/owncloud/android/ui/interfaces/OCFileListFragmentInterface.java @@ -35,4 +35,6 @@ public interface OCFileListFragmentInterface { boolean isLoading(); void onHeaderClicked(); + + void setLoadingEmptyListState(); } From 1107c08f3e0300ff98e8dde0490b971599ccb16d Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 21 Nov 2025 15:58:53 +0100 Subject: [PATCH 15/32] faster filtering Signed-off-by: alperozturk # Conflicts: # app/src/main/java/com/nextcloud/utils/extensions/OCFileExtensions.kt --- .../android/ui/adapter/OCFileListAdapter.java | 7 +- .../adapter/helper/OCFileListAdapterHelper.kt | 68 ++++++++++++------- .../ui/fragment/OCFileListFragment.java | 5 +- .../ui/fragment/OCFileListSearchTask.kt | 3 +- .../OCFileListFragmentInterface.java | 3 +- 5 files changed, 56 insertions(+), 30 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 d3783dca6685..2efa3ad32669 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 @@ -59,6 +59,7 @@ import com.owncloud.android.ui.activity.ComponentsGetter; import com.owncloud.android.ui.activity.FileDisplayActivity; import com.owncloud.android.ui.adapter.helper.OCFileListAdapterHelper; +import com.owncloud.android.ui.fragment.EmptyListState; import com.owncloud.android.ui.fragment.OCFileListFragment; import com.owncloud.android.ui.fragment.SearchType; import com.owncloud.android.ui.interfaces.OCFileListFragmentInterface; @@ -859,7 +860,7 @@ public void swapDirectory( return; } - ocFileListFragmentInterface.setLoadingEmptyListState(); + ocFileListFragmentInterface.setNewEmptyListState(EmptyListState.LOADING); helper.prepareFileList(directory, updatedStorageManager, @@ -895,6 +896,10 @@ public void updateAdapter(List newFiles, OCFile directory) { searchType = null; activity.runOnUiThread(this::notifyDataSetChanged); + + if (mFiles.isEmpty()) { + ocFileListFragmentInterface.setNewEmptyListState(SearchType.NO_SEARCH); + } } public void prepareForSearchData(FileDataStorageManager storageManager, SearchType searchType) { 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/helper/OCFileListAdapterHelper.kt index a9343af2a82c..69122c02b425 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterHelper.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterHelper.kt @@ -9,11 +9,8 @@ package com.owncloud.android.ui.adapter.helper import com.nextcloud.client.database.entity.FileEntity import com.nextcloud.client.preferences.AppPreferences -import com.nextcloud.utils.extensions.filterByMimeType import com.nextcloud.utils.extensions.filterFilenames -import com.nextcloud.utils.extensions.filterHiddenFiles -import com.nextcloud.utils.extensions.filterTempFilter -import com.nextcloud.utils.extensions.limitToPersonalFiles +import com.nextcloud.utils.extensions.isTempFile import com.owncloud.android.MainApp import com.owncloud.android.R import com.owncloud.android.datamodel.FileDataStorageManager @@ -27,6 +24,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.util.ArrayList class OCFileListAdapterHelper { private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) @@ -43,36 +41,56 @@ class OCFileListAdapterHelper { onComplete: (List, FileSortOrder) -> Unit ) { job = scope.launch { - var result = getFolderContent(directory, storageManager, onlyOnDevice) + val showHiddenFiles = preferences.isShowHiddenFilesEnabled() + val hasMimeTypeFilter = limitToMimeType.isNotEmpty() + val isRootAndPersonalOnly = (OCFile.ROOT_PATH == directory.remotePath && MainApp.isOnlyPersonFiles()) + val isSharedView = (DrawerActivity.menuItemId == R.id.nav_shared) + val isFavoritesView = (DrawerActivity.menuItemId == R.id.nav_favorites) + + val rawResult = getFolderContent(directory, storageManager, onlyOnDevice) + val filtered = ArrayList(rawResult.size) + + for (file in rawResult) { + if (!showHiddenFiles && file.isHidden) { + continue + } - if (!preferences.isShowHiddenFilesEnabled()) { - result = result.filterHiddenFiles() - } + if (hasMimeTypeFilter && !(file.isFolder || file.mimeType.startsWith(limitToMimeType))) { + continue + } - if (!limitToMimeType.isEmpty()) { - result = result.filterByMimeType(limitToMimeType) - } + if (isRootAndPersonalOnly) { + val isPersonal = file.ownerId?.let { ownerId -> + ownerId == userId && !file.isSharedWithMe && !file.mounted() + } == true - if (OCFile.ROOT_PATH == directory.remotePath && MainApp.isOnlyPersonFiles()) { - result = result.limitToPersonalFiles(userId) - } + if (!isPersonal) { + continue + } + } - if (DrawerActivity.menuItemId == R.id.nav_shared) { - result = result.filter { it.isShared } - } + if (isSharedView && !file.isShared) { + continue + } + + if (isFavoritesView && !file.isFavorite) { + continue + } + + if (file.isTempFile()) { + continue + } - if (DrawerActivity.menuItemId == R.id.nav_favorites) { - result = result.filter { it.isFavorite } + filtered.add(file) } - result = result.filterTempFilter() - result = result.filterFilenames() - result = mergeOCFilesForLivePhoto(result) - result = addOfflineOperations(result, directory.fileId, storageManager) - val (newList, newSortOrder) = sortData(directory, result, preferences) + val afterFilenameFilter = filtered.filterFilenames() + val merged = mergeOCFilesForLivePhoto(afterFilenameFilter) + val finalList = addOfflineOperations(merged, directory.fileId, storageManager) + val (sortedList, sortOrder) = sortData(directory, finalList, preferences) withContext(Dispatchers.Main) { - onComplete(newList, newSortOrder) + onComplete(sortedList, sortOrder) } } } 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 0ac28b890208..75920628210c 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 @@ -22,6 +22,7 @@ import android.os.Bundle; import android.os.Handler; import android.os.Looper; +import android.os.Parcelable; import android.text.TextUtils; import android.util.Log; import android.util.Pair; @@ -716,8 +717,8 @@ public void onHeaderClicked() { } @Override - public void setLoadingEmptyListState() { - setEmptyListMessage(EmptyListState.LOADING); + public void setNewEmptyListState(Parcelable emptyListState) { + setEmptyListMessage(emptyListState); } @Override diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt index 5c2176fc727c..23c3c0c3dcd5 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt @@ -199,7 +199,8 @@ class OCFileListSearchTask( } @Suppress("DEPRECATION") - private suspend fun parseAndSaveVirtuals(data: List, fragment: OCFileListFragment) = withContext(Dispatchers.IO) { + private suspend fun parseAndSaveVirtuals(data: List, fragment: OCFileListFragment) = + withContext(Dispatchers.IO) { val fileDataStorageManager = fileDataStorageManager ?: return@withContext val activity = fragment.activity ?: return@withContext val now = System.currentTimeMillis() diff --git a/app/src/main/java/com/owncloud/android/ui/interfaces/OCFileListFragmentInterface.java b/app/src/main/java/com/owncloud/android/ui/interfaces/OCFileListFragmentInterface.java index 5a45e185d634..a5308a7c5184 100644 --- a/app/src/main/java/com/owncloud/android/ui/interfaces/OCFileListFragmentInterface.java +++ b/app/src/main/java/com/owncloud/android/ui/interfaces/OCFileListFragmentInterface.java @@ -7,6 +7,7 @@ */ package com.owncloud.android.ui.interfaces; +import android.os.Parcelable; import android.view.View; import com.owncloud.android.datamodel.OCFile; @@ -36,5 +37,5 @@ public interface OCFileListFragmentInterface { void onHeaderClicked(); - void setLoadingEmptyListState(); + void setNewEmptyListState(Parcelable state); } From ac25f9b0a4a023c905fca2035576c9d5b6568688 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Mon, 24 Nov 2025 11:26:09 +0100 Subject: [PATCH 16/32] do not set empty list state outside of the empty list recycler view Signed-off-by: alperozturk --- .../com/owncloud/android/ui/adapter/OCFileListAdapter.java | 7 ------- .../owncloud/android/ui/fragment/OCFileListFragment.java | 6 ------ .../android/ui/interfaces/OCFileListFragmentInterface.java | 3 --- 3 files changed, 16 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 2efa3ad32669..6ddbe857065a 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 @@ -59,7 +59,6 @@ import com.owncloud.android.ui.activity.ComponentsGetter; import com.owncloud.android.ui.activity.FileDisplayActivity; import com.owncloud.android.ui.adapter.helper.OCFileListAdapterHelper; -import com.owncloud.android.ui.fragment.EmptyListState; import com.owncloud.android.ui.fragment.OCFileListFragment; import com.owncloud.android.ui.fragment.SearchType; import com.owncloud.android.ui.interfaces.OCFileListFragmentInterface; @@ -860,8 +859,6 @@ public void swapDirectory( return; } - ocFileListFragmentInterface.setNewEmptyListState(EmptyListState.LOADING); - helper.prepareFileList(directory, updatedStorageManager, onlyOnDevice, @@ -896,10 +893,6 @@ public void updateAdapter(List newFiles, OCFile directory) { searchType = null; activity.runOnUiThread(this::notifyDataSetChanged); - - if (mFiles.isEmpty()) { - ocFileListFragmentInterface.setNewEmptyListState(SearchType.NO_SEARCH); - } } public void prepareForSearchData(FileDataStorageManager storageManager, SearchType searchType) { 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 75920628210c..196f66c96577 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 @@ -22,7 +22,6 @@ import android.os.Bundle; import android.os.Handler; import android.os.Looper; -import android.os.Parcelable; import android.text.TextUtils; import android.util.Log; import android.util.Pair; @@ -716,11 +715,6 @@ public void onHeaderClicked() { fda.startRichWorkspacePreview(file); } - @Override - public void setNewEmptyListState(Parcelable emptyListState) { - setEmptyListMessage(emptyListState); - } - @Override public void showTemplate(@NonNull Creator creator, @NonNull String headline) { ChooseTemplateDialogFragment.newInstance(mFile, creator, headline).show(requireActivity().getSupportFragmentManager(), diff --git a/app/src/main/java/com/owncloud/android/ui/interfaces/OCFileListFragmentInterface.java b/app/src/main/java/com/owncloud/android/ui/interfaces/OCFileListFragmentInterface.java index a5308a7c5184..2d47d841299e 100644 --- a/app/src/main/java/com/owncloud/android/ui/interfaces/OCFileListFragmentInterface.java +++ b/app/src/main/java/com/owncloud/android/ui/interfaces/OCFileListFragmentInterface.java @@ -7,7 +7,6 @@ */ package com.owncloud.android.ui.interfaces; -import android.os.Parcelable; import android.view.View; import com.owncloud.android.datamodel.OCFile; @@ -36,6 +35,4 @@ public interface OCFileListFragmentInterface { boolean isLoading(); void onHeaderClicked(); - - void setNewEmptyListState(Parcelable state); } From 1f92478c6195c1832ccfc7e4c58716d3f6598764 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Mon, 24 Nov 2025 13:48:04 +0100 Subject: [PATCH 17/32] do not set empty list state outside of the empty list recycler view Signed-off-by: alperozturk --- .../android/ui/adapter/helper/OCFileListAdapterHelper.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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/helper/OCFileListAdapterHelper.kt index 69122c02b425..6226c76e86cb 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterHelper.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterHelper.kt @@ -111,7 +111,7 @@ class OCFileListAdapterHelper { } @Suppress("NestedBlockDepth") - fun mergeOCFilesForLivePhoto(files: List): List { + private fun mergeOCFilesForLivePhoto(files: List): List { val filesToRemove = mutableSetOf() for (i in files.indices) { From 77f31515501276bde3ab47096def813a6aaf5511 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Tue, 25 Nov 2025 15:55:53 +0100 Subject: [PATCH 18/32] add OCFileListAdapterHelperTest Signed-off-by: alperozturk --- .../OCFileListAdapterDataProviderImpl.kt | 22 ++ .../android/ui/adapter/OCFileListAdapter.java | 9 +- .../helper/OCFileListAdapterDataProvider.kt | 17 ++ .../adapter/helper/OCFileListAdapterHelper.kt | 111 ++++++---- .../MockOCFileListAdapterDataProvider.kt | 203 ++++++++++++++++++ .../ui/adapter/OCFileListAdapterHelperTest.kt | 79 +++++++ 6 files changed, 393 insertions(+), 48 deletions(-) create mode 100644 app/src/main/java/com/owncloud/android/datamodel/OCFileListAdapterDataProviderImpl.kt create mode 100644 app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterDataProvider.kt create mode 100644 app/src/test/java/com/owncloud/android/ui/adapter/MockOCFileListAdapterDataProvider.kt create mode 100644 app/src/test/java/com/owncloud/android/ui/adapter/OCFileListAdapterHelperTest.kt diff --git a/app/src/main/java/com/owncloud/android/datamodel/OCFileListAdapterDataProviderImpl.kt b/app/src/main/java/com/owncloud/android/datamodel/OCFileListAdapterDataProviderImpl.kt new file mode 100644 index 000000000000..66622510612f --- /dev/null +++ b/app/src/main/java/com/owncloud/android/datamodel/OCFileListAdapterDataProviderImpl.kt @@ -0,0 +1,22 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.datamodel + +import com.nextcloud.client.database.entity.FileEntity +import com.owncloud.android.ui.adapter.helper.OCFileListAdapterDataProvider + +class OCFileListAdapterDataProviderImpl(private val storageManager: FileDataStorageManager) : + OCFileListAdapterDataProvider { + override fun convertToOCFiles(id: Long): List = + storageManager.offlineOperationsRepository.convertToOCFiles(id) + + override suspend fun getFolderContent(id: Long): List = + storageManager.fileDao.getFolderContentSuspended(id) + + override fun createFileInstance(entity: FileEntity): OCFile = storageManager.createFileInstance(entity) +} 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 6ddbe857065a..579cd3120f68 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 @@ -47,6 +47,7 @@ import com.owncloud.android.databinding.ListItemBinding; import com.owncloud.android.datamodel.FileDataStorageManager; import com.owncloud.android.datamodel.OCFile; +import com.owncloud.android.datamodel.OCFileListAdapterDataProviderImpl; import com.owncloud.android.datamodel.SyncedFolderProvider; import com.owncloud.android.datamodel.ThumbnailsCacheManager; import com.owncloud.android.datamodel.VirtualFolderType; @@ -58,6 +59,7 @@ import com.owncloud.android.lib.resources.tags.Tag; import com.owncloud.android.ui.activity.ComponentsGetter; import com.owncloud.android.ui.activity.FileDisplayActivity; +import com.owncloud.android.ui.adapter.helper.OCFileListAdapterDataProvider; import com.owncloud.android.ui.adapter.helper.OCFileListAdapterHelper; import com.owncloud.android.ui.fragment.OCFileListFragment; import com.owncloud.android.ui.fragment.SearchType; @@ -107,6 +109,7 @@ public class OCFileListAdapter extends RecyclerView.Adapter listOfHiddenFiles = new ArrayList<>(); private FileDataStorageManager mStorageManager; + private OCFileListAdapterDataProvider adapterDataProvider; private User user; private final OCFileListFragmentInterface ocFileListFragmentInterface; private final boolean isRTL; @@ -161,6 +164,8 @@ public OCFileListAdapter( mStorageManager = new FileDataStorageManager(user, activity.getContentResolver()); } + adapterDataProvider = new OCFileListAdapterDataProviderImpl(mStorageManager); + userId = AccountManager .get(activity) .getUserData(this.user.toPlatformAccount(), @@ -850,6 +855,7 @@ public void swapDirectory( if (!updatedStorageManager.equals(mStorageManager)) { mStorageManager = updatedStorageManager; + adapterDataProvider = new OCFileListAdapterDataProviderImpl(mStorageManager); ocFileListDelegate.setShowShareAvatar(true); this.user = account; } @@ -860,7 +866,7 @@ public void swapDirectory( } helper.prepareFileList(directory, - updatedStorageManager, + adapterDataProvider, onlyOnDevice, limitToMimeType, preferences, @@ -907,6 +913,7 @@ private void initStorageManagerShowShareAvatar(FileDataStorageManager storageMan : new FileDataStorageManager(user, activity.getContentResolver()); if (storageManager != null) { + adapterDataProvider = new OCFileListAdapterDataProviderImpl(mStorageManager); ocFileListDelegate.setShowShareAvatar(true); } } 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/helper/OCFileListAdapterDataProvider.kt new file mode 100644 index 000000000000..191b7ce666e3 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterDataProvider.kt @@ -0,0 +1,17 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.ui.adapter.helper + +import com.nextcloud.client.database.entity.FileEntity +import com.owncloud.android.datamodel.OCFile + +interface OCFileListAdapterDataProvider { + fun convertToOCFiles(id: Long): List + suspend fun getFolderContent(id: Long): List + fun createFileInstance(entity: FileEntity): 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/helper/OCFileListAdapterHelper.kt index 6226c76e86cb..9d1fd774e87f 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterHelper.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterHelper.kt @@ -13,7 +13,6 @@ import com.nextcloud.utils.extensions.filterFilenames import com.nextcloud.utils.extensions.isTempFile import com.owncloud.android.MainApp import com.owncloud.android.R -import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.OCFile import com.owncloud.android.ui.activity.DrawerActivity import com.owncloud.android.utils.FileSortOrder @@ -33,7 +32,7 @@ class OCFileListAdapterHelper { @Suppress("LongParameterList") fun prepareFileList( directory: OCFile, - storageManager: FileDataStorageManager, + dataProvider: OCFileListAdapterDataProvider, onlyOnDevice: Boolean, limitToMimeType: String, preferences: AppPreferences, @@ -41,66 +40,83 @@ class OCFileListAdapterHelper { onComplete: (List, FileSortOrder) -> Unit ) { job = scope.launch { - val showHiddenFiles = preferences.isShowHiddenFilesEnabled() - val hasMimeTypeFilter = limitToMimeType.isNotEmpty() - val isRootAndPersonalOnly = (OCFile.ROOT_PATH == directory.remotePath && MainApp.isOnlyPersonFiles()) - val isSharedView = (DrawerActivity.menuItemId == R.id.nav_shared) - val isFavoritesView = (DrawerActivity.menuItemId == R.id.nav_favorites) - - val rawResult = getFolderContent(directory, storageManager, onlyOnDevice) - val filtered = ArrayList(rawResult.size) - - for (file in rawResult) { - if (!showHiddenFiles && file.isHidden) { - continue - } - - if (hasMimeTypeFilter && !(file.isFolder || file.mimeType.startsWith(limitToMimeType))) { - continue - } - - if (isRootAndPersonalOnly) { - val isPersonal = file.ownerId?.let { ownerId -> - ownerId == userId && !file.isSharedWithMe && !file.mounted() - } == true + val (sortedList, sortOrder) = prepareFileList( + directory, + dataProvider, + onlyOnDevice, + limitToMimeType, + preferences, + userId + ) + withContext(Dispatchers.Main) { + onComplete(sortedList, sortOrder) + } + } + } - if (!isPersonal) { - continue - } - } + suspend fun prepareFileList( + directory: OCFile, + dataProvider: OCFileListAdapterDataProvider, + onlyOnDevice: Boolean, + limitToMimeType: String, + preferences: AppPreferences, + userId: String + ): Pair, FileSortOrder> { + val showHiddenFiles = preferences.isShowHiddenFilesEnabled() + val hasMimeTypeFilter = limitToMimeType.isNotEmpty() + val isRootAndPersonalOnly = (OCFile.ROOT_PATH == directory.remotePath && MainApp.isOnlyPersonFiles()) + val isSharedView = (DrawerActivity.menuItemId == R.id.nav_shared) + val isFavoritesView = (DrawerActivity.menuItemId == R.id.nav_favorites) + + val rawResult = getFolderContent(directory, dataProvider, onlyOnDevice) + val filtered = ArrayList(rawResult.size) + + for (file in rawResult) { + if (!showHiddenFiles && file.isHidden) { + continue + } - if (isSharedView && !file.isShared) { - continue - } + if (hasMimeTypeFilter && !(file.isFolder || file.mimeType.startsWith(limitToMimeType))) { + continue + } - if (isFavoritesView && !file.isFavorite) { - continue - } + if (isRootAndPersonalOnly) { + val isPersonal = file.ownerId?.let { ownerId -> + ownerId == userId && !file.isSharedWithMe && !file.mounted() + } == true - if (file.isTempFile()) { + if (!isPersonal) { continue } + } - filtered.add(file) + if (isSharedView && !file.isShared) { + continue } - val afterFilenameFilter = filtered.filterFilenames() - val merged = mergeOCFilesForLivePhoto(afterFilenameFilter) - val finalList = addOfflineOperations(merged, directory.fileId, storageManager) - val (sortedList, sortOrder) = sortData(directory, finalList, preferences) + if (isFavoritesView && !file.isFavorite) { + continue + } - withContext(Dispatchers.Main) { - onComplete(sortedList, sortOrder) + if (file.isTempFile()) { + continue } + + filtered.add(file) } + + val afterFilenameFilter = filtered.filterFilenames() + val merged = mergeOCFilesForLivePhoto(afterFilenameFilter) + val finalList = addOfflineOperations(merged, directory.fileId, dataProvider) + return sortData(directory, finalList, preferences) } private fun addOfflineOperations( files: List, fileId: Long, - storageManager: FileDataStorageManager + dataProvider: OCFileListAdapterDataProvider ): List { - val offlineOperations = storageManager.offlineOperationsRepository.convertToOCFiles(fileId) + val offlineOperations = dataProvider.convertToOCFiles(fileId) if (offlineOperations.isEmpty()) return files val newFiles = offlineOperations.filter { offlineFile -> @@ -128,6 +144,7 @@ class OCFileListAdapterHelper { nextFile.livePhotoVideo = file filesToRemove.add(file) } + MimeTypeUtil.isVideo(nextFile.mimeType) -> { file.livePhotoVideo = nextFile filesToRemove.add(nextFile) @@ -156,17 +173,17 @@ class OCFileListAdapterHelper { private suspend fun getFolderContent( ocFile: OCFile, - storageManager: FileDataStorageManager, + dataProvider: OCFileListAdapterDataProvider, onlyOnDevice: Boolean ): List = withContext(Dispatchers.IO) { if (!ocFile.isFolder || !ocFile.fileExists()) { return@withContext emptyList() } - val fileEntities: List = storageManager.fileDao.getFolderContentSuspended(ocFile.fileId) + val fileEntities: List = dataProvider.getFolderContent(ocFile.fileId) return@withContext fileEntities.mapNotNull { fileEntity -> - val file = storageManager.createFileInstance(fileEntity) + val file = dataProvider.createFileInstance(fileEntity) if (!onlyOnDevice || file.existsOnDevice()) { file } else { 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 new file mode 100644 index 000000000000..911476991821 --- /dev/null +++ b/app/src/test/java/com/owncloud/android/ui/adapter/MockOCFileListAdapterDataProvider.kt @@ -0,0 +1,203 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +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.utils.MimeType + +@Suppress("LongParameterList", "MagicNumber") +class MockOCFileListAdapterDataProvider : OCFileListAdapterDataProvider { + + private val directory = OCFile(OCFile.ROOT_PATH).apply { + setFolder() + fileId = 101L + ownerId = "user123" + remoteId = "0" + fileId = 0 + } + private val hidden = createTestOCFile( + directory.fileId, + "/.hidden.jpg", + 1, + ownerId = "user123", + mimeType = MimeType.JPEG, + isHidden = true, + localPath = "/local/hidden.jpg" + ) + private val image = createTestOCFile( + directory.fileId, + "/image.jpg", + 2, + ownerId = "user123", + mimeType = MimeType.JPEG, + localPath = "/local/image.jpg" + ) + private val video = createTestOCFile( + directory.fileId, + "/video.mp4", + 3, + ownerId = "user123", + mimeType = "video/mp4", + localPath = "/local/video.mp4" + ) + private val temp = createTestOCFile( + directory.fileId, + "/temp.tmp", + 4, + ownerId = "user123", + mimeType = MimeType.FILE, + localPath = "/local/temp.tmp" + ) + private val otherUsersFile = + createTestOCFile(202, "/other.jpg", 5, ownerId = "x", mimeType = MimeType.JPEG, localPath = "/local/other.jpg") + private val personal = createTestOCFile( + directory.fileId, + "/personal.jpg", + 6, + ownerId = "user123", + mimeType = MimeType.JPEG, + localPath = "/local/personal.jpg" + ) + private val shared = createTestOCFile( + directory.fileId, + "/shared.jpg", + 7, + ownerId = "user123", + mimeType = MimeType.JPEG, + isSharedViaLink = true, + localPath = "/local/shared.jpg" + ) + private val favorite = createTestOCFile( + directory.fileId, + "/favorite.jpg", + 8, + ownerId = "user123", + mimeType = MimeType.JPEG, + isFavorite = true, + localPath = "/local/favorite.jpg" + ) + private val livePhotoImg = createTestOCFile( + directory.fileId, + "/live.jpg", + 9, + ownerId = "user123", + mimeType = MimeType.JPEG, + localId = 77, + localPath = "/local/live.jpg" + ) + private val livePhotoVideo = createTestOCFile( + directory.fileId, + "/live_video.mp4", + 10, + ownerId = "user123", + mimeType = "video/mp4", + localPath = "/local/live_video.mp4" + ).apply { + setLivePhoto("77") + } + private val offlineOCFile = createTestOCFile( + directory.fileId, + "/offline.jpg", + 11, + ownerId = "user123", + mimeType = MimeType.JPEG, + localPath = "/local/offline.jpg" + ) + private val files = + listOf(hidden, image, video, temp, otherUsersFile, personal, shared, favorite, livePhotoImg, livePhotoVideo) + private val entities: List = files.map { file -> + FileEntity( + id = file.fileId, + name = file.fileName, + path = file.remotePath ?: file.fileName, + pathDecrypted = file.remotePath ?: file.fileName, + contentType = file.mimeType ?: MimeType.FILE, + accountOwner = file.ownerId ?: "unknown", + favorite = if (file.isFavorite) 1 else 0, + hidden = if (file.isHidden) 1 else 0, + sharedViaLink = if (file.isSharedViaLink) 1 else 0, + encryptedName = null, + parent = file.parentId, + creation = 0L, + modified = 0L, + contentLength = 0L, + storagePath = file.storagePath, + lastSyncDate = 0L, + lastSyncDateForData = 0L, + modifiedAtLastSyncForData = 0L, + etag = file.etag, + etagOnServer = null, + permissions = null, + remoteId = file.remoteId, + localId = file.localId, + updateThumbnail = 0, + isDownloading = 0, + isEncrypted = 0, + etagInConflict = null, + sharedWithSharee = 0, + mountType = 0, + hasPreview = 0, + unreadCommentsCount = 0, + ownerId = file.ownerId ?: "unknown", + ownerDisplayName = null, + note = null, + sharees = null, + richWorkspace = null, + metadataSize = null, + metadataLivePhoto = null, + locked = 0, + lockType = 0, + lockOwner = null, + lockOwnerDisplayName = null, + lockOwnerEditor = null, + lockTimestamp = 0L, + lockTimeout = 0, + lockToken = null, + tags = null, + metadataGPS = null, + e2eCounter = 0L, + internalTwoWaySync = 0L, + internalTwoWaySyncResult = null, + uploaded = 0L + ) + } + + override fun convertToOCFiles(id: Long): List = listOf(offlineOCFile) + + override suspend fun getFolderContent(id: Long): List = entities.filter { it.parent == id } + + override fun createFileInstance(entity: FileEntity): OCFile = files.first { it.fileId == entity.id } + + private fun createTestOCFile( + parentId: Long, + path: String, + fileId: Long, + ownerId: String? = null, + mimeType: String? = MimeType.FILE, + isHidden: Boolean = false, + isFavorite: Boolean = false, + isSharedViaLink: Boolean = false, + localId: Long = -1, + etag: String = "etag_$fileId", + localPath: String? = null + ): OCFile = OCFile(path).apply { + this.parentId = parentId + this.fileId = fileId + this.remotePath = path + this.ownerId = ownerId + this.mimeType = mimeType + this.isHidden = isHidden + this.isFavorite = isFavorite + this.isSharedViaLink = isSharedViaLink + this.localId = localId + this.etag = etag + this.storagePath = localPath + } +} 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 new file mode 100644 index 000000000000..694f1ba44ada --- /dev/null +++ b/app/src/test/java/com/owncloud/android/ui/adapter/OCFileListAdapterHelperTest.kt @@ -0,0 +1,79 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.ui.adapter + +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.utils.FileSortOrder +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test + +class OCFileListAdapterHelperTest { + + private val context = mockk(relaxed = true) + private val helper = OCFileListAdapterHelper() + + private val preferences = mockk(relaxed = true) + private val dataProvider = MockOCFileListAdapterDataProvider() + private lateinit var directory: OCFile + + @OptIn(ExperimentalCoroutinesApi::class) + @Before + fun setUp() { + mockkStatic(MainApp::class) + every { MainApp.getAppContext() } returns context + every { MainApp.isOnlyPersonFiles() } returns false + directory = OCFile(OCFile.ROOT_PATH).apply { + setFolder() + fileId = 101L + ownerId = "user123" + remoteId = "0" + fileId = 0 + } + } + + @Test + fun `prepareFileList dont show hidden files and sort a to z`() = runBlocking { + val userId = "user123" + + every { preferences.isShowHiddenFilesEnabled() } returns false + every { preferences.getSortOrderByFolder(directory) } returns FileSortOrder.SORT_A_TO_Z + every { preferences.isSortFoldersBeforeFiles() } returns true + every { preferences.isSortFavoritesFirst() } returns false + + val (list, sort) = helper.prepareFileList( + directory = directory, + dataProvider = dataProvider, + onlyOnDevice = false, + limitToMimeType = "image", + preferences = preferences, + userId = userId + ) + + val expected = listOf( + "favorite.jpg", + "image.jpg", + "live.jpg", + "offline.jpg", + "personal.jpg", + "shared.jpg" + ) + + assertEquals(expected, list.map { it.fileName }) + assertEquals(FileSortOrder.SORT_A_TO_Z, sort) + } +} From 75e59553ceee35c9aaa628231e9157ef6dba5eb9 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Wed, 26 Nov 2025 08:40:44 +0100 Subject: [PATCH 19/32] add more OCFileListAdapterHelperTest Signed-off-by: alperozturk --- .../MockOCFileListAdapterDataProvider.kt | 144 +------ .../ui/adapter/OCFileListAdapterHelperTest.kt | 398 ++++++++++++++++++ 2 files changed, 417 insertions(+), 125 deletions(-) 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 911476991821..c2b71334a569 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 @@ -15,104 +15,10 @@ import com.owncloud.android.utils.MimeType @Suppress("LongParameterList", "MagicNumber") class MockOCFileListAdapterDataProvider : OCFileListAdapterDataProvider { - private val directory = OCFile(OCFile.ROOT_PATH).apply { - setFolder() - fileId = 101L - ownerId = "user123" - remoteId = "0" - fileId = 0 - } - private val hidden = createTestOCFile( - directory.fileId, - "/.hidden.jpg", - 1, - ownerId = "user123", - mimeType = MimeType.JPEG, - isHidden = true, - localPath = "/local/hidden.jpg" - ) - private val image = createTestOCFile( - directory.fileId, - "/image.jpg", - 2, - ownerId = "user123", - mimeType = MimeType.JPEG, - localPath = "/local/image.jpg" - ) - private val video = createTestOCFile( - directory.fileId, - "/video.mp4", - 3, - ownerId = "user123", - mimeType = "video/mp4", - localPath = "/local/video.mp4" - ) - private val temp = createTestOCFile( - directory.fileId, - "/temp.tmp", - 4, - ownerId = "user123", - mimeType = MimeType.FILE, - localPath = "/local/temp.tmp" - ) - private val otherUsersFile = - createTestOCFile(202, "/other.jpg", 5, ownerId = "x", mimeType = MimeType.JPEG, localPath = "/local/other.jpg") - private val personal = createTestOCFile( - directory.fileId, - "/personal.jpg", - 6, - ownerId = "user123", - mimeType = MimeType.JPEG, - localPath = "/local/personal.jpg" - ) - private val shared = createTestOCFile( - directory.fileId, - "/shared.jpg", - 7, - ownerId = "user123", - mimeType = MimeType.JPEG, - isSharedViaLink = true, - localPath = "/local/shared.jpg" - ) - private val favorite = createTestOCFile( - directory.fileId, - "/favorite.jpg", - 8, - ownerId = "user123", - mimeType = MimeType.JPEG, - isFavorite = true, - localPath = "/local/favorite.jpg" - ) - private val livePhotoImg = createTestOCFile( - directory.fileId, - "/live.jpg", - 9, - ownerId = "user123", - mimeType = MimeType.JPEG, - localId = 77, - localPath = "/local/live.jpg" - ) - private val livePhotoVideo = createTestOCFile( - directory.fileId, - "/live_video.mp4", - 10, - ownerId = "user123", - mimeType = "video/mp4", - localPath = "/local/live_video.mp4" - ).apply { - setLivePhoto("77") - } - private val offlineOCFile = createTestOCFile( - directory.fileId, - "/offline.jpg", - 11, - ownerId = "user123", - mimeType = MimeType.JPEG, - localPath = "/local/offline.jpg" - ) - private val files = - listOf(hidden, image, video, temp, otherUsersFile, personal, shared, favorite, livePhotoImg, livePhotoVideo) - private val entities: List = files.map { file -> + private var offlineOCFile: OCFile? = null + private var files = listOf() + + private fun getEntities(): List = files.map { file -> FileEntity( id = file.fileId, name = file.fileName, @@ -169,35 +75,23 @@ class MockOCFileListAdapterDataProvider : OCFileListAdapterDataProvider { ) } - override fun convertToOCFiles(id: Long): List = listOf(offlineOCFile) + fun setEntities(files: List) { + this.files = files + } - override suspend fun getFolderContent(id: Long): List = entities.filter { it.parent == id } + fun setOfflineFile(file: OCFile) { + offlineOCFile = file + } - override fun createFileInstance(entity: FileEntity): OCFile = files.first { it.fileId == entity.id } + override fun convertToOCFiles(id: Long): List = if (offlineOCFile != null) { + listOf(offlineOCFile!!) + } else { + listOf() + } - private fun createTestOCFile( - parentId: Long, - path: String, - fileId: Long, - ownerId: String? = null, - mimeType: String? = MimeType.FILE, - isHidden: Boolean = false, - isFavorite: Boolean = false, - isSharedViaLink: Boolean = false, - localId: Long = -1, - etag: String = "etag_$fileId", - localPath: String? = null - ): OCFile = OCFile(path).apply { - this.parentId = parentId - this.fileId = fileId - this.remotePath = path - this.ownerId = ownerId - this.mimeType = mimeType - this.isHidden = isHidden - this.isFavorite = isFavorite - this.isSharedViaLink = isSharedViaLink - this.localId = localId - this.etag = etag - this.storagePath = localPath + override suspend fun getFolderContent(id: Long): List = getEntities().filter { + it.parent == id && it.path != OCFile.ROOT_PATH } + + override fun createFileInstance(entity: FileEntity): OCFile = files.first { it.fileId == entity.id } } 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 694f1ba44ada..d58e3958f42a 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 @@ -13,6 +13,7 @@ import com.owncloud.android.MainApp import com.owncloud.android.datamodel.OCFile import com.owncloud.android.ui.adapter.helper.OCFileListAdapterHelper import com.owncloud.android.utils.FileSortOrder +import com.owncloud.android.utils.MimeType import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic @@ -22,6 +23,7 @@ import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test +@Suppress("LongMethod", "LongParameterList") class OCFileListAdapterHelperTest { private val context = mockk(relaxed = true) @@ -46,10 +48,380 @@ class OCFileListAdapterHelperTest { } } + @Test + fun `prepareFileList with multiple folders and sort z to a`() = runBlocking { + val userId = "user123" + val directory = OCFile(OCFile.ROOT_PATH).apply { + setFolder() + fileId = 101L + ownerId = "user123" + remoteId = "0" + fileId = 0 + } + val subDirectory = OCFile("/subDir").apply { + setFolder() + fileId = 102L + ownerId = "user123" + remoteId = "1" + fileId = 1 + parentId = 0 + } + val subDirectory2 = OCFile("/subDir2").apply { + setFolder() + fileId = 103L + ownerId = "user123" + remoteId = "2" + fileId = 2 + parentId = 0 + } + val file1 = createTestOCFile( + directory.fileId, + "/image.jpg", + 11, + ownerId = "user123", + mimeType = MimeType.JPEG, + localPath = "/local/image.jpg" + ) + val file2 = createTestOCFile( + directory.fileId, + "/video.mp4", + 12, + ownerId = "user123", + mimeType = "video/mp4", + localPath = "/local/video.mp4" + ) + val subFile1 = createTestOCFile( + subDirectory.fileId, + "/video2.mp4", + 21, + ownerId = "user123", + mimeType = "video/mp4", + localPath = "/local/video.mp4" + ) + + val files = + listOf(directory, subDirectory, subDirectory2, file1, file2, subFile1) + dataProvider.setEntities(files) + + every { preferences.isShowHiddenFilesEnabled() } returns false + every { preferences.getSortOrderByFolder(directory) } returns FileSortOrder.SORT_Z_TO_A + every { preferences.isSortFoldersBeforeFiles() } returns true + every { preferences.isSortFavoritesFirst() } returns true + + val (list, sort) = helper.prepareFileList( + directory = directory, + dataProvider = dataProvider, + onlyOnDevice = false, + limitToMimeType = "", + preferences = preferences, + userId = userId + ) + + val expected = listOf( + "subDir2", + "subDir", + "video.mp4", + "image.jpg" + ) + + assertEquals(expected, list.map { it.fileName }) + assertEquals(FileSortOrder.SORT_Z_TO_A, sort) + } + + @Test + fun `prepareFileList with multiple folders and favorites firsts`() = runBlocking { + val userId = "user123" + val directory = OCFile(OCFile.ROOT_PATH).apply { + setFolder() + fileId = 101L + ownerId = "user123" + remoteId = "0" + fileId = 0 + } + val subDirectory = OCFile("/subDir").apply { + setFolder() + fileId = 102L + ownerId = "user123" + remoteId = "1" + fileId = 1 + parentId = 0 + } + val subDirectory2 = OCFile("/subDir2").apply { + setFolder() + fileId = 103L + ownerId = "user123" + remoteId = "2" + fileId = 2 + parentId = 0 + } + val file1 = createTestOCFile( + directory.fileId, + "/image.jpg", + 11, + ownerId = "user123", + mimeType = MimeType.JPEG, + localPath = "/local/image.jpg" + ) + val file2 = createTestOCFile( + directory.fileId, + "/video.mp4", + 12, + ownerId = "user123", + mimeType = "video/mp4", + localPath = "/local/video.mp4" + ) + val subFile1 = createTestOCFile( + subDirectory.fileId, + "/video2.mp4", + 21, + ownerId = "user123", + mimeType = "video/mp4", + localPath = "/local/video.mp4" + ) + val file3 = createTestOCFile( + directory.fileId, + "/fav_image.jpg", + 19, + ownerId = "user123", + mimeType = MimeType.JPEG, + localPath = "/local/image9.jpg", + isFavorite = true + ) + + val files = + listOf(directory, subDirectory, subDirectory2, file1, file2, subFile1, file3) + dataProvider.setEntities(files) + + every { preferences.isShowHiddenFilesEnabled() } returns false + every { preferences.getSortOrderByFolder(directory) } returns FileSortOrder.SORT_A_TO_Z + every { preferences.isSortFoldersBeforeFiles() } returns true + every { preferences.isSortFavoritesFirst() } returns true + + val (list, sort) = helper.prepareFileList( + directory = directory, + dataProvider = dataProvider, + onlyOnDevice = false, + limitToMimeType = "", + preferences = preferences, + userId = userId + ) + + val expected = listOf( + "fav_image.jpg", + "subDir", + "subDir2", + "image.jpg", + "video.mp4" + ) + + assertEquals(expected, list.map { it.fileName }) + assertEquals(FileSortOrder.SORT_A_TO_Z, sort) + } + + @Test + fun `prepareFileList with multiple folders`() = runBlocking { + val userId = "user123" + val directory = OCFile(OCFile.ROOT_PATH).apply { + setFolder() + fileId = 101L + ownerId = "user123" + remoteId = "0" + fileId = 0 + } + val subDirectory = OCFile("/subDir").apply { + setFolder() + fileId = 102L + ownerId = "user123" + remoteId = "1" + fileId = 1 + parentId = 0 + } + val subDirectory2 = OCFile("/subDir2").apply { + setFolder() + fileId = 103L + ownerId = "user123" + remoteId = "2" + fileId = 2 + parentId = 0 + } + val file1 = createTestOCFile( + directory.fileId, + "/image.jpg", + 11, + ownerId = "user123", + mimeType = MimeType.JPEG, + localPath = "/local/image.jpg" + ) + val file2 = createTestOCFile( + directory.fileId, + "/video.mp4", + 12, + ownerId = "user123", + mimeType = "video/mp4", + localPath = "/local/video.mp4" + ) + val subFile1 = createTestOCFile( + subDirectory.fileId, + "/video2.mp4", + 21, + ownerId = "user123", + mimeType = "video/mp4", + localPath = "/local/video.mp4" + ) + + val files = + listOf(directory, subDirectory, subDirectory2, file1, file2, subFile1) + dataProvider.setEntities(files) + + every { preferences.isShowHiddenFilesEnabled() } returns false + every { preferences.getSortOrderByFolder(directory) } returns FileSortOrder.SORT_A_TO_Z + every { preferences.isSortFoldersBeforeFiles() } returns true + every { preferences.isSortFavoritesFirst() } returns false + + val (list, sort) = helper.prepareFileList( + directory = directory, + dataProvider = dataProvider, + onlyOnDevice = false, + limitToMimeType = "", + preferences = preferences, + userId = userId + ) + + val expected = listOf( + "subDir", + "subDir2", + "image.jpg", + "video.mp4" + ) + + assertEquals(expected, list.map { it.fileName }) + assertEquals(FileSortOrder.SORT_A_TO_Z, sort) + } + @Test fun `prepareFileList dont show hidden files and sort a to z`() = runBlocking { val userId = "user123" + val directory = OCFile(OCFile.ROOT_PATH).apply { + setFolder() + fileId = 101L + ownerId = "user123" + remoteId = "0" + fileId = 0 + } + val hidden = createTestOCFile( + directory.fileId, + "/.hidden.jpg", + 1, + ownerId = "user123", + mimeType = MimeType.JPEG, + isHidden = true, + localPath = "/local/hidden.jpg" + ) + val image = createTestOCFile( + directory.fileId, + "/image.jpg", + 2, + ownerId = "user123", + mimeType = MimeType.JPEG, + localPath = "/local/image.jpg" + ) + val video = createTestOCFile( + directory.fileId, + "/video.mp4", + 3, + ownerId = "user123", + mimeType = "video/mp4", + localPath = "/local/video.mp4" + ) + val temp = createTestOCFile( + directory.fileId, + "/temp.tmp", + 4, + ownerId = "user123", + mimeType = MimeType.FILE, + localPath = "/local/temp.tmp" + ) + val otherUsersFile = + createTestOCFile( + 202, + "/other.jpg", + 5, + ownerId = "x", + mimeType = MimeType.JPEG, + localPath = "/local/other.jpg" + ) + val personal = createTestOCFile( + directory.fileId, + "/personal.jpg", + 6, + ownerId = "user123", + mimeType = MimeType.JPEG, + localPath = "/local/personal.jpg" + ) + val shared = createTestOCFile( + directory.fileId, + "/shared.jpg", + 7, + ownerId = "user123", + mimeType = MimeType.JPEG, + isSharedViaLink = true, + localPath = "/local/shared.jpg" + ) + val favorite = createTestOCFile( + directory.fileId, + "/favorite.jpg", + 8, + ownerId = "user123", + mimeType = MimeType.JPEG, + isFavorite = true, + localPath = "/local/favorite.jpg" + ) + val livePhotoImg = createTestOCFile( + directory.fileId, + "/live.jpg", + 9, + ownerId = "user123", + mimeType = MimeType.JPEG, + localId = 77, + localPath = "/local/live.jpg" + ) + val livePhotoVideo = createTestOCFile( + directory.fileId, + "/live_video.mp4", + 10, + ownerId = "user123", + mimeType = "video/mp4", + localPath = "/local/live_video.mp4" + ).apply { + setLivePhoto("77") + } + val offlineOCFile = createTestOCFile( + directory.fileId, + "/offline.jpg", + 11, + ownerId = "user123", + mimeType = MimeType.JPEG, + localPath = "/local/offline.jpg" + ) + + val files = + listOf( + directory, + hidden, + image, + video, + temp, + otherUsersFile, + personal, + shared, + favorite, + livePhotoImg, + livePhotoVideo + ) + dataProvider.setEntities(files) + dataProvider.setOfflineFile(offlineOCFile) + every { preferences.isShowHiddenFilesEnabled() } returns false every { preferences.getSortOrderByFolder(directory) } returns FileSortOrder.SORT_A_TO_Z every { preferences.isSortFoldersBeforeFiles() } returns true @@ -76,4 +448,30 @@ class OCFileListAdapterHelperTest { assertEquals(expected, list.map { it.fileName }) assertEquals(FileSortOrder.SORT_A_TO_Z, sort) } + + private fun createTestOCFile( + parentId: Long, + path: String, + fileId: Long, + ownerId: String? = null, + mimeType: String? = MimeType.FILE, + isHidden: Boolean = false, + isFavorite: Boolean = false, + isSharedViaLink: Boolean = false, + localId: Long = -1, + etag: String = "etag_$fileId", + localPath: String? = null + ): OCFile = OCFile(path).apply { + this.parentId = parentId + this.fileId = fileId + this.remotePath = path + this.ownerId = ownerId + this.mimeType = mimeType + this.isHidden = isHidden + this.isFavorite = isFavorite + this.isSharedViaLink = isSharedViaLink + this.localId = localId + this.etag = etag + this.storagePath = localPath + } } From 62838621baad48e6b4568b4ac07e09ff53309f7a Mon Sep 17 00:00:00 2001 From: alperozturk Date: Wed, 26 Nov 2025 08:54:00 +0100 Subject: [PATCH 20/32] add more OCFileListAdapterHelperTest Signed-off-by: alperozturk --- .../com/owncloud/android/utils/MimeType.java | 1 + .../ui/adapter/OCFileListAdapterHelperTest.kt | 521 +++++------------- 2 files changed, 131 insertions(+), 391 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/utils/MimeType.java b/app/src/main/java/com/owncloud/android/utils/MimeType.java index c0e6642b8622..ed466eb0f2c0 100644 --- a/app/src/main/java/com/owncloud/android/utils/MimeType.java +++ b/app/src/main/java/com/owncloud/android/utils/MimeType.java @@ -22,6 +22,7 @@ public final class MimeType { public static final String TEXT_PLAIN = "text/plain"; public static final String FILE = "application/octet-stream"; public static final String PDF = "application/pdf"; + public static final String MP4 = "video/mp4"; private MimeType() { // No instance 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 d58e3958f42a..65db28cfe60a 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 @@ -31,7 +31,8 @@ class OCFileListAdapterHelperTest { private val preferences = mockk(relaxed = true) private val dataProvider = MockOCFileListAdapterDataProvider() - private lateinit var directory: OCFile + + private val userId = "user123" @OptIn(ExperimentalCoroutinesApi::class) @Before @@ -39,439 +40,177 @@ class OCFileListAdapterHelperTest { mockkStatic(MainApp::class) every { MainApp.getAppContext() } returns context every { MainApp.isOnlyPersonFiles() } returns false - directory = OCFile(OCFile.ROOT_PATH).apply { - setFolder() - fileId = 101L - ownerId = "user123" - remoteId = "0" - fileId = 0 - } } - @Test - fun `prepareFileList with multiple folders and sort z to a`() = runBlocking { - val userId = "user123" - val directory = OCFile(OCFile.ROOT_PATH).apply { - setFolder() - fileId = 101L - ownerId = "user123" - remoteId = "0" - fileId = 0 - } - val subDirectory = OCFile("/subDir").apply { - setFolder() - fileId = 102L - ownerId = "user123" - remoteId = "1" - fileId = 1 - parentId = 0 - } - val subDirectory2 = OCFile("/subDir2").apply { + private inner class Sut { + val root = directory("/", id = 0) + + fun directory(path: String, id: Long) = OCFile(path).apply { setFolder() - fileId = 103L - ownerId = "user123" - remoteId = "2" - fileId = 2 + fileId = id parentId = 0 + ownerId = userId + remoteId = id.toString() + remotePath = path + mimeType = MimeType.DIRECTORY + storagePath = "" + etag = "etag_$id" } - val file1 = createTestOCFile( - directory.fileId, - "/image.jpg", - 11, - ownerId = "user123", - mimeType = MimeType.JPEG, - localPath = "/local/image.jpg" - ) - val file2 = createTestOCFile( - directory.fileId, - "/video.mp4", - 12, - ownerId = "user123", - mimeType = "video/mp4", - localPath = "/local/video.mp4" - ) - val subFile1 = createTestOCFile( - subDirectory.fileId, - "/video2.mp4", - 21, - ownerId = "user123", - mimeType = "video/mp4", - localPath = "/local/video.mp4" - ) - val files = - listOf(directory, subDirectory, subDirectory2, file1, file2, subFile1) - dataProvider.setEntities(files) + fun file( + parent: OCFile, + name: String, + id: Long, + mime: String = MimeType.FILE, + hidden: Boolean = false, + favorite: Boolean = false, + shared: Boolean = false, + localId: Long = -1, + localPath: String = "" + ) = OCFile("/$name").apply { + parentId = parent.fileId + fileId = id + remotePath = "/$name" + ownerId = userId + mimeType = mime + isHidden = hidden + isFavorite = favorite + isSharedViaLink = shared + this.localId = localId + etag = "etag_$id" + storagePath = localPath + } - every { preferences.isShowHiddenFilesEnabled() } returns false - every { preferences.getSortOrderByFolder(directory) } returns FileSortOrder.SORT_Z_TO_A - every { preferences.isSortFoldersBeforeFiles() } returns true - every { preferences.isSortFavoritesFirst() } returns true + fun prepare(files: List, offline: OCFile? = null) { + dataProvider.setEntities(files) + offline?.let { dataProvider.setOfflineFile(it) } + } - val (list, sort) = helper.prepareFileList( + suspend fun run(directory: OCFile, mime: String = "") = helper.prepareFileList( directory = directory, dataProvider = dataProvider, onlyOnDevice = false, - limitToMimeType = "", + limitToMimeType = mime, preferences = preferences, userId = userId ) + } - val expected = listOf( - "subDir2", - "subDir", - "video.mp4", - "image.jpg" - ) + private fun stubPreferences( + showHidden: Boolean = false, + sort: FileSortOrder, + folderFirst: Boolean = true, + favFirst: Boolean = false + ) { + every { preferences.isShowHiddenFilesEnabled() } returns showHidden + every { preferences.getSortOrderByFolder(any()) } returns sort + every { preferences.isSortFoldersBeforeFiles() } returns folderFirst + every { preferences.isSortFavoritesFirst() } returns favFirst + } + + @Test + fun `prepareFileList with multiple folders and sort Z to A`() = runBlocking { + val env = Sut() + val root = env.root + + val sub1 = env.directory("/subDir", 1) + val sub2 = env.directory("/subDir2", 2) + + val fImage = env.file(root, "image.jpg", 11, MimeType.JPEG) + val fVideo = env.file(root, "video.mp4", 12, MimeType.MP4) + val fSub = env.file(sub1, "video2.mp4", 21, MimeType.MP4) + + env.prepare(listOf(root, sub1, sub2, fImage, fVideo, fSub)) - assertEquals(expected, list.map { it.fileName }) + stubPreferences(sort = FileSortOrder.SORT_Z_TO_A) + + val (list, sort) = env.run(root) + + assertEquals(listOf("subDir2", "subDir", "video.mp4", "image.jpg"), list.map { it.fileName }) assertEquals(FileSortOrder.SORT_Z_TO_A, sort) } @Test - fun `prepareFileList with multiple folders and favorites firsts`() = runBlocking { - val userId = "user123" - val directory = OCFile(OCFile.ROOT_PATH).apply { - setFolder() - fileId = 101L - ownerId = "user123" - remoteId = "0" - fileId = 0 - } - val subDirectory = OCFile("/subDir").apply { - setFolder() - fileId = 102L - ownerId = "user123" - remoteId = "1" - fileId = 1 - parentId = 0 - } - val subDirectory2 = OCFile("/subDir2").apply { - setFolder() - fileId = 103L - ownerId = "user123" - remoteId = "2" - fileId = 2 - parentId = 0 - } - val file1 = createTestOCFile( - directory.fileId, - "/image.jpg", - 11, - ownerId = "user123", - mimeType = MimeType.JPEG, - localPath = "/local/image.jpg" - ) - val file2 = createTestOCFile( - directory.fileId, - "/video.mp4", - 12, - ownerId = "user123", - mimeType = "video/mp4", - localPath = "/local/video.mp4" - ) - val subFile1 = createTestOCFile( - subDirectory.fileId, - "/video2.mp4", - 21, - ownerId = "user123", - mimeType = "video/mp4", - localPath = "/local/video.mp4" - ) - val file3 = createTestOCFile( - directory.fileId, - "/fav_image.jpg", - 19, - ownerId = "user123", - mimeType = MimeType.JPEG, - localPath = "/local/image9.jpg", - isFavorite = true - ) + fun `prepareFileList with multiple folders and favorites first`() = runBlocking { + val env = Sut() + val root = env.root - val files = - listOf(directory, subDirectory, subDirectory2, file1, file2, subFile1, file3) - dataProvider.setEntities(files) + val sub1 = env.directory("/subDir", 1) + val sub2 = env.directory("/subDir2", 2) - every { preferences.isShowHiddenFilesEnabled() } returns false - every { preferences.getSortOrderByFolder(directory) } returns FileSortOrder.SORT_A_TO_Z - every { preferences.isSortFoldersBeforeFiles() } returns true - every { preferences.isSortFavoritesFirst() } returns true + val fImage = env.file(root, "image.jpg", 11, MimeType.JPEG) + val fVideo = env.file(root, "video.mp4", 12, MimeType.MP4) + val fFav = env.file(root, "fav_image.jpg", 19, MimeType.JPEG, favorite = true) + val fSub = env.file(sub1, "video2.mp4", 21, MimeType.MP4) - val (list, sort) = helper.prepareFileList( - directory = directory, - dataProvider = dataProvider, - onlyOnDevice = false, - limitToMimeType = "", - preferences = preferences, - userId = userId - ) + env.prepare(listOf(root, sub1, sub2, fImage, fVideo, fFav, fSub)) - val expected = listOf( - "fav_image.jpg", - "subDir", - "subDir2", - "image.jpg", - "video.mp4" - ) + stubPreferences(sort = FileSortOrder.SORT_A_TO_Z, favFirst = true) + + val (list, sort) = env.run(root) - assertEquals(expected, list.map { it.fileName }) + assertEquals( + listOf("fav_image.jpg", "subDir", "subDir2", "image.jpg", "video.mp4"), + list.map { it.fileName } + ) assertEquals(FileSortOrder.SORT_A_TO_Z, sort) } @Test fun `prepareFileList with multiple folders`() = runBlocking { - val userId = "user123" - val directory = OCFile(OCFile.ROOT_PATH).apply { - setFolder() - fileId = 101L - ownerId = "user123" - remoteId = "0" - fileId = 0 - } - val subDirectory = OCFile("/subDir").apply { - setFolder() - fileId = 102L - ownerId = "user123" - remoteId = "1" - fileId = 1 - parentId = 0 - } - val subDirectory2 = OCFile("/subDir2").apply { - setFolder() - fileId = 103L - ownerId = "user123" - remoteId = "2" - fileId = 2 - parentId = 0 - } - val file1 = createTestOCFile( - directory.fileId, - "/image.jpg", - 11, - ownerId = "user123", - mimeType = MimeType.JPEG, - localPath = "/local/image.jpg" - ) - val file2 = createTestOCFile( - directory.fileId, - "/video.mp4", - 12, - ownerId = "user123", - mimeType = "video/mp4", - localPath = "/local/video.mp4" - ) - val subFile1 = createTestOCFile( - subDirectory.fileId, - "/video2.mp4", - 21, - ownerId = "user123", - mimeType = "video/mp4", - localPath = "/local/video.mp4" - ) + val env = Sut() + val root = env.root - val files = - listOf(directory, subDirectory, subDirectory2, file1, file2, subFile1) - dataProvider.setEntities(files) + val sub1 = env.directory("/subDir", 1) + val sub2 = env.directory("/subDir2", 2) - every { preferences.isShowHiddenFilesEnabled() } returns false - every { preferences.getSortOrderByFolder(directory) } returns FileSortOrder.SORT_A_TO_Z - every { preferences.isSortFoldersBeforeFiles() } returns true - every { preferences.isSortFavoritesFirst() } returns false + val fImg = env.file(root, "image.jpg", 11, MimeType.JPEG) + val fVid = env.file(root, "video.mp4", 12, MimeType.MP4) + val fSubVid = env.file(sub1, "video2.mp4", 21, MimeType.MP4) - val (list, sort) = helper.prepareFileList( - directory = directory, - dataProvider = dataProvider, - onlyOnDevice = false, - limitToMimeType = "", - preferences = preferences, - userId = userId - ) + env.prepare(listOf(root, sub1, sub2, fImg, fVid, fSubVid)) - val expected = listOf( - "subDir", - "subDir2", - "image.jpg", - "video.mp4" - ) + stubPreferences(sort = FileSortOrder.SORT_A_TO_Z) + + val (list, sort) = env.run(root) - assertEquals(expected, list.map { it.fileName }) + assertEquals(listOf("subDir", "subDir2", "image.jpg", "video.mp4"), list.map { it.fileName }) assertEquals(FileSortOrder.SORT_A_TO_Z, sort) } @Test - fun `prepareFileList dont show hidden files and sort a to z`() = runBlocking { - val userId = "user123" - - val directory = OCFile(OCFile.ROOT_PATH).apply { - setFolder() - fileId = 101L - ownerId = "user123" - remoteId = "0" - fileId = 0 - } - val hidden = createTestOCFile( - directory.fileId, - "/.hidden.jpg", - 1, - ownerId = "user123", - mimeType = MimeType.JPEG, - isHidden = true, - localPath = "/local/hidden.jpg" - ) - val image = createTestOCFile( - directory.fileId, - "/image.jpg", - 2, - ownerId = "user123", - mimeType = MimeType.JPEG, - localPath = "/local/image.jpg" - ) - val video = createTestOCFile( - directory.fileId, - "/video.mp4", - 3, - ownerId = "user123", - mimeType = "video/mp4", - localPath = "/local/video.mp4" - ) - val temp = createTestOCFile( - directory.fileId, - "/temp.tmp", - 4, - ownerId = "user123", - mimeType = MimeType.FILE, - localPath = "/local/temp.tmp" - ) - val otherUsersFile = - createTestOCFile( - 202, - "/other.jpg", - 5, - ownerId = "x", - mimeType = MimeType.JPEG, - localPath = "/local/other.jpg" - ) - val personal = createTestOCFile( - directory.fileId, - "/personal.jpg", - 6, - ownerId = "user123", - mimeType = MimeType.JPEG, - localPath = "/local/personal.jpg" - ) - val shared = createTestOCFile( - directory.fileId, - "/shared.jpg", - 7, - ownerId = "user123", - mimeType = MimeType.JPEG, - isSharedViaLink = true, - localPath = "/local/shared.jpg" - ) - val favorite = createTestOCFile( - directory.fileId, - "/favorite.jpg", - 8, - ownerId = "user123", - mimeType = MimeType.JPEG, - isFavorite = true, - localPath = "/local/favorite.jpg" - ) - val livePhotoImg = createTestOCFile( - directory.fileId, - "/live.jpg", - 9, - ownerId = "user123", - mimeType = MimeType.JPEG, - localId = 77, - localPath = "/local/live.jpg" - ) - val livePhotoVideo = createTestOCFile( - directory.fileId, - "/live_video.mp4", - 10, - ownerId = "user123", - mimeType = "video/mp4", - localPath = "/local/live_video.mp4" - ).apply { - setLivePhoto("77") - } - val offlineOCFile = createTestOCFile( - directory.fileId, - "/offline.jpg", - 11, - ownerId = "user123", - mimeType = MimeType.JPEG, - localPath = "/local/offline.jpg" - ) - - val files = + fun `prepareFileList hides hidden files and sorts A to Z`() = runBlocking { + val env = Sut() + val root = env.root + + val fHidden = env.file(root, ".hidden.jpg", 1, MimeType.JPEG, hidden = true) + val fImg = env.file(root, "image.jpg", 2, MimeType.JPEG) + val fVid = env.file(root, "video.mp4", 3, MimeType.MP4) + val fTemp = env.file(root, "temp.tmp", 4, MimeType.FILE) + val fOther = env.file(env.directory("/other", 202), "other.jpg", 5, MimeType.JPEG) + val fPersonal = env.file(root, "personal.jpg", 6, MimeType.JPEG) + val fShared = env.file(root, "shared.jpg", 7, MimeType.JPEG, shared = true) + val fFav = env.file(root, "favorite.jpg", 8, MimeType.JPEG, favorite = true) + val fLiveImg = env.file(root, "live.jpg", 9, MimeType.JPEG, localId = 77) + val fLiveVid = env.file(root, "live_video.mp4", 10, MimeType.MP4).apply { setLivePhoto("77") } + val offline = env.file(root, "offline.jpg", 11, MimeType.JPEG) + + env.prepare( listOf( - directory, - hidden, - image, - video, - temp, - otherUsersFile, - personal, - shared, - favorite, - livePhotoImg, - livePhotoVideo - ) - dataProvider.setEntities(files) - dataProvider.setOfflineFile(offlineOCFile) - - every { preferences.isShowHiddenFilesEnabled() } returns false - every { preferences.getSortOrderByFolder(directory) } returns FileSortOrder.SORT_A_TO_Z - every { preferences.isSortFoldersBeforeFiles() } returns true - every { preferences.isSortFavoritesFirst() } returns false - - val (list, sort) = helper.prepareFileList( - directory = directory, - dataProvider = dataProvider, - onlyOnDevice = false, - limitToMimeType = "image", - preferences = preferences, - userId = userId + root, fHidden, fImg, fVid, fTemp, fOther, fPersonal, + fShared, fFav, fLiveImg, fLiveVid + ), + offline = offline ) - val expected = listOf( - "favorite.jpg", - "image.jpg", - "live.jpg", - "offline.jpg", - "personal.jpg", - "shared.jpg" - ) + stubPreferences(sort = FileSortOrder.SORT_A_TO_Z) - assertEquals(expected, list.map { it.fileName }) - assertEquals(FileSortOrder.SORT_A_TO_Z, sort) - } + val (list, sort) = env.run(root, mime = "image") - private fun createTestOCFile( - parentId: Long, - path: String, - fileId: Long, - ownerId: String? = null, - mimeType: String? = MimeType.FILE, - isHidden: Boolean = false, - isFavorite: Boolean = false, - isSharedViaLink: Boolean = false, - localId: Long = -1, - etag: String = "etag_$fileId", - localPath: String? = null - ): OCFile = OCFile(path).apply { - this.parentId = parentId - this.fileId = fileId - this.remotePath = path - this.ownerId = ownerId - this.mimeType = mimeType - this.isHidden = isHidden - this.isFavorite = isFavorite - this.isSharedViaLink = isSharedViaLink - this.localId = localId - this.etag = etag - this.storagePath = localPath + assertEquals( + listOf("favorite.jpg", "image.jpg", "live.jpg", "offline.jpg", "personal.jpg", "shared.jpg"), + list.map { it.fileName } + ) + assertEquals(FileSortOrder.SORT_A_TO_Z, sort) } } From 48351df0f99ddbe3aabc54ccdda9e105357334f9 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Wed, 26 Nov 2025 14:01:24 +0100 Subject: [PATCH 21/32] speed up parseAndSaveShares Signed-off-by: alperozturk --- .../client/database/NextcloudDatabase.kt | 2 + .../nextcloud/client/database/dao/ShareDao.kt | 24 ++++ .../FileDataStorageManagerExtensions.kt | 16 +++ .../utils/extensions/OCShareExtensions.kt | 29 +++++ .../datamodel/FileDataStorageManager.java | 113 +----------------- .../ui/adapter/OCShareToOCFileConverter.kt | 36 ++++-- 6 files changed, 103 insertions(+), 117 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/client/database/dao/ShareDao.kt diff --git a/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt b/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt index 87ce9c999c68..a5bafa686668 100644 --- a/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt +++ b/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt @@ -21,6 +21,7 @@ import com.nextcloud.client.database.dao.FileDao import com.nextcloud.client.database.dao.FileSystemDao import com.nextcloud.client.database.dao.OfflineOperationDao import com.nextcloud.client.database.dao.RecommendedFileDao +import com.nextcloud.client.database.dao.ShareDao import com.nextcloud.client.database.dao.SyncedFolderDao import com.nextcloud.client.database.dao.UploadDao import com.nextcloud.client.database.entity.ArbitraryDataEntity @@ -106,6 +107,7 @@ abstract class NextcloudDatabase : RoomDatabase() { abstract fun fileSystemDao(): FileSystemDao abstract fun syncedFolderDao(): SyncedFolderDao abstract fun assistantDao(): AssistantDao + abstract fun shareDao(): ShareDao companion object { const val FIRST_ROOM_DB_VERSION = 65 diff --git a/app/src/main/java/com/nextcloud/client/database/dao/ShareDao.kt b/app/src/main/java/com/nextcloud/client/database/dao/ShareDao.kt new file mode 100644 index 000000000000..61ea4a09c54a --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/database/dao/ShareDao.kt @@ -0,0 +1,24 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.database.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.nextcloud.client.database.entity.ShareEntity + +@Dao +interface ShareDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAll(shares: List) + + @Query("DELETE FROM ocshares WHERE owner_share = :accountName") + suspend fun clearSharesForAccount(accountName: String) +} diff --git a/app/src/main/java/com/nextcloud/utils/extensions/FileDataStorageManagerExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/FileDataStorageManagerExtensions.kt index 2fec3fcc12d8..fd4eb83c2404 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/FileDataStorageManagerExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/FileDataStorageManagerExtensions.kt @@ -9,6 +9,22 @@ package com.nextcloud.utils.extensions import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.resources.shares.OCShare +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +suspend fun FileDataStorageManager.saveShares(shares: List, accountName: String) { + withContext(Dispatchers.IO) { + shareDao.clearSharesForAccount(accountName) + + val entities = shares.map { share -> + share.toEntity(accountName) + } + + shareDao.insertAll(entities) + } +} + fun FileDataStorageManager.searchFilesByName(file: OCFile, accountName: String, query: String): List = fileDao.searchFilesInFolder(file.fileId, accountName, query).map { diff --git a/app/src/main/java/com/nextcloud/utils/extensions/OCShareExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/OCShareExtensions.kt index 9161bf9fbf69..cda5139e6226 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/OCShareExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/OCShareExtensions.kt @@ -7,8 +7,37 @@ package com.nextcloud.utils.extensions +import com.nextcloud.client.database.entity.ShareEntity import com.owncloud.android.lib.resources.shares.OCShare fun OCShare.hasFileRequestPermission(): Boolean = (isFolder && shareType?.isPublicOrMail() == true) fun List.mergeDistinctByToken(other: List): List = (this + other).distinctBy { it.token } + +fun OCShare.toEntity(accountName: String): ShareEntity { + return ShareEntity( + id = id.toInt(), + idRemoteShared = remoteId.toInt(), + path = path, + itemSource = itemSource.toInt(), + fileSource = fileSource.toInt(), + shareType = shareType?.value, + shareWith = shareWith, + permissions = permissions, + sharedDate = sharedDate.toInt(), + expirationDate = expirationDate.toInt(), + token = token, + shareWithDisplayName = sharedWithDisplayName, + isDirectory = if (isFolder) 1 else 0, + userId = userId, + accountOwner = accountName, + isPasswordProtected = if (isPasswordProtected) 1 else 0, + note = note, + hideDownload = if (isHideFileDownload) 1 else 0, + shareLink = shareLink, + shareLabel = label, + attributes = attributes, + downloadLimitLimit = fileDownloadLimit?.limit, + downloadLimitCount = fileDownloadLimit?.count + ) +} diff --git a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java index 517f40263400..f7998103a2c3 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java @@ -38,6 +38,7 @@ import com.nextcloud.client.database.dao.FileDao; import com.nextcloud.client.database.dao.OfflineOperationDao; import com.nextcloud.client.database.dao.RecommendedFileDao; +import com.nextcloud.client.database.dao.ShareDao; import com.nextcloud.client.database.entity.FileEntity; import com.nextcloud.client.database.entity.OfflineOperationEntity; import com.nextcloud.client.jobs.offlineOperations.repository.OfflineOperationsRepository; @@ -88,7 +89,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import kotlin.Pair; @@ -112,6 +112,8 @@ public class FileDataStorageManager { public final RecommendedFileDao recommendedFileDao = NextcloudDatabase.getInstance(MainApp.getAppContext()).recommendedFileDao(); public final OfflineOperationDao offlineOperationDao = NextcloudDatabase.getInstance(MainApp.getAppContext()).offlineOperationDao(); public final FileDao fileDao = NextcloudDatabase.getInstance(MainApp.getAppContext()).fileDao(); + public final ShareDao shareDao = NextcloudDatabase.getInstance(MainApp.getAppContext()).shareDao(); + private final Gson gson = new Gson(); public final OfflineOperationsRepositoryType offlineOperationsRepository; private final static int DEFAULT_CURSOR_INT_VALUE = -1; @@ -1689,67 +1691,6 @@ private void resetShareFlagInAFile(String filePath) { } } - @VisibleForTesting - public void cleanShares() { - String where = ProviderTableMeta.OCSHARES_ACCOUNT_OWNER + "=?"; - String[] whereArgs = new String[]{user.getAccountName()}; - - if (getContentResolver() != null) { - getContentResolver().delete(ProviderTableMeta.CONTENT_URI_SHARE, where, whereArgs); - - } else { - try { - getContentProviderClient().delete(ProviderTableMeta.CONTENT_URI_SHARE, where, whereArgs); - } catch (RemoteException e) { - Log_OC.e(TAG, "Exception in cleanShares" + e.getMessage(), e); - } - } - } - - // TODO shares null? - public void saveShares(List shares) { - cleanShares(); - ArrayList operations = new ArrayList<>(shares.size()); - - // prepare operations to insert or update files to save in the given folder - for (OCShare share : shares) { - ContentValues contentValues = createContentValueForShare(share); - - if (shareExistsForRemoteId(share.getRemoteId())) { - // updating an existing file - operations.add( - ContentProviderOperation.newUpdate(ProviderTableMeta.CONTENT_URI_SHARE) - .withValues(contentValues) - .withSelection(ProviderTableMeta.OCSHARES_ID_REMOTE_SHARED + " = ?", - new String[]{String.valueOf(share.getRemoteId())}) - .build()); - } else { - // adding a new file - operations.add( - ContentProviderOperation.newInsert(ProviderTableMeta.CONTENT_URI_SHARE) - .withValues(contentValues) - .build() - ); - } - } - - // apply operations in batch - if (operations.size() > 0) { - Log_OC.d(TAG, String.format(Locale.ENGLISH, SENDING_TO_FILECONTENTPROVIDER_MSG, operations.size())); - try { - if (getContentResolver() != null) { - getContentResolver().applyBatch(MainApp.getAuthority(), - operations); - } else { - getContentProviderClient().applyBatch(operations); - } - - } catch (OperationApplicationException | RemoteException e) { - Log_OC.e(TAG, EXCEPTION_MSG + e.getMessage(), e); - } - } - } - public void removeShare(OCShare share) { Uri contentUriShare = ProviderTableMeta.CONTENT_URI_SHARE; String where = ProviderTableMeta.OCSHARES_ACCOUNT_OWNER + AND + @@ -1887,33 +1828,6 @@ public void removeSharesForFile(String remotePath) { } } - // TOOD check if shares can be null - public void saveSharesInFolder(ArrayList shares, OCFile folder) { - resetShareFlagsInFolder(folder); - ArrayList operations = new ArrayList<>(); - operations = prepareRemoveSharesInFolder(folder, operations); - - // prepare operations to insert or update files to save in the given folder - operations = prepareInsertShares(shares, operations); - - // apply operations in batch - if (operations.size() > 0) { - Log_OC.d(TAG, String.format(Locale.ENGLISH, SENDING_TO_FILECONTENTPROVIDER_MSG, operations.size())); - try { - if (getContentResolver() != null) { - getContentResolver().applyBatch(MainApp.getAuthority(), operations); - - } else { - - getContentProviderClient().applyBatch(operations); - } - - } catch (OperationApplicationException | RemoteException e) { - Log_OC.e(TAG, EXCEPTION_MSG + e.getMessage(), e); - } - } - } - /** * Prepare operations to insert or update files to save in the given folder * @@ -1938,27 +1852,6 @@ private ArrayList prepareInsertShares(Iterable prepareRemoveSharesInFolder( - OCFile folder, ArrayList preparedOperations) { - if (folder != null) { - String where = ProviderTableMeta.OCSHARES_PATH + AND - + ProviderTableMeta.OCSHARES_ACCOUNT_OWNER + "=?"; - String[] whereArgs = new String[]{"", user.getAccountName()}; - - List files = getFolderContent(folder, false); - - for (OCFile file : files) { - whereArgs[0] = file.getRemotePath(); - preparedOperations.add( - ContentProviderOperation.newDelete(ProviderTableMeta.CONTENT_URI_SHARE). - withSelection(where, whereArgs). - build() - ); - } - } - return preparedOperations; - } - private ArrayList prepareRemoveSharesInFile( String filePath, ArrayList preparedOperations) { diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt index 8cff475db99c..f966efb738a6 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt @@ -7,6 +7,7 @@ */ package com.owncloud.android.ui.adapter +import com.nextcloud.utils.extensions.saveShares import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.OCFile import com.owncloud.android.lib.resources.shares.OCShare @@ -14,7 +15,10 @@ import com.owncloud.android.lib.resources.shares.ShareType import com.owncloud.android.lib.resources.shares.ShareeUser import com.owncloud.android.utils.FileStorageUtils import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.withContext +import java.io.File object OCShareToOCFileConverter { private const val MILLIS_PER_SECOND = 1000 @@ -46,21 +50,39 @@ object OCShareToOCFileConverter { accountName: String ): List = withContext(Dispatchers.IO) { if (data.isEmpty()) { - return@withContext listOf() + return@withContext emptyList() } val shares = data.filterIsInstance() if (shares.isEmpty()) { - return@withContext listOf() + return@withContext emptyList() } - val files = buildOCFilesFromShares(shares).onEach { file -> - FileStorageUtils.searchForLocalFileInDefaultPath(file, accountName) - } - storageManager?.saveShares(shares) - files + val files = buildOCFilesFromShares(shares) + val baseSavePath = FileStorageUtils.getSavePath(accountName) + + // Parallelized file lookup + val resolvedFiles = files.map { file -> + async { + if (!file.isFolder && (file.storagePath == null || !File(file.storagePath).exists())) { + val fullPath = baseSavePath + file.decryptedRemotePath + val candidate = File(fullPath) + + if (candidate.exists()) { + file.storagePath = candidate.absolutePath + file.lastSyncDateForData = candidate.lastModified() + } + } + file + } + }.awaitAll() + + storageManager?.saveShares(shares, accountName) + + resolvedFiles } + private fun buildOcFile(path: String, shares: List): OCFile { require(shares.all { it.path == path }) // common attributes From 7b3e948b4d6e1c17bd5a0d490b2e3570f427be23 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Wed, 26 Nov 2025 15:48:10 +0100 Subject: [PATCH 22/32] speed up parseAndSaveShares Signed-off-by: alperozturk --- .../nextcloud/client/database/dao/FileDao.kt | 19 +++++++++++++++---- .../FileDataStorageManagerExtensions.kt | 2 +- .../utils/extensions/OCShareExtensions.kt | 2 +- .../datamodel/FileDataStorageManager.java | 19 ------------------- 4 files changed, 17 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt b/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt index d75a021099e2..ac7b4eb2a738 100644 --- a/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt +++ b/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt @@ -38,6 +38,9 @@ interface FileDao { @Query("SELECT * FROM filelist WHERE remote_id = :remoteId AND file_owner = :fileOwner LIMIT 1") fun getFileByRemoteId(remoteId: String, fileOwner: String): FileEntity? + @Query("SELECT * FROM filelist WHERE remote_id = :remoteId LIMIT 1") + suspend fun getFileByRemoteId(remoteId: String): FileEntity? + @Query("SELECT * FROM filelist WHERE parent = :parentId ORDER BY ${ProviderTableMeta.FILE_DEFAULT_SORT_ORDER}") fun getFolderContent(parentId: Long): List @@ -117,10 +120,15 @@ interface FileDao { @Query( """ - SELECT * - FROM filelist - WHERE file_owner = :fileOwner - AND share_by_link = 1 + SELECT * + FROM filelist + WHERE file_owner = :fileOwner + AND ( + share_by_link = 1 + OR shared_via_users = 1 + OR permissions LIKE '%S%' + OR COALESCE(sharees, '') NOT IN ('', 'null', '[]') + ) ORDER BY ${ProviderTableMeta.FILE_DEFAULT_SORT_ORDER} """ ) @@ -136,4 +144,7 @@ interface FileDao { """ ) suspend fun getFavoriteFiles(fileOwner: String): List + + @Query("SELECT remote_id FROM filelist WHERE file_owner = :accountName AND remote_id IS NOT NULL") + fun getAllRemoteIds(accountName: String): List } diff --git a/app/src/main/java/com/nextcloud/utils/extensions/FileDataStorageManagerExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/FileDataStorageManagerExtensions.kt index fd4eb83c2404..f28ba91ae90c 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/FileDataStorageManagerExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/FileDataStorageManagerExtensions.kt @@ -15,7 +15,7 @@ import kotlinx.coroutines.withContext suspend fun FileDataStorageManager.saveShares(shares: List, accountName: String) { withContext(Dispatchers.IO) { - shareDao.clearSharesForAccount(accountName) + //shareDao.clearSharesForAccount(accountName) val entities = shares.map { share -> share.toEntity(accountName) diff --git a/app/src/main/java/com/nextcloud/utils/extensions/OCShareExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/OCShareExtensions.kt index cda5139e6226..f76cba360631 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/OCShareExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/OCShareExtensions.kt @@ -16,7 +16,7 @@ fun List.mergeDistinctByToken(other: List): List = (t fun OCShare.toEntity(accountName: String): ShareEntity { return ShareEntity( - id = id.toInt(), + id = remoteId.toInt(), // so that db is not keep updating same files idRemoteShared = remoteId.toInt(), path = path, itemSource = itemSource.toInt(), diff --git a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java index f7998103a2c3..1106e6239d5c 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java @@ -1653,25 +1653,6 @@ private int getIntOrDefault(Cursor cursor, String columnName) { return cursor.getInt(index); } - private void resetShareFlagsInFolder(OCFile folder) { - ContentValues contentValues = new ContentValues(); - contentValues.put(ProviderTableMeta.FILE_SHARED_VIA_LINK, Boolean.FALSE); - contentValues.put(ProviderTableMeta.FILE_SHARED_WITH_SHAREE, Boolean.FALSE); - String where = ProviderTableMeta.FILE_ACCOUNT_OWNER + AND + ProviderTableMeta.FILE_PARENT + " = ?"; - String[] whereArgs = new String[]{user.getAccountName(), String.valueOf(folder.getFileId())}; - - if (getContentResolver() != null) { - getContentResolver().update(ProviderTableMeta.CONTENT_URI, contentValues, where, whereArgs); - - } else { - try { - getContentProviderClient().update(ProviderTableMeta.CONTENT_URI, contentValues, where, whereArgs); - } catch (RemoteException e) { - Log_OC.e(TAG, "Exception in resetShareFlagsInFiles" + e.getMessage(), e); - } - } - } - private void resetShareFlagInAFile(String filePath) { ContentValues contentValues = new ContentValues(); contentValues.put(ProviderTableMeta.FILE_SHARED_VIA_LINK, Boolean.FALSE); From 80b1a9c04acee384129d2f04d031b56c3ed6b398 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Wed, 26 Nov 2025 16:01:12 +0100 Subject: [PATCH 23/32] speed up parseAndSaveShares Signed-off-by: alperozturk --- .../FileDataStorageManagerExtensions.kt | 3 +- .../utils/extensions/OCShareExtensions.kt | 52 +++++++++---------- .../ui/adapter/OCShareToOCFileConverter.kt | 29 ++++++++--- .../ui/fragment/OCFileListSearchTask.kt | 1 + 4 files changed, 49 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/com/nextcloud/utils/extensions/FileDataStorageManagerExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/FileDataStorageManagerExtensions.kt index f28ba91ae90c..8b93f79f6d44 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/FileDataStorageManagerExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/FileDataStorageManagerExtensions.kt @@ -15,7 +15,7 @@ import kotlinx.coroutines.withContext suspend fun FileDataStorageManager.saveShares(shares: List, accountName: String) { withContext(Dispatchers.IO) { - //shareDao.clearSharesForAccount(accountName) + // shareDao.clearSharesForAccount(accountName) val entities = shares.map { share -> share.toEntity(accountName) @@ -25,7 +25,6 @@ suspend fun FileDataStorageManager.saveShares(shares: List, accountName } } - fun FileDataStorageManager.searchFilesByName(file: OCFile, accountName: String, query: String): List = fileDao.searchFilesInFolder(file.fileId, accountName, query).map { createFileInstance(it) diff --git a/app/src/main/java/com/nextcloud/utils/extensions/OCShareExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/OCShareExtensions.kt index f76cba360631..dcb43b34dbfd 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/OCShareExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/OCShareExtensions.kt @@ -14,30 +14,28 @@ fun OCShare.hasFileRequestPermission(): Boolean = (isFolder && shareType?.isPubl fun List.mergeDistinctByToken(other: List): List = (this + other).distinctBy { it.token } -fun OCShare.toEntity(accountName: String): ShareEntity { - return ShareEntity( - id = remoteId.toInt(), // so that db is not keep updating same files - idRemoteShared = remoteId.toInt(), - path = path, - itemSource = itemSource.toInt(), - fileSource = fileSource.toInt(), - shareType = shareType?.value, - shareWith = shareWith, - permissions = permissions, - sharedDate = sharedDate.toInt(), - expirationDate = expirationDate.toInt(), - token = token, - shareWithDisplayName = sharedWithDisplayName, - isDirectory = if (isFolder) 1 else 0, - userId = userId, - accountOwner = accountName, - isPasswordProtected = if (isPasswordProtected) 1 else 0, - note = note, - hideDownload = if (isHideFileDownload) 1 else 0, - shareLink = shareLink, - shareLabel = label, - attributes = attributes, - downloadLimitLimit = fileDownloadLimit?.limit, - downloadLimitCount = fileDownloadLimit?.count - ) -} +fun OCShare.toEntity(accountName: String): ShareEntity = ShareEntity( + id = remoteId.toInt(), // so that db is not keep updating same files + idRemoteShared = remoteId.toInt(), + path = path, + itemSource = itemSource.toInt(), + fileSource = fileSource.toInt(), + shareType = shareType?.value, + shareWith = shareWith, + permissions = permissions, + sharedDate = sharedDate.toInt(), + expirationDate = expirationDate.toInt(), + token = token, + shareWithDisplayName = sharedWithDisplayName, + isDirectory = if (isFolder) 1 else 0, + userId = userId, + accountOwner = accountName, + isPasswordProtected = if (isPasswordProtected) 1 else 0, + note = note, + hideDownload = if (isHideFileDownload) 1 else 0, + shareLink = shareLink, + shareLabel = label, + attributes = attributes, + downloadLimitLimit = fileDownloadLimit?.limit, + downloadLimitCount = fileDownloadLimit?.count +) diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt index f966efb738a6..2adf9caa0fdb 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt @@ -45,6 +45,7 @@ object OCShareToOCFileConverter { } suspend fun parseAndSaveShares( + cachedFiles: List, data: List, storageManager: FileDataStorageManager?, accountName: String @@ -58,11 +59,27 @@ object OCShareToOCFileConverter { return@withContext emptyList() } - val files = buildOCFilesFromShares(shares) + val cachedPaths = cachedFiles.map { it.decryptedRemotePath }.toSet() + + // Group shares by path to identify unique files + val sharesByPath = shares + .filter { it.path != null } + .groupBy { it.path!! } + + // Identify ONLY new file paths that aren't in cache + val newSharesByPath = sharesByPath.filterKeys { path -> + path !in cachedPaths + } + + if (newSharesByPath.isEmpty()) { + return@withContext cachedFiles + } + + val newShares = newSharesByPath.values.flatten() + val newFiles = buildOCFilesFromShares(newShares) val baseSavePath = FileStorageUtils.getSavePath(accountName) - // Parallelized file lookup - val resolvedFiles = files.map { file -> + val resolvedNewFiles = newFiles.map { file -> async { if (!file.isFolder && (file.storagePath == null || !File(file.storagePath).exists())) { val fullPath = baseSavePath + file.decryptedRemotePath @@ -77,12 +94,10 @@ object OCShareToOCFileConverter { } }.awaitAll() - storageManager?.saveShares(shares, accountName) - - resolvedFiles + storageManager?.saveShares(newShares, accountName) + cachedFiles + resolvedNewFiles } - private fun buildOcFile(path: String, shares: List): OCFile { require(shares.all { it.path == path }) // common attributes diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt index 23c3c0c3dcd5..d383da47d591 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt @@ -95,6 +95,7 @@ class OCFileListSearchTask( val newList = if (searchType == SearchType.SHARED_FILTER) { OCShareToOCFileConverter.parseAndSaveShares( + sortedFilesInDb, result.resultData ?: listOf(), fileDataStorageManager, currentUser.accountName From 5dfd9a018a0e801415c4e08f8898f44116353b91 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Thu, 27 Nov 2025 08:23:30 +0100 Subject: [PATCH 24/32] speed up parseAndSaveShares Signed-off-by: alperozturk --- app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt b/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt index ac7b4eb2a738..0c2eebea5457 100644 --- a/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt +++ b/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt @@ -127,7 +127,6 @@ interface FileDao { share_by_link = 1 OR shared_via_users = 1 OR permissions LIKE '%S%' - OR COALESCE(sharees, '') NOT IN ('', 'null', '[]') ) ORDER BY ${ProviderTableMeta.FILE_DEFAULT_SORT_ORDER} """ From e5094f415539d84cc789875a1aad6934ab8c3003 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Thu, 27 Nov 2025 15:55:24 +0100 Subject: [PATCH 25/32] fix Signed-off-by: alperozturk --- .../nextcloud/client/database/dao/FileDao.kt | 14 ++++++------- .../ui/adapter/OCShareToOCFileConverter.kt | 21 ++----------------- .../ui/fragment/OCFileListSearchTask.kt | 19 +++++++---------- 3 files changed, 16 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt b/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt index 0c2eebea5457..3dcc3d348f15 100644 --- a/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt +++ b/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt @@ -122,16 +122,16 @@ interface FileDao { """ SELECT * FROM filelist - WHERE file_owner = :fileOwner - AND ( - share_by_link = 1 - OR shared_via_users = 1 - OR permissions LIKE '%S%' - ) + WHERE + (file_owner != :accountName + OR share_by_link = 1 + OR shared_via_users = 1 + OR permissions LIKE '%S%') + AND parent = 1 ORDER BY ${ProviderTableMeta.FILE_DEFAULT_SORT_ORDER} """ ) - suspend fun getSharedFiles(fileOwner: String): List + suspend fun getSharedFiles(accountName: String): List @Query( """ diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt index 2adf9caa0fdb..109decd4d977 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt @@ -59,24 +59,7 @@ object OCShareToOCFileConverter { return@withContext emptyList() } - val cachedPaths = cachedFiles.map { it.decryptedRemotePath }.toSet() - - // Group shares by path to identify unique files - val sharesByPath = shares - .filter { it.path != null } - .groupBy { it.path!! } - - // Identify ONLY new file paths that aren't in cache - val newSharesByPath = sharesByPath.filterKeys { path -> - path !in cachedPaths - } - - if (newSharesByPath.isEmpty()) { - return@withContext cachedFiles - } - - val newShares = newSharesByPath.values.flatten() - val newFiles = buildOCFilesFromShares(newShares) + val newFiles = buildOCFilesFromShares(shares) val baseSavePath = FileStorageUtils.getSavePath(accountName) val resolvedNewFiles = newFiles.map { file -> @@ -94,7 +77,7 @@ object OCShareToOCFileConverter { } }.awaitAll() - storageManager?.saveShares(newShares, accountName) + // storageManager?.saveShares(newShares, accountName) cachedFiles + resolvedNewFiles } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt index d383da47d591..dfa735b405e8 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt @@ -128,18 +128,13 @@ class OCFileListSearchTask( private suspend fun loadCachedDbFiles(searchType: SearchRemoteOperation.SearchType): List { val storage = fileDataStorageManager ?: return emptyList() - - val rows = when (searchType) { - SearchRemoteOperation.SearchType.SHARED_FILTER -> - storage.fileDao.getSharedFiles(currentUser.accountName) - - SearchRemoteOperation.SearchType.FAVORITE_SEARCH -> - storage.fileDao.getFavoriteFiles(currentUser.accountName) - - else -> null - } ?: return emptyList() - - return rows.mapNotNull { storage.createFileInstance(it) } + return if (searchType == SearchRemoteOperation.SearchType.SHARED_FILTER) { + storage.fileDao + .getSharedFiles(currentUser.accountName) + } else { + storage.fileDao + .getFavoriteFiles(currentUser.accountName) + } .mapNotNull { storage.createFileInstance(it) } } @Suppress("DEPRECATION") From dc86f03128c3a77b452f3b68238426d386eb1f4b Mon Sep 17 00:00:00 2001 From: alperozturk Date: Thu, 27 Nov 2025 15:59:10 +0100 Subject: [PATCH 26/32] fix Signed-off-by: alperozturk --- app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt | 1 - .../com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt | 1 - 2 files changed, 2 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt b/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt index 3dcc3d348f15..0b9535514774 100644 --- a/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt +++ b/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt @@ -127,7 +127,6 @@ interface FileDao { OR share_by_link = 1 OR shared_via_users = 1 OR permissions LIKE '%S%') - AND parent = 1 ORDER BY ${ProviderTableMeta.FILE_DEFAULT_SORT_ORDER} """ ) diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt index 109decd4d977..cbc4b0853773 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt @@ -7,7 +7,6 @@ */ package com.owncloud.android.ui.adapter -import com.nextcloud.utils.extensions.saveShares import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.OCFile import com.owncloud.android.lib.resources.shares.OCShare From 905ef229c8d5180314d1a5402986473ceeb0c5c0 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 28 Nov 2025 10:59:29 +0100 Subject: [PATCH 27/32] detect new shares Signed-off-by: alperozturk --- .../android/ui/adapter/OCShareToOCFileConverter.kt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt index cbc4b0853773..3d1d2e2da6f3 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt @@ -58,7 +58,15 @@ object OCShareToOCFileConverter { return@withContext emptyList() } - val newFiles = buildOCFilesFromShares(shares) + val newShares = shares.filter { share -> + cachedFiles.none { file -> file.localId == share.fileSource } + } + + if (newShares.isEmpty()) { + return@withContext cachedFiles + } + + val newFiles = buildOCFilesFromShares(newShares) val baseSavePath = FileStorageUtils.getSavePath(accountName) val resolvedNewFiles = newFiles.map { file -> From 9098840a5b7bd1840bd63c84ba76c5ddf4ddedd3 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 28 Nov 2025 11:33:34 +0100 Subject: [PATCH 28/32] fix caching mechanism use file dao Signed-off-by: alperozturk --- .../client/database/NextcloudDatabase.kt | 2 -- .../nextcloud/client/database/dao/FileDao.kt | 11 +++---- .../nextcloud/client/database/dao/ShareDao.kt | 24 --------------- .../FileDataStorageManagerExtensions.kt | 15 ---------- .../datamodel/FileDataStorageManager.java | 2 -- .../ui/adapter/OCShareToOCFileConverter.kt | 30 ++++++++----------- .../ui/fragment/OCFileListSearchTask.kt | 2 +- 7 files changed, 20 insertions(+), 66 deletions(-) delete mode 100644 app/src/main/java/com/nextcloud/client/database/dao/ShareDao.kt diff --git a/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt b/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt index a5bafa686668..87ce9c999c68 100644 --- a/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt +++ b/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt @@ -21,7 +21,6 @@ import com.nextcloud.client.database.dao.FileDao import com.nextcloud.client.database.dao.FileSystemDao import com.nextcloud.client.database.dao.OfflineOperationDao import com.nextcloud.client.database.dao.RecommendedFileDao -import com.nextcloud.client.database.dao.ShareDao import com.nextcloud.client.database.dao.SyncedFolderDao import com.nextcloud.client.database.dao.UploadDao import com.nextcloud.client.database.entity.ArbitraryDataEntity @@ -107,7 +106,6 @@ abstract class NextcloudDatabase : RoomDatabase() { abstract fun fileSystemDao(): FileSystemDao abstract fun syncedFolderDao(): SyncedFolderDao abstract fun assistantDao(): AssistantDao - abstract fun shareDao(): ShareDao companion object { const val FIRST_ROOM_DB_VERSION = 65 diff --git a/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt b/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt index 0b9535514774..60fc48c23571 100644 --- a/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt +++ b/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt @@ -122,11 +122,12 @@ interface FileDao { """ SELECT * FROM filelist - WHERE - (file_owner != :accountName - OR share_by_link = 1 - OR shared_via_users = 1 - OR permissions LIKE '%S%') + WHERE file_owner = :accountName + AND ( + share_by_link = 1 + OR shared_via_users = 1 + OR permissions LIKE '%S%' + ) ORDER BY ${ProviderTableMeta.FILE_DEFAULT_SORT_ORDER} """ ) diff --git a/app/src/main/java/com/nextcloud/client/database/dao/ShareDao.kt b/app/src/main/java/com/nextcloud/client/database/dao/ShareDao.kt deleted file mode 100644 index 61ea4a09c54a..000000000000 --- a/app/src/main/java/com/nextcloud/client/database/dao/ShareDao.kt +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2025 Alper Ozturk - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -package com.nextcloud.client.database.dao - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import com.nextcloud.client.database.entity.ShareEntity - -@Dao -interface ShareDao { - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertAll(shares: List) - - @Query("DELETE FROM ocshares WHERE owner_share = :accountName") - suspend fun clearSharesForAccount(accountName: String) -} diff --git a/app/src/main/java/com/nextcloud/utils/extensions/FileDataStorageManagerExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/FileDataStorageManagerExtensions.kt index 8b93f79f6d44..2fec3fcc12d8 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/FileDataStorageManagerExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/FileDataStorageManagerExtensions.kt @@ -9,21 +9,6 @@ package com.nextcloud.utils.extensions import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.OCFile -import com.owncloud.android.lib.resources.shares.OCShare -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - -suspend fun FileDataStorageManager.saveShares(shares: List, accountName: String) { - withContext(Dispatchers.IO) { - // shareDao.clearSharesForAccount(accountName) - - val entities = shares.map { share -> - share.toEntity(accountName) - } - - shareDao.insertAll(entities) - } -} fun FileDataStorageManager.searchFilesByName(file: OCFile, accountName: String, query: String): List = fileDao.searchFilesInFolder(file.fileId, accountName, query).map { diff --git a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java index 1106e6239d5c..02ccb650c5ec 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java @@ -38,7 +38,6 @@ import com.nextcloud.client.database.dao.FileDao; import com.nextcloud.client.database.dao.OfflineOperationDao; import com.nextcloud.client.database.dao.RecommendedFileDao; -import com.nextcloud.client.database.dao.ShareDao; import com.nextcloud.client.database.entity.FileEntity; import com.nextcloud.client.database.entity.OfflineOperationEntity; import com.nextcloud.client.jobs.offlineOperations.repository.OfflineOperationsRepository; @@ -112,7 +111,6 @@ public class FileDataStorageManager { public final RecommendedFileDao recommendedFileDao = NextcloudDatabase.getInstance(MainApp.getAppContext()).recommendedFileDao(); public final OfflineOperationDao offlineOperationDao = NextcloudDatabase.getInstance(MainApp.getAppContext()).offlineOperationDao(); public final FileDao fileDao = NextcloudDatabase.getInstance(MainApp.getAppContext()).fileDao(); - public final ShareDao shareDao = NextcloudDatabase.getInstance(MainApp.getAppContext()).shareDao(); private final Gson gson = new Gson(); public final OfflineOperationsRepositoryType offlineOperationsRepository; diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt index 3d1d2e2da6f3..93e0990b25e0 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt @@ -14,8 +14,6 @@ import com.owncloud.android.lib.resources.shares.ShareType import com.owncloud.android.lib.resources.shares.ShareeUser import com.owncloud.android.utils.FileStorageUtils import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll import kotlinx.coroutines.withContext import java.io.File @@ -66,26 +64,23 @@ object OCShareToOCFileConverter { return@withContext cachedFiles } - val newFiles = buildOCFilesFromShares(newShares) + val files = buildOCFilesFromShares(newShares) val baseSavePath = FileStorageUtils.getSavePath(accountName) - val resolvedNewFiles = newFiles.map { file -> - async { - if (!file.isFolder && (file.storagePath == null || !File(file.storagePath).exists())) { - val fullPath = baseSavePath + file.decryptedRemotePath - val candidate = File(fullPath) - - if (candidate.exists()) { - file.storagePath = candidate.absolutePath - file.lastSyncDateForData = candidate.lastModified() - } + val newFiles = files.map { file -> + if (!file.isFolder && (file.storagePath == null || !File(file.storagePath).exists())) { + val fullPath = baseSavePath + file.decryptedRemotePath + val candidate = File(fullPath) + if (candidate.exists()) { + file.storagePath = candidate.absolutePath + file.lastSyncDateForData = candidate.lastModified() } - file } - }.awaitAll() + storageManager?.saveFile(file) + file + } - // storageManager?.saveShares(newShares, accountName) - cachedFiles + resolvedNewFiles + cachedFiles + newFiles } private fun buildOcFile(path: String, shares: List): OCFile { @@ -100,6 +95,7 @@ object OCShareToOCFileConverter { mimeType = firstShare.mimetype note = firstShare.note fileId = firstShare.fileSource + localId = firstShare.fileSource remoteId = firstShare.remoteId.toString() // use first share timestamp as timestamp firstShareTimestamp = shares.minOf { it.sharedDate * MILLIS_PER_SECOND } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt index dfa735b405e8..ef938616dd6c 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt @@ -134,7 +134,7 @@ class OCFileListSearchTask( } else { storage.fileDao .getFavoriteFiles(currentUser.accountName) - } .mapNotNull { storage.createFileInstance(it) } + }.mapNotNull { storage.createFileInstance(it) } } @Suppress("DEPRECATION") From 6ae533c595ad5ada80d4be3272dbc203dfa00ba6 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 28 Nov 2025 11:41:13 +0100 Subject: [PATCH 29/32] fix caching mechanism use file dao Signed-off-by: alperozturk --- .../client/database/NextcloudDatabase.kt | 2 ++ .../nextcloud/client/database/dao/ShareDao.kt | 24 +++++++++++++++++++ .../FileDataStorageManagerExtensions.kt | 13 ++++++++++ .../datamodel/FileDataStorageManager.java | 2 ++ .../ui/adapter/OCShareToOCFileConverter.kt | 2 ++ 5 files changed, 43 insertions(+) create mode 100644 app/src/main/java/com/nextcloud/client/database/dao/ShareDao.kt diff --git a/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt b/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt index 87ce9c999c68..a5bafa686668 100644 --- a/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt +++ b/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt @@ -21,6 +21,7 @@ import com.nextcloud.client.database.dao.FileDao import com.nextcloud.client.database.dao.FileSystemDao import com.nextcloud.client.database.dao.OfflineOperationDao import com.nextcloud.client.database.dao.RecommendedFileDao +import com.nextcloud.client.database.dao.ShareDao import com.nextcloud.client.database.dao.SyncedFolderDao import com.nextcloud.client.database.dao.UploadDao import com.nextcloud.client.database.entity.ArbitraryDataEntity @@ -106,6 +107,7 @@ abstract class NextcloudDatabase : RoomDatabase() { abstract fun fileSystemDao(): FileSystemDao abstract fun syncedFolderDao(): SyncedFolderDao abstract fun assistantDao(): AssistantDao + abstract fun shareDao(): ShareDao companion object { const val FIRST_ROOM_DB_VERSION = 65 diff --git a/app/src/main/java/com/nextcloud/client/database/dao/ShareDao.kt b/app/src/main/java/com/nextcloud/client/database/dao/ShareDao.kt new file mode 100644 index 000000000000..61ea4a09c54a --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/database/dao/ShareDao.kt @@ -0,0 +1,24 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.database.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.nextcloud.client.database.entity.ShareEntity + +@Dao +interface ShareDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAll(shares: List) + + @Query("DELETE FROM ocshares WHERE owner_share = :accountName") + suspend fun clearSharesForAccount(accountName: String) +} diff --git a/app/src/main/java/com/nextcloud/utils/extensions/FileDataStorageManagerExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/FileDataStorageManagerExtensions.kt index 2fec3fcc12d8..6567d7c5b559 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/FileDataStorageManagerExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/FileDataStorageManagerExtensions.kt @@ -9,6 +9,19 @@ package com.nextcloud.utils.extensions import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.resources.shares.OCShare +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +suspend fun FileDataStorageManager.saveShares(shares: List, accountName: String) { + withContext(Dispatchers.IO) { + val entities = shares.map { share -> + share.toEntity(accountName) + } + + shareDao.insertAll(entities) + } +} fun FileDataStorageManager.searchFilesByName(file: OCFile, accountName: String, query: String): List = fileDao.searchFilesInFolder(file.fileId, accountName, query).map { diff --git a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java index 02ccb650c5ec..1106e6239d5c 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java @@ -38,6 +38,7 @@ import com.nextcloud.client.database.dao.FileDao; import com.nextcloud.client.database.dao.OfflineOperationDao; import com.nextcloud.client.database.dao.RecommendedFileDao; +import com.nextcloud.client.database.dao.ShareDao; import com.nextcloud.client.database.entity.FileEntity; import com.nextcloud.client.database.entity.OfflineOperationEntity; import com.nextcloud.client.jobs.offlineOperations.repository.OfflineOperationsRepository; @@ -111,6 +112,7 @@ public class FileDataStorageManager { public final RecommendedFileDao recommendedFileDao = NextcloudDatabase.getInstance(MainApp.getAppContext()).recommendedFileDao(); public final OfflineOperationDao offlineOperationDao = NextcloudDatabase.getInstance(MainApp.getAppContext()).offlineOperationDao(); public final FileDao fileDao = NextcloudDatabase.getInstance(MainApp.getAppContext()).fileDao(); + public final ShareDao shareDao = NextcloudDatabase.getInstance(MainApp.getAppContext()).shareDao(); private final Gson gson = new Gson(); public final OfflineOperationsRepositoryType offlineOperationsRepository; diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt index 93e0990b25e0..e0fc6a1ef59d 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt @@ -7,6 +7,7 @@ */ package com.owncloud.android.ui.adapter +import com.nextcloud.utils.extensions.saveShares import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.OCFile import com.owncloud.android.lib.resources.shares.OCShare @@ -80,6 +81,7 @@ object OCShareToOCFileConverter { file } + storageManager?.saveShares(newShares, accountName) cachedFiles + newFiles } From 493d6606869de4dd49a8fd19b853674568fdf96e Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 28 Nov 2025 11:55:25 +0100 Subject: [PATCH 30/32] fix caching mechanism use file dao Signed-off-by: alperozturk --- .../owncloud/android/ui/adapter/OCShareToOCFileConverter.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt index e0fc6a1ef59d..dbbf41c6a76e 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt @@ -58,7 +58,7 @@ object OCShareToOCFileConverter { } val newShares = shares.filter { share -> - cachedFiles.none { file -> file.localId == share.fileSource } + cachedFiles.none { file -> file.decryptedRemotePath == share.path } } if (newShares.isEmpty()) { @@ -97,7 +97,6 @@ object OCShareToOCFileConverter { mimeType = firstShare.mimetype note = firstShare.note fileId = firstShare.fileSource - localId = firstShare.fileSource remoteId = firstShare.remoteId.toString() // use first share timestamp as timestamp firstShareTimestamp = shares.minOf { it.sharedDate * MILLIS_PER_SECOND } From 35255cd78186997b9ba2822ad081a33dd1177bf4 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Mon, 1 Dec 2025 09:00:23 +0100 Subject: [PATCH 31/32] fix: ocfile constructor from share Signed-off-by: alperozturk --- .../ui/adapter/OCShareToOCFileConverter.kt | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt index dbbf41c6a76e..4a3bfa8a0b40 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt @@ -1,6 +1,7 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2025 Alper Ozturk * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2022 Nextcloud GmbH * SPDX-License-Identifier: AGPL-3.0-or-later @@ -32,15 +33,16 @@ object OCShareToOCFileConverter { * * Note: This works only for files shared *by* the user, not files shared *with* the user. */ - @JvmStatic - fun buildOCFilesFromShares(shares: List): List { - val groupedByPath: Map> = shares - .filter { it.path != null } - .groupBy { it.path!! } - return groupedByPath - .map { (path: String, shares: List) -> buildOcFile(path, shares) } - .sortedByDescending { it.firstShareTimestamp } - } + fun buildOCFilesFromShares(shares: List): List = shares + .filter { !it.path.isNullOrEmpty() } + .groupBy { it.path!! } + .filterKeys { path -> + path.isNotEmpty() && !path.startsWith(OCFile.PATH_SEPARATOR) + } + .map { (path, sharesForPath) -> + buildOcFile(path, sharesForPath) + } + .sortedByDescending { it.firstShareTimestamp } suspend fun parseAndSaveShares( cachedFiles: List, From c387abcd4a9195d63060135cc086c713e2953f5f Mon Sep 17 00:00:00 2001 From: alperozturk Date: Tue, 2 Dec 2025 09:58:31 +0100 Subject: [PATCH 32/32] fix git conflict Signed-off-by: alperozturk --- .../nextcloud/utils/extensions/OCFileExtensions.kt | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/app/src/main/java/com/nextcloud/utils/extensions/OCFileExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/OCFileExtensions.kt index 5ed33a0cf57a..d19a586de530 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/OCFileExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/OCFileExtensions.kt @@ -37,25 +37,12 @@ fun List.hasSameContentAs(other: List): Boolean { fun List.filterFilenames(): List = distinctBy { it.fileName } -fun List.filterTempFilter(): List = filterNot { it.isTempFile() } - fun OCFile.isTempFile(): Boolean { val context = MainApp.getAppContext() val appTempPath = FileStorageUtils.getAppTempDirectoryPath(context) return storagePath?.startsWith(appTempPath) == true } -fun List.filterHiddenFiles(): List = filterNot { it.isHidden }.distinct() - -fun List.filterByMimeType(mimeType: String): List = - filter { it.isFolder || it.mimeType.startsWith(mimeType) } - -fun List.limitToPersonalFiles(userId: String): List = filter { file -> - file.ownerId?.let { ownerId -> - ownerId == userId && !file.isSharedWithMe && !file.mounted() - } == true -} - fun OCFile.mediaSize(defaultThumbnailSize: Float): Pair { val width = (imageDimension?.width?.toInt() ?: defaultThumbnailSize.toInt()) val height = (imageDimension?.height?.toInt() ?: defaultThumbnailSize.toInt())