Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
d7e377c
feature/1475-delete-uploaded-files: Menu item and popup
daniele-verducci Aug 18, 2026
be9bc78
feature/1475-delete-uploaded-files: WIP proof of concept
daniele-verducci Aug 19, 2026
947bfc1
feature/1475-delete-uploaded-files: working deletion based only on fi…
daniele-verducci Aug 20, 2026
b154a1e
feature/1475-delete-uploaded-files: multi folder deletion, refactoring
daniele-verducci Aug 20, 2026
88a86f7
feature/1475-delete-uploaded-files: WIP (working folder refresh)
daniele-verducci Aug 20, 2026
1ae981d
feature/1475-delete-uploaded-files: Working files checks
daniele-verducci Aug 20, 2026
774c44a
feature/1475-delete-uploaded-files: Cleanup
daniele-verducci Aug 20, 2026
2e2029a
feature/1475-delete-uploaded-files: refresh subfolders as needed
daniele-verducci Aug 21, 2026
5df2ec6
feature/1475-delete-uploaded-files: Added option to system manage spa…
daniele-verducci Aug 21, 2026
417bc57
feature/1475-delete-uploaded-files: Moved logic to background worker,…
daniele-verducci Aug 21, 2026
ba9f473
feature/1475-delete-uploaded-files: Passing needed objects to backgro…
daniele-verducci Aug 21, 2026
8319354
feature/1475-delete-uploaded-files: notifications
daniele-verducci Aug 24, 2026
322772b
feature/1475-delete-uploaded-files: lint
daniele-verducci Aug 24, 2026
bbeb49d
feature/1475-delete-uploaded-files: Manage non-writeable folders
daniele-verducci Aug 24, 2026
3000ee9
feature/1475-delete-uploaded-files: Fix user race condition
daniele-verducci Aug 25, 2026
a10dfa1
feature/1475-delete-uploaded-files: Stats
daniele-verducci Aug 26, 2026
69d46e0
feature/1475-delete-uploaded-files: Fixed untouched files report
daniele-verducci Aug 26, 2026
312ab7a
feature/1475-delete-uploaded-files: Better user notifications
daniele-verducci Aug 26, 2026
858a5c7
feature/1475-delete-uploaded-files: spotless run
daniele-verducci Aug 27, 2026
3e1f092
feature/1475-delete-uploaded-files: show runtime in stats
daniele-verducci Aug 27, 2026
04d5325
feature/1475-delete-uploaded-files: more logs & user info, fix
daniele-verducci Aug 27, 2026
6de4c1a
feature/1475-delete-uploaded-files: apply to current user or all user…
daniele-verducci Aug 27, 2026
2e6e268
feature/1475-delete-uploaded-files: fixed bug preventing removal of p…
daniele-verducci Aug 27, 2026
6110dac
feature/1475-delete-uploaded-files: enabled real file deletion
daniele-verducci Aug 27, 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 @@ -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
Expand Down Expand Up @@ -107,6 +108,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 @@ -311,4 +313,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 @@ -171,4 +171,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 @@ -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"

Expand Down Expand Up @@ -824,4 +826,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,157 @@
/*
* 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

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, runTimeMs=$runTimeMs"
)
return Result.success()
}

private fun createSuccessNotification(
users: Int,
foldersRemoved: Long,
filesRemoved: Long,
filesPreserved: Long,
spaceFreed: Long,
timeElapsed: Long
): Notification {
var notificationContent = context.getString(
R.string.autoupload_delete_uploaded_notif_ended_content,
DisplayUtils.bytesToHumanReadable(spaceFreed),
filesRemoved,
foldersRemoved,
users,
DisplayUtils.unixTimeDurationToHumanReadable(context, timeElapsed)
)
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)
.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