From 688fdea470bbe00ce83144f2e8cf6166f2defdee Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 4 May 2026 11:04:02 +0200 Subject: [PATCH 1/6] handle(auto-upload): LOCKED Signed-off-by: alperozturk96 --- .../jobs/autoUpload/AutoUploadWorker.kt | 12 +++++++- .../client/jobs/upload/FileUploadWorker.kt | 8 ++++++ .../client/jobs/upload/UploadRetryPolicy.kt | 28 +++++++++++++++++++ .../utils/UploadErrorNotificationManager.kt | 8 +++++- .../java/com/nextcloud/utils/TimeConstants.kt | 1 + .../RemoteOperationResultExtensions.kt | 7 +++-- .../android/utils/ErrorMessageAdapter.java | 2 ++ app/src/main/res/values/strings.xml | 3 +- 8 files changed, 63 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/client/jobs/upload/UploadRetryPolicy.kt diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadWorker.kt index 84902f10becb..1a54ec206584 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadWorker.kt @@ -21,6 +21,7 @@ import com.nextcloud.client.device.PowerManagementService import com.nextcloud.client.jobs.upload.FileUploadEventBroadcaster import com.nextcloud.client.jobs.upload.FileUploadHelper import com.nextcloud.client.jobs.upload.FileUploadWorker +import com.nextcloud.client.jobs.upload.UploadRetryPolicy import com.nextcloud.client.jobs.utils.UploadErrorNotificationManager import com.nextcloud.client.network.ConnectivityService import com.nextcloud.utils.extensions.getLog @@ -46,6 +47,7 @@ import com.owncloud.android.ui.activity.SettingsActivity import com.owncloud.android.utils.theme.CapabilityUtils import com.owncloud.android.utils.theme.ViewThemeUtils import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.ensureActive import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext @@ -79,6 +81,7 @@ class AutoUploadWorker( private lateinit var syncedFolder: SyncedFolder private val notificationManager = AutoUploadNotificationManager(context, viewThemeUtils, NOTIFICATION_ID) private val fileUploadHelper = FileUploadHelper.instance() + private val retryPolicy = UploadRetryPolicy() @Suppress("ReturnCount") override suspend fun doWork(): Result { @@ -115,6 +118,8 @@ class AutoUploadWorker( } catch (e: Exception) { Log_OC.e(TAG, "❌ failed: ${e.message}") Result.failure() + } finally { + retryPolicy.reset() } } @@ -269,6 +274,8 @@ class AutoUploadWorker( filePathsWithIds.forEachIndexed { batchIndex, (path, id) -> ensureActive() + delay(retryPolicy.getDelay()) + val file = File(path) val localPath = file.absolutePath val remotePath = syncFolderHelper.getAutoUploadRemotePath(syncedFolder, file) @@ -308,7 +315,10 @@ class AutoUploadWorker( context, notificationManager, operation, - result + result, + onLocked = { + retryPolicy.increase() + } ) if (result.isSuccess) { diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt index ff931fa75578..94e9efd5bf09 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt @@ -47,6 +47,7 @@ import com.owncloud.android.operations.factory.UploadFileOperationFactory import com.owncloud.android.ui.notifications.NotificationUtils import com.owncloud.android.utils.theme.ViewThemeUtils import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.ensureActive import kotlinx.coroutines.withContext import java.io.File @@ -134,6 +135,7 @@ class FileUploadWorker( private val notificationManager = UploadNotificationManager(context, viewThemeUtils, notificationId) private val intents = FileUploaderIntents(context) private val fileUploadEventBroadcaster = FileUploadEventBroadcaster(localBroadcastManager) + private val retryPolicy = UploadRetryPolicy() override suspend fun doWork(): Result = try { trySetForeground() @@ -155,6 +157,7 @@ class FileUploadWorker( // Ensure all database operations are complete before signaling completion uploadsStorageManager.notifyObserversNow() notificationManager.dismissNotification() + retryPolicy.reset() } private suspend fun trySetForeground() { @@ -248,6 +251,8 @@ class FileUploadWorker( for ((index, upload) in uploads.withIndex()) { ensureActive() + delay(retryPolicy.getDelay()) + if (!skipAutoUploadCheck && isBelongToAnySyncedFolder(upload, syncFolderHelper, syncedFolders)) { Log_OC.d(TAG, "skipping upload, will be handled by AutoUploadWorker: ${upload.localPath}") uploadsStorageManager.uploadDao.deleteByRemotePathAndAccountName( @@ -394,6 +399,9 @@ class FileUploadWorker( notificationManager.showSameFileAlreadyExistsNotification(operation.fileName) } } + }, + onLocked = { + retryPolicy.increase() } ) } diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/UploadRetryPolicy.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/UploadRetryPolicy.kt new file mode 100644 index 000000000000..235102e39262 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/UploadRetryPolicy.kt @@ -0,0 +1,28 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.jobs.upload + +import com.nextcloud.utils.TimeConstants + +class UploadRetryPolicy { + private var delayInMs: Long = 0 + + fun increase() { + if (delayInMs >= TimeConstants.ONE_SECOND.times(10)) { + return + } + + delayInMs += TimeConstants.ONE_SECOND + } + + fun getDelay(): Long = delayInMs + + fun reset() { + delayInMs = 0L + } +} diff --git a/app/src/main/java/com/nextcloud/client/jobs/utils/UploadErrorNotificationManager.kt b/app/src/main/java/com/nextcloud/client/jobs/utils/UploadErrorNotificationManager.kt index e1ec1276ecfe..5a6b142f8724 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/utils/UploadErrorNotificationManager.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/utils/UploadErrorNotificationManager.kt @@ -52,7 +52,8 @@ object UploadErrorNotificationManager { notificationManager: WorkerNotificationManager, operation: UploadFileOperation, result: RemoteOperationResult, - onSameFileConflict: suspend () -> Unit = {} + onSameFileConflict: suspend () -> Unit = {}, + onLocked: () -> Unit = {} ) { Log_OC.d(TAG, "handle upload result with result code: " + result.code) @@ -107,6 +108,10 @@ object UploadErrorNotificationManager { Log_OC.d(TAG, "🔔" + "notification created") withContext(Dispatchers.Main) { + if (result.code == ResultCode.LOCKED) { + onLocked() + } + // if error code is file specific show new notification for each file if (result.code.isFileSpecificError()) { notificationManager.showNotification(operation.ocUploadId.toInt(), notification) @@ -163,6 +168,7 @@ object UploadErrorNotificationManager { ResultCode.UNAUTHORIZED -> R.string.uploader_upload_failed_credentials_error ResultCode.SYNC_CONFLICT -> R.string.uploader_upload_failed_sync_conflict_error ResultCode.CONFLICT -> R.string.uploader_upload_failed_sync_conflict_error + ResultCode.LOCKED -> R.string.upload_locked_title else -> R.string.uploader_upload_failed_ticker } diff --git a/app/src/main/java/com/nextcloud/utils/TimeConstants.kt b/app/src/main/java/com/nextcloud/utils/TimeConstants.kt index 7c5797649190..e73385176cda 100644 --- a/app/src/main/java/com/nextcloud/utils/TimeConstants.kt +++ b/app/src/main/java/com/nextcloud/utils/TimeConstants.kt @@ -8,5 +8,6 @@ package com.nextcloud.utils object TimeConstants { + const val ONE_SECOND = 1000L const val MILLIS_PER_SECOND = 1000 } diff --git a/app/src/main/java/com/nextcloud/utils/extensions/RemoteOperationResultExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/RemoteOperationResultExtensions.kt index e55d49e3e941..e0e29b337031 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/RemoteOperationResultExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/RemoteOperationResultExtensions.kt @@ -22,7 +22,7 @@ fun Pair?, RemoteOperation<*>?>?.getErrorMessage(): Str } fun ResultCode.isFileSpecificError(): Boolean { - val errorCodes = listOf( + val generalErrorCodes = listOf( ResultCode.INSTANCE_NOT_CONFIGURED, ResultCode.QUOTA_EXCEEDED, ResultCode.LOCAL_STORAGE_FULL, @@ -37,10 +37,11 @@ fun ResultCode.isFileSpecificError(): Boolean { ResultCode.ACCOUNT_NOT_FOUND, ResultCode.ACCOUNT_USES_STANDARD_PASSWORD, ResultCode.INCORRECT_ADDRESS, - ResultCode.BAD_OC_VERSION + ResultCode.BAD_OC_VERSION, + ResultCode.LOCKED // most likely following upload will fail as well, server still in progress ) - return !errorCodes.contains(this) + return !generalErrorCodes.contains(this) } fun ResultCode.isConflict(): Boolean { diff --git a/app/src/main/java/com/owncloud/android/utils/ErrorMessageAdapter.java b/app/src/main/java/com/owncloud/android/utils/ErrorMessageAdapter.java index a2b028d0ecb7..f14655cc4ef4 100644 --- a/app/src/main/java/com/owncloud/android/utils/ErrorMessageAdapter.java +++ b/app/src/main/java/com/owncloud/android/utils/ErrorMessageAdapter.java @@ -434,6 +434,8 @@ String getMessageForResult(RemoteOperationResult result, Resources res) { } else if (result.getCode() == ResultCode.QUOTA_EXCEEDED) { message = res.getString(R.string.upload_quota_exceeded); + } else if (result.getCode() == ResultCode.LOCKED) { + message = res.getString(R.string.upload_locked_message); } else if (!TextUtils.isEmpty(result.getHttpPhrase())) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f763f4f7973d..bf9083d5531f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1200,7 +1200,8 @@ Delete New %1$s %2$s - + Upload in progress + The server is still working on a previous upload. Retrying… File request View only Can edit From 2efe0d7247e421c55f61ef2803ca81635a2aa8f0 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 4 May 2026 11:16:50 +0200 Subject: [PATCH 2/6] add random delay Signed-off-by: alperozturk96 --- .../com/nextcloud/client/jobs/upload/UploadRetryPolicy.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/UploadRetryPolicy.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/UploadRetryPolicy.kt index 235102e39262..47cca9780127 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/UploadRetryPolicy.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/UploadRetryPolicy.kt @@ -8,16 +8,22 @@ package com.nextcloud.client.jobs.upload import com.nextcloud.utils.TimeConstants +import kotlin.random.Random class UploadRetryPolicy { private var delayInMs: Long = 0 + companion object { + private const val MAX_RANDOM_DELAY = 200L + } + fun increase() { if (delayInMs >= TimeConstants.ONE_SECOND.times(10)) { return } - delayInMs += TimeConstants.ONE_SECOND + // random next long used for prevent retrying at the same time if uploads are in parallel + delayInMs += (TimeConstants.ONE_SECOND + Random.nextLong(MAX_RANDOM_DELAY)) } fun getDelay(): Long = delayInMs From 211ec3da251c0fd7fe657ca57509c1da92c54f59 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 4 May 2026 11:17:00 +0200 Subject: [PATCH 3/6] fix skipped uploads clear function Signed-off-by: alperozturk96 --- .../android/datamodel/UploadsStorageManager.kt | 13 +++++++++++++ .../ui/adapter/uploadList/UploadListAdapter.kt | 7 ++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.kt b/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.kt index 73be317b47b5..e294114fb003 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.kt +++ b/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.kt @@ -459,6 +459,19 @@ class UploadsStorageManager( if (deleted > 0) notifyObserversNow() } + fun clearSkippedUploads() { + val user = currentAccountProvider.user + val deleted = contentResolver.delete( + ProviderTableMeta.CONTENT_URI_UPLOADS, + ProviderTableMeta.UPLOADS_STATUS + EQUAL + UploadStatus.UPLOAD_SUCCEEDED.value + + AND + ProviderTableMeta.UPLOADS_NAME_COLLISION_POLICY + EQUAL + NameCollisionPolicy.SKIP.serialize() + + AND + ProviderTableMeta.UPLOADS_ACCOUNT_NAME + IS_EQUAL, + arrayOf(user.accountName) + ) + Log_OC.d(TAG, "delete all skipped uploads") + if (deleted > 0) notifyObserversNow() + } + fun updateDatabaseUploadResult(uploadResult: RemoteOperationResult<*>, upload: UploadFileOperation) { Log_OC.d(TAG, "updateDatabaseUploadResult uploadResult: $uploadResult upload: $upload") diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/UploadListAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/UploadListAdapter.kt index 33b0a835cad1..a7b2c3c776bb 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/UploadListAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/UploadListAdapter.kt @@ -123,7 +123,7 @@ class UploadListAdapter( private fun bindHeaderActionButton(holder: HeaderViewHolder, group: UploadListSection) { val iconRes = when (group.type) { - UploadListType.CURRENT, UploadListType.COMPLETED -> R.drawable.ic_close + UploadListType.CURRENT, UploadListType.COMPLETED, UploadListType.SKIPPED -> R.drawable.ic_close UploadListType.CANCELLED, UploadListType.FAILED -> R.drawable.ic_dots_vertical else -> return } @@ -146,6 +146,11 @@ class UploadListAdapter( loadUploadItemsFromDb() } + UploadListType.SKIPPED -> { + uploadsStorageManager.clearSkippedUploads() + loadUploadItemsFromDb() + } + UploadListType.FAILED -> showFailedPopupMenu(holder) UploadListType.CANCELLED -> showCancelledPopupMenu(holder) From 36aa6e432d1884ba4f986875745a2601e35d8ed1 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 4 May 2026 11:23:04 +0200 Subject: [PATCH 4/6] fix codacy Signed-off-by: alperozturk96 --- .../java/com/nextcloud/client/jobs/upload/UploadRetryPolicy.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/UploadRetryPolicy.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/UploadRetryPolicy.kt index 47cca9780127..4a1d526a8798 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/UploadRetryPolicy.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/UploadRetryPolicy.kt @@ -10,6 +10,7 @@ package com.nextcloud.client.jobs.upload import com.nextcloud.utils.TimeConstants import kotlin.random.Random +@Suppress("MagicNumber") class UploadRetryPolicy { private var delayInMs: Long = 0 From 4bbf1611516df623eb1d727dc03e3735048cc659 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alper=20=C3=96zt=C3=BCrk?= <67455295+alperozturk96@users.noreply.github.com> Date: Mon, 4 May 2026 15:41:28 +0200 Subject: [PATCH 5/6] Update app/src/main/res/values/strings.xml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Laura Kramolis Signed-off-by: Alper Öztürk <67455295+alperozturk96@users.noreply.github.com> --- app/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bf9083d5531f..8fac148f5684 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1201,7 +1201,7 @@ New %1$s %2$s Upload in progress - The server is still working on a previous upload. Retrying… + The server is busy. Retrying… File request View only Can edit From bcc46f7ae78881018ea848bc02655985c0f91c6f Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 5 May 2026 08:22:24 +0200 Subject: [PATCH 6/6] adjust timing Signed-off-by: alperozturk96 --- .../nextcloud/client/jobs/autoUpload/AutoUploadWorker.kt | 4 ++-- .../com/nextcloud/client/jobs/upload/FileUploadWorker.kt | 2 +- .../{UploadRetryPolicy.kt => UploadDelayPolicy.kt} | 9 +++++---- app/src/main/java/com/nextcloud/utils/TimeConstants.kt | 1 - 4 files changed, 8 insertions(+), 8 deletions(-) rename app/src/main/java/com/nextcloud/client/jobs/upload/{UploadRetryPolicy.kt => UploadDelayPolicy.kt} (72%) diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadWorker.kt index 1a54ec206584..777d03235757 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadWorker.kt @@ -21,7 +21,7 @@ import com.nextcloud.client.device.PowerManagementService import com.nextcloud.client.jobs.upload.FileUploadEventBroadcaster import com.nextcloud.client.jobs.upload.FileUploadHelper import com.nextcloud.client.jobs.upload.FileUploadWorker -import com.nextcloud.client.jobs.upload.UploadRetryPolicy +import com.nextcloud.client.jobs.upload.UploadDelayPolicy import com.nextcloud.client.jobs.utils.UploadErrorNotificationManager import com.nextcloud.client.network.ConnectivityService import com.nextcloud.utils.extensions.getLog @@ -81,7 +81,7 @@ class AutoUploadWorker( private lateinit var syncedFolder: SyncedFolder private val notificationManager = AutoUploadNotificationManager(context, viewThemeUtils, NOTIFICATION_ID) private val fileUploadHelper = FileUploadHelper.instance() - private val retryPolicy = UploadRetryPolicy() + private val retryPolicy = UploadDelayPolicy() @Suppress("ReturnCount") override suspend fun doWork(): Result { diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt index 94e9efd5bf09..6524ac85714b 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt @@ -135,7 +135,7 @@ class FileUploadWorker( private val notificationManager = UploadNotificationManager(context, viewThemeUtils, notificationId) private val intents = FileUploaderIntents(context) private val fileUploadEventBroadcaster = FileUploadEventBroadcaster(localBroadcastManager) - private val retryPolicy = UploadRetryPolicy() + private val retryPolicy = UploadDelayPolicy() override suspend fun doWork(): Result = try { trySetForeground() diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/UploadRetryPolicy.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/UploadDelayPolicy.kt similarity index 72% rename from app/src/main/java/com/nextcloud/client/jobs/upload/UploadRetryPolicy.kt rename to app/src/main/java/com/nextcloud/client/jobs/upload/UploadDelayPolicy.kt index 4a1d526a8798..d1c4fef65e64 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/UploadRetryPolicy.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/UploadDelayPolicy.kt @@ -7,24 +7,25 @@ package com.nextcloud.client.jobs.upload -import com.nextcloud.utils.TimeConstants import kotlin.random.Random @Suppress("MagicNumber") -class UploadRetryPolicy { +class UploadDelayPolicy { private var delayInMs: Long = 0 companion object { + private const val MAX_DELAY = 120_000L + private const val INCREMENT_VALUE = 3500L private const val MAX_RANDOM_DELAY = 200L } fun increase() { - if (delayInMs >= TimeConstants.ONE_SECOND.times(10)) { + if (delayInMs >= MAX_DELAY) { return } // random next long used for prevent retrying at the same time if uploads are in parallel - delayInMs += (TimeConstants.ONE_SECOND + Random.nextLong(MAX_RANDOM_DELAY)) + delayInMs += (INCREMENT_VALUE + Random.nextLong(MAX_RANDOM_DELAY)) } fun getDelay(): Long = delayInMs diff --git a/app/src/main/java/com/nextcloud/utils/TimeConstants.kt b/app/src/main/java/com/nextcloud/utils/TimeConstants.kt index e73385176cda..7c5797649190 100644 --- a/app/src/main/java/com/nextcloud/utils/TimeConstants.kt +++ b/app/src/main/java/com/nextcloud/utils/TimeConstants.kt @@ -8,6 +8,5 @@ package com.nextcloud.utils object TimeConstants { - const val ONE_SECOND = 1000L const val MILLIS_PER_SECOND = 1000 }