From 0797c8f445d108343316f75a9f770f28a66bf27c Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Fri, 4 Sep 2026 12:44:24 +0200 Subject: [PATCH 01/12] fix/internal-104835: do not overwrite existent file metadata Signed-off-by: daniele-verducci --- .../datamodel/FileDataStorageManager.java | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java index 42d5db1feb35..05d4cddbc750 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java @@ -230,8 +230,19 @@ public OfflineOperationEntity addCreateFolderOfflineOperation(String path, Strin return entity; } - public void createPendingFile(String path, String mimeType, long createdAt, long modificationTimestamp) { - OCFile file = new OCFile(path); + public void createPendingFile(String remotePath, String mimeType, long createdAt, long modificationTimestamp) { + final OCFile existingFile = getFileByRemotePath(remotePath); + final boolean existingFileIsTheSame = + existingFile != null && + existingFile.getMimeType().equals(mimeType) && + existingFile.getCreationTimestamp() == createdAt && + existingFile.getModificationTimestamp() == modificationTimestamp; + if (existingFileIsTheSame) { + // In case the same file was already uploaded, do not overwrite it to avoid triggering a conflict + return; + } + + OCFile file = new OCFile(remotePath); file.setMimeType(mimeType); file.setCreationTimestamp(createdAt); file.setModificationTimestamp(modificationTimestamp); @@ -541,7 +552,7 @@ public List getFolderImagesAndVideos(OCFile folder, boolean onlyOnDevice } public boolean saveFile(OCFile ocFile) { - Log_OC.d(TAG, "saving file: " + ocFile.getRemotePath()); + Log_OC.d(TAG, "saving file " + ocFile.getFileName() + " into " + ocFile.getRemotePath()); boolean overridden = false; final ContentValues cv = createContentValuesForFile(ocFile); From 0fce6f0ace8b04fe069e63e93777a9b18b0c94cf Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Fri, 4 Sep 2026 14:07:33 +0200 Subject: [PATCH 02/12] wip Signed-off-by: daniele-verducci --- .../com/nextcloud/client/jobs/upload/FileUploadWorker.kt | 5 +++++ 1 file changed, 5 insertions(+) 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 541e48c4827d..975db08a4e22 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 @@ -356,6 +356,11 @@ class FileUploadWorker( Log_OC.e(TAG, "credentials are no longer valid, stopping uploads") break } + + if (result.isSuccess && result.resultData is String) { + // TODO: Save this to avoid the conflict, both in online and offline uploads + operation.file.etagOnServer = result.resultData.toString() + } } val uploadFilesResult = when { From e34a82556615464ab5816676ab9aa6d942da0a59 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Tue, 8 Sep 2026 11:55:18 +0200 Subject: [PATCH 03/12] fix/internal-104835: avoid showing conflict window for an unmodified, already uploaded file Signed-off-by: daniele-verducci --- .../client/jobs/upload/FileUploadWorker.kt | 6 ++- .../datamodel/FileDataStorageManager.java | 42 ++++++++++++++----- 2 files changed, 36 insertions(+), 12 deletions(-) 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 975db08a4e22..91765daa9fd8 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 @@ -358,8 +358,10 @@ class FileUploadWorker( } if (result.isSuccess && result.resultData is String) { - // TODO: Save this to avoid the conflict, both in online and offline uploads - operation.file.etagOnServer = result.resultData.toString() + // Update the remote etag to avoid conflict when uploading same file, both in online and offline uploads + val file = operation.storageManager.getFileByRemotePath(upload.remotePath) + file?.etagOnServer = result.resultData.toString() + operation.storageManager.saveFile(file) } } diff --git a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java index 05d4cddbc750..51c8151cf0c3 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java @@ -28,6 +28,7 @@ import android.os.RemoteException; import android.provider.MediaStore; import android.text.TextUtils; +import android.util.Log; import com.google.gson.Gson; import com.google.gson.JsonSyntaxException; @@ -78,6 +79,8 @@ import java.io.File; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -200,7 +203,7 @@ public void addCreateFileOfflineOperation(String[] localPaths, String[] remotePa } offlineOperationDao.insert(entity); - createPendingFile(remotePath, mimeType, createdAt, modificationTimestamp); + createPendingFile(remotePath, mimeType, createdAt, modificationTimestamp, localPath); } } @@ -230,16 +233,15 @@ public OfflineOperationEntity addCreateFolderOfflineOperation(String path, Strin return entity; } - public void createPendingFile(String remotePath, String mimeType, long createdAt, long modificationTimestamp) { + public void createPendingFile(String remotePath, String mimeType, long createdAt, long modificationTimestamp, String localPath) { final OCFile existingFile = getFileByRemotePath(remotePath); - final boolean existingFileIsTheSame = - existingFile != null && - existingFile.getMimeType().equals(mimeType) && - existingFile.getCreationTimestamp() == createdAt && - existingFile.getModificationTimestamp() == modificationTimestamp; - if (existingFileIsTheSame) { - // In case the same file was already uploaded, do not overwrite it to avoid triggering a conflict - return; + if (existingFile != null) { + final File localFile = new File(localPath); + if (fileIsTheSame(existingFile, localFile)) { + // In case the same file was already uploaded, do not overwrite it to avoid triggering a conflict + Log_OC.i(TAG, "Creating pendingFile for an already uploaded file: keeping metadata"); + return; + } } OCFile file = new OCFile(remotePath); @@ -249,6 +251,26 @@ public void createPendingFile(String remotePath, String mimeType, long createdAt saveFileWithParent(file, MainApp.getAppContext()); } + private boolean fileIsTheSame(OCFile ocFile, File localFile) { + try { + BasicFileAttributes attr = Files.readAttributes(localFile.toPath(), BasicFileAttributes.class); + String localName = localFile.getName(); + String remoteName = ocFile.getFileName(); + long localCreated = attr.creationTime().toMillis() / 1000; // Unix time in milliseconds + long localModified = attr.lastModifiedTime().toMillis() / 1000; // Unix time in milliseconds + long remoteCreated = ocFile.getCreationTimestamp(); // Unix time in seconds! + long remoteModified = ocFile.getModificationTimestamp() / 1000; // Unix time in milliseconds + final boolean existingFileIsTheSame = + remoteName.equals(localName) && + remoteCreated == localCreated && + remoteModified == localModified; + return existingFileIsTheSame; + } catch (IOException e) { + Log.e(TAG, "fileIsTheSame: unable to obtain local file attributes for comparing"); + return false; + } + } + public void createPendingDirectory(String path, long createdAt, long modificationTimestamp) { OCFile directory = new OCFile(path); directory.setMimeType(MimeType.DIRECTORY); From 4acbe4edcb0608d3f84eafcd94853927ce7f20be Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Tue, 8 Sep 2026 13:05:57 +0200 Subject: [PATCH 04/12] fix/internal-104835: check size as well Signed-off-by: daniele-verducci --- .../owncloud/android/datamodel/FileDataStorageManager.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java index 51c8151cf0c3..c2e082335f43 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java @@ -256,12 +256,16 @@ private boolean fileIsTheSame(OCFile ocFile, File localFile) { BasicFileAttributes attr = Files.readAttributes(localFile.toPath(), BasicFileAttributes.class); String localName = localFile.getName(); String remoteName = ocFile.getFileName(); + long localSize = localFile.length(); + long remoteSize = ocFile.getFileLength(); long localCreated = attr.creationTime().toMillis() / 1000; // Unix time in milliseconds long localModified = attr.lastModifiedTime().toMillis() / 1000; // Unix time in milliseconds long remoteCreated = ocFile.getCreationTimestamp(); // Unix time in seconds! long remoteModified = ocFile.getModificationTimestamp() / 1000; // Unix time in milliseconds + Log.d(TAG, "localCreated:" + localCreated + " localModified:" + localModified + " remoteCreated" + remoteCreated + " remoteModified:" + remoteModified); final boolean existingFileIsTheSame = remoteName.equals(localName) && + remoteSize == localSize && remoteCreated == localCreated && remoteModified == localModified; return existingFileIsTheSame; From 7b1188df418c52fe6a8caca0b1b983958aec6fb2 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Tue, 8 Sep 2026 18:05:01 +0200 Subject: [PATCH 05/12] fix/internal-104835: lint Signed-off-by: daniele-verducci --- .../android/datamodel/FileDataStorageManager.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java index c2e082335f43..b2a721b36900 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java @@ -233,7 +233,13 @@ public OfflineOperationEntity addCreateFolderOfflineOperation(String path, Strin return entity; } - public void createPendingFile(String remotePath, String mimeType, long createdAt, long modificationTimestamp, String localPath) { + public void createPendingFile( + String remotePath, + String mimeType, + long createdAt, + long modificationTimestamp, + String localPath + ) { final OCFile existingFile = getFileByRemotePath(remotePath); if (existingFile != null) { final File localFile = new File(localPath); @@ -262,13 +268,11 @@ private boolean fileIsTheSame(OCFile ocFile, File localFile) { long localModified = attr.lastModifiedTime().toMillis() / 1000; // Unix time in milliseconds long remoteCreated = ocFile.getCreationTimestamp(); // Unix time in seconds! long remoteModified = ocFile.getModificationTimestamp() / 1000; // Unix time in milliseconds - Log.d(TAG, "localCreated:" + localCreated + " localModified:" + localModified + " remoteCreated" + remoteCreated + " remoteModified:" + remoteModified); - final boolean existingFileIsTheSame = + return remoteName.equals(localName) && remoteSize == localSize && remoteCreated == localCreated && remoteModified == localModified; - return existingFileIsTheSame; } catch (IOException e) { Log.e(TAG, "fileIsTheSame: unable to obtain local file attributes for comparing"); return false; From a7a86847de7435903db9f6038ed84e5571bf2067 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Wed, 9 Sep 2026 12:09:31 +0200 Subject: [PATCH 06/12] Refactored java function: moved into FileExtensions Signed-off-by: daniele-verducci --- .../utils/extensions/FileExtensions.kt | 27 ++++++++++++++ .../datamodel/FileDataStorageManager.java | 37 +++---------------- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/com/nextcloud/utils/extensions/FileExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/FileExtensions.kt index 138af0d0060c..fa5e5d9085ff 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/FileExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/FileExtensions.kt @@ -8,14 +8,19 @@ package com.nextcloud.utils.extensions import android.graphics.Bitmap +import android.util.Log import androidx.exifinterface.media.ExifInterface +import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.OCFile import com.owncloud.android.datamodel.ThumbnailsCacheManager import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.lib.resources.files.model.ServerFileInterface import com.owncloud.android.utils.DisplayUtils import java.io.File +import java.io.IOException +import java.nio.file.Files import java.nio.file.Path +import java.nio.file.attribute.BasicFileAttributes private const val TAG = "FileExtensions" @@ -111,3 +116,25 @@ fun String.getBitmapSize(): Pair? = try { } catch (_: Exception) { null } + +fun OCFile?.isTheSameAs(localFile: File?): Boolean = try { + this ?: return false + localFile ?: return false + + val attr = Files.readAttributes(localFile.toPath(), BasicFileAttributes::class.java) + val localName = localFile.getName() + val remoteName = this.fileName + val localSize = localFile.length() + val remoteSize = this.fileLength + val localCreated = attr.creationTime().toMillis() / 1000 // Unix time in milliseconds + val localModified = attr.lastModifiedTime().toMillis() / 1000 // Unix time in milliseconds + val remoteCreated = this.creationTimestamp // Unix time in seconds! + val remoteModified = this.modificationTimestamp / 1000 // Unix time in milliseconds + remoteName == localName && + remoteSize == localSize && + remoteCreated == localCreated && + remoteModified == localModified +} catch (e: IOException) { + Log.e(FileDataStorageManager.TAG, "fileIsTheSame: unable to obtain local file attributes for comparing: $e") + false +} diff --git a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java index b2a721b36900..0dbcef72b5c3 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java @@ -28,7 +28,6 @@ import android.os.RemoteException; import android.provider.MediaStore; import android.text.TextUtils; -import android.util.Log; import com.google.gson.Gson; import com.google.gson.JsonSyntaxException; @@ -79,8 +78,6 @@ import java.io.File; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -241,13 +238,11 @@ public void createPendingFile( String localPath ) { final OCFile existingFile = getFileByRemotePath(remotePath); - if (existingFile != null) { - final File localFile = new File(localPath); - if (fileIsTheSame(existingFile, localFile)) { - // In case the same file was already uploaded, do not overwrite it to avoid triggering a conflict - Log_OC.i(TAG, "Creating pendingFile for an already uploaded file: keeping metadata"); - return; - } + final File localFile = FileExtensionsKt.toFile(localPath); + if (FileExtensionsKt.isTheSameAs(existingFile, localFile)) { + // In case the same file was already uploaded, do not overwrite it to avoid triggering a conflict + Log_OC.i(TAG, "Creating pendingFile for an already uploaded file: keeping metadata"); + return; } OCFile file = new OCFile(remotePath); @@ -257,28 +252,6 @@ public void createPendingFile( saveFileWithParent(file, MainApp.getAppContext()); } - private boolean fileIsTheSame(OCFile ocFile, File localFile) { - try { - BasicFileAttributes attr = Files.readAttributes(localFile.toPath(), BasicFileAttributes.class); - String localName = localFile.getName(); - String remoteName = ocFile.getFileName(); - long localSize = localFile.length(); - long remoteSize = ocFile.getFileLength(); - long localCreated = attr.creationTime().toMillis() / 1000; // Unix time in milliseconds - long localModified = attr.lastModifiedTime().toMillis() / 1000; // Unix time in milliseconds - long remoteCreated = ocFile.getCreationTimestamp(); // Unix time in seconds! - long remoteModified = ocFile.getModificationTimestamp() / 1000; // Unix time in milliseconds - return - remoteName.equals(localName) && - remoteSize == localSize && - remoteCreated == localCreated && - remoteModified == localModified; - } catch (IOException e) { - Log.e(TAG, "fileIsTheSame: unable to obtain local file attributes for comparing"); - return false; - } - } - public void createPendingDirectory(String path, long createdAt, long modificationTimestamp) { OCFile directory = new OCFile(path); directory.setMimeType(MimeType.DIRECTORY); From c27ef930d69acd69e6c8502c4dc7be7471f326df Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Wed, 9 Sep 2026 12:11:32 +0200 Subject: [PATCH 07/12] Removed wrongly-placed file update code from uploadworker and updaging etagOnServer field properly in UploadFileOperation Signed-off-by: daniele-verducci --- .../com/nextcloud/client/jobs/upload/FileUploadWorker.kt | 7 ------- .../owncloud/android/operations/UploadFileOperation.java | 1 + 2 files changed, 1 insertion(+), 7 deletions(-) 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 91765daa9fd8..541e48c4827d 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 @@ -356,13 +356,6 @@ class FileUploadWorker( Log_OC.e(TAG, "credentials are no longer valid, stopping uploads") break } - - if (result.isSuccess && result.resultData is String) { - // Update the remote etag to avoid conflict when uploading same file, both in online and offline uploads - val file = operation.storageManager.getFileByRemotePath(upload.remotePath) - file?.etagOnServer = result.resultData.toString() - operation.storageManager.saveFile(file) - } } val uploadFilesResult = when { 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 d525f37de441..953566e90393 100644 --- a/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java @@ -1786,6 +1786,7 @@ private void updateOCFile(OCFile file, RemoteFile remoteFile) { file.setModificationTimestamp(remoteFile.getModifiedTimestamp()); file.setModificationTimestampAtLastSyncForData(remoteFile.getModifiedTimestamp()); file.setEtag(remoteFile.getEtag()); + file.setEtagOnServer(remoteFile.getEtag()); file.setRemoteId(remoteFile.getRemoteId()); file.setPermissions(remoteFile.getPermissions()); file.setUploadTimestamp(remoteFile.getUploadTimestamp()); From 1c3ea1ccda2ff30ff1e286176f37942c9aa157d7 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Wed, 9 Sep 2026 12:19:34 +0200 Subject: [PATCH 08/12] Fixed detekt Signed-off-by: daniele-verducci --- .../java/com/nextcloud/utils/extensions/FileExtensions.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/nextcloud/utils/extensions/FileExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/FileExtensions.kt index fa5e5d9085ff..4b7187f3760c 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/FileExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/FileExtensions.kt @@ -23,6 +23,7 @@ import java.nio.file.Path import java.nio.file.attribute.BasicFileAttributes private const val TAG = "FileExtensions" +private const val MS_IN_SECOND = 1000 fun OCFile?.logFileSize(tag: String) { val size = DisplayUtils.bytesToHumanReadable(this?.fileLength ?: -1) @@ -126,10 +127,10 @@ fun OCFile?.isTheSameAs(localFile: File?): Boolean = try { val remoteName = this.fileName val localSize = localFile.length() val remoteSize = this.fileLength - val localCreated = attr.creationTime().toMillis() / 1000 // Unix time in milliseconds - val localModified = attr.lastModifiedTime().toMillis() / 1000 // Unix time in milliseconds + val localCreated = attr.creationTime().toMillis() / MS_IN_SECOND // Unix time in milliseconds + val localModified = attr.lastModifiedTime().toMillis() / MS_IN_SECOND // Unix time in milliseconds val remoteCreated = this.creationTimestamp // Unix time in seconds! - val remoteModified = this.modificationTimestamp / 1000 // Unix time in milliseconds + val remoteModified = this.modificationTimestamp / MS_IN_SECOND // Unix time in milliseconds remoteName == localName && remoteSize == localSize && remoteCreated == localCreated && From d342ff8153b310aa914e61c091f41f2ba0715692 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Wed, 9 Sep 2026 16:20:49 +0200 Subject: [PATCH 09/12] Working fix proof of concept, to be refactored and not taking account edge cases Signed-off-by: daniele-verducci --- .../repository/OfflineOperationsRepository.kt | 20 +++++++++++++++++++ .../OfflineOperationsRepositoryType.kt | 1 + .../datamodel/FileDataStorageManager.java | 5 +++-- .../ui/activity/ConflictsResolveActivity.kt | 7 ++++--- .../conflict/ConflictResolveDialogFactory.kt | 13 +++++++++++- 5 files changed, 40 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/repository/OfflineOperationsRepository.kt b/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/repository/OfflineOperationsRepository.kt index 86170e455de3..653712c40bd8 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/repository/OfflineOperationsRepository.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/repository/OfflineOperationsRepository.kt @@ -101,6 +101,26 @@ class OfflineOperationsRepository(private val fileDataStorageManager: FileDataSt .forEach { dao.update(it) } } + override fun updateOperationForMove(operation: OfflineOperationEntity, file: OCFile, newPath: String) { + operation.apply { + type = when (type) { + is OfflineOperationType.CreateFile -> + (type as OfflineOperationType.CreateFile).copy( + remotePath = newPath + ) + + is OfflineOperationType.CreateFolder -> + (type as OfflineOperationType.CreateFolder).copy( + path = newPath + ) + + else -> type + } + path = newPath + } + dao.update(operation) + } + override fun convertToOCFiles(fileId: Long): List = dao.getSubEntitiesByParentOCFileId(fileId).map { entity -> OCFile(entity.path).apply { diff --git a/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/repository/OfflineOperationsRepositoryType.kt b/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/repository/OfflineOperationsRepositoryType.kt index b6509093fac9..a735a365ca8c 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/repository/OfflineOperationsRepositoryType.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/repository/OfflineOperationsRepositoryType.kt @@ -14,5 +14,6 @@ interface OfflineOperationsRepositoryType { fun getAllSubEntities(fileId: Long): List fun deleteOperation(file: OCFile) fun updateNextOperations(operation: OfflineOperationEntity) + fun updateOperationForMove(operation: OfflineOperationEntity, file: OCFile, newPath: String) fun convertToOCFiles(fileId: Long): List } diff --git a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java index 0dbcef72b5c3..81b115606261 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java @@ -371,9 +371,10 @@ public void keepOfflineOperationAndServerFile(OfflineOperationEntity entity, OCF String currentDateTime = DateExtensionsKt.currentDateRepresentation(new Date(), formatPattern); String newFolderName = oldFileName + " - " + currentDateTime; - String newPath = parentFolder.getDecryptedRemotePath() + newFolderName + OCFile.PATH_SEPARATOR; + String newPath = parentFolder.getDecryptedRemotePath() + newFolderName + + (file.isFolder() ? OCFile.PATH_SEPARATOR : ""); + offlineOperationsRepository.updateOperationForMove(entity, file, newPath); moveLocalFile(file, newPath, parentFolder.getDecryptedRemotePath()); - offlineOperationsRepository.updateNextOperations(entity); } @Nullable diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.kt index 5f9d0caed0db..abeee2fe4b73 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.kt @@ -37,9 +37,9 @@ import com.owncloud.android.files.services.NameCollisionPolicy import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation import com.owncloud.android.lib.resources.files.model.RemoteFile +import com.owncloud.android.ui.dialog.conflict.ConflictResolveDialogFactory import com.owncloud.android.ui.dialog.conflict.ConflictsResolveDialog.Decision import com.owncloud.android.ui.dialog.conflict.ConflictsResolveDialog.OnConflictDecisionMadeListener -import com.owncloud.android.ui.dialog.conflict.ConflictResolveDialogFactory import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.FileStorageUtils import kotlinx.coroutines.Dispatchers @@ -272,11 +272,12 @@ class ConflictsResolveActivity : return } - val (ft, _) = prepareDialogTransaction() + val (ft, user) = prepareDialogTransaction() ConflictResolveDialogFactory.forOffline( context = this, leftFile = offlineOperation, - rightFile = newFile!! + rightFile = newFile!!, + user = user ).show(ft, "conflictDialog") } diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/conflict/ConflictResolveDialogFactory.kt b/app/src/main/java/com/owncloud/android/ui/dialog/conflict/ConflictResolveDialogFactory.kt index 9a6fd6a32a98..aa8e6b9e3497 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/conflict/ConflictResolveDialogFactory.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/conflict/ConflictResolveDialogFactory.kt @@ -12,6 +12,8 @@ import android.os.Bundle import androidx.annotation.StringRes import com.nextcloud.client.account.User import com.nextcloud.client.database.entity.OfflineOperationEntity +import com.nextcloud.model.OfflineOperationType +import com.nextcloud.utils.extensions.toFile import com.owncloud.android.R import com.owncloud.android.datamodel.OCFile import com.owncloud.android.ui.dialog.conflict.model.ConflictDialogData @@ -25,7 +27,8 @@ object ConflictResolveDialogFactory { private const val SECONDS_TO_MILLIS = 1000L private const val UNKNOWN_FOLDER_SIZE = 0L - fun forOffline(context: Context, leftFile: OfflineOperationEntity, rightFile: OCFile): ConflictsResolveDialog { + fun forOffline(context: Context, leftFile: OfflineOperationEntity, rightFile: OCFile, user: User?): + ConflictsResolveDialog { val data = ConflictDialogData( headline = context.getString(R.string.conflict_folder_headline), description = context.getString(R.string.conflict_message_description_for_folder), @@ -37,8 +40,16 @@ object ConflictResolveDialogFactory { serverFile = context.conflictFileData(R.string.prefs_synced_folders_remote_path_title, rightFile) ) + val localFile = + if (leftFile.type is OfflineOperationType.CreateFile) + (leftFile.type as OfflineOperationType.CreateFile).localPath.toFile() + else + null + return createDialog(ConflictDialogType.Offline(data)) { + putSerializable(ConflictsResolveDialog.ARG_LEFT_FILE, localFile) putParcelable(ConflictsResolveDialog.ARG_RIGHT_FILE, rightFile) + putParcelable(ConflictsResolveDialog.ARG_USER, user) } } From 0ee60204cfc863e9779bb2743cfa0628d7940140 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Wed, 9 Sep 2026 17:02:14 +0200 Subject: [PATCH 10/12] Working conflict resolution for "keep both" in offline upload, aligned new name to the one used for online Signed-off-by: daniele-verducci --- .../repository/OfflineOperationsRepository.kt | 35 +++++++------------ .../OfflineOperationsRepositoryType.kt | 2 +- .../datamodel/FileDataStorageManager.java | 22 ++++++------ .../ui/activity/ConflictsResolveActivity.kt | 6 +++- 4 files changed, 28 insertions(+), 37 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/repository/OfflineOperationsRepository.kt b/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/repository/OfflineOperationsRepository.kt index 653712c40bd8..02821e438543 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/repository/OfflineOperationsRepository.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/repository/OfflineOperationsRepository.kt @@ -73,27 +73,7 @@ class OfflineOperationsRepository(private val fileDataStorageManager: FileDataSt fileDataStorageManager.getFileById(parentId)?.let { ocFile -> ocFile.decryptedRemotePath?.let { updatedPath -> val newPath = updatedPath + nextOperation.filename + pathSeparator - - if (newPath != nextOperation.path) { - nextOperation.apply { - type = when (type) { - is OfflineOperationType.CreateFile -> - (type as OfflineOperationType.CreateFile).copy( - remotePath = newPath - ) - - is OfflineOperationType.CreateFolder -> - (type as OfflineOperationType.CreateFolder).copy( - path = newPath - ) - - else -> type - } - path = newPath - } - } else { - null - } + updateOperationPath(newPath, nextOperation) } } } @@ -101,8 +81,11 @@ class OfflineOperationsRepository(private val fileDataStorageManager: FileDataSt .forEach { dao.update(it) } } - override fun updateOperationForMove(operation: OfflineOperationEntity, file: OCFile, newPath: String) { - operation.apply { + private fun updateOperationPath( + newPath: String, + nextOperation: OfflineOperationEntity + ): OfflineOperationEntity? = if (newPath != nextOperation.path) { + nextOperation.apply { type = when (type) { is OfflineOperationType.CreateFile -> (type as OfflineOperationType.CreateFile).copy( @@ -118,6 +101,12 @@ class OfflineOperationsRepository(private val fileDataStorageManager: FileDataSt } path = newPath } + } else { + null + } + + override fun updateOperationForKeepBoth(operation: OfflineOperationEntity, newPath: String) { + updateOperationPath(newPath, operation) dao.update(operation) } diff --git a/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/repository/OfflineOperationsRepositoryType.kt b/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/repository/OfflineOperationsRepositoryType.kt index a735a365ca8c..51be196fffe7 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/repository/OfflineOperationsRepositoryType.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/repository/OfflineOperationsRepositoryType.kt @@ -14,6 +14,6 @@ interface OfflineOperationsRepositoryType { fun getAllSubEntities(fileId: Long): List fun deleteOperation(file: OCFile) fun updateNextOperations(operation: OfflineOperationEntity) - fun updateOperationForMove(operation: OfflineOperationEntity, file: OCFile, newPath: String) + fun updateOperationForKeepBoth(operation: OfflineOperationEntity, newPath: String) fun convertToOCFiles(fileId: Long): List } diff --git a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java index 81b115606261..e6f60968ecfa 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java @@ -54,6 +54,7 @@ import com.owncloud.android.MainApp; import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFolderMetadataFile; import com.owncloud.android.db.ProviderMeta.ProviderTableMeta; +import com.owncloud.android.lib.common.OwnCloudClient; import com.owncloud.android.lib.common.network.WebdavEntry; import com.owncloud.android.lib.common.utils.Log_OC; import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation; @@ -70,6 +71,7 @@ import com.owncloud.android.lib.resources.status.OCCapability; import com.owncloud.android.lib.resources.tags.Tag; import com.owncloud.android.operations.RemoteOperationFailedException; +import com.owncloud.android.operations.UploadFileOperation; import com.owncloud.android.utils.FileStorageUtils; import com.owncloud.android.utils.MimeType; import com.owncloud.android.utils.MimeTypeUtil; @@ -354,10 +356,7 @@ public void renameOfflineOperation(OCFile file, String newFolderName) { moveLocalFile(file, newPath, parentFolder.getDecryptedRemotePath()); } - @SuppressLint("SimpleDateFormat") - public void keepOfflineOperationAndServerFile(OfflineOperationEntity entity, OCFile file) { - if (file == null) return; - + public void keepOfflineOperationAndServerFile(OfflineOperationEntity entity, OCFile file, OwnCloudClient client) { String oldFileName = entity.getFilename(); if (oldFileName == null) return; @@ -367,14 +366,13 @@ public void keepOfflineOperationAndServerFile(OfflineOperationEntity entity, OCF OCFile parentFolder = getFileById(parentOCFileId); if (parentFolder == null) return; - DateFormatPattern formatPattern = DateFormatPattern.FullDateWithHours; - String currentDateTime = DateExtensionsKt.currentDateRepresentation(new Date(), formatPattern); - - String newFolderName = oldFileName + " - " + currentDateTime; - String newPath = parentFolder.getDecryptedRemotePath() + newFolderName + - (file.isFolder() ? OCFile.PATH_SEPARATOR : ""); - offlineOperationsRepository.updateOperationForMove(entity, file, newPath); - moveLocalFile(file, newPath, parentFolder.getDecryptedRemotePath()); + final String newPath = UploadFileOperation.getNewAvailableRemotePath( + client, + (entity.getPath() != null) ? entity.getPath() : file.getDecryptedRemotePath(), + List.of(oldFileName), + file.isEncrypted() + ); + offlineOperationsRepository.updateOperationForKeepBoth(entity, newPath); } @Nullable diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.kt index abeee2fe4b73..280623aece4e 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.kt @@ -201,7 +201,11 @@ class ConflictsResolveActivity : private suspend fun keepBothFolder(offlineOperation: OfflineOperationEntity?, serverFile: OCFile?) { offlineOperation ?: return - fileDataStorageManager.keepOfflineOperationAndServerFile(offlineOperation, serverFile) + fileDataStorageManager.keepOfflineOperationAndServerFile( + offlineOperation, + serverFile, + clientRepository.getOwncloudClient() + ) backgroundJobManager.startOfflineOperations() withContext(Dispatchers.Main) { offlineOperationNotificationManager.dismissNotification(offlineOperation.id) From 971ae9a56ceeda844d29bd17faec703b5c763358 Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Wed, 9 Sep 2026 17:29:21 +0200 Subject: [PATCH 11/12] Fixed conflict resolution dialog content for offline uploads Signed-off-by: daniele-verducci --- .../conflict/ConflictResolveDialogFactory.kt | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/conflict/ConflictResolveDialogFactory.kt b/app/src/main/java/com/owncloud/android/ui/dialog/conflict/ConflictResolveDialogFactory.kt index aa8e6b9e3497..ad54d70cb079 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/conflict/ConflictResolveDialogFactory.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/conflict/ConflictResolveDialogFactory.kt @@ -24,21 +24,8 @@ import java.io.File object ConflictResolveDialogFactory { - private const val SECONDS_TO_MILLIS = 1000L - private const val UNKNOWN_FOLDER_SIZE = 0L - fun forOffline(context: Context, leftFile: OfflineOperationEntity, rightFile: OCFile, user: User?): ConflictsResolveDialog { - val data = ConflictDialogData( - headline = context.getString(R.string.conflict_folder_headline), - description = context.getString(R.string.conflict_message_description_for_folder), - localFile = context.conflictFileData( - titleId = R.string.prefs_synced_folders_local_path_title, - timestamp = (leftFile.createdAt ?: 0L) * SECONDS_TO_MILLIS, - fileLength = UNKNOWN_FOLDER_SIZE - ), - serverFile = context.conflictFileData(R.string.prefs_synced_folders_remote_path_title, rightFile) - ) val localFile = if (leftFile.type is OfflineOperationType.CreateFile) @@ -46,6 +33,17 @@ object ConflictResolveDialogFactory { else null + val data = ConflictDialogData( + headline = context.getString(R.string.choose_which_file), + description = context.getString(R.string.conflict_message_description), + localFile = context.conflictFileData( + titleId = R.string.conflict_local_file, + timestamp = (localFile?.lastModified() ?: 0L), + fileLength = (localFile?.length() ?: 0L) + ), + serverFile = context.conflictFileData(R.string.prefs_synced_folders_remote_path_title, rightFile) + ) + return createDialog(ConflictDialogType.Offline(data)) { putSerializable(ConflictsResolveDialog.ARG_LEFT_FILE, localFile) putParcelable(ConflictsResolveDialog.ARG_RIGHT_FILE, rightFile) From 57df3ddc7bb5878ee02757bf97e3fbb267a61d5b Mon Sep 17 00:00:00 2001 From: daniele-verducci Date: Wed, 9 Sep 2026 17:46:57 +0200 Subject: [PATCH 12/12] Fixed lint Signed-off-by: daniele-verducci --- .../repository/OfflineOperationsRepository.kt | 40 +++++++++---------- .../conflict/ConflictResolveDialogFactory.kt | 14 ++++--- app/src/main/res/values/strings.xml | 2 - 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/repository/OfflineOperationsRepository.kt b/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/repository/OfflineOperationsRepository.kt index 02821e438543..cc9e7e645d6c 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/repository/OfflineOperationsRepository.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/repository/OfflineOperationsRepository.kt @@ -81,29 +81,27 @@ class OfflineOperationsRepository(private val fileDataStorageManager: FileDataSt .forEach { dao.update(it) } } - private fun updateOperationPath( - newPath: String, - nextOperation: OfflineOperationEntity - ): OfflineOperationEntity? = if (newPath != nextOperation.path) { - nextOperation.apply { - type = when (type) { - is OfflineOperationType.CreateFile -> - (type as OfflineOperationType.CreateFile).copy( - remotePath = newPath - ) - - is OfflineOperationType.CreateFolder -> - (type as OfflineOperationType.CreateFolder).copy( - path = newPath - ) - - else -> type + private fun updateOperationPath(newPath: String, nextOperation: OfflineOperationEntity): OfflineOperationEntity? = + if (newPath != nextOperation.path) { + nextOperation.apply { + type = when (type) { + is OfflineOperationType.CreateFile -> + (type as OfflineOperationType.CreateFile).copy( + remotePath = newPath + ) + + is OfflineOperationType.CreateFolder -> + (type as OfflineOperationType.CreateFolder).copy( + path = newPath + ) + + else -> type + } + path = newPath } - path = newPath + } else { + null } - } else { - null - } override fun updateOperationForKeepBoth(operation: OfflineOperationEntity, newPath: String) { updateOperationPath(newPath, operation) diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/conflict/ConflictResolveDialogFactory.kt b/app/src/main/java/com/owncloud/android/ui/dialog/conflict/ConflictResolveDialogFactory.kt index ad54d70cb079..ec01acb3ca0e 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/conflict/ConflictResolveDialogFactory.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/conflict/ConflictResolveDialogFactory.kt @@ -24,14 +24,18 @@ import java.io.File object ConflictResolveDialogFactory { - fun forOffline(context: Context, leftFile: OfflineOperationEntity, rightFile: OCFile, user: User?): - ConflictsResolveDialog { - + fun forOffline( + context: Context, + leftFile: OfflineOperationEntity, + rightFile: OCFile, + user: User? + ): ConflictsResolveDialog { val localFile = - if (leftFile.type is OfflineOperationType.CreateFile) + if (leftFile.type is OfflineOperationType.CreateFile) { (leftFile.type as OfflineOperationType.CreateFile).localPath.toFile() - else + } else { null + } val data = ConflictDialogData( headline = context.getString(R.string.choose_which_file), diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c1589ad6f91d..50aee8feed07 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1267,8 +1267,6 @@ Free up space %1$s is %2$s, but there is only %3$s available on device. Not enough space - Folder conflict - If you select both versions, the local folder will have a number appended to its name. If you select both versions, the local file will have a number appended to its name. Local file Server file