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..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 @@ -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,6 +81,33 @@ 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 + } + path = newPath + } + } else { + null + } + + override fun updateOperationForKeepBoth(operation: OfflineOperationEntity, newPath: String) { + updateOperationPath(newPath, operation) + 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..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,5 +14,6 @@ interface OfflineOperationsRepositoryType { fun getAllSubEntities(fileId: Long): List fun deleteOperation(file: OCFile) fun updateNextOperations(operation: OfflineOperationEntity) + fun updateOperationForKeepBoth(operation: OfflineOperationEntity, newPath: String) fun convertToOCFiles(fileId: Long): List } 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..4b7187f3760c 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/FileExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/FileExtensions.kt @@ -8,16 +8,22 @@ 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" +private const val MS_IN_SECOND = 1000 fun OCFile?.logFileSize(tag: String) { val size = DisplayUtils.bytesToHumanReadable(this?.fileLength ?: -1) @@ -111,3 +117,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() / 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 / MS_IN_SECOND // 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 42d5db1feb35..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; @@ -200,7 +202,7 @@ public void addCreateFileOfflineOperation(String[] localPaths, String[] remotePa } offlineOperationDao.insert(entity); - createPendingFile(remotePath, mimeType, createdAt, modificationTimestamp); + createPendingFile(remotePath, mimeType, createdAt, modificationTimestamp, localPath); } } @@ -230,8 +232,22 @@ 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, + String localPath + ) { + final OCFile existingFile = getFileByRemotePath(remotePath); + 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); file.setMimeType(mimeType); file.setCreationTimestamp(createdAt); file.setModificationTimestamp(modificationTimestamp); @@ -340,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; @@ -353,13 +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 + OCFile.PATH_SEPARATOR; - moveLocalFile(file, newPath, parentFolder.getDecryptedRemotePath()); - offlineOperationsRepository.updateNextOperations(entity); + final String newPath = UploadFileOperation.getNewAvailableRemotePath( + client, + (entity.getPath() != null) ? entity.getPath() : file.getDecryptedRemotePath(), + List.of(oldFileName), + file.isEncrypted() + ); + offlineOperationsRepository.updateOperationForKeepBoth(entity, newPath); } @Nullable @@ -541,7 +554,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); 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()); 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..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 @@ -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 @@ -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) @@ -272,11 +276,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..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 @@ -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 @@ -22,23 +24,34 @@ 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 localFile = + if (leftFile.type is OfflineOperationType.CreateFile) { + (leftFile.type as OfflineOperationType.CreateFile).localPath.toFile() + } else { + null + } - fun forOffline(context: Context, leftFile: OfflineOperationEntity, rightFile: OCFile): ConflictsResolveDialog { val data = ConflictDialogData( - headline = context.getString(R.string.conflict_folder_headline), - description = context.getString(R.string.conflict_message_description_for_folder), + headline = context.getString(R.string.choose_which_file), + description = context.getString(R.string.conflict_message_description), localFile = context.conflictFileData( - titleId = R.string.prefs_synced_folders_local_path_title, - timestamp = (leftFile.createdAt ?: 0L) * SECONDS_TO_MILLIS, - fileLength = UNKNOWN_FOLDER_SIZE + 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) + putParcelable(ConflictsResolveDialog.ARG_USER, user) } } 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