diff --git a/app/schemas/com.nextcloud.client.database.NextcloudDatabase/106.json b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/106.json new file mode 100644 index 000000000000..a4ebb6d3eade --- /dev/null +++ b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/106.json @@ -0,0 +1,1388 @@ +{ + "formatVersion": 1, + "database": { + "version": 106, + "identityHash": "7808373d76f390fd40da3113e3849d59", + "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, `governance` INTEGER, `forbidden_filename_characters` TEXT, `forbidden_filenames` TEXT, `forbidden_filename_extensions` TEXT, `forbidden_filename_basenames` TEXT, `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, `mod_rewrite_working` INTEGER, `chunked_upload_max_size` INTEGER, `sharing_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": "governance", + "columnName": "governance", + "affinity": "INTEGER" + }, + { + "fieldPath": "forbiddenFileNameCharacters", + "columnName": "forbidden_filename_characters", + "affinity": "TEXT" + }, + { + "fieldPath": "forbiddenFileNames", + "columnName": "forbidden_filenames", + "affinity": "TEXT" + }, + { + "fieldPath": "forbiddenFileNameExtensions", + "columnName": "forbidden_filename_extensions", + "affinity": "TEXT" + }, + { + "fieldPath": "forbiddenFilenameBaseNames", + "columnName": "forbidden_filename_basenames", + "affinity": "TEXT" + }, + { + "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" + }, + { + "fieldPath": "modRewriteWorking", + "columnName": "mod_rewrite_working", + "affinity": "INTEGER" + }, + { + "fieldPath": "chunkedUploadMaxSize", + "columnName": "chunked_upload_max_size", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingJson", + "columnName": "sharing_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, `is_read_only` 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": "isReadOnly", + "columnName": "is_read_only", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "filesystem", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `is_folder` INTEGER, `found_at` INTEGER, `upload_triggered` INTEGER, `syncedfolder_id` TEXT, `crc32` TEXT, `modified_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT" + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT" + }, + { + "fieldPath": "fileIsFolder", + "columnName": "is_folder", + "affinity": "INTEGER" + }, + { + "fieldPath": "fileFoundRecently", + "columnName": "found_at", + "affinity": "INTEGER" + }, + { + "fieldPath": "fileSentForUpload", + "columnName": "upload_triggered", + "affinity": "INTEGER" + }, + { + "fieldPath": "syncedFolderId", + "columnName": "syncedfolder_id", + "affinity": "TEXT" + }, + { + "fieldPath": "crc32", + "columnName": "crc32", + "affinity": "TEXT" + }, + { + "fieldPath": "fileModified", + "columnName": "modified_at", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [ + { + "name": "index_filesystem_local_path_syncedfolder_id", + "unique": false, + "columnNames": [ + "local_path", + "syncedfolder_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_filesystem_local_path_syncedfolder_id` ON `${TABLE_NAME}` (`local_path`, `syncedfolder_id`)" + }, + { + "name": "index_filesystem_syncedfolder_id_upload_triggered_is_folder", + "unique": false, + "columnNames": [ + "syncedfolder_id", + "upload_triggered", + "is_folder" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_filesystem_syncedfolder_id_upload_triggered_is_folder` ON `${TABLE_NAME}` (`syncedfolder_id`, `upload_triggered`, `is_folder`)" + } + ] + }, + { + "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" + ] + }, + "indices": [ + { + "name": "index_list_of_uploads_account_name_status", + "unique": false, + "columnNames": [ + "account_name", + "status" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_list_of_uploads_account_name_status` ON `${TABLE_NAME}` (`account_name`, `status`)" + }, + { + "name": "index_list_of_uploads_remote_path_account_name", + "unique": false, + "columnNames": [ + "remote_path", + "account_name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_list_of_uploads_remote_path_account_name` ON `${TABLE_NAME}` (`remote_path`, `account_name`)" + }, + { + "name": "index_list_of_uploads_account_name_local_path", + "unique": false, + "columnNames": [ + "account_name", + "local_path" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_list_of_uploads_account_name_local_path` ON `${TABLE_NAME}` (`account_name`, `local_path`)" + } + ] + }, + { + "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, '7808373d76f390fd40da3113e3849d59')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/nextcloud/test/FlakyTestFilter.kt b/app/src/androidTest/java/com/nextcloud/test/FlakyTestFilter.kt new file mode 100644 index 000000000000..4f5a0074b1ef --- /dev/null +++ b/app/src/androidTest/java/com/nextcloud/test/FlakyTestFilter.kt @@ -0,0 +1,25 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.nextcloud.test + +import org.junit.runner.Description +import org.junit.runner.manipulation.Filter + +class FlakyTestFilter : Filter() { + override fun shouldRun(description: Description): Boolean = when { + description.isTest -> !description.isFlaky() + else -> description.children.any { shouldRun(it) } + } + + override fun describe(): String = "skip tests annotated with @Flaky" + + private fun Description.isFlaky(): Boolean { + val onMethod = getAnnotation(Flaky::class.java) != null + val onClass = testClass?.isAnnotationPresent(Flaky::class.java) == true + return onMethod || onClass + } +} diff --git a/app/src/androidTest/java/com/owncloud/android/GrantFolderExistenceTests.kt b/app/src/androidTest/java/com/owncloud/android/GrantFolderExistenceTests.kt index 82d771add397..b36775aae2d5 100644 --- a/app/src/androidTest/java/com/owncloud/android/GrantFolderExistenceTests.kt +++ b/app/src/androidTest/java/com/owncloud/android/GrantFolderExistenceTests.kt @@ -20,6 +20,7 @@ import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue +import org.junit.Assert.fail import org.junit.Before import org.junit.Test import java.io.IOException @@ -50,7 +51,7 @@ class GrantFolderExistenceTests : AbstractOnServerIT() { assertTrue("month folder should exist on server", existsOnServer(monthFolder)) assertNotNull("month folder should be cached locally", storageManager.getFileByDecryptedRemotePath(monthFolder)) - removeYearFolderOnServerOnly() + removeOnServer(yearFolder) assertFalse("month folder should be deleted", existsOnServer(monthFolder)) assertNotNull( @@ -74,10 +75,7 @@ class GrantFolderExistenceTests : AbstractOnServerIT() { fun testUploadFileThenDeleteRootOnServerOnlyThenUploadAgainShouldRecreateAllFolderLevelsAndReturnOk() { uploadAndAssertSuccess("first.txt") - assertTrue( - "root folder should be removed", - RemoveFileRemoteOperation(root).execute(client).isSuccess - ) + removeOnServer(root) assertFalse(existsOnServer(root)) val result = upload("nonEmpty.txt", monthFolder + "nonEmpty.txt") @@ -98,11 +96,18 @@ class GrantFolderExistenceTests : AbstractOnServerIT() { assertTrue("uploaded file should exist on server", existsOnServer(monthFolder + filename)) } - private fun removeYearFolderOnServerOnly() { - assertTrue( - "year folder should be removed", - RemoveFileRemoteOperation(yearFolder).execute(client).isSuccess - ) + private fun removeOnServer(remotePath: String) { + // the server keeps a transient lock on a just uploaded file, so a DELETE on one of its + // parent folders answers 423 until that lock expires + repeat(REMOVE_ATTEMPTS) { + if (RemoveFileRemoteOperation(remotePath).execute(client).isSuccess) { + return + } + + shortSleep() + } + + fail("$remotePath should be removed on server") } private fun existsOnServer(remotePath: String): Boolean = @@ -139,5 +144,6 @@ class GrantFolderExistenceTests : AbstractOnServerIT() { companion object { private const val FILE_LINE_COUNT = 100 + private const val REMOVE_ATTEMPTS = 5 } } diff --git a/app/src/androidTest/java/com/owncloud/android/providers/DocumentsStorageProviderIT.kt b/app/src/androidTest/java/com/owncloud/android/providers/DocumentsStorageProviderIT.kt index 3bdd0e010152..0c5066e00bf6 100644 --- a/app/src/androidTest/java/com/owncloud/android/providers/DocumentsStorageProviderIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/providers/DocumentsStorageProviderIT.kt @@ -8,10 +8,15 @@ package com.owncloud.android.providers import android.provider.DocumentsContract import androidx.documentfile.provider.DocumentFile +import com.nextcloud.client.account.UserAccountManagerImpl +import com.nextcloud.client.jobs.upload.FileUploadHelper import com.nextcloud.test.RandomStringGenerator import com.owncloud.android.AbstractOnServerIT import com.owncloud.android.R import com.owncloud.android.datamodel.OCFile.ROOT_PATH +import com.owncloud.android.datamodel.UploadsStorageManager +import com.owncloud.android.datamodel.UploadsStorageManager.UploadStatus +import com.owncloud.android.db.OCUpload import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.providers.DocumentsProviderUtils.assertExistsOnServer import com.owncloud.android.providers.DocumentsProviderUtils.assertListFilesEquals @@ -31,12 +36,16 @@ import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue +import org.junit.Assert.fail import org.junit.Before import org.junit.Test import kotlin.random.Random private const val MAX_FILE_NAME_LENGTH = 225 +// the provider can only enqueue the upload worker, so give it a generous budget before giving up +private const val UPLOAD_POLL_ATTEMPTS = 45 + class DocumentsStorageProviderIT : AbstractOnServerIT() { private val context = targetContext @@ -48,8 +57,17 @@ class DocumentsStorageProviderIT : AbstractOnServerIT() { private val uri = DocumentsContract.buildTreeDocumentUri(authority, documentId) private val rootDir get() = DocumentFile.fromTreeUri(context, uri)!! + private val uploadsStorageManager = UploadsStorageManager( + UserAccountManagerImpl.fromContext(context), + contentResolver + ) + @Before fun before() { + // an upload left over by a previous test fails once its local file is gone, and WorkManager + // then drops everything appended behind it in the upload chain, including our own uploads + FileUploadHelper.instance().cancel(user.accountName) + // DocumentsProvider#onCreate() is called when the application is started // which is *after* AbstractOnServerIT adds the accounts (when the app is freshly installed). // So we need to query our roots here to ensure that the internal storage map is initialized. @@ -211,11 +229,7 @@ class DocumentsStorageProviderIT : AbstractOnServerIT() { it!!.write(content1) } - // refresh - while (file1.getOCFile(storageManager)!!.etagOnServer == createdETag) { - shortSleep() - rootDir.listFiles() - } + awaitUploadedToServer(file1, createdETag) val remotePath = file1.getOCFile(storageManager)!!.remotePath @@ -250,11 +264,7 @@ class DocumentsStorageProviderIT : AbstractOnServerIT() { it!!.write(content1) } - // refresh - while (file1.getOCFile(storageManager)!!.etagOnServer == createdETag) { - shortSleep() - rootDir.listFiles() - } + awaitUploadedToServer(file1, createdETag) val content2 = "new content".toByteArray() @@ -266,4 +276,31 @@ class DocumentsStorageProviderIT : AbstractOnServerIT() { val bytes = contentResolver.openInputStream(file1.uri)?.readBytes() ?: ByteArray(0) assertEquals(String(content2), String(bytes)) } + + private fun awaitUploadedToServer(file: DocumentFile, etagBeforeUpload: String) { + val remotePath = file.getOCFile(storageManager)!!.remotePath + + repeat(UPLOAD_POLL_ATTEMPTS) { + if (file.getOCFile(storageManager)!!.etagOnServer != etagBeforeUpload) { + return + } + + failedUpload(remotePath)?.let { + fail("upload of $remotePath failed with ${it.lastResult}") + } + + shortSleep() + rootDir.listFiles() + } + + fail("upload of $remotePath did not finish, stored uploads: ${describeUploads()}") + } + + private fun failedUpload(remotePath: String): OCUpload? = uploadsStorageManager + .getUploadsForAccount(user.accountName) + .firstOrNull { it.remotePath == remotePath && it.uploadStatus == UploadStatus.UPLOAD_FAILED } + + private fun describeUploads(): String = uploadsStorageManager + .getUploadsForAccount(user.accountName) + .joinToString { "${it.remotePath} is ${it.uploadStatus} with ${it.lastResult}" } } diff --git a/app/src/androidTest/java/com/owncloud/android/ui/fragment/UnifiedSearchFragmentIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/fragment/UnifiedSearchFragmentIT.kt index 017ab07c3086..b58d5162f9f9 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/fragment/UnifiedSearchFragmentIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/fragment/UnifiedSearchFragmentIT.kt @@ -67,6 +67,7 @@ class UnifiedSearchFragmentIT : AbstractIT() { scenario.onActivity { activity -> val sut = UnifiedSearchFragment.newInstance(null, null, "/") val testViewModel = UnifiedSearchViewModel(activity.application) + testViewModel.setCurrentAccountProvider(activity.userAccountManager) testViewModel.setConnectivityService(activity.connectivityServiceMock) val localRepository = UnifiedSearchFakeRepository() testViewModel.setRepository(localRepository) 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 3f1e4be006b0..60e017489fb1 100644 --- a/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt +++ b/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt @@ -104,7 +104,8 @@ import com.owncloud.android.db.ProviderMeta AutoMigration(from = 101, to = 102, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class), AutoMigration(from = 102, to = 103, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class), AutoMigration(from = 103, to = 104), - AutoMigration(from = 104, to = 105) + AutoMigration(from = 104, to = 105), + AutoMigration(from = 105, to = 106, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class) ], exportSchema = true ) diff --git a/app/src/main/java/com/nextcloud/client/database/entity/CapabilityEntity.kt b/app/src/main/java/com/nextcloud/client/database/entity/CapabilityEntity.kt index 0ff14aa9e944..253ddc547a1a 100644 --- a/app/src/main/java/com/nextcloud/client/database/entity/CapabilityEntity.kt +++ b/app/src/main/java/com/nextcloud/client/database/entity/CapabilityEntity.kt @@ -157,7 +157,9 @@ data class CapabilityEntity( @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_MOD_REWRITE_WORKING) val modRewriteWorking: Int?, @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_CHUNKED_UPLOAD_MAX_SIZE) - val chunkedUploadMaxSize: Long? + val chunkedUploadMaxSize: Long?, + @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_SHARING_JSON) + val sharingJson: String? ) @Suppress("LongMethod", "ReturnCount") diff --git a/app/src/main/java/com/nextcloud/client/player/ui/PlayerLauncher.kt b/app/src/main/java/com/nextcloud/client/player/ui/PlayerLauncher.kt index c316d2fbaddd..9306da9035e1 100644 --- a/app/src/main/java/com/nextcloud/client/player/ui/PlayerLauncher.kt +++ b/app/src/main/java/com/nextcloud/client/player/ui/PlayerLauncher.kt @@ -19,6 +19,7 @@ import com.nextcloud.client.player.model.file.PlaybackFiles import com.nextcloud.client.player.model.file.PlaybackFilesComparator import com.nextcloud.client.player.model.file.PlaybackFilesRepository import com.nextcloud.client.player.util.PlayerUtil.toPlaybackFile +import com.nextcloud.utils.extensions.resolveMimeType import com.owncloud.android.datamodel.OCFile import kotlinx.coroutines.Job import kotlinx.coroutines.launch @@ -68,7 +69,7 @@ class PlayerLauncher @Inject constructor( } private suspend fun prepareQueue(file: OCFile, collection: PlaybackCollection): PlaybackFileType { - val fileType = PlaybackFileType.ofMimeType(file.mimeType) + val fileType = PlaybackFileType.ofMimeType(file.resolveMimeType()) playbackResumptionConfigStore.saveConfig(file.localId.toString(), file.parentId, fileType, collection) playbackModel.start() diff --git a/app/src/main/java/com/nextcloud/client/player/util/PlayerUtil.kt b/app/src/main/java/com/nextcloud/client/player/util/PlayerUtil.kt index e8ff924c2aeb..d7dbaa93864a 100644 --- a/app/src/main/java/com/nextcloud/client/player/util/PlayerUtil.kt +++ b/app/src/main/java/com/nextcloud/client/player/util/PlayerUtil.kt @@ -34,6 +34,7 @@ import com.nextcloud.client.player.model.state.PlaybackState import com.nextcloud.client.player.model.state.PlayerState import com.nextcloud.client.player.model.state.RepeatMode import com.nextcloud.client.player.model.state.VideoSize +import com.nextcloud.utils.extensions.resolveMimeType import com.owncloud.android.datamodel.OCFile import com.owncloud.android.lib.resources.shares.OCShare import com.owncloud.android.utils.MimeTypeUtil @@ -177,7 +178,7 @@ object PlayerUtil { id = localId.toString(), uri = getPlaybackUri().toString(), name = fileName, - mimeType = mimeType, + mimeType = resolveMimeType(), contentLength = fileLength, lastModified = modificationTimestamp, isFavorite = isFavorite @@ -187,13 +188,13 @@ object PlayerUtil { id = fileSource.toString(), uri = getPlaybackUri().toString(), name = path?.let { File(it).name } ?: "", - mimeType = getMimeType(), + mimeType = resolveMimeType(), contentLength = UNKNOWN_CONTENT_LENGTH, lastModified = sharedDate * SECOND_IN_MILLISECONDS, isFavorite = isFavorite ) - private fun OCShare.getMimeType(): String = mimetype + private fun OCShare.resolveMimeType(): String = mimetype ?.takeIf { it.isNotEmpty() } ?: path?.let { MimeTypeUtil.getMimeTypeFromPath(it) } ?: "" diff --git a/app/src/main/java/com/nextcloud/utils/extensions/MimeTypeExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/MimeTypeExtensions.kt new file mode 100644 index 000000000000..e58c7d58f12c --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/extensions/MimeTypeExtensions.kt @@ -0,0 +1,15 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.nextcloud.utils.extensions + +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.utils.MimeTypeUtil + +fun OCFile.resolveMimeType(): String = mimeType + ?.takeIf { it.isNotEmpty() } + ?: remotePath?.let { MimeTypeUtil.getMimeTypeFromPath(it) } + ?: "" diff --git a/app/src/main/java/com/nextcloud/utils/extensions/OwnCloudClientExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/OwnCloudClientExtensions.kt index 857b3e8399b7..621bf4a75f48 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/OwnCloudClientExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/OwnCloudClientExtensions.kt @@ -9,6 +9,7 @@ package com.nextcloud.utils.extensions import android.content.Context +import com.nextcloud.android.common.ui.network.auth.ServerCredentials import com.nextcloud.common.NextcloudClient import com.owncloud.android.lib.common.OwnCloudClient import com.owncloud.android.lib.common.OwnCloudClientFactory @@ -34,3 +35,9 @@ fun OwnCloudClient.getVideoPreviewEndpoint(localFileId: Long, size: Int): String localFileId + "&x=" + size + "&y=" + size + "&a=1&forceIcon=0" + +/** + * Used in Android Common + */ +fun OwnCloudClient.toServerCredentials(baseURL: String): ServerCredentials = + ServerCredentials(baseURL, userIdPlain, credentials.authToken) diff --git a/app/src/main/java/com/nextcloud/utils/extensions/ServerCredentialsExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/ServerCredentialsExtensions.kt new file mode 100644 index 000000000000..e30b915d210e --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/extensions/ServerCredentialsExtensions.kt @@ -0,0 +1,24 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils.extensions + +import com.nextcloud.android.common.ui.network.auth.ServerCredentials +import com.nextcloud.android.common.ui.network.http.NextcloudHttpClient +import com.nextcloud.android.common.ui.network.model.dataOrElse +import com.nextcloud.android.common.ui.share.repository.ShareRemoteRepository +import com.owncloud.android.lib.common.utils.Log_OC + +private const val TAG = "ServerCredentialsExtensions" + +suspend fun ServerCredentials.supportsUnifiedShare(): Boolean { + val capabilities = ShareRemoteRepository(NextcloudHttpClient.create(this)) + .fetchSharingCapabilities() + .dataOrElse { Log_OC.e(TAG, "Failed to fetch sharing capabilities") } + + return capabilities?.isUnifiedShareEnabled == true +} diff --git a/app/src/main/java/com/nextcloud/utils/extensions/UserExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/UserExtensions.kt new file mode 100644 index 000000000000..327b004bfa43 --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/extensions/UserExtensions.kt @@ -0,0 +1,27 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils.extensions + +import com.nextcloud.android.common.ui.network.auth.ServerCredentials +import com.nextcloud.client.account.User +import com.owncloud.android.MainApp +import com.owncloud.android.lib.common.OwnCloudClientManagerFactory +import com.owncloud.android.lib.common.utils.Log_OC + +private const val TAG = "UserExtensions" + +@Suppress("TooGenericExceptionCaught") +fun User.toServerCredentials(): ServerCredentials? = try { + OwnCloudClientManagerFactory + .getDefaultSingleton() + .getClientFor(toOwnCloudAccount(), MainApp.getAppContext()) + .toServerCredentials(server.uri.toString()) +} catch (e: Exception) { + Log_OC.e(TAG, "Failed to create client for $accountName", e) + null +} 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 ccad44f05d14..42d5db1feb35 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java @@ -2373,6 +2373,8 @@ private ContentValues createContentValues(String accountName, OCCapability capab contentValues.put(ProviderTableMeta.CAPABILITIES_CHUNKED_UPLOAD_MAX_SIZE, capability.getChunkedUploadMaxSize()); + contentValues.put(ProviderTableMeta.CAPABILITIES_SHARING_JSON, capability.getSharingJson()); + return contentValues; } @@ -2576,6 +2578,8 @@ private OCCapability createCapabilityInstance(Cursor cursor) { capability.setChunkedUploadMaxSize( getLong(cursor, ProviderTableMeta.CAPABILITIES_CHUNKED_UPLOAD_MAX_SIZE)); + + capability.setSharingJson(getString(cursor, ProviderTableMeta.CAPABILITIES_SHARING_JSON)); } return capability; 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 b3a91ca55c87..3886a51210d6 100644 --- a/app/src/main/java/com/owncloud/android/db/ProviderMeta.java +++ b/app/src/main/java/com/owncloud/android/db/ProviderMeta.java @@ -24,7 +24,7 @@ */ public class ProviderMeta { public static final String DB_NAME = "filelist"; - public static final int DB_VERSION = 105; + public static final int DB_VERSION = 106; private ProviderMeta() { // No instance @@ -299,6 +299,7 @@ static public class ProviderTableMeta implements BaseColumns { public static final String CAPABILITIES_CLIENT_INTEGRATION_JSON = "client_integration_json"; public static final String CAPABILITIES_MOD_REWRITE_WORKING = "mod_rewrite_working"; public static final String CAPABILITIES_CHUNKED_UPLOAD_MAX_SIZE = "chunked_upload_max_size"; + public static final String CAPABILITIES_SHARING_JSON = "sharing_json"; //Columns of Uploads table public static final String UPLOADS_LOCAL_PATH = "local_path"; 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 d089a2aa17fa..d85ed24a469b 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 @@ -612,9 +612,12 @@ private void bindSharedAvatars(ListItemViewHolder holder, OCFile file) { if (sharedAvatars.getChildCount() > 0) { sharedAvatars.removeAllViews(); } - final var avatars = helper.getAvatarSharees(file, userId); - sharedAvatars.setAvatars(user, avatars, viewThemeUtils); - sharedAvatars.setOnClickListener(view -> ocFileListFragmentInterface.onShareIconClick(file)); + + helper.getAvatarSharees(file, user, userId, avatars -> { + sharedAvatars.setAvatars(user, avatars, viewThemeUtils); + sharedAvatars.setOnClickListener(view -> ocFileListFragmentInterface.onShareIconClick(file)); + return Unit.INSTANCE; + }); } private void bindListItemViewHolder(ListItemViewHolder holder, OCFile file) { diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterHelper.kt b/app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterHelper.kt index 09061a249049..68bbd807c576 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterHelper.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterHelper.kt @@ -7,10 +7,15 @@ package com.owncloud.android.ui.adapter.helper +import com.nextcloud.android.common.ui.share.avatar.ShareAvatarRepository +import com.nextcloud.android.common.ui.share.model.api.share.Share +import com.nextcloud.client.account.User import com.nextcloud.client.database.entity.FileEntity import com.nextcloud.client.preferences.AppPreferences import com.nextcloud.utils.extensions.filterFilenames import com.nextcloud.utils.extensions.isTempFile +import com.nextcloud.utils.extensions.supportsUnifiedShare +import com.nextcloud.utils.extensions.toServerCredentials import com.owncloud.android.MainApp import com.owncloud.android.datamodel.OCFile import com.owncloud.android.lib.resources.shares.ShareType @@ -56,19 +61,38 @@ class OCFileListAdapterHelper { } } - fun getAvatarSharees(file: OCFile, userId: String?): List { - val sharees = file.sharees - val ownerId = file.ownerId + fun getAvatarSharees(file: OCFile, user: User?, userId: String?, onComplete: (List) -> Unit) { + scope.launch { + val credentials = user?.toServerCredentials() + val result = if (credentials != null && credentials.supportsUnifiedShare()) { + ShareAvatarRepository( + credentials + ).fetchShareAvatars(file.localId.toString())?.toAvatarSharees().orEmpty() + } else { + file.toLocalSharees(userId) + } - val ownerSharee = if (!ownerId.isNullOrEmpty() && ownerId != userId) { - ShareeUser(ownerId, file.ownerDisplayName, ShareType.USER).takeIf { it !in sharees } - } else { - null + withContext(Dispatchers.Main) { + onComplete(result) + } } + } + + private fun OCFile.toLocalSharees(userId: String?): List { + val ownerSharee = ownerId + ?.takeIf { it.isNotEmpty() && it != userId } + ?.let { ShareeUser(it, ownerDisplayName, ShareType.USER) } + ?.takeIf { it !in sharees } return listOfNotNull(ownerSharee) + sharees.asReversed() } + private fun List.toAvatarSharees(): List = asSequence() + .flatMap { share -> share.invitedRecipients } + .distinctBy { recipient -> recipient.value } + .map { recipient -> ShareeUser(recipient.value, recipient.displayName, ShareType.USER) } + .toList() + suspend fun prepareFileList( directory: OCFile, dataProvider: OCFileListAdapterDataProvider, diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailActivitiesFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailActivitiesFragment.kt index 8283e0d82439..ac6fdd670935 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailActivitiesFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailActivitiesFragment.kt @@ -31,6 +31,7 @@ import com.nextcloud.client.di.Injectable import com.nextcloud.client.network.ClientFactory import com.nextcloud.client.network.ClientFactory.CreationException import com.nextcloud.common.NextcloudClient +import com.nextcloud.utils.ResultParser.list import com.nextcloud.utils.extensions.getParcelableArgument import com.owncloud.android.R import com.owncloud.android.databinding.FileDetailsActivitiesFragmentBinding @@ -302,40 +303,40 @@ class FileDetailActivitiesFragment : versions: ArrayList?, lastGiven: Long ) { - val data = result.data - if (result.isSuccess && data != null) { - val activitiesAndVersions = data[0] as ArrayList - this.lastGiven = data[1] as Long - - if (activitiesAndVersions.isEmpty()) { - this.lastGiven = END_REACHED.toLong() - } - - if (restoreFileVersionSupported && versions != null) { - activitiesAndVersions.addAll(versions) + val payload = result.list() + if (payload.size < ACTIVITIES_PAYLOAD_SIZE) { + Log_OC.d(TAG, result.logMessage) + val logMessage = if (result.httpCode == HttpStatus.SC_NOT_MODIFIED) { + getString(R.string.activities_no_results_message) + } else { + result.getLogMessage(activity) } - if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) { - populateList(activitiesAndVersions, lastGiven == -1L) + if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) { + setErrorContent(logMessage) + isLoadingActivities = false } - isDataFetched = true + isDataFetched = false return } - Log_OC.d(TAG, result.logMessage) - val logMessage = if (result.httpCode == HttpStatus.SC_NOT_MODIFIED) { - getString(R.string.activities_no_results_message) - } else { - result.getLogMessage(activity) + val activitiesAndVersions = payload[0] as ArrayList + this.lastGiven = payload[1] as Long + + if (activitiesAndVersions.isEmpty()) { + this.lastGiven = END_REACHED.toLong() } - if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) { - setErrorContent(logMessage) - isLoadingActivities = false + if (restoreFileVersionSupported && versions != null) { + activitiesAndVersions.addAll(versions) } - isDataFetched = false + if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) { + populateList(activitiesAndVersions, lastGiven == -1L) + } + + isDataFetched = true } // endregion @@ -480,6 +481,9 @@ class FileDetailActivitiesFragment : private const val END_REACHED = 0 private const val LOAD_MORE_THRESHOLD = 5 + // the activities result carries the activity list and the paging marker + private const val ACTIVITIES_PAYLOAD_SIZE = 2 + @JvmStatic fun newInstance(file: OCFile?, user: User?): FileDetailActivitiesFragment = FileDetailActivitiesFragment().apply { diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt index cdacde73a97f..4f1341340974 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt @@ -31,6 +31,8 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.button.MaterialButton +import com.nextcloud.android.common.ui.network.auth.ServerCredentials +import com.nextcloud.android.common.ui.share.initShareScreen import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.client.account.User import com.nextcloud.client.account.UserAccountManager @@ -40,6 +42,8 @@ import com.nextcloud.client.utils.IntentUtil import com.nextcloud.utils.extensions.getParcelableArgument import com.nextcloud.utils.extensions.mergeDistinctByToken import com.nextcloud.utils.extensions.setVisibleIf +import com.nextcloud.utils.extensions.supportsUnifiedShare +import com.nextcloud.utils.extensions.toServerCredentials import com.nextcloud.utils.mdm.MDMConfig.shareViaUser import com.owncloud.android.R import com.owncloud.android.databinding.FileDetailsSharingFragmentBinding @@ -126,25 +130,13 @@ class FileDetailSharingFragment : override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - fileActivity ?: return fileDataStorageManager = fileActivity?.storageManager fileOperationsHelper = fileActivity?.fileOperationsHelper - - startAnimation() - - val userId = getUserId() - - setupInternalShares(userId) - setupExternalShares(userId) - - binding?.pickContactEmailBtn?.setOnClickListener { checkContactPermission() } - - fetchSharees() - setupView() + initializeSharingMode() } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { binding = FileDetailsSharingFragmentBinding.inflate(inflater, container, false) return binding!!.getRoot() } @@ -177,6 +169,49 @@ class FileDetailSharingFragment : // endregion // region private methods + private fun initializeSharingMode() { + viewLifecycleOwner.lifecycleScope.launch { + withContext(Dispatchers.IO) { + user?.toServerCredentials()?.takeIf { it.supportsUnifiedShare() } + }?.let(::showUnifiedShare) ?: showLegacyShare() + } + } + + private fun showLegacyShare() { + binding?.sharingModeProgress?.visibility = View.GONE + + val userId = getUserId() + setupInternalShares(userId) + setupExternalShares(userId) + + startShimmerAnimation() + setupLegacyShareUi() + fetchSharees() + } + + private fun showUnifiedShare(credentials: ServerCredentials) { + val binding = binding ?: return + val user = user ?: return + val file = file ?: return + val sourceId = file.localId.toString() + + binding.sharingModeProgress.visibility = View.GONE + binding.shareContainer.visibility = View.GONE + binding.unifiedShare.visibility = View.VISIBLE + + val shimmerLayout = binding.shimmerLayout.root + shimmerLayout.clearAnimation() + shimmerLayout.visibility = View.GONE + + binding.unifiedShare.initShareScreen( + viewModelStoreOwner = this, + sourceId = sourceId, + internalLink = createInternalLink(user, file, capabilities), + credentials = credentials, + colorScheme = viewThemeUtils.files.getColorScheme(requireContext()) + ) + } + private fun initArguments(savedInstanceState: Bundle?) { val args = (savedInstanceState ?: arguments) ?: return file = args.getParcelableArgument(ARG_FILE, OCFile::class.java) @@ -222,9 +257,11 @@ class FileDetailSharingFragment : private fun createShareListLayoutManager(): LinearLayoutManager = LinearLayoutManager(requireContext()) - private fun startAnimation() { + private fun startShimmerAnimation() { + val shimmerLayout = binding?.shimmerLayout?.root ?: return + shimmerLayout.visibility = View.VISIBLE val blinkAnimation = AnimationUtils.loadAnimation(requireContext(), R.anim.blink) - binding?.shimmerLayout?.getRoot()?.startAnimation(blinkAnimation) + shimmerLayout.startAnimation(blinkAnimation) } private fun fetchSharees() { @@ -243,16 +280,16 @@ class FileDetailSharingFragment : if (result) { refreshCapabilitiesFromDB() refreshSharesFromDB() - stopLoadingAnimationAndShowShareContainer() + hideShimmerAndShowShareContainer() return@launch } - stopLoadingAnimationAndShowShareContainer() + hideShimmerAndShowShareContainer() DisplayUtils.showSnackMessage(this@FileDetailSharingFragment, R.string.error_fetching_sharees) } } - private fun stopLoadingAnimationAndShowShareContainer() { + private fun hideShimmerAndShowShareContainer() { binding?.run { shimmerLayout.root.run { clearAnimation() @@ -272,25 +309,27 @@ class FileDetailSharingFragment : } } - private fun setupView() { + private fun setupLegacyShareUi() { resetSearchView() setShareWithYou() binding?.run { + pickContactEmailBtn.setOnClickListener { checkContactPermission() } + FileDetailSharingFragmentHelper.setupSearchView( fileActivity?.getSystemService(Context.SEARCH_SERVICE) as SearchManager?, searchView, fileActivity?.componentName ) - themeView(this) - setupShareList(this) + applyLegacyShareTheme(this) + setupShowAllButtons(this) if (file?.canReshare() == true && !FileDetailSharingFragmentHelper.isPublicShareDisabled(capabilities)) { val parentFile = file?.parentId?.let { fileDataStorageManager?.getFileById(it) } - setupShareView(this, parentFile) + configureCreateLinkSection(this, parentFile) } else { - setupDisabledShareView(this) + disableResharingUi(this) } checkShareViaUser() @@ -310,7 +349,7 @@ class FileDetailSharingFragment : } } - private fun themeView(binding: FileDetailsSharingFragmentBinding) { + private fun applyLegacyShareTheme(binding: FileDetailsSharingFragmentBinding) { binding.run { viewThemeUtils.material.run { themeSearchCardView(searchCardWrapper) @@ -332,7 +371,7 @@ class FileDetailSharingFragment : } } - private fun setupShareList(binding: FileDetailsSharingFragmentBinding) { + private fun setupShowAllButtons(binding: FileDetailsSharingFragmentBinding) { binding.run { sharesListInternalShowAll.setOnClickListener { expandOrCollapseAdapter(internalShareeListAdapter, sharesListInternalShowAll) @@ -352,7 +391,7 @@ class FileDetailSharingFragment : button.setText(actionTextId) } - private fun setupViewForEncryptedShare(binding: FileDetailsSharingFragmentBinding) { + private fun configureEncryptedShareUi(binding: FileDetailsSharingFragmentBinding) { binding.run { internalShareHeadline.text = resources.getString(R.string.internal_share_headline_end_to_end_encrypted) internalShareDescription.visibility = View.VISIBLE @@ -374,7 +413,7 @@ class FileDetailSharingFragment : searchView.setQueryHint(resources.getString(R.string.secure_share_search)) if (file?.isSharedViaLink == true) { - setupSearchViewForSharedLink(searchView) + disableSearchViewForFileDrop(searchView) } } } @@ -386,16 +425,16 @@ class FileDetailSharingFragment : binding.createLink.visibility = View.GONE } - private fun setupSearchViewForSharedLink(searchView: SearchView) { + private fun disableSearchViewForFileDrop(searchView: SearchView) { searchView.setQueryHint(resources.getString(R.string.share_not_allowed_when_file_drop)) searchView.inputType = InputType.TYPE_NULL toggleSearchViewEnable(searchView, false) } - private fun setupShareView(binding: FileDetailsSharingFragmentBinding, parentFile: OCFile?) { + private fun configureCreateLinkSection(binding: FileDetailsSharingFragmentBinding, parentFile: OCFile?) { binding.run { if (file?.isEncrypted == true || (parentFile != null && parentFile.isEncrypted)) { - setupViewForEncryptedShare(this) + configureEncryptedShareUi(this) } else { createLink.setText(R.string.create_link) searchView.setQueryHint(getResources().getString(R.string.share_search_internal)) @@ -405,7 +444,7 @@ class FileDetailSharingFragment : } } - private fun setupDisabledShareView(binding: FileDetailsSharingFragmentBinding) { + private fun disableResharingUi(binding: FileDetailsSharingFragmentBinding) { binding.run { searchView.setQueryHint(getResources().getString(R.string.resharing_is_not_allowed)) createLink.visibility = View.GONE @@ -518,9 +557,9 @@ class FileDetailSharingFragment : FileActivity.showShareLinkDialog(fileActivity, file, publicShare.shareLink) } - private fun refreshUiFromDB() { + private fun refreshLegacyShareUiFromDb() { refreshSharesFromDB() - setupView() + setupLegacyShareUi() } private fun unShareWith(share: OCShare) { @@ -812,9 +851,9 @@ class FileDetailSharingFragment : } if (result.isSuccess) { - refreshUiFromDB() + refreshLegacyShareUiFromDb() } else { - setupView() + setupLegacyShareUi() } } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java index b473910fd773..2e40256f37b7 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java @@ -61,6 +61,7 @@ import com.nextcloud.utils.extensions.FileExtensionsKt; import com.nextcloud.utils.extensions.FragmentExtensionsKt; import com.nextcloud.utils.extensions.IntentExtensionsKt; +import com.nextcloud.utils.extensions.MimeTypeExtensionsKt; import com.nextcloud.utils.extensions.OCFileExtensionsKt; import com.nextcloud.utils.extensions.OCFileListFragmentExtensionsKt; import com.nextcloud.utils.extensions.ViewExtensionsKt; @@ -707,7 +708,8 @@ public void openActionsMenu(final int filesCount, final Set checkedFiles throttler.run("overflowClick", () -> { final var actionsToHide = FileAction.Companion.getFileListActionsToHide(checkedFiles); - List endpoints = getCapabilities().getClientIntegrationEndpoints(Type.CONTEXT_MENU, checkedFiles.iterator().next().getMimeType()); + final var mimeType = MimeTypeExtensionsKt.resolveMimeType(checkedFiles.iterator().next()); + List endpoints = getCapabilities().getClientIntegrationEndpoints(Type.CONTEXT_MENU, mimeType); final var childFragmentManager = getChildFragmentManager(); final var actionBottomSheet = FileActionsBottomSheet.newInstance(filesCount, checkedFiles, isOverflow, actionsToHide, endpoints) diff --git a/app/src/main/java/com/owncloud/android/ui/unifiedsearch/UnifiedSearchViewModel.kt b/app/src/main/java/com/owncloud/android/ui/unifiedsearch/UnifiedSearchViewModel.kt index d9e09b6f9ff0..8082dc3cc054 100644 --- a/app/src/main/java/com/owncloud/android/ui/unifiedsearch/UnifiedSearchViewModel.kt +++ b/app/src/main/java/com/owncloud/android/ui/unifiedsearch/UnifiedSearchViewModel.kt @@ -28,6 +28,7 @@ import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.ui.asynctasks.GetRemoteFileTask import com.owncloud.android.ui.fragment.UnifiedSearchFragmentScreenState import javax.inject.Inject +import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -59,6 +60,10 @@ class UnifiedSearchViewModel(application: Application) : private var results: MutableMap = mutableMapOf() private var searchResultsVersion = 0 + private val sectionBuildErrorHandler = CoroutineExceptionHandler { _, throwable -> + Log_OC.e(TAG, "Cannot build search result sections", throwable) + } + override val screenState: MutableLiveData = MutableLiveData(UnifiedSearchFragmentScreenState.ShowingContent) override val isLoading = MutableLiveData(false) @@ -208,7 +213,7 @@ class UnifiedSearchViewModel(application: Application) : val version = ++searchResultsVersion - viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch(Dispatchers.IO + sectionBuildErrorHandler) { val sections = buildSections(snapshot) withContext(Dispatchers.Main) { @@ -274,6 +279,11 @@ class UnifiedSearchViewModel(application: Application) : this.connectivityService = connectivityService } + @VisibleForTesting + fun setCurrentAccountProvider(currentAccountProvider: CurrentAccountProvider) { + this.currentAccountProvider = currentAccountProvider + } + override fun updateScreenState(state: UnifiedSearchFragmentScreenState) { screenState.value = state } diff --git a/app/src/main/res/layout/file_details_sharing_fragment.xml b/app/src/main/res/layout/file_details_sharing_fragment.xml index 14a056da8f5c..8358c515c737 100644 --- a/app/src/main/res/layout/file_details_sharing_fragment.xml +++ b/app/src/main/res/layout/file_details_sharing_fragment.xml @@ -5,25 +5,25 @@ ~ SPDX-FileCopyrightText: 2018 Andy Scherzinger ~ SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only --> - + android:layout_below="@id/appbar"> - + android:paddingTop="@dimen/standard_eight_padding"> + tools:visibility="visible"> + app:iconGravity="textStart" /> + android:textAppearance="?android:attr/textAppearanceMedium" + android:visibility="gone" + tools:visibility="visible" /> @@ -209,9 +209,9 @@ android:layout_height="wrap_content" android:layout_marginStart="@dimen/standard_margin" android:layout_marginEnd="@dimen/standard_margin" + android:text="@string/create_link" app:icon="@drawable/file_link" - app:iconGravity="textStart" - android:text="@string/create_link" /> + app:iconGravity="textStart" /> - + + + + + - + - + diff --git a/app/src/main/res/layout/file_details_sharing_shimmer.xml b/app/src/main/res/layout/file_details_sharing_shimmer.xml index f679386e4a1b..99a1c8cbdfb1 100644 --- a/app/src/main/res/layout/file_details_sharing_shimmer.xml +++ b/app/src/main/res/layout/file_details_sharing_shimmer.xml @@ -7,7 +7,7 @@ --> diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7e37a2b7cd74..0d57bc6e65f1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ # SPDX-License-Identifier: AGPL-3.0-or-later [versions] -androidCommonLibraryVersion = "0.33.2" +androidCommonLibraryVersion = "f46fb3b6789dee1f476269903da61e9b48c99587" androidGifDrawableVersion = "1.2.32" androidImageCropperVersion = "4.7.0" androidLibraryVersion ="rc-2.26.0" @@ -115,7 +115,7 @@ photoview = { module = "com.github.Baseflow:PhotoView", version.ref = "photoview material = { module = "com.google.android.material:material", version.ref = "materialVersion" } android-gif-drawable = { module = "pl.droidsonroids.gif:android-gif-drawable", version.ref = "androidGifDrawableVersion" } android-image-cropper = { module = "com.vanniktech:android-image-cropper", version.ref = "androidImageCropperVersion" } -androidsvg = { module = "com.caverock:androidsvg", version.ref = "androidsvgVersion" } +androidsvg = { module = "com.caverock:androidsvg-aar", version.ref = "androidsvgVersion" } coil = { module = "io.coil-kt:coil", version.ref = "coilVersion" } constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayoutVersion" } emoji-google = { module = "com.vanniktech:emoji-google", version.ref = "emojiGoogleVersion" } diff --git a/gradle/verification-keyring.keys b/gradle/verification-keyring.keys index 482067875174..16e740845ef2 100644 --- a/gradle/verification-keyring.keys +++ b/gradle/verification-keyring.keys @@ -2644,6 +2644,31 @@ Zo/Xr9x/cYkFS8ieh6XR0y1Yx4xZIDTi3Q== =BLWG -----END PGP PUBLIC KEY BLOCK----- +pub CFAE163B64AC9189 +uid JetBrains Compose Team + +sub 57CE36BB68F1BC57 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +xjMEYYx3eRYJKwYBBAHaRw8BAQdAV7zh1T+xL7mD2O63rTIvRfQ9kwL2Gvq/Q6PD +9apCe2LNLkpldEJyYWlucyBDb21wb3NlIFRlYW0gPGNvbXBvc2VAamV0YnJhaW5z +LmNvbT7CmgQTFgoAQgIbAwULCQgHAgMiAgEGFQoJCAsCBBYCAwECHgcCF4AWIQQg +cjpjmbwGAVQoOzfPrhY7ZKyRiQUCaG6CXQUJCqRx5AAKCRDPrhY7ZKyRiWDHAP44 +d+F1AdeplpzPVOyymiUyPSRebatZiuIW3hV0xxzRXwD/X/CYkfgaxsuZBauiJRdV +CZ6MGKktzduPjii7uG2tnwnCmgQTFgoAQhYhBCByOmOZvAYBVCg7N8+uFjtkrJGJ +BQJhjHd5AhsDBQkDwmcABQsJCAcCAyICAQYVCgkICwIEFgIDAQIeBwIXgAAKCRDP +rhY7ZKyRiRslAQCEXiOkwreTSEDg7UXsRd1IruaQEUkrAFsk+1/FkqqvcwEAiAx2 +FgUTgDV8RoflRcaBkqPfkSvkvUBuJBQwqjGuhwbOOARhjHd5EgorBgEEAZdVAQUB +AQdA/AOwflH/rum1g1gvFPBcqhvFOcN80VUb7OqoENSzUD4DAQgHwn4EGBYKACYC +GwwWIQQgcjpjmbwGAVQoOzfPrhY7ZKyRiQUCaG6CZAUJCqRx6wAKCRDPrhY7ZKyR +ic9RAQDDzuctJKtjgbxXk0TrWhxYtbU3s84Y2ygPVP2nXPw4ZQD/a5oUoK7Pjoud +jnyqb/8E51C4PlN84d6IkEgMWo3bgQ/CfgQYFgoAJhYhBCByOmOZvAYBVCg7N8+u +FjtkrJGJBQJhjHd5AhsMBQkDwmcAAAoJEM+uFjtkrJGJKrQBAITAZEZ3kw5NPaQo +l/0KmFfQs6ypUEFI2/OiWhXTjDPLAP4/hOYjP8KyKCzMNDLzKXX+sa+zOjuBpTCd ++eo3rUkeAA== +=MIIq +-----END PGP PUBLIC KEY BLOCK----- + pub D041CAD2E452550F uid Deanna @@ -4993,6 +5018,28 @@ GSJf =L+Xu -----END PGP PUBLIC KEY BLOCK----- +pub 1188B69F6D6259CA +uid Chris Banes + +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBF2hcBkBCAC2H5WcFoeByKBUAjRDjmP+5P6FRsZjLe6c1wy7G1ha6/EQUVK4 +gZUZYE9W7l/4QKHvAu4PvFWdD+5FXGZuB/2kw348CtabAlJTehm1QlPt5//ODkxB +fWPPz3uHBo5PQJZuLpStQn+aEkTTHK6Sk5033e6fa7mV5X/c8pTmTzkunG58NFbj +5VSVbks4pbafKoMY7rSN0/I2hEApCjB9tx/7DJuQ9gbaGhmabnhBTnwHXEV1/hIF +369lfiqeoeqDMOKj99C6KFD/NPRrRLfoPRpqL3LPsPp7P+TzyZN2q89Xqn2ysyI9 +jDtrlssEXLskU5kA8fVa179V/QR6QtrJ29m/ABEBAAG0HENocmlzIEJhbmVzIDxj +aHJpc0BiYW5lcy5tZT6JAVQEEwEIAD4WIQRkubCfFkqgv4h0LrYRiLafbWJZygUC +XaFwGQIbAwUJA8JnAAULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRARiLafbWJZ +ygNIB/wPXRnQJ5Pj+G/S0sbcDjTFegfN5TzXRRu0wXyogHPCt5ItE+8DURp7coq1 +eaocMZQ1186QLow/gInpji5l2pyv8BjC+H0GrXrT00S4wWcmd4ink+cNIwCtO8Dk +Bi25t9RxA12zKqXn62W8sATRl4DlL17ASw8k1bYDGWWJAM8GJG2yucaFN7RohO7q +5QmcjsXmuzfZtcKrlVBVNkYoGQKfjgr5fPjG1p5aOKELYt33u16stDGOuvXyJL2h +4FhqMJL9UvFj7uOdMQLK10QPb3tFXELchvBWCkD/GVHtH1hwFumLkLbIKaj43Sq8 +Kap/NfRkl5GXw5e8UQDQevfTXdJd +=hRFp +-----END PGP PUBLIC KEY BLOCK----- + pub 15C71C0A4E0B8EDD uid Matthias Bläsing diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 35383bb8734a..cd34639c89ce 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -126,6 +126,7 @@ + @@ -146,11 +147,17 @@ + + + + + + @@ -167,6 +174,8 @@ + + @@ -269,6 +278,7 @@ + @@ -291,6 +301,7 @@ + @@ -524,11 +535,15 @@ - + + + + + @@ -553,7 +568,10 @@ - + + + + @@ -10785,7 +10803,7 @@ - + @@ -21034,6 +21052,14 @@ + + + + + + + + @@ -22775,6 +22801,14 @@ + + + + + + + + @@ -22839,6 +22873,22 @@ + + + + + + + + + + + + + + + + @@ -23019,6 +23069,14 @@ + + + + + + + + @@ -23083,6 +23141,22 @@ + + + + + + + + + + + + + + + + @@ -23259,6 +23333,14 @@ + + + + + + + + @@ -23323,6 +23405,22 @@ + + + + + + + + + + + + + + + + @@ -27865,6 +27963,14 @@ + + + + + + + + @@ -27942,6 +28048,14 @@ + + + + + + + + @@ -37761,6 +37875,11 @@ + + + + + diff --git a/scripts/runCombinedTest.sh b/scripts/runCombinedTest.sh index b65aa64dd7c6..93bd567e13f2 100755 --- a/scripts/runCombinedTest.sh +++ b/scripts/runCombinedTest.sh @@ -30,12 +30,10 @@ scripts/wait_for_emulator.sh || exit 1 adb logcat -c adb logcat > logcat.txt & LOGCAT_PID=$! -# Screenshot tests only run when updating/testing screenshots, tests annotated with -# com.nextcloud.test.Flaky are known to be unstable and must not block a pull request. -EXCLUDED_ANNOTATIONS="com.owncloud.android.utils.ScreenshotTest,com.nextcloud.test.Flaky" - ./gradlew createGplayDebugCoverageReport \ --Pcoverage -Pandroid.testInstrumentationRunnerArguments.notAnnotation="$EXCLUDED_ANNOTATIONS" \ +-Pcoverage \ +-Pandroid.testInstrumentationRunnerArguments.notAnnotation=com.owncloud.android.utils.ScreenshotTest \ +-Pandroid.testInstrumentationRunnerArguments.filter=com.nextcloud.test.FlakyTestFilter \ -Dorg.gradle.jvmargs="--add-opens java.base/java.nio=ALL-UNNAMED --add-opens java.base/java.nio.channels=ALL-UNNAMED --add-exports java.base/sun.nio.ch=ALL-UNNAMED" stat=$?