Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
80f3b69
feature/1475-delete-uploaded-files: Menu item and popup
daniele-verducci Aug 18, 2026
1d72b07
feature/1475-delete-uploaded-files: WIP proof of concept
daniele-verducci Aug 19, 2026
964eda3
feature/1475-delete-uploaded-files: working deletion based only on fi…
daniele-verducci Aug 20, 2026
5968958
feature/1475-delete-uploaded-files: multi folder deletion, refactoring
daniele-verducci Aug 20, 2026
6aec6dd
feature/1475-delete-uploaded-files: WIP (working folder refresh)
daniele-verducci Aug 20, 2026
2779302
feature/1475-delete-uploaded-files: Working files checks
daniele-verducci Aug 20, 2026
538347c
feature/1475-delete-uploaded-files: Cleanup
daniele-verducci Aug 20, 2026
b2bef4a
feature/1475-delete-uploaded-files: refresh subfolders as needed
daniele-verducci Aug 21, 2026
0f03ff9
feature/1475-delete-uploaded-files: Added option to system manage spa…
daniele-verducci Aug 21, 2026
8a7eb8e
feature/1475-delete-uploaded-files: Moved logic to background worker,…
daniele-verducci Aug 21, 2026
cfec844
feature/1475-delete-uploaded-files: Passing needed objects to backgro…
daniele-verducci Aug 21, 2026
a6cc236
feature/1475-delete-uploaded-files: notifications
daniele-verducci Aug 24, 2026
880ef3b
feature/1475-delete-uploaded-files: lint
daniele-verducci Aug 24, 2026
5ee406f
feature/1475-delete-uploaded-files: Manage non-writeable folders
daniele-verducci Aug 24, 2026
d90384b
feature/1475-delete-uploaded-files: Fix user race condition
daniele-verducci Aug 25, 2026
3b0efbb
feature/1475-delete-uploaded-files: Stats
daniele-verducci Aug 26, 2026
12f8c05
feature/1475-delete-uploaded-files: Fixed untouched files report
daniele-verducci Aug 26, 2026
cb2de5c
feature/1475-delete-uploaded-files: Better user notifications
daniele-verducci Aug 26, 2026
af99094
feature/1475-delete-uploaded-files: spotless run
daniele-verducci Aug 27, 2026
0a3d4d5
feature/1475-delete-uploaded-files: show runtime in stats
daniele-verducci Aug 27, 2026
011d83b
feature/1475-delete-uploaded-files: more logs & user info, fix
daniele-verducci Aug 27, 2026
e5b56f3
feature/1475-delete-uploaded-files: apply to current user or all user…
daniele-verducci Aug 27, 2026
c742489
feature/1475-delete-uploaded-files: fixed bug preventing removal of p…
daniele-verducci Aug 27, 2026
5fbc88c
feature/1475-delete-uploaded-files: enabled real file deletion
daniele-verducci Aug 27, 2026
6533621
feature/1475-delete-uploaded-files: changed some strings, simpler com…
daniele-verducci Aug 28, 2026
4dfe198
feature/1475-delete-uploaded-files: Pluralized strings
daniele-verducci Sep 2, 2026
4b4391e
feature/1475-delete-uploaded-files: Simplified log, removed over engi…
daniele-verducci Sep 2, 2026
90ae4ed
feature/1475-delete-uploaded-files: Fix Detekt + lint issues
daniele-verducci Sep 2, 2026
57311cf
feature/1475-delete-uploaded-files: Replace Toast with Snackbar
daniele-verducci Sep 2, 2026
6d9fabb
feature/1475-delete-uploaded-files: Disabled codacy check for more st…
daniele-verducci Sep 2, 2026
c9917de
wip
alperozturk96 Sep 2, 2026
66e1f00
wip
alperozturk96 Sep 2, 2026
14a7e87
feature/1475-delete-uploaded-files: fix lint
daniele-verducci Sep 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -110,6 +111,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
}
}
Expand Down Expand Up @@ -328,4 +330,15 @@ class BackgroundJobFactory @Inject constructor(
localBroadcastManager.get(),
params
)

private fun createAutoUploadLocalDeletionWorker(
context: Context,
params: WorkerParameters
): AutoUploadLocalDeletionWorker = AutoUploadLocalDeletionWorker(
context = context,
params = params,
userAccountManager = accountManager,
syncedFolderProvider = syncedFolderProvider,
viewThemeUtils = viewThemeUtils.get()
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -177,4 +177,5 @@ interface BackgroundJobManager {
fun startMetadataSyncJob(currentDirPath: String)
fun downloadFolder(folder: OCFile, accountName: String)
fun cancelFolderDownload()
fun locallyDeleteAutoUploadedFiles(syncedFolders: List<SyncedFolder>)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"

Expand Down Expand Up @@ -905,4 +907,33 @@ internal class BackgroundJobManagerImpl(
override fun cancelFolderDownload() {
workManager.cancelAllWorkByTag(JOB_DOWNLOAD_FOLDER)
}

override fun locallyDeleteAutoUploadedFiles(syncedFolders: List<SyncedFolder>) {
val syncedFolderIDs = syncedFolders
.filter { it.isEnabled }
.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
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Daniele Verducci <daniele.verducci@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

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.DisplayUtils
import com.owncloud.android.utils.FileUtil
import com.owncloud.android.utils.theme.ViewThemeUtils
import java.io.File

class AutoUploadLocalDeletionWorker(
private val context: Context,
params: WorkerParameters,
private val userAccountManager: UserAccountManager,
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
const val MS_IN_SECOND = 1000

private const val TAG = "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 {
showNotification(
createNotification(context.getString(R.string.autoupload_delete_uploaded_notif_started_title))
)
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) }

var users = HashSet<String>()
var filesPreserved = 0L
var foldersAnalyzed = 0L
var filesRemoved = 0L
var spaceFreed = 0L
val timeStarted = System.currentTimeMillis()
syncedFolders
.filterNotNull()
.filter { it.isEnabled }
.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,
context,
fileDataStorageManager
)
val res = op.run()
if (res.code != RemoteOperationResult.ResultCode.OK) {
Log_OC.d(TAG, "Failed")
showNotification(
createNotification(context.getString(R.string.autoupload_delete_uploaded_notif_error_title))
)
return Result.failure()
}
foldersAnalyzed++
filesPreserved += res.resultData.filesPreserved
filesRemoved += res.resultData.filesRemoved
spaceFreed += res.resultData.spaceFreed
}

val runTimeMs = System.currentTimeMillis() - timeStarted
showNotification(
createSuccessNotification(
users.size,
foldersAnalyzed,
filesRemoved,
filesPreserved,
spaceFreed,
runTimeMs
)
)
Log_OC.d(
TAG,
"Success: users=$users, foldersAnalyzed=$foldersAnalyzed, filesPreserved=$filesPreserved, " +
"filesRemoved=$filesRemoved, spaceFreed=$spaceFreed bytes, runTime=${runTimeMs / MS_IN_SECOND} seconds"
)
return Result.success()
}

private fun createSuccessNotification(
users: Int,
foldersRemoved: Long,
filesRemoved: Long,
filesPreserved: Long,
spaceFreed: Long,
timeElapsed: Long
): Notification {
val notificationContent = context.getString(
R.string.autoupload_delete_uploaded_notif_ended_content,
DisplayUtils.bytesToHumanReadable(spaceFreed),
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),
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)
.setOnlyAlertOnce(true)
.setSilent(true)
.setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_BACKGROUND_OPERATIONS)
.build()

private fun showNotification(notification: Notification) = notificationManager.showNotification()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading
Loading