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..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,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.UploadDelayPolicy 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 = UploadDelayPolicy() @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..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 @@ -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 = UploadDelayPolicy() 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/UploadDelayPolicy.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/UploadDelayPolicy.kt new file mode 100644 index 000000000000..d1c4fef65e64 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/UploadDelayPolicy.kt @@ -0,0 +1,36 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.jobs.upload + +import kotlin.random.Random + +@Suppress("MagicNumber") +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 >= MAX_DELAY) { + return + } + + // random next long used for prevent retrying at the same time if uploads are in parallel + delayInMs += (INCREMENT_VALUE + Random.nextLong(MAX_RANDOM_DELAY)) + } + + 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/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/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) 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..8fac148f5684 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 busy. Retrying… File request View only Can edit