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 0dfef42aba27..e89c436e46f1 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt @@ -169,6 +169,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, recursive: Boolean = false) 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 8a8647e53d5f..56ee87fc1837 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt @@ -17,6 +17,7 @@ import androidx.work.ExistingWorkPolicy import androidx.work.ListenableWorker import androidx.work.NetworkType import androidx.work.OneTimeWorkRequest +import androidx.work.OneTimeWorkRequestBuilder import androidx.work.Operation import androidx.work.OutOfQuotaPolicy import androidx.work.PeriodicWorkRequest @@ -795,25 +796,62 @@ 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, recursive: Boolean) { val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .setRequiresStorageNotLow(true) .build() - val data = Data.Builder() + // Prepare input data for FolderDownloadWorker + val downloadData = Data.Builder() .putLong(FolderDownloadWorker.FOLDER_ID, folder.fileId) .putString(FolderDownloadWorker.ACCOUNT_NAME, accountName) + .putBoolean(FolderDownloadWorker.RECURSIVE_DOWNLOAD, recursive) + .build() + + // Prepare input data for MetadataWorker (needs folder path to sync metadata) + // IMPORTANT: Add FORCE_REFRESH flag to ensure MetadataWorker fetches content + // regardless of eTag - this fixes the issue where FolderDownloadWorker runs + // before database is populated with file entries + val metadataData = Data.Builder() + .putString(MetadataWorker.FILE_PATH, folder.remotePath) + .putBoolean(MetadataWorker.FORCE_REFRESH, true) // Force full refresh + .build() + + val metadataConstraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .setRequiresBatteryNotLow(true) .build() - val request = oneTimeRequestBuilder(FolderDownloadWorker::class, JOB_DOWNLOAD_FOLDER) + // Create MetadataWorker request - this will sync folder metadata to database first + val metadataWork = OneTimeWorkRequestBuilder() + .addTag(TAG_ALL) + .addTag(formatNameTag(JOB_METADATA_SYNC)) + .addTag(formatTimeTag(clock.currentTime)) + .addTag(formatClassTag(MetadataWorker::class)) + .setConstraints(metadataConstraints) + .setInputData(metadataData) + .build() + + // Create FolderDownloadWorker request + val downloadWork = OneTimeWorkRequestBuilder() + .addTag(TAG_ALL) + .addTag(formatNameTag(JOB_DOWNLOAD_FOLDER)) + .addTag(formatTimeTag(clock.currentTime)) + .addTag(formatClassTag(FolderDownloadWorker::class)) .addTag(JOB_DOWNLOAD_FOLDER) - .setInputData(data) + .setInputData(downloadData) .setConstraints(constraints) .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) .build() - workManager.enqueueUniqueWork(JOB_DOWNLOAD_FOLDER, ExistingWorkPolicy.APPEND_OR_REPLACE, request) + // Use WorkChain to ensure MetadataWorker completes before FolderDownloadWorker starts + // This fixes the race condition where FolderDownloadWorker was running before + // MetadataWorker populated the database with folder contents + workManager + .beginWith(metadataWork) + .then(downloadWork) + .enqueue() } override fun cancelFolderDownload() { 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 76f4b99ae7b3..824f8c89d159 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 @@ -139,12 +139,12 @@ class FileDownloadHelper { ) } - fun downloadFolder(folder: OCFile?, accountName: String) { + fun downloadFolder(folder: OCFile?, accountName: String, recursive: Boolean = false) { if (folder == null) { Log_OC.e(TAG, "folder cannot be null, cant sync") return } - backgroundJobManager.downloadFolder(folder, accountName) + backgroundJobManager.downloadFolder(folder, accountName, recursive) } 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 337ff1bf6ee7..f32316dfd182 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,6 +39,7 @@ class FolderDownloadWorker( private const val TAG = "📂" + "FolderDownloadWorker" const val FOLDER_ID = "FOLDER_ID" const val ACCOUNT_NAME = "ACCOUNT_NAME" + const val RECURSIVE_DOWNLOAD = "RECURSIVE_DOWNLOAD" private val pendingDownloads: MutableSet = ConcurrentHashMap.newKeySet() @@ -61,6 +62,8 @@ class FolderDownloadWorker( return Result.failure() } + val recursiveDownload = inputData.getBoolean(RECURSIVE_DOWNLOAD, false) + val optionalUser = accountManager.getUser(accountName) if (optionalUser.isEmpty) { Log_OC.e(TAG, "failed user is not present") @@ -75,7 +78,7 @@ class FolderDownloadWorker( return Result.failure() } - Log_OC.d(TAG, "🕒 started for ${user.accountName} downloading ${folder.fileName}") + Log_OC.d(TAG, "🕒 started for ${user.accountName} downloading ${folder.fileName} (recursive: $recursiveDownload)") trySetForeground(folder) @@ -85,7 +88,13 @@ class FolderDownloadWorker( return withContext(Dispatchers.IO) { try { - val files = getFiles(folder, storageManager) + val files = getFiles(folder, storageManager, recursiveDownload) + + // Add warning log when no files found for recursive download + if (files.isEmpty()) { + Log_OC.w(TAG, "⚠️ No files found for recursive download in folder: ${folder.fileName}") + } + val account = user.toOwnCloudAccount() val client = OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(account, context) @@ -182,9 +191,40 @@ 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, recursive: Boolean): List { + if (recursive) { + return getAllFilesRecursive(folder, storageManager) + } + // Use folder ID to avoid fileExists() check + return storageManager.getFolderContent(folder.fileId, false) + .filter { !it.isFolder } + } + + /** + * Recursively get all files in the folder and its subfolders + */ + private fun getAllFilesRecursive(folder: OCFile, storageManager: FileDataStorageManager): List { + val result = mutableListOf() + + // Use the folder ID directly to avoid fileExists() check that fails for subfolders not yet downloaded + val folderContent = storageManager.getFolderContent(folder.fileId, false) + + Log_OC.d(TAG, "📂 getAllFilesRecursive: folder=${folder.fileName}, folderId=${folder.fileId}, contentCount=${folderContent.size}") + + for (file in folderContent) { + if (!file.isFolder) { + // Add all files, regardless of download status, to ensure subfolders are synced + result.add(file) + } else { + Log_OC.d(TAG, "📂 Found subfolder: ${file.fileName}, recursing...") + // Recursively process subfolders + result.addAll(getAllFilesRecursive(file, storageManager)) + } + } + + Log_OC.d(TAG, "📂 getAllFilesRecursive: returning ${result.size} files from folder ${folder.fileName}") + return result + } private fun checkDiskSize(file: OCFile): Boolean { val fileSizeInByte = file.fileLength diff --git a/app/src/main/java/com/nextcloud/client/jobs/metadata/MetadataWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/metadata/MetadataWorker.kt index 5221bbe3d663..1775925f0cb6 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/metadata/MetadataWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/metadata/MetadataWorker.kt @@ -18,6 +18,8 @@ import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.operations.RefreshFolderOperation import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import java.util.LinkedList +import java.util.Queue @Suppress("DEPRECATION", "ReturnCount", "TooGenericExceptionCaught") class MetadataWorker(private val context: Context, params: WorkerParameters, private val user: User) : @@ -26,6 +28,7 @@ class MetadataWorker(private val context: Context, params: WorkerParameters, pri companion object { private const val TAG = "MetadataWorker" const val FILE_PATH = "file_path" + const val FORCE_REFRESH = "force_refresh" // When true, ignore eTag and always fetch content } override suspend fun doWork(): Result { @@ -35,6 +38,10 @@ class MetadataWorker(private val context: Context, params: WorkerParameters, pri return Result.failure() } + // Check if we should force a full refresh (ignore eTag) + val forceRefresh = inputData.getBoolean(FORCE_REFRESH, false) + Log_OC.d(TAG, "📥 Force refresh mode: $forceRefresh for path: $filePath") + if (user.isAnonymous) { Log_OC.w(TAG, "user is anonymous cannot start metadata worker") return Result.failure() @@ -54,7 +61,7 @@ class MetadataWorker(private val context: Context, params: WorkerParameters, pri Log_OC.d(TAG, "🕒 Starting metadata sync for folder: $filePath, id: ${currentDir.fileId}") // First check current dir - val currentRefreshResult = refreshFolder(currentDir, storageManager) + val currentRefreshResult = refreshFolder(currentDir, storageManager, forceRefresh) if (!currentRefreshResult) { Log_OC.e(TAG, "❌ Failed to refresh current directory: $filePath") return Result.failure() @@ -67,31 +74,72 @@ class MetadataWorker(private val context: Context, params: WorkerParameters, pri return Result.failure() } - // then get up-to-date subfolders - val subfolders = storageManager.getNonEncryptedSubfolders(refreshedDir.fileId, user.accountName) - Log_OC.d(TAG, "Found ${subfolders.size} subfolders to sync") - + // IMPORTANT: Also fetch immediate files in the current folder, not just subfolders + // This ensures FolderDownloadWorker has access to files when it runs after MetadataWorker + val currentFolderFiles = storageManager.getFolderContent(refreshedDir.fileId, false) + .filter { !it.isFolder } + Log_OC.d(TAG, "Found ${currentFolderFiles.size} files in current folder: $filePath") + + // Use BFS to recursively process ALL nested subfolders + // This ensures we fetch metadata for folders at all depth levels + // (e.g., Artists/ABBA, Artists/Beatles, etc.) + val folderQueue: Queue = LinkedList() + + // Get the first level of subfolders + val initialSubfolders = storageManager.getNonEncryptedSubfolders(refreshedDir.fileId, user.accountName) + Log_OC.d(TAG, "Found ${initialSubfolders.size} top-level subfolders to sync") + folderQueue.addAll(initialSubfolders) + + var processedCount = 0 var failedCount = 0 - subfolders.forEach { subFolder -> + + // BFS: Process all folders level by level + while (folderQueue.isNotEmpty()) { + val subFolder = folderQueue.poll() ?: continue + processedCount++ + if (!subFolder.hasValidParentId()) { Log_OC.e(TAG, "❌ Skipping subfolder with invalid ID: ${subFolder.remotePath}") failedCount++ - return@forEach + continue } - val success = refreshFolder(subFolder, storageManager) + Log_OC.d(TAG, "📂 Processing folder (${processedCount}): ${subFolder.remotePath}") + + // Refresh this folder + val success = refreshFolder(subFolder, storageManager, forceRefresh) if (!success) { + Log_OC.e(TAG, "❌ Failed to refresh folder: ${subFolder.remotePath}") failedCount++ } + + // After refreshing, get this folder's subfolders and add them to the queue + // This enables recursive processing of ALL nested folders + val reloadedFolder = storageManager.getFileByPath(subFolder.remotePath) + if (reloadedFolder != null && reloadedFolder.hasValidParentId()) { + val nestedSubfolders = storageManager.getNonEncryptedSubfolders(reloadedFolder.fileId, user.accountName) + if (nestedSubfolders.isNotEmpty()) { + Log_OC.d(TAG, " └── Found ${nestedSubfolders.size} nested subfolders in: ${subFolder.remotePath}") + folderQueue.addAll(nestedSubfolders) + } + + // Also fetch files in this subfolder (not just sub-subfolders) + // This ensures FolderDownloadWorker has access to all files at every level + val subfolderFiles = storageManager.getFolderContent(reloadedFolder.fileId, false) + .filter { !it.isFolder } + if (subfolderFiles.isNotEmpty()) { + Log_OC.d(TAG, " └── Found ${subfolderFiles.size} files in: ${subFolder.remotePath}") + } + } } - Log_OC.d(TAG, "🏁 Metadata sync completed for folder: $filePath. Failed: $failedCount/${subfolders.size}") + Log_OC.d(TAG, "🏁 Metadata sync completed for folder: $filePath. Processed: $processedCount, Failed: $failedCount") return Result.success() } @Suppress("DEPRECATION") - private suspend fun refreshFolder(folder: OCFile, storageManager: FileDataStorageManager): Boolean = + private suspend fun refreshFolder(folder: OCFile, storageManager: FileDataStorageManager, forceRefresh: Boolean = false): Boolean = withContext(Dispatchers.IO) { Log_OC.d( TAG, @@ -105,11 +153,17 @@ class MetadataWorker(private val context: Context, params: WorkerParameters, pri return@withContext false } - if (!folder.isEtagChanged) { + // Skip eTag check if forceRefresh is true + if (!forceRefresh && !folder.isEtagChanged) { Log_OC.d(TAG, "Skipping ${folder.remotePath}, eTag didn't change") return@withContext true } + // If forceRefresh is true, log that we're doing a forced refresh + if (forceRefresh) { + Log_OC.d(TAG, "🔄 Forcing refresh for: ${folder.remotePath}, ignoring eTag") + } + Log_OC.d(TAG, "⏳ Fetching metadata for: ${folder.remotePath}, id: ${folder.fileId}") val operation = RefreshFolderOperation(folder, storageManager, user, context) 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..6090d43e7ccd 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_FOLDER_RECURSIVE(R.id.action_sync_file_recursive, R.string.filedetails_sync_file_recursive, R.drawable.ic_sync), CANCEL_SYNC(R.id.action_cancel_sync, R.string.common_cancel_sync, R.drawable.ic_sync_off), // File sharing @@ -136,6 +137,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_file_recursive) } if (file?.isAPKorAAB == true) { diff --git a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java index 497776f6babf..35f6b1718ac9 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java @@ -1192,7 +1192,7 @@ public void migrateStoredFiles(String sourcePath, String destinationPath) } } - private List getFolderContent(long parentId, boolean onlyOnDevice) { + public List getFolderContent(long parentId, boolean onlyOnDevice) { Log_OC.d(TAG, "getFolderContent - start"); List folderContent = new ArrayList<>(); 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 ee77d865ea70..d1595ff6de95 100644 --- a/app/src/main/java/com/owncloud/android/operations/SynchronizeFolderOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/SynchronizeFolderOperation.java @@ -488,7 +488,7 @@ private void startDirectDownloads() { Log_OC.d(TAG, "Exception caught at startDirectDownloads" + e); } } else { - fileDownloadHelper.downloadFolder(mLocalFolder, user.getAccountName()); + fileDownloadHelper.downloadFolder(mLocalFolder, user.getAccountName(), false); } } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java index 47e079c1f67e..892cbb2af392 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java @@ -30,7 +30,9 @@ import android.os.Handler; import android.os.IBinder; import android.text.TextUtils; +import android.view.View; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.snackbar.Snackbar; import com.nextcloud.client.account.User; import com.nextcloud.client.account.UserAccountManager; @@ -1036,6 +1038,53 @@ public void showSyncLoadingDialog(boolean isFolder) { showLoadingDialog(getApplicationContext().getString(R.string.wait_a_moment)); } + /** + * Show confirmation dialog for downloading all subfolders recursively + */ + public void showDownloadFolderRecursiveConfirmation(OCFile folder) { + if (folder == null || !folder.isFolder()) { + return; + } + + new com.google.android.material.dialog.MaterialAlertDialogBuilder(this) + .setTitle(R.string.filedetails_sync_file_recursive) + .setMessage(R.string.filedetails_sync_file_recursive_message) + .setPositiveButton(R.string.common_ok, (dialog, which) -> { + // Trigger the recursive download + getFileOperationsHelper().downloadFolderRecursive(folder); + }) + .setNegativeButton(R.string.common_cancel, null) + .show(); + } + + /** + * Show confirmation dialog for syncing a folder with optional subfolder sync + */ + public void showSyncFolderConfirmation(OCFile folder) { + if (folder == null || !folder.isFolder()) { + return; + } + + View checkBoxView = View.inflate(this, android.R.layout.simple_list_item_multiple_choice, null); + android.widget.CheckedTextView checkBox = checkBoxView.findViewById(android.R.id.text1); + checkBox.setText(R.string.filedetails_sync_file_include_subfolders); + checkBox.setChecked(false); + checkBox.setFocusable(true); + checkBox.setClickable(true); + checkBox.setOnClickListener(v -> checkBox.setChecked(!checkBox.isChecked())); + + new com.google.android.material.dialog.MaterialAlertDialogBuilder(this) + .setTitle(R.string.filedetails_sync_file) + .setMessage(R.string.filedetails_sync_file_message) + .setView(checkBoxView) + .setPositiveButton(R.string.common_ok, (dialog, which) -> { + // Trigger sync with or without recursive based on checkbox + getFileOperationsHelper().syncFile(folder, checkBox.isChecked()); + }) + .setNegativeButton(R.string.common_cancel, null) + .show(); + } + @Subscribe(threadMode = ThreadMode.MAIN) public void handleSyncDialogEvent(DialogEvent event) { if (event.getType() == DialogEventType.SYNC) { 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..e356b44bd7b8 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 @@ -466,10 +466,19 @@ private void optionsItemSelected(@IdRes final int itemId) { } 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) { - if (containerActivity instanceof FileActivity activity) { - activity.showSyncLoadingDialog(getFile().isFolder()); + if (getFile().isFolder() && containerActivity instanceof FileActivity activity) { + // Show confirmation dialog for folders + activity.showSyncFolderConfirmation(getFile()); + } else { + if (containerActivity instanceof FileActivity activity) { + activity.showSyncLoadingDialog(getFile().isFolder()); + } + containerActivity.getFileOperationsHelper().syncFile(getFile()); + } + } else if (itemId == R.id.action_sync_file_recursive) { + if (getFile().isFolder() && containerActivity instanceof FileActivity activity) { + activity.showDownloadFolderRecursiveConfirmation(getFile()); } - containerActivity.getFileOperationsHelper().syncFile(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 bb5a065114c4..56c8e3a7e701 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 @@ -1369,10 +1369,33 @@ public boolean onFileActionChosen(@IdRes final int itemId, Set checkedFi RemoveFilesDialogFragment.newInstance(new ArrayList<>(checkedFiles), mActiveActionMode); dialog.show(getFragmentManager(), ConfirmationDialogFragment.FTAG_CONFIRMATION); return true; - } else if (itemId == R.id.action_download_file || itemId == R.id.action_sync_file) { + } else if (itemId == R.id.action_download_file) { syncAndCheckFiles(checkedFiles); exitSelectionMode(); return true; + } else if (itemId == R.id.action_sync_file) { + // Check if a single folder is selected - show confirmation dialog for folders + if (checkedFiles.size() == 1) { + OCFile file = checkedFiles.iterator().next(); + if (file.isFolder() && mContainerActivity instanceof FileActivity activity) { + activity.showSyncFolderConfirmation(file); + exitSelectionMode(); + return true; + } + } + syncAndCheckFiles(checkedFiles); + exitSelectionMode(); + return true; + } else if (itemId == R.id.action_sync_file_recursive) { + // Handle recursive download - only for single folder selection + if (checkedFiles.size() == 1) { + OCFile folder = checkedFiles.iterator().next(); + if (folder.isFolder() && mContainerActivity instanceof FileActivity) { + ((FileActivity) mContainerActivity).showDownloadFolderRecursiveConfirmation(folder); + } + } + exitSelectionMode(); + return true; } else if (itemId == R.id.action_export_file) { mContainerActivity.getFileOperationsHelper().exportFiles(checkedFiles, getContext(), 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 b071a92a9eff..e76dd08ca843 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,24 @@ public void setPictureAs(OCFile file, View view) { * @param file The file or folder to synchronize */ public void syncFile(OCFile file) { + syncFile(file, false); + } + + /** + * Request the synchronization of a file or folder with the OC server, optionally including subfolders. + * + * @param file The file or folder to synchronize + * @param recursive If true and file is a folder, sync all subfolders recursively + */ + public void syncFile(OCFile file, boolean recursive) { if (file.isFolder()) { - Intent intent = getSyncFolderIntent(file); - fileActivity.startService(intent); + if (recursive) { + // Sync folder recursively + downloadFolderRecursive(file); + } else { + Intent intent = getSyncFolderIntent(file); + fileActivity.startService(intent); + } } else { Intent intent = getSyncFileIntent(file); mWaitingForOpId = fileActivity.getOperationsServiceBinder().queueNewOperation(intent); @@ -900,14 +915,21 @@ private Intent getSyncFileIntent(ServerFileInterface file) { } - public void syncFile(OCFile file, boolean postDialogEvent) { - if (file.isFolder()) { - Intent intent = getSyncFolderIntent(file); - fileActivity.startService(intent); - } else { - Intent intent = getSyncFileIntent(file); - intent.putExtra(OperationsService.EXTRA_POST_DIALOG_EVENT, postDialogEvent); - mWaitingForOpId = fileActivity.getOperationsServiceBinder().queueNewOperation(intent); + /** + * Download all files and subfolders from a folder recursively + * This uses the FolderDownloadWorker to perform the download + * + * @param folder The folder to download recursively + */ + public void downloadFolderRecursive(OCFile folder) { + if (folder == null || !folder.isFolder()) { + return; + } + + java.util.Optional userOpt = fileActivity.getUser(); + if (userOpt.isPresent()) { + User user = userOpt.get(); + FileDownloadHelper.Companion.instance().downloadFolder(folder, user.getAccountName(), true); } } 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 9be03392fcbc..01ce4cabf905 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 @@ -303,10 +303,15 @@ private void onFileActionChosen(final int itemId) { } else if (itemId == R.id.action_see_details) { seeDetails(); } else if (itemId == R.id.action_sync_file) { - if (containerActivity instanceof FileActivity activity) { - activity.showSyncLoadingDialog(getFile().isFolder()); + if (getFile().isFolder() && containerActivity instanceof FileActivity activity) { + // Show confirmation dialog for folders + activity.showSyncFolderConfirmation(getFile()); + } else { + if (containerActivity instanceof FileActivity activity) { + activity.showSyncLoadingDialog(getFile().isFolder()); + } + containerActivity.getFileOperationsHelper().syncFile(getFile()); } - containerActivity.getFileOperationsHelper().syncFile(getFile()); } 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/values/ids.xml b/app/src/main/res/values/ids.xml index 7093928147b7..bfc7ec0da2ba 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 3912bf80aca3..4cfe359cbaee 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -235,6 +235,10 @@ Upload failed. No internet connection Download Sync + Do you want to sync this folder? + Also sync subfolders + Download all subfolders + This will download all files and subfolders from this folder. This may use a large amount of data and storage space. Do you want to continue? File renamed %1$s during upload Listed layout Send/share