From 7c0bcef69685a22ca31e5053e841bd78171f9be3 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 dd66ce23c604..9e078f400397 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 @@ -617,6 +617,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 8cae254d2abc..122e7529de7f 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 3e12905176eee394411869cb4fea2125c8b76720 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 9e078f400397..57c750d23865 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 @@ -630,7 +632,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 a88f115e65c2aa60bc0328f777712b2035d7dbd7 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 57c750d23865..3e8cf14a866d 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 @@ -645,15 +645,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 7143e08b7e0b97e2fd34ae8cdc0fb728fa13e4c1 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 3e8cf14a866d..2dae03ef856e 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 @@ -591,6 +593,8 @@ class SyncedFoldersActivity : result = super.onOptionsItemSelected(item) } + R.id.action_auto_upload_all_folders_delete_uploaded -> onAllSyncFolderDeleteUploadedClick() + else -> result = super.onOptionsItemSelected(item) } return result @@ -604,22 +608,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, @@ -632,38 +654,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?) { @@ -672,6 +663,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) ) { @@ -679,12 +674,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 122e7529de7f..bee9fffb809e 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 e40a3a45deedc09e78f87c78a9118754a845e02a 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 2dae03ef856e..734f20bde02d 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 @@ -682,32 +682,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 25ed0dadf11e6a71907c2ed7bb077222052a9ffc 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 ee851072ad324f14b19b66783aa371149f9189e1 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 f8a842a1c05fe2f48b90e25510d880bb8c49ae74 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 1d34e1c5481a56368a86eeee27016302cb62d28e 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 734f20bde02d..f3b6612fb4e4 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 @@ -680,8 +680,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 bee9fffb809e..a9778428ced8 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 3d1bfa92ae77c9fe978f566878edf711eaf47534 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 3a951e17ab90..35d0c90e8513 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt @@ -175,4 +175,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 81709351ab86..b0a3a46705af 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" @@ -848,4 +850,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 f3b6612fb4e4..00736a6c9417 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 @@ -624,10 +624,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?) { @@ -654,7 +651,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?) { @@ -680,19 +677,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 7e0e8f9fdbb7..4ece3c315a54 100644 --- a/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.kt +++ b/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.kt @@ -15,6 +15,7 @@ import com.nextcloud.client.jobs.BackgroundJobManager import com.nextcloud.client.jobs.autoUpload.AutoUploadRequestResult 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 @@ -75,4 +76,24 @@ object FilesSyncHelper { AutoUploadRequestResult.STARTED } } + + @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 88b21e965c36b25492f7d8477604001f010baf61 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 30f53715504daa91aaad6c1163425563eb917c8c 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 a9778428ced8..99edb2da51bd 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 a60a92252a3967693b66ca8cc5ed481cf2ac42d9 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 4ece3c315a54..27182692fb74 100644 --- a/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.kt +++ b/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.kt @@ -78,20 +78,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 9a6f249e29e8bb87ca1cb620641dc050ea717887 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 b0a3a46705af..2816290e45ea 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt @@ -852,7 +852,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 fd2ff291892a64d6337ce6a6b48ff23016b5cfc0 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 30e4236f23e7406605fea9c7e2a156aebe094874 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 99edb2da51bd..d8ccdf202f38 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 ef181570e4b824392e67ad2ca36e72cc6c665bd0 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 2061caca17677ebecf5f2d1a78d18b460a1cd795 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 d8ccdf202f38..82f1ff227900 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 ba15e8364ca2d31874ad12072b7ba2de1d115845 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 00736a6c9417..3d20fd948607 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 @@ -627,11 +627,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) @@ -654,11 +652,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 91cf3182363ce84a4a9c6c1e4076eaddf50369f8 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 82f1ff227900..a81c0525783f 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 @@ -1569,4 +1569,9 @@ You do not have permission to change this label Governance Image details + + days + hours + minutes + seconds From e13be39df2dd0879a0e1205bea00098df62dd27c 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 a81c0525783f..37a9156f9cfb 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 0beac4f677f5afdd0401782488dbbb8f3b655df4 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 3d20fd948607..ddb2b64c03c2 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 @@ -617,9 +617,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 { @@ -627,9 +627,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 27182692fb74..673040d0b65b 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 @@ -79,11 +80,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 37a9156f9cfb..9f29e6a2eccd 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 4ab3bcabdb0d908e052257eb828c4c8a44edd97d 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 93e1fe508bbd8ab090175e07b6e0ceb575d4b815 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 dc5dd3466842d0623f476a502214d03777892900 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 9f29e6a2eccd..b0f613c052e6 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