From d7e377c1a09f44ba5430442964e26da3c465338e Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Tue, 18 Aug 2026 17:55:35 +0200 Subject: [PATCH 01/25] feature/1475-delete-uploaded-files: Menu item and popup Signed-off-by: daniele-verducci --- .../ui/activity/SyncedFoldersActivity.kt | 35 +++++++++++++++++++ .../android/ui/adapter/SyncedFolderAdapter.kt | 15 +++++--- .../main/res/menu/synced_folders_adapter.xml | 4 +++ app/src/main/res/values/strings.xml | 4 +++ 4 files changed, 53 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt index 967c1995390d..ce0bdd152f0d 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt @@ -610,6 +610,41 @@ class SyncedFoldersActivity : } } + override fun onSyncFolderDeleteUploadedClick(section: Int, syncedFolderDisplayItem: SyncedFolderDisplayItem?) { + val dialog = ConfirmationDialogFragment.newInstance( + R.string.autoupload_delete_uploaded_dialog_description, + null, + R.string.autoupload_delete_uploaded_dialog_title, + R.drawable.selector_trashbin, + R.string.common_ok, + R.string.common_cancel, + -1 + ) + dialog.isCancelable = false + dialog.setOnConfirmationListener(object : ConfirmationDialogFragment.ConfirmationDialogFragmentListener { + override fun onConfirmation(callerTag: String?) { + TODO("Not yet implemented") + } + + override fun onNeutral(callerTag: String?) { + } + + override fun onCancel(callerTag: String?) { + } + }) + if (isDialogFragmentReady(dialog) && + lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED) + ) { + val fragmentTransaction = supportFragmentManager + .beginTransaction() + .addToBackStack(null) + + dialog.show(fragmentTransaction, SYNCED_FOLDER_PREFERENCES_DIALOG_TAG) + } else { + Log_OC.d(TAG, "SyncFolderDeleteUploaded dialog not ready") + } + } + override fun onVisibilityToggleClick(section: Int, syncedFolder: SyncedFolderDisplayItem?) { if (syncedFolder == null) return diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/SyncedFolderAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/SyncedFolderAdapter.kt index 126af0068702..28d21ba22151 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/SyncedFolderAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/SyncedFolderAdapter.kt @@ -388,11 +388,15 @@ class SyncedFolderAdapter( } private fun optionsItemSelected(menuItem: MenuItem, section: Int, item: SyncedFolderDisplayItem): Boolean { - if (menuItem.itemId == R.id.action_auto_upload_folder_toggle_visibility) { - clickListener.onVisibilityToggleClick(section, item) - } else { - // default: R.id.action_create_custom_folder - clickListener.onSyncFolderSettingsClick(section, item) + when (menuItem.itemId) { + R.id.action_auto_upload_folder_toggle_visibility -> clickListener.onVisibilityToggleClick(section, item) + + R.id.action_auto_upload_folder_settings -> clickListener.onSyncFolderSettingsClick(section, item) + + R.id.action_auto_upload_folder_delete_uploaded -> clickListener.onSyncFolderDeleteUploadedClick( + section, + item + ) } return true } @@ -508,6 +512,7 @@ class SyncedFolderAdapter( interface ClickListener { fun onSyncStatusToggleClick(section: Int, item: SyncedFolderDisplayItem?) fun onSyncFolderSettingsClick(section: Int, syncedFolderDisplayItem: SyncedFolderDisplayItem?) + fun onSyncFolderDeleteUploadedClick(section: Int, syncedFolderDisplayItem: SyncedFolderDisplayItem?) fun onVisibilityToggleClick(section: Int, item: SyncedFolderDisplayItem?) fun showSubFolderWarningDialog() } diff --git a/app/src/main/res/menu/synced_folders_adapter.xml b/app/src/main/res/menu/synced_folders_adapter.xml index 227769ed905b..f0752530a2ac 100644 --- a/app/src/main/res/menu/synced_folders_adapter.xml +++ b/app/src/main/res/menu/synced_folders_adapter.xml @@ -14,4 +14,8 @@ + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3b6cb7d99a11..6c6305790aa6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -878,8 +878,12 @@ Create new custom folder setup Hide folder Configure + Locally delete uploaded files Configure folders + Locally delete uploaded files + All the files already uploaded to the cloud will be removed from the current device + Test server connection  • From be9bc783ab8011f5205f9f038d476a76ca0440d1 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Wed, 19 Aug 2026 18:11:53 +0200 Subject: [PATCH 02/25] feature/1475-delete-uploaded-files: WIP proof of concept Signed-off-by: daniele-verducci --- .../ui/activity/SyncedFoldersActivity.kt | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt index ce0bdd152f0d..c862d9ef1ba3 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt @@ -31,6 +31,8 @@ import com.nextcloud.client.device.PowerManagementService import com.nextcloud.client.di.Injectable import com.nextcloud.client.jobs.MediaFoldersDetectionWork import com.nextcloud.client.jobs.NotificationWork +import com.nextcloud.client.jobs.autoUpload.SyncFolderHelper +import com.nextcloud.client.jobs.operation.FileOperationHelper import com.nextcloud.client.jobs.upload.FileUploadWorker import com.nextcloud.client.preferences.SubFolderRule import com.nextcloud.ui.component.UploadWarningCard @@ -623,7 +625,31 @@ class SyncedFoldersActivity : dialog.isCancelable = false dialog.setOnConfirmationListener(object : ConfirmationDialogFragment.ConfirmationDialogFragmentListener { override fun onConfirmation(callerTag: String?) { - TODO("Not yet implemented") + // DEBUG + lifecycleScope.launch { + val client = clientRepository.getOwncloudClient() ?: return@launch + val syncFolderHelper = SyncFolderHelper(this@SyncedFoldersActivity) + val filesOperationHelper = + FileOperationHelper(user.get(), this@SyncedFoldersActivity, fileDataStorageManager) + val syncedFolderArrayList = syncedFolderProvider.syncedFolders + // TODO: For each synced folder for which the files should be deleted locally + val syncedFolder = + syncedFolderArrayList[0] + val localFolder = File(syncedFolder.localPath) + val files = SyncedFolderUtils.getFileList(localFolder) + files.forEach { + val remotePath = syncFolderHelper.getAutoUploadRemotePath(syncedFolder, it) + val ocFile = OCFile(remotePath) // WRONG: this does not set the remote path, removeFile() fails + filesOperationHelper.removeFile( + file = ocFile, + onlyLocalCopy = true, + inBackground = true, + client = client + ) + Log_OC.d(TAG, "Checking local file ${it.absolutePath} against remote file ${ocFile.remotePath}") + } + } + // DEBUG } override fun onNeutral(callerTag: String?) { From 947bfc1f06c6d858f4dfb9d483c08ca16cef3904 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 20 Aug 2026 09:41:55 +0200 Subject: [PATCH 03/25] feature/1475-delete-uploaded-files: working deletion based only on filename Signed-off-by: daniele-verducci --- .../android/ui/activity/SyncedFoldersActivity.kt | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt index c862d9ef1ba3..78c5ee56a040 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt @@ -638,15 +638,22 @@ class SyncedFoldersActivity : val localFolder = File(syncedFolder.localPath) val files = SyncedFolderUtils.getFileList(localFolder) files.forEach { - val remotePath = syncFolderHelper.getAutoUploadRemotePath(syncedFolder, it) - val ocFile = OCFile(remotePath) // WRONG: this does not set the remote path, removeFile() fails - filesOperationHelper.removeFile( + val ocFile = storageManager.getFileByLocalPath(it.path) + if (ocFile == null) { + Log_OC.d(TAG, "Unable to obtain remote counterpart for file $ocFile") + return@forEach + } + val success = filesOperationHelper.removeFile( file = ocFile, onlyLocalCopy = true, inBackground = true, client = client ) - Log_OC.d(TAG, "Checking local file ${it.absolutePath} against remote file ${ocFile.remotePath}") + if (success) { + Log_OC.d(TAG, "Removed local file ${it.absolutePath}") + } else { + Log_OC.d(TAG, "Error removing local file ${it.absolutePath}") + } } } // DEBUG From b154a1ecebd2e8e59e259d7bcb75ac4d18dadec7 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 20 Aug 2026 15:37:10 +0200 Subject: [PATCH 04/25] feature/1475-delete-uploaded-files: multi folder deletion, refactoring Signed-off-by: daniele-verducci --- .../ui/activity/SyncedFoldersActivity.kt | 118 +++++++++++------- .../main/res/menu/activity_synced_folders.xml | 4 + app/src/main/res/values/strings.xml | 3 + 3 files changed, 79 insertions(+), 46 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt index 78c5ee56a040..3c8bde2cb06d 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt @@ -21,6 +21,7 @@ import android.view.View import androidx.annotation.VisibleForTesting import androidx.appcompat.app.AlertDialog import androidx.drawerlayout.widget.DrawerLayout +import androidx.fragment.app.DialogFragment import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.GridLayoutManager @@ -31,7 +32,6 @@ import com.nextcloud.client.device.PowerManagementService import com.nextcloud.client.di.Injectable import com.nextcloud.client.jobs.MediaFoldersDetectionWork import com.nextcloud.client.jobs.NotificationWork -import com.nextcloud.client.jobs.autoUpload.SyncFolderHelper import com.nextcloud.client.jobs.operation.FileOperationHelper import com.nextcloud.client.jobs.upload.FileUploadWorker import com.nextcloud.client.preferences.SubFolderRule @@ -83,6 +83,8 @@ class SyncedFoldersActivity : companion object { private const val SYNCED_FOLDER_PREFERENCES_DIALOG_TAG = "SYNCED_FOLDER_PREFERENCES_DIALOG" + private const val SYNCED_FOLDER_DELETE_UPLOADED_DIALOG_TAG = "SYNCED_FOLDER_DELETE_UPLOADED_DIALOG_TAG" + private const val SYNCED_FOLDER_DELETE_ALL_UPLOADED_DIALOG_TAG = "SYNCED_FOLDER_DELETE_ALL_UPLOADED_DIALOG_TAG" private const val SUB_FOLDER_WARNING_DIALOG_TAG = "SUB_FOLDER_WARNING_DIALOG_TAG" // yes, there is a typo in this value @@ -584,6 +586,8 @@ class SyncedFoldersActivity : result = super.onOptionsItemSelected(item) } + R.id.action_auto_upload_all_folders_delete_uploaded -> onAllSyncFolderDeleteUploadedClick() + else -> result = super.onOptionsItemSelected(item) } return result @@ -597,22 +601,40 @@ class SyncedFoldersActivity : section ) - dialogFragment?.let { folderPreferencesDialog -> - if (isDialogFragmentReady(folderPreferencesDialog) && - lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED) - ) { - val fragmentTransaction = supportFragmentManager - .beginTransaction() - .addToBackStack(null) + showDialog(dialogFragment!!, SYNCED_FOLDER_PREFERENCES_DIALOG_TAG) + } - folderPreferencesDialog.show(fragmentTransaction, SYNCED_FOLDER_PREFERENCES_DIALOG_TAG) - } else { - Log_OC.d(TAG, "SyncedFolderPreferencesDialogFragment not ready") + private fun onAllSyncFolderDeleteUploadedClick() { + val dialog = ConfirmationDialogFragment.newInstance( + R.string.autoupload_delete_uploaded_all_dialog_description, + null, + R.string.autoupload_delete_uploaded_all_dialog_title, + R.drawable.selector_trashbin, + R.string.common_ok, + R.string.common_cancel, + -1 + ) + dialog.isCancelable = false + dialog.setOnConfirmationListener(object : ConfirmationDialogFragment.ConfirmationDialogFragmentListener { + override fun onConfirmation(callerTag: String?) { + val syncedFolderArrayList = syncedFolderProvider.syncedFolders + syncedFolderArrayList + .filter { it.isEnabled } + .forEach { deleteUploadedItemFromSyncFolder(it) } } - } - } + override fun onNeutral(callerTag: String?) { + } + + override fun onCancel(callerTag: String?) { + } + }) + + showDialog(dialog, SYNCED_FOLDER_DELETE_ALL_UPLOADED_DIALOG_TAG) + } override fun onSyncFolderDeleteUploadedClick(section: Int, syncedFolderDisplayItem: SyncedFolderDisplayItem?) { + syncedFolderDisplayItem ?: return + val dialog = ConfirmationDialogFragment.newInstance( R.string.autoupload_delete_uploaded_dialog_description, null, @@ -625,38 +647,7 @@ class SyncedFoldersActivity : dialog.isCancelable = false dialog.setOnConfirmationListener(object : ConfirmationDialogFragment.ConfirmationDialogFragmentListener { override fun onConfirmation(callerTag: String?) { - // DEBUG - lifecycleScope.launch { - val client = clientRepository.getOwncloudClient() ?: return@launch - val syncFolderHelper = SyncFolderHelper(this@SyncedFoldersActivity) - val filesOperationHelper = - FileOperationHelper(user.get(), this@SyncedFoldersActivity, fileDataStorageManager) - val syncedFolderArrayList = syncedFolderProvider.syncedFolders - // TODO: For each synced folder for which the files should be deleted locally - val syncedFolder = - syncedFolderArrayList[0] - val localFolder = File(syncedFolder.localPath) - val files = SyncedFolderUtils.getFileList(localFolder) - files.forEach { - val ocFile = storageManager.getFileByLocalPath(it.path) - if (ocFile == null) { - Log_OC.d(TAG, "Unable to obtain remote counterpart for file $ocFile") - return@forEach - } - val success = filesOperationHelper.removeFile( - file = ocFile, - onlyLocalCopy = true, - inBackground = true, - client = client - ) - if (success) { - Log_OC.d(TAG, "Removed local file ${it.absolutePath}") - } else { - Log_OC.d(TAG, "Error removing local file ${it.absolutePath}") - } - } - } - // DEBUG + deleteUploadedItemFromSyncFolder(syncedFolderDisplayItem) } override fun onNeutral(callerTag: String?) { @@ -665,6 +656,10 @@ class SyncedFoldersActivity : override fun onCancel(callerTag: String?) { } }) + showDialog(dialog, SYNCED_FOLDER_DELETE_UPLOADED_DIALOG_TAG) + } + + private fun showDialog(dialog: DialogFragment, tag: String) { if (isDialogFragmentReady(dialog) && lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED) ) { @@ -672,12 +667,43 @@ class SyncedFoldersActivity : .beginTransaction() .addToBackStack(null) - dialog.show(fragmentTransaction, SYNCED_FOLDER_PREFERENCES_DIALOG_TAG) + dialog.show(fragmentTransaction, tag) } else { Log_OC.d(TAG, "SyncFolderDeleteUploaded dialog not ready") } } + // TODO: Temporary proof-of-concept impl, to be refactored and moved elsewhere + private fun deleteUploadedItemFromSyncFolder(syncedFolder: SyncedFolder) { + // TODO: Refresh remote data with RefreshFolderOperation (and do recursively for inner folders) + lifecycleScope.launch { + val client = clientRepository.getOwncloudClient() ?: return@launch + val filesOperationHelper = + FileOperationHelper(user.get(), this@SyncedFoldersActivity, fileDataStorageManager) + val localFolder = File(syncedFolder.localPath) + val files = SyncedFolderUtils.getFileList(localFolder) + files.forEach { + val ocFile = storageManager.getFileByLocalPath(it.path) + Log_OC.d(TAG, "LastSyncDate: ${ocFile?.lastSyncDateForProperties}") + if (ocFile == null) { + Log_OC.i(TAG, "deleteUploadedItemFromSyncFolder: Leaving local-only file ${it.name} in place") + return@forEach + } + val success = filesOperationHelper.removeFile( + file = ocFile, + onlyLocalCopy = true, + inBackground = true, + client = client + ) + if (success) { + Log_OC.i(TAG, "deleteUploadedItemFromSyncFolder: Removed local file ${it.absolutePath}") + } else { + Log_OC.e(TAG, "deleteUploadedItemFromSyncFolder: Error removing local file ${it.absolutePath}") + } + } + } + } + override fun onVisibilityToggleClick(section: Int, syncedFolder: SyncedFolderDisplayItem?) { if (syncedFolder == null) return diff --git a/app/src/main/res/menu/activity_synced_folders.xml b/app/src/main/res/menu/activity_synced_folders.xml index af450aa933d2..363c68c06ea6 100644 --- a/app/src/main/res/menu/activity_synced_folders.xml +++ b/app/src/main/res/menu/activity_synced_folders.xml @@ -19,5 +19,9 @@ android:title="@string/autoupload_disable_power_save_check" android:visible="false" android:checkable="true" /> + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6c6305790aa6..e359876ae689 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -884,6 +884,9 @@ Locally delete uploaded files All the files already uploaded to the cloud will be removed from the current device + Locally delete uploaded files for all auto upload folders + All the files already uploaded to the cloud will be removed from the current device for all the autoupload folders + Test server connection  • From 88a86f79037e6a5a202e633637f136754ca1e431 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 20 Aug 2026 17:30:08 +0200 Subject: [PATCH 05/25] feature/1475-delete-uploaded-files: WIP (working folder refresh) Signed-off-by: daniele-verducci --- .../upload/DeleteUploadedFileOperation.kt | 122 ++++++++++++++++++ .../ui/activity/SyncedFoldersActivity.kt | 34 ++--- 2 files changed, 130 insertions(+), 26 deletions(-) create mode 100644 app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt diff --git a/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt b/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt new file mode 100644 index 000000000000..4b0fb1f7f8aa --- /dev/null +++ b/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt @@ -0,0 +1,122 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Your Name + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.operations.upload + +import android.content.Context +import com.nextcloud.client.account.User +import com.nextcloud.client.jobs.autoUpload.SyncFolderHelper +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.datamodel.SyncedFolder +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.operations.RefreshFolderOperation +import com.owncloud.android.utils.SyncedFolderUtils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.File + +class DeleteUploadedFileOperation( + private val syncedFolder: SyncedFolder, + private val user: User, + private val context: Context, + private val storageManager: FileDataStorageManager +) { + + companion object { + const val TAG = "DeleteUploadedFileOperation" + } + private val syncFolderHelper = SyncFolderHelper(context) + + suspend fun run(): RemoteOperationResult<*> { + // TODO: Refresh remote data with RefreshFolderOperation (and do recursively for inner folders) + + val folder = storageManager.getFileByRemotePath(syncedFolder.remotePath) + if (folder == null) { + Log_OC.e(TAG, "Unable to obtain remote folder to refresh metadata") + return RemoteOperationResult(RemoteOperationResult.ResultCode.METADATA_NOT_FOUND) + } + val metadataRefreshSuccess = refreshFolder(folder, storageManager) + if (!metadataRefreshSuccess) { + Log_OC.e(TAG, "Unable to refresh folder metadata") + return RemoteOperationResult(RemoteOperationResult.ResultCode.METADATA_NOT_FOUND) + } + + + + val localFolder = File(syncedFolder.localPath) + val files = SyncedFolderUtils.getFileList(localFolder) + files.forEach { localFile -> + /* + val entity = fileSystemDao.getFileByPathAndFolder(syncedFolder.localPath, syncedFolder.id.toString()) + val fileSentForUpload = entity?.fileSentForUpload == 1 + + if (!fileSentForUpload) { + Log_OC.i(TAG, "File ${localFile.name} never uploaded, leaving in place") + return@forEach + } + + val hasNotChanged = entity.fileModified == localFile.lastModified() + if (!hasNotChanged) { + Log_OC.i(TAG, "File ${localFile.name} has changed, leaving in place") + return@forEach + } + + */ + + //val ocFile = storageManager.getFileByLocalPath(localFile.path) + val remotePath = syncFolderHelper.getAutoUploadRemotePath(syncedFolder, localFile) + val ocFile = storageManager.getFileByRemotePath(remotePath) + if (ocFile == null) { + Log_OC.i(TAG, "Unable to compare file ${localFile.name} with its remote counterpart, leaving in place") + return@forEach + } + Log_OC.d(TAG, "LastSyncDate: ${ocFile?.lastSyncDateForProperties}, modificationTimestamp: ${ocFile.modificationTimestamp}") + + ocFile.lastSyncDateForProperties < localFile.lastModified() + ocFile.modificationTimestamp == localFile.lastModified() + + // File deletion + /* + val success = filesOperationHelper.removeFile( + file = ocFile, + onlyLocalCopy = true, + inBackground = true, + client = client + ) + if (success) { + Log_OC.i(TAG, "deleteUploadedItemFromSyncFolder: Removed local file ${it.absolutePath}") + } else { + Log_OC.e(TAG, "deleteUploadedItemFromSyncFolder: Error removing local file ${it.absolutePath}") + } + */ + Log_OC.d(TAG, "DELETING FILE ${localFile.name}") + } + + return RemoteOperationResult(RemoteOperationResult.ResultCode.OK) + } + + + private suspend fun refreshFolder(folder: OCFile, storageManager: FileDataStorageManager): Boolean = + withContext(Dispatchers.IO) { + val operation = RefreshFolderOperation(folder, storageManager, user, context) + return@withContext try { + val result = operation.execute(user, context) + if (result.isSuccess) { + Log_OC.d(TAG, "Successfully fetched metadata for: ${folder.remotePath}") + true + } else { + Log_OC.e(TAG, "Failed to fetch metadata for: ${folder.remotePath}") + false + } + } catch (e: Exception) { + Log_OC.e(TAG, "Exception refreshing folder ${folder.remotePath}: ${e.message}", e) + false + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt index 3c8bde2cb06d..cc673a6144ad 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt @@ -32,7 +32,6 @@ import com.nextcloud.client.device.PowerManagementService import com.nextcloud.client.di.Injectable import com.nextcloud.client.jobs.MediaFoldersDetectionWork import com.nextcloud.client.jobs.NotificationWork -import com.nextcloud.client.jobs.operation.FileOperationHelper import com.nextcloud.client.jobs.upload.FileUploadWorker import com.nextcloud.client.preferences.SubFolderRule import com.nextcloud.ui.component.UploadWarningCard @@ -54,6 +53,7 @@ import com.owncloud.android.datamodel.SyncedFolderDisplayItem import com.owncloud.android.datamodel.SyncedFolderProvider import com.owncloud.android.files.services.NameCollisionPolicy import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.operations.upload.DeleteUploadedFileOperation import com.owncloud.android.ui.adapter.SyncedFolderAdapter import com.owncloud.android.ui.adapter.storagePermissionBanner.setup import com.owncloud.android.ui.decoration.MediaGridItemDecoration @@ -675,32 +675,14 @@ class SyncedFoldersActivity : // TODO: Temporary proof-of-concept impl, to be refactored and moved elsewhere private fun deleteUploadedItemFromSyncFolder(syncedFolder: SyncedFolder) { - // TODO: Refresh remote data with RefreshFolderOperation (and do recursively for inner folders) + val op = DeleteUploadedFileOperation( + syncedFolder, + user.get(), + this, + storageManager + ) lifecycleScope.launch { - val client = clientRepository.getOwncloudClient() ?: return@launch - val filesOperationHelper = - FileOperationHelper(user.get(), this@SyncedFoldersActivity, fileDataStorageManager) - val localFolder = File(syncedFolder.localPath) - val files = SyncedFolderUtils.getFileList(localFolder) - files.forEach { - val ocFile = storageManager.getFileByLocalPath(it.path) - Log_OC.d(TAG, "LastSyncDate: ${ocFile?.lastSyncDateForProperties}") - if (ocFile == null) { - Log_OC.i(TAG, "deleteUploadedItemFromSyncFolder: Leaving local-only file ${it.name} in place") - return@forEach - } - val success = filesOperationHelper.removeFile( - file = ocFile, - onlyLocalCopy = true, - inBackground = true, - client = client - ) - if (success) { - Log_OC.i(TAG, "deleteUploadedItemFromSyncFolder: Removed local file ${it.absolutePath}") - } else { - Log_OC.e(TAG, "deleteUploadedItemFromSyncFolder: Error removing local file ${it.absolutePath}") - } - } + op.run() } } From 1ae981d57dba7df3f13b48f08ae0d825cfeccbca Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 20 Aug 2026 18:07:12 +0200 Subject: [PATCH 06/25] feature/1475-delete-uploaded-files: Working files checks Signed-off-by: daniele-verducci --- .../upload/DeleteUploadedFileOperation.kt | 40 ++++++++++++++++--- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt b/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt index 4b0fb1f7f8aa..943d030ff808 100644 --- a/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt +++ b/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt @@ -34,8 +34,7 @@ class DeleteUploadedFileOperation( private val syncFolderHelper = SyncFolderHelper(context) suspend fun run(): RemoteOperationResult<*> { - // TODO: Refresh remote data with RefreshFolderOperation (and do recursively for inner folders) - + // TODO: Refresh remote folder data recursively! val folder = storageManager.getFileByRemotePath(syncedFolder.remotePath) if (folder == null) { Log_OC.e(TAG, "Unable to obtain remote folder to refresh metadata") @@ -76,12 +75,36 @@ class DeleteUploadedFileOperation( Log_OC.i(TAG, "Unable to compare file ${localFile.name} with its remote counterpart, leaving in place") return@forEach } - Log_OC.d(TAG, "LastSyncDate: ${ocFile?.lastSyncDateForProperties}, modificationTimestamp: ${ocFile.modificationTimestamp}") - ocFile.lastSyncDateForProperties < localFile.lastModified() - ocFile.modificationTimestamp == localFile.lastModified() + // Check the file wasn't modified after uploading + // TODO: Is this redundant? + val localLastMod = localFile.lastModified() + val lastSyncDate = ocFile.lastSyncDateForProperties + if ( lastSyncDate < localLastMod) { + Log_OC.i(TAG, "File ${localFile.name} has been modified ($localLastMod " + + "after it was synced ($lastSyncDate), leaving in place") + return@forEach + } + + // Check the file has same mod date. Note that the remote mod date is rounded to the second. + val remoteLastMod = ocFile.modificationTimestamp + if (remoteLastMod / 1000 != localLastMod / 1000) { + Log_OC.i(TAG, "Local and remote mod date differs for file file ${localFile.name}: " + + "$localLastMod : $remoteLastMod, leaving in place") + return@forEach + } + + // Check the file has same size + val localSize = localFile.length() + val remoteSize = ocFile.fileLength + if (localSize != remoteSize) { + Log_OC.d(TAG, "Local and remote file sizes differs for file ${localFile.name}: " + + "$localSize : $remoteSize, leaving in place") + return@forEach + } // File deletion + // TODO: This requires an OwnCloudClient but we have a NextCloudClient /* val success = filesOperationHelper.removeFile( file = ocFile, @@ -95,7 +118,12 @@ class DeleteUploadedFileOperation( Log_OC.e(TAG, "deleteUploadedItemFromSyncFolder: Error removing local file ${it.absolutePath}") } */ - Log_OC.d(TAG, "DELETING FILE ${localFile.name}") + val deleted = localFile.delete() + if (deleted) { + Log_OC.i(TAG, "Deleted file ${localFile.name}") + } else { + Log_OC.e(TAG, "Error deleting file ${localFile.name}") + } } return RemoteOperationResult(RemoteOperationResult.ResultCode.OK) From 774c44a8360def9e9b666803ab62c3cad2449042 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 20 Aug 2026 18:10:51 +0200 Subject: [PATCH 07/25] feature/1475-delete-uploaded-files: Cleanup Signed-off-by: daniele-verducci --- .../upload/DeleteUploadedFileOperation.kt | 62 ++++++------------- 1 file changed, 18 insertions(+), 44 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt b/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt index 943d030ff808..c7b20ce8ea1a 100644 --- a/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt +++ b/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt @@ -27,7 +27,7 @@ class DeleteUploadedFileOperation( private val context: Context, private val storageManager: FileDataStorageManager ) { - + companion object { const val TAG = "DeleteUploadedFileOperation" } @@ -46,29 +46,9 @@ class DeleteUploadedFileOperation( return RemoteOperationResult(RemoteOperationResult.ResultCode.METADATA_NOT_FOUND) } - - val localFolder = File(syncedFolder.localPath) val files = SyncedFolderUtils.getFileList(localFolder) files.forEach { localFile -> - /* - val entity = fileSystemDao.getFileByPathAndFolder(syncedFolder.localPath, syncedFolder.id.toString()) - val fileSentForUpload = entity?.fileSentForUpload == 1 - - if (!fileSentForUpload) { - Log_OC.i(TAG, "File ${localFile.name} never uploaded, leaving in place") - return@forEach - } - - val hasNotChanged = entity.fileModified == localFile.lastModified() - if (!hasNotChanged) { - Log_OC.i(TAG, "File ${localFile.name} has changed, leaving in place") - return@forEach - } - - */ - - //val ocFile = storageManager.getFileByLocalPath(localFile.path) val remotePath = syncFolderHelper.getAutoUploadRemotePath(syncedFolder, localFile) val ocFile = storageManager.getFileByRemotePath(remotePath) if (ocFile == null) { @@ -80,17 +60,23 @@ class DeleteUploadedFileOperation( // TODO: Is this redundant? val localLastMod = localFile.lastModified() val lastSyncDate = ocFile.lastSyncDateForProperties - if ( lastSyncDate < localLastMod) { - Log_OC.i(TAG, "File ${localFile.name} has been modified ($localLastMod " + - "after it was synced ($lastSyncDate), leaving in place") + if (lastSyncDate < localLastMod) { + Log_OC.i( + TAG, + "File ${localFile.name} has been modified ($localLastMod " + + "after it was synced ($lastSyncDate), leaving in place" + ) return@forEach } // Check the file has same mod date. Note that the remote mod date is rounded to the second. val remoteLastMod = ocFile.modificationTimestamp if (remoteLastMod / 1000 != localLastMod / 1000) { - Log_OC.i(TAG, "Local and remote mod date differs for file file ${localFile.name}: " + - "$localLastMod : $remoteLastMod, leaving in place") + Log_OC.i( + TAG, + "Local and remote mod date differs for file file ${localFile.name}: " + + "$localLastMod : $remoteLastMod, leaving in place" + ) return@forEach } @@ -98,26 +84,15 @@ class DeleteUploadedFileOperation( val localSize = localFile.length() val remoteSize = ocFile.fileLength if (localSize != remoteSize) { - Log_OC.d(TAG, "Local and remote file sizes differs for file ${localFile.name}: " + - "$localSize : $remoteSize, leaving in place") + Log_OC.d( + TAG, + "Local and remote file sizes differs for file ${localFile.name}: " + + "$localSize : $remoteSize, leaving in place" + ) return@forEach } // File deletion - // TODO: This requires an OwnCloudClient but we have a NextCloudClient - /* - val success = filesOperationHelper.removeFile( - file = ocFile, - onlyLocalCopy = true, - inBackground = true, - client = client - ) - if (success) { - Log_OC.i(TAG, "deleteUploadedItemFromSyncFolder: Removed local file ${it.absolutePath}") - } else { - Log_OC.e(TAG, "deleteUploadedItemFromSyncFolder: Error removing local file ${it.absolutePath}") - } - */ val deleted = localFile.delete() if (deleted) { Log_OC.i(TAG, "Deleted file ${localFile.name}") @@ -129,7 +104,6 @@ class DeleteUploadedFileOperation( return RemoteOperationResult(RemoteOperationResult.ResultCode.OK) } - private suspend fun refreshFolder(folder: OCFile, storageManager: FileDataStorageManager): Boolean = withContext(Dispatchers.IO) { val operation = RefreshFolderOperation(folder, storageManager, user, context) @@ -147,4 +121,4 @@ class DeleteUploadedFileOperation( false } } -} \ No newline at end of file +} From 2e2029accf14a94469211d1db0e87aad90daae6f Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Fri, 21 Aug 2026 07:43:24 +0200 Subject: [PATCH 08/25] feature/1475-delete-uploaded-files: refresh subfolders as needed Signed-off-by: daniele-verducci --- .../upload/DeleteUploadedFileOperation.kt | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt b/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt index c7b20ce8ea1a..facb1320a479 100644 --- a/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt +++ b/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt @@ -34,17 +34,20 @@ class DeleteUploadedFileOperation( private val syncFolderHelper = SyncFolderHelper(context) suspend fun run(): RemoteOperationResult<*> { - // TODO: Refresh remote folder data recursively! + // Obtain synced folder data val folder = storageManager.getFileByRemotePath(syncedFolder.remotePath) if (folder == null) { Log_OC.e(TAG, "Unable to obtain remote folder to refresh metadata") return RemoteOperationResult(RemoteOperationResult.ResultCode.METADATA_NOT_FOUND) } + + // Refresh synced folder metadata val metadataRefreshSuccess = refreshFolder(folder, storageManager) if (!metadataRefreshSuccess) { Log_OC.e(TAG, "Unable to refresh folder metadata") return RemoteOperationResult(RemoteOperationResult.ResultCode.METADATA_NOT_FOUND) } + val refreshedFolders = hashSetOf(folder.remotePath) val localFolder = File(syncedFolder.localPath) val files = SyncedFolderUtils.getFileList(localFolder) @@ -56,6 +59,22 @@ class DeleteUploadedFileOperation( return@forEach } + val parentFolderRemotePath = ocFile.parentRemotePath + if (syncedFolder.isSubfolderByDate && parentFolderRemotePath !in refreshedFolders) { + // Files are stored in subfolder by date on the server. + // Refresh only subfolders containing one of the files to be checked + val subFolder = storageManager.getFileByRemotePath(parentFolderRemotePath) + if (subFolder == null) { + Log_OC.e(TAG, "Subfolder $parentFolderRemotePath not found on the server") + return RemoteOperationResult(RemoteOperationResult.ResultCode.METADATA_NOT_FOUND) + } + val metadataRefreshSuccess = refreshFolder(subFolder, storageManager) + if (!metadataRefreshSuccess) { + Log_OC.e(TAG, "Unable to refresh folder metadata for $parentFolderRemotePath") + return RemoteOperationResult(RemoteOperationResult.ResultCode.METADATA_NOT_FOUND) + } + } + // Check the file wasn't modified after uploading // TODO: Is this redundant? val localLastMod = localFile.lastModified() @@ -93,7 +112,7 @@ class DeleteUploadedFileOperation( } // File deletion - val deleted = localFile.delete() + val deleted = true //localFile.delete() if (deleted) { Log_OC.i(TAG, "Deleted file ${localFile.name}") } else { From 5df2ec6e515bfdb90a79e6ceba6d4d2c00b466c2 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Fri, 21 Aug 2026 08:21:54 +0200 Subject: [PATCH 09/25] feature/1475-delete-uploaded-files: Added option to system manage space screen Signed-off-by: daniele-verducci --- .../ui/activity/ManageSpaceActivity.kt | 13 +++++ .../ui/activity/SyncedFoldersActivity.kt | 2 +- .../main/res/layout/activity_manage_space.xml | 58 ++++++++++++++++++- app/src/main/res/values/strings.xml | 3 + 4 files changed, 74 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ManageSpaceActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/ManageSpaceActivity.kt index b176788af43b..60bcc15c4265 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/ManageSpaceActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/ManageSpaceActivity.kt @@ -51,9 +51,11 @@ class ManageSpaceActivity : setContentView(binding.root) binding.run { + manageActivityToolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() } + generalDescription.text = getString(R.string.manage_space_description, getString(R.string.app_name)) clearDataButton.setOnClickListener { lifecycleScope.launch { @@ -61,6 +63,13 @@ class ManageSpaceActivity : } } viewThemeUtils.material.colorMaterialButtonPrimaryTonal(clearDataButton) + + autoUploadDeleteButton.setOnClickListener { + lifecycleScope.launch { + clearAutoUploadData() + } + } + viewThemeUtils.material.colorMaterialButtonPrimaryTonal(autoUploadDeleteButton) } } @@ -149,6 +158,10 @@ class ManageSpaceActivity : return dir?.delete() ?: false } + private fun clearAutoUploadData() { + + } + override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { android.R.id.home -> { finish() diff --git a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt index cc673a6144ad..1918a43de5d7 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt @@ -673,8 +673,8 @@ class SyncedFoldersActivity : } } - // TODO: Temporary proof-of-concept impl, to be refactored and moved elsewhere private fun deleteUploadedItemFromSyncFolder(syncedFolder: SyncedFolder) { + // TODO: Run in a background job with notifications to the user about the progression val op = DeleteUploadedFileOperation( syncedFolder, user.get(), diff --git a/app/src/main/res/layout/activity_manage_space.xml b/app/src/main/res/layout/activity_manage_space.xml index 1ef4a0dd1af3..7097badb06ab 100644 --- a/app/src/main/res/layout/activity_manage_space.xml +++ b/app/src/main/res/layout/activity_manage_space.xml @@ -26,7 +26,7 @@ android:background="@color/appbar" /> + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e359876ae689..1047184c6274 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -767,6 +767,9 @@ Manage space Settings, database and server certificates from %1$s\'s data will be deleted permanently. \n\nDownloaded files will be kept untouched.\n\nThis process can take a while. Clear data + Locally delete auto uploaded files + All files already uploaded to the server from the Auto Upload folders will be deleted locally to free up space in the device.\n\nThis process can take a while. + Locally delete auto uploaded files File not found in local file system Do you really want to delete the selected items? From 417bc5715c2a0762e68826d779e25a105ce8cb55 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Fri, 21 Aug 2026 12:45:38 +0200 Subject: [PATCH 10/25] feature/1475-delete-uploaded-files: Moved logic to background worker, wired up manage space button Signed-off-by: daniele-verducci --- .../client/jobs/BackgroundJobManager.kt | 1 + .../client/jobs/BackgroundJobManagerImpl.kt | 29 +++++++++ .../AutoUploadLocalDeletionWorker.kt | 63 +++++++++++++++++++ .../upload/DeleteUploadedFileOperation.kt | 4 +- .../ui/activity/ManageSpaceActivity.kt | 11 +++- .../ui/activity/SyncedFoldersActivity.kt | 22 +------ .../owncloud/android/utils/FilesSyncHelper.kt | 21 +++++++ 7 files changed, 129 insertions(+), 22 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt 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 a859e5808d56..9bfd2c1392a5 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt @@ -171,4 +171,5 @@ interface BackgroundJobManager { fun startMetadataSyncJob(currentDirPath: String) fun downloadFolder(folder: OCFile, accountName: String) fun cancelFolderDownload() + fun locallyDeleteAutoUploadedFiles(syncedFolders: List) } 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 4a28b1715dea..4d7a6f41b030 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt @@ -28,6 +28,7 @@ import com.nextcloud.client.account.User import com.nextcloud.client.core.Clock import com.nextcloud.client.di.Injectable import com.nextcloud.client.documentscan.GeneratePdfFromImagesWork +import com.nextcloud.client.jobs.autoUpload.AutoUploadLocalDeletionWorker import com.nextcloud.client.jobs.autoUpload.AutoUploadWorker import com.nextcloud.client.jobs.download.FileDownloadWorker import com.nextcloud.client.jobs.folderDownload.FolderDownloadWorker @@ -103,6 +104,7 @@ internal class BackgroundJobManagerImpl( const val JOB_DOWNLOAD_FOLDER = "download_folder" const val JOB_METADATA_SYNC = "metadata_sync" const val JOB_INTERNAL_TWO_WAY_SYNC = "internal_two_way_sync" + const val JOB_AUTO_UPLOAD_LOCAL_DELETION = "auto_upload_local_deletion" const val JOB_TEST = "test_job" @@ -824,4 +826,31 @@ internal class BackgroundJobManagerImpl( override fun cancelFolderDownload() { workManager.cancelAllWorkByTag(JOB_DOWNLOAD_FOLDER) } + + override fun locallyDeleteAutoUploadedFiles(syncedFolders: List) { + val syncedFolderIDs = syncedFolders.map { it.id } + + val arguments = Data.Builder() + .putLongArray(AutoUploadLocalDeletionWorker.SYNCED_FOLDER_IDS, syncedFolderIDs.toLongArray()) + .build() + + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + + val jobName = JOB_AUTO_UPLOAD_LOCAL_DELETION + "_" + syncedFolderIDs.joinToString("-") + val request = oneTimeRequestBuilder( + jobClass = AutoUploadLocalDeletionWorker::class, + jobName = jobName + ) + .setInputData(arguments) + .setConstraints(constraints) + .build() + + workManager.enqueueUniqueWork( + jobName, + ExistingWorkPolicy.KEEP, + request + ) + } } diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt new file mode 100644 index 000000000000..7de6779454c4 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt @@ -0,0 +1,63 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Daniele Verducci + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.jobs.autoUpload + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import com.nextcloud.client.account.User +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.datamodel.SyncedFolderProvider +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.operations.upload.DeleteUploadedFileOperation + +class AutoUploadLocalDeletionWorker( + private val context: Context, + params: WorkerParameters, + private val user: User, + private val storageManager: FileDataStorageManager, + private val syncedFolderProvider: SyncedFolderProvider, +) : CoroutineWorker (context, params) { + + companion object { + const val SYNCED_FOLDER_IDS = "synced_folder_IDs" + + private const val TAG = "\uD83D\uDDD1\uFE0F AutoUploadLocalDeletionWorker" + } + + override suspend fun doWork(): Result { + // TODO: Notify user the operation started + Log_OC.d(TAG, "Started") + + val syncedFolderIDs = inputData.getLongArray(SYNCED_FOLDER_IDS) + ?: throw IllegalArgumentException("$SYNCED_FOLDER_IDS param is mandatory") + val syncedFolders = syncedFolderIDs.map { syncedFolderProvider.getSyncedFolderByID(it) } + + syncedFolders + .filterNotNull() + .filter { it.isEnabled } + .forEach { + val op = DeleteUploadedFileOperation( + it, + user, + context, + storageManager + ) + val res = op.run() + if (res.code != RemoteOperationResult.ResultCode.OK) { + Log_OC.d(TAG, "Failed") + return Result.failure() // TODO: Notify user the operation failed + } + } + // TODO: Notify user the operation completed + Log_OC.d(TAG, "Success") + return Result.success() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt b/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt index facb1320a479..bca096bbb0a3 100644 --- a/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt +++ b/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt @@ -1,7 +1,7 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2026 Your Name + * SPDX-FileCopyrightText: 2026 Daniele Verducci * SPDX-License-Identifier: AGPL-3.0-or-later */ @@ -60,7 +60,7 @@ class DeleteUploadedFileOperation( } val parentFolderRemotePath = ocFile.parentRemotePath - if (syncedFolder.isSubfolderByDate && parentFolderRemotePath !in refreshedFolders) { + if (/*syncedFolder.isSubfolderByDate &&*/ parentFolderRemotePath !in refreshedFolders) { // Files are stored in subfolder by date on the server. // Refresh only subfolders containing one of the files to be checked val subFolder = storageManager.getFileByRemotePath(parentFolderRemotePath) diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ManageSpaceActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/ManageSpaceActivity.kt index 60bcc15c4265..be8764a6f90c 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/ManageSpaceActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/ManageSpaceActivity.kt @@ -16,10 +16,13 @@ import com.google.android.material.snackbar.Snackbar import com.nextcloud.android.common.ui.util.extensions.applyEdgeToEdgeWithSystemBarPadding import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.di.Injectable +import com.nextcloud.client.jobs.BackgroundJobManager import com.nextcloud.client.preferences.AppPreferences import com.owncloud.android.R import com.owncloud.android.databinding.ActivityManageSpaceBinding +import com.owncloud.android.datamodel.SyncedFolderProvider import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.utils.FilesSyncHelper import com.owncloud.android.utils.theme.ViewThemeUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -41,6 +44,12 @@ class ManageSpaceActivity : @Inject lateinit var viewThemeUtils: ViewThemeUtils + @Inject + lateinit var syncedFolderProvider: SyncedFolderProvider + + @Inject + lateinit var backgroundJobManager: BackgroundJobManager + private lateinit var binding: ActivityManageSpaceBinding override fun onCreate(savedInstanceState: Bundle?) { @@ -159,7 +168,7 @@ class ManageSpaceActivity : } private fun clearAutoUploadData() { - + FilesSyncHelper.startLocalDeletionForEnabledSyncedFolders(syncedFolderProvider, backgroundJobManager) } override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { diff --git a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt index 1918a43de5d7..61d7c7516419 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt @@ -53,7 +53,6 @@ import com.owncloud.android.datamodel.SyncedFolderDisplayItem import com.owncloud.android.datamodel.SyncedFolderProvider import com.owncloud.android.files.services.NameCollisionPolicy import com.owncloud.android.lib.common.utils.Log_OC -import com.owncloud.android.operations.upload.DeleteUploadedFileOperation import com.owncloud.android.ui.adapter.SyncedFolderAdapter import com.owncloud.android.ui.adapter.storagePermissionBanner.setup import com.owncloud.android.ui.decoration.MediaGridItemDecoration @@ -62,6 +61,7 @@ import com.owncloud.android.ui.dialog.SyncedFolderPreferencesDialogFragment import com.owncloud.android.ui.dialog.SyncedFolderPreferencesDialogFragment.OnSyncedFolderPreferenceListener import com.owncloud.android.ui.dialog.extensions.themeButtons import com.owncloud.android.ui.dialog.parcel.SyncedFolderParcelable +import com.owncloud.android.utils.FilesSyncHelper import com.owncloud.android.utils.PermissionUtil import com.owncloud.android.utils.SyncedFolderUtils import kotlinx.coroutines.Dispatchers @@ -617,10 +617,7 @@ class SyncedFoldersActivity : dialog.isCancelable = false dialog.setOnConfirmationListener(object : ConfirmationDialogFragment.ConfirmationDialogFragmentListener { override fun onConfirmation(callerTag: String?) { - val syncedFolderArrayList = syncedFolderProvider.syncedFolders - syncedFolderArrayList - .filter { it.isEnabled } - .forEach { deleteUploadedItemFromSyncFolder(it) } + FilesSyncHelper.startLocalDeletionForEnabledSyncedFolders(syncedFolderProvider, backgroundJobManager) } override fun onNeutral(callerTag: String?) { @@ -647,7 +644,7 @@ class SyncedFoldersActivity : dialog.isCancelable = false dialog.setOnConfirmationListener(object : ConfirmationDialogFragment.ConfirmationDialogFragmentListener { override fun onConfirmation(callerTag: String?) { - deleteUploadedItemFromSyncFolder(syncedFolderDisplayItem) + FilesSyncHelper.startLocalDeletionForSyncedFolder(syncedFolderDisplayItem, backgroundJobManager) } override fun onNeutral(callerTag: String?) { @@ -673,19 +670,6 @@ class SyncedFoldersActivity : } } - private fun deleteUploadedItemFromSyncFolder(syncedFolder: SyncedFolder) { - // TODO: Run in a background job with notifications to the user about the progression - val op = DeleteUploadedFileOperation( - syncedFolder, - user.get(), - this, - storageManager - ) - lifecycleScope.launch { - op.run() - } - } - override fun onVisibilityToggleClick(section: Int, syncedFolder: SyncedFolderDisplayItem?) { if (syncedFolder == null) return diff --git a/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.kt b/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.kt index 9b18dea58a5b..e00b30bce679 100644 --- a/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.kt +++ b/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.kt @@ -14,6 +14,7 @@ import com.nextcloud.client.device.PowerManagementService import com.nextcloud.client.jobs.BackgroundJobManager import com.nextcloud.client.jobs.upload.FileUploadHelper.Companion.instance import com.nextcloud.client.network.ConnectivityService +import com.owncloud.android.datamodel.SyncedFolder import com.owncloud.android.datamodel.SyncedFolderProvider import com.owncloud.android.datamodel.UploadsStorageManager import com.owncloud.android.lib.common.utils.Log_OC @@ -52,4 +53,24 @@ object FilesSyncHelper { } } } + + @JvmStatic + fun startLocalDeletionForEnabledSyncedFolders( + provider: SyncedFolderProvider, + manager: BackgroundJobManager + ) { + Log_OC.d(TAG, "start local deletion worker for each enabled folder") + + manager.locallyDeleteAutoUploadedFiles(provider.syncedFolders) + } + + @JvmStatic + fun startLocalDeletionForSyncedFolder( + folder: SyncedFolder, + manager: BackgroundJobManager + ) { + Log_OC.d(TAG, "start local deletion worker for folder ${folder.localPath}") + + manager.locallyDeleteAutoUploadedFiles(listOf(folder)) + } } From ba9f473c6dea31f7799db780d471f36ef0283d29 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Fri, 21 Aug 2026 15:56:47 +0200 Subject: [PATCH 11/25] feature/1475-delete-uploaded-files: Passing needed objects to background worker Signed-off-by: daniele-verducci --- .../nextcloud/client/jobs/BackgroundJobFactory.kt | 14 ++++++++++++++ .../autoUpload/AutoUploadLocalDeletionWorker.kt | 9 ++++----- .../upload/DeleteUploadedFileOperation.kt | 9 +++++---- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt index b9145d7e8769..964cb24016d4 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt @@ -23,6 +23,7 @@ import com.nextcloud.client.documentscan.GeneratePDFUseCase import com.nextcloud.client.documentscan.GeneratePdfFromImagesWork import com.nextcloud.client.integrations.deck.DeckApi import com.nextcloud.client.jobs.autoUpload.AutoUploadHelper +import com.nextcloud.client.jobs.autoUpload.AutoUploadLocalDeletionWorker import com.nextcloud.client.jobs.autoUpload.AutoUploadWorker import com.nextcloud.client.jobs.autoUpload.FileSystemRepository import com.nextcloud.client.jobs.download.FileDownloadWorker @@ -34,6 +35,7 @@ import com.nextcloud.client.logger.Logger import com.nextcloud.client.network.ConnectivityService import com.nextcloud.client.preferences.AppPreferences import com.owncloud.android.datamodel.ArbitraryDataProvider +import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.SyncedFolderProvider import com.owncloud.android.datamodel.UploadsStorageManager import com.owncloud.android.operations.factory.UploadFileOperationFactory @@ -59,6 +61,7 @@ class BackgroundJobFactory @Inject constructor( private val resources: Resources, private val arbitraryDataProvider: ArbitraryDataProvider, private val uploadsStorageManager: UploadsStorageManager, + private val fileDataStorageManager: FileDataStorageManager, private val connectivityService: ConnectivityService, private val notificationManager: NotificationManager, private val eventBus: EventBus, @@ -107,6 +110,7 @@ class BackgroundJobFactory @Inject constructor( InternalTwoWaySyncWork::class -> createInternalTwoWaySyncWork(context, workerParameters) MetadataWorker::class -> createMetadataWorker(context, workerParameters) FolderDownloadWorker::class -> createFolderDownloadWorker(context, workerParameters) + AutoUploadLocalDeletionWorker::class -> createAutoUploadLocalDeletionWorker(context, workerParameters) else -> null // caller falls back to default factory } } @@ -311,4 +315,14 @@ class BackgroundJobFactory @Inject constructor( localBroadcastManager.get(), params ) + + private fun createAutoUploadLocalDeletionWorker(context: Context, params: WorkerParameters): + AutoUploadLocalDeletionWorker = + AutoUploadLocalDeletionWorker( + context = context, + params = params, + userAccountManager = accountManager, + fileDataStorageManager = fileDataStorageManager, + syncedFolderProvider = syncedFolderProvider + ) } diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt index 7de6779454c4..f80cfb521f15 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt @@ -10,7 +10,7 @@ package com.nextcloud.client.jobs.autoUpload import android.content.Context import androidx.work.CoroutineWorker import androidx.work.WorkerParameters -import com.nextcloud.client.account.User +import com.nextcloud.client.account.UserAccountManager import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.SyncedFolderProvider import com.owncloud.android.lib.common.operations.RemoteOperationResult @@ -20,8 +20,8 @@ import com.owncloud.android.operations.upload.DeleteUploadedFileOperation class AutoUploadLocalDeletionWorker( private val context: Context, params: WorkerParameters, - private val user: User, - private val storageManager: FileDataStorageManager, + private val userAccountManager: UserAccountManager, + private val fileDataStorageManager: FileDataStorageManager, private val syncedFolderProvider: SyncedFolderProvider, ) : CoroutineWorker (context, params) { @@ -45,9 +45,8 @@ class AutoUploadLocalDeletionWorker( .forEach { val op = DeleteUploadedFileOperation( it, - user, context, - storageManager + fileDataStorageManager ) val res = op.run() if (res.code != RemoteOperationResult.ResultCode.OK) { diff --git a/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt b/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt index bca096bbb0a3..7adce8f032f4 100644 --- a/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt +++ b/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt @@ -8,7 +8,6 @@ package com.owncloud.android.operations.upload import android.content.Context -import com.nextcloud.client.account.User import com.nextcloud.client.jobs.autoUpload.SyncFolderHelper import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.OCFile @@ -23,7 +22,6 @@ import java.io.File class DeleteUploadedFileOperation( private val syncedFolder: SyncedFolder, - private val user: User, private val context: Context, private val storageManager: FileDataStorageManager ) { @@ -34,8 +32,10 @@ class DeleteUploadedFileOperation( private val syncFolderHelper = SyncFolderHelper(context) suspend fun run(): RemoteOperationResult<*> { + Log_OC.d(TAG, "StorageManager user is ${storageManager.user}") // Obtain synced folder data val folder = storageManager.getFileByRemotePath(syncedFolder.remotePath) + ?: storageManager.getFileByLocalPath(syncedFolder.localPath) if (folder == null) { Log_OC.e(TAG, "Unable to obtain remote folder to refresh metadata") return RemoteOperationResult(RemoteOperationResult.ResultCode.METADATA_NOT_FOUND) @@ -54,6 +54,7 @@ class DeleteUploadedFileOperation( files.forEach { localFile -> val remotePath = syncFolderHelper.getAutoUploadRemotePath(syncedFolder, localFile) val ocFile = storageManager.getFileByRemotePath(remotePath) + ?: storageManager.getFileByLocalPath(localFile.absolutePath) if (ocFile == null) { Log_OC.i(TAG, "Unable to compare file ${localFile.name} with its remote counterpart, leaving in place") return@forEach @@ -125,9 +126,9 @@ class DeleteUploadedFileOperation( private suspend fun refreshFolder(folder: OCFile, storageManager: FileDataStorageManager): Boolean = withContext(Dispatchers.IO) { - val operation = RefreshFolderOperation(folder, storageManager, user, context) + val operation = RefreshFolderOperation(folder, storageManager, storageManager.user, context) return@withContext try { - val result = operation.execute(user, context) + val result = operation.execute(storageManager.user, context) if (result.isSuccess) { Log_OC.d(TAG, "Successfully fetched metadata for: ${folder.remotePath}") true From 83193544e119dc5492774e9cfd55499274351e49 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Mon, 24 Aug 2026 17:10:00 +0200 Subject: [PATCH 12/25] feature/1475-delete-uploaded-files: notifications Signed-off-by: daniele-verducci --- .../client/jobs/BackgroundJobFactory.kt | 3 +- .../AutoUploadLocalDeletionWorker.kt | 39 +++++++++++++++++-- app/src/main/res/values/strings.xml | 4 ++ 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt index 964cb24016d4..1e04a0df2ad3 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt @@ -323,6 +323,7 @@ class BackgroundJobFactory @Inject constructor( params = params, userAccountManager = accountManager, fileDataStorageManager = fileDataStorageManager, - syncedFolderProvider = syncedFolderProvider + syncedFolderProvider = syncedFolderProvider, + viewThemeUtils = viewThemeUtils.get() ) } diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt index f80cfb521f15..ac81bf2bb707 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt @@ -7,15 +7,20 @@ package com.nextcloud.client.jobs.autoUpload +import android.app.Notification import android.content.Context import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import com.nextcloud.client.account.UserAccountManager +import com.nextcloud.client.jobs.notification.WorkerNotificationManager +import com.owncloud.android.R import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.SyncedFolderProvider import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.operations.upload.DeleteUploadedFileOperation +import com.owncloud.android.ui.notifications.NotificationUtils +import com.owncloud.android.utils.theme.ViewThemeUtils class AutoUploadLocalDeletionWorker( private val context: Context, @@ -23,16 +28,28 @@ class AutoUploadLocalDeletionWorker( private val userAccountManager: UserAccountManager, private val fileDataStorageManager: FileDataStorageManager, private val syncedFolderProvider: SyncedFolderProvider, + val viewThemeUtils: ViewThemeUtils, ) : CoroutineWorker (context, params) { companion object { const val SYNCED_FOLDER_IDS = "synced_folder_IDs" + const val NOTIFICATION_ID = 267 private const val TAG = "\uD83D\uDDD1\uFE0F AutoUploadLocalDeletionWorker" } + private val notificationManager = WorkerNotificationManager( + NOTIFICATION_ID, + context, + viewThemeUtils, + R.string.autoupload_delete_uploaded_notif_ticker, + NotificationUtils.NOTIFICATION_CHANNEL_BACKGROUND_OPERATIONS + ) + override suspend fun doWork(): Result { - // TODO: Notify user the operation started + showNotification( + createNotification(context.getString(R.string.autoupload_delete_uploaded_notif_started_title)) + ) Log_OC.d(TAG, "Started") val syncedFolderIDs = inputData.getLongArray(SYNCED_FOLDER_IDS) @@ -51,12 +68,28 @@ class AutoUploadLocalDeletionWorker( val res = op.run() if (res.code != RemoteOperationResult.ResultCode.OK) { Log_OC.d(TAG, "Failed") - return Result.failure() // TODO: Notify user the operation failed + showNotification( + createNotification(context.getString(R.string.autoupload_delete_uploaded_notif_error_title)) + ) + return Result.failure() } } - // TODO: Notify user the operation completed + showNotification( + createNotification(context.getString(R.string.autoupload_delete_uploaded_notif_ended_title)) + ) Log_OC.d(TAG, "Success") return Result.success() } + private fun createNotification(title: String): Notification = notificationManager.notificationBuilder + .setContentTitle(title) + .setSmallIcon(R.drawable.ic_delete) + .setSound(null) + .setVibrate(null) + .setOnlyAlertOnce(true) + .setSilent(true) + .setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_BACKGROUND_OPERATIONS) + .build() + + private fun showNotification(notification: Notification) = notificationManager.showNotification() } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1047184c6274..d324f039f2ef 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -882,6 +882,10 @@ Hide folder Configure Locally delete uploaded files + Local files deletion + Started to locally delete already uploaded files + Locally deleted already uploaded files + Unable to locally delete already uploaded files Configure folders Locally delete uploaded files From 322772b164252db659b4822ec47423e57852971b Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Mon, 24 Aug 2026 17:18:53 +0200 Subject: [PATCH 13/25] feature/1475-delete-uploaded-files: lint Signed-off-by: daniele-verducci --- .../client/jobs/BackgroundJobFactory.kt | 21 ++++++++++--------- .../AutoUploadLocalDeletionWorker.kt | 6 +++--- .../upload/DeleteUploadedFileOperation.kt | 4 ++-- .../ui/activity/ManageSpaceActivity.kt | 1 - .../owncloud/android/utils/FilesSyncHelper.kt | 10 ++------- 5 files changed, 18 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt index 1e04a0df2ad3..49fdd9705a2d 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt @@ -316,14 +316,15 @@ class BackgroundJobFactory @Inject constructor( params ) - private fun createAutoUploadLocalDeletionWorker(context: Context, params: WorkerParameters): - AutoUploadLocalDeletionWorker = - AutoUploadLocalDeletionWorker( - context = context, - params = params, - userAccountManager = accountManager, - fileDataStorageManager = fileDataStorageManager, - syncedFolderProvider = syncedFolderProvider, - viewThemeUtils = viewThemeUtils.get() - ) + private fun createAutoUploadLocalDeletionWorker( + context: Context, + params: WorkerParameters + ): AutoUploadLocalDeletionWorker = AutoUploadLocalDeletionWorker( + context = context, + params = params, + userAccountManager = accountManager, + fileDataStorageManager = fileDataStorageManager, + syncedFolderProvider = syncedFolderProvider, + viewThemeUtils = viewThemeUtils.get() + ) } diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt index ac81bf2bb707..e5d13e15fbfd 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt @@ -28,8 +28,8 @@ class AutoUploadLocalDeletionWorker( private val userAccountManager: UserAccountManager, private val fileDataStorageManager: FileDataStorageManager, private val syncedFolderProvider: SyncedFolderProvider, - val viewThemeUtils: ViewThemeUtils, -) : CoroutineWorker (context, params) { + val viewThemeUtils: ViewThemeUtils +) : CoroutineWorker(context, params) { companion object { const val SYNCED_FOLDER_IDS = "synced_folder_IDs" @@ -92,4 +92,4 @@ class AutoUploadLocalDeletionWorker( .build() private fun showNotification(notification: Notification) = notificationManager.showNotification() -} \ No newline at end of file +} diff --git a/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt b/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt index 7adce8f032f4..9cf91603c846 100644 --- a/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt +++ b/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt @@ -61,7 +61,7 @@ class DeleteUploadedFileOperation( } val parentFolderRemotePath = ocFile.parentRemotePath - if (/*syncedFolder.isSubfolderByDate &&*/ parentFolderRemotePath !in refreshedFolders) { + if (parentFolderRemotePath !in refreshedFolders) { // Files are stored in subfolder by date on the server. // Refresh only subfolders containing one of the files to be checked val subFolder = storageManager.getFileByRemotePath(parentFolderRemotePath) @@ -113,7 +113,7 @@ class DeleteUploadedFileOperation( } // File deletion - val deleted = true //localFile.delete() + val deleted = true // localFile.delete() if (deleted) { Log_OC.i(TAG, "Deleted file ${localFile.name}") } else { diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ManageSpaceActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/ManageSpaceActivity.kt index be8764a6f90c..6050426a8393 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/ManageSpaceActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/ManageSpaceActivity.kt @@ -60,7 +60,6 @@ class ManageSpaceActivity : setContentView(binding.root) binding.run { - manageActivityToolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() } diff --git a/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.kt b/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.kt index e00b30bce679..f0bc12570be6 100644 --- a/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.kt +++ b/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.kt @@ -55,20 +55,14 @@ object FilesSyncHelper { } @JvmStatic - fun startLocalDeletionForEnabledSyncedFolders( - provider: SyncedFolderProvider, - manager: BackgroundJobManager - ) { + fun startLocalDeletionForEnabledSyncedFolders(provider: SyncedFolderProvider, manager: BackgroundJobManager) { Log_OC.d(TAG, "start local deletion worker for each enabled folder") manager.locallyDeleteAutoUploadedFiles(provider.syncedFolders) } @JvmStatic - fun startLocalDeletionForSyncedFolder( - folder: SyncedFolder, - manager: BackgroundJobManager - ) { + fun startLocalDeletionForSyncedFolder(folder: SyncedFolder, manager: BackgroundJobManager) { Log_OC.d(TAG, "start local deletion worker for folder ${folder.localPath}") manager.locallyDeleteAutoUploadedFiles(listOf(folder)) From bbeb49d0e08d4f80ce85b38157c0b8c0dc7ff02d Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Mon, 24 Aug 2026 18:25:38 +0200 Subject: [PATCH 14/25] feature/1475-delete-uploaded-files: Manage non-writeable folders Signed-off-by: daniele-verducci --- .../client/jobs/BackgroundJobManagerImpl.kt | 4 +++- .../autoUpload/AutoUploadLocalDeletionWorker.kt | 6 +++++- .../android/ui/adapter/SyncedFolderAdapter.kt | 16 ++++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) 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 4d7a6f41b030..946813296fa8 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt @@ -828,7 +828,9 @@ internal class BackgroundJobManagerImpl( } override fun locallyDeleteAutoUploadedFiles(syncedFolders: List) { - val syncedFolderIDs = syncedFolders.map { it.id } + val syncedFolderIDs = syncedFolders + .filter { it.isEnabled } + .map { it.id } val arguments = Data.Builder() .putLongArray(AutoUploadLocalDeletionWorker.SYNCED_FOLDER_IDS, syncedFolderIDs.toLongArray()) diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt index e5d13e15fbfd..4f821a8d73c2 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt @@ -20,7 +20,9 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.operations.upload.DeleteUploadedFileOperation import com.owncloud.android.ui.notifications.NotificationUtils +import com.owncloud.android.utils.FileUtil import com.owncloud.android.utils.theme.ViewThemeUtils +import java.io.File class AutoUploadLocalDeletionWorker( private val context: Context, @@ -54,11 +56,13 @@ class AutoUploadLocalDeletionWorker( val syncedFolderIDs = inputData.getLongArray(SYNCED_FOLDER_IDS) ?: throw IllegalArgumentException("$SYNCED_FOLDER_IDS param is mandatory") - val syncedFolders = syncedFolderIDs.map { syncedFolderProvider.getSyncedFolderByID(it) } + val syncedFolders = syncedFolderIDs + .map { syncedFolderProvider.getSyncedFolderByID(it) } syncedFolders .filterNotNull() .filter { it.isEnabled } + .filter { FileUtil.isFolderWritable(File(it.localPath)) } .forEach { val op = DeleteUploadedFileOperation( it, diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/SyncedFolderAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/SyncedFolderAdapter.kt index 28d21ba22151..67b92725c408 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/SyncedFolderAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/SyncedFolderAdapter.kt @@ -40,6 +40,7 @@ import com.owncloud.android.datamodel.SyncedFolderDisplayItem import com.owncloud.android.datamodel.ThumbnailsCacheManager import com.owncloud.android.datamodel.ThumbnailsCacheManager.AsyncMediaThumbnailDrawable import com.owncloud.android.datamodel.ThumbnailsCacheManager.MediaThumbnailGenerationTask +import com.owncloud.android.utils.FileUtil import com.owncloud.android.utils.theme.ViewThemeUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -382,6 +383,10 @@ class SyncedFolderAdapter( menu .findItem(R.id.action_auto_upload_folder_toggle_visibility) .setChecked(item.isHidden) + updateDeletionMenuItem( + item, + menu.findItem(R.id.action_auto_upload_folder_delete_uploaded) + ) } popup.show() @@ -543,6 +548,17 @@ class SyncedFolderAdapter( } } + private fun updateDeletionMenuItem(syncedFolder: SyncedFolderDisplayItem, menuItem: MenuItem) { + val folderFile = syncedFolder.localPath?.let { File(it) } + lifecycleScope.launch { + val writable = FileUtil.isFolderWritable(folderFile) + withContext(Dispatchers.Main) { + menuItem.isEnabled = writable + menuItem.isVisible = writable + } + } + } + companion object { private const val VIEW_TYPE_EMPTY = Int.MAX_VALUE private const val VIEW_TYPE_ITEM = 1 From 3000ee91afe056f79cd2c5f11e58b444f770491a Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Tue, 25 Aug 2026 14:26:39 +0200 Subject: [PATCH 15/25] feature/1475-delete-uploaded-files: Fix user race condition Signed-off-by: daniele-verducci --- .../com/nextcloud/client/jobs/BackgroundJobFactory.kt | 3 --- .../jobs/autoUpload/AutoUploadLocalDeletionWorker.kt | 3 ++- .../nextcloud/client/jobs/autoUpload/SyncFolderHelper.kt | 8 ++++++++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt index 49fdd9705a2d..661aa8b332df 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt @@ -35,7 +35,6 @@ import com.nextcloud.client.logger.Logger import com.nextcloud.client.network.ConnectivityService import com.nextcloud.client.preferences.AppPreferences import com.owncloud.android.datamodel.ArbitraryDataProvider -import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.SyncedFolderProvider import com.owncloud.android.datamodel.UploadsStorageManager import com.owncloud.android.operations.factory.UploadFileOperationFactory @@ -61,7 +60,6 @@ class BackgroundJobFactory @Inject constructor( private val resources: Resources, private val arbitraryDataProvider: ArbitraryDataProvider, private val uploadsStorageManager: UploadsStorageManager, - private val fileDataStorageManager: FileDataStorageManager, private val connectivityService: ConnectivityService, private val notificationManager: NotificationManager, private val eventBus: EventBus, @@ -323,7 +321,6 @@ class BackgroundJobFactory @Inject constructor( context = context, params = params, userAccountManager = accountManager, - fileDataStorageManager = fileDataStorageManager, syncedFolderProvider = syncedFolderProvider, viewThemeUtils = viewThemeUtils.get() ) diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt index 4f821a8d73c2..d8e74fb8a722 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt @@ -28,7 +28,6 @@ class AutoUploadLocalDeletionWorker( private val context: Context, params: WorkerParameters, private val userAccountManager: UserAccountManager, - private val fileDataStorageManager: FileDataStorageManager, private val syncedFolderProvider: SyncedFolderProvider, val viewThemeUtils: ViewThemeUtils ) : CoroutineWorker(context, params) { @@ -64,6 +63,8 @@ class AutoUploadLocalDeletionWorker( .filter { it.isEnabled } .filter { FileUtil.isFolderWritable(File(it.localPath)) } .forEach { + val sharedFolderOwner = userAccountManager.getUser(it.account).get() + val fileDataStorageManager = FileDataStorageManager(sharedFolderOwner, context.contentResolver) val op = DeleteUploadedFileOperation( it, context, diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/SyncFolderHelper.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/SyncFolderHelper.kt index bff89554fb55..b8553cb4e661 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/SyncFolderHelper.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/SyncFolderHelper.kt @@ -29,6 +29,14 @@ class SyncFolderHelper(private val context: Context) { private const val TAG = "SyncFolderHelper" } + /** + * Computes the auto upload remote path for a given file based on the current syncedFolder folder settings. + * Note that if the user changed the syncedFolder's settings after the file was already uploaded, + * this may not reflect the actual uploaded file's path. + * @param syncedFolder containing the file + * @param file contained in the syncedFolder + * @return the remote path based on the current syncedFolder folder settings + */ fun getAutoUploadRemotePath(syncedFolder: SyncedFolder, file: File): String { val resources = context.resources val isLightVersion = resources.getBoolean(R.bool.syncedFolder_light) From a10dfa101f2c95c08c8257329063fcc5421b6d9b Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Wed, 26 Aug 2026 16:59:17 +0200 Subject: [PATCH 16/25] feature/1475-delete-uploaded-files: Stats Signed-off-by: daniele-verducci --- .../AutoUploadLocalDeletionWorker.kt | 50 +++++++++++++++++-- .../upload/DeleteUploadedFileOperation.kt | 42 +++++++++++++--- app/src/main/res/values/strings.xml | 2 + 3 files changed, 83 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt index d8e74fb8a722..b008e8f6dcbf 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt @@ -20,6 +20,7 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.operations.upload.DeleteUploadedFileOperation import com.owncloud.android.ui.notifications.NotificationUtils +import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.FileUtil import com.owncloud.android.utils.theme.ViewThemeUtils import java.io.File @@ -58,6 +59,10 @@ class AutoUploadLocalDeletionWorker( val syncedFolders = syncedFolderIDs .map { syncedFolderProvider.getSyncedFolderByID(it) } + var filesPreserved = 0L + var foldersAnalyzed = 0L + var filesRemoved = 0L + var spaceFreed = 0L syncedFolders .filterNotNull() .filter { it.isEnabled } @@ -78,16 +83,55 @@ class AutoUploadLocalDeletionWorker( ) return Result.failure() } + foldersAnalyzed ++ + filesPreserved += res.resultData.filesPreserved + filesRemoved += res.resultData.filesRemoved + spaceFreed += res.resultData.spaceFreed } + + showNotification( - createNotification(context.getString(R.string.autoupload_delete_uploaded_notif_ended_title)) + createSuccessNotification( + foldersAnalyzed, + filesRemoved, + filesPreserved, + spaceFreed + ) ) - Log_OC.d(TAG, "Success") + Log_OC.d(TAG, "Success: foldersAnalyzed=$foldersAnalyzed, filesPreserved=$filesPreserved, " + + "filesRemoved=$filesRemoved, spaceFreed=$spaceFreed bytes") return Result.success() } - private fun createNotification(title: String): Notification = notificationManager.notificationBuilder + private fun createSuccessNotification( + foldersRemoved: Long, + filesRemoved: Long, + filesPreserved: Long, + spaceFreed: Long + ): Notification { + var notificationContent = context.getString( + R.string.autoupload_delete_uploaded_notif_ended_content, + DisplayUtils.bytesToHumanReadable(spaceFreed), + filesRemoved, + foldersRemoved + ) + if (filesPreserved > 0) { + notificationContent += + "\n" + + context.getString( + R.string.autoupload_delete_uploaded_notif_ended_content_preserved, + filesPreserved + ) + } + return createNotification( + title = context.getString(R.string.autoupload_delete_uploaded_notif_ended_title), + content = notificationContent + ) + } + + private fun createNotification(title: String, content: String? = null): Notification = notificationManager.notificationBuilder .setContentTitle(title) + .setContentText(content) .setSmallIcon(R.drawable.ic_delete) .setSound(null) .setVibrate(null) diff --git a/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt b/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt index 9cf91603c846..fe2cb95fe18f 100644 --- a/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt +++ b/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt @@ -31,30 +31,34 @@ class DeleteUploadedFileOperation( } private val syncFolderHelper = SyncFolderHelper(context) - suspend fun run(): RemoteOperationResult<*> { + suspend fun run(): RemoteOperationResult { Log_OC.d(TAG, "StorageManager user is ${storageManager.user}") // Obtain synced folder data val folder = storageManager.getFileByRemotePath(syncedFolder.remotePath) ?: storageManager.getFileByLocalPath(syncedFolder.localPath) if (folder == null) { Log_OC.e(TAG, "Unable to obtain remote folder to refresh metadata") - return RemoteOperationResult(RemoteOperationResult.ResultCode.METADATA_NOT_FOUND) + return RemoteOperationResult(RemoteOperationResult.ResultCode.METADATA_NOT_FOUND) } // Refresh synced folder metadata val metadataRefreshSuccess = refreshFolder(folder, storageManager) if (!metadataRefreshSuccess) { Log_OC.e(TAG, "Unable to refresh folder metadata") - return RemoteOperationResult(RemoteOperationResult.ResultCode.METADATA_NOT_FOUND) + return RemoteOperationResult(RemoteOperationResult.ResultCode.METADATA_NOT_FOUND) } val refreshedFolders = hashSetOf(folder.remotePath) val localFolder = File(syncedFolder.localPath) val files = SyncedFolderUtils.getFileList(localFolder) + + var filesPreserved = 0L + var filesRemoved = 0L + var spaceFreed = 0L + files.forEach { localFile -> val remotePath = syncFolderHelper.getAutoUploadRemotePath(syncedFolder, localFile) val ocFile = storageManager.getFileByRemotePath(remotePath) - ?: storageManager.getFileByLocalPath(localFile.absolutePath) if (ocFile == null) { Log_OC.i(TAG, "Unable to compare file ${localFile.name} with its remote counterpart, leaving in place") return@forEach @@ -67,17 +71,16 @@ class DeleteUploadedFileOperation( val subFolder = storageManager.getFileByRemotePath(parentFolderRemotePath) if (subFolder == null) { Log_OC.e(TAG, "Subfolder $parentFolderRemotePath not found on the server") - return RemoteOperationResult(RemoteOperationResult.ResultCode.METADATA_NOT_FOUND) + return RemoteOperationResult(RemoteOperationResult.ResultCode.METADATA_NOT_FOUND) } val metadataRefreshSuccess = refreshFolder(subFolder, storageManager) if (!metadataRefreshSuccess) { Log_OC.e(TAG, "Unable to refresh folder metadata for $parentFolderRemotePath") - return RemoteOperationResult(RemoteOperationResult.ResultCode.METADATA_NOT_FOUND) + return RemoteOperationResult(RemoteOperationResult.ResultCode.METADATA_NOT_FOUND) } } // Check the file wasn't modified after uploading - // TODO: Is this redundant? val localLastMod = localFile.lastModified() val lastSyncDate = ocFile.lastSyncDateForProperties if (lastSyncDate < localLastMod) { @@ -86,6 +89,7 @@ class DeleteUploadedFileOperation( "File ${localFile.name} has been modified ($localLastMod " + "after it was synced ($lastSyncDate), leaving in place" ) + filesPreserved ++ return@forEach } @@ -97,6 +101,7 @@ class DeleteUploadedFileOperation( "Local and remote mod date differs for file file ${localFile.name}: " + "$localLastMod : $remoteLastMod, leaving in place" ) + filesPreserved ++ return@forEach } @@ -109,6 +114,7 @@ class DeleteUploadedFileOperation( "Local and remote file sizes differs for file ${localFile.name}: " + "$localSize : $remoteSize, leaving in place" ) + filesPreserved ++ return@forEach } @@ -116,12 +122,20 @@ class DeleteUploadedFileOperation( val deleted = true // localFile.delete() if (deleted) { Log_OC.i(TAG, "Deleted file ${localFile.name}") + filesRemoved ++ + spaceFreed += localSize } else { Log_OC.e(TAG, "Error deleting file ${localFile.name}") } } - return RemoteOperationResult(RemoteOperationResult.ResultCode.OK) + val result = RemoteOperationResult(RemoteOperationResult.ResultCode.OK) + result.resultData = Stats( + filesPreserved, + filesRemoved, + spaceFreed + ) + return result } private suspend fun refreshFolder(folder: OCFile, storageManager: FileDataStorageManager): Boolean = @@ -141,4 +155,16 @@ class DeleteUploadedFileOperation( false } } + + /** + * Contains the statistics about the run + * @param filesPreserved Files not deleted from the device + * @param filesRemoved Files deleted from the device + * @param spaceFreed Space freed deleting files on the device, in bytes + */ + data class Stats( + val filesPreserved: Long, + val filesRemoved: Long, + val spaceFreed: Long + ) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d324f039f2ef..853e14054a7e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -886,6 +886,8 @@ Started to locally delete already uploaded files Locally deleted already uploaded files Unable to locally delete already uploaded files + Freed up %1$s removing %2$d files across %3$d folders. + %1$d files have been left in place because their remote counterpart could not be safely identified. Configure folders Locally delete uploaded files From 69d46e0b4df47f9cc8a7a81e85e5e417f162eb20 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Wed, 26 Aug 2026 17:13:38 +0200 Subject: [PATCH 17/25] feature/1475-delete-uploaded-files: Fixed untouched files report Signed-off-by: daniele-verducci --- .../android/operations/upload/DeleteUploadedFileOperation.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt b/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt index fe2cb95fe18f..2b6c6b11f92c 100644 --- a/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt +++ b/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt @@ -61,6 +61,7 @@ class DeleteUploadedFileOperation( val ocFile = storageManager.getFileByRemotePath(remotePath) if (ocFile == null) { Log_OC.i(TAG, "Unable to compare file ${localFile.name} with its remote counterpart, leaving in place") + filesPreserved ++ return@forEach } From 312ab7afc0c4c0c93618dca09ad96534ca1a9288 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Wed, 26 Aug 2026 17:14:10 +0200 Subject: [PATCH 18/25] feature/1475-delete-uploaded-files: Better user notifications Signed-off-by: daniele-verducci --- .../com/owncloud/android/ui/activity/ManageSpaceActivity.kt | 4 ++++ app/src/main/res/values/strings.xml | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ManageSpaceActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/ManageSpaceActivity.kt index 6050426a8393..153c5bc3e567 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/ManageSpaceActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/ManageSpaceActivity.kt @@ -9,6 +9,7 @@ package com.owncloud.android.ui.activity import android.os.Bundle import android.view.MenuItem +import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope import androidx.work.WorkManager @@ -168,6 +169,9 @@ class ManageSpaceActivity : private fun clearAutoUploadData() { FilesSyncHelper.startLocalDeletionForEnabledSyncedFolders(syncedFolderProvider, backgroundJobManager) + Toast + .makeText(this, R.string.autoupload_delete_uploaded_notif_started_title, Toast.LENGTH_SHORT) + .show() } override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 853e14054a7e..6b89c10f27b6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -883,11 +883,11 @@ Configure Locally delete uploaded files Local files deletion - Started to locally delete already uploaded files + Started to locally delete already uploaded files in background Locally deleted already uploaded files Unable to locally delete already uploaded files Freed up %1$s removing %2$d files across %3$d folders. - %1$d files have been left in place because their remote counterpart could not be safely identified. + %1$d files have been left in place because not (yet) uploaded or modified. Configure folders Locally delete uploaded files From 858a5c72e2365a08e7ebd4664d7ed8a6dbb0c42c Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 27 Aug 2026 12:53:12 +0200 Subject: [PATCH 19/25] feature/1475-delete-uploaded-files: spotless run Signed-off-by: daniele-verducci --- .../AutoUploadLocalDeletionWorker.kt | 31 ++++++++++--------- .../upload/DeleteUploadedFileOperation.kt | 20 ++++++------ .../ui/activity/SyncedFoldersActivity.kt | 12 +++---- 3 files changed, 29 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt index b008e8f6dcbf..48f9b957fdf4 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt @@ -83,7 +83,7 @@ class AutoUploadLocalDeletionWorker( ) return Result.failure() } - foldersAnalyzed ++ + foldersAnalyzed++ filesPreserved += res.resultData.filesPreserved filesRemoved += res.resultData.filesRemoved spaceFreed += res.resultData.spaceFreed @@ -118,10 +118,10 @@ class AutoUploadLocalDeletionWorker( if (filesPreserved > 0) { notificationContent += "\n" + - context.getString( - R.string.autoupload_delete_uploaded_notif_ended_content_preserved, - filesPreserved - ) + context.getString( + R.string.autoupload_delete_uploaded_notif_ended_content_preserved, + filesPreserved + ) } return createNotification( title = context.getString(R.string.autoupload_delete_uploaded_notif_ended_title), @@ -129,16 +129,17 @@ class AutoUploadLocalDeletionWorker( ) } - private fun createNotification(title: String, content: String? = null): Notification = notificationManager.notificationBuilder - .setContentTitle(title) - .setContentText(content) - .setSmallIcon(R.drawable.ic_delete) - .setSound(null) - .setVibrate(null) - .setOnlyAlertOnce(true) - .setSilent(true) - .setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_BACKGROUND_OPERATIONS) - .build() + private fun createNotification(title: String, content: String? = null): Notification = + notificationManager.notificationBuilder + .setContentTitle(title) + .setContentText(content) + .setSmallIcon(R.drawable.ic_delete) + .setSound(null) + .setVibrate(null) + .setOnlyAlertOnce(true) + .setSilent(true) + .setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_BACKGROUND_OPERATIONS) + .build() private fun showNotification(notification: Notification) = notificationManager.showNotification() } diff --git a/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt b/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt index 2b6c6b11f92c..8051454ee6fa 100644 --- a/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt +++ b/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt @@ -28,6 +28,8 @@ class DeleteUploadedFileOperation( companion object { const val TAG = "DeleteUploadedFileOperation" + + const val MS_IN_SECOND = 1000 } private val syncFolderHelper = SyncFolderHelper(context) @@ -61,7 +63,7 @@ class DeleteUploadedFileOperation( val ocFile = storageManager.getFileByRemotePath(remotePath) if (ocFile == null) { Log_OC.i(TAG, "Unable to compare file ${localFile.name} with its remote counterpart, leaving in place") - filesPreserved ++ + filesPreserved++ return@forEach } @@ -90,19 +92,19 @@ class DeleteUploadedFileOperation( "File ${localFile.name} has been modified ($localLastMod " + "after it was synced ($lastSyncDate), leaving in place" ) - filesPreserved ++ + filesPreserved++ return@forEach } // Check the file has same mod date. Note that the remote mod date is rounded to the second. val remoteLastMod = ocFile.modificationTimestamp - if (remoteLastMod / 1000 != localLastMod / 1000) { + if (remoteLastMod / MS_IN_SECOND != localLastMod / MS_IN_SECOND) { Log_OC.i( TAG, "Local and remote mod date differs for file file ${localFile.name}: " + "$localLastMod : $remoteLastMod, leaving in place" ) - filesPreserved ++ + filesPreserved++ return@forEach } @@ -115,7 +117,7 @@ class DeleteUploadedFileOperation( "Local and remote file sizes differs for file ${localFile.name}: " + "$localSize : $remoteSize, leaving in place" ) - filesPreserved ++ + filesPreserved++ return@forEach } @@ -123,7 +125,7 @@ class DeleteUploadedFileOperation( val deleted = true // localFile.delete() if (deleted) { Log_OC.i(TAG, "Deleted file ${localFile.name}") - filesRemoved ++ + filesRemoved++ spaceFreed += localSize } else { Log_OC.e(TAG, "Error deleting file ${localFile.name}") @@ -163,9 +165,5 @@ class DeleteUploadedFileOperation( * @param filesRemoved Files deleted from the device * @param spaceFreed Space freed deleting files on the device, in bytes */ - data class Stats( - val filesPreserved: Long, - val filesRemoved: Long, - val spaceFreed: Long - ) + data class Stats(val filesPreserved: Long, val filesRemoved: Long, val spaceFreed: Long) } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt index 61d7c7516419..57026e725013 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt @@ -620,11 +620,9 @@ class SyncedFoldersActivity : FilesSyncHelper.startLocalDeletionForEnabledSyncedFolders(syncedFolderProvider, backgroundJobManager) } - override fun onNeutral(callerTag: String?) { - } + override fun onNeutral(callerTag: String?) = Unit - override fun onCancel(callerTag: String?) { - } + override fun onCancel(callerTag: String?) = Unit }) showDialog(dialog, SYNCED_FOLDER_DELETE_ALL_UPLOADED_DIALOG_TAG) @@ -647,11 +645,9 @@ class SyncedFoldersActivity : FilesSyncHelper.startLocalDeletionForSyncedFolder(syncedFolderDisplayItem, backgroundJobManager) } - override fun onNeutral(callerTag: String?) { - } + override fun onNeutral(callerTag: String?) = Unit - override fun onCancel(callerTag: String?) { - } + override fun onCancel(callerTag: String?) = Unit }) showDialog(dialog, SYNCED_FOLDER_DELETE_UPLOADED_DIALOG_TAG) } From 3e1f092820c529daa17e5263a2363e239887b624 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 27 Aug 2026 13:03:25 +0200 Subject: [PATCH 20/25] feature/1475-delete-uploaded-files: show runtime in stats Signed-off-by: daniele-verducci --- .../AutoUploadLocalDeletionWorker.kt | 19 +++++--- .../upload/DeleteUploadedFileOperation.kt | 18 +++---- .../owncloud/android/utils/DisplayUtils.java | 47 +++++++++++++++++++ app/src/main/res/values/strings.xml | 7 ++- 4 files changed, 73 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt index 48f9b957fdf4..b4cb0cacc658 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt @@ -63,6 +63,7 @@ class AutoUploadLocalDeletionWorker( var foldersAnalyzed = 0L var filesRemoved = 0L var spaceFreed = 0L + val timeStarted = System.currentTimeMillis() syncedFolders .filterNotNull() .filter { it.isEnabled } @@ -89,17 +90,21 @@ class AutoUploadLocalDeletionWorker( spaceFreed += res.resultData.spaceFreed } - + val runTimeMs = System.currentTimeMillis() - timeStarted showNotification( createSuccessNotification( foldersAnalyzed, filesRemoved, filesPreserved, - spaceFreed + spaceFreed, + runTimeMs ) ) - Log_OC.d(TAG, "Success: foldersAnalyzed=$foldersAnalyzed, filesPreserved=$filesPreserved, " + - "filesRemoved=$filesRemoved, spaceFreed=$spaceFreed bytes") + Log_OC.d( + TAG, + "Success: foldersAnalyzed=$foldersAnalyzed, filesPreserved=$filesPreserved, " + + "filesRemoved=$filesRemoved, spaceFreed=$spaceFreed bytes, runTimeMs=$runTimeMs" + ) return Result.success() } @@ -107,13 +112,15 @@ class AutoUploadLocalDeletionWorker( foldersRemoved: Long, filesRemoved: Long, filesPreserved: Long, - spaceFreed: Long + spaceFreed: Long, + timeElapsed: Long ): Notification { var notificationContent = context.getString( R.string.autoupload_delete_uploaded_notif_ended_content, DisplayUtils.bytesToHumanReadable(spaceFreed), filesRemoved, - foldersRemoved + foldersRemoved, + DisplayUtils.unixTimeDurationToHumanReadable(context, timeElapsed) ) if (filesPreserved > 0) { notificationContent += diff --git a/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt b/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt index 8051454ee6fa..31fa4c17301b 100644 --- a/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt +++ b/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt @@ -33,6 +33,7 @@ class DeleteUploadedFileOperation( } private val syncFolderHelper = SyncFolderHelper(context) + @Suppress("ReturnCount") suspend fun run(): RemoteOperationResult { Log_OC.d(TAG, "StorageManager user is ${storageManager.user}") // Obtain synced folder data @@ -144,17 +145,12 @@ class DeleteUploadedFileOperation( private suspend fun refreshFolder(folder: OCFile, storageManager: FileDataStorageManager): Boolean = withContext(Dispatchers.IO) { val operation = RefreshFolderOperation(folder, storageManager, storageManager.user, context) - return@withContext try { - val result = operation.execute(storageManager.user, context) - if (result.isSuccess) { - Log_OC.d(TAG, "Successfully fetched metadata for: ${folder.remotePath}") - true - } else { - Log_OC.e(TAG, "Failed to fetch metadata for: ${folder.remotePath}") - false - } - } catch (e: Exception) { - Log_OC.e(TAG, "Exception refreshing folder ${folder.remotePath}: ${e.message}", e) + val result = operation.executeNextcloudClient(storageManager.user, context) + if (result.isSuccess) { + Log_OC.d(TAG, "Successfully fetched metadata for: ${folder.remotePath}") + true + } else { + Log_OC.e(TAG, "Failed to fetch metadata for: ${folder.remotePath}") false } } diff --git a/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java b/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java index 9519fdbd4d07..53825980e539 100644 --- a/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java +++ b/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java @@ -106,6 +106,11 @@ public final class DisplayUtils { public static final String MONTH_PATTERN = "MMMM"; public static final String YEAR_PATTERN = "yyyy"; + public static final long SECOND_IN_MS = 1000; + public static final long MINUTE_IN_MS = 60 * SECOND_IN_MS; + public static final long HOUR_IN_MS = 60 * MINUTE_IN_MS; + public static final long DAY_IN_MS = 24 * HOUR_IN_MS; + private DisplayUtils() { // utility class -> private constructor } @@ -149,6 +154,48 @@ public static String unixTimeToHumanReadable(long milliseconds) { return df.format(date); } + /** + * Converts Unix time duration to human readable format + * + * @param milliseconds that the operation did require + * @return The human readable time duration for the users locale + */ + public static String unixTimeDurationToHumanReadable(Context context, long milliseconds) { + long days = milliseconds / DAY_IN_MS; + long hours = (milliseconds % DAY_IN_MS) / HOUR_IN_MS; + long minutes = (milliseconds % HOUR_IN_MS) / MINUTE_IN_MS; + long seconds = (milliseconds % MINUTE_IN_MS) / SECOND_IN_MS; + + StringBuilder builder = new StringBuilder(); + + if (days > 0) { + builder.append(days); + builder.append(" "); + builder.append(context.getString(R.string.duration_days)); + builder.append(", "); + } + + if (hours > 0) { + builder.append(hours); + builder.append(" "); + builder.append(context.getString(R.string.duration_hours)); + builder.append(", "); + } + + if (minutes > 0) { + builder.append(minutes); + builder.append(" "); + builder.append(context.getString(R.string.duration_minutes)); + builder.append(", "); + } + + builder.append(seconds); + builder.append(" "); + builder.append(context.getString(R.string.duration_seconds)); + + return builder.toString(); + } + /** * beautifies a given URL by removing any http/https protocol prefix. * diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6b89c10f27b6..007f0b6bb800 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -886,7 +886,7 @@ Started to locally delete already uploaded files in background Locally deleted already uploaded files Unable to locally delete already uploaded files - Freed up %1$s removing %2$d files across %3$d folders. + Freed up %1$s removing %2$d files across %3$d folders. The operation required %4$s. %1$d files have been left in place because not (yet) uploaded or modified. Configure folders @@ -1565,4 +1565,9 @@ You do not have permission to change this label Governance Image details + + days + hours + minutes + seconds From 04d5325dce2ce5bcf2eaa04c07c9df4f8f0fa0c6 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 27 Aug 2026 15:08:20 +0200 Subject: [PATCH 21/25] feature/1475-delete-uploaded-files: more logs & user info, fix Signed-off-by: daniele-verducci --- .../client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt | 2 +- .../operations/upload/DeleteUploadedFileOperation.kt | 6 ++++-- app/src/main/res/values/strings.xml | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt index b4cb0cacc658..9ad3b77efa1c 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt @@ -37,7 +37,7 @@ class AutoUploadLocalDeletionWorker( const val SYNCED_FOLDER_IDS = "synced_folder_IDs" const val NOTIFICATION_ID = 267 - private const val TAG = "\uD83D\uDDD1\uFE0F AutoUploadLocalDeletionWorker" + private const val TAG = "AutoUploadLocalDeletionWorker" } private val notificationManager = WorkerNotificationManager( diff --git a/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt b/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt index 31fa4c17301b..a4783fa2ddd8 100644 --- a/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt +++ b/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt @@ -35,7 +35,8 @@ class DeleteUploadedFileOperation( @Suppress("ReturnCount") suspend fun run(): RemoteOperationResult { - Log_OC.d(TAG, "StorageManager user is ${storageManager.user}") + Log_OC.d(TAG, "Analyzing folder ${syncedFolder.remotePath}") + // Obtain synced folder data val folder = storageManager.getFileByRemotePath(syncedFolder.remotePath) ?: storageManager.getFileByLocalPath(syncedFolder.localPath) @@ -60,6 +61,7 @@ class DeleteUploadedFileOperation( var spaceFreed = 0L files.forEach { localFile -> + Log_OC.d(TAG, "Analyzing file $localFile from folder ${folder.remotePath}") val remotePath = syncFolderHelper.getAutoUploadRemotePath(syncedFolder, localFile) val ocFile = storageManager.getFileByRemotePath(remotePath) if (ocFile == null) { @@ -145,7 +147,7 @@ class DeleteUploadedFileOperation( private suspend fun refreshFolder(folder: OCFile, storageManager: FileDataStorageManager): Boolean = withContext(Dispatchers.IO) { val operation = RefreshFolderOperation(folder, storageManager, storageManager.user, context) - val result = operation.executeNextcloudClient(storageManager.user, context) + val result = operation.execute(storageManager.user, context) if (result.isSuccess) { Log_OC.d(TAG, "Successfully fetched metadata for: ${folder.remotePath}") true diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 007f0b6bb800..a53350d23751 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -894,7 +894,7 @@ All the files already uploaded to the cloud will be removed from the current device Locally delete uploaded files for all auto upload folders - All the files already uploaded to the cloud will be removed from the current device for all the autoupload folders + All the files already uploaded to the cloud will be removed from the current device for all the autoupload folders from all the configured users Test server connection From 6de4c1ab1f2aee4b331709fd3dbfea1f6524596b Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 27 Aug 2026 16:01:51 +0200 Subject: [PATCH 22/25] feature/1475-delete-uploaded-files: apply to current user or all users, added users in report Signed-off-by: daniele-verducci --- .../AutoUploadLocalDeletionWorker.kt | 7 ++++++- .../upload/DeleteUploadedFileOperation.kt | 2 +- .../ui/activity/SyncedFoldersActivity.kt | 16 +++++++++++----- .../owncloud/android/utils/FilesSyncHelper.kt | 18 +++++++++++++++++- app/src/main/res/values/strings.xml | 6 ++++-- 5 files changed, 39 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt index 9ad3b77efa1c..d833896f30df 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt @@ -59,6 +59,7 @@ class AutoUploadLocalDeletionWorker( val syncedFolders = syncedFolderIDs .map { syncedFolderProvider.getSyncedFolderByID(it) } + var users = HashSet() var filesPreserved = 0L var foldersAnalyzed = 0L var filesRemoved = 0L @@ -70,6 +71,7 @@ class AutoUploadLocalDeletionWorker( .filter { FileUtil.isFolderWritable(File(it.localPath)) } .forEach { val sharedFolderOwner = userAccountManager.getUser(it.account).get() + users.add(sharedFolderOwner.accountName) val fileDataStorageManager = FileDataStorageManager(sharedFolderOwner, context.contentResolver) val op = DeleteUploadedFileOperation( it, @@ -93,6 +95,7 @@ class AutoUploadLocalDeletionWorker( val runTimeMs = System.currentTimeMillis() - timeStarted showNotification( createSuccessNotification( + users.size, foldersAnalyzed, filesRemoved, filesPreserved, @@ -102,13 +105,14 @@ class AutoUploadLocalDeletionWorker( ) Log_OC.d( TAG, - "Success: foldersAnalyzed=$foldersAnalyzed, filesPreserved=$filesPreserved, " + + "Success: users=$users, foldersAnalyzed=$foldersAnalyzed, filesPreserved=$filesPreserved, " + "filesRemoved=$filesRemoved, spaceFreed=$spaceFreed bytes, runTimeMs=$runTimeMs" ) return Result.success() } private fun createSuccessNotification( + users: Int, foldersRemoved: Long, filesRemoved: Long, filesPreserved: Long, @@ -120,6 +124,7 @@ class AutoUploadLocalDeletionWorker( DisplayUtils.bytesToHumanReadable(spaceFreed), filesRemoved, foldersRemoved, + users, DisplayUtils.unixTimeDurationToHumanReadable(context, timeElapsed) ) if (filesPreserved > 0) { diff --git a/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt b/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt index a4783fa2ddd8..cd10b3a22b68 100644 --- a/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt +++ b/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt @@ -35,7 +35,7 @@ class DeleteUploadedFileOperation( @Suppress("ReturnCount") suspend fun run(): RemoteOperationResult { - Log_OC.d(TAG, "Analyzing folder ${syncedFolder.remotePath}") + Log_OC.d(TAG, "Analyzing folder ${syncedFolder.remotePath} from user ${syncedFolder.account}") // Obtain synced folder data val folder = storageManager.getFileByRemotePath(syncedFolder.remotePath) diff --git a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt index 57026e725013..80722f73042b 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt @@ -610,9 +610,9 @@ class SyncedFoldersActivity : null, R.string.autoupload_delete_uploaded_all_dialog_title, R.drawable.selector_trashbin, - R.string.common_ok, - R.string.common_cancel, - -1 + R.string.autoupload_delete_uploaded_all_dialog_button_all_users, + R.string.autoupload_delete_uploaded_all_dialog_button_current_user, + R.string.common_cancel ) dialog.isCancelable = false dialog.setOnConfirmationListener(object : ConfirmationDialogFragment.ConfirmationDialogFragmentListener { @@ -620,9 +620,15 @@ class SyncedFoldersActivity : FilesSyncHelper.startLocalDeletionForEnabledSyncedFolders(syncedFolderProvider, backgroundJobManager) } - override fun onNeutral(callerTag: String?) = Unit + override fun onCancel(callerTag: String?) { + FilesSyncHelper.startLocalDeletionForEnabledSyncedFolders( + syncedFolderProvider, + backgroundJobManager, + user.get() + ) + } - override fun onCancel(callerTag: String?) = Unit + override fun onNeutral(callerTag: String?) = Unit }) showDialog(dialog, SYNCED_FOLDER_DELETE_ALL_UPLOADED_DIALOG_TAG) diff --git a/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.kt b/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.kt index f0bc12570be6..570e7e9d39ef 100644 --- a/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.kt +++ b/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.kt @@ -9,6 +9,7 @@ */ package com.owncloud.android.utils +import com.nextcloud.client.account.User import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.device.PowerManagementService import com.nextcloud.client.jobs.BackgroundJobManager @@ -56,11 +57,26 @@ object FilesSyncHelper { @JvmStatic fun startLocalDeletionForEnabledSyncedFolders(provider: SyncedFolderProvider, manager: BackgroundJobManager) { - Log_OC.d(TAG, "start local deletion worker for each enabled folder") + Log_OC.d(TAG, "Start local deletion worker for each enabled folder by any user") manager.locallyDeleteAutoUploadedFiles(provider.syncedFolders) } + @JvmStatic + fun startLocalDeletionForEnabledSyncedFolders( + provider: SyncedFolderProvider, + manager: BackgroundJobManager, + user: User + ) { + Log_OC.d(TAG, "start local deletion worker for each enabled folder under user ${user.accountName}") + + manager.locallyDeleteAutoUploadedFiles( + provider.syncedFolders.filter { + it.account.equals(user.accountName) + } + ) + } + @JvmStatic fun startLocalDeletionForSyncedFolder(folder: SyncedFolder, manager: BackgroundJobManager) { Log_OC.d(TAG, "start local deletion worker for folder ${folder.localPath}") diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a53350d23751..b524ece1b2b7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -886,7 +886,7 @@ Started to locally delete already uploaded files in background Locally deleted already uploaded files Unable to locally delete already uploaded files - Freed up %1$s removing %2$d files across %3$d folders. The operation required %4$s. + Freed up %1$s removing %2$d files across %3$d folders from %4$d users. The operation required %5$s. %1$d files have been left in place because not (yet) uploaded or modified. Configure folders @@ -894,7 +894,9 @@ All the files already uploaded to the cloud will be removed from the current device Locally delete uploaded files for all auto upload folders - All the files already uploaded to the cloud will be removed from the current device for all the autoupload folders from all the configured users + All the files already uploaded to the cloud will be removed from the current device for all the autoupload folders + From all users + From current user Test server connection From 2e6e268365107702516c030ce8c62d42872dd01c Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 27 Aug 2026 17:18:52 +0200 Subject: [PATCH 23/25] feature/1475-delete-uploaded-files: fixed bug preventing removal of pictures uploaded before enabling subfolders by date Signed-off-by: daniele-verducci --- .../operations/upload/DeleteUploadedFileOperation.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt b/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt index cd10b3a22b68..eeb16bc062cd 100644 --- a/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt +++ b/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt @@ -63,7 +63,11 @@ class DeleteUploadedFileOperation( files.forEach { localFile -> Log_OC.d(TAG, "Analyzing file $localFile from folder ${folder.remotePath}") val remotePath = syncFolderHelper.getAutoUploadRemotePath(syncedFolder, localFile) - val ocFile = storageManager.getFileByRemotePath(remotePath) + val ocFile = + storageManager.getFileByRemotePath(remotePath) + // If file is null, try searching in the parent folder, + // in case it was uploaded before enabling isSubfolderByDate + ?: storageManager.getFileByRemotePath("${folder.remotePath}${localFile.name}") if (ocFile == null) { Log_OC.i(TAG, "Unable to compare file ${localFile.name} with its remote counterpart, leaving in place") filesPreserved++ @@ -104,7 +108,7 @@ class DeleteUploadedFileOperation( if (remoteLastMod / MS_IN_SECOND != localLastMod / MS_IN_SECOND) { Log_OC.i( TAG, - "Local and remote mod date differs for file file ${localFile.name}: " + + "Local and remote mod date differs for file ${localFile.name}: " + "$localLastMod : $remoteLastMod, leaving in place" ) filesPreserved++ From 6110dac680dbfb866c8868845575e26e73c0932f Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 27 Aug 2026 17:24:51 +0200 Subject: [PATCH 24/25] feature/1475-delete-uploaded-files: enabled real file deletion Signed-off-by: daniele-verducci --- .../android/operations/upload/DeleteUploadedFileOperation.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt b/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt index eeb16bc062cd..673d739b4e6c 100644 --- a/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt +++ b/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt @@ -129,7 +129,7 @@ class DeleteUploadedFileOperation( } // File deletion - val deleted = true // localFile.delete() + val deleted = localFile.delete() if (deleted) { Log_OC.i(TAG, "Deleted file ${localFile.name}") filesRemoved++ From 64ff7ccb2225350476113a846b268526c07dc45b Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Fri, 28 Aug 2026 14:47:13 +0200 Subject: [PATCH 25/25] feature/1475-delete-uploaded-files: changed some strings, simpler completion notification Signed-off-by: daniele-verducci --- .../autoUpload/AutoUploadLocalDeletionWorker.kt | 16 ++++------------ app/src/main/res/values/strings.xml | 7 +++---- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt index d833896f30df..ddf7a49e30ee 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt @@ -106,7 +106,8 @@ class AutoUploadLocalDeletionWorker( Log_OC.d( TAG, "Success: users=$users, foldersAnalyzed=$foldersAnalyzed, filesPreserved=$filesPreserved, " + - "filesRemoved=$filesRemoved, spaceFreed=$spaceFreed bytes, runTimeMs=$runTimeMs" + "filesRemoved=$filesRemoved, spaceFreed=$spaceFreed bytes, " + + "runTime=${DisplayUtils.unixTimeDurationToHumanReadable(context, runTimeMs)}" ) return Result.success() } @@ -119,22 +120,13 @@ class AutoUploadLocalDeletionWorker( spaceFreed: Long, timeElapsed: Long ): Notification { - var notificationContent = context.getString( + val notificationContent = context.getString( R.string.autoupload_delete_uploaded_notif_ended_content, DisplayUtils.bytesToHumanReadable(spaceFreed), filesRemoved, foldersRemoved, - users, - DisplayUtils.unixTimeDurationToHumanReadable(context, timeElapsed) + users ) - if (filesPreserved > 0) { - notificationContent += - "\n" + - context.getString( - R.string.autoupload_delete_uploaded_notif_ended_content_preserved, - filesPreserved - ) - } return createNotification( title = context.getString(R.string.autoupload_delete_uploaded_notif_ended_title), content = notificationContent diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b524ece1b2b7..a11a4eda8f56 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -768,7 +768,7 @@ Settings, database and server certificates from %1$s\'s data will be deleted permanently. \n\nDownloaded files will be kept untouched.\n\nThis process can take a while. Clear data Locally delete auto uploaded files - All files already uploaded to the server from the Auto Upload folders will be deleted locally to free up space in the device.\n\nThis process can take a while. + All files already uploaded to the server from the auto upload folders will be deleted locally to free up space on the device.\n\nThis process can take a while. Locally delete auto uploaded files File not found in local file system @@ -886,15 +886,14 @@ Started to locally delete already uploaded files in background Locally deleted already uploaded files Unable to locally delete already uploaded files - Freed up %1$s removing %2$d files across %3$d folders from %4$d users. The operation required %5$s. - %1$d files have been left in place because not (yet) uploaded or modified. + Freed up %1$s removing %2$d files across %3$d folders from %4$d users Configure folders Locally delete uploaded files All the files already uploaded to the cloud will be removed from the current device Locally delete uploaded files for all auto upload folders - All the files already uploaded to the cloud will be removed from the current device for all the autoupload folders + All the files already uploaded to the cloud will be removed from the current device for all the auto upload folders From all users From current user