From 020457e3fa8d74db360f2d249f0e7f1f9ff1d1d4 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 20 Mar 2026 10:38:10 +0100 Subject: [PATCH 1/8] fix(auto-upload): clean stale upload entities Signed-off-by: alperozturk96 --- .../client/database/dao/UploadDao.kt | 9 +++ .../ui/activity/SyncedFoldersActivity.kt | 65 ++++++++++++------- .../android/ui/adapter/SyncedFolderAdapter.kt | 2 +- 3 files changed, 53 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/database/dao/UploadDao.kt b/app/src/main/java/com/nextcloud/client/database/dao/UploadDao.kt index 2e1d86d726d5..f561c5575b39 100644 --- a/app/src/main/java/com/nextcloud/client/database/dao/UploadDao.kt +++ b/app/src/main/java/com/nextcloud/client/database/dao/UploadDao.kt @@ -16,6 +16,15 @@ import com.owncloud.android.db.ProviderMeta.ProviderTableMeta @Dao interface UploadDao { + @Query( + """ + DELETE FROM ${ProviderTableMeta.UPLOADS_TABLE_NAME} + WHERE ${ProviderTableMeta.UPLOADS_ACCOUNT_NAME} = :accountName + AND ${ProviderTableMeta.UPLOADS_REMOTE_PATH} LIKE :remotePath || '%' + """ + ) + suspend fun deleteAllForAutoUploadFolder(accountName: String, remotePath: String) + @Query( "SELECT _id FROM " + ProviderTableMeta.UPLOADS_TABLE_NAME + " WHERE " + ProviderTableMeta.UPLOADS_STATUS + " = :status AND " + diff --git a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt index 4f32e0d8f9b8..93c045885f80 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt @@ -569,26 +569,6 @@ class SyncedFoldersActivity : return result } - override fun onSyncStatusToggleClick(section: Int, syncedFolderDisplayItem: SyncedFolderDisplayItem?) { - if (syncedFolderDisplayItem == null) return - - if (syncedFolderDisplayItem.id > SyncedFolder.UNPERSISTED_ID) { - syncedFolderProvider.updateSyncedFolderEnabled( - syncedFolderDisplayItem.id, - syncedFolderDisplayItem.isEnabled - ) - } else { - val storedId = syncedFolderProvider.storeSyncedFolder(syncedFolderDisplayItem) - if (storedId != -1L) { - syncedFolderDisplayItem.id = storedId - } - } - if (syncedFolderDisplayItem.isEnabled) { - backgroundJobManager.startAutoUpload(syncedFolderDisplayItem, overridePowerSaving = false) - showBatteryOptimizationDialogIfNeeded() - } - } - override fun onSyncFolderSettingsClick(section: Int, syncedFolderDisplayItem: SyncedFolderDisplayItem?) { check(Looper.getMainLooper().isCurrentThread) { "This must be called on the main thread!" } @@ -776,13 +756,54 @@ class SyncedFoldersActivity : dialogFragment = null } + override fun onSyncStatusToggleClick(section: Int, item: SyncedFolderDisplayItem?) { + item ?: return + + // Ensure the item is persisted + if (item.id <= SyncedFolder.UNPERSISTED_ID) { + syncedFolderProvider.storeSyncedFolder(item) + .takeIf { it != -1L } + ?.let { item.id = it } + } else { + syncedFolderProvider.updateSyncedFolderEnabled(item.id, item.isEnabled) + } + + if (item.isEnabled) { + Log_OC.d(TAG, "auto-upload configuration sync status is enabled: " + item.remotePath) + backgroundJobManager.startAutoUpload(item, overridePowerSaving = false) + showBatteryOptimizationDialogIfNeeded() + return + } + + Log_OC.d(TAG, "auto-upload configuration sync status is disabled: " + item.remotePath) + + lifecycleScope.launch(Dispatchers.IO) { + fileUploadHelper.uploadsStorageManager.uploadDao + .deleteAllForAutoUploadFolder( + accountName = userAccountManager.user.accountName, + remotePath = item.remotePath + ) + } + } + override fun onDeleteSyncedFolderPreference(syncedFolder: SyncedFolderParcelable?) { if (syncedFolder == null) { return } - syncedFolderProvider.deleteSyncedFolder(syncedFolder.id) - adapter.removeItem(syncedFolder.section) + Log_OC.d(TAG, "deleting auto upload configuration: " + syncedFolder.remotePath) + + lifecycleScope.launch(Dispatchers.IO) { + fileUploadHelper.uploadsStorageManager.uploadDao + .deleteAllForAutoUploadFolder( + accountName = userAccountManager.user.accountName, + remotePath = syncedFolder.remotePath + ) + syncedFolderProvider.deleteSyncedFolder(syncedFolder.id) + withContext(Dispatchers.Main) { + adapter.removeItem(syncedFolder.section) + } + } } /** diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/SyncedFolderAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/SyncedFolderAdapter.kt index a08fe38d189f..050674feae5b 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/SyncedFolderAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/SyncedFolderAdapter.kt @@ -513,7 +513,7 @@ class SyncedFolderAdapter( get() = syncFolderItems.size - filteredSyncFolderItems.size interface ClickListener { - fun onSyncStatusToggleClick(section: Int, syncedFolderDisplayItem: SyncedFolderDisplayItem?) + fun onSyncStatusToggleClick(section: Int, item: SyncedFolderDisplayItem?) fun onSyncFolderSettingsClick(section: Int, syncedFolderDisplayItem: SyncedFolderDisplayItem?) fun onVisibilityToggleClick(section: Int, item: SyncedFolderDisplayItem?) fun showSubFolderWarningDialog() From 5d00cccb02533cb031cf0821f381e986c7f8184a Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 20 Mar 2026 10:38:41 +0100 Subject: [PATCH 2/8] fix(auto-upload): clean stale upload entities Signed-off-by: alperozturk96 --- .../main/java/com/nextcloud/client/database/dao/UploadDao.kt | 2 +- .../com/owncloud/android/ui/activity/SyncedFoldersActivity.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/database/dao/UploadDao.kt b/app/src/main/java/com/nextcloud/client/database/dao/UploadDao.kt index f561c5575b39..9c181301f6d1 100644 --- a/app/src/main/java/com/nextcloud/client/database/dao/UploadDao.kt +++ b/app/src/main/java/com/nextcloud/client/database/dao/UploadDao.kt @@ -23,7 +23,7 @@ interface UploadDao { AND ${ProviderTableMeta.UPLOADS_REMOTE_PATH} LIKE :remotePath || '%' """ ) - suspend fun deleteAllForAutoUploadFolder(accountName: String, remotePath: String) + suspend fun removeEntities(accountName: String, remotePath: String) @Query( "SELECT _id FROM " + ProviderTableMeta.UPLOADS_TABLE_NAME + diff --git a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt index 93c045885f80..d72cbf0b614e 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt @@ -779,7 +779,7 @@ class SyncedFoldersActivity : lifecycleScope.launch(Dispatchers.IO) { fileUploadHelper.uploadsStorageManager.uploadDao - .deleteAllForAutoUploadFolder( + .removeEntities( accountName = userAccountManager.user.accountName, remotePath = item.remotePath ) @@ -795,7 +795,7 @@ class SyncedFoldersActivity : lifecycleScope.launch(Dispatchers.IO) { fileUploadHelper.uploadsStorageManager.uploadDao - .deleteAllForAutoUploadFolder( + .removeEntities( accountName = userAccountManager.user.accountName, remotePath = syncedFolder.remotePath ) From 877d53f7ac7d25f6127146b895a243b9af4c123f Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 20 Mar 2026 11:23:27 +0100 Subject: [PATCH 3/8] fix(auto-upload): clean stale upload entities Signed-off-by: alperozturk96 # Conflicts: # app/src/main/java/com/nextcloud/client/database/dao/FileSystemDao.kt --- .../99.json | 1308 +++++++++++++++++ .../client/database/NextcloudDatabase.kt | 3 +- .../client/database/dao/UploadDao.kt | 18 +- .../database/entity/FilesystemEntity.kt | 2 + .../jobs/autoUpload/AutoUploadWorker.kt | 1 + .../jobs/autoUpload/FileSystemRepository.kt | 17 + .../datamodel/UploadsStorageManager.java | 4 +- .../com/owncloud/android/db/ProviderMeta.java | 3 +- .../ui/activity/SyncedFoldersActivity.kt | 28 +- 9 files changed, 1362 insertions(+), 22 deletions(-) create mode 100644 app/schemas/com.nextcloud.client.database.NextcloudDatabase/99.json diff --git a/app/schemas/com.nextcloud.client.database.NextcloudDatabase/99.json b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/99.json new file mode 100644 index 000000000000..e9146d470f08 --- /dev/null +++ b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/99.json @@ -0,0 +1,1308 @@ +{ + "formatVersion": 1, + "database": { + "version": 99, + "identityHash": "29842d7d75a54c57a2203a5b041346d7", + "entities": [ + { + "tableName": "arbitrary_data", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `cloud_id` TEXT, `key` TEXT, `value` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "cloudId", + "columnName": "cloud_id", + "affinity": "TEXT" + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "capabilities", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `assistant` INTEGER, `account` TEXT, `version_mayor` INTEGER, `version_minor` INTEGER, `version_micro` INTEGER, `version_string` TEXT, `version_edition` TEXT, `extended_support` INTEGER, `core_pollinterval` INTEGER, `sharing_api_enabled` INTEGER, `sharing_public_enabled` INTEGER, `sharing_public_password_enforced` INTEGER, `sharing_public_expire_date_enabled` INTEGER, `sharing_public_expire_date_days` INTEGER, `sharing_public_expire_date_enforced` INTEGER, `sharing_public_send_mail` INTEGER, `sharing_public_upload` INTEGER, `sharing_user_send_mail` INTEGER, `sharing_resharing` INTEGER, `sharing_federation_outgoing` INTEGER, `sharing_federation_incoming` INTEGER, `files_bigfilechunking` INTEGER, `files_undelete` INTEGER, `files_versioning` INTEGER, `external_links` INTEGER, `server_name` TEXT, `server_color` TEXT, `server_text_color` TEXT, `server_element_color` TEXT, `server_slogan` TEXT, `server_logo` TEXT, `background_url` TEXT, `end_to_end_encryption` INTEGER, `end_to_end_encryption_keys_exist` INTEGER, `end_to_end_encryption_api_version` TEXT, `activity` INTEGER, `background_default` INTEGER, `background_plain` INTEGER, `richdocument` INTEGER, `richdocument_mimetype_list` TEXT, `richdocument_direct_editing` INTEGER, `richdocument_direct_templates` INTEGER, `richdocument_optional_mimetype_list` TEXT, `sharing_public_ask_for_optional_password` INTEGER, `richdocument_product_name` TEXT, `direct_editing_etag` TEXT, `user_status` INTEGER, `user_status_supports_emoji` INTEGER, `etag` TEXT, `files_locking_version` TEXT, `groupfolders` INTEGER, `drop_account` INTEGER, `security_guard` INTEGER, `forbidden_filename_characters` INTEGER, `forbidden_filenames` INTEGER, `forbidden_filename_extensions` INTEGER, `forbidden_filename_basenames` INTEGER, `files_download_limit` INTEGER, `files_download_limit_default` INTEGER, `recommendation` INTEGER, `notes_folder_path` TEXT, `default_permissions` INTEGER, `user_status_supports_busy` INTEGER, `windows_compatible_filenames` INTEGER, `has_valid_subscription` INTEGER, `client_integration_json` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "assistant", + "columnName": "assistant", + "affinity": "INTEGER" + }, + { + "fieldPath": "accountName", + "columnName": "account", + "affinity": "TEXT" + }, + { + "fieldPath": "versionMajor", + "columnName": "version_mayor", + "affinity": "INTEGER" + }, + { + "fieldPath": "versionMinor", + "columnName": "version_minor", + "affinity": "INTEGER" + }, + { + "fieldPath": "versionMicro", + "columnName": "version_micro", + "affinity": "INTEGER" + }, + { + "fieldPath": "versionString", + "columnName": "version_string", + "affinity": "TEXT" + }, + { + "fieldPath": "versionEditor", + "columnName": "version_edition", + "affinity": "TEXT" + }, + { + "fieldPath": "extendedSupport", + "columnName": "extended_support", + "affinity": "INTEGER" + }, + { + "fieldPath": "corePollinterval", + "columnName": "core_pollinterval", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingApiEnabled", + "columnName": "sharing_api_enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicEnabled", + "columnName": "sharing_public_enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicPasswordEnforced", + "columnName": "sharing_public_password_enforced", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicExpireDateEnabled", + "columnName": "sharing_public_expire_date_enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicExpireDateDays", + "columnName": "sharing_public_expire_date_days", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicExpireDateEnforced", + "columnName": "sharing_public_expire_date_enforced", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicSendMail", + "columnName": "sharing_public_send_mail", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicUpload", + "columnName": "sharing_public_upload", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingUserSendMail", + "columnName": "sharing_user_send_mail", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingResharing", + "columnName": "sharing_resharing", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingFederationOutgoing", + "columnName": "sharing_federation_outgoing", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingFederationIncoming", + "columnName": "sharing_federation_incoming", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesBigfilechunking", + "columnName": "files_bigfilechunking", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesUndelete", + "columnName": "files_undelete", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesVersioning", + "columnName": "files_versioning", + "affinity": "INTEGER" + }, + { + "fieldPath": "externalLinks", + "columnName": "external_links", + "affinity": "INTEGER" + }, + { + "fieldPath": "serverName", + "columnName": "server_name", + "affinity": "TEXT" + }, + { + "fieldPath": "serverColor", + "columnName": "server_color", + "affinity": "TEXT" + }, + { + "fieldPath": "serverTextColor", + "columnName": "server_text_color", + "affinity": "TEXT" + }, + { + "fieldPath": "serverElementColor", + "columnName": "server_element_color", + "affinity": "TEXT" + }, + { + "fieldPath": "serverSlogan", + "columnName": "server_slogan", + "affinity": "TEXT" + }, + { + "fieldPath": "serverLogo", + "columnName": "server_logo", + "affinity": "TEXT" + }, + { + "fieldPath": "serverBackgroundUrl", + "columnName": "background_url", + "affinity": "TEXT" + }, + { + "fieldPath": "endToEndEncryption", + "columnName": "end_to_end_encryption", + "affinity": "INTEGER" + }, + { + "fieldPath": "endToEndEncryptionKeysExist", + "columnName": "end_to_end_encryption_keys_exist", + "affinity": "INTEGER" + }, + { + "fieldPath": "endToEndEncryptionApiVersion", + "columnName": "end_to_end_encryption_api_version", + "affinity": "TEXT" + }, + { + "fieldPath": "activity", + "columnName": "activity", + "affinity": "INTEGER" + }, + { + "fieldPath": "serverBackgroundDefault", + "columnName": "background_default", + "affinity": "INTEGER" + }, + { + "fieldPath": "serverBackgroundPlain", + "columnName": "background_plain", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocument", + "columnName": "richdocument", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentMimetypeList", + "columnName": "richdocument_mimetype_list", + "affinity": "TEXT" + }, + { + "fieldPath": "richdocumentDirectEditing", + "columnName": "richdocument_direct_editing", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentTemplates", + "columnName": "richdocument_direct_templates", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentOptionalMimetypeList", + "columnName": "richdocument_optional_mimetype_list", + "affinity": "TEXT" + }, + { + "fieldPath": "sharingPublicAskForOptionalPassword", + "columnName": "sharing_public_ask_for_optional_password", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentProductName", + "columnName": "richdocument_product_name", + "affinity": "TEXT" + }, + { + "fieldPath": "directEditingEtag", + "columnName": "direct_editing_etag", + "affinity": "TEXT" + }, + { + "fieldPath": "userStatus", + "columnName": "user_status", + "affinity": "INTEGER" + }, + { + "fieldPath": "userStatusSupportsEmoji", + "columnName": "user_status_supports_emoji", + "affinity": "INTEGER" + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT" + }, + { + "fieldPath": "filesLockingVersion", + "columnName": "files_locking_version", + "affinity": "TEXT" + }, + { + "fieldPath": "groupfolders", + "columnName": "groupfolders", + "affinity": "INTEGER" + }, + { + "fieldPath": "dropAccount", + "columnName": "drop_account", + "affinity": "INTEGER" + }, + { + "fieldPath": "securityGuard", + "columnName": "security_guard", + "affinity": "INTEGER" + }, + { + "fieldPath": "forbiddenFileNameCharacters", + "columnName": "forbidden_filename_characters", + "affinity": "INTEGER" + }, + { + "fieldPath": "forbiddenFileNames", + "columnName": "forbidden_filenames", + "affinity": "INTEGER" + }, + { + "fieldPath": "forbiddenFileNameExtensions", + "columnName": "forbidden_filename_extensions", + "affinity": "INTEGER" + }, + { + "fieldPath": "forbiddenFilenameBaseNames", + "columnName": "forbidden_filename_basenames", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesDownloadLimit", + "columnName": "files_download_limit", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesDownloadLimitDefault", + "columnName": "files_download_limit_default", + "affinity": "INTEGER" + }, + { + "fieldPath": "recommendation", + "columnName": "recommendation", + "affinity": "INTEGER" + }, + { + "fieldPath": "notesFolderPath", + "columnName": "notes_folder_path", + "affinity": "TEXT" + }, + { + "fieldPath": "defaultPermissions", + "columnName": "default_permissions", + "affinity": "INTEGER" + }, + { + "fieldPath": "userStatusSupportsBusy", + "columnName": "user_status_supports_busy", + "affinity": "INTEGER" + }, + { + "fieldPath": "isWCFEnabled", + "columnName": "windows_compatible_filenames", + "affinity": "INTEGER" + }, + { + "fieldPath": "hasValidSubscription", + "columnName": "has_valid_subscription", + "affinity": "INTEGER" + }, + { + "fieldPath": "clientIntegrationJson", + "columnName": "client_integration_json", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "external_links", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `icon_url` TEXT, `language` TEXT, `type` INTEGER, `name` TEXT, `url` TEXT, `redirect` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "iconUrl", + "columnName": "icon_url", + "affinity": "TEXT" + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT" + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT" + }, + { + "fieldPath": "redirect", + "columnName": "redirect", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "filelist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `filename` TEXT, `encrypted_filename` TEXT, `path` TEXT, `path_decrypted` TEXT, `parent` INTEGER, `created` INTEGER, `modified` INTEGER, `content_type` TEXT, `content_length` INTEGER, `media_path` TEXT, `file_owner` TEXT, `last_sync_date` INTEGER, `last_sync_date_for_data` INTEGER, `modified_at_last_sync_for_data` INTEGER, `etag` TEXT, `etag_on_server` TEXT, `share_by_link` INTEGER, `permissions` TEXT, `remote_id` TEXT, `local_id` INTEGER NOT NULL DEFAULT -1, `update_thumbnail` INTEGER, `is_downloading` INTEGER, `favorite` INTEGER, `hidden` INTEGER, `is_encrypted` INTEGER, `etag_in_conflict` TEXT, `shared_via_users` INTEGER, `mount_type` INTEGER, `has_preview` INTEGER, `unread_comments_count` INTEGER, `owner_id` TEXT, `owner_display_name` TEXT, `note` TEXT, `sharees` TEXT, `rich_workspace` TEXT, `metadata_size` TEXT, `metadata_live_photo` TEXT, `locked` INTEGER, `lock_type` INTEGER, `lock_owner` TEXT, `lock_owner_display_name` TEXT, `lock_owner_editor` TEXT, `lock_timestamp` INTEGER, `lock_timeout` INTEGER, `lock_token` TEXT, `tags` TEXT, `metadata_gps` TEXT, `e2e_counter` INTEGER, `internal_two_way_sync_timestamp` INTEGER, `internal_two_way_sync_result` TEXT, `uploaded` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "name", + "columnName": "filename", + "affinity": "TEXT" + }, + { + "fieldPath": "encryptedName", + "columnName": "encrypted_filename", + "affinity": "TEXT" + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT" + }, + { + "fieldPath": "pathDecrypted", + "columnName": "path_decrypted", + "affinity": "TEXT" + }, + { + "fieldPath": "parent", + "columnName": "parent", + "affinity": "INTEGER" + }, + { + "fieldPath": "creation", + "columnName": "created", + "affinity": "INTEGER" + }, + { + "fieldPath": "modified", + "columnName": "modified", + "affinity": "INTEGER" + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT" + }, + { + "fieldPath": "contentLength", + "columnName": "content_length", + "affinity": "INTEGER" + }, + { + "fieldPath": "storagePath", + "columnName": "media_path", + "affinity": "TEXT" + }, + { + "fieldPath": "accountOwner", + "columnName": "file_owner", + "affinity": "TEXT" + }, + { + "fieldPath": "lastSyncDate", + "columnName": "last_sync_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastSyncDateForData", + "columnName": "last_sync_date_for_data", + "affinity": "INTEGER" + }, + { + "fieldPath": "modifiedAtLastSyncForData", + "columnName": "modified_at_last_sync_for_data", + "affinity": "INTEGER" + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT" + }, + { + "fieldPath": "etagOnServer", + "columnName": "etag_on_server", + "affinity": "TEXT" + }, + { + "fieldPath": "sharedViaLink", + "columnName": "share_by_link", + "affinity": "INTEGER" + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "TEXT" + }, + { + "fieldPath": "remoteId", + "columnName": "remote_id", + "affinity": "TEXT" + }, + { + "fieldPath": "localId", + "columnName": "local_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "updateThumbnail", + "columnName": "update_thumbnail", + "affinity": "INTEGER" + }, + { + "fieldPath": "isDownloading", + "columnName": "is_downloading", + "affinity": "INTEGER" + }, + { + "fieldPath": "favorite", + "columnName": "favorite", + "affinity": "INTEGER" + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER" + }, + { + "fieldPath": "isEncrypted", + "columnName": "is_encrypted", + "affinity": "INTEGER" + }, + { + "fieldPath": "etagInConflict", + "columnName": "etag_in_conflict", + "affinity": "TEXT" + }, + { + "fieldPath": "sharedWithSharee", + "columnName": "shared_via_users", + "affinity": "INTEGER" + }, + { + "fieldPath": "mountType", + "columnName": "mount_type", + "affinity": "INTEGER" + }, + { + "fieldPath": "hasPreview", + "columnName": "has_preview", + "affinity": "INTEGER" + }, + { + "fieldPath": "unreadCommentsCount", + "columnName": "unread_comments_count", + "affinity": "INTEGER" + }, + { + "fieldPath": "ownerId", + "columnName": "owner_id", + "affinity": "TEXT" + }, + { + "fieldPath": "ownerDisplayName", + "columnName": "owner_display_name", + "affinity": "TEXT" + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT" + }, + { + "fieldPath": "sharees", + "columnName": "sharees", + "affinity": "TEXT" + }, + { + "fieldPath": "richWorkspace", + "columnName": "rich_workspace", + "affinity": "TEXT" + }, + { + "fieldPath": "metadataSize", + "columnName": "metadata_size", + "affinity": "TEXT" + }, + { + "fieldPath": "metadataLivePhoto", + "columnName": "metadata_live_photo", + "affinity": "TEXT" + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockType", + "columnName": "lock_type", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockOwner", + "columnName": "lock_owner", + "affinity": "TEXT" + }, + { + "fieldPath": "lockOwnerDisplayName", + "columnName": "lock_owner_display_name", + "affinity": "TEXT" + }, + { + "fieldPath": "lockOwnerEditor", + "columnName": "lock_owner_editor", + "affinity": "TEXT" + }, + { + "fieldPath": "lockTimestamp", + "columnName": "lock_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockTimeout", + "columnName": "lock_timeout", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockToken", + "columnName": "lock_token", + "affinity": "TEXT" + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT" + }, + { + "fieldPath": "metadataGPS", + "columnName": "metadata_gps", + "affinity": "TEXT" + }, + { + "fieldPath": "e2eCounter", + "columnName": "e2e_counter", + "affinity": "INTEGER" + }, + { + "fieldPath": "internalTwoWaySync", + "columnName": "internal_two_way_sync_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "internalTwoWaySyncResult", + "columnName": "internal_two_way_sync_result", + "affinity": "TEXT" + }, + { + "fieldPath": "uploaded", + "columnName": "uploaded", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "filesystem", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `is_folder` INTEGER, `found_at` INTEGER, `upload_triggered` INTEGER, `syncedfolder_id` TEXT, `crc32` TEXT, `modified_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT" + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT" + }, + { + "fieldPath": "fileIsFolder", + "columnName": "is_folder", + "affinity": "INTEGER" + }, + { + "fieldPath": "fileFoundRecently", + "columnName": "found_at", + "affinity": "INTEGER" + }, + { + "fieldPath": "fileSentForUpload", + "columnName": "upload_triggered", + "affinity": "INTEGER" + }, + { + "fieldPath": "syncedFolderId", + "columnName": "syncedfolder_id", + "affinity": "TEXT" + }, + { + "fieldPath": "crc32", + "columnName": "crc32", + "affinity": "TEXT" + }, + { + "fieldPath": "fileModified", + "columnName": "modified_at", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "ocshares", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `file_source` INTEGER, `item_source` INTEGER, `share_type` INTEGER, `shate_with` TEXT, `path` TEXT, `permissions` INTEGER, `shared_date` INTEGER, `expiration_date` INTEGER, `token` TEXT, `shared_with_display_name` TEXT, `is_directory` INTEGER, `user_id` TEXT, `id_remote_shared` INTEGER, `owner_share` TEXT, `is_password_protected` INTEGER, `note` TEXT, `hide_download` INTEGER, `share_link` TEXT, `share_label` TEXT, `download_limit_limit` INTEGER, `download_limit_count` INTEGER, `attributes` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "fileSource", + "columnName": "file_source", + "affinity": "INTEGER" + }, + { + "fieldPath": "itemSource", + "columnName": "item_source", + "affinity": "INTEGER" + }, + { + "fieldPath": "shareType", + "columnName": "share_type", + "affinity": "INTEGER" + }, + { + "fieldPath": "shareWith", + "columnName": "shate_with", + "affinity": "TEXT" + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT" + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharedDate", + "columnName": "shared_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "expirationDate", + "columnName": "expiration_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT" + }, + { + "fieldPath": "shareWithDisplayName", + "columnName": "shared_with_display_name", + "affinity": "TEXT" + }, + { + "fieldPath": "isDirectory", + "columnName": "is_directory", + "affinity": "INTEGER" + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT" + }, + { + "fieldPath": "idRemoteShared", + "columnName": "id_remote_shared", + "affinity": "INTEGER" + }, + { + "fieldPath": "accountOwner", + "columnName": "owner_share", + "affinity": "TEXT" + }, + { + "fieldPath": "isPasswordProtected", + "columnName": "is_password_protected", + "affinity": "INTEGER" + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT" + }, + { + "fieldPath": "hideDownload", + "columnName": "hide_download", + "affinity": "INTEGER" + }, + { + "fieldPath": "shareLink", + "columnName": "share_link", + "affinity": "TEXT" + }, + { + "fieldPath": "shareLabel", + "columnName": "share_label", + "affinity": "TEXT" + }, + { + "fieldPath": "downloadLimitLimit", + "columnName": "download_limit_limit", + "affinity": "INTEGER" + }, + { + "fieldPath": "downloadLimitCount", + "columnName": "download_limit_count", + "affinity": "INTEGER" + }, + { + "fieldPath": "attributes", + "columnName": "attributes", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "synced_folders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `wifi_only` INTEGER, `charging_only` INTEGER, `existing` INTEGER, `enabled` INTEGER, `enabled_timestamp_ms` INTEGER, `subfolder_by_date` INTEGER, `account` TEXT, `upload_option` INTEGER, `name_collision_policy` INTEGER, `type` INTEGER, `hidden` INTEGER, `sub_folder_rule` INTEGER, `exclude_hidden` INTEGER, `last_scan_timestamp_ms` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT" + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT" + }, + { + "fieldPath": "wifiOnly", + "columnName": "wifi_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "chargingOnly", + "columnName": "charging_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "existing", + "columnName": "existing", + "affinity": "INTEGER" + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "enabledTimestampMs", + "columnName": "enabled_timestamp_ms", + "affinity": "INTEGER" + }, + { + "fieldPath": "subfolderByDate", + "columnName": "subfolder_by_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "TEXT" + }, + { + "fieldPath": "uploadAction", + "columnName": "upload_option", + "affinity": "INTEGER" + }, + { + "fieldPath": "nameCollisionPolicy", + "columnName": "name_collision_policy", + "affinity": "INTEGER" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER" + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER" + }, + { + "fieldPath": "subFolderRule", + "columnName": "sub_folder_rule", + "affinity": "INTEGER" + }, + { + "fieldPath": "excludeHidden", + "columnName": "exclude_hidden", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastScanTimestampMs", + "columnName": "last_scan_timestamp_ms", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "list_of_uploads", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `account_name` TEXT, `file_size` INTEGER, `status` INTEGER, `local_behaviour` INTEGER, `upload_time` INTEGER, `name_collision_policy` INTEGER, `is_create_remote_folder` INTEGER, `upload_end_timestamp` INTEGER, `upload_end_timestamp_long` INTEGER, `last_result` INTEGER, `is_while_charging_only` INTEGER, `is_wifi_only` INTEGER, `created_by` INTEGER, `folder_unlock_token` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT" + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT" + }, + { + "fieldPath": "accountName", + "columnName": "account_name", + "affinity": "TEXT" + }, + { + "fieldPath": "fileSize", + "columnName": "file_size", + "affinity": "INTEGER" + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER" + }, + { + "fieldPath": "localBehaviour", + "columnName": "local_behaviour", + "affinity": "INTEGER" + }, + { + "fieldPath": "uploadTime", + "columnName": "upload_time", + "affinity": "INTEGER" + }, + { + "fieldPath": "nameCollisionPolicy", + "columnName": "name_collision_policy", + "affinity": "INTEGER" + }, + { + "fieldPath": "isCreateRemoteFolder", + "columnName": "is_create_remote_folder", + "affinity": "INTEGER" + }, + { + "fieldPath": "uploadEndTimestamp", + "columnName": "upload_end_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "uploadEndTimestampLong", + "columnName": "upload_end_timestamp_long", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastResult", + "columnName": "last_result", + "affinity": "INTEGER" + }, + { + "fieldPath": "isWhileChargingOnly", + "columnName": "is_while_charging_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "isWifiOnly", + "columnName": "is_wifi_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "createdBy", + "columnName": "created_by", + "affinity": "INTEGER" + }, + { + "fieldPath": "folderUnlockToken", + "columnName": "folder_unlock_token", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "virtual", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `type` TEXT, `ocfile_id` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT" + }, + { + "fieldPath": "ocFileId", + "columnName": "ocfile_id", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "offline_operations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `offline_operations_parent_oc_file_id` INTEGER, `offline_operations_path` TEXT, `offline_operations_type` TEXT, `offline_operations_file_name` TEXT, `offline_operations_created_at` INTEGER, `offline_operations_modified_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "parentOCFileId", + "columnName": "offline_operations_parent_oc_file_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "path", + "columnName": "offline_operations_path", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "offline_operations_type", + "affinity": "TEXT" + }, + { + "fieldPath": "filename", + "columnName": "offline_operations_file_name", + "affinity": "TEXT" + }, + { + "fieldPath": "createdAt", + "columnName": "offline_operations_created_at", + "affinity": "INTEGER" + }, + { + "fieldPath": "modifiedAt", + "columnName": "offline_operations_modified_at", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "recommended_files", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `directory` TEXT NOT NULL, `extension` TEXT NOT NULL, `mime_type` TEXT NOT NULL, `has_preview` INTEGER NOT NULL, `reason` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `account_name` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "directory", + "columnName": "directory", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "extension", + "columnName": "extension", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mimeType", + "columnName": "mime_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasPreview", + "columnName": "has_preview", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reason", + "columnName": "reason", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountName", + "columnName": "account_name", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "assistant", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountName` TEXT, `type` TEXT, `status` TEXT, `userId` TEXT, `appId` TEXT, `input` TEXT, `output` TEXT, `completionExpectedAt` INTEGER, `progress` INTEGER, `lastUpdated` INTEGER, `scheduledAt` INTEGER, `endedAt` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountName", + "columnName": "accountName", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT" + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT" + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT" + }, + { + "fieldPath": "appId", + "columnName": "appId", + "affinity": "TEXT" + }, + { + "fieldPath": "input", + "columnName": "input", + "affinity": "TEXT" + }, + { + "fieldPath": "output", + "columnName": "output", + "affinity": "TEXT" + }, + { + "fieldPath": "completionExpectedAt", + "columnName": "completionExpectedAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "progress", + "columnName": "progress", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "INTEGER" + }, + { + "fieldPath": "scheduledAt", + "columnName": "scheduledAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "endedAt", + "columnName": "endedAt", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '29842d7d75a54c57a2203a5b041346d7')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt b/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt index c82192516324..66cfc1667f68 100644 --- a/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt +++ b/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt @@ -93,8 +93,9 @@ import com.owncloud.android.db.ProviderMeta AutoMigration(from = 93, to = 94, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class), AutoMigration(from = 94, to = 95, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class), AutoMigration(from = 95, to = 96), - AutoMigration(from = 96, to = 97, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class) + AutoMigration(from = 96, to = 97, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class), // manual migration used for 97 to 98 + AutoMigration(from = 98, to = 99) ], exportSchema = true ) diff --git a/app/src/main/java/com/nextcloud/client/database/dao/UploadDao.kt b/app/src/main/java/com/nextcloud/client/database/dao/UploadDao.kt index 9c181301f6d1..f9ec0faef0cd 100644 --- a/app/src/main/java/com/nextcloud/client/database/dao/UploadDao.kt +++ b/app/src/main/java/com/nextcloud/client/database/dao/UploadDao.kt @@ -16,15 +16,6 @@ import com.owncloud.android.db.ProviderMeta.ProviderTableMeta @Dao interface UploadDao { - @Query( - """ - DELETE FROM ${ProviderTableMeta.UPLOADS_TABLE_NAME} - WHERE ${ProviderTableMeta.UPLOADS_ACCOUNT_NAME} = :accountName - AND ${ProviderTableMeta.UPLOADS_REMOTE_PATH} LIKE :remotePath || '%' - """ - ) - suspend fun removeEntities(accountName: String, remotePath: String) - @Query( "SELECT _id FROM " + ProviderTableMeta.UPLOADS_TABLE_NAME + " WHERE " + ProviderTableMeta.UPLOADS_STATUS + " = :status AND " + @@ -52,6 +43,15 @@ interface UploadDao { ) fun deleteByRemotePathAndAccountName(remotePath: String, accountName: String) + @Query( + """ + DELETE FROM ${ProviderTableMeta.UPLOADS_TABLE_NAME} + WHERE ${ProviderTableMeta.UPLOADS_LOCAL_PATH} = :localPath + AND ${ProviderTableMeta.UPLOADS_REMOTE_PATH} = :remotePath +""" + ) + suspend fun deleteByLocalRemotePath(localPath: String, remotePath: String) + @Query( "SELECT * FROM " + ProviderTableMeta.UPLOADS_TABLE_NAME + " WHERE " + ProviderTableMeta._ID + " = :id AND " + diff --git a/app/src/main/java/com/nextcloud/client/database/entity/FilesystemEntity.kt b/app/src/main/java/com/nextcloud/client/database/entity/FilesystemEntity.kt index 7247ee15cadd..031fc52baeb3 100644 --- a/app/src/main/java/com/nextcloud/client/database/entity/FilesystemEntity.kt +++ b/app/src/main/java/com/nextcloud/client/database/entity/FilesystemEntity.kt @@ -19,6 +19,8 @@ data class FilesystemEntity( val id: Int?, @ColumnInfo(name = ProviderTableMeta.FILESYSTEM_FILE_LOCAL_PATH) val localPath: String?, + @ColumnInfo(name = ProviderTableMeta.FILESYSTEM_FILE_REMOTE_PATH) + val remotePath: String?, @ColumnInfo(name = ProviderTableMeta.FILESYSTEM_FILE_IS_FOLDER) val fileIsFolder: Int?, @ColumnInfo(name = ProviderTableMeta.FILESYSTEM_FILE_FOUND_RECENTLY) diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadWorker.kt index e174365a4a9e..8fe80bc2d727 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 @@ -292,6 +292,7 @@ class AutoUploadWorker( try { // Insert/update to IN_PROGRESS state before starting upload val generatedId = uploadsStorageManager.uploadDao.insertOrReplace(uploadEntity) + repository.updateRemotePath(upload, syncedFolder) uploadEntity = uploadEntity.copy(id = generatedId.toInt()) upload.uploadId = generatedId diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt index a7b69ac00a2e..0e7147f8f2a4 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt @@ -16,6 +16,7 @@ import com.nextcloud.client.database.entity.FilesystemEntity import com.nextcloud.utils.extensions.shouldSkipFile import com.owncloud.android.datamodel.SyncedFolder import com.owncloud.android.datamodel.UploadsStorageManager +import com.owncloud.android.db.OCUpload import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.utils.SyncedFolderUtils import java.io.File @@ -80,6 +81,21 @@ class FileSystemRepository( return filtered } + suspend fun updateRemotePath(upload: OCUpload, syncedFolder: SyncedFolder) { + val syncedFolderIdStr = syncedFolder.id.toString() + + try { + dao.updateRemotePath(remotePath = upload.remotePath, localPath = upload.localPath, syncedFolderIdStr) + Log_OC.d( + TAG, + "file system entity remote path updated. remotePath: ${upload.remotePath}, localPath: " + + "${upload.localPath} for syncedFolderId=$syncedFolderIdStr" + ) + } catch (e: Exception) { + Log_OC.e(TAG, "updateRemotePath(): ${e.message}", e) + } + } + suspend fun markFileAsHandled(localPath: String, syncedFolder: SyncedFolder) { val syncedFolderIdStr = syncedFolder.id.toString() @@ -199,6 +215,7 @@ class FileSystemRepository( val newEntity = FilesystemEntity( id = entity?.id, localPath = localPath, + remotePath = null, // will be updated later fileIsFolder = if (file.isDirectory) 1 else 0, fileFoundRecently = System.currentTimeMillis(), fileSentForUpload = 0, // Reset to 0 to queue for upload diff --git a/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.java b/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.java index 244f87cecfe2..92f38d53725c 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.java @@ -22,6 +22,7 @@ import com.nextcloud.client.account.CurrentAccountProvider; import com.nextcloud.client.account.User; import com.nextcloud.client.database.NextcloudDatabase; +import com.nextcloud.client.database.dao.FileSystemDao; import com.nextcloud.client.database.dao.UploadDao; import com.nextcloud.client.database.entity.UploadEntity; import com.nextcloud.client.database.entity.UploadEntityKt; @@ -69,7 +70,8 @@ public class UploadsStorageManager extends Observable { private final ContentResolver contentResolver; private final CurrentAccountProvider currentAccountProvider; private OCCapability capability; - public final UploadDao uploadDao = NextcloudDatabase.getInstance(MainApp.getAppContext()).uploadDao(); + public final UploadDao uploadDao = NextcloudDatabase.instance().uploadDao(); + public final FileSystemDao fileSystemDao = NextcloudDatabase.instance().fileSystemDao(); public UploadsStorageManager( CurrentAccountProvider currentAccountProvider, diff --git a/app/src/main/java/com/owncloud/android/db/ProviderMeta.java b/app/src/main/java/com/owncloud/android/db/ProviderMeta.java index 280bd47673b3..35f03b822ae4 100644 --- a/app/src/main/java/com/owncloud/android/db/ProviderMeta.java +++ b/app/src/main/java/com/owncloud/android/db/ProviderMeta.java @@ -23,7 +23,7 @@ */ public class ProviderMeta { public static final String DB_NAME = "filelist"; - public static final int DB_VERSION = 98; + public static final int DB_VERSION = 99; private ProviderMeta() { // No instance @@ -361,6 +361,7 @@ static public class ProviderTableMeta implements BaseColumns { // Columns of filesystem data table public static final String FILESYSTEM_FILE_LOCAL_PATH = "local_path"; + public static final String FILESYSTEM_FILE_REMOTE_PATH = "remote_path"; public static final String FILESYSTEM_FILE_MODIFIED = "modified_at"; public static final String FILESYSTEM_FILE_IS_FOLDER = "is_folder"; public static final String FILESYSTEM_FILE_FOUND_RECENTLY = "found_at"; diff --git a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt index d72cbf0b614e..b33f41b7e2fb 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt @@ -778,14 +778,26 @@ class SyncedFoldersActivity : Log_OC.d(TAG, "auto-upload configuration sync status is disabled: " + item.remotePath) lifecycleScope.launch(Dispatchers.IO) { - fileUploadHelper.uploadsStorageManager.uploadDao - .removeEntities( - accountName = userAccountManager.user.accountName, - remotePath = item.remotePath - ) + removeEntityFromUploadEntities(item.id) } } + private suspend fun removeEntityFromUploadEntities(id: Long) { + val storageManager = fileUploadHelper.uploadsStorageManager + storageManager.fileSystemDao.getBySyncedFolderId(id.toString()) + .filter { it.localPath != null && it.remotePath != null } + .forEach { + Log_OC.d( + TAG, + "deleting upload entity localPath: ${it.localPath}, " + "remotePath: ${it.remotePath}" + ) + storageManager.uploadDao.deleteByLocalRemotePath( + localPath = it.localPath!!, + remotePath = it.remotePath!! + ) + } + } + override fun onDeleteSyncedFolderPreference(syncedFolder: SyncedFolderParcelable?) { if (syncedFolder == null) { return @@ -794,11 +806,7 @@ class SyncedFoldersActivity : Log_OC.d(TAG, "deleting auto upload configuration: " + syncedFolder.remotePath) lifecycleScope.launch(Dispatchers.IO) { - fileUploadHelper.uploadsStorageManager.uploadDao - .removeEntities( - accountName = userAccountManager.user.accountName, - remotePath = syncedFolder.remotePath - ) + removeEntityFromUploadEntities(syncedFolder.id) syncedFolderProvider.deleteSyncedFolder(syncedFolder.id) withContext(Dispatchers.Main) { adapter.removeItem(syncedFolder.section) From f2d01a891188d0fc2fcb1bfdfd5e79e62e75890a Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 20 Mar 2026 11:27:03 +0100 Subject: [PATCH 4/8] fix(auto-upload): clean stale upload entities Signed-off-by: alperozturk96 --- .../owncloud/android/ui/activity/SyncedFoldersActivity.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt index b33f41b7e2fb..ec7eba2cfc17 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt @@ -782,6 +782,13 @@ class SyncedFoldersActivity : } } + /** + * When a synced folder is disabled or deleted, its associated OCUpload entries in the uploads + * table must be cleaned up. Without this, stale upload entries outlive the folder config that + * created them, causing FileUploadWorker to keep retrying uploads for a folder that no longer + * exists or is intentionally turned off, and AutoUploadWorker to re-queue already handled files + * on its next scan via FileSystemRepository.getFilePathsWithIds. + */ private suspend fun removeEntityFromUploadEntities(id: Long) { val storageManager = fileUploadHelper.uploadsStorageManager storageManager.fileSystemDao.getBySyncedFolderId(id.toString()) From 8a2e2703f076d09c6643f1a8dfbb5c2bf4587586 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 20 Mar 2026 12:07:29 +0100 Subject: [PATCH 5/8] fix(auto-upload): clean stale upload entities when user delete auto upload folder Signed-off-by: alperozturk96 --- .../client/database/dao/SyncedFolderDao.kt | 10 ++++++ .../client/jobs/upload/FileUploadHelper.kt | 32 +++++++++++++++++++ .../datamodel/UploadsStorageManager.java | 2 ++ .../ui/activity/FileDisplayActivity.kt | 15 +++++++++ .../ui/activity/SyncedFoldersActivity.kt | 27 ++-------------- 5 files changed, 61 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/database/dao/SyncedFolderDao.kt b/app/src/main/java/com/nextcloud/client/database/dao/SyncedFolderDao.kt index e72ef28b2376..fb07cbeca955 100644 --- a/app/src/main/java/com/nextcloud/client/database/dao/SyncedFolderDao.kt +++ b/app/src/main/java/com/nextcloud/client/database/dao/SyncedFolderDao.kt @@ -27,4 +27,14 @@ interface SyncedFolderDao { @Query("SELECT * FROM ${ProviderMeta.ProviderTableMeta.SYNCED_FOLDERS_TABLE_NAME}") fun getAllAsFlow(): Flow> + + @Query( + """ + SELECT * FROM ${ProviderMeta.ProviderTableMeta.SYNCED_FOLDERS_TABLE_NAME} + WHERE ${ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_REMOTE_PATH} = :remotePath + AND ${ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_ACCOUNT} = :account + LIMIT 1 +""" + ) + suspend fun findByRemotePathAndAccount(remotePath: String, account: String): SyncedFolderEntity? } 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 634c60827d1c..6c84a58e9643 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.SyncedFolderEntity import com.nextcloud.client.database.entity.UploadEntity import com.nextcloud.client.database.entity.toOCUpload import com.nextcloud.client.database.entity.toUploadEntity @@ -39,9 +40,11 @@ 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.files.model.ServerFileInterface import com.owncloud.android.lib.resources.status.OCCapability import com.owncloud.android.operations.RemoveFileOperation import com.owncloud.android.operations.UploadFileOperation +import com.owncloud.android.ui.activity.SyncedFoldersActivity import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.FileUtil import kotlinx.coroutines.CoroutineScope @@ -615,4 +618,33 @@ class FileUploadHelper { } } } + + /** + * When a synced folder is disabled or deleted, its associated OCUpload entries in the uploads + * table must be cleaned up. Without this, stale upload entries outlive the folder config that + * created them, causing FileUploadWorker to keep retrying uploads for a folder that no longer + * exists or is intentionally turned off, and AutoUploadWorker to re-queue already handled files + * on its next scan via FileSystemRepository.getFilePathsWithIds. + */ + suspend fun removeEntityFromUploadEntities(id: Long) { + uploadsStorageManager.fileSystemDao.getBySyncedFolderId(id.toString()) + .filter { it.localPath != null && it.remotePath != null } + .forEach { + Log_OC.d( + TAG, + "deleting upload entity localPath: ${it.localPath}, " + "remotePath: ${it.remotePath}" + ) + uploadsStorageManager.uploadDao.deleteByLocalRemotePath( + localPath = it.localPath!!, + remotePath = it.remotePath!! + ) + } + } + + suspend fun getAutoUploadFolderEntity(file: ServerFileInterface, user: User): SyncedFolderEntity? { + val dao = uploadsStorageManager.syncedFolderDao + val normalizedRemotePath = file.remotePath.trimEnd() + if (normalizedRemotePath.isEmpty()) return null + return dao.findByRemotePathAndAccount(normalizedRemotePath, user.accountName) + } } diff --git a/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.java b/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.java index 92f38d53725c..8699642d15ab 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.java @@ -23,6 +23,7 @@ import com.nextcloud.client.account.User; import com.nextcloud.client.database.NextcloudDatabase; import com.nextcloud.client.database.dao.FileSystemDao; +import com.nextcloud.client.database.dao.SyncedFolderDao; import com.nextcloud.client.database.dao.UploadDao; import com.nextcloud.client.database.entity.UploadEntity; import com.nextcloud.client.database.entity.UploadEntityKt; @@ -72,6 +73,7 @@ public class UploadsStorageManager extends Observable { private OCCapability capability; public final UploadDao uploadDao = NextcloudDatabase.instance().uploadDao(); public final FileSystemDao fileSystemDao = NextcloudDatabase.instance().fileSystemDao(); + public final SyncedFolderDao syncedFolderDao = NextcloudDatabase.instance().syncedFolderDao(); public UploadsStorageManager( CurrentAccountProvider currentAccountProvider, diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt index 1377a60f672a..c4de9b2b03ab 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt @@ -2185,6 +2185,21 @@ class FileDisplayActivity : } supportInvalidateOptionsMenu() fetchRecommendedFilesIfNeeded(ignoreETag = true, currentDir) + + // clean stale upload entities for auto upload folder + if (removedFile.isFolder) { + lifecycleScope.launch(Dispatchers.IO) { + val optionalUser = user + if (user.isEmpty) { + return@launch + } + + val autoUploadFolder = fileUploadHelper + .getAutoUploadFolderEntity(removedFile, optionalUser.get()) ?: return@launch + + autoUploadFolder.id?.toLong()?.let { fileUploadHelper.removeEntityFromUploadEntities(it) } + } + } } else { if (result.isSslRecoverableException) { mLastSslUntrustedServerResult = result diff --git a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt index ec7eba2cfc17..9dd1d31c95a7 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt @@ -778,33 +778,10 @@ class SyncedFoldersActivity : Log_OC.d(TAG, "auto-upload configuration sync status is disabled: " + item.remotePath) lifecycleScope.launch(Dispatchers.IO) { - removeEntityFromUploadEntities(item.id) + fileUploadHelper.removeEntityFromUploadEntities(item.id) } } - /** - * When a synced folder is disabled or deleted, its associated OCUpload entries in the uploads - * table must be cleaned up. Without this, stale upload entries outlive the folder config that - * created them, causing FileUploadWorker to keep retrying uploads for a folder that no longer - * exists or is intentionally turned off, and AutoUploadWorker to re-queue already handled files - * on its next scan via FileSystemRepository.getFilePathsWithIds. - */ - private suspend fun removeEntityFromUploadEntities(id: Long) { - val storageManager = fileUploadHelper.uploadsStorageManager - storageManager.fileSystemDao.getBySyncedFolderId(id.toString()) - .filter { it.localPath != null && it.remotePath != null } - .forEach { - Log_OC.d( - TAG, - "deleting upload entity localPath: ${it.localPath}, " + "remotePath: ${it.remotePath}" - ) - storageManager.uploadDao.deleteByLocalRemotePath( - localPath = it.localPath!!, - remotePath = it.remotePath!! - ) - } - } - override fun onDeleteSyncedFolderPreference(syncedFolder: SyncedFolderParcelable?) { if (syncedFolder == null) { return @@ -813,7 +790,7 @@ class SyncedFoldersActivity : Log_OC.d(TAG, "deleting auto upload configuration: " + syncedFolder.remotePath) lifecycleScope.launch(Dispatchers.IO) { - removeEntityFromUploadEntities(syncedFolder.id) + fileUploadHelper.removeEntityFromUploadEntities(syncedFolder.id) syncedFolderProvider.deleteSyncedFolder(syncedFolder.id) withContext(Dispatchers.Main) { adapter.removeItem(syncedFolder.section) From acbb113214a75efa1ca245949c3fe9632df8efc3 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 26 Mar 2026 11:33:15 +0100 Subject: [PATCH 6/8] add dao funcs Signed-off-by: alperozturk96 --- .../client/database/dao/FileSystemDao.kt | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) 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 16e5db119231..068b819e8c4b 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 @@ -17,6 +17,25 @@ import com.owncloud.android.db.ProviderMeta @Dao interface FileSystemDao { + @Query( + """ + UPDATE ${ProviderMeta.ProviderTableMeta.FILESYSTEM_TABLE_NAME} + SET ${ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_REMOTE_PATH} = :remotePath + WHERE ${ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_LOCAL_PATH} = :localPath + AND ${ProviderMeta.ProviderTableMeta.FILESYSTEM_SYNCED_FOLDER_ID} = :syncedFolderId + """ + ) + suspend fun updateRemotePath(remotePath: String, localPath: String, syncedFolderId: String) + + @Query( + """ + SELECT * + FROM ${ProviderMeta.ProviderTableMeta.FILESYSTEM_TABLE_NAME} + WHERE ${ProviderMeta.ProviderTableMeta.FILESYSTEM_SYNCED_FOLDER_ID} = :syncedFolderId + """ + ) + suspend fun getBySyncedFolderId(syncedFolderId: String): List + @Query( """ SELECT COUNT(*) > 0 FROM ${ProviderMeta.ProviderTableMeta.FILESYSTEM_TABLE_NAME} From 841c1780dbebf8ada201da03d7046b76620f9ccd Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 26 Mar 2026 12:51:26 +0100 Subject: [PATCH 7/8] add onAutoUploadFolderRemoved Signed-off-by: alperozturk96 --- .../client/jobs/upload/FileUploadHelper.kt | 10 ++- .../extensions/FileActivityExtensions.kt | 50 +++++++++++++ .../ui/activity/FileDisplayActivity.kt | 73 +++++++++++++++---- .../ui/activity/OnFilesRemovedListener.kt | 8 ++ .../ui/dialog/RemoveFilesDialogFragment.kt | 68 ++++++++--------- .../ui/preview/PreviewImageActivity.kt | 7 ++ app/src/main/res/values/strings.xml | 4 + 7 files changed, 167 insertions(+), 53 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/utils/extensions/FileActivityExtensions.kt 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 6c84a58e9643..319e51e46af9 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 @@ -44,7 +44,6 @@ import com.owncloud.android.lib.resources.files.model.ServerFileInterface import com.owncloud.android.lib.resources.status.OCCapability import com.owncloud.android.operations.RemoveFileOperation import com.owncloud.android.operations.UploadFileOperation -import com.owncloud.android.ui.activity.SyncedFoldersActivity import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.FileUtil import kotlinx.coroutines.CoroutineScope @@ -641,10 +640,15 @@ class FileUploadHelper { } } - suspend fun getAutoUploadFolderEntity(file: ServerFileInterface, user: User): SyncedFolderEntity? { + suspend fun getAutoUploadFolder(files: List, accountName: String): List = + files.mapNotNull { file -> + getAutoUploadFolderEntity(file, accountName) + } + + suspend fun getAutoUploadFolderEntity(file: ServerFileInterface, accountName: String): SyncedFolderEntity? { val dao = uploadsStorageManager.syncedFolderDao val normalizedRemotePath = file.remotePath.trimEnd() if (normalizedRemotePath.isEmpty()) return null - return dao.findByRemotePathAndAccount(normalizedRemotePath, user.accountName) + return dao.findByRemotePathAndAccount(normalizedRemotePath, accountName) } } diff --git a/app/src/main/java/com/nextcloud/utils/extensions/FileActivityExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/FileActivityExtensions.kt new file mode 100644 index 000000000000..d558b38014fd --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/extensions/FileActivityExtensions.kt @@ -0,0 +1,50 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils.extensions + +import com.owncloud.android.R +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.ui.activity.FileActivity +import com.owncloud.android.ui.activity.FileDisplayActivity +import com.owncloud.android.ui.activity.OnFilesRemovedListener + +fun FileActivity.removeFiles( + offlineFiles: List, + files: List, + onlyLocalCopy: Boolean, + filesRemovedListener: OnFilesRemovedListener? +) { + connectivityService.isNetworkAndServerAvailable { isAvailable -> + if (isAvailable) { + showLoadingDialog(getString(R.string.wait_a_moment)) + + (this as? FileDisplayActivity) + ?.deleteBatchTracker + ?.startBatchDelete(files.size) + + if (files.isNotEmpty()) { + val inBackground = (files.size != 1) + fileOperationsHelper?.removeFiles(files, onlyLocalCopy, inBackground) + } + + if (offlineFiles.isNotEmpty()) { + filesRemovedListener?.onFilesRemoved() + } + + dismissLoadingDialog() + } else { + if (onlyLocalCopy) { + fileOperationsHelper?.removeFiles(files, true, true) + } else { + files.forEach(storageManager::addRemoveFileOfflineOperation) + } + + filesRemovedListener?.onFilesRemoved() + } + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt index c4de9b2b03ab..eb9bcc46f24c 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt @@ -60,6 +60,7 @@ import com.nextcloud.client.account.User import com.nextcloud.client.appinfo.AppInfo import com.nextcloud.client.core.AsyncRunner import com.nextcloud.client.core.Clock +import com.nextcloud.client.database.entity.SyncedFolderEntity import com.nextcloud.client.di.Injectable import com.nextcloud.client.editimage.EditImageActivity import com.nextcloud.client.files.DeepLinkHandler @@ -78,6 +79,7 @@ import com.nextcloud.model.WorkerState.OfflineOperationsCompleted import com.nextcloud.ui.composeActivity.ComposeProcessTextAlias import com.nextcloud.utils.extensions.getParcelableArgument import com.nextcloud.utils.extensions.isActive +import com.nextcloud.utils.extensions.isDialogFragmentReady import com.nextcloud.utils.extensions.lastFragment import com.nextcloud.utils.extensions.logFileSize import com.nextcloud.utils.extensions.navigateToAllFiles @@ -114,6 +116,7 @@ import com.owncloud.android.ui.asynctasks.CheckAvailableSpaceTask import com.owncloud.android.ui.asynctasks.CheckAvailableSpaceTask.CheckAvailableSpaceListener import com.owncloud.android.ui.asynctasks.FetchRemoteFileTask import com.owncloud.android.ui.asynctasks.GetRemoteFileTask +import com.owncloud.android.ui.dialog.ConfirmationDialogFragment import com.owncloud.android.ui.dialog.DeleteBatchTracker import com.owncloud.android.ui.dialog.SendShareDialog.SendShareDialogDownloader import com.owncloud.android.ui.dialog.SortingOrderDialogFragment.OnSortingOrderListener @@ -2185,26 +2188,68 @@ class FileDisplayActivity : } supportInvalidateOptionsMenu() fetchRecommendedFilesIfNeeded(ignoreETag = true, currentDir) + } else { + if (result.isSslRecoverableException) { + mLastSslUntrustedServerResult = result + showUntrustedCertDialog(mLastSslUntrustedServerResult) + } + } + } - // clean stale upload entities for auto upload folder - if (removedFile.isFolder) { + override fun onAutoUploadFolderRemoved( + entities: List, + filesToRemove: List, + onlyLocalCopy: Boolean + ) { + val dialog = ConfirmationDialogFragment.newInstance( + messageResId = R.string.auto_upload_delete_dialog_description, + messageArguments = null, + titleResId = R.string.auto_upload_delete_dialog_title, + titleIconId = R.drawable.ic_info, + positiveButtonTextId = R.string.common_delete, + negativeButtonTextId = R.string.common_cancel, + neutralButtonTextId = -1 + ) + + dialog.setOnConfirmationListener(object : ConfirmationDialogFragment.ConfirmationDialogFragmentListener { + override fun onConfirmation(callerTag: String?) { lifecycleScope.launch(Dispatchers.IO) { - val optionalUser = user - if (user.isEmpty) { - return@launch + entities.forEach { entity -> + entity.id?.toLong()?.let { + fileUploadHelper.removeEntityFromUploadEntities(it) + syncedFolderProvider.deleteSyncedFolder(it) + } } - val autoUploadFolder = fileUploadHelper - .getAutoUploadFolderEntity(removedFile, optionalUser.get()) ?: return@launch - - autoUploadFolder.id?.toLong()?.let { fileUploadHelper.removeEntityFromUploadEntities(it) } + withContext(Dispatchers.Main) { + connectivityService.isNetworkAndServerAvailable { isAvailable -> + if (isAvailable) { + fileOperationsHelper?.removeFiles( + filesToRemove, + onlyLocalCopy, + true + ) + } else { + if (onlyLocalCopy) { + fileOperationsHelper?.removeFiles(filesToRemove, true, true) + } else { + filesToRemove.forEach { file -> + fileDataStorageManager.addRemoveFileOfflineOperation(file) + } + } + } + onFilesRemoved() + } + } } } - } else { - if (result.isSslRecoverableException) { - mLastSslUntrustedServerResult = result - showUntrustedCertDialog(mLastSslUntrustedServerResult) - } + + override fun onNeutral(callerTag: String?) = Unit + override fun onCancel(callerTag: String?) = Unit + }) + + if (isDialogFragmentReady(dialog)) { + dialog.show(supportFragmentManager, null) } } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/OnFilesRemovedListener.kt b/app/src/main/java/com/owncloud/android/ui/activity/OnFilesRemovedListener.kt index edd275ee79fe..c843acc0d01c 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/OnFilesRemovedListener.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/OnFilesRemovedListener.kt @@ -7,6 +7,14 @@ package com.owncloud.android.ui.activity +import com.nextcloud.client.database.entity.SyncedFolderEntity +import com.owncloud.android.datamodel.OCFile + interface OnFilesRemovedListener { fun onFilesRemoved() + fun onAutoUploadFolderRemoved( + entities: List, + filesToRemove: List, + onlyLocalCopy: Boolean + ) } diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/RemoveFilesDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/RemoveFilesDialogFragment.kt index 9e8d98c562c4..9fdfc4a7e136 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/RemoveFilesDialogFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/RemoveFilesDialogFragment.kt @@ -16,17 +16,23 @@ import android.app.Dialog import android.os.Bundle import android.view.ActionMode import androidx.appcompat.app.AlertDialog +import androidx.lifecycle.lifecycleScope import com.google.android.material.button.MaterialButton +import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.di.Injectable +import com.nextcloud.client.jobs.upload.FileUploadHelper import com.nextcloud.client.network.ConnectivityService import com.nextcloud.utils.extensions.getTypedActivity +import com.nextcloud.utils.extensions.removeFiles import com.owncloud.android.R import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.OCFile import com.owncloud.android.ui.activity.FileActivity -import com.owncloud.android.ui.activity.FileDisplayActivity import com.owncloud.android.ui.activity.OnFilesRemovedListener import com.owncloud.android.ui.dialog.ConfirmationDialogFragment.ConfirmationDialogFragmentListener +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import javax.inject.Inject /** @@ -46,6 +52,9 @@ class RemoveFilesDialogFragment : @Inject lateinit var connectivityService: ConnectivityService + @Inject + lateinit var userAccountManager: UserAccountManager + private var positiveButton: MaterialButton? = null override fun onStart() { @@ -94,44 +103,31 @@ class RemoveFilesDialogFragment : } private fun removeFiles(onlyLocalCopy: Boolean) { - val (offlineFiles, files) = mTargetFiles?.partition { it.isOfflineOperation } ?: Pair(emptyList(), emptyList()) - - offlineFiles.forEach { - fileDataStorageManager.deleteOfflineOperation(it) - } - - val fileActivity = getTypedActivity(FileActivity::class.java) - val fda = getTypedActivity(FileDisplayActivity::class.java) - val filesRemovedListener = getTypedActivity(OnFilesRemovedListener::class.java) - connectivityService.isNetworkAndServerAvailable { isAvailable -> - if (isAvailable) { - fileActivity?.showLoadingDialog(fileActivity.getString(R.string.wait_a_moment)) - - fda?.deleteBatchTracker?.startBatchDelete(files.size) - - if (files.isNotEmpty()) { - // Display the snackbar message only when a single file is deleted. - val inBackground = (files.size != 1) - fileActivity?.fileOperationsHelper?.removeFiles(files, onlyLocalCopy, inBackground) - } - - if (offlineFiles.isNotEmpty()) { - filesRemovedListener?.onFilesRemoved() + val (offlineFiles, files) = mTargetFiles + ?.partition { it.isOfflineOperation } + ?: (emptyList() to emptyList()) + + offlineFiles.forEach(fileDataStorageManager::deleteOfflineOperation) + + val listener = getTypedActivity(OnFilesRemovedListener::class.java) + + lifecycleScope.launch(Dispatchers.IO) { + val autoUploadEntities = + FileUploadHelper.instance().getAutoUploadFolder(files, userAccountManager.user.accountName) + withContext(Dispatchers.Main) { + if (autoUploadEntities.isNotEmpty()) { + listener?.onAutoUploadFolderRemoved( + entities = autoUploadEntities, + filesToRemove = files, + onlyLocalCopy = onlyLocalCopy + ) + return@withContext } - fileActivity?.dismissLoadingDialog() - } else { - if (onlyLocalCopy) { - fileActivity?.fileOperationsHelper?.removeFiles(files, true, true) - } else { - files.forEach { file -> - fileDataStorageManager.addRemoveFileOfflineOperation(file) - } - } - filesRemovedListener?.onFilesRemoved() + val fileActivity = getTypedActivity(FileActivity::class.java) + fileActivity?.removeFiles(offlineFiles, files, onlyLocalCopy, listener) + finishActionMode() } - - finishActionMode() } } diff --git a/app/src/main/java/com/owncloud/android/ui/preview/PreviewImageActivity.kt b/app/src/main/java/com/owncloud/android/ui/preview/PreviewImageActivity.kt index 4bf68f617a1b..fbca6af56939 100644 --- a/app/src/main/java/com/owncloud/android/ui/preview/PreviewImageActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/preview/PreviewImageActivity.kt @@ -22,6 +22,7 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback import com.nextcloud.client.account.User +import com.nextcloud.client.database.entity.SyncedFolderEntity import com.nextcloud.client.di.Injectable import com.nextcloud.client.editimage.EditImageActivity import com.nextcloud.client.jobs.download.FileDownloadEventBroadcaster @@ -216,6 +217,12 @@ class PreviewImageActivity : initViewPager() } + override fun onAutoUploadFolderRemoved( + entities: List, + filesToRemove: List, + onlyLocalCopy: Boolean + ) = Unit + fun initViewPager() { if (user.isPresent) { initViewPager(user.get()) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 903ccd04fec3..6e1b428408e5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1471,6 +1471,10 @@ Create link This folder is best viewed in %1$s. Open in %1$s + Delete auto-upload folder? + + This will remove the folder and auto-upload configuration. Any unfinished uploads will be canceled. + Auto-upload is paused because Battery Saver is on. This folder is already included in the parent folder’s sync, which may cause duplicate uploads Sync anyway From 26eb15a7b206e6acce814ee6981178f875723bf9 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 26 Mar 2026 13:44:54 +0100 Subject: [PATCH 8/8] allow user to delete normal files together with auto upload folders Signed-off-by: alperozturk96 --- .../client/jobs/upload/FileUploadHelper.kt | 26 ++++++++++++++++--- .../ui/dialog/RemoveFilesDialogFragment.kt | 7 +++-- 2 files changed, 26 insertions(+), 7 deletions(-) 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 319e51e46af9..1cfb98b903cf 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 @@ -640,11 +640,31 @@ class FileUploadHelper { } } - suspend fun getAutoUploadFolder(files: List, accountName: String): List = - files.mapNotNull { file -> - getAutoUploadFolderEntity(file, accountName) + /** + * Splits a list of files into: + * 1. Files that have an auto-upload folder configured. + * 2. Files that don't. + */ + suspend fun splitFilesByAutoUpload( + files: List, + accountName: String + ): Pair, List> { + + val autoUploadFolders = mutableListOf() + val nonAutoUploadFiles = mutableListOf() + + for (file in files) { + val entity = getAutoUploadFolderEntity(file, accountName) + if (entity != null) { + autoUploadFolders.add(entity) + } else { + nonAutoUploadFiles.add(file) + } } + return autoUploadFolders to nonAutoUploadFiles + } + suspend fun getAutoUploadFolderEntity(file: ServerFileInterface, accountName: String): SyncedFolderEntity? { val dao = uploadsStorageManager.syncedFolderDao val normalizedRemotePath = file.remotePath.trimEnd() diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/RemoveFilesDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/RemoveFilesDialogFragment.kt index 9fdfc4a7e136..304b0961d8ea 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/RemoveFilesDialogFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/RemoveFilesDialogFragment.kt @@ -112,8 +112,8 @@ class RemoveFilesDialogFragment : val listener = getTypedActivity(OnFilesRemovedListener::class.java) lifecycleScope.launch(Dispatchers.IO) { - val autoUploadEntities = - FileUploadHelper.instance().getAutoUploadFolder(files, userAccountManager.user.accountName) + val (autoUploadEntities, filesToRemove) = + FileUploadHelper.instance().splitFilesByAutoUpload(files, userAccountManager.user.accountName) withContext(Dispatchers.Main) { if (autoUploadEntities.isNotEmpty()) { listener?.onAutoUploadFolderRemoved( @@ -121,11 +121,10 @@ class RemoveFilesDialogFragment : filesToRemove = files, onlyLocalCopy = onlyLocalCopy ) - return@withContext } val fileActivity = getTypedActivity(FileActivity::class.java) - fileActivity?.removeFiles(offlineFiles, files, onlyLocalCopy, listener) + fileActivity?.removeFiles(offlineFiles, filesToRemove, onlyLocalCopy, listener) finishActionMode() } }