diff --git a/app/src/main/java/com/nextcloud/client/preferences/AppPreferences.java b/app/src/main/java/com/nextcloud/client/preferences/AppPreferences.java index b7d6818ea5a1..4f4e85599480 100644 --- a/app/src/main/java/com/nextcloud/client/preferences/AppPreferences.java +++ b/app/src/main/java/com/nextcloud/client/preferences/AppPreferences.java @@ -13,6 +13,7 @@ import com.nextcloud.appReview.AppReviewShownModel; import com.nextcloud.client.jobs.LogEntry; import com.owncloud.android.datamodel.OCFile; +import com.owncloud.android.ui.fragment.FolderLayout; import com.owncloud.android.utils.FileSortOrder; import java.util.List; @@ -131,19 +132,19 @@ default void onDarkThemeModeChanged(DarkMode mode) { /** * Get preferred folder display type. * - * @param folder Folder + * @param layout FolderLayout * @return preference value, default is * {@link com.owncloud.android.ui.fragment.OCFileListFragment#FOLDER_LAYOUT_LIST} */ - String getFolderLayout(OCFile folder); + String getFolderLayout(FolderLayout layout); /** * Set preferred folder display type. * - * @param folder Folder which layout is being set or null for root folder + * @param layout FolderLayout which layout is being set * @param layoutName preference value */ - void setFolderLayout(@Nullable OCFile folder, String layoutName); + void setFolderLayout(@NonNull FolderLayout layout, String layoutName); /** * Saves the path where the user selected to do the last upload of a file shared from other app. diff --git a/app/src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java b/app/src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java index a16beaeaaad9..709f368080d8 100644 --- a/app/src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java +++ b/app/src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java @@ -28,6 +28,7 @@ import com.owncloud.android.datamodel.OCFile; import com.owncloud.android.ui.activity.PassCodeActivity; import com.owncloud.android.ui.activity.SettingsActivity; +import com.owncloud.android.ui.fragment.FolderLayout; import com.owncloud.android.utils.FileSortOrder; import java.lang.reflect.Type; @@ -82,7 +83,6 @@ public final class AppPreferencesImpl implements AppPreferences { private static final String PREF__AUTO_UPLOAD_SPLIT_OUT = "autoUploadEntriesSplitOut"; private static final String PREF__AUTO_UPLOAD_INIT = "autoUploadInit"; private static final String PREF__FOLDER_SORT_ORDER = "folder_sort_order"; - private static final String PREF__FOLDER_LAYOUT = "folder_layout"; private static final String PREF__LOCK_TIMESTAMP = "lock_timestamp"; private static final String PREF__SHOW_MEDIA_SCAN_NOTIFICATIONS = "show_media_scan_notifications"; @@ -336,21 +336,39 @@ public String[] getPassCode() { } @Override - public String getFolderLayout(OCFile folder) { - return getFolderPreference(context, - userAccountManager.getUser(), - PREF__FOLDER_LAYOUT, - folder, - FOLDER_LAYOUT_LIST); + public String getFolderLayout(FolderLayout layout) { + if (layout instanceof FolderLayout.Child child) { + // keep existing logic for child directories + return getFolderPreference(context, + userAccountManager.getUser(), + layout.getKey(), + child.getFolder(), + FOLDER_LAYOUT_LIST); + } else { + User user = userAccountManager.getUser(); + if (user.isAnonymous()) { + return FOLDER_LAYOUT_LIST; + } + + return preferences.getString(layout.getPrefKey(user), FOLDER_LAYOUT_LIST); + } } @Override - public void setFolderLayout(@Nullable OCFile folder, String layoutName) { - setFolderPreference(context, - userAccountManager.getUser(), - PREF__FOLDER_LAYOUT, - folder, - layoutName); + public void setFolderLayout(@NonNull FolderLayout layout, String layoutName) { + if (layout instanceof FolderLayout.Child child) { + setFolderPreference(context, + userAccountManager.getUser(), + child.getKey(), + child.getFolder(), + layoutName); + return; + } + + // only use new way for root shared, favorite and all files + preferences.edit() + .putString(layout.getPrefKey(userAccountManager.getUser()), layoutName) + .apply(); } @Override diff --git a/app/src/main/java/com/owncloud/android/operations/RenameFileOperation.java b/app/src/main/java/com/owncloud/android/operations/RenameFileOperation.java index 93e60b156689..2e9241d37ae1 100644 --- a/app/src/main/java/com/owncloud/android/operations/RenameFileOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/RenameFileOperation.java @@ -91,8 +91,10 @@ protected RemoteOperationResult run(OwnCloudClient client) { if (result.isSuccess()) { if (file.isFolder()) { getStorageManager().moveLocalFile(file, newRemotePath, parent); - //saveLocalDirectory(); - + file.setFileName(newName); + if (!file.isEncrypted()) { + file.setDecryptedRemotePath(newRemotePath); + } } else { saveLocalFile(newRemotePath); } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt index db6f451d3ef8..2fb3834aeb7e 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt @@ -1691,6 +1691,10 @@ class FileDisplayActivity : it.setEmptyListMessage(EmptyListState.ONLY_ON_DEVICE) } + it.searchEvent?.searchType == SearchRemoteOperation.SearchType.FAVORITE_SEARCH -> { + it.setEmptyListMessage(SearchType.FAVORITE_SEARCH) + } + else -> it.setEmptyListMessage(SearchType.NO_SEARCH) } } @@ -2174,41 +2178,39 @@ class FileDisplayActivity : */ private fun onRemoveFileOperationFinish(operation: RemoveFileOperation, result: RemoteOperationResult<*>) { deleteBatchTracker.onSingleDeleteFinished() + if (!result.isSuccess && result.isSslRecoverableException) { + mLastSslUntrustedServerResult = result + showUntrustedCertDialog(mLastSslUntrustedServerResult) + return + } - if (result.isSuccess) { - val removedFile = operation.file - tryStopPlaying(removedFile) - val leftFragment = this.leftFragment + if (!result.isSuccess) { + Log_OC.e(TAG, "deletion failed") + return + } - // check if file is still available, if so do nothing - val fileAvailable = storageManager.fileExists(removedFile.fileId) - if (leftFragment is FileFragment && !fileAvailable && removedFile == leftFragment.file) { - file = storageManager.getFileById(removedFile.parentId) - resetScrollingAndUpdateActionBar() - } - val parentFile = storageManager.getFileById(removedFile.parentId) - if (parentFile != null && parentFile == getCurrentDir()) { - updateListOfFilesFragment() - } else if (leftFragment is OCFileListFragment && - SearchRemoteOperation.SearchType.FAVORITE_SEARCH == leftFragment.searchEvent?.searchType - ) { - leftFragment.adapter?.run { - val file = files.find { it.fileId == removedFile.fileId } - if (file != null) { - val pos = getItemPosition(file) - files.remove(file) - notifyItemRemoved(pos) - } - } - } - supportInvalidateOptionsMenu() - fetchRecommendedFilesIfNeeded(ignoreETag = true, currentDir) - } else { - if (result.isSslRecoverableException) { - mLastSslUntrustedServerResult = result - showUntrustedCertDialog(mLastSslUntrustedServerResult) + val removedFile = operation.file + tryStopPlaying(removedFile) + val leftFragment = this.leftFragment + + // check if file is still available, if so do nothing + val fileAvailable = storageManager.fileExists(removedFile.fileId) + if (leftFragment is FileFragment && !fileAvailable && removedFile == leftFragment.file) { + file = storageManager.getFileById(removedFile.parentId) + resetScrollingAndUpdateActionBar() + } + + if (leftFragment is OCFileListFragment) { + leftFragment.adapter?.removeFile(removedFile) + + if (leftFragment.adapter?.isEmpty == true) { + val emptyState = leftFragment.searchEvent?.toSearchType() ?: SearchType.NO_SEARCH + leftFragment.setEmptyListMessage(emptyState) } } + + supportInvalidateOptionsMenu() + fetchRecommendedFilesIfNeeded(ignoreETag = true, currentDir) } override fun onAutoUploadFolderRemoved( @@ -2352,38 +2354,7 @@ class FileDisplayActivity : private fun onRenameFileOperationFinish(operation: RenameFileOperation, result: RemoteOperationResult<*>) { val optionalUser = user val renamedFile = operation.file - if (result.isSuccess && optionalUser.isPresent) { - val currentUser = optionalUser.get() - val leftFragment = this.leftFragment - if (leftFragment is FileFragment) { - if (leftFragment is FileDetailFragment && renamedFile == leftFragment.file) { - leftFragment.updateFileDetails(renamedFile, currentUser) - showDetails(renamedFile) - } else if (leftFragment is PreviewMediaFragment && renamedFile == leftFragment.file) { - leftFragment.updateFile(renamedFile) - if (PreviewMediaFragment.canBePreviewed(renamedFile)) { - val position = leftFragment.position - startMediaPreview(renamedFile, position, true, true, true, false) - } else { - fileOperationsHelper.openFile(renamedFile) - } - } else if (leftFragment is PreviewTextFragment && renamedFile == leftFragment.file) { - (leftFragment as PreviewTextFileFragment).updateFile(renamedFile) - if (PreviewTextFileFragment.canBePreviewed(renamedFile)) { - startTextPreview(renamedFile, true) - } else { - fileOperationsHelper.openFile(renamedFile) - } - } - } - - val file = storageManager.getFileById(renamedFile.parentId) - if (file != null && file == getCurrentDir()) { - updateListOfFilesFragment() - } - refreshGalleryFragmentIfNeeded() - fetchRecommendedFilesIfNeeded(ignoreETag = true, currentDir) - } else { + if (!result.isSuccess || optionalUser.isEmpty) { DisplayUtils.showSnackMessage( this, ErrorMessageAdapter.getErrorCauseMessage(result, operation, getResources()) @@ -2393,6 +2364,57 @@ class FileDisplayActivity : mLastSslUntrustedServerResult = result showUntrustedCertDialog(mLastSslUntrustedServerResult) } + return + } + + val currentUser = optionalUser.get() + val leftFragment = this.leftFragment + if (leftFragment is FileFragment) { + onRenameFileOperationFinishForFileFragment(leftFragment, renamedFile, currentUser) + } + + val file = storageManager.getFileById(renamedFile.parentId) + if (file != null && file == getCurrentDir()) { + fileListFragment?.adapter?.updateFile(renamedFile) + } + + refreshGalleryFragmentIfNeeded() + fetchRecommendedFilesIfNeeded(ignoreETag = true, currentDir) + } + + private fun onRenameFileOperationFinishForFileFragment(fragment: FileFragment, ocFile: OCFile, user: User) { + if (fragment.file != ocFile) return + + when (fragment) { + is FileDetailFragment -> { + fragment.updateFileDetails(ocFile, user) + showDetails(ocFile) + } + + is PreviewMediaFragment -> { + fragment.updateFile(ocFile) + if (PreviewMediaFragment.canBePreviewed(ocFile)) { + startMediaPreview( + ocFile, + fragment.position, + true, + true, + true, + false + ) + } else { + fileOperationsHelper.openFile(ocFile) + } + } + + is PreviewTextFileFragment -> { + fragment.updateFile(ocFile) + if (PreviewTextFileFragment.canBePreviewed(ocFile)) { + startTextPreview(ocFile, true) + } else { + fileOperationsHelper.openFile(ocFile) + } + } } } 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 a2c7616dda3c..a7566c809d19 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 @@ -81,6 +81,7 @@ import java.util.Locale; import java.util.Set; import java.util.UUID; +import java.util.stream.IntStream; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -875,7 +876,9 @@ public void swapDirectory( public void updateAdapter(List newFiles, OCFile directory) { Log_OC.d(TAG, "updating the adapter"); - mFiles = new ArrayList<>(newFiles); + mFiles.clear(); + mFiles.addAll(newFiles); + mFilesAll.clear(); mFilesAll.addAll(mFiles); @@ -982,12 +985,6 @@ public void insertFile(@Nullable OCFile file) { } } - 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); @@ -1112,4 +1109,67 @@ public void removeAllFiles() { mFilesAll.clear(); notifyDataSetChanged(); } + + @SuppressLint("NotifyDataSetChanged") + public void removeFile(@NonNull OCFile file) { + int position = getItemPosition(file); + + mFiles.remove(file); + mFilesAll.remove(file); + + if (position != -1) { + notifyItemRemoved(position); + } else { + notifyDataSetChanged(); + } + } + + @SuppressLint("NotifyDataSetChanged") + public void updateFile(@NonNull OCFile updatedFile) { + long fileId = updatedFile.getFileId(); + + IntStream.range(0, mFilesAll.size()) + .filter(i -> mFilesAll.get(i).getFileId() == fileId) + .findFirst() + .ifPresent(i -> mFilesAll.set(i, updatedFile)); + + int oldIndex = IntStream.range(0, mFiles.size()) + .filter(i -> mFiles.get(i).getFileId() == fileId) + .findFirst() + .orElse(-1); + if (oldIndex == -1) return; + + mFiles.remove(oldIndex); + mFiles.add(updatedFile); + + FileSortOrder currentSortOrder = preferences.getSortOrderByFolder(currentDirectory); + if (searchType == SearchType.SHARED_FILTER) { + mFiles.sort((o1, o2) -> Long.compare(o2.getFirstShareTimestamp(), o1.getFirstShareTimestamp())); + } else { + boolean foldersBeforeFiles = preferences.isSortFoldersBeforeFiles(); + boolean favoritesFirst = preferences.isSortFavoritesFirst(); + mFiles = currentSortOrder.sortCloudFiles(mFiles, foldersBeforeFiles, favoritesFirst); + } + + int newIndex = mFiles.indexOf(updatedFile); + if (newIndex == -1) { + notifyDataSetChanged(); + return; + } + + int headerOffset = shouldShowHeader() ? 1 : 0; + int oldAdapterPos = oldIndex + headerOffset; + int newAdapterPos = newIndex + headerOffset; + + if (oldAdapterPos != newAdapterPos) { + notifyItemMoved(oldAdapterPos, newAdapterPos); + } + notifyItemChanged(newAdapterPos); + + if (shouldShowRecommendedFiles() && recommendedFilesAdapter != null && updatedFile.isRecommendedFile()) { + int pos = recommendedFilesAdapter.getItemPosition(updatedFile); + if (pos != -1) recommendedFilesAdapter.notifyItemChanged(pos); + } + } + } diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.kt index 4042629d7560..c420fab77023 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.kt @@ -37,10 +37,6 @@ import com.owncloud.android.utils.KeyboardUtils import com.owncloud.android.utils.theme.ViewThemeUtils import javax.inject.Inject -/** - * Dialog to input a new name for an [OCFile] being renamed. - * Triggers the rename operation. - */ class RenameFileDialogFragment : DialogFragment(), DialogInterface.OnClickListener, @@ -58,7 +54,7 @@ class RenameFileDialogFragment : lateinit var currentAccount: CurrentAccountProvider private lateinit var binding: EditBoxDialogBinding - private var mTargetFile: OCFile? = null + private var targetFile: OCFile? = null private var positiveButton: MaterialButton? = null private var fileNames: MutableSet? = null @@ -73,15 +69,15 @@ class RenameFileDialogFragment : } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - mTargetFile = requireArguments().getParcelableArgument(ARG_TARGET_FILE, OCFile::class.java) + targetFile = requireArguments().getParcelableArgument(ARG_TARGET_FILE, OCFile::class.java) val inflater = requireActivity().layoutInflater binding = EditBoxDialogBinding.inflate(inflater, null, false) - val currentName = mTargetFile?.fileName + val currentName = targetFile?.fileName binding.userInput.setText(currentName) viewThemeUtils.material.colorTextInputLayout(binding.userInputContainer) - val extensionStart = if (mTargetFile?.isFolder == true) -1 else currentName?.lastIndexOf('.') + val extensionStart = if (targetFile?.isFolder == true) -1 else currentName?.lastIndexOf('.') val selectionEnd = if ((extensionStart ?: -1) >= 0) extensionStart else currentName?.length if (selectionEnd != null) { binding.userInput.setSelection(0, selectionEnd) @@ -97,7 +93,7 @@ class RenameFileDialogFragment : binding.userInput.addTextChangedListener( FileNameTextWatcher( - previousFileName = mTargetFile?.fileName, + previousFileName = targetFile?.fileName, context = binding.userInputContainer.context, capabilitiesProvider = { oCCapability }, existingFileNamesProvider = { fileNames ?: setOf() }, @@ -168,17 +164,24 @@ class RenameFileDialogFragment : return } - if (mTargetFile?.isOfflineOperation == true) { - fileDataStorageManager.renameOfflineOperation(mTargetFile, newFileName) - typedActivity()?.refreshCurrentDirectory() - } else { - typedActivity()?.connectivityService?.isNetworkAndServerAvailable { result -> - if (result) { - typedActivity()?.fileOperationsHelper?.renameFile(mTargetFile, newFileName) - } else { - fileDataStorageManager.addRenameFileOfflineOperation(mTargetFile, newFileName) - typedActivity()?.refreshCurrentDirectory() - } + val fda = typedActivity() + + if (targetFile?.isOfflineOperation == true) { + fileDataStorageManager.renameOfflineOperation(targetFile, newFileName) + fda?.refreshCurrentDirectory() + return + } + + fda?.connectivityService?.isNetworkAndServerAvailable { result -> + if (result) { + /* + * result of it triggered by + * [com.owncloud.android.ui.activity.FileDisplayActivity.onRemoteOperationFinish] + */ + typedActivity()?.fileOperationsHelper?.renameFile(targetFile, newFileName) + } else { + fileDataStorageManager.addRenameFileOfflineOperation(targetFile, newFileName) + fda?.refreshCurrentDirectory() } } } @@ -188,22 +191,13 @@ class RenameFileDialogFragment : private const val ARG_TARGET_FILE = "TARGET_FILE" private const val ARG_PARENT_FOLDER = "PARENT_FOLDER" - /** - * Public factory method to create new RenameFileDialogFragment instances. - * - * @param file File to rename. - * @return Dialog ready to show. - */ @JvmStatic - fun newInstance(file: OCFile?, parentFolder: OCFile?): RenameFileDialogFragment { - val bundle = Bundle().apply { - putParcelable(ARG_TARGET_FILE, file) - putParcelable(ARG_PARENT_FOLDER, parentFolder) - } - - return RenameFileDialogFragment().apply { - arguments = bundle + fun newInstance(file: OCFile?, parentFolder: OCFile?): RenameFileDialogFragment = + RenameFileDialogFragment().apply { + arguments = Bundle().apply { + putParcelable(ARG_TARGET_FILE, file) + putParcelable(ARG_PARENT_FOLDER, parentFolder) + } } - } } } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileListLayoutManager.kt b/app/src/main/java/com/owncloud/android/ui/fragment/FileListLayoutManager.kt index 87070d218f14..1f4375466bc7 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileListLayoutManager.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileListLayoutManager.kt @@ -14,7 +14,6 @@ import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.nextcloud.client.preferences.AppPreferences -import com.owncloud.android.datamodel.OCFile import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.ui.adapter.OCFileListAdapter import com.owncloud.android.utils.DisplayUtils @@ -27,26 +26,13 @@ class FileListLayoutManager(private val fragment: OCFileListFragment, private va sortOrder?.let { fragment.mAdapter.setSortOrder(fragment.mFile, it) } } - /** - * Determines whether a folder should be displayed in grid or list view. - * - * - * The preference is checked for the given folder. If the folder itself does not have a preference set, - * it will fall back to its parent folder recursively until a preference is found (root folder is always set). - * Additionally, if a search event is active and is of type `SHARED_FILTER`, grid view is disabled. - * - * @param folder The folder to check, or `null` to refer to the root folder. - * @return `true` if the folder should be displayed in grid mode, `false` if list mode is preferred. - */ - fun isGridViewPreferred(folder: OCFile?): Boolean = if (fragment.searchEvent != null) { - (fragment.searchEvent.toSearchType() != SearchType.SHARED_FILTER) && - (OCFileListFragment.FOLDER_LAYOUT_GRID == preferences.getFolderLayout(folder)) - } else { - OCFileListFragment.FOLDER_LAYOUT_GRID == preferences.getFolderLayout(folder) + fun isGridViewPreferred(): Boolean { + val layout = FolderLayout.get(fragment.mFile, fragment.currentSearchType) + return preferences.getFolderLayout(layout) == OCFileListFragment.FOLDER_LAYOUT_GRID } fun setLayoutViewMode() { - val isGrid = isGridViewPreferred(fragment.mFile) + val isGrid = isGridViewPreferred() if (isGrid) { switchToGridView() @@ -58,7 +44,8 @@ class FileListLayoutManager(private val fragment: OCFileListFragment, private va } fun setListAsPreferred() { - preferences.setFolderLayout(fragment.mFile, OCFileListFragment.FOLDER_LAYOUT_LIST) + val layout = FolderLayout.get(fragment.mFile, fragment.currentSearchType) + preferences.setFolderLayout(layout, OCFileListFragment.FOLDER_LAYOUT_LIST) switchToListView() } @@ -69,7 +56,8 @@ class FileListLayoutManager(private val fragment: OCFileListFragment, private va } fun setGridAsPreferred() { - preferences.setFolderLayout(fragment.mFile, OCFileListFragment.FOLDER_LAYOUT_GRID) + val layout = FolderLayout.get(fragment.mFile, fragment.currentSearchType) + preferences.setFolderLayout(layout, OCFileListFragment.FOLDER_LAYOUT_GRID) switchToGridView() } @@ -100,12 +88,11 @@ class FileListLayoutManager(private val fragment: OCFileListFragment, private va val layoutManager: RecyclerView.LayoutManager? if (grid) { layoutManager = GridLayoutManager(context, fragment.columnsCount) - val gridLayoutManager = layoutManager - gridLayoutManager.spanSizeLookup = object : SpanSizeLookup() { + layoutManager.spanSizeLookup = object : SpanSizeLookup() { override fun getSpanSize(position: Int): Int = if (position == fragment.adapter.itemCount - 1 || (position == 0 && fragment.adapter.shouldShowHeader()) ) { - gridLayoutManager.spanCount + layoutManager.spanCount } else { 1 } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FolderLayout.kt b/app/src/main/java/com/owncloud/android/ui/fragment/FolderLayout.kt new file mode 100644 index 000000000000..cfa69a6493c7 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FolderLayout.kt @@ -0,0 +1,53 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.ui.fragment + +import com.nextcloud.client.account.User +import com.owncloud.android.datamodel.OCFile + +/** + * AllFiles, Favorites and Shared represents only for root of them + * Child is valid for any child directory of all files, favorites or shared. + * + * Important: + * Do not change key to not lose data. + */ +sealed class FolderLayout(val key: String) { + data object AllFiles : FolderLayout("all_files_folder_layout") + data object Favorites : FolderLayout("favorite_folder_layout") + data object Shared : FolderLayout("shared_folder_layout") + data class Child(val folder: OCFile) : FolderLayout("folder_layout") + + /** + * Returns shared pref key only child uses key without user so that we dont lose + * previous information. + * + * User is needed since multiple account can be used. + */ + fun getPrefKey(user: User): String { + if (this is Child) { + return key + } + + return user.accountName + "_" + key + } + + companion object { + fun get(folder: OCFile?, searchType: SearchType): FolderLayout { + if (folder != null && folder.isFolder && !folder.isRootDirectory) { + return Child(folder) + } + + return when (searchType) { + SearchType.SHARED_FILTER -> Shared + SearchType.FAVORITE_SEARCH -> Favorites + else -> AllFiles + } + } + } +} 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 d31dd21f5fba..92c8c1749821 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 @@ -468,7 +468,7 @@ protected void setAdapter(Bundle args) { boolean hideItemOptions = args != null && args.getBoolean(ARG_HIDE_ITEM_OPTIONS, false); boolean isGridViewPreferred = false; if (fileListLayoutManager != null) { - isGridViewPreferred = fileListLayoutManager.isGridViewPreferred(mFile); + isGridViewPreferred = fileListLayoutManager.isGridViewPreferred(); } mAdapter = new OCFileListAdapter( @@ -1818,7 +1818,9 @@ protected void handleSearchEvent(SearchEvent event) { } final var activity = getActivity(); - if (activity != null) { + + // only show loading state first time if app doesn't have active search task + if (activity != null && searchTask == null) { activity.runOnUiThread(() -> { getAdapter().removeAllFiles(); setEmptyListMessage(EmptyListState.LOADING); 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 26a8943b311d..6788110f67cf 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 @@ -10,6 +10,7 @@ package com.owncloud.android.ui.fragment import android.annotation.SuppressLint import android.app.Activity +import android.accounts.Account import android.content.ContentValues import androidx.lifecycle.lifecycleScope import com.nextcloud.client.account.User @@ -67,68 +68,68 @@ class OCFileListSearchTask( job = fragment.lifecycleScope.launch(Dispatchers.IO) { 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) - - // updating cache and refreshing adapter - val result = fetchRemoteResults() - if (result?.isSuccess == true) { - if (result.resultData?.isEmpty() == true) { - withContext(Dispatchers.Main) { - fragment.setEmptyListMessage(fragment.currentSearchType) - return@withContext - } - - return@launch - } - - fragment.adapter.prepareForSearchData(storageManager, fragment.currentSearchType) - - val newList = if (searchType == SearchType.SHARED_FILTER) { - OCShareToOCFileConverter.parseAndSaveShares( - sortedFilesInDb, - result.resultData ?: listOf(), - storageManager, - currentUser.accountName - ) - } else { - parseAndSaveVirtuals(result.resultData ?: listOf(), fragment) - fragment.adapter.files - } - - val sortedNewList = sortSearchData(newList, searchType, null, setNewSortOrder = { - fragment.adapter.setSortOrder(it) - }) + val cachedFiles = loadSortedCachedDbFiles(event.searchType, searchType, fragment) + if (cachedFiles.isNotEmpty()) { + updateAdapterData(fragment, cachedFiles) + } - updateAdapterData(fragment, sortedNewList) + val result = fetchRemoteResults()?.takeIf { it.isSuccess } ?: run { + showSnackbarError(fragment) + return@launch + } + val resultData = result.resultData?.takeIf { it.isNotEmpty() } ?: run { + withContext(Dispatchers.Main) { fragment.setEmptyListMessage(fragment.currentSearchType) } return@launch } - withContext(Dispatchers.Main) { - fragment.activity?.let { - DisplayUtils.showSnackMessage(it, R.string.error_fetching_sharees) - } + fragment.adapter.prepareForSearchData(storageManager, fragment.currentSearchType) + val remoteFiles = fetchAndSortRemoteFiles(searchType, cachedFiles, resultData, fragment) + updateAdapterData(fragment, remoteFiles) + } + } + + private suspend fun showSnackbarError(fragment: OCFileListFragment) { + withContext(Dispatchers.Main) { + fragment.activity?.let { + DisplayUtils.showSnackMessage(it, R.string.error_fetching_sharees) } } } - fun cancel() = job?.cancel(null) + private suspend fun loadSortedCachedDbFiles( + searchType: SearchRemoteOperation.SearchType, + fragmentSearchType: SearchType, + fragment: OCFileListFragment + ): List { + val files = if (searchType == SearchRemoteOperation.SearchType.SHARED_FILTER) { + storageManager.fileDao.getSharedFiles(currentUser.accountName) + } else { + storageManager.fileDao.getFavoriteFiles(currentUser.accountName) + }.mapNotNull { storageManager.createFileInstance(it) } - fun isFinished(): Boolean = job?.isCompleted == true + return sortSearchData(files, fragmentSearchType, fragment) + } - private suspend fun loadCachedDbFiles(searchType: SearchRemoteOperation.SearchType): List = - if (searchType == SearchRemoteOperation.SearchType.SHARED_FILTER) { - storageManager.fileDao - .getSharedFiles(currentUser.accountName) + private suspend fun fetchAndSortRemoteFiles( + searchType: SearchType, + sortedFilesInDb: List, + resultData: List, + fragment: OCFileListFragment + ): List { + val newList = if (searchType == SearchType.SHARED_FILTER) { + OCShareToOCFileConverter.parseAndSaveShares( + sortedFilesInDb, + resultData, + storageManager, + currentUser.accountName + ) } else { - storageManager.fileDao - .getFavoriteFiles(currentUser.accountName) - }.mapNotNull { storageManager.createFileInstance(it) } + parseAndSaveVirtuals(resultData, fragment) + } + + return sortSearchData(newList, searchType, fragment) + } @Suppress("DEPRECATION") private suspend fun fetchRemoteResults(): RemoteOperationResult>? { @@ -155,18 +156,11 @@ class OCFileListSearchTask( fragment.adapter.updateAdapter(newList, null) } - private suspend fun sortSearchData( - list: List, - searchType: SearchType, - folder: OCFile?, - setNewSortOrder: (FileSortOrder) -> Unit - ): List = withContext(Dispatchers.IO) { - var newList = list.toMutableList() - + private fun sortSearchData(list: List, searchType: SearchType, fragment: OCFileListFragment): List { if (searchType == SearchType.GALLERY_SEARCH || searchType == SearchType.RECENT_FILES_SEARCH ) { - return@withContext FileStorageUtils.sortOcFolderDescDateModifiedWithoutFavoritesFirst(newList) + return FileStorageUtils.sortOcFolderDescDateModifiedWithoutFavoritesFirst(list) } val foldersBeforeFiles = preferences.isSortFoldersBeforeFiles() @@ -182,20 +176,18 @@ class OCFileListSearchTask( } else -> { - preferences.getSortOrderByFolder(folder) + preferences.getSortOrderByFolder(null) } } - setNewSortOrder(sortOrder) - newList = sortOrder.sortCloudFiles(newList, foldersBeforeFiles, favoritesFirst) - - return@withContext newList + fragment.adapter.setSortOrder(sortOrder) + return sortOrder.sortCloudFiles(list.toMutableList(), foldersBeforeFiles, favoritesFirst) } @Suppress("DEPRECATION") - private suspend fun parseAndSaveVirtuals(data: List, fragment: OCFileListFragment) = + private suspend fun parseAndSaveVirtuals(data: List, fragment: OCFileListFragment): List = withContext(Dispatchers.IO) { - val activity = fragment.activity ?: return@withContext + val activity = fragment.activity ?: return@withContext emptyList() val now = System.currentTimeMillis() val (virtualType, onlyMedia) = when (fragment.currentSearchType) { @@ -205,64 +197,60 @@ class OCFileListSearchTask( } val contentValuesList = ArrayList() + val resultFiles = ArrayList() + var cachedClient: Account? = null for (obj in data) { try { val remoteFile = obj as? RemoteFile ?: continue var ocFile = FileStorageUtils.fillOCFile(remoteFile) FileStorageUtils.searchForLocalFileInDefaultPath(ocFile, currentUser.accountName) + resolveLocalFileId(ocFile) ocFile = storageManager.saveFileWithParent(ocFile, activity) - ocFile = handleEncryptionIfNeeded(ocFile, storageManager, activity) + ocFile = handleEncryptionIfNeeded(ocFile, storageManager, activity) { + cachedClient ?: currentUser.toPlatformAccount().also { cachedClient = it } + } if (fragment.currentSearchType != SearchType.GALLERY_SEARCH && ocFile.isFolder) { - RefreshFolderOperation( - ocFile, - now, - true, - false, - storageManager, - currentUser, - activity - ).execute(currentUser, activity) + RefreshFolderOperation(ocFile, now, true, false, storageManager, currentUser, activity) + .execute(currentUser, activity) } - val isMediaAllowed = - !onlyMedia || MimeTypeUtil.isImage(ocFile) || MimeTypeUtil.isVideo(ocFile) - + val isMediaAllowed = !onlyMedia || MimeTypeUtil.isImage(ocFile) || MimeTypeUtil.isVideo(ocFile) if (isMediaAllowed) { - fragment.adapter.addVirtualFile(ocFile) + resultFiles.add(ocFile) } - val cv = ContentValues().apply { - put(ProviderMeta.ProviderTableMeta.VIRTUAL_TYPE, virtualType.toString()) - put(ProviderMeta.ProviderTableMeta.VIRTUAL_OCFILE_ID, ocFile.fileId) - } - contentValuesList.add(cv) + contentValuesList.add( + ContentValues().apply { + put(ProviderMeta.ProviderTableMeta.VIRTUAL_TYPE, virtualType.toString()) + put(ProviderMeta.ProviderTableMeta.VIRTUAL_OCFILE_ID, ocFile.fileId) + } + ) } catch (e: Exception) { Log_OC.e(TAG, "parseAndSaveVirtuals():", e) } } - // Save timestamp + virtual entries preferences.setPhotoSearchTimestamp(System.currentTimeMillis()) storageManager.saveVirtuals(contentValuesList) + + return@withContext resultFiles } @Suppress("DEPRECATION") private fun handleEncryptionIfNeeded( ocFile: OCFile, fileDataStorage: FileDataStorageManager, - activity: Activity + activity: Activity, + accountProvider: () -> Account ): OCFile { val parent = fileDataStorage.getFileById(ocFile.parentId) ?: return ocFile if (!ocFile.isEncrypted && !parent.isEncrypted) return ocFile - val client = OwnCloudClientFactory.createOwnCloudClient( - currentUser.toPlatformAccount(), - activity - ) + val client = OwnCloudClientFactory.createOwnCloudClient(accountProvider(), activity) val metadata = RefreshFolderOperation.getDecryptedFolderMetadata( true, @@ -290,4 +278,16 @@ class OCFileListSearchTask( return fileDataStorage.saveFileWithParent(ocFile, activity) } + + private fun resolveLocalFileId(ocFile: OCFile) { + if (ocFile.fileId != -1L) return + val localFile = storageManager.getFileByLocalId(ocFile.localId) ?: return + ocFile.fileId = localFile.fileId + } + + // region public methods + fun cancel() = job?.cancel(null) + + fun isFinished(): Boolean = job?.isCompleted == true + // endregion }