From 80f3b69c6b9f92ba7568285c87ade15c905f441b Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Tue, 18 Aug 2026 17:55:35 +0200 Subject: [PATCH 01/33] 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 4acb64b38f68..b1f919d06fa0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -910,8 +910,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 1d72b071b5f713b2ba0b3ec8427078593d9516ab Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Wed, 19 Aug 2026 18:11:53 +0200 Subject: [PATCH 02/33] 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 964eda3d5a7c946c02ed74b9a15c0f841842d33d Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 20 Aug 2026 09:41:55 +0200 Subject: [PATCH 03/33] 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 59689586c9cd3da9abcd9127040a9c8e4a23a388 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 20 Aug 2026 15:37:10 +0200 Subject: [PATCH 04/33] 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 b1f919d06fa0..8301b6a2426f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -916,6 +916,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 6aec6dd706725cebc0647e6ddefdfae865bbda20 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 20 Aug 2026 17:30:08 +0200 Subject: [PATCH 05/33] 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 27793020bc0fbd6aa0759b1392b0f06d32a2fbc7 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 20 Aug 2026 18:07:12 +0200 Subject: [PATCH 06/33] 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 538347cc470c00a5ba6bbfed2d6754a8e7705b6d Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 20 Aug 2026 18:10:51 +0200 Subject: [PATCH 07/33] 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 b2bef4a86ab3cbfe96ad7de2ce98a684ff50a19a Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Fri, 21 Aug 2026 07:43:24 +0200 Subject: [PATCH 08/33] 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 0f03ff9b36ad7f7fea0678cef5751df1006400b5 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Fri, 21 Aug 2026 08:21:54 +0200 Subject: [PATCH 09/33] 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 8301b6a2426f..1ac23cfe098c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -798,6 +798,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 remove the selected files from the album? From 8a7eb8e96a288684f63504adab75af1ac4629209 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Fri, 21 Aug 2026 12:45:38 +0200 Subject: [PATCH 10/33] 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 e9eeabeb3270..4aa562956c40 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt @@ -177,4 +177,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 e0f6c5e98a8c..f379b2d8bd5f 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 @@ -104,6 +105,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" @@ -905,4 +907,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 cfec844cb89ce0cf8d321d6a2ee3c30ac879f106 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Fri, 21 Aug 2026 15:56:47 +0200 Subject: [PATCH 11/33] 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 f1e6d520ced5..518d39240b3c 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt @@ -24,6 +24,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 @@ -36,6 +37,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 @@ -61,6 +63,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, @@ -110,6 +113,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 } } @@ -328,4 +332,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 a6cc2369924b033e960a10ad582e3066775ba017 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Mon, 24 Aug 2026 17:10:00 +0200 Subject: [PATCH 12/33] 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 518d39240b3c..47ad3e059385 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt @@ -340,6 +340,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 1ac23cfe098c..94ae3167052a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -914,6 +914,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 880ef3b489cb0cf62e5e94e16c9bcd12a35da687 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Mon, 24 Aug 2026 17:18:53 +0200 Subject: [PATCH 13/33] 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 47ad3e059385..05d602b83d23 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt @@ -333,14 +333,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 5ee406f451dadc8b48d706d7528d8a76ea62ede4 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Mon, 24 Aug 2026 18:25:38 +0200 Subject: [PATCH 14/33] 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 f379b2d8bd5f..daba20131e20 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt @@ -909,7 +909,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 d90384b34b3f60d8f291c24b682747fe79907736 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Tue, 25 Aug 2026 14:26:39 +0200 Subject: [PATCH 15/33] 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 05d602b83d23..06fda4b00a25 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt @@ -37,7 +37,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 @@ -63,7 +62,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, @@ -340,7 +338,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 3b0efbb2a735d69df0c2f029b7ef3c4ec5e7de5f Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Wed, 26 Aug 2026 16:59:17 +0200 Subject: [PATCH 16/33] 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 94ae3167052a..f10276ed8974 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -918,6 +918,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 12f8c055ac66c833e1f81724b09626eaf597e723 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Wed, 26 Aug 2026 17:13:38 +0200 Subject: [PATCH 17/33] 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 cb2de5ca718e1f9b53c0e28c34bac845389e3aad Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Wed, 26 Aug 2026 17:14:10 +0200 Subject: [PATCH 18/33] 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 f10276ed8974..02b6ff1dc3dc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -915,11 +915,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 af9909421967e8b4ca7444e7a0ae1c361377d2d2 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 27 Aug 2026 12:53:12 +0200 Subject: [PATCH 19/33] 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 0a3d4d559e7e2b5e82f29fe984346c4e004b4233 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 27 Aug 2026 13:03:25 +0200 Subject: [PATCH 20/33] feature/1475-delete-uploaded-files: show runtime in stats Signed-off-by: daniele-verducci # Conflicts: # app/src/main/res/values/strings.xml --- .../AutoUploadLocalDeletionWorker.kt | 19 +++++--- .../upload/DeleteUploadedFileOperation.kt | 18 +++---- .../owncloud/android/utils/DisplayUtils.java | 47 +++++++++++++++++++ 3 files changed, 67 insertions(+), 17 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 a678575caba3..790c684e5c5c 100644 --- a/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java +++ b/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java @@ -105,6 +105,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 } @@ -148,6 +153,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. * From 011d83b508d80c56fcdbf39ee1beee624d63d3b4 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 27 Aug 2026 15:08:20 +0200 Subject: [PATCH 21/33] 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 02b6ff1dc3dc..21bd97fd7fdb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -926,7 +926,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 e5b56f3d931cc6b3f52baf444f135d9c834bab93 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 27 Aug 2026 16:01:51 +0200 Subject: [PATCH 22/33] 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 +++++++++++++++++- 4 files changed, 35 insertions(+), 8 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}") From c74248938a45af3d14821a75d4169991e3157593 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 27 Aug 2026 17:18:52 +0200 Subject: [PATCH 23/33] 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 5fbc88c21215ff217fb28fad7f8894ded2ee182d Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 27 Aug 2026 17:24:51 +0200 Subject: [PATCH 24/33] 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 65336213a62a5690a2f81c88839e766dc6f0d860 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Fri, 28 Aug 2026 14:47:13 +0200 Subject: [PATCH 25/33] feature/1475-delete-uploaded-files: changed some strings, simpler completion notification Signed-off-by: daniele-verducci --- .../autoUpload/AutoUploadLocalDeletionWorker.kt | 16 ++++------------ 1 file changed, 4 insertions(+), 12 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 From 4dfe198732588448e5095cc3846c6ed0852f609d Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Wed, 2 Sep 2026 11:08:17 +0200 Subject: [PATCH 26/33] feature/1475-delete-uploaded-files: Pluralized strings Signed-off-by: daniele-verducci --- .../AutoUploadLocalDeletionWorker.kt | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 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 ddf7a49e30ee..4bc313a52526 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 @@ -123,9 +123,21 @@ class AutoUploadLocalDeletionWorker( val notificationContent = context.getString( R.string.autoupload_delete_uploaded_notif_ended_content, DisplayUtils.bytesToHumanReadable(spaceFreed), - filesRemoved, - foldersRemoved, - users + context.resources.getQuantityString( + R.plurals.autoupload_delete_uploaded_notif_ended_content_files, + filesRemoved.toInt(), + filesRemoved + ), + context.resources.getQuantityString( + R.plurals.autoupload_delete_uploaded_notif_ended_content_folders, + foldersRemoved.toInt(), + foldersRemoved + ), + context.resources.getQuantityString( + R.plurals.autoupload_delete_uploaded_notif_ended_content_users, + users, + users + ), ) return createNotification( title = context.getString(R.string.autoupload_delete_uploaded_notif_ended_title), From 4b4391e1455e16ebedcfddaf5c11bd9f770461a0 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Wed, 2 Sep 2026 11:11:15 +0200 Subject: [PATCH 27/33] feature/1475-delete-uploaded-files: Simplified log, removed over engineered formatting function Signed-off-by: daniele-verducci --- .../AutoUploadLocalDeletionWorker.kt | 2 +- .../owncloud/android/utils/DisplayUtils.java | 42 ------------------- 2 files changed, 1 insertion(+), 43 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 4bc313a52526..2ae9b9e17084 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 @@ -107,7 +107,7 @@ class AutoUploadLocalDeletionWorker( TAG, "Success: users=$users, foldersAnalyzed=$foldersAnalyzed, filesPreserved=$filesPreserved, " + "filesRemoved=$filesRemoved, spaceFreed=$spaceFreed bytes, " + - "runTime=${DisplayUtils.unixTimeDurationToHumanReadable(context, runTimeMs)}" + "runTime=${runTimeMs/1000} seconds" ) return Result.success() } 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 790c684e5c5c..a60466a0d6b9 100644 --- a/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java +++ b/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java @@ -153,48 +153,6 @@ 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. * From 90ae4ed1e264e810f3873c611ff035ed4b501926 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Wed, 2 Sep 2026 11:57:59 +0200 Subject: [PATCH 28/33] feature/1475-delete-uploaded-files: Fix Detekt + lint issues Signed-off-by: daniele-verducci --- .../AutoUploadLocalDeletionWorker.kt | 6 +- .../upload/DeleteUploadedFileOperation.kt | 156 +++++++++--------- 2 files changed, 84 insertions(+), 78 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 2ae9b9e17084..778d290ce594 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 @@ -36,6 +36,7 @@ class AutoUploadLocalDeletionWorker( companion object { const val SYNCED_FOLDER_IDS = "synced_folder_IDs" const val NOTIFICATION_ID = 267 + const val MS_IN_SECOND = 1000 private const val TAG = "AutoUploadLocalDeletionWorker" } @@ -106,8 +107,7 @@ class AutoUploadLocalDeletionWorker( Log_OC.d( TAG, "Success: users=$users, foldersAnalyzed=$foldersAnalyzed, filesPreserved=$filesPreserved, " + - "filesRemoved=$filesRemoved, spaceFreed=$spaceFreed bytes, " + - "runTime=${runTimeMs/1000} seconds" + "filesRemoved=$filesRemoved, spaceFreed=$spaceFreed bytes, runTime=${runTimeMs / MS_IN_SECOND} seconds" ) return Result.success() } @@ -137,7 +137,7 @@ class AutoUploadLocalDeletionWorker( R.plurals.autoupload_delete_uploaded_notif_ended_content_users, users, users - ), + ) ) return createNotification( title = context.getString(R.string.autoupload_delete_uploaded_notif_ended_title), 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 673d739b4e6c..043d73fc9847 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,7 +33,7 @@ class DeleteUploadedFileOperation( } private val syncFolderHelper = SyncFolderHelper(context) - @Suppress("ReturnCount") + @Suppress("NestedBlockDepth") suspend fun run(): RemoteOperationResult { Log_OC.d(TAG, "Analyzing folder ${syncedFolder.remotePath} from user ${syncedFolder.account}") @@ -51,7 +51,6 @@ class DeleteUploadedFileOperation( 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) @@ -62,80 +61,26 @@ 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) - // 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++ - return@forEach - } - - val parentFolderRemotePath = ocFile.parentRemotePath - 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) - if (subFolder == null) { - Log_OC.e(TAG, "Subfolder $parentFolderRemotePath not found on the server") - return RemoteOperationResult(RemoteOperationResult.ResultCode.METADATA_NOT_FOUND) + try { + val fileCanBeDeleted = fileCanBeDeleted(localFile, folder) + + if (fileCanBeDeleted) { + val fileSize = localFile.length() + // File deletion + val deleted = localFile.delete() + if (deleted) { + Log_OC.i(TAG, "Deleted file ${localFile.name}") + filesRemoved++ + spaceFreed += fileSize + } else { + Log_OC.e(TAG, "Error deleting file ${localFile.name}") + } + } else { + filesPreserved++ } - 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 - 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" - ) - 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 / MS_IN_SECOND != localLastMod / MS_IN_SECOND) { - Log_OC.i( - TAG, - "Local and remote mod date differs for file ${localFile.name}: " + - "$localLastMod : $remoteLastMod, leaving in place" - ) - filesPreserved++ - 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" - ) - filesPreserved++ - return@forEach - } - - // File deletion - val deleted = 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}") + } catch (e: NoSuchElementException) { + Log_OC.e(TAG, e.toString()) + return RemoteOperationResult(RemoteOperationResult.ResultCode.METADATA_NOT_FOUND) } } @@ -148,6 +93,67 @@ class DeleteUploadedFileOperation( return result } + @Suppress("ReturnCount") + private suspend fun fileCanBeDeleted(localFile: File, folder: OCFile): Boolean { + val remotePath = syncFolderHelper.getAutoUploadRemotePath(syncedFolder, localFile) + val ocFile = + storageManager.getFileByRemotePath(remotePath) + // If file is null, search in the parent folder, in case it was uploaded before enabling subfolderByDate + ?: 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") + return false + } + + val refreshedFolders = hashSetOf(folder.remotePath) + val parentFolderRemotePath = ocFile.parentRemotePath + if (parentFolderRemotePath !in refreshedFolders) { + // Files are stored in subfolder by date on the server. Refresh only subfolders containing files to check + val subFolder = storageManager.getFileByRemotePath(parentFolderRemotePath) + ?: throw NoSuchElementException("Subfolder $parentFolderRemotePath not found on the server") + val metadataRefreshSuccess = refreshFolder(subFolder, storageManager) + if (!metadataRefreshSuccess) { + throw NoSuchElementException("Unable to refresh folder metadata for $parentFolderRemotePath") + } + } + + // Check the file wasn't modified after uploading + 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 false + } + + // Check the file has same mod date. Note that the remote mod date is rounded to the second. + val remoteLastMod = ocFile.modificationTimestamp + if (remoteLastMod / MS_IN_SECOND != localLastMod / MS_IN_SECOND) { + Log_OC.i( + TAG, + "Local and remote mod date differs for file ${localFile.name}: " + + "$localLastMod : $remoteLastMod, leaving in place" + ) + return false + } + + // 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 false + } + return true + } + private suspend fun refreshFolder(folder: OCFile, storageManager: FileDataStorageManager): Boolean = withContext(Dispatchers.IO) { val operation = RefreshFolderOperation(folder, storageManager, storageManager.user, context) From 57311cf2b838d66b1537f296bc1b886b304948c3 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Wed, 2 Sep 2026 12:09:59 +0200 Subject: [PATCH 29/33] feature/1475-delete-uploaded-files: Replace Toast with Snackbar Signed-off-by: daniele-verducci --- .../com/owncloud/android/ui/activity/ManageSpaceActivity.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 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 153c5bc3e567..2c75fcdc29d1 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,7 +9,6 @@ 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 @@ -169,8 +168,8 @@ class ManageSpaceActivity : private fun clearAutoUploadData() { FilesSyncHelper.startLocalDeletionForEnabledSyncedFolders(syncedFolderProvider, backgroundJobManager) - Toast - .makeText(this, R.string.autoupload_delete_uploaded_notif_started_title, Toast.LENGTH_SHORT) + Snackbar + .make(binding.root, R.string.autoupload_delete_uploaded_notif_started_title, Snackbar.LENGTH_SHORT) .show() } From 6d9fabb3982474227221dea5a2bc70341d4416dd Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Wed, 2 Sep 2026 13:01:26 +0200 Subject: [PATCH 30/33] feature/1475-delete-uploaded-files: Disabled codacy check for more streamlined flow 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 043d73fc9847..6e42ae8320b8 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,7 +33,7 @@ class DeleteUploadedFileOperation( } private val syncFolderHelper = SyncFolderHelper(context) - @Suppress("NestedBlockDepth") + @Suppress("NestedBlockDepth", "ReturnCount") suspend fun run(): RemoteOperationResult { Log_OC.d(TAG, "Analyzing folder ${syncedFolder.remotePath} from user ${syncedFolder.account}") From c9917ded8e4eba2ed65c1fdb14a8239e9ebcd4fb Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 2 Sep 2026 14:50:16 +0200 Subject: [PATCH 31/33] wip Signed-off-by: alperozturk96 --- .../main/java/com/owncloud/android/utils/DisplayUtils.java | 5 ----- 1 file changed, 5 deletions(-) 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 a60466a0d6b9..a678575caba3 100644 --- a/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java +++ b/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java @@ -105,11 +105,6 @@ 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 } From 66e1f00d4243e6b92a7a90175d5628bb414ec593 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 2 Sep 2026 14:54:15 +0200 Subject: [PATCH 32/33] wip Signed-off-by: alperozturk96 --- app/src/main/res/values/strings.xml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 21bd97fd7fdb..4143a426b392 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -909,6 +909,20 @@ File could not be synced. Showing latest available version. + From all users + From current user + + %d file + %d files + + + %d folder + %d folders + + + %d user + %d users + Set up a custom folder Create new custom folder setup Hide folder From 14a7e87bdd49e3d69d31797015107ef8fc1e1490 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 3 Sep 2026 09:49:07 +0200 Subject: [PATCH 33/33] feature/1475-delete-uploaded-files: fix lint Signed-off-by: daniele-verducci --- app/src/main/res/values/strings.xml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4143a426b392..62321a715267 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -932,8 +932,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. - %1$d files have been left in place because not (yet) uploaded or modified. + Freed up %1$s removing %2$s across %3$s from %4$s. Configure folders Locally delete uploaded files