From 6705afcbf56d8177c9d213ecf0bff027a2460379 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Tue, 18 Aug 2026 17:55:35 +0200 Subject: [PATCH 01/14] feature/1475-delete-uploaded-files: Menu item and popup Signed-off-by: daniele-verducci --- .../ui/activity/SyncedFoldersActivity.kt | 35 +++++++++++++++++++ .../android/ui/adapter/SyncedFolderAdapter.kt | 15 +++++--- .../main/res/menu/synced_folders_adapter.xml | 4 +++ app/src/main/res/values/strings.xml | 4 +++ 4 files changed, 53 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt index 967c1995390d..ce0bdd152f0d 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt @@ -610,6 +610,41 @@ class SyncedFoldersActivity : } } + override fun onSyncFolderDeleteUploadedClick(section: Int, syncedFolderDisplayItem: SyncedFolderDisplayItem?) { + val dialog = ConfirmationDialogFragment.newInstance( + R.string.autoupload_delete_uploaded_dialog_description, + null, + R.string.autoupload_delete_uploaded_dialog_title, + R.drawable.selector_trashbin, + R.string.common_ok, + R.string.common_cancel, + -1 + ) + dialog.isCancelable = false + dialog.setOnConfirmationListener(object : ConfirmationDialogFragment.ConfirmationDialogFragmentListener { + override fun onConfirmation(callerTag: String?) { + TODO("Not yet implemented") + } + + override fun onNeutral(callerTag: String?) { + } + + override fun onCancel(callerTag: String?) { + } + }) + if (isDialogFragmentReady(dialog) && + lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED) + ) { + val fragmentTransaction = supportFragmentManager + .beginTransaction() + .addToBackStack(null) + + dialog.show(fragmentTransaction, SYNCED_FOLDER_PREFERENCES_DIALOG_TAG) + } else { + Log_OC.d(TAG, "SyncFolderDeleteUploaded dialog not ready") + } + } + override fun onVisibilityToggleClick(section: Int, syncedFolder: SyncedFolderDisplayItem?) { if (syncedFolder == null) return diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/SyncedFolderAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/SyncedFolderAdapter.kt index 126af0068702..28d21ba22151 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/SyncedFolderAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/SyncedFolderAdapter.kt @@ -388,11 +388,15 @@ class SyncedFolderAdapter( } private fun optionsItemSelected(menuItem: MenuItem, section: Int, item: SyncedFolderDisplayItem): Boolean { - if (menuItem.itemId == R.id.action_auto_upload_folder_toggle_visibility) { - clickListener.onVisibilityToggleClick(section, item) - } else { - // default: R.id.action_create_custom_folder - clickListener.onSyncFolderSettingsClick(section, item) + when (menuItem.itemId) { + R.id.action_auto_upload_folder_toggle_visibility -> clickListener.onVisibilityToggleClick(section, item) + + R.id.action_auto_upload_folder_settings -> clickListener.onSyncFolderSettingsClick(section, item) + + R.id.action_auto_upload_folder_delete_uploaded -> clickListener.onSyncFolderDeleteUploadedClick( + section, + item + ) } return true } @@ -508,6 +512,7 @@ class SyncedFolderAdapter( interface ClickListener { fun onSyncStatusToggleClick(section: Int, item: SyncedFolderDisplayItem?) fun onSyncFolderSettingsClick(section: Int, syncedFolderDisplayItem: SyncedFolderDisplayItem?) + fun onSyncFolderDeleteUploadedClick(section: Int, syncedFolderDisplayItem: SyncedFolderDisplayItem?) fun onVisibilityToggleClick(section: Int, item: SyncedFolderDisplayItem?) fun showSubFolderWarningDialog() } diff --git a/app/src/main/res/menu/synced_folders_adapter.xml b/app/src/main/res/menu/synced_folders_adapter.xml index 227769ed905b..f0752530a2ac 100644 --- a/app/src/main/res/menu/synced_folders_adapter.xml +++ b/app/src/main/res/menu/synced_folders_adapter.xml @@ -14,4 +14,8 @@ + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3b6cb7d99a11..6c6305790aa6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -878,8 +878,12 @@ Create new custom folder setup Hide folder Configure + Locally delete uploaded files Configure folders + Locally delete uploaded files + All the files already uploaded to the cloud will be removed from the current device + Test server connection  • From ded98a8e68ffa166b9f4c4dc69ad06676be0d2cb Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Wed, 19 Aug 2026 18:11:53 +0200 Subject: [PATCH 02/14] feature/1475-delete-uploaded-files: WIP proof of concept Signed-off-by: daniele-verducci --- .../ui/activity/SyncedFoldersActivity.kt | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt index ce0bdd152f0d..c862d9ef1ba3 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt @@ -31,6 +31,8 @@ import com.nextcloud.client.device.PowerManagementService import com.nextcloud.client.di.Injectable import com.nextcloud.client.jobs.MediaFoldersDetectionWork import com.nextcloud.client.jobs.NotificationWork +import com.nextcloud.client.jobs.autoUpload.SyncFolderHelper +import com.nextcloud.client.jobs.operation.FileOperationHelper import com.nextcloud.client.jobs.upload.FileUploadWorker import com.nextcloud.client.preferences.SubFolderRule import com.nextcloud.ui.component.UploadWarningCard @@ -623,7 +625,31 @@ class SyncedFoldersActivity : dialog.isCancelable = false dialog.setOnConfirmationListener(object : ConfirmationDialogFragment.ConfirmationDialogFragmentListener { override fun onConfirmation(callerTag: String?) { - TODO("Not yet implemented") + // DEBUG + lifecycleScope.launch { + val client = clientRepository.getOwncloudClient() ?: return@launch + val syncFolderHelper = SyncFolderHelper(this@SyncedFoldersActivity) + val filesOperationHelper = + FileOperationHelper(user.get(), this@SyncedFoldersActivity, fileDataStorageManager) + val syncedFolderArrayList = syncedFolderProvider.syncedFolders + // TODO: For each synced folder for which the files should be deleted locally + val syncedFolder = + syncedFolderArrayList[0] + val localFolder = File(syncedFolder.localPath) + val files = SyncedFolderUtils.getFileList(localFolder) + files.forEach { + val remotePath = syncFolderHelper.getAutoUploadRemotePath(syncedFolder, it) + val ocFile = OCFile(remotePath) // WRONG: this does not set the remote path, removeFile() fails + filesOperationHelper.removeFile( + file = ocFile, + onlyLocalCopy = true, + inBackground = true, + client = client + ) + Log_OC.d(TAG, "Checking local file ${it.absolutePath} against remote file ${ocFile.remotePath}") + } + } + // DEBUG } override fun onNeutral(callerTag: String?) { From 0f191b2410e60d9ee4179a47fd3cc901fc29ed32 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 20 Aug 2026 09:41:55 +0200 Subject: [PATCH 03/14] feature/1475-delete-uploaded-files: working deletion based only on filename Signed-off-by: daniele-verducci --- .../android/ui/activity/SyncedFoldersActivity.kt | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt index c862d9ef1ba3..78c5ee56a040 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt @@ -638,15 +638,22 @@ class SyncedFoldersActivity : val localFolder = File(syncedFolder.localPath) val files = SyncedFolderUtils.getFileList(localFolder) files.forEach { - val remotePath = syncFolderHelper.getAutoUploadRemotePath(syncedFolder, it) - val ocFile = OCFile(remotePath) // WRONG: this does not set the remote path, removeFile() fails - filesOperationHelper.removeFile( + val ocFile = storageManager.getFileByLocalPath(it.path) + if (ocFile == null) { + Log_OC.d(TAG, "Unable to obtain remote counterpart for file $ocFile") + return@forEach + } + val success = filesOperationHelper.removeFile( file = ocFile, onlyLocalCopy = true, inBackground = true, client = client ) - Log_OC.d(TAG, "Checking local file ${it.absolutePath} against remote file ${ocFile.remotePath}") + if (success) { + Log_OC.d(TAG, "Removed local file ${it.absolutePath}") + } else { + Log_OC.d(TAG, "Error removing local file ${it.absolutePath}") + } } } // DEBUG From 0bb65d753347b78560fb6e818d46318842be6b2b Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 20 Aug 2026 15:37:10 +0200 Subject: [PATCH 04/14] feature/1475-delete-uploaded-files: multi folder deletion, refactoring Signed-off-by: daniele-verducci --- .../ui/activity/SyncedFoldersActivity.kt | 118 +++++++++++------- .../main/res/menu/activity_synced_folders.xml | 4 + app/src/main/res/values/strings.xml | 3 + 3 files changed, 79 insertions(+), 46 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt index 78c5ee56a040..3c8bde2cb06d 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt @@ -21,6 +21,7 @@ import android.view.View import androidx.annotation.VisibleForTesting import androidx.appcompat.app.AlertDialog import androidx.drawerlayout.widget.DrawerLayout +import androidx.fragment.app.DialogFragment import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.GridLayoutManager @@ -31,7 +32,6 @@ import com.nextcloud.client.device.PowerManagementService import com.nextcloud.client.di.Injectable import com.nextcloud.client.jobs.MediaFoldersDetectionWork import com.nextcloud.client.jobs.NotificationWork -import com.nextcloud.client.jobs.autoUpload.SyncFolderHelper import com.nextcloud.client.jobs.operation.FileOperationHelper import com.nextcloud.client.jobs.upload.FileUploadWorker import com.nextcloud.client.preferences.SubFolderRule @@ -83,6 +83,8 @@ class SyncedFoldersActivity : companion object { private const val SYNCED_FOLDER_PREFERENCES_DIALOG_TAG = "SYNCED_FOLDER_PREFERENCES_DIALOG" + private const val SYNCED_FOLDER_DELETE_UPLOADED_DIALOG_TAG = "SYNCED_FOLDER_DELETE_UPLOADED_DIALOG_TAG" + private const val SYNCED_FOLDER_DELETE_ALL_UPLOADED_DIALOG_TAG = "SYNCED_FOLDER_DELETE_ALL_UPLOADED_DIALOG_TAG" private const val SUB_FOLDER_WARNING_DIALOG_TAG = "SUB_FOLDER_WARNING_DIALOG_TAG" // yes, there is a typo in this value @@ -584,6 +586,8 @@ class SyncedFoldersActivity : result = super.onOptionsItemSelected(item) } + R.id.action_auto_upload_all_folders_delete_uploaded -> onAllSyncFolderDeleteUploadedClick() + else -> result = super.onOptionsItemSelected(item) } return result @@ -597,22 +601,40 @@ class SyncedFoldersActivity : section ) - dialogFragment?.let { folderPreferencesDialog -> - if (isDialogFragmentReady(folderPreferencesDialog) && - lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED) - ) { - val fragmentTransaction = supportFragmentManager - .beginTransaction() - .addToBackStack(null) + showDialog(dialogFragment!!, SYNCED_FOLDER_PREFERENCES_DIALOG_TAG) + } - folderPreferencesDialog.show(fragmentTransaction, SYNCED_FOLDER_PREFERENCES_DIALOG_TAG) - } else { - Log_OC.d(TAG, "SyncedFolderPreferencesDialogFragment not ready") + private fun onAllSyncFolderDeleteUploadedClick() { + val dialog = ConfirmationDialogFragment.newInstance( + R.string.autoupload_delete_uploaded_all_dialog_description, + null, + R.string.autoupload_delete_uploaded_all_dialog_title, + R.drawable.selector_trashbin, + R.string.common_ok, + R.string.common_cancel, + -1 + ) + dialog.isCancelable = false + dialog.setOnConfirmationListener(object : ConfirmationDialogFragment.ConfirmationDialogFragmentListener { + override fun onConfirmation(callerTag: String?) { + val syncedFolderArrayList = syncedFolderProvider.syncedFolders + syncedFolderArrayList + .filter { it.isEnabled } + .forEach { deleteUploadedItemFromSyncFolder(it) } } - } - } + override fun onNeutral(callerTag: String?) { + } + + override fun onCancel(callerTag: String?) { + } + }) + + showDialog(dialog, SYNCED_FOLDER_DELETE_ALL_UPLOADED_DIALOG_TAG) + } override fun onSyncFolderDeleteUploadedClick(section: Int, syncedFolderDisplayItem: SyncedFolderDisplayItem?) { + syncedFolderDisplayItem ?: return + val dialog = ConfirmationDialogFragment.newInstance( R.string.autoupload_delete_uploaded_dialog_description, null, @@ -625,38 +647,7 @@ class SyncedFoldersActivity : dialog.isCancelable = false dialog.setOnConfirmationListener(object : ConfirmationDialogFragment.ConfirmationDialogFragmentListener { override fun onConfirmation(callerTag: String?) { - // DEBUG - lifecycleScope.launch { - val client = clientRepository.getOwncloudClient() ?: return@launch - val syncFolderHelper = SyncFolderHelper(this@SyncedFoldersActivity) - val filesOperationHelper = - FileOperationHelper(user.get(), this@SyncedFoldersActivity, fileDataStorageManager) - val syncedFolderArrayList = syncedFolderProvider.syncedFolders - // TODO: For each synced folder for which the files should be deleted locally - val syncedFolder = - syncedFolderArrayList[0] - val localFolder = File(syncedFolder.localPath) - val files = SyncedFolderUtils.getFileList(localFolder) - files.forEach { - val ocFile = storageManager.getFileByLocalPath(it.path) - if (ocFile == null) { - Log_OC.d(TAG, "Unable to obtain remote counterpart for file $ocFile") - return@forEach - } - val success = filesOperationHelper.removeFile( - file = ocFile, - onlyLocalCopy = true, - inBackground = true, - client = client - ) - if (success) { - Log_OC.d(TAG, "Removed local file ${it.absolutePath}") - } else { - Log_OC.d(TAG, "Error removing local file ${it.absolutePath}") - } - } - } - // DEBUG + deleteUploadedItemFromSyncFolder(syncedFolderDisplayItem) } override fun onNeutral(callerTag: String?) { @@ -665,6 +656,10 @@ class SyncedFoldersActivity : override fun onCancel(callerTag: String?) { } }) + showDialog(dialog, SYNCED_FOLDER_DELETE_UPLOADED_DIALOG_TAG) + } + + private fun showDialog(dialog: DialogFragment, tag: String) { if (isDialogFragmentReady(dialog) && lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED) ) { @@ -672,12 +667,43 @@ class SyncedFoldersActivity : .beginTransaction() .addToBackStack(null) - dialog.show(fragmentTransaction, SYNCED_FOLDER_PREFERENCES_DIALOG_TAG) + dialog.show(fragmentTransaction, tag) } else { Log_OC.d(TAG, "SyncFolderDeleteUploaded dialog not ready") } } + // TODO: Temporary proof-of-concept impl, to be refactored and moved elsewhere + private fun deleteUploadedItemFromSyncFolder(syncedFolder: SyncedFolder) { + // TODO: Refresh remote data with RefreshFolderOperation (and do recursively for inner folders) + lifecycleScope.launch { + val client = clientRepository.getOwncloudClient() ?: return@launch + val filesOperationHelper = + FileOperationHelper(user.get(), this@SyncedFoldersActivity, fileDataStorageManager) + val localFolder = File(syncedFolder.localPath) + val files = SyncedFolderUtils.getFileList(localFolder) + files.forEach { + val ocFile = storageManager.getFileByLocalPath(it.path) + Log_OC.d(TAG, "LastSyncDate: ${ocFile?.lastSyncDateForProperties}") + if (ocFile == null) { + Log_OC.i(TAG, "deleteUploadedItemFromSyncFolder: Leaving local-only file ${it.name} in place") + return@forEach + } + val success = filesOperationHelper.removeFile( + file = ocFile, + onlyLocalCopy = true, + inBackground = true, + client = client + ) + if (success) { + Log_OC.i(TAG, "deleteUploadedItemFromSyncFolder: Removed local file ${it.absolutePath}") + } else { + Log_OC.e(TAG, "deleteUploadedItemFromSyncFolder: Error removing local file ${it.absolutePath}") + } + } + } + } + override fun onVisibilityToggleClick(section: Int, syncedFolder: SyncedFolderDisplayItem?) { if (syncedFolder == null) return diff --git a/app/src/main/res/menu/activity_synced_folders.xml b/app/src/main/res/menu/activity_synced_folders.xml index af450aa933d2..363c68c06ea6 100644 --- a/app/src/main/res/menu/activity_synced_folders.xml +++ b/app/src/main/res/menu/activity_synced_folders.xml @@ -19,5 +19,9 @@ android:title="@string/autoupload_disable_power_save_check" android:visible="false" android:checkable="true" /> + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6c6305790aa6..e359876ae689 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -884,6 +884,9 @@ Locally delete uploaded files All the files already uploaded to the cloud will be removed from the current device + Locally delete uploaded files for all auto upload folders + All the files already uploaded to the cloud will be removed from the current device for all the autoupload folders + Test server connection  • From b0f98668ce3a48adb4595e420fdb5014f3a55676 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 20 Aug 2026 17:30:08 +0200 Subject: [PATCH 05/14] feature/1475-delete-uploaded-files: WIP (working folder refresh) Signed-off-by: daniele-verducci --- .../upload/DeleteUploadedFileOperation.kt | 122 ++++++++++++++++++ .../ui/activity/SyncedFoldersActivity.kt | 34 ++--- 2 files changed, 130 insertions(+), 26 deletions(-) create mode 100644 app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt diff --git a/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt b/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt new file mode 100644 index 000000000000..4b0fb1f7f8aa --- /dev/null +++ b/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt @@ -0,0 +1,122 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Your Name + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.operations.upload + +import android.content.Context +import com.nextcloud.client.account.User +import com.nextcloud.client.jobs.autoUpload.SyncFolderHelper +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.datamodel.SyncedFolder +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.operations.RefreshFolderOperation +import com.owncloud.android.utils.SyncedFolderUtils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.File + +class DeleteUploadedFileOperation( + private val syncedFolder: SyncedFolder, + private val user: User, + private val context: Context, + private val storageManager: FileDataStorageManager +) { + + companion object { + const val TAG = "DeleteUploadedFileOperation" + } + private val syncFolderHelper = SyncFolderHelper(context) + + suspend fun run(): RemoteOperationResult<*> { + // TODO: Refresh remote data with RefreshFolderOperation (and do recursively for inner folders) + + val folder = storageManager.getFileByRemotePath(syncedFolder.remotePath) + if (folder == null) { + Log_OC.e(TAG, "Unable to obtain remote folder to refresh metadata") + return RemoteOperationResult(RemoteOperationResult.ResultCode.METADATA_NOT_FOUND) + } + val metadataRefreshSuccess = refreshFolder(folder, storageManager) + if (!metadataRefreshSuccess) { + Log_OC.e(TAG, "Unable to refresh folder metadata") + return RemoteOperationResult(RemoteOperationResult.ResultCode.METADATA_NOT_FOUND) + } + + + + val localFolder = File(syncedFolder.localPath) + val files = SyncedFolderUtils.getFileList(localFolder) + files.forEach { localFile -> + /* + val entity = fileSystemDao.getFileByPathAndFolder(syncedFolder.localPath, syncedFolder.id.toString()) + val fileSentForUpload = entity?.fileSentForUpload == 1 + + if (!fileSentForUpload) { + Log_OC.i(TAG, "File ${localFile.name} never uploaded, leaving in place") + return@forEach + } + + val hasNotChanged = entity.fileModified == localFile.lastModified() + if (!hasNotChanged) { + Log_OC.i(TAG, "File ${localFile.name} has changed, leaving in place") + return@forEach + } + + */ + + //val ocFile = storageManager.getFileByLocalPath(localFile.path) + val remotePath = syncFolderHelper.getAutoUploadRemotePath(syncedFolder, localFile) + val ocFile = storageManager.getFileByRemotePath(remotePath) + if (ocFile == null) { + Log_OC.i(TAG, "Unable to compare file ${localFile.name} with its remote counterpart, leaving in place") + return@forEach + } + Log_OC.d(TAG, "LastSyncDate: ${ocFile?.lastSyncDateForProperties}, modificationTimestamp: ${ocFile.modificationTimestamp}") + + ocFile.lastSyncDateForProperties < localFile.lastModified() + ocFile.modificationTimestamp == localFile.lastModified() + + // File deletion + /* + val success = filesOperationHelper.removeFile( + file = ocFile, + onlyLocalCopy = true, + inBackground = true, + client = client + ) + if (success) { + Log_OC.i(TAG, "deleteUploadedItemFromSyncFolder: Removed local file ${it.absolutePath}") + } else { + Log_OC.e(TAG, "deleteUploadedItemFromSyncFolder: Error removing local file ${it.absolutePath}") + } + */ + Log_OC.d(TAG, "DELETING FILE ${localFile.name}") + } + + return RemoteOperationResult(RemoteOperationResult.ResultCode.OK) + } + + + private suspend fun refreshFolder(folder: OCFile, storageManager: FileDataStorageManager): Boolean = + withContext(Dispatchers.IO) { + val operation = RefreshFolderOperation(folder, storageManager, user, context) + return@withContext try { + val result = operation.execute(user, context) + if (result.isSuccess) { + Log_OC.d(TAG, "Successfully fetched metadata for: ${folder.remotePath}") + true + } else { + Log_OC.e(TAG, "Failed to fetch metadata for: ${folder.remotePath}") + false + } + } catch (e: Exception) { + Log_OC.e(TAG, "Exception refreshing folder ${folder.remotePath}: ${e.message}", e) + false + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt index 3c8bde2cb06d..cc673a6144ad 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt @@ -32,7 +32,6 @@ import com.nextcloud.client.device.PowerManagementService import com.nextcloud.client.di.Injectable import com.nextcloud.client.jobs.MediaFoldersDetectionWork import com.nextcloud.client.jobs.NotificationWork -import com.nextcloud.client.jobs.operation.FileOperationHelper import com.nextcloud.client.jobs.upload.FileUploadWorker import com.nextcloud.client.preferences.SubFolderRule import com.nextcloud.ui.component.UploadWarningCard @@ -54,6 +53,7 @@ import com.owncloud.android.datamodel.SyncedFolderDisplayItem import com.owncloud.android.datamodel.SyncedFolderProvider import com.owncloud.android.files.services.NameCollisionPolicy import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.operations.upload.DeleteUploadedFileOperation import com.owncloud.android.ui.adapter.SyncedFolderAdapter import com.owncloud.android.ui.adapter.storagePermissionBanner.setup import com.owncloud.android.ui.decoration.MediaGridItemDecoration @@ -675,32 +675,14 @@ class SyncedFoldersActivity : // TODO: Temporary proof-of-concept impl, to be refactored and moved elsewhere private fun deleteUploadedItemFromSyncFolder(syncedFolder: SyncedFolder) { - // TODO: Refresh remote data with RefreshFolderOperation (and do recursively for inner folders) + val op = DeleteUploadedFileOperation( + syncedFolder, + user.get(), + this, + storageManager + ) lifecycleScope.launch { - val client = clientRepository.getOwncloudClient() ?: return@launch - val filesOperationHelper = - FileOperationHelper(user.get(), this@SyncedFoldersActivity, fileDataStorageManager) - val localFolder = File(syncedFolder.localPath) - val files = SyncedFolderUtils.getFileList(localFolder) - files.forEach { - val ocFile = storageManager.getFileByLocalPath(it.path) - Log_OC.d(TAG, "LastSyncDate: ${ocFile?.lastSyncDateForProperties}") - if (ocFile == null) { - Log_OC.i(TAG, "deleteUploadedItemFromSyncFolder: Leaving local-only file ${it.name} in place") - return@forEach - } - val success = filesOperationHelper.removeFile( - file = ocFile, - onlyLocalCopy = true, - inBackground = true, - client = client - ) - if (success) { - Log_OC.i(TAG, "deleteUploadedItemFromSyncFolder: Removed local file ${it.absolutePath}") - } else { - Log_OC.e(TAG, "deleteUploadedItemFromSyncFolder: Error removing local file ${it.absolutePath}") - } - } + op.run() } } From 7b04805b5c2be61909551b9ed73eab78f6260aca Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 20 Aug 2026 18:07:12 +0200 Subject: [PATCH 06/14] 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 1f973a088614fb747f7ca50e588baf27d5b667dc Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Thu, 20 Aug 2026 18:10:51 +0200 Subject: [PATCH 07/14] 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 ffd385a01a083cf9232a0edfd2e59314afdd7b41 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Fri, 21 Aug 2026 07:43:24 +0200 Subject: [PATCH 08/14] 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 2ac202f844654703a9b85ed939cde7147fea0a2c Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Fri, 21 Aug 2026 08:21:54 +0200 Subject: [PATCH 09/14] feature/1475-delete-uploaded-files: Added option to system manage space screen Signed-off-by: daniele-verducci --- .../ui/activity/ManageSpaceActivity.kt | 13 +++++ .../ui/activity/SyncedFoldersActivity.kt | 2 +- .../main/res/layout/activity_manage_space.xml | 58 ++++++++++++++++++- app/src/main/res/values/strings.xml | 3 + 4 files changed, 74 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ManageSpaceActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/ManageSpaceActivity.kt index b176788af43b..60bcc15c4265 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/ManageSpaceActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/ManageSpaceActivity.kt @@ -51,9 +51,11 @@ class ManageSpaceActivity : setContentView(binding.root) binding.run { + manageActivityToolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() } + generalDescription.text = getString(R.string.manage_space_description, getString(R.string.app_name)) clearDataButton.setOnClickListener { lifecycleScope.launch { @@ -61,6 +63,13 @@ class ManageSpaceActivity : } } viewThemeUtils.material.colorMaterialButtonPrimaryTonal(clearDataButton) + + autoUploadDeleteButton.setOnClickListener { + lifecycleScope.launch { + clearAutoUploadData() + } + } + viewThemeUtils.material.colorMaterialButtonPrimaryTonal(autoUploadDeleteButton) } } @@ -149,6 +158,10 @@ class ManageSpaceActivity : return dir?.delete() ?: false } + private fun clearAutoUploadData() { + + } + override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { android.R.id.home -> { finish() diff --git a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt index cc673a6144ad..1918a43de5d7 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt @@ -673,8 +673,8 @@ class SyncedFoldersActivity : } } - // TODO: Temporary proof-of-concept impl, to be refactored and moved elsewhere private fun deleteUploadedItemFromSyncFolder(syncedFolder: SyncedFolder) { + // TODO: Run in a background job with notifications to the user about the progression val op = DeleteUploadedFileOperation( syncedFolder, user.get(), diff --git a/app/src/main/res/layout/activity_manage_space.xml b/app/src/main/res/layout/activity_manage_space.xml index 1ef4a0dd1af3..7097badb06ab 100644 --- a/app/src/main/res/layout/activity_manage_space.xml +++ b/app/src/main/res/layout/activity_manage_space.xml @@ -26,7 +26,7 @@ android:background="@color/appbar" /> + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e359876ae689..1047184c6274 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -767,6 +767,9 @@ Manage space Settings, database and server certificates from %1$s\'s data will be deleted permanently. \n\nDownloaded files will be kept untouched.\n\nThis process can take a while. Clear data + Locally delete auto uploaded files + All files already uploaded to the server from the Auto Upload folders will be deleted locally to free up space in the device.\n\nThis process can take a while. + Locally delete auto uploaded files File not found in local file system Do you really want to delete the selected items? From 96c90f38c71a917f733d3070aceed5f3b3a12611 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Fri, 21 Aug 2026 12:45:38 +0200 Subject: [PATCH 10/14] feature/1475-delete-uploaded-files: Moved logic to background worker, wired up manage space button Signed-off-by: daniele-verducci --- .../client/jobs/BackgroundJobManager.kt | 1 + .../client/jobs/BackgroundJobManagerImpl.kt | 29 +++++++++ .../AutoUploadLocalDeletionWorker.kt | 63 +++++++++++++++++++ .../upload/DeleteUploadedFileOperation.kt | 4 +- .../ui/activity/ManageSpaceActivity.kt | 11 +++- .../ui/activity/SyncedFoldersActivity.kt | 22 +------ .../owncloud/android/utils/FilesSyncHelper.kt | 21 +++++++ 7 files changed, 129 insertions(+), 22 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt index a859e5808d56..9bfd2c1392a5 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt @@ -171,4 +171,5 @@ interface BackgroundJobManager { fun startMetadataSyncJob(currentDirPath: String) fun downloadFolder(folder: OCFile, accountName: String) fun cancelFolderDownload() + fun locallyDeleteAutoUploadedFiles(syncedFolders: List) } diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt index 4a28b1715dea..4d7a6f41b030 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt @@ -28,6 +28,7 @@ import com.nextcloud.client.account.User import com.nextcloud.client.core.Clock import com.nextcloud.client.di.Injectable import com.nextcloud.client.documentscan.GeneratePdfFromImagesWork +import com.nextcloud.client.jobs.autoUpload.AutoUploadLocalDeletionWorker import com.nextcloud.client.jobs.autoUpload.AutoUploadWorker import com.nextcloud.client.jobs.download.FileDownloadWorker import com.nextcloud.client.jobs.folderDownload.FolderDownloadWorker @@ -103,6 +104,7 @@ internal class BackgroundJobManagerImpl( const val JOB_DOWNLOAD_FOLDER = "download_folder" const val JOB_METADATA_SYNC = "metadata_sync" const val JOB_INTERNAL_TWO_WAY_SYNC = "internal_two_way_sync" + const val JOB_AUTO_UPLOAD_LOCAL_DELETION = "auto_upload_local_deletion" const val JOB_TEST = "test_job" @@ -824,4 +826,31 @@ internal class BackgroundJobManagerImpl( override fun cancelFolderDownload() { workManager.cancelAllWorkByTag(JOB_DOWNLOAD_FOLDER) } + + override fun locallyDeleteAutoUploadedFiles(syncedFolders: List) { + val syncedFolderIDs = syncedFolders.map { it.id } + + val arguments = Data.Builder() + .putLongArray(AutoUploadLocalDeletionWorker.SYNCED_FOLDER_IDS, syncedFolderIDs.toLongArray()) + .build() + + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + + val jobName = JOB_AUTO_UPLOAD_LOCAL_DELETION + "_" + syncedFolderIDs.joinToString("-") + val request = oneTimeRequestBuilder( + jobClass = AutoUploadLocalDeletionWorker::class, + jobName = jobName + ) + .setInputData(arguments) + .setConstraints(constraints) + .build() + + workManager.enqueueUniqueWork( + jobName, + ExistingWorkPolicy.KEEP, + request + ) + } } diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt new file mode 100644 index 000000000000..7de6779454c4 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt @@ -0,0 +1,63 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Daniele Verducci + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.jobs.autoUpload + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import com.nextcloud.client.account.User +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.datamodel.SyncedFolderProvider +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.operations.upload.DeleteUploadedFileOperation + +class AutoUploadLocalDeletionWorker( + private val context: Context, + params: WorkerParameters, + private val user: User, + private val storageManager: FileDataStorageManager, + private val syncedFolderProvider: SyncedFolderProvider, +) : CoroutineWorker (context, params) { + + companion object { + const val SYNCED_FOLDER_IDS = "synced_folder_IDs" + + private const val TAG = "\uD83D\uDDD1\uFE0F AutoUploadLocalDeletionWorker" + } + + override suspend fun doWork(): Result { + // TODO: Notify user the operation started + Log_OC.d(TAG, "Started") + + val syncedFolderIDs = inputData.getLongArray(SYNCED_FOLDER_IDS) + ?: throw IllegalArgumentException("$SYNCED_FOLDER_IDS param is mandatory") + val syncedFolders = syncedFolderIDs.map { syncedFolderProvider.getSyncedFolderByID(it) } + + syncedFolders + .filterNotNull() + .filter { it.isEnabled } + .forEach { + val op = DeleteUploadedFileOperation( + it, + user, + context, + storageManager + ) + val res = op.run() + if (res.code != RemoteOperationResult.ResultCode.OK) { + Log_OC.d(TAG, "Failed") + return Result.failure() // TODO: Notify user the operation failed + } + } + // TODO: Notify user the operation completed + Log_OC.d(TAG, "Success") + return Result.success() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt b/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt index facb1320a479..bca096bbb0a3 100644 --- a/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt +++ b/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt @@ -1,7 +1,7 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2026 Your Name + * SPDX-FileCopyrightText: 2026 Daniele Verducci * SPDX-License-Identifier: AGPL-3.0-or-later */ @@ -60,7 +60,7 @@ class DeleteUploadedFileOperation( } val parentFolderRemotePath = ocFile.parentRemotePath - if (syncedFolder.isSubfolderByDate && parentFolderRemotePath !in refreshedFolders) { + if (/*syncedFolder.isSubfolderByDate &&*/ parentFolderRemotePath !in refreshedFolders) { // Files are stored in subfolder by date on the server. // Refresh only subfolders containing one of the files to be checked val subFolder = storageManager.getFileByRemotePath(parentFolderRemotePath) diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ManageSpaceActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/ManageSpaceActivity.kt index 60bcc15c4265..be8764a6f90c 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/ManageSpaceActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/ManageSpaceActivity.kt @@ -16,10 +16,13 @@ import com.google.android.material.snackbar.Snackbar import com.nextcloud.android.common.ui.util.extensions.applyEdgeToEdgeWithSystemBarPadding import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.di.Injectable +import com.nextcloud.client.jobs.BackgroundJobManager import com.nextcloud.client.preferences.AppPreferences import com.owncloud.android.R import com.owncloud.android.databinding.ActivityManageSpaceBinding +import com.owncloud.android.datamodel.SyncedFolderProvider import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.utils.FilesSyncHelper import com.owncloud.android.utils.theme.ViewThemeUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -41,6 +44,12 @@ class ManageSpaceActivity : @Inject lateinit var viewThemeUtils: ViewThemeUtils + @Inject + lateinit var syncedFolderProvider: SyncedFolderProvider + + @Inject + lateinit var backgroundJobManager: BackgroundJobManager + private lateinit var binding: ActivityManageSpaceBinding override fun onCreate(savedInstanceState: Bundle?) { @@ -159,7 +168,7 @@ class ManageSpaceActivity : } private fun clearAutoUploadData() { - + FilesSyncHelper.startLocalDeletionForEnabledSyncedFolders(syncedFolderProvider, backgroundJobManager) } override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { diff --git a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt index 1918a43de5d7..61d7c7516419 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt @@ -53,7 +53,6 @@ import com.owncloud.android.datamodel.SyncedFolderDisplayItem import com.owncloud.android.datamodel.SyncedFolderProvider import com.owncloud.android.files.services.NameCollisionPolicy import com.owncloud.android.lib.common.utils.Log_OC -import com.owncloud.android.operations.upload.DeleteUploadedFileOperation import com.owncloud.android.ui.adapter.SyncedFolderAdapter import com.owncloud.android.ui.adapter.storagePermissionBanner.setup import com.owncloud.android.ui.decoration.MediaGridItemDecoration @@ -62,6 +61,7 @@ import com.owncloud.android.ui.dialog.SyncedFolderPreferencesDialogFragment import com.owncloud.android.ui.dialog.SyncedFolderPreferencesDialogFragment.OnSyncedFolderPreferenceListener import com.owncloud.android.ui.dialog.extensions.themeButtons import com.owncloud.android.ui.dialog.parcel.SyncedFolderParcelable +import com.owncloud.android.utils.FilesSyncHelper import com.owncloud.android.utils.PermissionUtil import com.owncloud.android.utils.SyncedFolderUtils import kotlinx.coroutines.Dispatchers @@ -617,10 +617,7 @@ class SyncedFoldersActivity : dialog.isCancelable = false dialog.setOnConfirmationListener(object : ConfirmationDialogFragment.ConfirmationDialogFragmentListener { override fun onConfirmation(callerTag: String?) { - val syncedFolderArrayList = syncedFolderProvider.syncedFolders - syncedFolderArrayList - .filter { it.isEnabled } - .forEach { deleteUploadedItemFromSyncFolder(it) } + FilesSyncHelper.startLocalDeletionForEnabledSyncedFolders(syncedFolderProvider, backgroundJobManager) } override fun onNeutral(callerTag: String?) { @@ -647,7 +644,7 @@ class SyncedFoldersActivity : dialog.isCancelable = false dialog.setOnConfirmationListener(object : ConfirmationDialogFragment.ConfirmationDialogFragmentListener { override fun onConfirmation(callerTag: String?) { - deleteUploadedItemFromSyncFolder(syncedFolderDisplayItem) + FilesSyncHelper.startLocalDeletionForSyncedFolder(syncedFolderDisplayItem, backgroundJobManager) } override fun onNeutral(callerTag: String?) { @@ -673,19 +670,6 @@ class SyncedFoldersActivity : } } - private fun deleteUploadedItemFromSyncFolder(syncedFolder: SyncedFolder) { - // TODO: Run in a background job with notifications to the user about the progression - val op = DeleteUploadedFileOperation( - syncedFolder, - user.get(), - this, - storageManager - ) - lifecycleScope.launch { - op.run() - } - } - override fun onVisibilityToggleClick(section: Int, syncedFolder: SyncedFolderDisplayItem?) { if (syncedFolder == null) return diff --git a/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.kt b/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.kt index 9b18dea58a5b..e00b30bce679 100644 --- a/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.kt +++ b/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.kt @@ -14,6 +14,7 @@ import com.nextcloud.client.device.PowerManagementService import com.nextcloud.client.jobs.BackgroundJobManager import com.nextcloud.client.jobs.upload.FileUploadHelper.Companion.instance import com.nextcloud.client.network.ConnectivityService +import com.owncloud.android.datamodel.SyncedFolder import com.owncloud.android.datamodel.SyncedFolderProvider import com.owncloud.android.datamodel.UploadsStorageManager import com.owncloud.android.lib.common.utils.Log_OC @@ -52,4 +53,24 @@ object FilesSyncHelper { } } } + + @JvmStatic + fun startLocalDeletionForEnabledSyncedFolders( + provider: SyncedFolderProvider, + manager: BackgroundJobManager + ) { + Log_OC.d(TAG, "start local deletion worker for each enabled folder") + + manager.locallyDeleteAutoUploadedFiles(provider.syncedFolders) + } + + @JvmStatic + fun startLocalDeletionForSyncedFolder( + folder: SyncedFolder, + manager: BackgroundJobManager + ) { + Log_OC.d(TAG, "start local deletion worker for folder ${folder.localPath}") + + manager.locallyDeleteAutoUploadedFiles(listOf(folder)) + } } From 06d03da42543a4ed147c2b249177d1342b754956 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Fri, 21 Aug 2026 15:56:47 +0200 Subject: [PATCH 11/14] feature/1475-delete-uploaded-files: Passing needed objects to background worker Signed-off-by: daniele-verducci --- .../nextcloud/client/jobs/BackgroundJobFactory.kt | 14 ++++++++++++++ .../autoUpload/AutoUploadLocalDeletionWorker.kt | 9 ++++----- .../upload/DeleteUploadedFileOperation.kt | 9 +++++---- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt index b9145d7e8769..964cb24016d4 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt @@ -23,6 +23,7 @@ import com.nextcloud.client.documentscan.GeneratePDFUseCase import com.nextcloud.client.documentscan.GeneratePdfFromImagesWork import com.nextcloud.client.integrations.deck.DeckApi import com.nextcloud.client.jobs.autoUpload.AutoUploadHelper +import com.nextcloud.client.jobs.autoUpload.AutoUploadLocalDeletionWorker import com.nextcloud.client.jobs.autoUpload.AutoUploadWorker import com.nextcloud.client.jobs.autoUpload.FileSystemRepository import com.nextcloud.client.jobs.download.FileDownloadWorker @@ -34,6 +35,7 @@ import com.nextcloud.client.logger.Logger import com.nextcloud.client.network.ConnectivityService import com.nextcloud.client.preferences.AppPreferences import com.owncloud.android.datamodel.ArbitraryDataProvider +import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.SyncedFolderProvider import com.owncloud.android.datamodel.UploadsStorageManager import com.owncloud.android.operations.factory.UploadFileOperationFactory @@ -59,6 +61,7 @@ class BackgroundJobFactory @Inject constructor( private val resources: Resources, private val arbitraryDataProvider: ArbitraryDataProvider, private val uploadsStorageManager: UploadsStorageManager, + private val fileDataStorageManager: FileDataStorageManager, private val connectivityService: ConnectivityService, private val notificationManager: NotificationManager, private val eventBus: EventBus, @@ -107,6 +110,7 @@ class BackgroundJobFactory @Inject constructor( InternalTwoWaySyncWork::class -> createInternalTwoWaySyncWork(context, workerParameters) MetadataWorker::class -> createMetadataWorker(context, workerParameters) FolderDownloadWorker::class -> createFolderDownloadWorker(context, workerParameters) + AutoUploadLocalDeletionWorker::class -> createAutoUploadLocalDeletionWorker(context, workerParameters) else -> null // caller falls back to default factory } } @@ -311,4 +315,14 @@ class BackgroundJobFactory @Inject constructor( localBroadcastManager.get(), params ) + + private fun createAutoUploadLocalDeletionWorker(context: Context, params: WorkerParameters): + AutoUploadLocalDeletionWorker = + AutoUploadLocalDeletionWorker( + context = context, + params = params, + userAccountManager = accountManager, + fileDataStorageManager = fileDataStorageManager, + syncedFolderProvider = syncedFolderProvider + ) } diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt index 7de6779454c4..f80cfb521f15 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt @@ -10,7 +10,7 @@ package com.nextcloud.client.jobs.autoUpload import android.content.Context import androidx.work.CoroutineWorker import androidx.work.WorkerParameters -import com.nextcloud.client.account.User +import com.nextcloud.client.account.UserAccountManager import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.SyncedFolderProvider import com.owncloud.android.lib.common.operations.RemoteOperationResult @@ -20,8 +20,8 @@ import com.owncloud.android.operations.upload.DeleteUploadedFileOperation class AutoUploadLocalDeletionWorker( private val context: Context, params: WorkerParameters, - private val user: User, - private val storageManager: FileDataStorageManager, + private val userAccountManager: UserAccountManager, + private val fileDataStorageManager: FileDataStorageManager, private val syncedFolderProvider: SyncedFolderProvider, ) : CoroutineWorker (context, params) { @@ -45,9 +45,8 @@ class AutoUploadLocalDeletionWorker( .forEach { val op = DeleteUploadedFileOperation( it, - user, context, - storageManager + fileDataStorageManager ) val res = op.run() if (res.code != RemoteOperationResult.ResultCode.OK) { diff --git a/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt b/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt index bca096bbb0a3..7adce8f032f4 100644 --- a/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt +++ b/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt @@ -8,7 +8,6 @@ package com.owncloud.android.operations.upload import android.content.Context -import com.nextcloud.client.account.User import com.nextcloud.client.jobs.autoUpload.SyncFolderHelper import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.OCFile @@ -23,7 +22,6 @@ import java.io.File class DeleteUploadedFileOperation( private val syncedFolder: SyncedFolder, - private val user: User, private val context: Context, private val storageManager: FileDataStorageManager ) { @@ -34,8 +32,10 @@ class DeleteUploadedFileOperation( private val syncFolderHelper = SyncFolderHelper(context) suspend fun run(): RemoteOperationResult<*> { + Log_OC.d(TAG, "StorageManager user is ${storageManager.user}") // Obtain synced folder data val folder = storageManager.getFileByRemotePath(syncedFolder.remotePath) + ?: storageManager.getFileByLocalPath(syncedFolder.localPath) if (folder == null) { Log_OC.e(TAG, "Unable to obtain remote folder to refresh metadata") return RemoteOperationResult(RemoteOperationResult.ResultCode.METADATA_NOT_FOUND) @@ -54,6 +54,7 @@ class DeleteUploadedFileOperation( files.forEach { localFile -> val remotePath = syncFolderHelper.getAutoUploadRemotePath(syncedFolder, localFile) val ocFile = storageManager.getFileByRemotePath(remotePath) + ?: storageManager.getFileByLocalPath(localFile.absolutePath) if (ocFile == null) { Log_OC.i(TAG, "Unable to compare file ${localFile.name} with its remote counterpart, leaving in place") return@forEach @@ -125,9 +126,9 @@ class DeleteUploadedFileOperation( private suspend fun refreshFolder(folder: OCFile, storageManager: FileDataStorageManager): Boolean = withContext(Dispatchers.IO) { - val operation = RefreshFolderOperation(folder, storageManager, user, context) + val operation = RefreshFolderOperation(folder, storageManager, storageManager.user, context) return@withContext try { - val result = operation.execute(user, context) + val result = operation.execute(storageManager.user, context) if (result.isSuccess) { Log_OC.d(TAG, "Successfully fetched metadata for: ${folder.remotePath}") true From 034c1cec91b0bb02a176e2b2564e4f7ba3d6eda8 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Mon, 24 Aug 2026 17:10:00 +0200 Subject: [PATCH 12/14] feature/1475-delete-uploaded-files: notifications Signed-off-by: daniele-verducci --- .../client/jobs/BackgroundJobFactory.kt | 3 +- .../AutoUploadLocalDeletionWorker.kt | 39 +++++++++++++++++-- app/src/main/res/values/strings.xml | 4 ++ 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt index 964cb24016d4..1e04a0df2ad3 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt @@ -323,6 +323,7 @@ class BackgroundJobFactory @Inject constructor( params = params, userAccountManager = accountManager, fileDataStorageManager = fileDataStorageManager, - syncedFolderProvider = syncedFolderProvider + syncedFolderProvider = syncedFolderProvider, + viewThemeUtils = viewThemeUtils.get() ) } diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt index f80cfb521f15..ac81bf2bb707 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt @@ -7,15 +7,20 @@ package com.nextcloud.client.jobs.autoUpload +import android.app.Notification import android.content.Context import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import com.nextcloud.client.account.UserAccountManager +import com.nextcloud.client.jobs.notification.WorkerNotificationManager +import com.owncloud.android.R import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.SyncedFolderProvider import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.operations.upload.DeleteUploadedFileOperation +import com.owncloud.android.ui.notifications.NotificationUtils +import com.owncloud.android.utils.theme.ViewThemeUtils class AutoUploadLocalDeletionWorker( private val context: Context, @@ -23,16 +28,28 @@ class AutoUploadLocalDeletionWorker( private val userAccountManager: UserAccountManager, private val fileDataStorageManager: FileDataStorageManager, private val syncedFolderProvider: SyncedFolderProvider, + val viewThemeUtils: ViewThemeUtils, ) : CoroutineWorker (context, params) { companion object { const val SYNCED_FOLDER_IDS = "synced_folder_IDs" + const val NOTIFICATION_ID = 267 private const val TAG = "\uD83D\uDDD1\uFE0F AutoUploadLocalDeletionWorker" } + private val notificationManager = WorkerNotificationManager( + NOTIFICATION_ID, + context, + viewThemeUtils, + R.string.autoupload_delete_uploaded_notif_ticker, + NotificationUtils.NOTIFICATION_CHANNEL_BACKGROUND_OPERATIONS + ) + override suspend fun doWork(): Result { - // TODO: Notify user the operation started + showNotification( + createNotification(context.getString(R.string.autoupload_delete_uploaded_notif_started_title)) + ) Log_OC.d(TAG, "Started") val syncedFolderIDs = inputData.getLongArray(SYNCED_FOLDER_IDS) @@ -51,12 +68,28 @@ class AutoUploadLocalDeletionWorker( val res = op.run() if (res.code != RemoteOperationResult.ResultCode.OK) { Log_OC.d(TAG, "Failed") - return Result.failure() // TODO: Notify user the operation failed + showNotification( + createNotification(context.getString(R.string.autoupload_delete_uploaded_notif_error_title)) + ) + return Result.failure() } } - // TODO: Notify user the operation completed + showNotification( + createNotification(context.getString(R.string.autoupload_delete_uploaded_notif_ended_title)) + ) Log_OC.d(TAG, "Success") return Result.success() } + private fun createNotification(title: String): Notification = notificationManager.notificationBuilder + .setContentTitle(title) + .setSmallIcon(R.drawable.ic_delete) + .setSound(null) + .setVibrate(null) + .setOnlyAlertOnce(true) + .setSilent(true) + .setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_BACKGROUND_OPERATIONS) + .build() + + private fun showNotification(notification: Notification) = notificationManager.showNotification() } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1047184c6274..d324f039f2ef 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -882,6 +882,10 @@ Hide folder Configure Locally delete uploaded files + Local files deletion + Started to locally delete already uploaded files + Locally deleted already uploaded files + Unable to locally delete already uploaded files Configure folders Locally delete uploaded files From a262a0c0b914f4ac97049ad9cc0f09849937346e Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Mon, 24 Aug 2026 17:18:53 +0200 Subject: [PATCH 13/14] feature/1475-delete-uploaded-files: lint Signed-off-by: daniele-verducci --- .../client/jobs/BackgroundJobFactory.kt | 21 ++++++++++--------- .../AutoUploadLocalDeletionWorker.kt | 6 +++--- .../upload/DeleteUploadedFileOperation.kt | 4 ++-- .../ui/activity/ManageSpaceActivity.kt | 1 - .../owncloud/android/utils/FilesSyncHelper.kt | 10 ++------- 5 files changed, 18 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt index 1e04a0df2ad3..49fdd9705a2d 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt @@ -316,14 +316,15 @@ class BackgroundJobFactory @Inject constructor( params ) - private fun createAutoUploadLocalDeletionWorker(context: Context, params: WorkerParameters): - AutoUploadLocalDeletionWorker = - AutoUploadLocalDeletionWorker( - context = context, - params = params, - userAccountManager = accountManager, - fileDataStorageManager = fileDataStorageManager, - syncedFolderProvider = syncedFolderProvider, - viewThemeUtils = viewThemeUtils.get() - ) + private fun createAutoUploadLocalDeletionWorker( + context: Context, + params: WorkerParameters + ): AutoUploadLocalDeletionWorker = AutoUploadLocalDeletionWorker( + context = context, + params = params, + userAccountManager = accountManager, + fileDataStorageManager = fileDataStorageManager, + syncedFolderProvider = syncedFolderProvider, + viewThemeUtils = viewThemeUtils.get() + ) } diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt index ac81bf2bb707..e5d13e15fbfd 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt @@ -28,8 +28,8 @@ class AutoUploadLocalDeletionWorker( private val userAccountManager: UserAccountManager, private val fileDataStorageManager: FileDataStorageManager, private val syncedFolderProvider: SyncedFolderProvider, - val viewThemeUtils: ViewThemeUtils, -) : CoroutineWorker (context, params) { + val viewThemeUtils: ViewThemeUtils +) : CoroutineWorker(context, params) { companion object { const val SYNCED_FOLDER_IDS = "synced_folder_IDs" @@ -92,4 +92,4 @@ class AutoUploadLocalDeletionWorker( .build() private fun showNotification(notification: Notification) = notificationManager.showNotification() -} \ No newline at end of file +} diff --git a/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt b/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt index 7adce8f032f4..9cf91603c846 100644 --- a/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt +++ b/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt @@ -61,7 +61,7 @@ class DeleteUploadedFileOperation( } val parentFolderRemotePath = ocFile.parentRemotePath - if (/*syncedFolder.isSubfolderByDate &&*/ parentFolderRemotePath !in refreshedFolders) { + if (parentFolderRemotePath !in refreshedFolders) { // Files are stored in subfolder by date on the server. // Refresh only subfolders containing one of the files to be checked val subFolder = storageManager.getFileByRemotePath(parentFolderRemotePath) @@ -113,7 +113,7 @@ class DeleteUploadedFileOperation( } // File deletion - val deleted = true //localFile.delete() + val deleted = true // localFile.delete() if (deleted) { Log_OC.i(TAG, "Deleted file ${localFile.name}") } else { diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ManageSpaceActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/ManageSpaceActivity.kt index be8764a6f90c..6050426a8393 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/ManageSpaceActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/ManageSpaceActivity.kt @@ -60,7 +60,6 @@ class ManageSpaceActivity : setContentView(binding.root) binding.run { - manageActivityToolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() } diff --git a/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.kt b/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.kt index e00b30bce679..f0bc12570be6 100644 --- a/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.kt +++ b/app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.kt @@ -55,20 +55,14 @@ object FilesSyncHelper { } @JvmStatic - fun startLocalDeletionForEnabledSyncedFolders( - provider: SyncedFolderProvider, - manager: BackgroundJobManager - ) { + fun startLocalDeletionForEnabledSyncedFolders(provider: SyncedFolderProvider, manager: BackgroundJobManager) { Log_OC.d(TAG, "start local deletion worker for each enabled folder") manager.locallyDeleteAutoUploadedFiles(provider.syncedFolders) } @JvmStatic - fun startLocalDeletionForSyncedFolder( - folder: SyncedFolder, - manager: BackgroundJobManager - ) { + fun startLocalDeletionForSyncedFolder(folder: SyncedFolder, manager: BackgroundJobManager) { Log_OC.d(TAG, "start local deletion worker for folder ${folder.localPath}") manager.locallyDeleteAutoUploadedFiles(listOf(folder)) From 23ed719809937befcece5ccdd7655485acc349e9 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Mon, 24 Aug 2026 18:25:38 +0200 Subject: [PATCH 14/14] feature/1475-delete-uploaded-files: Manage non-writeable folders Signed-off-by: daniele-verducci --- .../client/jobs/BackgroundJobManagerImpl.kt | 4 +++- .../autoUpload/AutoUploadLocalDeletionWorker.kt | 6 +++++- .../android/ui/adapter/SyncedFolderAdapter.kt | 16 ++++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt index 4d7a6f41b030..946813296fa8 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt @@ -828,7 +828,9 @@ internal class BackgroundJobManagerImpl( } override fun locallyDeleteAutoUploadedFiles(syncedFolders: List) { - val syncedFolderIDs = syncedFolders.map { it.id } + val syncedFolderIDs = syncedFolders + .filter { it.isEnabled } + .map { it.id } val arguments = Data.Builder() .putLongArray(AutoUploadLocalDeletionWorker.SYNCED_FOLDER_IDS, syncedFolderIDs.toLongArray()) diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt index e5d13e15fbfd..4f821a8d73c2 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt @@ -20,7 +20,9 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.operations.upload.DeleteUploadedFileOperation import com.owncloud.android.ui.notifications.NotificationUtils +import com.owncloud.android.utils.FileUtil import com.owncloud.android.utils.theme.ViewThemeUtils +import java.io.File class AutoUploadLocalDeletionWorker( private val context: Context, @@ -54,11 +56,13 @@ class AutoUploadLocalDeletionWorker( val syncedFolderIDs = inputData.getLongArray(SYNCED_FOLDER_IDS) ?: throw IllegalArgumentException("$SYNCED_FOLDER_IDS param is mandatory") - val syncedFolders = syncedFolderIDs.map { syncedFolderProvider.getSyncedFolderByID(it) } + val syncedFolders = syncedFolderIDs + .map { syncedFolderProvider.getSyncedFolderByID(it) } syncedFolders .filterNotNull() .filter { it.isEnabled } + .filter { FileUtil.isFolderWritable(File(it.localPath)) } .forEach { val op = DeleteUploadedFileOperation( it, diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/SyncedFolderAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/SyncedFolderAdapter.kt index 28d21ba22151..67b92725c408 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/SyncedFolderAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/SyncedFolderAdapter.kt @@ -40,6 +40,7 @@ import com.owncloud.android.datamodel.SyncedFolderDisplayItem import com.owncloud.android.datamodel.ThumbnailsCacheManager import com.owncloud.android.datamodel.ThumbnailsCacheManager.AsyncMediaThumbnailDrawable import com.owncloud.android.datamodel.ThumbnailsCacheManager.MediaThumbnailGenerationTask +import com.owncloud.android.utils.FileUtil import com.owncloud.android.utils.theme.ViewThemeUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -382,6 +383,10 @@ class SyncedFolderAdapter( menu .findItem(R.id.action_auto_upload_folder_toggle_visibility) .setChecked(item.isHidden) + updateDeletionMenuItem( + item, + menu.findItem(R.id.action_auto_upload_folder_delete_uploaded) + ) } popup.show() @@ -543,6 +548,17 @@ class SyncedFolderAdapter( } } + private fun updateDeletionMenuItem(syncedFolder: SyncedFolderDisplayItem, menuItem: MenuItem) { + val folderFile = syncedFolder.localPath?.let { File(it) } + lifecycleScope.launch { + val writable = FileUtil.isFolderWritable(folderFile) + withContext(Dispatchers.Main) { + menuItem.isEnabled = writable + menuItem.isVisible = writable + } + } + } + companion object { private const val VIEW_TYPE_EMPTY = Int.MAX_VALUE private const val VIEW_TYPE_ITEM = 1