From fe93cbdbbb50bdc552580d33ef901f6841f5d46d Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 13 Feb 2026 12:58:47 +0100 Subject: [PATCH 01/17] fix(file-listing): local file indicator icon Signed-off-by: alperozturk96 --- .../99.json | 1308 +++++++++++++++++ .../client/database/NextcloudDatabase.kt | 3 +- .../nextcloud/client/database/dao/FileDao.kt | 17 + .../client/database/entity/FileEntity.kt | 4 +- .../client/files/FileIndicatorManager.kt | 35 + .../jobs/download/FileDownloadWorker.kt | 6 + .../folderDownload/FolderDownloadWorker.kt | 8 + .../datamodel/FileDataStorageManager.java | 43 +- .../owncloud/android/datamodel/OCFile.java | 13 + .../com/owncloud/android/db/ProviderMeta.java | 6 +- .../ui/activity/FileDisplayActivity.kt | 40 + .../android/ui/adapter/OCFileListAdapter.java | 28 + .../android/ui/adapter/OCFileListDelegate.kt | 53 +- .../MockOCFileListAdapterDataProvider.kt | 3 +- 14 files changed, 1520 insertions(+), 47 deletions(-) create mode 100644 app/schemas/com.nextcloud.client.database.NextcloudDatabase/99.json create mode 100644 app/src/main/java/com/nextcloud/client/files/FileIndicatorManager.kt 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..df3dcf664f9a --- /dev/null +++ b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/99.json @@ -0,0 +1,1308 @@ +{ + "formatVersion": 1, + "database": { + "version": 99, + "identityHash": "34850512e217b570d0ab1616ee91b87f", + "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, `file_indicator` 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" + }, + { + "fieldPath": "fileIndicator", + "columnName": "file_indicator", + "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, `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": "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, '34850512e217b570d0ab1616ee91b87f')" + ] + } +} \ 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/FileDao.kt b/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt index 86d02064c7a3..b7f50f793d19 100644 --- a/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt +++ b/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt @@ -9,6 +9,7 @@ package com.nextcloud.client.database.dao import androidx.room.Dao import androidx.room.Query +import androidx.room.Transaction import androidx.room.Update import com.nextcloud.client.database.entity.FileEntity import com.owncloud.android.db.ProviderMeta.ProviderTableMeta @@ -146,4 +147,20 @@ interface FileDao { @Query("SELECT remote_id FROM filelist WHERE file_owner = :accountName AND remote_id IS NOT NULL") fun getAllRemoteIds(accountName: String): List + + @Transaction + fun updateFileIndicatorsBatch(updates: List>) { + updates.forEach { (fileId, indicator) -> + updateFileIndicator(fileId, indicator) + } + } + + @Query( + """ + UPDATE ${ProviderTableMeta.FILE_TABLE_NAME} + SET ${ProviderTableMeta.FILE_INDICATOR} = :indicator + WHERE ${ProviderTableMeta._ID} = :fileId + """ + ) + fun updateFileIndicator(fileId: Long, indicator: Int?) } diff --git a/app/src/main/java/com/nextcloud/client/database/entity/FileEntity.kt b/app/src/main/java/com/nextcloud/client/database/entity/FileEntity.kt index 175287ba738c..3d54b912adbc 100644 --- a/app/src/main/java/com/nextcloud/client/database/entity/FileEntity.kt +++ b/app/src/main/java/com/nextcloud/client/database/entity/FileEntity.kt @@ -121,5 +121,7 @@ data class FileEntity( @ColumnInfo(name = ProviderTableMeta.FILE_INTERNAL_TWO_WAY_SYNC_RESULT) val internalTwoWaySyncResult: String?, @ColumnInfo(name = ProviderTableMeta.FILE_UPLOADED) - val uploaded: Long? + val uploaded: Long?, + @ColumnInfo(name = ProviderTableMeta.FILE_INDICATOR) + val fileIndicator: Int? ) diff --git a/app/src/main/java/com/nextcloud/client/files/FileIndicatorManager.kt b/app/src/main/java/com/nextcloud/client/files/FileIndicatorManager.kt new file mode 100644 index 000000000000..7ac21bc5c14b --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/files/FileIndicatorManager.kt @@ -0,0 +1,35 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.files + +import com.owncloud.android.R +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update + +sealed class FileIndicator(val iconRes: Int?) { + data object Idle : FileIndicator(null) + data object Downloading : FileIndicator(R.drawable.ic_synchronizing) + data object Error : FileIndicator(R.drawable.ic_synchronizing_error) + data object Downloaded : FileIndicator(R.drawable.ic_synced) +} + +object FileIndicatorManager { + private val _activeTransfers = MutableStateFlow>(emptyMap()) + val activeTransfers: StateFlow> = _activeTransfers + + fun update(fileId: Long, status: FileIndicator) { + _activeTransfers.update { current -> + if (status is FileIndicator.Idle) { + current - fileId + } else { + current + (fileId to status) + } + } + } +} diff --git a/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadWorker.kt index dfa4c12524ae..7d091ea9269a 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadWorker.kt @@ -21,6 +21,8 @@ import androidx.work.ForegroundInfo import androidx.work.WorkerParameters import com.nextcloud.client.account.User import com.nextcloud.client.account.UserAccountManager +import com.nextcloud.client.files.FileIndicator +import com.nextcloud.client.files.FileIndicatorManager import com.nextcloud.model.WorkerState import com.nextcloud.model.WorkerStateObserver import com.nextcloud.utils.ForegroundServiceHelper @@ -211,6 +213,7 @@ class FileDownloadWorker( file.remotePath, operation ) ?: Pair(null, null) + FileIndicatorManager.update(file.fileId, FileIndicator.Downloading) downloadKey?.let { requestedDownloads.add(downloadKey) @@ -354,6 +357,7 @@ class FileDownloadWorker( private fun checkDownloadError(result: RemoteOperationResult<*>) { if (result.isSuccess || downloadError != null) { + currentDownload?.file?.fileId?.let { FileIndicatorManager.update(it, FileIndicator.Downloaded) } notificationManager.dismissNotification() return } @@ -363,6 +367,8 @@ class FileDownloadWorker( } else { FileDownloadError.Failed } + + currentDownload?.file?.fileId?.let { FileIndicatorManager.update(it, FileIndicator.Error) } } private fun showDownloadErrorNotification(downloadError: FileDownloadError) { diff --git a/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorker.kt index 337ff1bf6ee7..49e2ca3fddfa 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorker.kt @@ -12,6 +12,8 @@ import androidx.work.CoroutineWorker import androidx.work.ForegroundInfo import androidx.work.WorkerParameters import com.nextcloud.client.account.UserAccountManager +import com.nextcloud.client.files.FileIndicator +import com.nextcloud.client.files.FileIndicatorManager import com.nextcloud.client.jobs.download.FileDownloadHelper import com.nextcloud.model.WorkerState import com.nextcloud.model.WorkerStateObserver @@ -75,6 +77,7 @@ class FolderDownloadWorker( return Result.failure() } + FileIndicatorManager.update(folder.fileId, FileIndicator.Downloading) Log_OC.d(TAG, "🕒 started for ${user.accountName} downloading ${folder.fileName}") trySetForeground(folder) @@ -108,9 +111,11 @@ class FolderDownloadWorker( setForeground(foregroundInfo) } + FileIndicatorManager.update(file.fileId, FileIndicator.Downloading) val operation = DownloadFileOperation(user, file, context) val operationResult = operation.execute(client) if (operationResult?.isSuccess == true && operation.downloadType === DownloadType.DOWNLOAD) { + FileIndicatorManager.update(file.fileId, FileIndicator.Downloaded) getOCFile(operation)?.let { ocFile -> downloadHelper.saveFile(ocFile, operation, storageManager) } @@ -127,13 +132,16 @@ class FolderDownloadWorker( if (result) { Log_OC.d(TAG, "✅ completed") + FileIndicatorManager.update(folderID, FileIndicator.Downloaded) Result.success() } else { Log_OC.d(TAG, "❌ failed") + FileIndicatorManager.update(folderID, FileIndicator.Error) Result.failure() } } catch (e: Exception) { Log_OC.d(TAG, "❌ failed reason: $e") + FileIndicatorManager.update(folderID, FileIndicator.Error) Result.failure() } finally { WorkerStateObserver.send(WorkerState.FolderDownloadCompleted(folder)) diff --git a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java index 497776f6babf..ad85d60523e5 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java @@ -785,6 +785,12 @@ public void saveFolder(OCFile folder, List updatedFiles, Collection 0) { + fileDao.updateFileIndicator(folder.getFileId(), null); + } + + List children = getFolderContent(folder.getFileId(), false); + for (OCFile child : children) { + if (child.isFolder()) { + clearFileIndicatorsForFolderAndChildren(child); + } else { + if (child.getFileId() > 0) { + fileDao.updateFileIndicator(child.getFileId(), null); + } + } + } + } catch (Exception e) { + Log_OC.e(TAG, "Error clearing file indicators for folder: " + folder.getRemotePath(), e); + } + } public boolean removeFolder(OCFile folder, boolean removeDBData, boolean removeLocalContent) { boolean success = true; @@ -926,6 +963,7 @@ public boolean removeFolder(OCFile folder, boolean removeDBData, boolean removeL success = removeFolderInDb(folder); } if (removeLocalContent && success) { + clearFileIndicatorsForFolderAndChildren(folder); success = removeLocalFolder(folder); } } else { @@ -972,6 +1010,8 @@ private boolean removeLocalFolder(OCFile folder) { // notify MediaScanner about removed file deleteFileInMediaScan(ocFile.getStoragePath()); ocFile.setStoragePath(null); + fileDao.updateFileIndicator(ocFile.getFileId(), null); + ocFile.setFileIndicator(null); saveFile(ocFile); } } @@ -1258,6 +1298,7 @@ public OCFile createFileInstance(FileEntity fileEntity) { } ocFile.setFileLength(nullToZero(fileEntity.getContentLength())); ocFile.setUploadTimestamp(nullToZero(fileEntity.getUploaded())); + ocFile.setFileIndicator(nullToZero(fileEntity.getFileIndicator())); ocFile.setCreationTimestamp(nullToZero(fileEntity.getCreation())); ocFile.setModificationTimestamp(nullToZero(fileEntity.getModified())); ocFile.setModificationTimestampAtLastSyncForData(nullToZero(fileEntity.getModifiedAtLastSyncForData())); diff --git a/app/src/main/java/com/owncloud/android/datamodel/OCFile.java b/app/src/main/java/com/owncloud/android/datamodel/OCFile.java index 03d85e22a902..7c0678582275 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/OCFile.java +++ b/app/src/main/java/com/owncloud/android/datamodel/OCFile.java @@ -134,6 +134,8 @@ public class OCFile implements Parcelable, Comparable, ServerFileInterfa private String reason = ""; // endregion + private Integer fileIndicator = null; + /** * URI to the local path of the file contents, if stored in the device; cached after first call to * {@link #getStorageUri()} @@ -212,6 +214,7 @@ private OCFile(Parcel source) { lockTimeout = source.readLong(); lockToken = source.readString(); livePhoto = source.readString(); + fileIndicator = source.readInt(); } @Override @@ -258,6 +261,7 @@ public void writeToParcel(Parcel dest, int flags) { dest.writeLong(lockTimeout); dest.writeString(lockToken); dest.writeString(livePhoto); + dest.writeInt(fileIndicator != null ? fileIndicator : -1); } public String getLinkedFileIdForLivePhoto() { @@ -530,6 +534,7 @@ private void resetData() { lockToken = null; livePhoto = null; imageDimension = null; + fileIndicator = null; } /** @@ -1175,4 +1180,12 @@ public boolean hasValidParentId() { return getParentId() != 0; } } + + public void setFileIndicator(Integer indicator) { + fileIndicator = indicator; + } + + public Integer getFileIndicator() { + return fileIndicator; + } } 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..6cd0bcd93f89 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 @@ -91,6 +91,7 @@ static public class ProviderTableMeta implements BaseColumns { public static final String FILE_CREATION = "created"; public static final String FILE_MODIFIED = "modified"; public static final String FILE_UPLOADED = "uploaded"; + public static final String FILE_INDICATOR = "file_indicator"; public static final String FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA = "modified_at_last_sync_for_data"; public static final String FILE_CONTENT_LENGTH = "content_length"; public static final String FILE_CONTENT_TYPE = "content_type"; @@ -190,7 +191,8 @@ static public class ProviderTableMeta implements BaseColumns { FILE_TAGS, FILE_METADATA_GPS, FILE_INTERNAL_TWO_WAY_SYNC_TIMESTAMP, - FILE_INTERNAL_TWO_WAY_SYNC_RESULT); + FILE_INTERNAL_TWO_WAY_SYNC_RESULT, + FILE_INDICATOR); public static final String FILE_DEFAULT_SORT_ORDER = FILE_NAME + " collate nocase asc"; // Columns of ocshares table 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 2b22bdb8f58f..8da7231425bd 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 @@ -47,7 +47,9 @@ import androidx.appcompat.widget.SearchView import androidx.core.view.MenuItemCompat import androidx.core.view.isVisible import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.localbroadcastmanager.content.LocalBroadcastManager import com.google.android.material.appbar.AppBarLayout import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -61,6 +63,8 @@ import com.nextcloud.client.core.Clock import com.nextcloud.client.di.Injectable import com.nextcloud.client.editimage.EditImageActivity import com.nextcloud.client.files.DeepLinkHandler +import com.nextcloud.client.files.FileIndicator +import com.nextcloud.client.files.FileIndicatorManager import com.nextcloud.client.jobs.download.FileDownloadHelper import com.nextcloud.client.jobs.download.FileDownloadWorker import com.nextcloud.client.jobs.download.FileDownloadWorker.Companion.getDownloadAddedMessage @@ -157,6 +161,9 @@ import com.owncloud.android.utils.PushUtils import com.owncloud.android.utils.StringUtils import com.owncloud.android.utils.theme.CapabilityUtils import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.greenrobot.eventbus.EventBus @@ -279,6 +286,7 @@ class FileDisplayActivity : startMetadataSyncForRoot() handleBackPress() setupDrawer(menuItemId) + observeFileIndicatorState() } /** @@ -2129,6 +2137,10 @@ class FileDisplayActivity : } supportInvalidateOptionsMenu() fetchRecommendedFilesIfNeeded(ignoreETag = true, currentDir) + + FileIndicatorManager.update(removedFile.fileId, FileIndicator.Idle) + storageManager.fileDao.updateFileIndicator(removedFile.fileId, null) + listOfFilesFragment?.adapter?.updateFileIndicators(mapOf(removedFile.fileId to FileIndicator.Idle)) } else { if (result.isSslRecoverableException) { mLastSslUntrustedServerResult = result @@ -3058,6 +3070,34 @@ class FileDisplayActivity : } // endregion + @Suppress("MagicNumber") + @OptIn(FlowPreview::class) + private fun observeFileIndicatorState() { + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + FileIndicatorManager.activeTransfers + .debounce(100) + .collectLatest { indicators -> + if (indicators.isEmpty()) return@collectLatest + + withContext(Dispatchers.Main) { + // update UI with hot data + listOfFilesFragment?.adapter?.updateFileIndicators(indicators) + + // update cold data in background + launch(Dispatchers.IO) { + storageManager.fileDao.updateFileIndicatorsBatch( + indicators.map { (fileId, indicator) -> + fileId to indicator.iconRes + } + ) + } + } + } + } + } + } + companion object { const val RESTART: String = "RESTART" const val ALL_FILES: String = "ALL_FILES" diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java index c93624ba352c..8a2ae2d3b728 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java @@ -30,6 +30,7 @@ import com.nextcloud.android.common.ui.theme.utils.ColorRole; import com.nextcloud.client.account.User; import com.nextcloud.client.database.entity.OfflineOperationEntity; +import com.nextcloud.client.files.FileIndicator; import com.nextcloud.client.jobs.upload.FileUploadHelper; import com.nextcloud.client.preferences.AppPreferences; import com.nextcloud.model.OfflineOperationType; @@ -75,10 +76,14 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Date; +import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.UUID; +import java.util.function.BiConsumer; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -1061,4 +1066,27 @@ public void removeAllFiles() { mFilesAll.clear(); notifyDataSetChanged(); } + + public void updateFileIndicators(Map indicators) { + if (indicators == null || indicators.isEmpty()) { + return; + } + + Map positionMap = new HashMap<>(); + for (int i = 0; i < mFiles.size(); i++) { + positionMap.put(mFiles.get(i).getFileId(), i); + } + + indicators.forEach((id, fileIndicator) -> { + Integer position = positionMap.get(id); + if (position != null) { + OCFile file = mFiles.get(position); + Integer newIndicator = fileIndicator.getIconRes(); + if (!Objects.equals(file.getFileIndicator(), newIndicator)) { + file.setFileIndicator(newIndicator); + notifyItemChanged(file); + } + } + }); + } } diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt index 343659e8938a..c0a0e184aeb5 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt @@ -14,13 +14,12 @@ import androidx.core.content.ContextCompat import com.elyeproj.loaderviewlibrary.LoaderImageView import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.client.account.User -import com.nextcloud.client.jobs.download.FileDownloadHelper +import com.nextcloud.client.files.FileIndicator import com.nextcloud.client.jobs.gallery.GalleryImageGenerationJob import com.nextcloud.client.jobs.gallery.GalleryImageGenerationListener import com.nextcloud.client.jobs.upload.FileUploadHelper import com.nextcloud.client.preferences.AppPreferences import com.nextcloud.utils.OCFileUtils -import com.nextcloud.utils.extensions.getSubfiles import com.nextcloud.utils.extensions.makeRounded import com.nextcloud.utils.extensions.setVisibleIf import com.nextcloud.utils.mdm.MDMConfig @@ -43,7 +42,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext @Suppress("LongParameterList", "TooManyFunctions") class OCFileListDelegate( @@ -315,7 +313,7 @@ class OCFileListDelegate( private fun bindGridMetadataViews(file: OCFile, gridViewHolder: ListViewHolder) { if (showMetadata) { - showLocalFileIndicator(file, gridViewHolder) + showFileIndicator(file, gridViewHolder) gridViewHolder.favorite.visibility = if (file.isFavorite) View.VISIBLE else View.GONE } else { gridViewHolder.localFileIndicator.visibility = View.GONE @@ -323,45 +321,18 @@ class OCFileListDelegate( } } - private suspend fun isFolderFullyDownloaded(file: OCFile): Boolean = withContext(Dispatchers.IO) { - file.isFolder && - storageManager.getSubfiles(file.fileId, user.accountName) - .takeIf { it.isNotEmpty() } - ?.all { it.isDown } == true - } - - private fun isSynchronizing(file: OCFile): Boolean { - val operationsServiceBinder = transferServiceGetter.operationsServiceBinder - val fileDownloadHelper = FileDownloadHelper.instance() - - return operationsServiceBinder?.isSynchronizing(user, file) == true || - fileDownloadHelper.isDownloading(user, file) || - fileUploadHelper.isUploading(file.remotePath, user.accountName) - } - - private fun showLocalFileIndicator(file: OCFile, holder: ListViewHolder) { - ioScope.launch { - val isFullyDownloaded = isFolderFullyDownloaded(file) - val isSyncing = isSynchronizing(file) - val hasConflict = (file.etagInConflict != null) - val isDown = file.isDown - - val icon = when { - isSyncing -> R.drawable.ic_synchronizing - hasConflict -> R.drawable.ic_synchronizing_error - isDown || isFullyDownloaded -> R.drawable.ic_synced - else -> null + private fun showFileIndicator(file: OCFile, holder: ListViewHolder) { + holder.localFileIndicator.run { + var indicator = file.fileIndicator + if (file.etagInConflict != null) { + indicator = FileIndicator.Error.iconRes } - withContext(Dispatchers.Main) { - holder.localFileIndicator.run { - if (icon != null && showMetadata) { - setImageResource(icon) - visibility = View.VISIBLE - } else { - visibility = View.GONE - } - } + if (indicator != null && indicator != 0) { + setImageResource(indicator) + visibility = View.VISIBLE + } else { + visibility = View.GONE } } } diff --git a/app/src/test/java/com/owncloud/android/ui/adapter/MockOCFileListAdapterDataProvider.kt b/app/src/test/java/com/owncloud/android/ui/adapter/MockOCFileListAdapterDataProvider.kt index c2b71334a569..6c781896c98b 100644 --- a/app/src/test/java/com/owncloud/android/ui/adapter/MockOCFileListAdapterDataProvider.kt +++ b/app/src/test/java/com/owncloud/android/ui/adapter/MockOCFileListAdapterDataProvider.kt @@ -71,7 +71,8 @@ class MockOCFileListAdapterDataProvider : OCFileListAdapterDataProvider { e2eCounter = 0L, internalTwoWaySync = 0L, internalTwoWaySyncResult = null, - uploaded = 0L + uploaded = 0L, + fileIndicator = null ) } From fe0eef9b9e29687b3e00efb87737c1e31d7d7d95 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 13 Feb 2026 13:32:24 +0100 Subject: [PATCH 02/17] fix(file-listing): single file deletion Signed-off-by: alperozturk96 --- .../android/ui/activity/FileDisplayActivity.kt | 2 +- .../android/ui/adapter/OCFileListAdapter.java | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) 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 8da7231425bd..005cb65b73c7 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 @@ -2140,7 +2140,7 @@ class FileDisplayActivity : FileIndicatorManager.update(removedFile.fileId, FileIndicator.Idle) storageManager.fileDao.updateFileIndicator(removedFile.fileId, null) - listOfFilesFragment?.adapter?.updateFileIndicators(mapOf(removedFile.fileId to FileIndicator.Idle)) + listOfFilesFragment?.adapter?.removeFileIndicator(removedFile) } else { if (result.isSslRecoverableException) { mLastSslUntrustedServerResult = result diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java index 8a2ae2d3b728..60624ae9e225 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java @@ -1067,6 +1067,13 @@ public void removeAllFiles() { notifyDataSetChanged(); } + public void removeFileIndicator(OCFile file) { + if (file != null) { + file.setFileIndicator(null); + notifyItemChanged(file); + } + } + public void updateFileIndicators(Map indicators) { if (indicators == null || indicators.isEmpty()) { return; @@ -1089,4 +1096,13 @@ public void updateFileIndicators(Map indicators) { } }); } + + private OCFile findOCFile(long id) { + for (OCFile file : mFiles) { + if (file.getFileId() == id) { + return file; + } + } + return null; + } } From c450aa5c5b28a7fa59b06955bf0371643d760265 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 13 Feb 2026 13:36:37 +0100 Subject: [PATCH 03/17] fix(file-listing): folder's file deletion also removes folder's indicator Signed-off-by: alperozturk96 --- .../owncloud/android/ui/activity/FileDisplayActivity.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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 005cb65b73c7..61ccb3f6f699 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 @@ -2122,6 +2122,7 @@ class FileDisplayActivity : } val parentFile = storageManager.getFileById(removedFile.parentId) if (parentFile != null && parentFile == getCurrentDir()) { + removeFileIndicator(parentFile) updateListOfFilesFragment() } else if (leftFragment is OCFileListFragment && SearchRemoteOperation.SearchType.FAVORITE_SEARCH == leftFragment.searchEvent?.searchType @@ -2138,8 +2139,7 @@ class FileDisplayActivity : supportInvalidateOptionsMenu() fetchRecommendedFilesIfNeeded(ignoreETag = true, currentDir) - FileIndicatorManager.update(removedFile.fileId, FileIndicator.Idle) - storageManager.fileDao.updateFileIndicator(removedFile.fileId, null) + removeFileIndicator(removedFile) listOfFilesFragment?.adapter?.removeFileIndicator(removedFile) } else { if (result.isSslRecoverableException) { @@ -2149,6 +2149,11 @@ class FileDisplayActivity : } } + private fun removeFileIndicator(file: OCFile) { + FileIndicatorManager.update(file.fileId, FileIndicator.Idle) + storageManager.fileDao.updateFileIndicator(file.fileId, null) + } + private fun onRestoreFileVersionOperationFinish(result: RemoteOperationResult<*>) { if (result.isSuccess) { val file = getFile() From 4282ebdbb417b7bc0550f4d602d0d2eddc762480 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 13 Feb 2026 15:07:11 +0100 Subject: [PATCH 04/17] fix spotbugs Signed-off-by: alperozturk96 --- .../android/ui/adapter/OCFileListAdapter.java | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java index 60624ae9e225..760c2df95d10 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java @@ -1079,7 +1079,7 @@ public void updateFileIndicators(Map indicators) { return; } - Map positionMap = new HashMap<>(); + Map positionMap = new HashMap<>(mFiles.size()); for (int i = 0; i < mFiles.size(); i++) { positionMap.put(mFiles.get(i).getFileId(), i); } @@ -1096,13 +1096,4 @@ public void updateFileIndicators(Map indicators) { } }); } - - private OCFile findOCFile(long id) { - for (OCFile file : mFiles) { - if (file.getFileId() == id) { - return file; - } - } - return null; - } } From 7e5ee19612b7209426445a1b9f8ae9d6ea816e0e Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 13 Feb 2026 16:00:02 +0100 Subject: [PATCH 05/17] fix deletion Signed-off-by: alperozturk96 --- .../client/files/FileIndicatorManager.kt | 6 +-- .../datamodel/FileDataStorageManager.java | 37 +------------------ .../ui/activity/FileDisplayActivity.kt | 29 +++++++++++---- 3 files changed, 24 insertions(+), 48 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/files/FileIndicatorManager.kt b/app/src/main/java/com/nextcloud/client/files/FileIndicatorManager.kt index 7ac21bc5c14b..ea43b7b25a31 100644 --- a/app/src/main/java/com/nextcloud/client/files/FileIndicatorManager.kt +++ b/app/src/main/java/com/nextcloud/client/files/FileIndicatorManager.kt @@ -25,11 +25,7 @@ object FileIndicatorManager { fun update(fileId: Long, status: FileIndicator) { _activeTransfers.update { current -> - if (status is FileIndicator.Idle) { - current - fileId - } else { - current + (fileId to status) - } + current + (fileId to status) } } } diff --git a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java index ad85d60523e5..e187b65d878f 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java @@ -912,8 +912,6 @@ public boolean removeFile(OCFile ocFile, boolean removeDBData, boolean removeLoc if (success && !removeDBData) { // maybe unnecessary, but should be checked TODO remove if unnecessary ocFile.setStoragePath(null); - fileDao.updateFileIndicator(ocFile.getFileId(), null); - ocFile.setFileIndicator(null); saveFile(ocFile); saveConflict(ocFile, null); } @@ -923,37 +921,7 @@ public boolean removeFile(OCFile ocFile, boolean removeDBData, boolean removeLoc return false; } - boolean result = success; - if (success) { - fileDao.updateFileIndicator(ocFile.getFileId(),null); - } - - return result; - } - - private void clearFileIndicatorsForFolderAndChildren(OCFile folder) { - if (folder == null || !folder.isFolder()) { - return; - } - - try { - if (folder.getFileId() > 0) { - fileDao.updateFileIndicator(folder.getFileId(), null); - } - - List children = getFolderContent(folder.getFileId(), false); - for (OCFile child : children) { - if (child.isFolder()) { - clearFileIndicatorsForFolderAndChildren(child); - } else { - if (child.getFileId() > 0) { - fileDao.updateFileIndicator(child.getFileId(), null); - } - } - } - } catch (Exception e) { - Log_OC.e(TAG, "Error clearing file indicators for folder: " + folder.getRemotePath(), e); - } + return success; } public boolean removeFolder(OCFile folder, boolean removeDBData, boolean removeLocalContent) { @@ -963,7 +931,6 @@ public boolean removeFolder(OCFile folder, boolean removeDBData, boolean removeL success = removeFolderInDb(folder); } if (removeLocalContent && success) { - clearFileIndicatorsForFolderAndChildren(folder); success = removeLocalFolder(folder); } } else { @@ -1010,8 +977,6 @@ private boolean removeLocalFolder(OCFile folder) { // notify MediaScanner about removed file deleteFileInMediaScan(ocFile.getStoragePath()); ocFile.setStoragePath(null); - fileDao.updateFileIndicator(ocFile.getFileId(), null); - ocFile.setFileIndicator(null); saveFile(ocFile); } } 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 61ccb3f6f699..37df1a180061 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 @@ -162,7 +162,6 @@ import com.owncloud.android.utils.StringUtils import com.owncloud.android.utils.theme.CapabilityUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -2122,7 +2121,6 @@ class FileDisplayActivity : } val parentFile = storageManager.getFileById(removedFile.parentId) if (parentFile != null && parentFile == getCurrentDir()) { - removeFileIndicator(parentFile) updateListOfFilesFragment() } else if (leftFragment is OCFileListFragment && SearchRemoteOperation.SearchType.FAVORITE_SEARCH == leftFragment.searchEvent?.searchType @@ -2138,9 +2136,7 @@ class FileDisplayActivity : } supportInvalidateOptionsMenu() fetchRecommendedFilesIfNeeded(ignoreETag = true, currentDir) - removeFileIndicator(removedFile) - listOfFilesFragment?.adapter?.removeFileIndicator(removedFile) } else { if (result.isSslRecoverableException) { mLastSslUntrustedServerResult = result @@ -2151,7 +2147,26 @@ class FileDisplayActivity : private fun removeFileIndicator(file: OCFile) { FileIndicatorManager.update(file.fileId, FileIndicator.Idle) - storageManager.fileDao.updateFileIndicator(file.fileId, null) + + lifecycleScope.launch(Dispatchers.IO) { + if (user.isEmpty) { + return@launch + } + + if (file.isFolder) { + // clearing first depth child files + storageManager.fileDao.getSubfiles(file.fileId, user.get().accountName).forEach { + it.id?.let { id -> + FileIndicatorManager.update(id, FileIndicator.Idle) + } + } + } else { + val parent = storageManager.getFileById(file.parentId) + parent?.fileId?.let { parentId -> + FileIndicatorManager.update(parentId, FileIndicator.Idle) + } + } + } } private fun onRestoreFileVersionOperationFinish(result: RemoteOperationResult<*>) { @@ -3082,8 +3097,8 @@ class FileDisplayActivity : repeatOnLifecycle(Lifecycle.State.STARTED) { FileIndicatorManager.activeTransfers .debounce(100) - .collectLatest { indicators -> - if (indicators.isEmpty()) return@collectLatest + .collect { indicators -> + Log_OC.d(TAG, "observing file indicators") withContext(Dispatchers.Main) { // update UI with hot data From fa6947ff96137a0b810b5a775ff7f55ecb9d4d86 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 13 Feb 2026 16:06:08 +0100 Subject: [PATCH 06/17] fix spotbugs Signed-off-by: alperozturk96 --- .../android/ui/adapter/OCFileListAdapter.java | 35 +++++++------------ 1 file changed, 12 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java index 760c2df95d10..18808b75e929 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java @@ -76,14 +76,12 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Date; -import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.UUID; -import java.util.function.BiConsumer; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -1067,33 +1065,24 @@ public void removeAllFiles() { notifyDataSetChanged(); } - public void removeFileIndicator(OCFile file) { - if (file != null) { - file.setFileIndicator(null); - notifyItemChanged(file); - } - } - public void updateFileIndicators(Map indicators) { if (indicators == null || indicators.isEmpty()) { return; } - Map positionMap = new HashMap<>(mFiles.size()); - for (int i = 0; i < mFiles.size(); i++) { - positionMap.put(mFiles.get(i).getFileId(), i); - } + for (OCFile file: mFiles) { + final var fileIndicator = indicators.get(file.getFileId()); + if (fileIndicator == null) { + continue; + } - indicators.forEach((id, fileIndicator) -> { - Integer position = positionMap.get(id); - if (position != null) { - OCFile file = mFiles.get(position); - Integer newIndicator = fileIndicator.getIconRes(); - if (!Objects.equals(file.getFileIndicator(), newIndicator)) { - file.setFileIndicator(newIndicator); - notifyItemChanged(file); - } + final var newIndicator = fileIndicator.getIconRes(); + if (Objects.equals(file.getFileIndicator(), newIndicator)) { + continue; } - }); + + file.setFileIndicator(newIndicator); + notifyItemChanged(file); + } } } From a6a46d4ef6ead5cfeffd1c5b45e0eebbc57fdf61 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 13 Feb 2026 16:40:58 +0100 Subject: [PATCH 07/17] fix uploaded file's indicator Signed-off-by: alperozturk96 --- .../android/ui/activity/FileDisplayActivity.kt | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) 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 37df1a180061..3a9a52249275 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 @@ -1697,6 +1697,12 @@ class FileDisplayActivity : sameFile = file?.remotePath == uploadedRemotePath || renamedInUpload } + if (uploadWasFine) { + file?.let { + removeFileIndicator(it, includeSubFiles = false) + } + } + if (sameAccount && sameFile && this@FileDisplayActivity.leftFragment is FileDetailFragment) { val fileDetailFragment = leftFragment as FileDetailFragment if (uploadWasFine) { @@ -2145,9 +2151,13 @@ class FileDisplayActivity : } } - private fun removeFileIndicator(file: OCFile) { + private fun removeFileIndicator(file: OCFile, includeSubFiles: Boolean = true) { FileIndicatorManager.update(file.fileId, FileIndicator.Idle) + if (!includeSubFiles) { + return + } + lifecycleScope.launch(Dispatchers.IO) { if (user.isEmpty) { return@launch From 6fb9cec8fb888a49a16888af3d3bde5a4b0011d5 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 13 Feb 2026 16:41:57 +0100 Subject: [PATCH 08/17] fix uploaded file's indicator Signed-off-by: alperozturk96 --- .../com/owncloud/android/ui/activity/FileDisplayActivity.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 3a9a52249275..1896060d2a83 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 @@ -1699,7 +1699,7 @@ class FileDisplayActivity : if (uploadWasFine) { file?.let { - removeFileIndicator(it, includeSubFiles = false) + setIdleFileIndicator(it, includeSubFiles = false) } } @@ -2142,7 +2142,7 @@ class FileDisplayActivity : } supportInvalidateOptionsMenu() fetchRecommendedFilesIfNeeded(ignoreETag = true, currentDir) - removeFileIndicator(removedFile) + setIdleFileIndicator(removedFile) } else { if (result.isSslRecoverableException) { mLastSslUntrustedServerResult = result @@ -2151,7 +2151,7 @@ class FileDisplayActivity : } } - private fun removeFileIndicator(file: OCFile, includeSubFiles: Boolean = true) { + private fun setIdleFileIndicator(file: OCFile, includeSubFiles: Boolean = true) { FileIndicatorManager.update(file.fileId, FileIndicator.Idle) if (!includeSubFiles) { From 546b1b9d610c1b6c5d876e4051b3c6100c6d51a5 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 13 Feb 2026 16:46:18 +0100 Subject: [PATCH 09/17] fix uploaded file's indicator Signed-off-by: alperozturk96 --- .../com/owncloud/android/ui/activity/FileDisplayActivity.kt | 2 ++ 1 file changed, 2 insertions(+) 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 1896060d2a83..b24be0a0114b 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 @@ -2154,10 +2154,12 @@ class FileDisplayActivity : private fun setIdleFileIndicator(file: OCFile, includeSubFiles: Boolean = true) { FileIndicatorManager.update(file.fileId, FileIndicator.Idle) + // while uploading files don't include so that downloaded icon can be removed for directory if (!includeSubFiles) { return } + // while removing files include sub files since it's needed lifecycleScope.launch(Dispatchers.IO) { if (user.isEmpty) { return@launch From a8362351ab5b55302976981957a69ee1b6086c04 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 20 Feb 2026 11:48:48 +0100 Subject: [PATCH 10/17] use enum instead integer Signed-off-by: alperozturk96 --- .../99.json | 8 ++++---- .../nextcloud/client/database/dao/FileDao.kt | 4 ++-- .../client/database/entity/FileEntity.kt | 2 +- .../client/files/FileIndicatorManager.kt | 16 +++++++++++----- .../datamodel/FileDataStorageManager.java | 9 ++++----- .../com/owncloud/android/datamodel/OCFile.java | 17 +++++++++-------- .../android/ui/activity/FileDisplayActivity.kt | 2 +- .../android/ui/adapter/OCFileListAdapter.java | 5 ++--- .../android/ui/adapter/OCFileListDelegate.kt | 4 ++-- 9 files changed, 36 insertions(+), 31 deletions(-) diff --git a/app/schemas/com.nextcloud.client.database.NextcloudDatabase/99.json b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/99.json index df3dcf664f9a..7da654e6f2ee 100644 --- a/app/schemas/com.nextcloud.client.database.NextcloudDatabase/99.json +++ b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/99.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 99, - "identityHash": "34850512e217b570d0ab1616ee91b87f", + "identityHash": "0f574aa10ac45b16ad92704fe0bffd86", "entities": [ { "tableName": "arbitrary_data", @@ -432,7 +432,7 @@ }, { "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, `file_indicator` INTEGER)", + "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, `file_indicator` TEXT)", "fields": [ { "fieldPath": "id", @@ -699,7 +699,7 @@ { "fieldPath": "fileIndicator", "columnName": "file_indicator", - "affinity": "INTEGER" + "affinity": "TEXT" } ], "primaryKey": { @@ -1302,7 +1302,7 @@ ], "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, '34850512e217b570d0ab1616ee91b87f')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0f574aa10ac45b16ad92704fe0bffd86')" ] } } \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt b/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt index b7f50f793d19..4675d8e1f019 100644 --- a/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt +++ b/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt @@ -149,7 +149,7 @@ interface FileDao { fun getAllRemoteIds(accountName: String): List @Transaction - fun updateFileIndicatorsBatch(updates: List>) { + fun updateFileIndicatorsBatch(updates: List>) { updates.forEach { (fileId, indicator) -> updateFileIndicator(fileId, indicator) } @@ -162,5 +162,5 @@ interface FileDao { WHERE ${ProviderTableMeta._ID} = :fileId """ ) - fun updateFileIndicator(fileId: Long, indicator: Int?) + fun updateFileIndicator(fileId: Long, indicator: String?) } diff --git a/app/src/main/java/com/nextcloud/client/database/entity/FileEntity.kt b/app/src/main/java/com/nextcloud/client/database/entity/FileEntity.kt index 3d54b912adbc..2c1a7cdbec06 100644 --- a/app/src/main/java/com/nextcloud/client/database/entity/FileEntity.kt +++ b/app/src/main/java/com/nextcloud/client/database/entity/FileEntity.kt @@ -123,5 +123,5 @@ data class FileEntity( @ColumnInfo(name = ProviderTableMeta.FILE_UPLOADED) val uploaded: Long?, @ColumnInfo(name = ProviderTableMeta.FILE_INDICATOR) - val fileIndicator: Int? + val fileIndicator: String? ) diff --git a/app/src/main/java/com/nextcloud/client/files/FileIndicatorManager.kt b/app/src/main/java/com/nextcloud/client/files/FileIndicatorManager.kt index ea43b7b25a31..b70e78ce5cd6 100644 --- a/app/src/main/java/com/nextcloud/client/files/FileIndicatorManager.kt +++ b/app/src/main/java/com/nextcloud/client/files/FileIndicatorManager.kt @@ -12,11 +12,17 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update -sealed class FileIndicator(val iconRes: Int?) { - data object Idle : FileIndicator(null) - data object Downloading : FileIndicator(R.drawable.ic_synchronizing) - data object Error : FileIndicator(R.drawable.ic_synchronizing_error) - data object Downloaded : FileIndicator(R.drawable.ic_synced) +enum class FileIndicator { + Idle, Downloading, Error, Downloaded; + + fun getIconId(): Int? { + return when(this) { + Idle -> null + Downloading -> R.drawable.ic_synchronizing + Error -> R.drawable.ic_synchronizing_error + Downloaded -> R.drawable.ic_synced + } + } } object FileIndicatorManager { diff --git a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java index e187b65d878f..849c9212c151 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java @@ -41,6 +41,7 @@ import com.nextcloud.client.database.dao.ShareDao; import com.nextcloud.client.database.entity.FileEntity; import com.nextcloud.client.database.entity.OfflineOperationEntity; +import com.nextcloud.client.files.FileIndicator; import com.nextcloud.client.jobs.offlineOperations.repository.OfflineOperationsRepository; import com.nextcloud.client.jobs.offlineOperations.repository.OfflineOperationsRepositoryType; import com.nextcloud.model.OfflineOperationRawType; @@ -786,10 +787,8 @@ public void saveFolder(OCFile folder, List updatedFiles, Collection, ServerFileInterfa private String reason = ""; // endregion - private Integer fileIndicator = null; + private String fileIndicator = FileIndicator.Idle.name(); /** * URI to the local path of the file contents, if stored in the device; cached after first call to @@ -214,7 +215,7 @@ private OCFile(Parcel source) { lockTimeout = source.readLong(); lockToken = source.readString(); livePhoto = source.readString(); - fileIndicator = source.readInt(); + fileIndicator = source.readString(); } @Override @@ -261,7 +262,7 @@ public void writeToParcel(Parcel dest, int flags) { dest.writeLong(lockTimeout); dest.writeString(lockToken); dest.writeString(livePhoto); - dest.writeInt(fileIndicator != null ? fileIndicator : -1); + dest.writeString(fileIndicator); } public String getLinkedFileIdForLivePhoto() { @@ -534,7 +535,7 @@ private void resetData() { lockToken = null; livePhoto = null; imageDimension = null; - fileIndicator = null; + fileIndicator = FileIndicator.Idle.name(); } /** @@ -1181,11 +1182,11 @@ public boolean hasValidParentId() { } } - public void setFileIndicator(Integer indicator) { - fileIndicator = indicator; + public void setFileIndicator(FileIndicator indicator) { + fileIndicator = indicator.name(); } - public Integer getFileIndicator() { - return fileIndicator; + public FileIndicator getFileIndicator() { + return FileIndicator.valueOf(fileIndicator); } } 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 b24be0a0114b..9298b28d6c37 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 @@ -3120,7 +3120,7 @@ class FileDisplayActivity : launch(Dispatchers.IO) { storageManager.fileDao.updateFileIndicatorsBatch( indicators.map { (fileId, indicator) -> - fileId to indicator.iconRes + fileId to indicator.name } ) } diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java index 18808b75e929..0c138f4a8d45 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java @@ -1076,12 +1076,11 @@ public void updateFileIndicators(Map indicators) { continue; } - final var newIndicator = fileIndicator.getIconRes(); - if (Objects.equals(file.getFileIndicator(), newIndicator)) { + if (Objects.equals(file.getFileIndicator(), fileIndicator)) { continue; } - file.setFileIndicator(newIndicator); + file.setFileIndicator(fileIndicator); notifyItemChanged(file); } } diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt index c0a0e184aeb5..b0d63dd1e92e 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt @@ -323,9 +323,9 @@ class OCFileListDelegate( private fun showFileIndicator(file: OCFile, holder: ListViewHolder) { holder.localFileIndicator.run { - var indicator = file.fileIndicator + var indicator = file.fileIndicator.getIconId() if (file.etagInConflict != null) { - indicator = FileIndicator.Error.iconRes + indicator = FileIndicator.Error.getIconId() } if (indicator != null && indicator != 0) { From 677588cd7c54cf54a295c769bc2eb7ee93d9202f Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 20 Feb 2026 13:35:42 +0100 Subject: [PATCH 11/17] indicate from uploads Signed-off-by: alperozturk96 --- .../client/files/FileIndicatorManager.kt | 6 ++--- .../jobs/autoUpload/AutoUploadWorker.kt | 12 ++++++++++ .../jobs/download/FileDownloadWorker.kt | 4 ++-- .../folderDownload/FolderDownloadWorker.kt | 8 +++---- .../client/jobs/upload/FileUploadWorker.kt | 24 ++++++++++++++++--- 5 files changed, 42 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/files/FileIndicatorManager.kt b/app/src/main/java/com/nextcloud/client/files/FileIndicatorManager.kt index b70e78ce5cd6..3e48292572ca 100644 --- a/app/src/main/java/com/nextcloud/client/files/FileIndicatorManager.kt +++ b/app/src/main/java/com/nextcloud/client/files/FileIndicatorManager.kt @@ -13,14 +13,14 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update enum class FileIndicator { - Idle, Downloading, Error, Downloaded; + Idle, Syncing, Error, Synced; fun getIconId(): Int? { return when(this) { Idle -> null - Downloading -> R.drawable.ic_synchronizing + Syncing -> R.drawable.ic_synchronizing Error -> R.drawable.ic_synchronizing_error - Downloaded -> R.drawable.ic_synced + Synced -> R.drawable.ic_synced } } } diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadWorker.kt index 639a66ed8c75..ee1b6a9e2018 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 @@ -18,6 +18,8 @@ import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.database.entity.toOCUpload import com.nextcloud.client.database.entity.toUploadEntity import com.nextcloud.client.device.PowerManagementService +import com.nextcloud.client.files.FileIndicator +import com.nextcloud.client.files.FileIndicatorManager import com.nextcloud.client.jobs.BackgroundJobManager import com.nextcloud.client.jobs.upload.FileUploadBroadcastManager import com.nextcloud.client.jobs.upload.FileUploadWorker @@ -273,6 +275,7 @@ class AutoUploadWorker( val ocAccount = OwnCloudAccount(user.toPlatformAccount(), context) val client = OwnCloudClientManagerFactory.getDefaultSingleton() .getClientFor(ocAccount, context) + val storageManager = FileDataStorageManager(user, context.contentResolver) trySetForeground() updateNotification() @@ -322,6 +325,11 @@ class AutoUploadWorker( val result = operation.execute(client) fileUploadBroadcastManager.sendStarted(operation, context) + val parentFile = + storageManager.getFileByDecryptedRemotePath(operation.file?.parentRemotePath) + parentFile?.let { + FileIndicatorManager.update(it.fileId, FileIndicator.Syncing) + } UploadErrorNotificationManager.handleResult( context, @@ -346,6 +354,10 @@ class AutoUploadWorker( } } + parentFile?.let { + FileIndicatorManager.update(it.fileId, FileIndicator.Idle) + } + val isLastInBatch = (batchIndex == filePathsWithIds.size - 1) if (isLastInBatch) { sendUploadFinishEvent(operation, result) diff --git a/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadWorker.kt index 7d091ea9269a..fb962312afed 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadWorker.kt @@ -213,7 +213,7 @@ class FileDownloadWorker( file.remotePath, operation ) ?: Pair(null, null) - FileIndicatorManager.update(file.fileId, FileIndicator.Downloading) + FileIndicatorManager.update(file.fileId, FileIndicator.Syncing) downloadKey?.let { requestedDownloads.add(downloadKey) @@ -357,7 +357,7 @@ class FileDownloadWorker( private fun checkDownloadError(result: RemoteOperationResult<*>) { if (result.isSuccess || downloadError != null) { - currentDownload?.file?.fileId?.let { FileIndicatorManager.update(it, FileIndicator.Downloaded) } + currentDownload?.file?.fileId?.let { FileIndicatorManager.update(it, FileIndicator.Synced) } notificationManager.dismissNotification() return } diff --git a/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorker.kt index 49e2ca3fddfa..07aa5397f13e 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorker.kt @@ -77,7 +77,7 @@ class FolderDownloadWorker( return Result.failure() } - FileIndicatorManager.update(folder.fileId, FileIndicator.Downloading) + FileIndicatorManager.update(folder.fileId, FileIndicator.Syncing) Log_OC.d(TAG, "🕒 started for ${user.accountName} downloading ${folder.fileName}") trySetForeground(folder) @@ -111,11 +111,11 @@ class FolderDownloadWorker( setForeground(foregroundInfo) } - FileIndicatorManager.update(file.fileId, FileIndicator.Downloading) + FileIndicatorManager.update(file.fileId, FileIndicator.Syncing) val operation = DownloadFileOperation(user, file, context) val operationResult = operation.execute(client) if (operationResult?.isSuccess == true && operation.downloadType === DownloadType.DOWNLOAD) { - FileIndicatorManager.update(file.fileId, FileIndicator.Downloaded) + FileIndicatorManager.update(file.fileId, FileIndicator.Synced) getOCFile(operation)?.let { ocFile -> downloadHelper.saveFile(ocFile, operation, storageManager) } @@ -132,7 +132,7 @@ class FolderDownloadWorker( if (result) { Log_OC.d(TAG, "✅ completed") - FileIndicatorManager.update(folderID, FileIndicator.Downloaded) + FileIndicatorManager.update(folderID, FileIndicator.Synced) Result.success() } else { Log_OC.d(TAG, "❌ failed") diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt index e64ca25ca296..d842464aa767 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt @@ -17,6 +17,8 @@ import androidx.work.WorkerParameters import com.nextcloud.client.account.User import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.device.PowerManagementService +import com.nextcloud.client.files.FileIndicator +import com.nextcloud.client.files.FileIndicatorManager import com.nextcloud.client.jobs.BackgroundJobManager import com.nextcloud.client.jobs.BackgroundJobManagerImpl import com.nextcloud.client.jobs.utils.UploadErrorNotificationManager @@ -225,6 +227,7 @@ class FileUploadWorker( val uploads = uploadsStorageManager.getUploadsByIds(uploadIds, accountName) val ocAccount = OwnCloudAccount(user.toPlatformAccount(), context) val client = OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(ocAccount, context) + val storageManager = FileDataStorageManager(user, context.contentResolver) for ((index, upload) in uploads.withIndex()) { ensureActive() @@ -243,8 +246,14 @@ class FileUploadWorker( } fileUploadBroadcastManager.sendAdded(context) - val operation = createUploadFileOperation(upload, user) + val operation = createUploadFileOperation(upload, user, storageManager) currentUploadFileOperation = operation + val parentFile = + storageManager.getFileByDecryptedRemotePath(currentUploadFileOperation?.file?.parentRemotePath) + + parentFile?.let { + FileIndicatorManager.update(it.fileId, FileIndicator.Syncing) + } val currentIndex = (index + 1) val currentUploadIndex = (currentIndex + previouslyUploadedFileSize) @@ -259,6 +268,11 @@ class FileUploadWorker( upload(upload, operation, user, client) } currentUploadFileOperation = null + if (result.isSuccess) { + parentFile?.let { + FileIndicatorManager.update(it.fileId, FileIndicator.Idle) + } + } if (result.code == ResultCode.QUOTA_EXCEEDED) { Log_OC.w(TAG, "Quota exceeded, stopping uploads") @@ -308,7 +322,11 @@ class FileUploadWorker( return result } - private fun createUploadFileOperation(upload: OCUpload, user: User): UploadFileOperation = UploadFileOperation( + private fun createUploadFileOperation( + upload: OCUpload, + user: User, + storageManager: FileDataStorageManager + ): UploadFileOperation = UploadFileOperation( uploadsStorageManager, connectivityService, powerManagementService, @@ -321,7 +339,7 @@ class FileUploadWorker( upload.isUseWifiOnly, upload.isWhileChargingOnly, true, - FileDataStorageManager(user, context.contentResolver) + storageManager ).apply { addDataTransferProgressListener(this@FileUploadWorker) } From a5f9d515deae74753cbee8a0de811ee7a07e84d7 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 20 Feb 2026 14:56:47 +0100 Subject: [PATCH 12/17] indicate from uploads Signed-off-by: alperozturk96 --- .../client/jobs/autoUpload/AutoUploadWorker.kt | 12 ++++++++++++ .../nextcloud/client/jobs/upload/FileUploadWorker.kt | 8 +++----- 2 files changed, 15 insertions(+), 5 deletions(-) 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 ee1b6a9e2018..73737377adb3 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 @@ -95,10 +95,18 @@ class AutoUploadWorker( return Result.retry() } + val storageManager = FileDataStorageManager(userAccountManager.user, context.contentResolver) + val parentDir = + storageManager.getFileByDecryptedRemotePath(syncedFolder.remotePath) + if (powerManagementService.isPowerSavingEnabled) { Log_OC.w(TAG, "power saving mode enabled") } + parentDir?.let { + FileIndicatorManager.update(it.fileId, FileIndicator.Syncing) + } + collectFileChangesFromContentObserverWork(contentUris) uploadFiles(syncedFolder) @@ -106,6 +114,10 @@ class AutoUploadWorker( syncedFolder.lastScanTimestampMs = System.currentTimeMillis() syncedFolderProvider.updateSyncFolder(syncedFolder) + parentDir?.let { + FileIndicatorManager.update(it.fileId, FileIndicator.Idle) + } + Log_OC.d(TAG, "✅ ${syncedFolder.remotePath} completed") Result.success() } catch (e: Exception) { diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt index d842464aa767..b86b603ca2e7 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt @@ -249,7 +249,7 @@ class FileUploadWorker( val operation = createUploadFileOperation(upload, user, storageManager) currentUploadFileOperation = operation val parentFile = - storageManager.getFileByDecryptedRemotePath(currentUploadFileOperation?.file?.parentRemotePath) + storageManager.getFileByDecryptedRemotePath(operation.file?.parentRemotePath) parentFile?.let { FileIndicatorManager.update(it.fileId, FileIndicator.Syncing) @@ -268,10 +268,8 @@ class FileUploadWorker( upload(upload, operation, user, client) } currentUploadFileOperation = null - if (result.isSuccess) { - parentFile?.let { - FileIndicatorManager.update(it.fileId, FileIndicator.Idle) - } + parentFile?.let { + FileIndicatorManager.update(it.fileId, FileIndicator.Idle) } if (result.code == ResultCode.QUOTA_EXCEEDED) { From f23c9698a2a341bcb1d4a45d3997a20cefe65c36 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 20 Feb 2026 15:17:25 +0100 Subject: [PATCH 13/17] check upload behaviour before reset icon Signed-off-by: alperozturk96 --- .../client/jobs/upload/FileUploadBroadcastManager.kt | 1 + .../com/nextcloud/client/jobs/upload/FileUploadWorker.kt | 4 +++- .../com/owncloud/android/ui/activity/FileDisplayActivity.kt | 6 ++++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadBroadcastManager.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadBroadcastManager.kt index 148e869ee8fc..96b8f998f712 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadBroadcastManager.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadBroadcastManager.kt @@ -102,6 +102,7 @@ class FileUploadBroadcastManager(private val broadcastManager: LocalBroadcastMan putExtra(FileUploadWorker.EXTRA_OLD_FILE_PATH, upload.originalStoragePath) putExtra(FileUploadWorker.ACCOUNT_NAME, upload.user.accountName) putExtra(FileUploadWorker.EXTRA_UPLOAD_RESULT, uploadResult.isSuccess) + putExtra(FileUploadWorker.EXTRA_BEHAVIOUR, upload.localBehaviour) if (unlinkedFromRemotePath != null) { putExtra(FileUploadWorker.EXTRA_LINKED_TO_PATH, unlinkedFromRemotePath) } diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt index b86b603ca2e7..2a423b13384f 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt @@ -84,8 +84,10 @@ class FileUploadWorker( const val EXTRA_OLD_REMOTE_PATH = "OLD_REMOTE_PATH" const val EXTRA_OLD_FILE_PATH = "OLD_FILE_PATH" const val EXTRA_LINKED_TO_PATH = "LINKED_TO" - const val ACCOUNT_NAME = "ACCOUNT_NAME" + const val EXTRA_BEHAVIOUR = "BEHAVIOUR" const val EXTRA_ACCOUNT_NAME = "ACCOUNT_NAME" + + const val ACCOUNT_NAME = "ACCOUNT_NAME" const val ACTION_CANCEL_BROADCAST = "CANCEL" const val LOCAL_BEHAVIOUR_COPY = 0 const val LOCAL_BEHAVIOUR_MOVE = 1 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 9298b28d6c37..134ab55e5876 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 @@ -1086,7 +1086,7 @@ class FileDisplayActivity : return@isNetworkAndServerAvailable } - FileUploadHelper.Companion.instance().uploadNewFiles( + FileUploadHelper.instance().uploadNewFiles( user.orElseThrow( Supplier { RuntimeException() } ), @@ -1673,6 +1673,7 @@ class FileDisplayActivity : Log_OC.d(tag, "upload finish received broadcast") val uploadedRemotePath = intent.getStringExtra(FileUploadWorker.EXTRA_REMOTE_PATH) + val behaviour = intent.getIntExtra(FileUploadWorker.EXTRA_BEHAVIOUR, -1) val accountName = intent.getStringExtra(FileUploadWorker.ACCOUNT_NAME) val account = getAccount() val sameAccount = accountName != null && account != null && accountName == account.name @@ -1697,7 +1698,8 @@ class FileDisplayActivity : sameFile = file?.remotePath == uploadedRemotePath || renamedInUpload } - if (uploadWasFine) { + // only synced icon can be remove if local behaviour is not move + if (uploadWasFine && behaviour != FileUploadWorker.LOCAL_BEHAVIOUR_MOVE) { file?.let { setIdleFileIndicator(it, includeSubFiles = false) } From b1af5f2aa589766f2a5ea77b6b0975919224026a Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 20 Feb 2026 15:18:14 +0100 Subject: [PATCH 14/17] fix codacy Signed-off-by: alperozturk96 --- .../client/files/FileIndicatorManager.kt | 17 +++++++++-------- .../android/ui/activity/FileDisplayActivity.kt | 1 + 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/files/FileIndicatorManager.kt b/app/src/main/java/com/nextcloud/client/files/FileIndicatorManager.kt index 3e48292572ca..6496dbe9a20e 100644 --- a/app/src/main/java/com/nextcloud/client/files/FileIndicatorManager.kt +++ b/app/src/main/java/com/nextcloud/client/files/FileIndicatorManager.kt @@ -13,15 +13,16 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update enum class FileIndicator { - Idle, Syncing, Error, Synced; + Idle, + Syncing, + Error, + Synced; - fun getIconId(): Int? { - return when(this) { - Idle -> null - Syncing -> R.drawable.ic_synchronizing - Error -> R.drawable.ic_synchronizing_error - Synced -> R.drawable.ic_synced - } + fun getIconId(): Int? = when (this) { + Idle -> null + Syncing -> R.drawable.ic_synchronizing + Error -> R.drawable.ic_synchronizing_error + Synced -> R.drawable.ic_synced } } 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 134ab55e5876..6a5929d78043 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 @@ -1669,6 +1669,7 @@ class FileDisplayActivity : private inner class UploadFinishReceiver : BroadcastReceiver() { private val tag = "UploadFinishReceiver" + @Suppress("LongMethod") override fun onReceive(context: Context?, intent: Intent) { Log_OC.d(tag, "upload finish received broadcast") From 122fc4dc7740e16f89b6899402d66f161d7c3af3 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 20 Feb 2026 15:30:06 +0100 Subject: [PATCH 15/17] indicate two-way-sync Signed-off-by: alperozturk96 --- .../java/com/nextcloud/client/jobs/InternalTwoWaySyncWork.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/java/com/nextcloud/client/jobs/InternalTwoWaySyncWork.kt b/app/src/main/java/com/nextcloud/client/jobs/InternalTwoWaySyncWork.kt index 1d1c89fd9d3f..e60bfe0f7076 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/InternalTwoWaySyncWork.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/InternalTwoWaySyncWork.kt @@ -12,6 +12,8 @@ import androidx.work.Worker import androidx.work.WorkerParameters import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.device.PowerManagementService +import com.nextcloud.client.files.FileIndicator +import com.nextcloud.client.files.FileIndicatorManager import com.nextcloud.client.network.ConnectivityService import com.nextcloud.client.preferences.AppPreferences import com.owncloud.android.MainApp @@ -67,6 +69,7 @@ class InternalTwoWaySyncWork( } Log_OC.d(TAG, "Folder ${folder.remotePath}: started!") + FileIndicatorManager.update(folder.fileId, FileIndicator.Syncing) operation = SynchronizeFolderOperation(context, folder.remotePath, user, fileDataStorageManager, true) val operationResult = operation?.execute(context) @@ -86,6 +89,7 @@ class InternalTwoWaySyncWork( } fileDataStorageManager.saveFile(folder) + FileIndicatorManager.update(folder.fileId, FileIndicator.Idle) } } From 798122f3ba1d737757b7e15850270df3ba314d87 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 20 Feb 2026 15:48:23 +0100 Subject: [PATCH 16/17] add indicator to sync file operation Signed-off-by: alperozturk96 --- .../android/operations/SynchronizeFileOperation.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/src/main/java/com/owncloud/android/operations/SynchronizeFileOperation.java b/app/src/main/java/com/owncloud/android/operations/SynchronizeFileOperation.java index 4aba8f2d5f63..e3e1411e0c79 100644 --- a/app/src/main/java/com/owncloud/android/operations/SynchronizeFileOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/SynchronizeFileOperation.java @@ -17,6 +17,8 @@ import android.text.TextUtils; import com.nextcloud.client.account.User; +import com.nextcloud.client.files.FileIndicator; +import com.nextcloud.client.files.FileIndicatorManager; import com.nextcloud.client.jobs.download.FileDownloadHelper; import com.nextcloud.client.jobs.upload.FileUploadHelper; import com.nextcloud.client.jobs.upload.FileUploadWorker; @@ -190,6 +192,8 @@ protected RemoteOperationResult run(OwnCloudClient client) { mLocalFile = getStorageManager().getFileByPath(mRemotePath); } + FileIndicatorManager.INSTANCE.update(mLocalFile.getFileId(), FileIndicator.Syncing); + if (!mLocalFile.isDown()) { /// easy decision requestForDownload(mLocalFile); @@ -291,6 +295,9 @@ protected RemoteOperationResult run(OwnCloudClient client) { if (postDialogEvent) { EventBus.getDefault().post(new DialogEvent(DialogEventType.SYNC)); } + + FileIndicatorManager.INSTANCE.update(mLocalFile.getFileId(), FileIndicator.Synced); + return result; } From c647bcf68d1f9cecfa196a3b56541301f8b476a3 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 20 Feb 2026 15:51:23 +0100 Subject: [PATCH 17/17] add indicator to sync folder operation Signed-off-by: alperozturk96 --- .../android/operations/SynchronizeFolderOperation.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/owncloud/android/operations/SynchronizeFolderOperation.java b/app/src/main/java/com/owncloud/android/operations/SynchronizeFolderOperation.java index ee77d865ea70..1f5456c6a679 100644 --- a/app/src/main/java/com/owncloud/android/operations/SynchronizeFolderOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/SynchronizeFolderOperation.java @@ -15,6 +15,8 @@ import android.text.TextUtils; import com.nextcloud.client.account.User; +import com.nextcloud.client.files.FileIndicator; +import com.nextcloud.client.files.FileIndicatorManager; import com.nextcloud.client.jobs.download.FileDownloadHelper; import com.owncloud.android.datamodel.FileDataStorageManager; import com.owncloud.android.datamodel.OCFile; @@ -132,6 +134,8 @@ protected RemoteOperationResult run(OwnCloudClient client) { return new RemoteOperationResult<>(ResultCode.FILE_NOT_FOUND); } + FileIndicatorManager.INSTANCE.update(mLocalFolder.getFileId(), FileIndicator.Syncing); + result = checkForChanges(client); if (result.isSuccess()) { @@ -143,15 +147,18 @@ protected RemoteOperationResult run(OwnCloudClient client) { if (result.isSuccess()) { syncContents(client); + FileIndicatorManager.INSTANCE.update(mLocalFolder.getFileId(), FileIndicator.Synced); } } if (mCancellationRequested.get()) { + FileIndicatorManager.INSTANCE.update(mLocalFolder.getFileId(), FileIndicator.Error); throw new OperationCancelledException(); } } catch (OperationCancelledException e) { - result = new RemoteOperationResult(e); + FileIndicatorManager.INSTANCE.update(mLocalFolder.getFileId(), FileIndicator.Error); + result = new RemoteOperationResult<>(e); } return result;