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..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 @@ -20,9 +20,10 @@ 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.delay import kotlinx.coroutines.withContext import java.util.concurrent.ConcurrentHashMap @@ -39,9 +40,8 @@ class FolderDownloadWorker( private const val TAG = "๐Ÿ“‚" + "FolderDownloadWorker" const val FOLDER_ID = "FOLDER_ID" const val ACCOUNT_NAME = "ACCOUNT_NAME" - - private val pendingDownloads: MutableSet = ConcurrentHashMap.newKeySet() - + const val SYNC_ALL = "SYNC_ALL" + private val pendingDownloads: MutableSet = ConcurrentHashMap.newKeySet() fun isDownloading(id: Long): Boolean = pendingDownloads.contains(id) } @@ -58,28 +58,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() } - 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) @@ -87,13 +88,35 @@ class FolderDownloadWorker( return withContext(Dispatchers.IO) { try { - val files = getFiles(folder, storageManager) 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() + } + } + + 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 (!checkDiskSize(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() } @@ -105,41 +128,43 @@ 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() } } @@ -155,11 +180,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) } } @@ -169,34 +195,25 @@ 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): List = - storageManager.getFolderContent(folder, false) - .filter { !it.isFolder && !it.isDown } - - private fun checkDiskSize(file: OCFile): Boolean { - val fileSizeInByte = file.fileLength - val availableDiskSpace = FileOperationsHelper.getAvailableSpaceOnDevice() - - return if (availableDiskSpace < fileSizeInByte) { - notificationManager.showNotAvailableDiskSpace() - false + private fun getFiles(folder: OCFile, storageManager: FileDataStorageManager, syncAll: Boolean): List = + if (syncAll) { + storageManager.getAllFilesRecursivelyInsideFolder(folder) + .filter { !it.isDown } } else { - true + storageManager.getFolderContent(folder, false) + .filter { !it.isFolder && !it.isDown } } - } } 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 { 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..a8bc18a56281 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 && !files.first().isEncrypted) { + 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..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 @@ -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.syncFileOrFolder(file) } 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..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 @@ -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. @@ -469,7 +466,7 @@ private void optionsItemSelected(@IdRes final int itemId) { if (containerActivity instanceof FileActivity activity) { activity.showSyncLoadingDialog(getFile().isFolder()); } - containerActivity.getFileOperationsHelper().syncFile(getFile()); + 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 177b994e9168..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 @@ -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; @@ -1418,6 +1417,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 +2202,83 @@ 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)) { + informUserForSyncAllAction(folder); + } else { + SyncFileNotEnoughSpaceDialogFragment + .newInstance(folder, FileOperationsHelper.getAvailableSpaceOnDevice()) + .show(getParentFragmentManager(), ConfirmationDialogFragment.FTAG_CONFIRMATION); } + } - if (mContainerActivity instanceof FileActivity activity && !files.isEmpty()) { - activity.showSyncLoadingDialog(isAnyFileFolder); - } + 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) { + } + }); - Iterator iterator = files.iterator(); - while (iterator.hasNext()) { - OCFile file = iterator.next(); + dialog.show(getParentFragmentManager(), ConfirmationDialogFragment.FTAG_CONFIRMATION); + } - long availableSpaceOnDevice = FileOperationsHelper.getAvailableSpaceOnDevice(); + private void syncAndCheckFiles(Collection files) { + if (files.isEmpty()) return; + + boolean hasFolder = files.stream().anyMatch(OCFile::isFolder); + + 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().syncFileOrFolder(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..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 @@ -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,49 +868,44 @@ 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) { + // region sync file or folder + public void syncFileOrFolder(OCFile file, boolean postDialogEvent, boolean syncAll) { if (file.isFolder()) { - Intent intent = getSyncFolderIntent(file); - fileActivity.startService(intent); + startSyncFolderIntent(file, syncAll); } else { - Intent intent = getSyncFileIntent(file); - mWaitingForOpId = fileActivity.getOperationsServiceBinder().queueNewOperation(intent); + queueSyncFileIntent(file, postDialogEvent); } } - private Intent getSyncFolderIntent(ServerFileInterface file) { - Intent intent = new Intent(fileActivity, OperationsService.class); + 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) { + 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()); - return intent; + intent.putExtra(OperationsService.EXTRA_SYNC_ALL, syncAll); + + 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) { - 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); - } + 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 3fd3acc3874e..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.syncFile(file) + 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 abc808e6f1eb..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.syncFile(file) + 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 5a19c90b77c3..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.syncFile(file) + 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 09ac5d38b681..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().syncFile(getFile()); + containerActivity.getFileOperationsHelper().syncFileOrFolder(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/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..00c3ecaf2ead 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 @@ -326,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