diff --git a/app/src/main/java/com/nextcloud/client/database/dao/FileSystemDao.kt b/app/src/main/java/com/nextcloud/client/database/dao/FileSystemDao.kt index 068b819e8c4b..8cf51e8a2744 100644 --- a/app/src/main/java/com/nextcloud/client/database/dao/FileSystemDao.kt +++ b/app/src/main/java/com/nextcloud/client/database/dao/FileSystemDao.kt @@ -36,6 +36,14 @@ interface FileSystemDao { ) suspend fun getBySyncedFolderId(syncedFolderId: String): List + @Query( + """ + DELETE FROM ${ProviderMeta.ProviderTableMeta.FILESYSTEM_TABLE_NAME} + WHERE ${ProviderMeta.ProviderTableMeta.FILESYSTEM_SYNCED_FOLDER_ID} = :syncedFolderId + """ + ) + suspend fun deleteBySyncedFolderId(syncedFolderId: String) + @Query( """ SELECT COUNT(*) > 0 FROM ${ProviderMeta.ProviderTableMeta.FILESYSTEM_TABLE_NAME} 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 8d404bdf8f82..026b4b9b809d 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 @@ -669,6 +669,10 @@ class FileUploadHelper { } } + suspend fun clearAutoUploadScanCache(id: Long) { + uploadsStorageManager.fileSystemDao.deleteBySyncedFolderId(id.toString()) + } + /** * Splits a list of files into: * 1. Files that have an auto-upload folder configured. diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/autoUpload/data/AutoUploadDisplayItemFactory.kt b/app/src/main/java/com/owncloud/android/ui/fragment/autoUpload/data/AutoUploadDisplayItemFactory.kt index 26efc5256347..50d287e4dd7a 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/autoUpload/data/AutoUploadDisplayItemFactory.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/autoUpload/data/AutoUploadDisplayItemFactory.kt @@ -122,7 +122,9 @@ class AutoUploadDisplayItemFactory( isSubfolderByDate = syncedFolder.isSubfolderByDate uploadAction = syncedFolder.uploadAction setNameCollisionPolicy(syncedFolder.nameCollisionPolicy.serialize()) - setEnabled(syncedFolder.isEnabled, clock.currentTime) + if (syncedFolder.isEnabled != isEnabled) { + setEnabled(syncedFolder.isEnabled, clock.currentTime) + } setSubFolderRule(syncedFolder.subFolderRule) setExcludeHidden(syncedFolder.isExcludeHidden) } @@ -146,7 +148,7 @@ class AutoUploadDisplayItemFactory( syncedFolder.uploadAction, syncedFolder.nameCollisionPolicyInt, syncedFolder.isEnabled, - clock.currentTime, + syncedFolder.enabledTimestampMs, filePaths, folderName, numberOfFiles, diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/autoUpload/handler/AutoUploadClickHandler.kt b/app/src/main/java/com/owncloud/android/ui/fragment/autoUpload/handler/AutoUploadClickHandler.kt index d6f32116f547..30e665d920e6 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/autoUpload/handler/AutoUploadClickHandler.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/autoUpload/handler/AutoUploadClickHandler.kt @@ -40,6 +40,8 @@ class AutoUploadClickHandler( private val fileUploadHelper = FileUploadHelper.instance() + private val folderStateDispatcher = Dispatchers.IO.limitedParallelism(1) + fun onCreateCustomFolderClick(activity: AppCompatActivity, accountName: String) { Log_OC.d(TAG, "Show custom folder dialog") if (!PermissionUtil.checkStoragePermission(activity)) { @@ -60,14 +62,21 @@ class AutoUploadClickHandler( if (item.isEnabled) { Log_OC.d(TAG, "auto-upload configuration sync status is enabled: " + item.remotePath) - backgroundJobManager.startAutoUpload(item, overridePowerSaving = false) + fragment.lifecycleScope.launch(folderStateDispatcher) { + fileUploadHelper.clearAutoUploadScanCache(item.id) + backgroundJobManager.startAutoUpload(item, overridePowerSaving = false) + } dialogs.showBatteryOptimizationDialogIfNeeded() return } Log_OC.d(TAG, "auto-upload configuration sync status is disabled: " + item.remotePath) - fragment.lifecycleScope.launch(Dispatchers.IO) { - fileUploadHelper.removeEntityFromUploadEntities(item.id) + removeUploadEntities(item.id) + } + + private fun removeUploadEntities(syncedFolderId: Long) { + fragment.lifecycleScope.launch(folderStateDispatcher) { + fileUploadHelper.removeEntityFromUploadEntities(syncedFolderId) } } @@ -135,8 +144,23 @@ class AutoUploadClickHandler( private fun updateExistingFolder(syncedFolder: SyncedFolderParcelable): Boolean { val adapter = getAdapter() val item = adapter.get(syncedFolder.section) ?: return false + val wasEnabled = item.isEnabled itemFactory.updateAutoUploadFolderItem(item, syncedFolder) - persistence.saveOrUpdateAutoUploadFolder(item) + + when { + !wasEnabled && item.isEnabled -> fragment.lifecycleScope.launch(folderStateDispatcher) { + fileUploadHelper.clearAutoUploadScanCache(item.id) + persistence.saveOrUpdateAutoUploadFolder(item) + } + + wasEnabled && !item.isEnabled -> { + persistence.saveOrUpdateAutoUploadFolder(item) + removeUploadEntities(item.id) + } + + else -> persistence.saveOrUpdateAutoUploadFolder(item) + } + adapter.notifyItemChanged(adapter.getSectionHeaderIndex(syncedFolder.section)) return true } diff --git a/app/src/test/java/com/owncloud/android/ui/fragment/autoUpload/data/AutoUploadDisplayItemFactoryTest.kt b/app/src/test/java/com/owncloud/android/ui/fragment/autoUpload/data/AutoUploadDisplayItemFactoryTest.kt new file mode 100644 index 000000000000..0163fa4b7837 --- /dev/null +++ b/app/src/test/java/com/owncloud/android/ui/fragment/autoUpload/data/AutoUploadDisplayItemFactoryTest.kt @@ -0,0 +1,104 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.fragment.autoUpload.data + +import com.nextcloud.client.core.Clock +import com.nextcloud.client.jobs.upload.FileUploadWorker +import com.nextcloud.client.preferences.SubFolderRule +import com.owncloud.android.datamodel.MediaFolder +import com.owncloud.android.datamodel.MediaFolderType +import com.owncloud.android.datamodel.SyncedFolder +import com.owncloud.android.datamodel.SyncedFolderDisplayItem +import com.owncloud.android.files.services.NameCollisionPolicy +import com.owncloud.android.ui.dialog.parcel.SyncedFolderParcelable +import io.mockk.every +import io.mockk.mockk +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test + +@Suppress("MagicNumber") +class AutoUploadDisplayItemFactoryTest { + + private val enabledAt = 1_000_000_000_000L + private val now = 2_000_000_000_000L + + private val clock: Clock = mockk() + private lateinit var factory: AutoUploadDisplayItemFactory + + @Before + fun setup() { + every { clock.currentTime } returns now + factory = AutoUploadDisplayItemFactory(clock, 4, "/InstantUpload") + } + + private fun createItem(enabled: Boolean, timestampMs: Long = enabledAt) = SyncedFolderDisplayItem( + 1L, + "/storage/emulated/0/DCIM/Camera", + "/InstantUpload/Camera", + true, + false, + false, + false, + "account", + FileUploadWorker.LOCAL_BEHAVIOUR_FORGET, + NameCollisionPolicy.ASK_USER.serialize(), + enabled, + timestampMs, + "Camera", + MediaFolderType.CUSTOM, + false, + SubFolderRule.YEAR_MONTH, + false, + SyncedFolder.NOT_SCANNED_YET + ) + + @Test + fun `saving settings of an enabled folder keeps its enabled timestamp`() { + val item = createItem(enabled = true) + val parcelable = SyncedFolderParcelable(item, 0).apply { isWifiOnly = false } + + factory.updateAutoUploadFolderItem(item, parcelable) + + assertEquals(enabledAt, item.enabledTimestampMs) + } + + @Test + fun `enabling a folder through its settings starts a new enabled period`() { + val item = createItem(enabled = false) + val parcelable = SyncedFolderParcelable(item, 0).apply { isEnabled = true } + + factory.updateAutoUploadFolderItem(item, parcelable) + + assertEquals(now, item.enabledTimestampMs) + } + + @Test + fun `disabling a folder through its settings clears its enabled timestamp`() { + val item = createItem(enabled = true) + val parcelable = SyncedFolderParcelable(item, 0).apply { isEnabled = false } + + factory.updateAutoUploadFolderItem(item, parcelable) + + assertEquals(SyncedFolder.EMPTY_ENABLED_TIMESTAMP_MS, item.enabledTimestampMs) + } + + @Test + fun `display item of a stored folder keeps the stored enabled timestamp`() { + val stored = createItem(enabled = true) + val mediaFolder = MediaFolder().apply { + folderName = "Camera" + absolutePath = stored.localPath + filePaths = emptyList() + type = MediaFolderType.IMAGE + } + + val item = factory.createAutoUploadFolder(stored, mediaFolder) + + assertEquals(enabledAt, item.enabledTimestampMs) + } +}