From b40f282507853f2e9ea6b7b5c3a4f0ad2037f75f 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 c949f83e04bf..7add4c681e4a 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 8bb48c3eb37f4afcaf7b433e034f19bceadb5427 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 7add4c681e4a..a9e7d9bf8578 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 fe7069a75d0be50a31c78de4a3730ee97247c5e6 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 # Conflicts: # app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.java --- .../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 + .../com/owncloud/android/db/ProviderMeta.java | 3 +- .../ui/activity/SyncedFoldersActivity.kt | 28 +- 8 files changed, 1359 insertions(+), 21 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 a9e7d9bf8578..07db239a7dd6 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 446e3da55975..84902f10becb 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 @@ -293,6 +293,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 685bb7a77dc7..6ca9e37629fc 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/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 e2f13c41ba3f0d8a9ca69f17f77c80b19cb02f5a 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 e76adc59f77434eddd2398d0778a8b95782b7bb0 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 +++++++++++++++++++ .../ui/activity/FileDisplayActivity.kt | 15 +++++++++ .../ui/activity/SyncedFoldersActivity.kt | 27 ++-------------- 4 files changed, 59 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 f1947b3287bf..ab05fefbbf61 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 @@ -40,9 +41,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 @@ -614,4 +617,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/ui/activity/FileDisplayActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt index 05178e85acaa..c5e2f7215320 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 @@ -2192,6 +2192,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 26f50d42494f4c30df70709ddb1e3f59571bf654 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 97cca77b03a877a34f8ff8026678beb721bcd821 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 ab05fefbbf61..f1ade62392ed 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 @@ -45,7 +45,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 @@ -640,10 +639,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 c5e2f7215320..a815fa69812c 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 @@ -115,6 +117,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 @@ -2192,26 +2195,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 65591ae894c2..48abb84f2cef 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1473,6 +1473,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 4c1bce28dd652982545e82e219a0255c7f30bd2f 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 f1ade62392ed..b0ec486822e3 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 @@ -639,11 +639,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() } }