From 4d7752f163e0f04a2e8596450d5629f785cdcc9e Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 24 Mar 2026 10:19:46 +0100 Subject: [PATCH 1/7] feat(sync): download folder including all nested files Signed-off-by: alperozturk96 --- .../nextcloud/client/database/dao/FileDao.kt | 23 +++++ .../client/jobs/BackgroundJobManager.kt | 2 +- .../client/jobs/BackgroundJobManagerImpl.kt | 3 +- .../client/jobs/InternalTwoWaySyncWork.kt | 10 +- .../jobs/download/FileDownloadHelper.kt | 4 +- .../folderDownload/FolderDownloadWorker.kt | 32 +++++-- .../nextcloud/ui/fileactions/FileAction.kt | 12 ++- .../android/files/FileMenuFilter.java | 1 + .../SynchronizeFolderOperation.java | 8 +- .../android/services/OperationsService.java | 7 +- .../ui/activity/FileDisplayActivity.kt | 4 +- .../SyncFileNotEnoughSpaceDialogFragment.kt | 92 ++++++++----------- .../ui/fragment/FileDetailFragment.java | 5 +- .../ui/fragment/OCFileListFragment.java | 60 +++++++----- .../ui/helpers/FileOperationsHelper.java | 11 ++- .../ui/preview/PreviewImageFragment.kt | 2 +- .../ui/preview/PreviewMediaActivity.kt | 2 +- .../ui/preview/PreviewMediaFragment.kt | 2 +- .../ui/preview/PreviewTextFileFragment.java | 2 +- app/src/main/res/drawable/ic_sync_all.xml | 18 ++++ app/src/main/res/values/ids.xml | 1 + app/src/main/res/values/strings.xml | 1 + 22 files changed, 192 insertions(+), 110 deletions(-) create mode 100644 app/src/main/res/drawable/ic_sync_all.xml 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 7f15f5ec3252..db6faff53958 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 @@ -162,4 +162,27 @@ interface FileDao { @Query("DELETE FROM filelist WHERE file_owner = :fileOwner AND path = :remotePath") fun deleteFileByRemotePath(fileOwner: String, remotePath: String): Int + + @Query( + """ + WITH RECURSIVE descendants AS ( + SELECT _id, content_length, content_type + FROM filelist + WHERE _id = :folderId AND file_owner = :fileOwner + + UNION ALL + + SELECT f._id, f.content_length, f.content_type + FROM filelist f + INNER JOIN descendants d ON f.parent = d._id + WHERE f.file_owner = :fileOwner + ) + SELECT COALESCE(SUM(content_length), 0) + FROM descendants + WHERE content_type IS NOT NULL + AND content_type != '${MimeType.DIRECTORY}' + AND content_type != '${MimeType.WEBDAV_FOLDER}' + """ + ) + suspend fun getTotalFolderSize(folderId: Long, fileOwner: String): Long } diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt index e7111715764d..5607c3342bb5 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt @@ -164,6 +164,6 @@ interface BackgroundJobManager { fun scheduleInternal2WaySync(intervalMinutes: Long) fun cancelAllFilesDownloadJobs() fun startMetadataSyncJob(currentDirPath: String) - fun downloadFolder(folder: OCFile, accountName: String) + fun downloadFolder(folder: OCFile, accountName: String, syncAll: Boolean) fun cancelFolderDownload() } diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt index b465446e3f82..507eefdd5317 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt @@ -785,7 +785,7 @@ internal class BackgroundJobManagerImpl( workManager.enqueueUniquePeriodicWork(JOB_INTERNAL_TWO_WAY_SYNC, ExistingPeriodicWorkPolicy.UPDATE, request) } - override fun downloadFolder(folder: OCFile, accountName: String) { + override fun downloadFolder(folder: OCFile, accountName: String, syncAll: Boolean) { val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .setRequiresStorageNotLow(true) @@ -794,6 +794,7 @@ internal class BackgroundJobManagerImpl( val data = Data.Builder() .putLong(FolderDownloadWorker.FOLDER_ID, folder.fileId) .putString(FolderDownloadWorker.ACCOUNT_NAME, accountName) + .putBoolean(FolderDownloadWorker.SYNC_ALL, syncAll) .build() val request = oneTimeRequestBuilder(FolderDownloadWorker::class, JOB_DOWNLOAD_FOLDER) diff --git a/app/src/main/java/com/nextcloud/client/jobs/InternalTwoWaySyncWork.kt b/app/src/main/java/com/nextcloud/client/jobs/InternalTwoWaySyncWork.kt index 1d1c89fd9d3f..888c94069947 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/InternalTwoWaySyncWork.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/InternalTwoWaySyncWork.kt @@ -67,7 +67,15 @@ class InternalTwoWaySyncWork( } Log_OC.d(TAG, "Folder ${folder.remotePath}: started!") - operation = SynchronizeFolderOperation(context, folder.remotePath, user, fileDataStorageManager, true) + operation = + SynchronizeFolderOperation( + context, + folder.remotePath, + user, + fileDataStorageManager, + true, + false + ) val operationResult = operation?.execute(context) if (operationResult?.isSuccess == true) { diff --git a/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadHelper.kt b/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadHelper.kt index 240fda49ef75..6ecafff7d62a 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadHelper.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadHelper.kt @@ -133,12 +133,12 @@ class FileDownloadHelper { ) } - fun downloadFolder(folder: OCFile?, accountName: String) { + fun downloadFolder(folder: OCFile?, accountName: String, syncAll: Boolean) { if (folder == null) { Log_OC.e(TAG, "folder cannot be null, cant sync") return } - backgroundJobManager.downloadFolder(folder, accountName) + backgroundJobManager.downloadFolder(folder, accountName, syncAll) } fun cancelFolderDownload() = backgroundJobManager.cancelFolderDownload() diff --git a/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorker.kt index cc0ec499b196..ada4b999d27e 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorker.kt @@ -39,8 +39,9 @@ class FolderDownloadWorker( private const val TAG = "πŸ“‚" + "FolderDownloadWorker" const val FOLDER_ID = "FOLDER_ID" const val ACCOUNT_NAME = "ACCOUNT_NAME" + const val SYNC_ALL = "SYNC_ALL" - private val pendingDownloads: MutableSet = ConcurrentHashMap.newKeySet() + private val pendingDownloads: MutableSet = ConcurrentHashMap.newKeySet() fun isDownloading(id: Long): Boolean = pendingDownloads.contains(id) } @@ -68,6 +69,8 @@ class FolderDownloadWorker( return Result.failure() } + val syncAll = inputData.getBoolean(SYNC_ALL, false) + val user = optionalUser.get() storageManager = FileDataStorageManager(user, context.contentResolver) val folder = storageManager.getFileById(folderID) @@ -76,6 +79,14 @@ class FolderDownloadWorker( return Result.failure() } + if (syncAll) { + Log_OC.d(TAG, "checking folder size including all nested subfolders") + val folderSize = storageManager.fileDao.getTotalFolderSize(folderID, accountName) + if (!checkDiskSize(folderSize)) { + return Result.failure() + } + } + Log_OC.d(TAG, "πŸ•’ started for ${user.accountName} downloading ${folder.fileName}") trySetForeground(folder) @@ -87,13 +98,13 @@ class FolderDownloadWorker( return withContext(Dispatchers.IO) { try { - val files = getFiles(folder, storageManager) + val files = getFiles(folder, storageManager, syncAll) val account = user.toOwnCloudAccount() val client = OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(account, context) var result = true files.forEachIndexed { index, file -> - if (!checkDiskSize(file)) { + if (!checkDiskSize(file.fileLength)) { return@withContext Result.failure() } @@ -184,15 +195,20 @@ class FolderDownloadWorker( return file } - private fun getFiles(folder: OCFile, storageManager: FileDataStorageManager): List = - storageManager.getFolderContent(folder, false) - .filter { !it.isFolder && !it.isDown } + private fun getFiles(folder: OCFile, storageManager: FileDataStorageManager, syncAll: Boolean): List = + if (syncAll) { + storageManager.getAllFilesRecursivelyInsideFolder(folder) + .filter { !it.isDown } + } else { + storageManager.getFolderContent(folder, false) + .filter { !it.isFolder && !it.isDown } + } - private fun checkDiskSize(file: OCFile): Boolean { - val fileSizeInByte = file.fileLength + private fun checkDiskSize(fileSizeInByte: Long): Boolean { val availableDiskSpace = FileOperationsHelper.getAvailableSpaceOnDevice() return if (availableDiskSpace < fileSizeInByte) { + Log_OC.w(TAG, "available disk space is not sufficient") notificationManager.showNotAvailableDiskSpace() false } else { diff --git a/app/src/main/java/com/nextcloud/ui/fileactions/FileAction.kt b/app/src/main/java/com/nextcloud/ui/fileactions/FileAction.kt index 43f1edc09109..c7d0c560df5b 100644 --- a/app/src/main/java/com/nextcloud/ui/fileactions/FileAction.kt +++ b/app/src/main/java/com/nextcloud/ui/fileactions/FileAction.kt @@ -40,6 +40,7 @@ enum class FileAction( // Uploads and downloads DOWNLOAD_FILE(R.id.action_download_file, R.string.filedetails_download, R.drawable.ic_cloud_download), DOWNLOAD_FOLDER(R.id.action_sync_file, R.string.filedetails_sync_file, R.drawable.ic_sync), + DOWNLOAD_ALL_FOLDERS(R.id.action_sync_all_files, R.string.filedetails_sync_all_files, R.drawable.ic_sync_all), CANCEL_SYNC(R.id.action_cancel_sync, R.string.common_cancel_sync, R.drawable.ic_sync_off), // File sharing @@ -96,6 +97,10 @@ enum class FileAction( PIN_TO_HOMESCREEN, RETRY ).apply { + if (files.size == 1 && files.first().isFolder) { + add(DOWNLOAD_ALL_FOLDERS) + } + val deleteOrLeaveShareAction = getDeleteOrLeaveShareAction(files) ?: return@apply add(deleteOrLeaveShareAction) } @@ -113,7 +118,7 @@ enum class FileAction( if (file != null) { val actionsToHide = getActionsToHide(setOf(file)) - result.removeAll(actionsToHide) + result.removeAll(actionsToHide.toSet()) } return result.toList() @@ -136,6 +141,7 @@ enum class FileAction( if (file?.isFolder == true) { result.add(R.id.action_send_file) result.add(R.id.action_sync_file) + result.add(R.id.action_sync_all_files) } if (file?.isAPKorAAB == true) { @@ -145,7 +151,7 @@ enum class FileAction( if (file != null) { val actionsToHide = getActionsToHide(setOf(file)) - result.removeAll(actionsToHide) + result.removeAll(actionsToHide.toSet()) } return result.toList() @@ -160,6 +166,7 @@ enum class FileAction( R.id.action_favorite, R.id.action_move_or_copy, R.id.action_sync_file, + R.id.action_sync_all_files, R.id.action_encrypted, R.id.action_unset_encrypted, R.id.action_edit, @@ -176,6 +183,7 @@ enum class FileAction( R.id.action_send_share_file, R.id.action_export_file, R.id.action_sync_file, + R.id.action_sync_all_files, R.id.action_download_file ) ) diff --git a/app/src/main/java/com/owncloud/android/files/FileMenuFilter.java b/app/src/main/java/com/owncloud/android/files/FileMenuFilter.java index 6665598c0a4b..9f29055f94fe 100644 --- a/app/src/main/java/com/owncloud/android/files/FileMenuFilter.java +++ b/app/src/main/java/com/owncloud/android/files/FileMenuFilter.java @@ -323,6 +323,7 @@ private void filterSync(List toHide, boolean synchronizing) { if (files.isEmpty() || (!anyFileDown() && !containsFolder()) || synchronizing || containsEncryptedFile() || containsEncryptedFolder()) { toHide.add(R.id.action_sync_file); + toHide.add(R.id.action_sync_all_files); } } diff --git a/app/src/main/java/com/owncloud/android/operations/SynchronizeFolderOperation.java b/app/src/main/java/com/owncloud/android/operations/SynchronizeFolderOperation.java index 9ffde49006c7..73a390a7e057 100644 --- a/app/src/main/java/com/owncloud/android/operations/SynchronizeFolderOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/SynchronizeFolderOperation.java @@ -88,6 +88,8 @@ public class SynchronizeFolderOperation extends SyncOperation { private final boolean syncInBackgroundWorker; + private final boolean syncAll; + /** * Creates a new instance of {@link SynchronizeFolderOperation}. * @@ -99,7 +101,8 @@ public SynchronizeFolderOperation(Context context, String remotePath, User user, FileDataStorageManager storageManager, - boolean syncInBackgroundWorker) { + boolean syncInBackgroundWorker, + boolean syncAll) { super(storageManager); mRemotePath = remotePath; @@ -110,6 +113,7 @@ public SynchronizeFolderOperation(Context context, mFilesToSyncContents = new Vector<>(); mCancellationRequested = new AtomicBoolean(false); this.syncInBackgroundWorker = syncInBackgroundWorker; + this.syncAll = syncAll; } @@ -488,7 +492,7 @@ private void startDirectDownloads() { Log_OC.d(TAG, "Exception caught at startDirectDownloads" + e); } } else { - fileDownloadHelper.downloadFolder(mLocalFolder, user.getAccountName()); + fileDownloadHelper.downloadFolder(mLocalFolder, user.getAccountName(), syncAll); } } diff --git a/app/src/main/java/com/owncloud/android/services/OperationsService.java b/app/src/main/java/com/owncloud/android/services/OperationsService.java index d8a645f6d98e..f19c385aa5b1 100644 --- a/app/src/main/java/com/owncloud/android/services/OperationsService.java +++ b/app/src/main/java/com/owncloud/android/services/OperationsService.java @@ -86,6 +86,7 @@ public class OperationsService extends Service { public static final String EXTRA_POST_DIALOG_EVENT = "EXTRA_POST_DIALOG_EVENT"; public static final String EXTRA_SERVER_URL = "SERVER_URL"; public static final String EXTRA_REMOTE_PATH = "REMOTE_PATH"; + public static final String EXTRA_SYNC_ALL = "SYNC_ALL"; public static final String EXTRA_NEWNAME = "NEWNAME"; public static final String EXTRA_REMOVE_ONLY_LOCAL = "REMOVE_LOCAL_COPY"; public static final String EXTRA_SYNC_FILE_CONTENTS = "SYNC_FILE_CONTENTS"; @@ -733,12 +734,14 @@ private Pair newOperation(Intent operationIntent) { case ACTION_SYNC_FOLDER: remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH); + boolean syncAll = operationIntent.getBooleanExtra(EXTRA_SYNC_ALL, false); operation = new SynchronizeFolderOperation( - this, // TODO remove this dependency from construction time + this, remotePath, user, fileDataStorageManager, - false + false, + syncAll ); break; 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 c21b7e7db188..d333e96bcc02 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 @@ -2191,8 +2191,8 @@ class FileDisplayActivity : fileOperationsHelper.removeFiles(list, true, true) // download new version, only if file was previously download - showSyncLoadingDialog(file.isFolder == true) - fileOperationsHelper.syncFile(file) + showSyncLoadingDialog(file.isFolder) + fileOperationsHelper.syncFile(file, false) } val parent = file?.let { storageManager.getFileById(it.parentId) } diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/SyncFileNotEnoughSpaceDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/SyncFileNotEnoughSpaceDialogFragment.kt index 3ccd4252360a..cca15c8cdcd4 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/SyncFileNotEnoughSpaceDialogFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/SyncFileNotEnoughSpaceDialogFragment.kt @@ -1,10 +1,8 @@ /* - * Nextcloud Android client application + * Nextcloud - Android Client * - * @author Kilian PΓ©risset - * Copyright (C) 2020 Infomaniak Network SA - * - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later */ package com.owncloud.android.ui.dialog @@ -12,87 +10,71 @@ import android.app.Dialog import android.content.Intent import android.os.Bundle import android.os.storage.StorageManager +import androidx.activity.result.contract.ActivityResultContracts import com.nextcloud.utils.extensions.getParcelableArgument import com.owncloud.android.R import com.owncloud.android.datamodel.OCFile import com.owncloud.android.ui.dialog.ConfirmationDialogFragment.ConfirmationDialogFragmentListener -import com.owncloud.android.ui.fragment.OCFileListFragment import com.owncloud.android.utils.DisplayUtils -/** - * Dialog requiring confirmation when a file/folder is too "big" to be synchronized/downloaded on device. - */ class SyncFileNotEnoughSpaceDialogFragment : ConfirmationDialogFragment(), ConfirmationDialogFragmentListener { private var targetFile: OCFile? = null + private val storageActivityResult = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { /* no-op */ } + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { targetFile = requireArguments().getParcelableArgument(ARG_PASSED_FILE, OCFile::class.java) setOnConfirmationListener(this) return super.onCreateDialog(savedInstanceState) } - /** - * (Only if file is a folder), will access the destination folder to allow user to choose what to synchronize - */ override fun onConfirmation(callerTag: String?) { - val frag = targetFragment as OCFileListFragment? - - if (frag != null && targetFile != null) { - frag.onItemClicked(targetFile) - } + parentFragmentManager.setFragmentResult( + REQUEST_KEY, + Bundle().apply { + putParcelable(RESULT_FILE, targetFile) + putString(RESULT_ACTION, ACTION_CHOOSE) + } + ) } - /** - * Will abort/cancel the process (is neutral to "hack" android button position ._.) - */ - override fun onNeutral(callerTag: String?) { - // Nothing - } + override fun onNeutral(callerTag: String?) = Unit - /** - * Will access to storage manager in order to empty useless files - */ override fun onCancel(callerTag: String?) { - val storageIntent = Intent(StorageManager.ACTION_MANAGE_STORAGE) - startActivityForResult(storageIntent, REQUEST_CODE_STORAGE) + storageActivityResult.launch(Intent(StorageManager.ACTION_MANAGE_STORAGE)) } companion object { + const val REQUEST_KEY = "SyncFileNotEnoughSpaceDialogFragment" + const val RESULT_FILE = "result_file" + const val RESULT_ACTION = "result_action" + const val ACTION_CHOOSE = "action_choose" + private const val ARG_PASSED_FILE = "fragment_parent_caller" - private const val REQUEST_CODE_STORAGE = 20 @JvmStatic - fun newInstance(file: OCFile, availableDeviceSpace: Long): SyncFileNotEnoughSpaceDialogFragment { - val args = Bundle() - val frag = SyncFileNotEnoughSpaceDialogFragment() - val properFileSize = DisplayUtils.bytesToHumanReadable(file.fileLength) - val properDiskAvailableSpace = DisplayUtils.bytesToHumanReadable(availableDeviceSpace) - - // Defining title, message and resources - args.putInt(ARG_TITLE_ID, R.string.sync_not_enough_space_dialog_title) - args.putInt(ARG_MESSAGE_RESOURCE_ID, R.string.sync_not_enough_space_dialog_placeholder) - args.putStringArray( - ARG_MESSAGE_ARGUMENTS, - arrayOf( - file.fileName, - properFileSize, - properDiskAvailableSpace + fun newInstance(file: OCFile, availableDeviceSpace: Long) = SyncFileNotEnoughSpaceDialogFragment().apply { + arguments = Bundle().apply { + putInt(ARG_TITLE_ID, R.string.sync_not_enough_space_dialog_title) + putInt(ARG_MESSAGE_RESOURCE_ID, R.string.sync_not_enough_space_dialog_placeholder) + putStringArray( + ARG_MESSAGE_ARGUMENTS, + arrayOf( + file.fileName, + DisplayUtils.bytesToHumanReadable(file.fileLength), + DisplayUtils.bytesToHumanReadable(availableDeviceSpace) + ) ) - ) - args.putParcelable(ARG_PASSED_FILE, file) - - // Defining buttons - if (file.isFolder) { - args.putInt(ARG_POSITIVE_BTN_RES, R.string.sync_not_enough_space_dialog_action_choose) + putParcelable(ARG_PASSED_FILE, file) + if (file.isFolder) putInt(ARG_POSITIVE_BTN_RES, R.string.sync_not_enough_space_dialog_action_choose) + putInt(ARG_NEGATIVE_BTN_RES, R.string.sync_not_enough_space_dialog_action_free_space) + putInt(ARG_NEUTRAL_BTN_RES, R.string.common_cancel) } - args.putInt(ARG_NEGATIVE_BTN_RES, R.string.sync_not_enough_space_dialog_action_free_space) - args.putInt(ARG_NEUTRAL_BTN_RES, R.string.common_cancel) - - frag.arguments = args - return frag } } } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java index 0cbf1aa8c700..4e11613690e4 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java @@ -465,11 +465,12 @@ private void optionsItemSelected(@IdRes final int itemId) { dialog.show(getFragmentManager(), FTAG_RENAME_FILE); } else if (itemId == R.id.action_cancel_sync) { ((FileDisplayActivity) containerActivity).cancelTransference(getFile()); - } else if (itemId == R.id.action_download_file || itemId == R.id.action_sync_file) { + } else if (itemId == R.id.action_download_file || itemId == R.id.action_sync_file || itemId == R.id.action_sync_all_files) { if (containerActivity instanceof FileActivity activity) { activity.showSyncLoadingDialog(getFile().isFolder()); } - containerActivity.getFileOperationsHelper().syncFile(getFile()); + boolean syncAll = (itemId == R.id.action_sync_all_files); + containerActivity.getFileOperationsHelper().syncFile(getFile(), syncAll); } else if (itemId == R.id.action_export_file) { ArrayList list = new ArrayList<>(); list.add(getFile()); 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 177b994e9168..180dfdefa47f 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 @@ -1418,6 +1418,9 @@ public boolean onFileActionChosen(@IdRes final int itemId, Set checkedFi } else if (itemId == R.id.action_retry) { backgroundJobManager.startOfflineOperations(); return true; + } else if (itemId == R.id.action_sync_all_files) { + syncFolderIncludingAllNestedFiles(singleFile); + return true; } } @@ -2200,42 +2203,53 @@ private boolean isSearchEventSet(SearchEvent event) { searchType == SearchRemoteOperation.SearchType.RECENTLY_MODIFIED_SEARCH; } - private void syncAndCheckFiles(Collection files) { - boolean isAnyFileFolder = false; - for (OCFile file: files) { - if (file.isFolder()) { - isAnyFileFolder = true; - break; - } + private void syncFolderIncludingAllNestedFiles(OCFile folder) { + if (FileStorageUtils.checkIfEnoughSpace(folder)) { + mContainerActivity.getFileOperationsHelper().syncFile(folder, false, true); + } else { + SyncFileNotEnoughSpaceDialogFragment + .newInstance(folder, FileOperationsHelper.getAvailableSpaceOnDevice()) + .show(getParentFragmentManager(), ConfirmationDialogFragment.FTAG_CONFIRMATION); } + } - if (mContainerActivity instanceof FileActivity activity && !files.isEmpty()) { - activity.showSyncLoadingDialog(isAnyFileFolder); - } + private void syncAndCheckFiles(Collection files) { + if (files.isEmpty()) return; - Iterator iterator = files.iterator(); - while (iterator.hasNext()) { - OCFile file = iterator.next(); + boolean hasFolder = files.stream().anyMatch(OCFile::isFolder); - long availableSpaceOnDevice = FileOperationsHelper.getAvailableSpaceOnDevice(); + if (mContainerActivity instanceof FileActivity activity) { + activity.showSyncLoadingDialog(hasFolder); + } + + List fileList = new ArrayList<>(files); + for (int i = 0; i < fileList.size(); i++) { + OCFile file = fileList.get(i); + boolean isLast = i == fileList.size() - 1; if (FileStorageUtils.checkIfEnoughSpace(file)) { - boolean isLastItem = !iterator.hasNext(); - mContainerActivity.getFileOperationsHelper().syncFile(file, isLastItem); + mContainerActivity.getFileOperationsHelper().syncFile(file, isLast, false); } else { - showSpaceErrorDialog(file, availableSpaceOnDevice); + showSpaceErrorDialog(file, FileOperationsHelper.getAvailableSpaceOnDevice()); } } } private void showSpaceErrorDialog(OCFile file, long availableSpaceOnDevice) { - SyncFileNotEnoughSpaceDialogFragment dialog = - SyncFileNotEnoughSpaceDialogFragment.newInstance(file, availableSpaceOnDevice); - dialog.setTargetFragment(this, NOT_ENOUGH_SPACE_FRAG_REQUEST_CODE); + getParentFragmentManager().setFragmentResultListener( + SyncFileNotEnoughSpaceDialogFragment.REQUEST_KEY, + getViewLifecycleOwner(), + (requestKey, result) -> { + OCFile resultFile = result.getParcelable(SyncFileNotEnoughSpaceDialogFragment.RESULT_FILE); + String action = result.getString(SyncFileNotEnoughSpaceDialogFragment.RESULT_ACTION); + if (SyncFileNotEnoughSpaceDialogFragment.ACTION_CHOOSE.equals(action) && resultFile != null) { + this.onItemClicked(resultFile); + } + }); - if (getFragmentManager() != null) { - dialog.show(getFragmentManager(), ConfirmationDialogFragment.FTAG_CONFIRMATION); - } + SyncFileNotEnoughSpaceDialogFragment + .newInstance(file, availableSpaceOnDevice) + .show(getParentFragmentManager(), ConfirmationDialogFragment.FTAG_CONFIRMATION); } @Override diff --git a/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java b/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java index e5a3824db421..e526a17f05df 100755 --- a/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java +++ b/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java @@ -873,9 +873,9 @@ public void setPictureAs(OCFile file, View view) { * * @param file The file or folder to synchronize */ - public void syncFile(OCFile file) { + public void syncFile(OCFile file, boolean syncAll) { if (file.isFolder()) { - Intent intent = getSyncFolderIntent(file); + Intent intent = getSyncFolderIntent(file, syncAll); fileActivity.startService(intent); } else { Intent intent = getSyncFileIntent(file); @@ -883,11 +883,12 @@ public void syncFile(OCFile file) { } } - private Intent getSyncFolderIntent(ServerFileInterface file) { + private Intent getSyncFolderIntent(ServerFileInterface file, boolean syncAll) { Intent intent = new Intent(fileActivity, OperationsService.class); intent.setAction(OperationsService.ACTION_SYNC_FOLDER); intent.putExtra(OperationsService.EXTRA_ACCOUNT, fileActivity.getAccount()); intent.putExtra(OperationsService.EXTRA_REMOTE_PATH, file.getRemotePath()); + intent.putExtra(OperationsService.EXTRA_SYNC_ALL, syncAll); return intent; } @@ -901,9 +902,9 @@ private Intent getSyncFileIntent(ServerFileInterface file) { } - public void syncFile(OCFile file, boolean postDialogEvent) { + public void syncFile(OCFile file, boolean postDialogEvent, boolean syncAll) { if (file.isFolder()) { - Intent intent = getSyncFolderIntent(file); + Intent intent = getSyncFolderIntent(file, syncAll); fileActivity.startService(intent); } else { Intent intent = getSyncFileIntent(file); diff --git a/app/src/main/java/com/owncloud/android/ui/preview/PreviewImageFragment.kt b/app/src/main/java/com/owncloud/android/ui/preview/PreviewImageFragment.kt index 3fd3acc3874e..d22bba3aaa3e 100644 --- a/app/src/main/java/com/owncloud/android/ui/preview/PreviewImageFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/preview/PreviewImageFragment.kt @@ -415,7 +415,7 @@ class PreviewImageFragment : val activity = containerActivity as FileActivity activity.showSyncLoadingDialog(file.isFolder) } - containerActivity.fileOperationsHelper.syncFile(file) + containerActivity.fileOperationsHelper.syncFile(file, false) } else if (itemId == R.id.action_cancel_sync) { containerActivity.fileOperationsHelper.cancelTransference(file) } else if (itemId == R.id.action_set_as_wallpaper) { diff --git a/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaActivity.kt b/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaActivity.kt index abc808e6f1eb..bc86e342f159 100644 --- a/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaActivity.kt @@ -584,7 +584,7 @@ class PreviewMediaActivity : R.id.action_sync_file -> { showSyncLoadingDialog(file?.isFolder == true) - fileOperationsHelper.syncFile(file) + fileOperationsHelper.syncFile(file, false) } R.id.action_cancel_sync -> { diff --git a/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaFragment.kt b/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaFragment.kt index 5a19c90b77c3..176940617460 100644 --- a/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaFragment.kt @@ -393,7 +393,7 @@ class PreviewMediaFragment : R.id.action_sync_file -> { getTypedActivity(FileActivity::class.java)?.showSyncLoadingDialog(file.isFolder) - containerActivity.fileOperationsHelper.syncFile(file) + containerActivity.fileOperationsHelper.syncFile(file, false) } R.id.action_cancel_sync -> { diff --git a/app/src/main/java/com/owncloud/android/ui/preview/PreviewTextFileFragment.java b/app/src/main/java/com/owncloud/android/ui/preview/PreviewTextFileFragment.java index 09ac5d38b681..ace040ce7d4a 100644 --- a/app/src/main/java/com/owncloud/android/ui/preview/PreviewTextFileFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/preview/PreviewTextFileFragment.java @@ -306,7 +306,7 @@ private void onFileActionChosen(final int itemId) { if (containerActivity instanceof FileActivity activity) { activity.showSyncLoadingDialog(getFile().isFolder()); } - containerActivity.getFileOperationsHelper().syncFile(getFile()); + containerActivity.getFileOperationsHelper().syncFile(getFile(), false); } else if(itemId == R.id.action_cancel_sync){ containerActivity.getFileOperationsHelper().cancelTransference(getFile()); } else if (itemId == R.id.action_edit) { diff --git a/app/src/main/res/drawable/ic_sync_all.xml b/app/src/main/res/drawable/ic_sync_all.xml new file mode 100644 index 000000000000..aa76d4079aa4 --- /dev/null +++ b/app/src/main/res/drawable/ic_sync_all.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml index 7093928147b7..34075e4237cf 100644 --- a/app/src/main/res/values/ids.xml +++ b/app/src/main/res/values/ids.xml @@ -24,6 +24,7 @@ + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 934d325fbaa3..0fb0c035b358 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -236,6 +236,7 @@ Upload failed. No internet connection Download Sync + Sync all File renamed %1$s during upload Listed layout Send/share From 3ef833e2dfe5ef7d66f44b807e0521cf38745102 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 24 Mar 2026 10:26:50 +0100 Subject: [PATCH 2/7] use checkIfEnoughSpace Signed-off-by: alperozturk96 --- .../nextcloud/client/database/dao/FileDao.kt | 23 ------------------- .../folderDownload/FolderDownloadWorker.kt | 21 ++++------------- 2 files changed, 5 insertions(+), 39 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 db6faff53958..7f15f5ec3252 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 @@ -162,27 +162,4 @@ interface FileDao { @Query("DELETE FROM filelist WHERE file_owner = :fileOwner AND path = :remotePath") fun deleteFileByRemotePath(fileOwner: String, remotePath: String): Int - - @Query( - """ - WITH RECURSIVE descendants AS ( - SELECT _id, content_length, content_type - FROM filelist - WHERE _id = :folderId AND file_owner = :fileOwner - - UNION ALL - - SELECT f._id, f.content_length, f.content_type - FROM filelist f - INNER JOIN descendants d ON f.parent = d._id - WHERE f.file_owner = :fileOwner - ) - SELECT COALESCE(SUM(content_length), 0) - FROM descendants - WHERE content_type IS NOT NULL - AND content_type != '${MimeType.DIRECTORY}' - AND content_type != '${MimeType.WEBDAV_FOLDER}' - """ - ) - suspend fun getTotalFolderSize(folderId: Long, fileOwner: String): Long } diff --git a/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorker.kt index ada4b999d27e..a47e0b8c2c6f 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorker.kt @@ -20,7 +20,7 @@ import com.owncloud.android.lib.common.OwnCloudClientManagerFactory import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.operations.DownloadFileOperation import com.owncloud.android.operations.DownloadType -import com.owncloud.android.ui.helpers.FileOperationsHelper +import com.owncloud.android.utils.FileStorageUtils import com.owncloud.android.utils.theme.ViewThemeUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -81,8 +81,8 @@ class FolderDownloadWorker( if (syncAll) { Log_OC.d(TAG, "checking folder size including all nested subfolders") - val folderSize = storageManager.fileDao.getTotalFolderSize(folderID, accountName) - if (!checkDiskSize(folderSize)) { + if (!FileStorageUtils.checkIfEnoughSpace(folder)) { + notificationManager.showNotAvailableDiskSpace() return Result.failure() } } @@ -104,7 +104,8 @@ class FolderDownloadWorker( var result = true files.forEachIndexed { index, file -> - if (!checkDiskSize(file.fileLength)) { + if (!FileStorageUtils.checkIfEnoughSpace(folder)) { + notificationManager.showNotAvailableDiskSpace() return@withContext Result.failure() } @@ -203,16 +204,4 @@ class FolderDownloadWorker( storageManager.getFolderContent(folder, false) .filter { !it.isFolder && !it.isDown } } - - private fun checkDiskSize(fileSizeInByte: Long): Boolean { - val availableDiskSpace = FileOperationsHelper.getAvailableSpaceOnDevice() - - return if (availableDiskSpace < fileSizeInByte) { - Log_OC.w(TAG, "available disk space is not sufficient") - notificationManager.showNotAvailableDiskSpace() - false - } else { - true - } - } } From 226aa2f7fd0c1600fd5fdbb61bc70e903c297cf9 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 24 Mar 2026 11:02:13 +0100 Subject: [PATCH 3/7] simplify sync file calls Signed-off-by: alperozturk96 --- .../ui/activity/FileDisplayActivity.kt | 2 +- .../ui/fragment/FileDetailFragment.java | 5 +- .../ui/fragment/OCFileListFragment.java | 5 +- .../ui/helpers/FileOperationsHelper.java | 49 +++++++------------ .../ui/preview/PreviewImageFragment.kt | 2 +- .../ui/preview/PreviewMediaActivity.kt | 2 +- .../ui/preview/PreviewMediaFragment.kt | 2 +- .../ui/preview/PreviewTextFileFragment.java | 2 +- 8 files changed, 27 insertions(+), 42 deletions(-) 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 d333e96bcc02..b14f9279110d 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 @@ -2192,7 +2192,7 @@ class FileDisplayActivity : // download new version, only if file was previously download showSyncLoadingDialog(file.isFolder) - fileOperationsHelper.syncFile(file, false) + fileOperationsHelper.syncFileOrFolder(file, false) } val parent = file?.let { storageManager.getFileById(it.parentId) } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java index 4e11613690e4..9bb219107d75 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java @@ -32,11 +32,9 @@ import com.nextcloud.client.network.ClientFactory; import com.nextcloud.client.network.ConnectivityService; import com.nextcloud.client.preferences.AppPreferences; -import com.nextcloud.model.WorkerState; import com.nextcloud.ui.fileactions.FileAction; import com.nextcloud.ui.fileactions.FileActionsBottomSheet; import com.nextcloud.utils.MenuUtils; -import com.nextcloud.utils.extensions.ActivityExtensionsKt; import com.nextcloud.utils.extensions.BundleExtensionsKt; import com.nextcloud.utils.extensions.FileExtensionsKt; import com.nextcloud.utils.mdm.MDMConfig; @@ -84,7 +82,6 @@ import androidx.core.content.res.ResourcesCompat; import androidx.fragment.app.FragmentManager; import androidx.viewpager2.widget.ViewPager2; -import kotlin.Unit; /** * This Fragment is used to display the details about a file. @@ -470,7 +467,7 @@ private void optionsItemSelected(@IdRes final int itemId) { activity.showSyncLoadingDialog(getFile().isFolder()); } boolean syncAll = (itemId == R.id.action_sync_all_files); - containerActivity.getFileOperationsHelper().syncFile(getFile(), syncAll); + containerActivity.getFileOperationsHelper().syncFileOrFolder(getFile(), syncAll); } else if (itemId == R.id.action_export_file) { ArrayList list = new ArrayList<>(); list.add(getFile()); 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 180dfdefa47f..2d9a7e0aed77 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 @@ -127,7 +127,6 @@ import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; -import java.util.Iterator; import java.util.List; import java.util.Objects; import java.util.Set; @@ -2205,7 +2204,7 @@ private boolean isSearchEventSet(SearchEvent event) { private void syncFolderIncludingAllNestedFiles(OCFile folder) { if (FileStorageUtils.checkIfEnoughSpace(folder)) { - mContainerActivity.getFileOperationsHelper().syncFile(folder, false, true); + mContainerActivity.getFileOperationsHelper().syncFileOrFolder(folder, false, true); } else { SyncFileNotEnoughSpaceDialogFragment .newInstance(folder, FileOperationsHelper.getAvailableSpaceOnDevice()) @@ -2228,7 +2227,7 @@ private void syncAndCheckFiles(Collection files) { boolean isLast = i == fileList.size() - 1; if (FileStorageUtils.checkIfEnoughSpace(file)) { - mContainerActivity.getFileOperationsHelper().syncFile(file, isLast, false); + mContainerActivity.getFileOperationsHelper().syncFileOrFolder(file, isLast, false); } else { showSpaceErrorDialog(file, FileOperationsHelper.getAvailableSpaceOnDevice()); } diff --git a/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java b/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java index e526a17f05df..028f7c2a7d33 100755 --- a/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java +++ b/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java @@ -183,7 +183,7 @@ public void startSyncForFileAndIntent(OCFile file, Intent intent) { // check if file is in conflict (this is known due to latest folder refresh) if (file.isInConflict()) { - syncFile(file, user, storageManager); + syncFileOrFolder(file, user, storageManager); EventBus.getDefault().post(new SyncEventFinished(intent)); return; @@ -212,14 +212,14 @@ public void startSyncForFileAndIntent(OCFile file, Intent intent) { // eTag changed, sync file if (result.getCode() == RemoteOperationResult.ResultCode.ETAG_CHANGED) { - syncFile(file, user, storageManager); + syncFileOrFolder(file, user, storageManager); } EventBus.getDefault().post(new SyncEventFinished(intent)); }).start(); } - private void syncFile(OCFile file, User user, FileDataStorageManager storageManager) { + private void syncFileOrFolder(OCFile file, User user, FileDataStorageManager storageManager) { fileActivity.runOnUiThread(() -> fileActivity.showLoadingDialog(fileActivity.getResources() .getString(R.string.sync_in_progress))); @@ -868,50 +868,39 @@ public void setPictureAs(OCFile file, View view) { } } - /** - * Request the synchronization of a file or folder with the OC server, including its contents. - * - * @param file The file or folder to synchronize - */ - public void syncFile(OCFile file, boolean syncAll) { + // region sync file or folder + public void syncFileOrFolder(OCFile file, boolean postDialogEvent, boolean syncAll) { if (file.isFolder()) { - Intent intent = getSyncFolderIntent(file, syncAll); - fileActivity.startService(intent); + startSyncFolderIntent(file, syncAll); } else { - Intent intent = getSyncFileIntent(file); - mWaitingForOpId = fileActivity.getOperationsServiceBinder().queueNewOperation(intent); + queueSyncFileIntent(file, postDialogEvent); } } - private Intent getSyncFolderIntent(ServerFileInterface file, boolean syncAll) { - Intent intent = new Intent(fileActivity, OperationsService.class); + public void syncFileOrFolder(OCFile file, boolean syncAll) { + syncFileOrFolder(file, false, syncAll); + } + + private void startSyncFolderIntent(ServerFileInterface file, boolean syncAll) { + final var intent = new Intent(fileActivity, OperationsService.class); intent.setAction(OperationsService.ACTION_SYNC_FOLDER); intent.putExtra(OperationsService.EXTRA_ACCOUNT, fileActivity.getAccount()); intent.putExtra(OperationsService.EXTRA_REMOTE_PATH, file.getRemotePath()); intent.putExtra(OperationsService.EXTRA_SYNC_ALL, syncAll); - return intent; + fileActivity.startService(intent); } - private Intent getSyncFileIntent(ServerFileInterface file) { - Intent intent = new Intent(fileActivity, OperationsService.class); + private void queueSyncFileIntent(ServerFileInterface file, boolean postDialog) { + final var intent = new Intent(fileActivity, OperationsService.class); intent.setAction(OperationsService.ACTION_SYNC_FILE); intent.putExtra(OperationsService.EXTRA_ACCOUNT, fileActivity.getAccount()); intent.putExtra(OperationsService.EXTRA_REMOTE_PATH, file.getRemotePath()); intent.putExtra(OperationsService.EXTRA_SYNC_FILE_CONTENTS, true); - return intent; - } - + intent.putExtra(OperationsService.EXTRA_POST_DIALOG_EVENT, postDialog); - public void syncFile(OCFile file, boolean postDialogEvent, boolean syncAll) { - if (file.isFolder()) { - Intent intent = getSyncFolderIntent(file, syncAll); - fileActivity.startService(intent); - } else { - Intent intent = getSyncFileIntent(file); - intent.putExtra(OperationsService.EXTRA_POST_DIALOG_EVENT, postDialogEvent); - mWaitingForOpId = fileActivity.getOperationsServiceBinder().queueNewOperation(intent); - } + mWaitingForOpId = fileActivity.getOperationsServiceBinder().queueNewOperation(intent); } + // endregion public void toggleFavoriteFiles(Collection files, boolean shouldBeFavorite) { List toToggle = new ArrayList<>(); diff --git a/app/src/main/java/com/owncloud/android/ui/preview/PreviewImageFragment.kt b/app/src/main/java/com/owncloud/android/ui/preview/PreviewImageFragment.kt index d22bba3aaa3e..927cf47c4c91 100644 --- a/app/src/main/java/com/owncloud/android/ui/preview/PreviewImageFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/preview/PreviewImageFragment.kt @@ -415,7 +415,7 @@ class PreviewImageFragment : val activity = containerActivity as FileActivity activity.showSyncLoadingDialog(file.isFolder) } - containerActivity.fileOperationsHelper.syncFile(file, false) + containerActivity.fileOperationsHelper.syncFileOrFolder(file, false) } else if (itemId == R.id.action_cancel_sync) { containerActivity.fileOperationsHelper.cancelTransference(file) } else if (itemId == R.id.action_set_as_wallpaper) { diff --git a/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaActivity.kt b/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaActivity.kt index bc86e342f159..cb3888d8aac3 100644 --- a/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaActivity.kt @@ -584,7 +584,7 @@ class PreviewMediaActivity : R.id.action_sync_file -> { showSyncLoadingDialog(file?.isFolder == true) - fileOperationsHelper.syncFile(file, false) + fileOperationsHelper.syncFileOrFolder(file, false) } R.id.action_cancel_sync -> { diff --git a/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaFragment.kt b/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaFragment.kt index 176940617460..5d8fdb5e6970 100644 --- a/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaFragment.kt @@ -393,7 +393,7 @@ class PreviewMediaFragment : R.id.action_sync_file -> { getTypedActivity(FileActivity::class.java)?.showSyncLoadingDialog(file.isFolder) - containerActivity.fileOperationsHelper.syncFile(file, false) + containerActivity.fileOperationsHelper.syncFileOrFolder(file, false) } R.id.action_cancel_sync -> { diff --git a/app/src/main/java/com/owncloud/android/ui/preview/PreviewTextFileFragment.java b/app/src/main/java/com/owncloud/android/ui/preview/PreviewTextFileFragment.java index ace040ce7d4a..086b286bc7e6 100644 --- a/app/src/main/java/com/owncloud/android/ui/preview/PreviewTextFileFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/preview/PreviewTextFileFragment.java @@ -306,7 +306,7 @@ private void onFileActionChosen(final int itemId) { if (containerActivity instanceof FileActivity activity) { activity.showSyncLoadingDialog(getFile().isFolder()); } - containerActivity.getFileOperationsHelper().syncFile(getFile(), false); + containerActivity.getFileOperationsHelper().syncFileOrFolder(getFile(), false); } else if(itemId == R.id.action_cancel_sync){ containerActivity.getFileOperationsHelper().cancelTransference(getFile()); } else if (itemId == R.id.action_edit) { From cf32dadd1864d10f380014c3f4b6dd16dc84e355 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 24 Mar 2026 11:06:57 +0100 Subject: [PATCH 4/7] simplify sync file calls Signed-off-by: alperozturk96 --- .../owncloud/android/ui/activity/FileDisplayActivity.kt | 2 +- .../owncloud/android/ui/fragment/FileDetailFragment.java | 5 ++--- .../owncloud/android/ui/fragment/OCFileListFragment.java | 2 +- .../android/ui/helpers/FileOperationsHelper.java | 9 +++++++-- .../owncloud/android/ui/preview/PreviewImageFragment.kt | 2 +- .../owncloud/android/ui/preview/PreviewMediaActivity.kt | 2 +- .../owncloud/android/ui/preview/PreviewMediaFragment.kt | 2 +- .../android/ui/preview/PreviewTextFileFragment.java | 2 +- 8 files changed, 15 insertions(+), 11 deletions(-) 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 b14f9279110d..f9d5e5f0d450 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 @@ -2192,7 +2192,7 @@ class FileDisplayActivity : // download new version, only if file was previously download showSyncLoadingDialog(file.isFolder) - fileOperationsHelper.syncFileOrFolder(file, false) + fileOperationsHelper.syncFileOrFolder(file) } val parent = file?.let { storageManager.getFileById(it.parentId) } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java index 9bb219107d75..e7f52eccf399 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java @@ -462,12 +462,11 @@ private void optionsItemSelected(@IdRes final int itemId) { dialog.show(getFragmentManager(), FTAG_RENAME_FILE); } else if (itemId == R.id.action_cancel_sync) { ((FileDisplayActivity) containerActivity).cancelTransference(getFile()); - } else if (itemId == R.id.action_download_file || itemId == R.id.action_sync_file || itemId == R.id.action_sync_all_files) { + } else if (itemId == R.id.action_download_file || itemId == R.id.action_sync_file) { if (containerActivity instanceof FileActivity activity) { activity.showSyncLoadingDialog(getFile().isFolder()); } - boolean syncAll = (itemId == R.id.action_sync_all_files); - containerActivity.getFileOperationsHelper().syncFileOrFolder(getFile(), syncAll); + containerActivity.getFileOperationsHelper().syncFileOrFolder(getFile()); } else if (itemId == R.id.action_export_file) { ArrayList list = new ArrayList<>(); list.add(getFile()); 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 2d9a7e0aed77..22bde8125fac 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 @@ -2204,7 +2204,7 @@ private boolean isSearchEventSet(SearchEvent event) { private void syncFolderIncludingAllNestedFiles(OCFile folder) { if (FileStorageUtils.checkIfEnoughSpace(folder)) { - mContainerActivity.getFileOperationsHelper().syncFileOrFolder(folder, false, true); + mContainerActivity.getFileOperationsHelper().syncFolderIncludingNestedFiles(folder); } else { SyncFileNotEnoughSpaceDialogFragment .newInstance(folder, FileOperationsHelper.getAvailableSpaceOnDevice()) diff --git a/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java b/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java index 028f7c2a7d33..7e532592ab25 100755 --- a/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java +++ b/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java @@ -877,8 +877,12 @@ public void syncFileOrFolder(OCFile file, boolean postDialogEvent, boolean syncA } } - public void syncFileOrFolder(OCFile file, boolean syncAll) { - syncFileOrFolder(file, false, syncAll); + public void syncFileOrFolder(OCFile file) { + syncFileOrFolder(file, false, false); + } + + public void syncFolderIncludingNestedFiles(OCFile file) { + syncFileOrFolder(file, false, true); } private void startSyncFolderIntent(ServerFileInterface file, boolean syncAll) { @@ -887,6 +891,7 @@ private void startSyncFolderIntent(ServerFileInterface file, boolean syncAll) { intent.putExtra(OperationsService.EXTRA_ACCOUNT, fileActivity.getAccount()); intent.putExtra(OperationsService.EXTRA_REMOTE_PATH, file.getRemotePath()); intent.putExtra(OperationsService.EXTRA_SYNC_ALL, syncAll); + fileActivity.startService(intent); } diff --git a/app/src/main/java/com/owncloud/android/ui/preview/PreviewImageFragment.kt b/app/src/main/java/com/owncloud/android/ui/preview/PreviewImageFragment.kt index 927cf47c4c91..128c5fc0a0cc 100644 --- a/app/src/main/java/com/owncloud/android/ui/preview/PreviewImageFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/preview/PreviewImageFragment.kt @@ -415,7 +415,7 @@ class PreviewImageFragment : val activity = containerActivity as FileActivity activity.showSyncLoadingDialog(file.isFolder) } - containerActivity.fileOperationsHelper.syncFileOrFolder(file, false) + containerActivity.fileOperationsHelper.syncFileOrFolder(file) } else if (itemId == R.id.action_cancel_sync) { containerActivity.fileOperationsHelper.cancelTransference(file) } else if (itemId == R.id.action_set_as_wallpaper) { diff --git a/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaActivity.kt b/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaActivity.kt index cb3888d8aac3..473d76b1a5fc 100644 --- a/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaActivity.kt @@ -584,7 +584,7 @@ class PreviewMediaActivity : R.id.action_sync_file -> { showSyncLoadingDialog(file?.isFolder == true) - fileOperationsHelper.syncFileOrFolder(file, false) + fileOperationsHelper.syncFileOrFolder(file) } R.id.action_cancel_sync -> { diff --git a/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaFragment.kt b/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaFragment.kt index 5d8fdb5e6970..61df8440eeff 100644 --- a/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaFragment.kt @@ -393,7 +393,7 @@ class PreviewMediaFragment : R.id.action_sync_file -> { getTypedActivity(FileActivity::class.java)?.showSyncLoadingDialog(file.isFolder) - containerActivity.fileOperationsHelper.syncFileOrFolder(file, false) + containerActivity.fileOperationsHelper.syncFileOrFolder(file) } R.id.action_cancel_sync -> { diff --git a/app/src/main/java/com/owncloud/android/ui/preview/PreviewTextFileFragment.java b/app/src/main/java/com/owncloud/android/ui/preview/PreviewTextFileFragment.java index 086b286bc7e6..0b32cc0a9a92 100644 --- a/app/src/main/java/com/owncloud/android/ui/preview/PreviewTextFileFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/preview/PreviewTextFileFragment.java @@ -306,7 +306,7 @@ private void onFileActionChosen(final int itemId) { if (containerActivity instanceof FileActivity activity) { activity.showSyncLoadingDialog(getFile().isFolder()); } - containerActivity.getFileOperationsHelper().syncFileOrFolder(getFile(), false); + containerActivity.getFileOperationsHelper().syncFileOrFolder(getFile()); } else if(itemId == R.id.action_cancel_sync){ containerActivity.getFileOperationsHelper().cancelTransference(getFile()); } else if (itemId == R.id.action_edit) { From 8d2b543c2668281521a4c0fd69b6b0804952d68d Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 24 Mar 2026 11:15:58 +0100 Subject: [PATCH 5/7] add confirmation dialog for sync all Signed-off-by: alperozturk96 --- .../ui/fragment/OCFileListFragment.java | 32 ++++++++++++++++++- app/src/main/res/values/strings.xml | 4 +++ 2 files changed, 35 insertions(+), 1 deletion(-) 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 22bde8125fac..16bfa6430dd2 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 @@ -2204,7 +2204,7 @@ private boolean isSearchEventSet(SearchEvent event) { private void syncFolderIncludingAllNestedFiles(OCFile folder) { if (FileStorageUtils.checkIfEnoughSpace(folder)) { - mContainerActivity.getFileOperationsHelper().syncFolderIncludingNestedFiles(folder); + informUserForSyncAllAction(folder); } else { SyncFileNotEnoughSpaceDialogFragment .newInstance(folder, FileOperationsHelper.getAvailableSpaceOnDevice()) @@ -2212,6 +2212,36 @@ private void syncFolderIncludingAllNestedFiles(OCFile folder) { } } + private void informUserForSyncAllAction(OCFile folder) { + ConfirmationDialogFragment dialog = ConfirmationDialogFragment.newInstance( + R.string.sync_all_action_dialog_description, + null, + R.string.sync_all_action_dialog_title, + R.drawable.ic_sync_all, + R.string.common_ok, + R.string.common_cancel, + -1); + + dialog.setCancelable(false); + + dialog.setOnConfirmationListener(new ConfirmationDialogFragment.ConfirmationDialogFragmentListener() { + @Override + public void onConfirmation(String callerTag) { + mContainerActivity.getFileOperationsHelper().syncFolderIncludingNestedFiles(folder); + } + + @Override + public void onNeutral(String callerTag) { + } + + @Override + public void onCancel(String callerTag) { + } + }); + + dialog.show(getParentFragmentManager(), ConfirmationDialogFragment.FTAG_CONFIRMATION); + } + private void syncAndCheckFiles(Collection files) { if (files.isEmpty()) return; diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0fb0c035b358..00c3ecaf2ead 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -327,6 +327,10 @@ Choose account Switch account Switch to account + Download entire folder? + + All files and subfolders will be downloaded to your device. This may use significant storage space and take some time depending on your connection. + Sync failed Sync failed, log in again Could not sync %1$s From 2c6104fac4cc281c11ca009a95f8b45822c2289e Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 24 Mar 2026 11:45:21 +0100 Subject: [PATCH 6/7] save files that not exists in db before sync all folder Signed-off-by: alperozturk96 --- .../folderDownload/FolderDownloadWorker.kt | 149 ++++++++++++------ ...FolderDownloadWorkerNotificationManager.kt | 8 +- 2 files changed, 110 insertions(+), 47 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorker.kt index a47e0b8c2c6f..f9c97667d0b3 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorker.kt @@ -12,17 +12,21 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.work.CoroutineWorker import androidx.work.ForegroundInfo import androidx.work.WorkerParameters +import com.nextcloud.client.account.User import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.jobs.download.FileDownloadHelper import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.common.OwnCloudClient import com.owncloud.android.lib.common.OwnCloudClientManagerFactory import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.operations.DownloadFileOperation import com.owncloud.android.operations.DownloadType +import com.owncloud.android.operations.RefreshFolderOperation import com.owncloud.android.utils.FileStorageUtils import com.owncloud.android.utils.theme.ViewThemeUtils import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.withContext import java.util.concurrent.ConcurrentHashMap @@ -40,9 +44,7 @@ class FolderDownloadWorker( const val FOLDER_ID = "FOLDER_ID" const val ACCOUNT_NAME = "ACCOUNT_NAME" const val SYNC_ALL = "SYNC_ALL" - private val pendingDownloads: MutableSet = ConcurrentHashMap.newKeySet() - fun isDownloading(id: Long): Boolean = pendingDownloads.contains(id) } @@ -59,38 +61,29 @@ class FolderDownloadWorker( val accountName = inputData.getString(ACCOUNT_NAME) if (accountName == null) { - Log_OC.e(TAG, "failed accountName cannot be null") + Log_OC.e(TAG, "failed: accountName cannot be null") return Result.failure() } val optionalUser = accountManager.getUser(accountName) if (optionalUser.isEmpty) { - Log_OC.e(TAG, "failed user is not present") + Log_OC.e(TAG, "failed: user is not present") return Result.failure() } val syncAll = inputData.getBoolean(SYNC_ALL, false) - val user = optionalUser.get() storageManager = FileDataStorageManager(user, context.contentResolver) + val folder = storageManager.getFileById(folderID) if (folder == null) { - Log_OC.e(TAG, "failed folder cannot be nul") + Log_OC.e(TAG, "failed: folder cannot be null") return Result.failure() } - if (syncAll) { - Log_OC.d(TAG, "checking folder size including all nested subfolders") - if (!FileStorageUtils.checkIfEnoughSpace(folder)) { - notificationManager.showNotAvailableDiskSpace() - return Result.failure() - } - } - - Log_OC.d(TAG, "πŸ•’ started for ${user.accountName} downloading ${folder.fileName}") + Log_OC.d(TAG, "πŸ•’ started for ${user.accountName} | folder=${folder.fileName} | syncAll=$syncAll") trySetForeground(folder) - folderDownloadEventBroadcaster.sendDownloadEnqueued(folder.fileId) pendingDownloads.add(folder.fileId) @@ -98,12 +91,35 @@ class FolderDownloadWorker( return withContext(Dispatchers.IO) { try { - val files = getFiles(folder, storageManager, syncAll) val account = user.toOwnCloudAccount() - val client = OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(account, context) + val client = OwnCloudClientManagerFactory.getDefaultSingleton() + .getClientFor(account, context) + + if (syncAll) { + Log_OC.d(TAG, "checking available disk space for full recursive download") + if (!FileStorageUtils.checkIfEnoughSpace(folder)) { + notificationManager.showNotAvailableDiskSpace() + return@withContext Result.failure() + } + Log_OC.d(TAG, "πŸ”„ syncing full folder tree from server before collecting files") + syncFolderRecursivelyFromServer(folder, user, client) + } + + val files = getFiles(folder, storageManager, syncAll) + if (files.isEmpty()) { + Log_OC.d(TAG, "βœ… no files need downloading") + notificationManager.showCompletionNotification(folder.fileName, true) + return@withContext Result.success() + } + + var overallSuccess = true - var result = true files.forEachIndexed { index, file -> + if (isStopped) { + Log_OC.d(TAG, "⚠️ worker stopped mid-download, aborting remaining files") + return@withContext Result.failure() + } + if (!FileStorageUtils.checkIfEnoughSpace(folder)) { notificationManager.showNotAvailableDiskSpace() return@withContext Result.failure() @@ -117,46 +133,92 @@ class FolderDownloadWorker( files.size ) notificationManager.showNotification(notification) - - val foregroundInfo = notificationManager.getForegroundInfo(notification) - setForeground(foregroundInfo) + setForeground(notificationManager.getForegroundInfo(notification)) } val operation = DownloadFileOperation(user, file, context) val operationResult = operation.execute(client) + if (operationResult?.isSuccess == true && operation.downloadType === DownloadType.DOWNLOAD) { getOCFile(operation)?.let { ocFile -> downloadHelper.saveFile(ocFile, operation, storageManager) } } - if (!operationResult.isSuccess) { - result = false + if (operationResult?.isSuccess != true) { + Log_OC.w(TAG, "⚠️ download failed for ${file.remotePath}: ${operationResult?.logMessage}") + overallSuccess = false } } - withContext(Dispatchers.Main) { - notificationManager.showCompletionNotification(folder.fileName, result) - } + notificationManager.showCompletionNotification(folder.fileName, overallSuccess) - if (result) { - Log_OC.d(TAG, "βœ… completed") + if (overallSuccess) { + Log_OC.d(TAG, "βœ… completed successfully") Result.success() } else { - Log_OC.d(TAG, "❌ failed") + Log_OC.d(TAG, "❌ completed with failures") Result.failure() } } catch (e: Exception) { - Log_OC.d(TAG, "❌ failed reason: $e") + Log_OC.e(TAG, "❌ unexpected failure: $e") + notificationManager.showCompletionNotification(folder.fileName, false) Result.failure() } finally { folderDownloadEventBroadcaster.sendDownloadCompleted(folder.fileId) pendingDownloads.remove(folder.fileId) + + // delay so that user can see the error or success notification + delay(2000) notificationManager.dismiss() } } } + private fun syncFolderRecursivelyFromServer(folder: OCFile, user: User, client: OwnCloudClient) { + if (isStopped) return + + try { + Log_OC.d(TAG, "πŸ”„ refreshing from server: ${folder.remotePath}") + + val operation = RefreshFolderOperation( + folder, + System.currentTimeMillis(), + false, + true, + false, + storageManager, + user, + context + ) + + val result = operation.execute(client) + + if (!result.isSuccess) { + Log_OC.w(TAG, "⚠️ failed to refresh ${folder.remotePath}: ${result.logMessage}") + return + } + + val refreshedFolder = storageManager.getFileById(folder.fileId) ?: run { + Log_OC.w(TAG, "⚠️ folder ${folder.fileId} missing from DB after refresh") + return + } + + val subFolders = storageManager + .getFolderContent(refreshedFolder, false) + .filter { it.isFolder } + + for (subFolder in subFolders) { + if (isStopped) return + syncFolderRecursivelyFromServer(subFolder, user, client) + } + } catch (e: Exception) { + Log_OC.w(TAG, "⚠️ exception syncing ${folder.remotePath}: $e") + } finally { + Log_OC.d(TAG, "sub folders are fetched before downloading all") + } + } + @Suppress("ReturnCount") override suspend fun getForegroundInfo(): ForegroundInfo { return try { @@ -167,11 +229,12 @@ class FolderDownloadWorker( return notificationManager.getForegroundInfo(null) } - val folder = storageManager.getFileById(folderID) ?: return notificationManager.getForegroundInfo(null) + val folder = storageManager.getFileById(folderID) + ?: return notificationManager.getForegroundInfo(null) - return notificationManager.getForegroundInfo(folder) + notificationManager.getForegroundInfo(folder) } catch (e: Exception) { - Log_OC.w(TAG, "⚠️ Error getting foreground info: ${e.message}") + Log_OC.w(TAG, "⚠️ error getting foreground info: ${e.message}") notificationManager.getForegroundInfo(null) } } @@ -181,20 +244,18 @@ class FolderDownloadWorker( val foregroundInfo = notificationManager.getForegroundInfo(folder) setForeground(foregroundInfo) } catch (e: Exception) { - Log_OC.w(TAG, "⚠️ Could not set foreground service: ${e.message}") + Log_OC.w(TAG, "⚠️ could not set foreground service: ${e.message}") } } - private fun getOCFile(operation: DownloadFileOperation): OCFile? { - val file = operation.file?.fileId?.let { storageManager.getFileById(it) } - ?: storageManager.getFileByDecryptedRemotePath(operation.file?.remotePath) - ?: run { - Log_OC.e(TAG, "could not save ${operation.file?.remotePath}") - return null - } - - return file + private fun getOCFile(operation: DownloadFileOperation): OCFile? = operation.file?.fileId?.let { + storageManager.getFileById(it) } + ?: storageManager.getFileByDecryptedRemotePath(operation.file?.remotePath) + ?: run { + Log_OC.e(TAG, "could not resolve OCFile for save: ${operation.file?.remotePath}") + null + } private fun getFiles(folder: OCFile, storageManager: FileDataStorageManager, syncAll: Boolean): List = if (syncAll) { diff --git a/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorkerNotificationManager.kt b/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorkerNotificationManager.kt index fed0b8feaff0..e227142c1976 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorkerNotificationManager.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorkerNotificationManager.kt @@ -17,6 +17,8 @@ import com.owncloud.android.R import com.owncloud.android.datamodel.OCFile import com.owncloud.android.ui.notifications.NotificationUtils import com.owncloud.android.utils.theme.ViewThemeUtils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import kotlin.random.Random class FolderDownloadWorkerNotificationManager(private val context: Context, viewThemeUtils: ViewThemeUtils) : @@ -78,7 +80,7 @@ class FolderDownloadWorkerNotificationManager(private val context: Context, view return getNotification(folderName, description, progress) } - fun showCompletionNotification(folderName: String, success: Boolean) { + suspend fun showCompletionNotification(folderName: String, success: Boolean) = withContext(Dispatchers.Main) { val titleId = if (success) { R.string.folder_download_success_notification_title } else { @@ -91,10 +93,10 @@ class FolderDownloadWorkerNotificationManager(private val context: Context, view notificationManager.notify(NOTIFICATION_ID, notification) } - fun showNotAvailableDiskSpace() { + suspend fun showNotAvailableDiskSpace() = withContext(Dispatchers.Main) { val title = context.getString(R.string.folder_download_insufficient_disk_space_notification_title) val notification = getNotification(title) - notificationManager.notify(NOTIFICATION_ID, notification) + notificationManager.notify(Random.nextInt(), notification) } fun getForegroundInfo(folder: OCFile?): ForegroundInfo { From 1dd3025ab4a803d9cd8df888d01c152193958f4a Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 24 Mar 2026 14:02:33 +0100 Subject: [PATCH 7/7] sync folder operation already handles nested files Signed-off-by: alperozturk96 --- .../folderDownload/FolderDownloadWorker.kt | 49 ------------------- .../nextcloud/ui/fileactions/FileAction.kt | 2 +- 2 files changed, 1 insertion(+), 50 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorker.kt index f9c97667d0b3..5ff4cf65a1b7 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorker.kt @@ -12,17 +12,14 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.work.CoroutineWorker import androidx.work.ForegroundInfo import androidx.work.WorkerParameters -import com.nextcloud.client.account.User import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.jobs.download.FileDownloadHelper import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.OCFile -import com.owncloud.android.lib.common.OwnCloudClient import com.owncloud.android.lib.common.OwnCloudClientManagerFactory import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.operations.DownloadFileOperation import com.owncloud.android.operations.DownloadType -import com.owncloud.android.operations.RefreshFolderOperation import com.owncloud.android.utils.FileStorageUtils import com.owncloud.android.utils.theme.ViewThemeUtils import kotlinx.coroutines.Dispatchers @@ -101,8 +98,6 @@ class FolderDownloadWorker( notificationManager.showNotAvailableDiskSpace() return@withContext Result.failure() } - Log_OC.d(TAG, "πŸ”„ syncing full folder tree from server before collecting files") - syncFolderRecursivelyFromServer(folder, user, client) } val files = getFiles(folder, storageManager, syncAll) @@ -175,50 +170,6 @@ class FolderDownloadWorker( } } - private fun syncFolderRecursivelyFromServer(folder: OCFile, user: User, client: OwnCloudClient) { - if (isStopped) return - - try { - Log_OC.d(TAG, "πŸ”„ refreshing from server: ${folder.remotePath}") - - val operation = RefreshFolderOperation( - folder, - System.currentTimeMillis(), - false, - true, - false, - storageManager, - user, - context - ) - - val result = operation.execute(client) - - if (!result.isSuccess) { - Log_OC.w(TAG, "⚠️ failed to refresh ${folder.remotePath}: ${result.logMessage}") - return - } - - val refreshedFolder = storageManager.getFileById(folder.fileId) ?: run { - Log_OC.w(TAG, "⚠️ folder ${folder.fileId} missing from DB after refresh") - return - } - - val subFolders = storageManager - .getFolderContent(refreshedFolder, false) - .filter { it.isFolder } - - for (subFolder in subFolders) { - if (isStopped) return - syncFolderRecursivelyFromServer(subFolder, user, client) - } - } catch (e: Exception) { - Log_OC.w(TAG, "⚠️ exception syncing ${folder.remotePath}: $e") - } finally { - Log_OC.d(TAG, "sub folders are fetched before downloading all") - } - } - @Suppress("ReturnCount") override suspend fun getForegroundInfo(): ForegroundInfo { return try { diff --git a/app/src/main/java/com/nextcloud/ui/fileactions/FileAction.kt b/app/src/main/java/com/nextcloud/ui/fileactions/FileAction.kt index c7d0c560df5b..a8bc18a56281 100644 --- a/app/src/main/java/com/nextcloud/ui/fileactions/FileAction.kt +++ b/app/src/main/java/com/nextcloud/ui/fileactions/FileAction.kt @@ -97,7 +97,7 @@ enum class FileAction( PIN_TO_HOMESCREEN, RETRY ).apply { - if (files.size == 1 && files.first().isFolder) { + if (files.size == 1 && files.first().isFolder && !files.first().isEncrypted) { add(DOWNLOAD_ALL_FOLDERS) }