Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -115,6 +118,8 @@ class AutoUploadWorker(
} catch (e: Exception) {
Log_OC.e(TAG, "❌ failed: ${e.message}")
Result.failure()
} finally {
retryPolicy.reset()
}
}

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -308,7 +315,10 @@ class AutoUploadWorker(
context,
notificationManager,
operation,
result
result,
onLocked = {
retryPolicy.increase()
}
)

if (result.isSuccess) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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() {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -394,6 +399,9 @@ class FileUploadWorker(
notificationManager.showSameFileAlreadyExistsNotification(operation.fileName)
}
}
},
onLocked = {
retryPolicy.increase()
}
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
* 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) {
Copy link
Copy Markdown
Collaborator Author

@alperozturk96 alperozturk96 May 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lock should only take a couple of seconds, maximum.

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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ object UploadErrorNotificationManager {
notificationManager: WorkerNotificationManager,
operation: UploadFileOperation,
result: RemoteOperationResult<Any?>,
onSameFileConflict: suspend () -> Unit = {}
onSameFileConflict: suspend () -> Unit = {},
onLocked: () -> Unit = {}
) {
Log_OC.d(TAG, "handle upload result with result code: " + result.code)

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ fun Pair<RemoteOperationResult<*>?, RemoteOperation<*>?>?.getErrorMessage(): Str
}

fun ResultCode.isFileSpecificError(): Boolean {
val errorCodes = listOf(
val generalErrorCodes = listOf(
ResultCode.INSTANCE_NOT_CONFIGURED,
ResultCode.QUOTA_EXCEEDED,
ResultCode.LOCAL_STORAGE_FULL,
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -146,6 +146,11 @@ class UploadListAdapter(
loadUploadItemsFromDb()
}

UploadListType.SKIPPED -> {
uploadsStorageManager.clearSkippedUploads()
loadUploadItemsFromDb()
}

UploadListType.FAILED -> showFailedPopupMenu(holder)

UploadListType.CANCELLED -> showCancelledPopupMenu(holder)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())) {
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1200,7 +1200,8 @@
<string name="upload_list_delete">Delete</string>
<string name="create_new">New</string>
<string name="editor_placeholder" translatable="false">%1$s %2$s</string>

<string name="upload_locked_title">Upload in progress</string>
<string name="upload_locked_message">The server is busy. Retrying…</string>
<string name="share_permission_file_request">File request</string>
<string name="share_permission_view_only">View only</string>
<string name="share_permission_can_edit">Can edit</string>
Expand Down
Loading