From ac550594840639970b8dde76a3b55bc61a2a309f Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 1 Apr 2026 12:53:18 +0200 Subject: [PATCH 1/2] fix(auto-upload): handle conflict and sync conflict Signed-off-by: alperozturk96 --- .../jobs/autoUpload/AutoUploadWorker.kt | 3 ++- .../client/jobs/upload/FileUploadHelper.kt | 3 ++- .../utils/UploadErrorNotificationManager.kt | 10 +++++--- .../utils/extensions/OCUploadExtensions.kt | 2 ++ .../RemoteOperationResultExtensions.kt | 9 +++++++ .../extensions/UploadResultExtensions.kt | 3 ++- .../datamodel/UploadsStorageManager.java | 3 ++- .../com/owncloud/android/db/UploadResult.java | 7 ++++++ .../operations/UploadFileOperation.java | 3 ++- .../adapter/uploadList/UploadListAdapter.kt | 25 +++++++++++-------- 10 files changed, 48 insertions(+), 20 deletions(-) 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 e174365a4a9e..446e3da55975 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 @@ -24,6 +24,7 @@ import com.nextcloud.client.jobs.upload.FileUploadWorker import com.nextcloud.client.jobs.utils.UploadErrorNotificationManager import com.nextcloud.client.network.ConnectivityService import com.nextcloud.utils.extensions.getLog +import com.nextcloud.utils.extensions.isConflict import com.nextcloud.utils.extensions.isNonRetryable import com.nextcloud.utils.extensions.updateStatus import com.owncloud.android.R @@ -319,7 +320,7 @@ class AutoUploadWorker( ) // Mark CONFLICT files as handled to prevent retries - if (result.code == RemoteOperationResult.ResultCode.SYNC_CONFLICT) { + if (result.code.isConflict()) { repository.markFileAsHandled(localPath, syncedFolder) Log_OC.w(TAG, "Marked CONFLICT file as handled: $localPath") } diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt index 2fd8ae6020cc..e8c3a173311d 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt @@ -24,6 +24,7 @@ import com.nextcloud.client.network.ConnectivityService import com.nextcloud.client.notifications.AppWideNotificationManager import com.nextcloud.utils.extensions.checkWCFRestrictions import com.nextcloud.utils.extensions.getUploadIds +import com.nextcloud.utils.extensions.isLastResultConflictError import com.owncloud.android.MainApp import com.owncloud.android.R import com.owncloud.android.datamodel.FileDataStorageManager @@ -180,7 +181,7 @@ class FileUploadHelper { val uploadsToRetry = mutableListOf() for (upload in uploads) { - if (upload.lastResult == UploadResult.SYNC_CONFLICT) { + if (upload.isLastResultConflictError()) { Log_OC.d(TAG, "retry upload skipped, sync conflict: ${upload.remotePath}") showSyncConflictNotification = true continue 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 c6289567d9fb..6b538dea1c90 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 @@ -15,6 +15,7 @@ import androidx.core.app.NotificationCompat import com.nextcloud.client.jobs.notification.WorkerNotificationManager import com.nextcloud.client.jobs.upload.FileUploadHelper import com.nextcloud.client.jobs.upload.UploadBroadcastAction +import com.nextcloud.utils.extensions.isConflict import com.nextcloud.utils.extensions.isFileSpecificError import com.owncloud.android.R import com.owncloud.android.authentication.AuthenticatorActivity @@ -33,11 +34,11 @@ object UploadErrorNotificationManager { /** * Processes the result of an upload operation and manages error notifications. - * * It filters out successful or silent results and handles [ResultCode.SYNC_CONFLICT] + * * It filters out successful or silent results and handles [ResultCode.SYNC_CONFLICT], [ResultCode.CONFLICT] * by checking if the remote file is identical. If it's a "real" conflict or error, * it displays a notification with relevant actions (e.g., Resolve Conflict, Pause, Cancel). * - * @param onSameFileConflict Triggered only if result code is SYNC_CONFLICT and files are identical. + * @param onSameFileConflict Triggered only if result code is CONFLICT or SYNC_CONFLICT and files are identical. */ @Suppress("ReturnCount") suspend fun handleResult( @@ -70,7 +71,7 @@ object UploadErrorNotificationManager { } // do not show an error notification when uploading the same file again - if (result.code == ResultCode.SYNC_CONFLICT) { + if (result.code.isConflict()) { val isSameFile = withContext(Dispatchers.IO) { FileUploadHelper.instance().isSameFileOnRemote( operation.user, @@ -135,7 +136,7 @@ object UploadErrorNotificationManager { // actions for all error types addAction(UploadBroadcastAction.PauseAndCancel(operation).cancelAction(context)) - if (result.code == ResultCode.SYNC_CONFLICT) { + if (result.code.isConflict()) { addAction( R.drawable.ic_cloud_upload, context.getString(R.string.upload_list_resolve_conflict), @@ -152,6 +153,7 @@ object UploadErrorNotificationManager { private fun ResultCode.toFailedResultTitleId(): Int = when (this) { 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 else -> R.string.uploader_upload_failed_ticker } diff --git a/app/src/main/java/com/nextcloud/utils/extensions/OCUploadExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/OCUploadExtensions.kt index df2b1c68059d..b9c4cbba4011 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/OCUploadExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/OCUploadExtensions.kt @@ -65,3 +65,5 @@ fun OCUpload.getStatusText(activity: Context, isGlobalUploadPaused: Boolean, isU return status } + +fun OCUpload.isLastResultConflictError(): Boolean = lastResult in UploadResult.CONFLICT_ERRORS 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 3a9f73c81f5f..e55d49e3e941 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/RemoteOperationResultExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/RemoteOperationResultExtensions.kt @@ -42,3 +42,12 @@ fun ResultCode.isFileSpecificError(): Boolean { return !errorCodes.contains(this) } + +fun ResultCode.isConflict(): Boolean { + val errorCodes = listOf( + ResultCode.SYNC_CONFLICT, + ResultCode.CONFLICT + ) + + return errorCodes.contains(this) +} diff --git a/app/src/main/java/com/nextcloud/utils/extensions/UploadResultExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/UploadResultExtensions.kt index 0a9da25da2e9..38614ed7dc45 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/UploadResultExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/UploadResultExtensions.kt @@ -17,10 +17,11 @@ fun UploadResult.isNonRetryable(): Boolean = when (this) { UploadResult.FOLDER_ERROR, UploadResult.CANNOT_CREATE_FILE, UploadResult.SYNC_CONFLICT, + UploadResult.CONFLICT_ERROR, + UploadResult.SAME_FILE_CONFLICT, UploadResult.LOCAL_STORAGE_NOT_COPIED, UploadResult.VIRUS_DETECTED, UploadResult.QUOTA_EXCEEDED, - UploadResult.SAME_FILE_CONFLICT, UploadResult.PRIVILEGES_ERROR, UploadResult.CREDENTIAL_ERROR, diff --git a/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.java b/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.java index 72065d317f56..9616641d9391 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.java @@ -30,6 +30,7 @@ import com.nextcloud.client.jobs.upload.FileUploadHelper; import com.nextcloud.client.jobs.upload.FileUploadWorker; import com.nextcloud.utils.autoRename.AutoRename; +import com.nextcloud.utils.extensions.RemoteOperationResultExtensionsKt; import com.owncloud.android.MainApp; import com.owncloud.android.db.OCUpload; import com.owncloud.android.db.ProviderMeta.ProviderTableMeta; @@ -577,7 +578,7 @@ public void updateDatabaseUploadResult(RemoteOperationResult uploadResult, Uploa if (uploadResult.isSuccess()) { status = UploadStatus.UPLOAD_SUCCEEDED; result = UploadResult.UPLOADED; - } else if (code == RemoteOperationResult.ResultCode.SYNC_CONFLICT) { + } else if (RemoteOperationResultExtensionsKt.isConflict(code)) { boolean isSame = new FileUploadHelper().isSameFileOnRemote( upload.getUser(), new File(upload.getStoragePath()), upload.getRemotePath(), upload.getContext()); diff --git a/app/src/main/java/com/owncloud/android/db/UploadResult.java b/app/src/main/java/com/owncloud/android/db/UploadResult.java index 8c523001cb2d..63ca74597e9f 100644 --- a/app/src/main/java/com/owncloud/android/db/UploadResult.java +++ b/app/src/main/java/com/owncloud/android/db/UploadResult.java @@ -12,6 +12,8 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult; +import java.util.ArrayList; +import java.util.List; import java.util.Map; public enum UploadResult { @@ -51,6 +53,11 @@ public int getValue() { return value; } + public static final List CONFLICT_ERRORS = List.of( + UploadResult.CONFLICT_ERROR, + UploadResult.SYNC_CONFLICT, + UploadResult.SAME_FILE_CONFLICT); + private static final Map valueMap = Map.ofEntries( Map.entry(0, UPLOADED), Map.entry(1, NETWORK_CONNECTION), diff --git a/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java b/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java index cfe827731dee..f966de291d7d 100644 --- a/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java @@ -25,6 +25,7 @@ import com.nextcloud.client.network.ConnectivityService; import com.nextcloud.utils.autoRename.AutoRename; import com.nextcloud.utils.e2ee.E2EVersionHelper; +import com.nextcloud.utils.extensions.RemoteOperationResultExtensionsKt; import com.owncloud.android.datamodel.ArbitraryDataProvider; import com.owncloud.android.datamodel.ArbitraryDataProviderImpl; import com.owncloud.android.datamodel.FileDataStorageManager; @@ -913,7 +914,7 @@ private void updateMetadataForV2(DecryptedFolderMetadataFile metadata, Encryptio private void completeE2EUpload(RemoteOperationResult result, E2EFiles e2eFiles, OwnCloudClient client) { if (result.isSuccess()) { handleLocalBehaviour(e2eFiles.getTemporalFile(), e2eFiles.getExpectedFile(), e2eFiles.getOriginalFile(), client); - } else if (result.getCode() == ResultCode.SYNC_CONFLICT) { + } else if (RemoteOperationResultExtensionsKt.isConflict(result.getCode())) { getStorageManager().saveConflict(mFile, mFile.getEtagInConflict()); } 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 54b0783f0761..ef9347912ddd 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 @@ -27,6 +27,7 @@ import com.nextcloud.client.jobs.upload.FileUploadHelper import com.nextcloud.client.jobs.upload.FileUploadWorker import com.nextcloud.client.network.ConnectivityService import com.nextcloud.utils.extensions.getStatusText +import com.nextcloud.utils.extensions.isLastResultConflictError import com.nextcloud.utils.extensions.setVisibleIf import com.nextcloud.utils.extensions.sortedByUploadOrder import com.nextcloud.utils.extensions.toFile @@ -418,7 +419,7 @@ class UploadListAdapter( UploadsStorageManager.UploadStatus.UPLOAD_FAILED -> { uploadRightButton.run { - if (item.lastResult == UploadResult.SYNC_CONFLICT) { + if (item.isLastResultConflictError()) { setImageResource(R.drawable.ic_dots_vertical) setOnClickListener { view -> optionalUser.ifPresent { user -> @@ -466,18 +467,20 @@ class UploadListAdapter( holder: ItemViewHolder, status: String ) { - when (item.lastResult) { - UploadResult.CREDENTIAL_ERROR -> { - val user = optionalUser.orElseThrow { RuntimeException() } - activity.fileOperationsHelper.checkCurrentCredentials(user) - } + if (optionalUser.isEmpty) { + return + } + val user = optionalUser.get() - UploadResult.SYNC_CONFLICT if optionalUser.isPresent -> { - if (checkAndOpenConflictResolutionDialog(optionalUser.get(), holder, item, status)) return - retryOrShowError(item) + if (item.lastResult == UploadResult.CREDENTIAL_ERROR) { + activity.fileOperationsHelper.checkCurrentCredentials(user) + } else if (item.isLastResultConflictError()) { + if (checkAndOpenConflictResolutionDialog(user, holder, item, status)) { + return } - - else -> retryOrShowError(item) + retryOrShowError(item) + } else { + retryOrShowError(item) } } From b6ffc7abc98d1e31653771943803dc5c0a62b91d Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 1 Apr 2026 13:10:00 +0200 Subject: [PATCH 2/2] fix(auto-upload): handle conflict and sync conflict Signed-off-by: alperozturk96 --- app/src/main/java/com/owncloud/android/db/UploadResult.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/db/UploadResult.java b/app/src/main/java/com/owncloud/android/db/UploadResult.java index 63ca74597e9f..79f25d9a4db6 100644 --- a/app/src/main/java/com/owncloud/android/db/UploadResult.java +++ b/app/src/main/java/com/owncloud/android/db/UploadResult.java @@ -55,8 +55,7 @@ public int getValue() { public static final List CONFLICT_ERRORS = List.of( UploadResult.CONFLICT_ERROR, - UploadResult.SYNC_CONFLICT, - UploadResult.SAME_FILE_CONFLICT); + UploadResult.SYNC_CONFLICT); private static final Map valueMap = Map.ofEntries( Map.entry(0, UPLOADED),