diff --git a/app/src/androidTest/java/com/nextcloud/utils/AutoRenameTests.kt b/app/src/androidTest/java/com/nextcloud/utils/AutoRenameTests.kt index 68e99b9b40df..2a9119e6eee0 100644 --- a/app/src/androidTest/java/com/nextcloud/utils/AutoRenameTests.kt +++ b/app/src/androidTest/java/com/nextcloud/utils/AutoRenameTests.kt @@ -250,4 +250,23 @@ class AutoRenameTests : AbstractOnServerIT() { val result = AutoRename.rename(filename, capability, isFolderPath = true) assert(result == filename) { "Expected $filename but got $result" } } + + @Test + fun testRenameWithLowercasedFiles() { + val filename = "1.txt" + val result = AutoRename.rename(filename, capability) + assert(result == filename) { "Expected $filename but got $result" } + + val secondFilename = "/1.txt" + val secondResult = AutoRename.rename(secondFilename, capability) + assert(secondResult == secondFilename) { "Expected $secondFilename but got $secondResult" } + + val thirdFilename = "/A/1.txt" + val thirdResult = AutoRename.rename(thirdFilename, capability) + assert(thirdResult == thirdFilename) { "Expected $thirdFilename but got $thirdResult" } + + val path = "/A/BB/" + val pathResult = AutoRename.rename(path, capability) + assert(pathResult == path) { "Expected $path but got $pathResult" } + } } diff --git a/app/src/androidTest/java/com/owncloud/android/utils/FileUploadHelperTest.kt b/app/src/androidTest/java/com/owncloud/android/utils/FileUploadHelperTest.kt new file mode 100644 index 000000000000..a611fd59aef2 --- /dev/null +++ b/app/src/androidTest/java/com/owncloud/android/utils/FileUploadHelperTest.kt @@ -0,0 +1,448 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.utils + +import com.nextcloud.client.database.dao.UploadDao +import com.nextcloud.client.database.entity.UploadEntity +import com.nextcloud.client.database.entity.toOCUpload +import com.nextcloud.client.database.entity.toUploadEntity +import com.nextcloud.client.jobs.upload.FileUploadHelper +import com.owncloud.android.datamodel.UploadsStorageManager.UploadStatus +import com.owncloud.android.db.OCUpload +import com.owncloud.android.db.UploadResult +import com.owncloud.android.files.services.NameCollisionPolicy +import com.owncloud.android.lib.resources.status.OCCapability +import io.mockk.every +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test + +@Suppress("TooManyFunctions") +class FileUploadHelperTest { + + private lateinit var uploadDao: UploadDao + private lateinit var fileUploadHelper: FileUploadHelper + + private val accountName = "test@nextcloud.example.com" + private val localPath = "/sdcard/DCIM/photo.jpg" + + private fun buildEntity( + id: Int = 1, + localPath: String = this.localPath, + remotePath: String = "/remote/path/photo.jpg", + accountName: String = this.accountName, + status: Int = UploadStatus.UPLOAD_IN_PROGRESS.value, + lastResult: Int = UploadResult.UNKNOWN.value, + nameCollisionPolicy: Int = NameCollisionPolicy.DEFAULT.serialize(), + fileSize: Long = 1024L, + isWifiOnly: Int = 0, + isWhileChargingOnly: Int = 0, + isCreateRemoteFolder: Int = 1, + createdBy: Int = 0, + folderUnlockToken: String? = null, + uploadEndTimestampLong: Long? = null + ) = UploadEntity( + id = id, + localPath = localPath, + remotePath = remotePath, + accountName = accountName, + fileSize = fileSize, + status = status, + localBehaviour = 0, + uploadTime = null, + nameCollisionPolicy = nameCollisionPolicy, + isCreateRemoteFolder = isCreateRemoteFolder, + uploadEndTimestamp = 0, + uploadEndTimestampLong = uploadEndTimestampLong, + lastResult = lastResult, + isWhileChargingOnly = isWhileChargingOnly, + isWifiOnly = isWifiOnly, + createdBy = createdBy, + folderUnlockToken = folderUnlockToken + ) + + private fun buildOCUpload( + localPath: String = this.localPath, + remotePath: String = "/remote/path/photo.jpg", + accountName: String = this.accountName + ): OCUpload = OCUpload(localPath, remotePath, accountName).apply { + uploadId = 1L + fileSize = 1024L + uploadStatus = UploadStatus.UPLOAD_IN_PROGRESS + nameCollisionPolicy = NameCollisionPolicy.DEFAULT + isCreateRemoteFolder = true + localAction = 0 + isUseWifiOnly = false + isWhileChargingOnly = false + lastResult = UploadResult.UNKNOWN + createdBy = 0 + folderUnlockToken = null + } + + @Before + fun setUp() { + uploadDao = mockk(relaxed = true) + fileUploadHelper = spyk(FileUploadHelper(), recordPrivateCalls = true) + val uploadsStorageManager = mockk(relaxed = true) + + val daoField = com.owncloud.android.datamodel.UploadsStorageManager::class.java + .declaredFields + .first { it.type == UploadDao::class.java } + daoField.isAccessible = true + daoField.set(uploadsStorageManager, uploadDao) + + val field = FileUploadHelper::class.java.getDeclaredField("uploadsStorageManager") + field.isAccessible = true + field.set(fileUploadHelper, uploadsStorageManager) + } + + @Test + fun getUploadByPathsExactMatch() { + val remotePath = "/remote/path/photo.jpg" + val entity = buildEntity(remotePath = remotePath) + every { uploadDao.getUploadByAccountAndPaths(accountName, localPath, remotePath) } returns entity + + val result = fileUploadHelper.getUploadByPaths(accountName, localPath, remotePath) + + assertNotNull(result) + assertEquals(remotePath, result?.remotePath) + verify(exactly = 1) { uploadDao.getUploadByAccountAndPaths(accountName, localPath, remotePath) } + + // alternative path should NOT be queried when exact match found + verify(exactly = 0) { uploadDao.getUploadByAccountAndPaths(accountName, localPath, "/remote/path/photo.JPG") } + } + + @Test + fun getUploadByPathsCaseInsensitiveExtensionFallback() { + // DB stores "/a/b/1.TXT", caller searches with "/a/b/1.txt" + val searchPath = "/a/b/AAA/b/1.txt" + val storedPath = "/a/b/AAA/b/1.TXT" + val entity = buildEntity(remotePath = storedPath) + + every { uploadDao.getUploadByAccountAndPaths(accountName, localPath, searchPath) } returns null + every { uploadDao.getUploadByAccountAndPaths(accountName, localPath, storedPath) } returns entity + + val result = fileUploadHelper.getUploadByPaths(accountName, localPath, searchPath) + + assertNotNull(result) + assertEquals(storedPath, result?.remotePath) + verify(exactly = 1) { uploadDao.getUploadByAccountAndPaths(accountName, localPath, searchPath) } + verify(exactly = 1) { uploadDao.getUploadByAccountAndPaths(accountName, localPath, storedPath) } + } + + @Test + fun getUploadByPathsFindsRecordWhenDBHasLowercaseExtensionButSearchUsesUppercase() { + // DB stores "/a/b/1.txt", caller searches with "/a/b/1.TXT" + val searchPath = "/a/b/AAA/b/1.TXT" + val storedPath = "/a/b/AAA/b/1.txt" + val entity = buildEntity(remotePath = storedPath) + + every { uploadDao.getUploadByAccountAndPaths(accountName, localPath, searchPath) } returns null + every { uploadDao.getUploadByAccountAndPaths(accountName, localPath, storedPath) } returns entity + + val result = fileUploadHelper.getUploadByPaths(accountName, localPath, searchPath) + + assertNotNull(result) + assertEquals(storedPath, result?.remotePath) + } + + @Test + fun getUploadByPathsReturnsNullWhenNeitherExactNorAlternativeExtensionPathExists() { + val remotePath = "/a/b/1.jpg" + every { uploadDao.getUploadByAccountAndPaths(accountName, localPath, remotePath) } returns null + every { uploadDao.getUploadByAccountAndPaths(accountName, localPath, "/a/b/1.JPG") } returns null + + val result = fileUploadHelper.getUploadByPaths(accountName, localPath, remotePath) + + assertNull(result) + } + + @Test + fun getUploadByPathsReturnsNullWhenRemotePathHasNoExtension() { + val remotePath = "/a/b/fileWithoutExtension" + every { uploadDao.getUploadByAccountAndPaths(accountName, localPath, remotePath) } returns null + + val result = fileUploadHelper.getUploadByPaths(accountName, localPath, remotePath) + + assertNull(result) + } + + @Test + fun getUploadByPathsReturnsNullWhenRemotePathIsEmpty() { + val remotePath = "" + every { uploadDao.getUploadByAccountAndPaths(accountName, localPath, remotePath) } returns null + + val result = fileUploadHelper.getUploadByPaths(accountName, localPath, remotePath) + + assertNull(result) + } + + @Test + fun getUploadByPathsHandlesDeepNestedPathWithUppercaseExtension() { + val searchPath = "/a/b/c/d/e/file.PNG" + val storedPath = "/a/b/c/d/e/file.png" + val entity = buildEntity(remotePath = storedPath) + + every { uploadDao.getUploadByAccountAndPaths(accountName, localPath, searchPath) } returns null + every { uploadDao.getUploadByAccountAndPaths(accountName, localPath, storedPath) } returns entity + + val result = fileUploadHelper.getUploadByPaths(accountName, localPath, searchPath) + + assertNotNull(result) + assertEquals(storedPath, result?.remotePath) + } + + @Test + fun getUploadByPathsOnlyTogglesExtensionNotRestOfFilenameOrPath() { + val searchPath = "/a/b/AAA/b/1.txt" + val wrongPath = "/a/b/aaa/b/1.TXT" + val storedPath = "/a/b/AAA/b/1.TXT" + val entity = buildEntity(remotePath = storedPath) + + every { uploadDao.getUploadByAccountAndPaths(accountName, localPath, searchPath) } returns null + every { uploadDao.getUploadByAccountAndPaths(accountName, localPath, storedPath) } returns entity + every { uploadDao.getUploadByAccountAndPaths(accountName, localPath, wrongPath) } returns null + + val result = fileUploadHelper.getUploadByPaths(accountName, localPath, searchPath) + + assertNotNull(result) + assertEquals(storedPath, result?.remotePath) + verify(exactly = 0) { uploadDao.getUploadByAccountAndPaths(accountName, localPath, wrongPath) } + } + + @Test + fun toOCUploadMapsAllFieldsCorrectlyWithoutCapability() { + val entity = buildEntity( + id = 42, + localPath = localPath, + remotePath = "/remote/photo.jpg", + accountName = accountName, + fileSize = 2048L, + status = UploadStatus.UPLOAD_IN_PROGRESS.value, + lastResult = UploadResult.UPLOADED.value, + nameCollisionPolicy = NameCollisionPolicy.RENAME.serialize(), + isWifiOnly = 1, + isWhileChargingOnly = 1, + isCreateRemoteFolder = 1, + createdBy = 2, + folderUnlockToken = "token-abc", + uploadEndTimestampLong = 1234567890L + ) + + val result = entity.toOCUpload() + + assertNotNull(result) + assertEquals(42L, result!!.uploadId) + assertEquals(localPath, result.localPath) + assertEquals("/remote/photo.jpg", result.remotePath) + assertEquals(accountName, result.accountName) + assertEquals(2048L, result.fileSize) + assertEquals(UploadStatus.UPLOAD_IN_PROGRESS, result.uploadStatus) + assertEquals(UploadResult.UPLOADED, result.lastResult) + assertEquals(NameCollisionPolicy.RENAME, result.nameCollisionPolicy) + assertEquals(true, result.isUseWifiOnly) + assertEquals(true, result.isWhileChargingOnly) + assertEquals(true, result.isCreateRemoteFolder) + assertEquals(2, result.createdBy) + assertEquals("token-abc", result.folderUnlockToken) + assertEquals(1234567890L, result.uploadEndTimestamp) + } + + @Test + fun toOCUploadMapsBooleanFalseFieldsCorrectly() { + val entity = buildEntity(isWifiOnly = 0, isWhileChargingOnly = 0, isCreateRemoteFolder = 0) + + val result = entity.toOCUpload() + + assertNotNull(result) + assertEquals(false, result!!.isUseWifiOnly) + assertEquals(false, result.isWhileChargingOnly) + assertEquals(false, result.isCreateRemoteFolder) + } + + @Test + fun toOCUploadReturnsNullWhenLocalPathIsNull() { + val entity = buildEntity().copy(localPath = null) + + val result = entity.toOCUpload() + + // OCUpload constructor throws IllegalArgumentException for null localPath + assertNull(result) + } + + @Test + fun toOCUploadReturnsNullWhenRemotePathIsNull() { + val entity = buildEntity().copy(remotePath = null) + + val result = entity.toOCUpload() + + // OCUpload constructor throws IllegalArgumentException for null remotePath + assertNull(result) + } + + @Test + fun toOCUploadAppliesAutoRenameWhenCapabilityIsProvided() { + val capability = mockk(relaxed = true) + val entity = buildEntity(remotePath = "/remote/photo.jpg") + val result = entity.toOCUpload(capability) + assertNotNull(result) + } + + @Test + fun toOCUploadHandlesNullOptionalFieldsGracefully() { + val entity = UploadEntity( + id = null, + localPath = localPath, + remotePath = "/remote/photo.jpg", + accountName = accountName, + fileSize = null, + status = null, + localBehaviour = null, + uploadTime = null, + nameCollisionPolicy = null, + isCreateRemoteFolder = null, + uploadEndTimestamp = null, + uploadEndTimestampLong = null, + lastResult = null, + isWhileChargingOnly = null, + isWifiOnly = null, + createdBy = null, + folderUnlockToken = null + ) + + // should not throw; optional fields simply stay at OCUpload defaults + val result = entity.toOCUpload() + + assertNotNull(result) + } + + @Test + fun toUploadEntityMapsAllFieldsCorrectlyWhenUploadIdIsSet() { + val upload = buildOCUpload().apply { + uploadId = 10L + fileSize = 512L + uploadStatus = UploadStatus.UPLOAD_FAILED + nameCollisionPolicy = NameCollisionPolicy.ASK_USER + isCreateRemoteFolder = false + localAction = 2 + isUseWifiOnly = true + isWhileChargingOnly = true + lastResult = UploadResult.FILE_NOT_FOUND + createdBy = 3 + folderUnlockToken = "unlock-token" + uploadEndTimestamp = 9876543210L + } + + val entity = upload.toUploadEntity() + + assertEquals(10, entity.id) + assertEquals(localPath, entity.localPath) + assertEquals("/remote/path/photo.jpg", entity.remotePath) + assertEquals(accountName, entity.accountName) + assertEquals(512L, entity.fileSize) + assertEquals(UploadStatus.UPLOAD_FAILED.value, entity.status) + assertEquals(NameCollisionPolicy.ASK_USER.serialize(), entity.nameCollisionPolicy) + assertEquals(0, entity.isCreateRemoteFolder) + assertEquals(2, entity.localBehaviour) + assertEquals(1, entity.isWifiOnly) + assertEquals(1, entity.isWhileChargingOnly) + assertEquals(UploadResult.FILE_NOT_FOUND.value, entity.lastResult) + assertEquals(3, entity.createdBy) + assertEquals("unlock-token", entity.folderUnlockToken) + assertEquals(9876543210L, entity.uploadEndTimestampLong) + assertEquals(0, entity.uploadEndTimestamp) // legacy field always 0 + assertNull(entity.uploadTime) // always null + } + + @Test + fun toUploadEntitySetsIdToNullWhenUploadIdIsNotValidToAllowRoomAutoGenerate() { + val upload = buildOCUpload().apply { uploadId = -1L } + + val entity = upload.toUploadEntity() + + assertNull(entity.id) + } + + @Test + fun toUploadEntityPreservesExistingIdWhenUploadIdIsPositive() { + val upload = buildOCUpload().apply { uploadId = 99L } + + val entity = upload.toUploadEntity() + + assertEquals(99, entity.id) + } + + @Test + fun toUploadEntityIsUseWifiOnlyIsFalseShouldReturnZero() { + val upload = buildOCUpload().apply { isUseWifiOnly = false } + + val entity = upload.toUploadEntity() + + assertEquals(0, entity.isWifiOnly) + } + + @Test + fun toUploadEntityMapsIsWhileChargingIsFalseShouldReturnZero() { + val upload = buildOCUpload().apply { isWhileChargingOnly = false } + + val entity = upload.toUploadEntity() + + assertEquals(0, entity.isWhileChargingOnly) + } + + @Test + fun toUploadEntityMapsIsCreateRemoteFolderTrueShouldReturnOne() { + val upload = buildOCUpload().apply { isCreateRemoteFolder = true } + + val entity = upload.toUploadEntity() + + assertEquals(1, entity.isCreateRemoteFolder) + } + + @Test + fun testEntityAndOCUploadConversionTogether() { + val original = buildOCUpload().apply { + uploadId = 7L + fileSize = 333L + uploadStatus = UploadStatus.UPLOAD_FAILED + nameCollisionPolicy = NameCollisionPolicy.RENAME + isCreateRemoteFolder = true + localAction = 1 + isUseWifiOnly = true + isWhileChargingOnly = false + lastResult = UploadResult.NETWORK_CONNECTION + createdBy = 1 + folderUnlockToken = "rt-token" + uploadEndTimestamp = 111L + } + + val entity = original.toUploadEntity() + val restored = entity.toOCUpload() + + assertNotNull(restored) + assertEquals(original.uploadId, restored!!.uploadId) + assertEquals(original.localPath, restored.localPath) + assertEquals(original.remotePath, restored.remotePath) + assertEquals(original.accountName, restored.accountName) + assertEquals(original.fileSize, restored.fileSize) + assertEquals(original.uploadStatus, restored.uploadStatus) + assertEquals(original.nameCollisionPolicy, restored.nameCollisionPolicy) + assertEquals(original.isCreateRemoteFolder, restored.isCreateRemoteFolder) + assertEquals(original.localAction, restored.localAction) + assertEquals(original.isUseWifiOnly, restored.isUseWifiOnly) + assertEquals(original.isWhileChargingOnly, restored.isWhileChargingOnly) + assertEquals(original.lastResult, restored.lastResult) + assertEquals(original.createdBy, restored.createdBy) + assertEquals(original.folderUnlockToken, restored.folderUnlockToken) + assertEquals(original.uploadEndTimestamp, restored.uploadEndTimestamp) + } +} 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 639a66ed8c75..ba0e0d8119f0 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 @@ -20,6 +20,7 @@ import com.nextcloud.client.database.entity.toUploadEntity import com.nextcloud.client.device.PowerManagementService import com.nextcloud.client.jobs.BackgroundJobManager import com.nextcloud.client.jobs.upload.FileUploadBroadcastManager +import com.nextcloud.client.jobs.upload.FileUploadHelper import com.nextcloud.client.jobs.upload.FileUploadWorker import com.nextcloud.client.jobs.utils.UploadErrorNotificationManager import com.nextcloud.client.network.ConnectivityService @@ -39,8 +40,10 @@ import com.owncloud.android.lib.common.OwnCloudAccount import com.owncloud.android.lib.common.OwnCloudClientManagerFactory import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.status.OCCapability import com.owncloud.android.operations.UploadFileOperation 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.withContext @@ -74,6 +77,7 @@ class AutoUploadWorker( private val fileUploadBroadcastManager = FileUploadBroadcastManager(localBroadcastManager) private lateinit var syncedFolder: SyncedFolder private val notificationManager = AutoUploadNotificationManager(context, viewThemeUtils, NOTIFICATION_ID) + private val fileUploadHelper = FileUploadHelper.instance() @Suppress("ReturnCount") override suspend fun doWork(): Result { @@ -273,6 +277,7 @@ class AutoUploadWorker( val ocAccount = OwnCloudAccount(user.toPlatformAccount(), context) val client = OwnCloudClientManagerFactory.getDefaultSingleton() .getClientFor(ocAccount, context) + val capability = CapabilityUtils.getCapability(user, context) trySetForeground() updateNotification() @@ -294,7 +299,7 @@ class AutoUploadWorker( val remotePath = syncFolderHelper.getAutoUploadRemotePath(syncedFolder, file) try { - val entityResult = getEntityResult(user, localPath, remotePath) + val entityResult = getEntityResult(user, localPath, remotePath, capability) if (entityResult !is AutoUploadEntityResult.Success) { repository.markFileAsHandled(localPath, syncedFolder) Log_OC.d(TAG, "marked file as handled: $localPath") @@ -387,12 +392,17 @@ class AutoUploadWorker( } @Suppress("ReturnCount") - private fun getEntityResult(user: User, localPath: String, remotePath: String): AutoUploadEntityResult { + private fun getEntityResult( + user: User, + localPath: String, + remotePath: String, + capability: OCCapability + ): AutoUploadEntityResult { val (needsCharging, needsWifi, uploadAction) = getUploadSettings(syncedFolder) Log_OC.d(TAG, "creating oc upload for ${user.accountName}") // Get existing upload or create new one - val uploadEntity = uploadsStorageManager.uploadDao.getUploadByAccountAndPaths( + val uploadEntity = fileUploadHelper.getUploadByPaths( localPath = localPath, remotePath = remotePath, accountName = user.accountName @@ -408,7 +418,7 @@ class AutoUploadWorker( } val upload = try { - uploadEntity?.toOCUpload(null) ?: OCUpload(localPath, remotePath, user.accountName) + uploadEntity?.toOCUpload(capability) ?: OCUpload(localPath, remotePath, user.accountName) } catch (_: IllegalArgumentException) { Log_OC.e(TAG, "cannot construct oc upload") return AutoUploadEntityResult.CreationError diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/SyncFolderHelper.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/SyncFolderHelper.kt index 75896f372d0b..bff89554fb55 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/SyncFolderHelper.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/SyncFolderHelper.kt @@ -10,12 +10,14 @@ package com.nextcloud.client.jobs.autoUpload import android.content.Context import androidx.exifinterface.media.ExifInterface import com.nextcloud.client.preferences.SubFolderRule +import com.nextcloud.utils.autoRename.AutoRename import com.owncloud.android.R import com.owncloud.android.datamodel.MediaFolderType import com.owncloud.android.datamodel.SyncedFolder import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.utils.FileStorageUtils import com.owncloud.android.utils.MimeType +import com.owncloud.android.utils.theme.CapabilityUtils import java.io.File import java.text.ParsePosition import java.text.SimpleDateFormat @@ -46,7 +48,7 @@ class SyncFolderHelper(private val context: Context) { subFolderRule = syncedFolder.subfolderRule } - val result = FileStorageUtils.getInstantUploadFilePath( + var result = FileStorageUtils.getInstantUploadFilePath( file, resources.configuration.locales[0], remoteFolder, @@ -56,6 +58,9 @@ class SyncFolderHelper(private val context: Context) { subFolderRule ) + val capability = CapabilityUtils.getCapability(context) + result = AutoRename.rename(result, capability) + Log_OC.d(TAG, "auto upload remote path: $result") return result 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 5bb40b30969d..ebb4fd3982ac 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 @@ -13,6 +13,7 @@ import android.content.Context import android.content.Intent import com.nextcloud.client.account.User import com.nextcloud.client.account.UserAccountManager +import com.nextcloud.client.database.entity.UploadEntity import com.nextcloud.client.database.entity.toOCUpload import com.nextcloud.client.database.entity.toUploadEntity import com.nextcloud.client.device.BatteryStatus @@ -38,6 +39,7 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult 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.lib.resources.status.OCCapability import com.owncloud.android.operations.RemoveFileOperation import com.owncloud.android.operations.UploadFileOperation import com.owncloud.android.utils.DisplayUtils @@ -119,9 +121,10 @@ class FileUploadHelper { } var isUploadStarted = false + val capability = fileStorageManager.getCapability(accountManager.user) try { - getUploadsByStatus(null, UploadStatus.UPLOAD_FAILED) { + getUploadsByStatus(null, UploadStatus.UPLOAD_FAILED, capability) { if (it.isNotEmpty()) { isUploadStarted = true } @@ -148,7 +151,9 @@ class FileUploadHelper { powerManagementService: PowerManagementService ): Boolean { var result = false - getUploadsByStatus(accountManager.user.accountName, UploadStatus.UPLOAD_CANCELLED) { + val capability = fileStorageManager.getCapability(accountManager.user) + + getUploadsByStatus(accountManager.user.accountName, UploadStatus.UPLOAD_CANCELLED, capability) { result = retryUploads( uploadsStorageManager, connectivityService, @@ -243,23 +248,69 @@ class FileUploadHelper { showSameFileAlreadyExistsNotification: Boolean = true ) { val uploads = localPaths.mapIndexed { index, localPath -> - val result = OCUpload(localPath, remotePaths[index], user.accountName).apply { - this.nameCollisionPolicy = nameCollisionPolicy - isUseWifiOnly = requiresWifi - isWhileChargingOnly = requiresCharging - uploadStatus = UploadStatus.UPLOAD_IN_PROGRESS - this.createdBy = createdBy - isCreateRemoteFolder = createRemoteFolder - localAction = localBehavior + fun createOCUpload(): OCUpload { + val result = OCUpload(localPath, remotePaths[index], user.accountName).apply { + this.nameCollisionPolicy = nameCollisionPolicy + isUseWifiOnly = requiresWifi + isWhileChargingOnly = requiresCharging + uploadStatus = UploadStatus.UPLOAD_IN_PROGRESS + this.createdBy = createdBy + isCreateRemoteFolder = createRemoteFolder + localAction = localBehavior + } + + val id = uploadsStorageManager.uploadDao.insertOrReplace(result.toUploadEntity()) + result.uploadId = id + return result } - val id = uploadsStorageManager.uploadDao.insertOrReplace(result.toUploadEntity()) - result.uploadId = id - result + val entity = getUploadByPaths( + accountName = user.accountName, + localPath = localPath, + remotePath = remotePaths[index] + ) + if (entity != null) { + val capability = fileStorageManager.getCapability(user) + entity.toOCUpload(capability) ?: createOCUpload() + } else { + createOCUpload() + } } backgroundJobManager.startFilesUploadJob(user, uploads.getUploadIds(), showSameFileAlreadyExistsNotification) } + @Suppress("ReturnCount") + fun getUploadByPaths(accountName: String, localPath: String, remotePath: String): UploadEntity? { + uploadsStorageManager.uploadDao.getUploadByAccountAndPaths( + accountName, + localPath, + remotePath + )?.let { return it } + + val dotIndex = remotePath.lastIndexOf('.') + if (dotIndex == -1) return null + + val namePart = remotePath.substring(0, dotIndex + 1) + val extension = remotePath.substring(dotIndex + 1) + + // before uploading file remote path may end with uppercase file extension thus we have to search + // via renamed remote path otherwise it will return null + val alternativeExtension = + if (extension == extension.lowercase()) { + extension.uppercase() + } else { + extension.lowercase() + } + + val alternativeRemotePath = namePart + alternativeExtension + + return uploadsStorageManager.uploadDao.getUploadByAccountAndPaths( + accountName, + localPath, + alternativeRemotePath + ) + } + fun removeFileUpload(remotePath: String, accountName: String) { uploadsStorageManager.uploadDao.deleteByRemotePathAndAccountName(remotePath, accountName) } @@ -289,6 +340,7 @@ class FileUploadHelper { fun getUploadsByStatus( accountName: String?, status: UploadStatus, + capability: OCCapability, nameCollisionPolicy: NameCollisionPolicy? = null, onCompleted: (Array) -> Unit ) { @@ -298,7 +350,9 @@ class FileUploadHelper { dao.getUploadsByAccountNameAndStatus(accountName, status.value, nameCollisionPolicy?.serialize()) } else { dao.getUploadsByStatus(status.value, nameCollisionPolicy?.serialize()) - }.mapNotNull { it.toOCUpload(null) }.toTypedArray() + }.mapNotNull { + it.toOCUpload(capability) + }.toTypedArray() onCompleted(result) } } @@ -399,19 +453,36 @@ class FileUploadHelper { val uploads = existingFiles.map { file -> file?.let { - val result = OCUpload(file, user).apply { - fileSize = file.fileLength - this.nameCollisionPolicy = nameCollisionPolicy - isCreateRemoteFolder = true - this.localAction = behaviour - isUseWifiOnly = false - isWhileChargingOnly = false - uploadStatus = UploadStatus.UPLOAD_IN_PROGRESS + fun createOCUpload(): OCUpload { + val result = OCUpload(file, user).apply { + fileSize = file.fileLength + this.nameCollisionPolicy = nameCollisionPolicy + isCreateRemoteFolder = true + this.localAction = behaviour + isUseWifiOnly = false + isWhileChargingOnly = false + uploadStatus = UploadStatus.UPLOAD_IN_PROGRESS + } + + val id = uploadsStorageManager.uploadDao.insertOrReplace(result.toUploadEntity()) + result.uploadId = id + return result } - val id = uploadsStorageManager.uploadDao.insertOrReplace(result.toUploadEntity()) - result.uploadId = id - result + val entity = + file.storagePath?.let { + getUploadByPaths( + accountName = user.accountName, + localPath = it, + remotePath = file.remotePath + ) + } + if (entity != null) { + val capability = fileStorageManager.getCapability(user) + entity.toOCUpload(capability) ?: createOCUpload() + } else { + createOCUpload() + } } } val uploadIds: LongArray = uploads.filterNotNull().map { it.uploadId }.toLongArray() diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/UploadListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/UploadListAdapter.java index 4d8e4164cbdf..c2e84500b892 100755 --- a/app/src/main/java/com/owncloud/android/ui/adapter/UploadListAdapter.java +++ b/app/src/main/java/com/owncloud/android/ui/adapter/UploadListAdapter.java @@ -166,7 +166,11 @@ private UploadGroup createUploadGroup(GroupConfig config, String accountName) { return new UploadGroup(config.type, parentActivity.getString(config.titleRes)) { @Override public void refresh(LoadCompleteListener listener) { - uploadHelper.getUploadsByStatus(accountName, config.status, config.nameCollisionPolicy,ocUploads -> { + uploadHelper.getUploadsByStatus(accountName, + config.status, + parentActivity.getCapabilities(), + config.nameCollisionPolicy, + ocUploads -> { fixAndSortItems(ocUploads); listener.onComplete(); return Unit.INSTANCE;