diff --git a/app/schemas/com.nextcloud.client.database.NextcloudDatabase/97.json b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/97.json new file mode 100644 index 000000000000..27f506701672 --- /dev/null +++ b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/97.json @@ -0,0 +1,1298 @@ +{ + "formatVersion": 1, + "database": { + "version": 97, + "identityHash": "1c5a77152bf79ee80f9e6eb2677d75a7", + "entities": [ + { + "tableName": "arbitrary_data", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `cloud_id` TEXT, `key` TEXT, `value` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "cloudId", + "columnName": "cloud_id", + "affinity": "TEXT" + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "capabilities", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `assistant` INTEGER, `account` TEXT, `version_mayor` INTEGER, `version_minor` INTEGER, `version_micro` INTEGER, `version_string` TEXT, `version_edition` TEXT, `extended_support` INTEGER, `core_pollinterval` INTEGER, `sharing_api_enabled` INTEGER, `sharing_public_enabled` INTEGER, `sharing_public_password_enforced` INTEGER, `sharing_public_expire_date_enabled` INTEGER, `sharing_public_expire_date_days` INTEGER, `sharing_public_expire_date_enforced` INTEGER, `sharing_public_send_mail` INTEGER, `sharing_public_upload` INTEGER, `sharing_user_send_mail` INTEGER, `sharing_resharing` INTEGER, `sharing_federation_outgoing` INTEGER, `sharing_federation_incoming` INTEGER, `files_bigfilechunking` INTEGER, `files_undelete` INTEGER, `files_versioning` INTEGER, `external_links` INTEGER, `server_name` TEXT, `server_color` TEXT, `server_text_color` TEXT, `server_element_color` TEXT, `server_slogan` TEXT, `server_logo` TEXT, `background_url` TEXT, `end_to_end_encryption` INTEGER, `end_to_end_encryption_keys_exist` INTEGER, `end_to_end_encryption_api_version` TEXT, `activity` INTEGER, `background_default` INTEGER, `background_plain` INTEGER, `richdocument` INTEGER, `richdocument_mimetype_list` TEXT, `richdocument_direct_editing` INTEGER, `richdocument_direct_templates` INTEGER, `richdocument_optional_mimetype_list` TEXT, `sharing_public_ask_for_optional_password` INTEGER, `richdocument_product_name` TEXT, `direct_editing_etag` TEXT, `user_status` INTEGER, `user_status_supports_emoji` INTEGER, `etag` TEXT, `files_locking_version` TEXT, `groupfolders` INTEGER, `drop_account` INTEGER, `security_guard` INTEGER, `forbidden_filename_characters` INTEGER, `forbidden_filenames` INTEGER, `forbidden_filename_extensions` INTEGER, `forbidden_filename_basenames` INTEGER, `files_download_limit` INTEGER, `files_download_limit_default` INTEGER, `recommendation` INTEGER, `notes_folder_path` TEXT, `default_permissions` INTEGER, `user_status_supports_busy` INTEGER, `windows_compatible_filenames` INTEGER, `has_valid_subscription` INTEGER, `client_integration_json` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "assistant", + "columnName": "assistant", + "affinity": "INTEGER" + }, + { + "fieldPath": "accountName", + "columnName": "account", + "affinity": "TEXT" + }, + { + "fieldPath": "versionMajor", + "columnName": "version_mayor", + "affinity": "INTEGER" + }, + { + "fieldPath": "versionMinor", + "columnName": "version_minor", + "affinity": "INTEGER" + }, + { + "fieldPath": "versionMicro", + "columnName": "version_micro", + "affinity": "INTEGER" + }, + { + "fieldPath": "versionString", + "columnName": "version_string", + "affinity": "TEXT" + }, + { + "fieldPath": "versionEditor", + "columnName": "version_edition", + "affinity": "TEXT" + }, + { + "fieldPath": "extendedSupport", + "columnName": "extended_support", + "affinity": "INTEGER" + }, + { + "fieldPath": "corePollinterval", + "columnName": "core_pollinterval", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingApiEnabled", + "columnName": "sharing_api_enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicEnabled", + "columnName": "sharing_public_enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicPasswordEnforced", + "columnName": "sharing_public_password_enforced", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicExpireDateEnabled", + "columnName": "sharing_public_expire_date_enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicExpireDateDays", + "columnName": "sharing_public_expire_date_days", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicExpireDateEnforced", + "columnName": "sharing_public_expire_date_enforced", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicSendMail", + "columnName": "sharing_public_send_mail", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicUpload", + "columnName": "sharing_public_upload", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingUserSendMail", + "columnName": "sharing_user_send_mail", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingResharing", + "columnName": "sharing_resharing", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingFederationOutgoing", + "columnName": "sharing_federation_outgoing", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingFederationIncoming", + "columnName": "sharing_federation_incoming", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesBigfilechunking", + "columnName": "files_bigfilechunking", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesUndelete", + "columnName": "files_undelete", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesVersioning", + "columnName": "files_versioning", + "affinity": "INTEGER" + }, + { + "fieldPath": "externalLinks", + "columnName": "external_links", + "affinity": "INTEGER" + }, + { + "fieldPath": "serverName", + "columnName": "server_name", + "affinity": "TEXT" + }, + { + "fieldPath": "serverColor", + "columnName": "server_color", + "affinity": "TEXT" + }, + { + "fieldPath": "serverTextColor", + "columnName": "server_text_color", + "affinity": "TEXT" + }, + { + "fieldPath": "serverElementColor", + "columnName": "server_element_color", + "affinity": "TEXT" + }, + { + "fieldPath": "serverSlogan", + "columnName": "server_slogan", + "affinity": "TEXT" + }, + { + "fieldPath": "serverLogo", + "columnName": "server_logo", + "affinity": "TEXT" + }, + { + "fieldPath": "serverBackgroundUrl", + "columnName": "background_url", + "affinity": "TEXT" + }, + { + "fieldPath": "endToEndEncryption", + "columnName": "end_to_end_encryption", + "affinity": "INTEGER" + }, + { + "fieldPath": "endToEndEncryptionKeysExist", + "columnName": "end_to_end_encryption_keys_exist", + "affinity": "INTEGER" + }, + { + "fieldPath": "endToEndEncryptionApiVersion", + "columnName": "end_to_end_encryption_api_version", + "affinity": "TEXT" + }, + { + "fieldPath": "activity", + "columnName": "activity", + "affinity": "INTEGER" + }, + { + "fieldPath": "serverBackgroundDefault", + "columnName": "background_default", + "affinity": "INTEGER" + }, + { + "fieldPath": "serverBackgroundPlain", + "columnName": "background_plain", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocument", + "columnName": "richdocument", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentMimetypeList", + "columnName": "richdocument_mimetype_list", + "affinity": "TEXT" + }, + { + "fieldPath": "richdocumentDirectEditing", + "columnName": "richdocument_direct_editing", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentTemplates", + "columnName": "richdocument_direct_templates", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentOptionalMimetypeList", + "columnName": "richdocument_optional_mimetype_list", + "affinity": "TEXT" + }, + { + "fieldPath": "sharingPublicAskForOptionalPassword", + "columnName": "sharing_public_ask_for_optional_password", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentProductName", + "columnName": "richdocument_product_name", + "affinity": "TEXT" + }, + { + "fieldPath": "directEditingEtag", + "columnName": "direct_editing_etag", + "affinity": "TEXT" + }, + { + "fieldPath": "userStatus", + "columnName": "user_status", + "affinity": "INTEGER" + }, + { + "fieldPath": "userStatusSupportsEmoji", + "columnName": "user_status_supports_emoji", + "affinity": "INTEGER" + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT" + }, + { + "fieldPath": "filesLockingVersion", + "columnName": "files_locking_version", + "affinity": "TEXT" + }, + { + "fieldPath": "groupfolders", + "columnName": "groupfolders", + "affinity": "INTEGER" + }, + { + "fieldPath": "dropAccount", + "columnName": "drop_account", + "affinity": "INTEGER" + }, + { + "fieldPath": "securityGuard", + "columnName": "security_guard", + "affinity": "INTEGER" + }, + { + "fieldPath": "forbiddenFileNameCharacters", + "columnName": "forbidden_filename_characters", + "affinity": "INTEGER" + }, + { + "fieldPath": "forbiddenFileNames", + "columnName": "forbidden_filenames", + "affinity": "INTEGER" + }, + { + "fieldPath": "forbiddenFileNameExtensions", + "columnName": "forbidden_filename_extensions", + "affinity": "INTEGER" + }, + { + "fieldPath": "forbiddenFilenameBaseNames", + "columnName": "forbidden_filename_basenames", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesDownloadLimit", + "columnName": "files_download_limit", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesDownloadLimitDefault", + "columnName": "files_download_limit_default", + "affinity": "INTEGER" + }, + { + "fieldPath": "recommendation", + "columnName": "recommendation", + "affinity": "INTEGER" + }, + { + "fieldPath": "notesFolderPath", + "columnName": "notes_folder_path", + "affinity": "TEXT" + }, + { + "fieldPath": "defaultPermissions", + "columnName": "default_permissions", + "affinity": "INTEGER" + }, + { + "fieldPath": "userStatusSupportsBusy", + "columnName": "user_status_supports_busy", + "affinity": "INTEGER" + }, + { + "fieldPath": "isWCFEnabled", + "columnName": "windows_compatible_filenames", + "affinity": "INTEGER" + }, + { + "fieldPath": "hasValidSubscription", + "columnName": "has_valid_subscription", + "affinity": "INTEGER" + }, + { + "fieldPath": "clientIntegrationJson", + "columnName": "client_integration_json", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "external_links", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `icon_url` TEXT, `language` TEXT, `type` INTEGER, `name` TEXT, `url` TEXT, `redirect` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "iconUrl", + "columnName": "icon_url", + "affinity": "TEXT" + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT" + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT" + }, + { + "fieldPath": "redirect", + "columnName": "redirect", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "filelist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `filename` TEXT, `encrypted_filename` TEXT, `path` TEXT, `path_decrypted` TEXT, `parent` INTEGER, `created` INTEGER, `modified` INTEGER, `content_type` TEXT, `content_length` INTEGER, `media_path` TEXT, `file_owner` TEXT, `last_sync_date` INTEGER, `last_sync_date_for_data` INTEGER, `modified_at_last_sync_for_data` INTEGER, `etag` TEXT, `etag_on_server` TEXT, `share_by_link` INTEGER, `permissions` TEXT, `remote_id` TEXT, `local_id` INTEGER NOT NULL DEFAULT -1, `update_thumbnail` INTEGER, `is_downloading` INTEGER, `favorite` INTEGER, `hidden` INTEGER, `is_encrypted` INTEGER, `etag_in_conflict` TEXT, `shared_via_users` INTEGER, `mount_type` INTEGER, `has_preview` INTEGER, `unread_comments_count` INTEGER, `owner_id` TEXT, `owner_display_name` TEXT, `note` TEXT, `sharees` TEXT, `rich_workspace` TEXT, `metadata_size` TEXT, `metadata_live_photo` TEXT, `locked` INTEGER, `lock_type` INTEGER, `lock_owner` TEXT, `lock_owner_display_name` TEXT, `lock_owner_editor` TEXT, `lock_timestamp` INTEGER, `lock_timeout` INTEGER, `lock_token` TEXT, `tags` TEXT, `metadata_gps` TEXT, `e2e_counter` INTEGER, `internal_two_way_sync_timestamp` INTEGER, `internal_two_way_sync_result` TEXT, `uploaded` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "name", + "columnName": "filename", + "affinity": "TEXT" + }, + { + "fieldPath": "encryptedName", + "columnName": "encrypted_filename", + "affinity": "TEXT" + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT" + }, + { + "fieldPath": "pathDecrypted", + "columnName": "path_decrypted", + "affinity": "TEXT" + }, + { + "fieldPath": "parent", + "columnName": "parent", + "affinity": "INTEGER" + }, + { + "fieldPath": "creation", + "columnName": "created", + "affinity": "INTEGER" + }, + { + "fieldPath": "modified", + "columnName": "modified", + "affinity": "INTEGER" + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT" + }, + { + "fieldPath": "contentLength", + "columnName": "content_length", + "affinity": "INTEGER" + }, + { + "fieldPath": "storagePath", + "columnName": "media_path", + "affinity": "TEXT" + }, + { + "fieldPath": "accountOwner", + "columnName": "file_owner", + "affinity": "TEXT" + }, + { + "fieldPath": "lastSyncDate", + "columnName": "last_sync_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastSyncDateForData", + "columnName": "last_sync_date_for_data", + "affinity": "INTEGER" + }, + { + "fieldPath": "modifiedAtLastSyncForData", + "columnName": "modified_at_last_sync_for_data", + "affinity": "INTEGER" + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT" + }, + { + "fieldPath": "etagOnServer", + "columnName": "etag_on_server", + "affinity": "TEXT" + }, + { + "fieldPath": "sharedViaLink", + "columnName": "share_by_link", + "affinity": "INTEGER" + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "TEXT" + }, + { + "fieldPath": "remoteId", + "columnName": "remote_id", + "affinity": "TEXT" + }, + { + "fieldPath": "localId", + "columnName": "local_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "updateThumbnail", + "columnName": "update_thumbnail", + "affinity": "INTEGER" + }, + { + "fieldPath": "isDownloading", + "columnName": "is_downloading", + "affinity": "INTEGER" + }, + { + "fieldPath": "favorite", + "columnName": "favorite", + "affinity": "INTEGER" + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER" + }, + { + "fieldPath": "isEncrypted", + "columnName": "is_encrypted", + "affinity": "INTEGER" + }, + { + "fieldPath": "etagInConflict", + "columnName": "etag_in_conflict", + "affinity": "TEXT" + }, + { + "fieldPath": "sharedWithSharee", + "columnName": "shared_via_users", + "affinity": "INTEGER" + }, + { + "fieldPath": "mountType", + "columnName": "mount_type", + "affinity": "INTEGER" + }, + { + "fieldPath": "hasPreview", + "columnName": "has_preview", + "affinity": "INTEGER" + }, + { + "fieldPath": "unreadCommentsCount", + "columnName": "unread_comments_count", + "affinity": "INTEGER" + }, + { + "fieldPath": "ownerId", + "columnName": "owner_id", + "affinity": "TEXT" + }, + { + "fieldPath": "ownerDisplayName", + "columnName": "owner_display_name", + "affinity": "TEXT" + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT" + }, + { + "fieldPath": "sharees", + "columnName": "sharees", + "affinity": "TEXT" + }, + { + "fieldPath": "richWorkspace", + "columnName": "rich_workspace", + "affinity": "TEXT" + }, + { + "fieldPath": "metadataSize", + "columnName": "metadata_size", + "affinity": "TEXT" + }, + { + "fieldPath": "metadataLivePhoto", + "columnName": "metadata_live_photo", + "affinity": "TEXT" + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockType", + "columnName": "lock_type", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockOwner", + "columnName": "lock_owner", + "affinity": "TEXT" + }, + { + "fieldPath": "lockOwnerDisplayName", + "columnName": "lock_owner_display_name", + "affinity": "TEXT" + }, + { + "fieldPath": "lockOwnerEditor", + "columnName": "lock_owner_editor", + "affinity": "TEXT" + }, + { + "fieldPath": "lockTimestamp", + "columnName": "lock_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockTimeout", + "columnName": "lock_timeout", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockToken", + "columnName": "lock_token", + "affinity": "TEXT" + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT" + }, + { + "fieldPath": "metadataGPS", + "columnName": "metadata_gps", + "affinity": "TEXT" + }, + { + "fieldPath": "e2eCounter", + "columnName": "e2e_counter", + "affinity": "INTEGER" + }, + { + "fieldPath": "internalTwoWaySync", + "columnName": "internal_two_way_sync_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "internalTwoWaySyncResult", + "columnName": "internal_two_way_sync_result", + "affinity": "TEXT" + }, + { + "fieldPath": "uploaded", + "columnName": "uploaded", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "filesystem", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `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, `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": "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, '1c5a77152bf79ee80f9e6eb2677d75a7')" + ] + } +} 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 a5bafa686668..de3d4911a253 100644 --- a/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt +++ b/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt @@ -91,7 +91,8 @@ import com.owncloud.android.db.ProviderMeta AutoMigration(from = 92, to = 93, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class), 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 = 95, to = 96), + AutoMigration(from = 96, to = 97, 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 56f33b18f0bd..a2712c7b04f7 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 @@ -146,5 +146,7 @@ data class CapabilityEntity( @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_WINDOWS_COMPATIBLE_FILENAMES) val isWCFEnabled: Int?, @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_HAS_VALID_SUBSCRIPTION) - val hasValidSubscription: Int? + val hasValidSubscription: Int?, + @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_CLIENT_INTEGRATION_JSON) + val clientIntegrationJson: String? ) diff --git a/app/src/main/java/com/nextcloud/ui/ClientIntegrationScreen.kt b/app/src/main/java/com/nextcloud/ui/ClientIntegrationScreen.kt new file mode 100644 index 000000000000..f7cc6b585097 --- /dev/null +++ b/app/src/main/java/com/nextcloud/ui/ClientIntegrationScreen.kt @@ -0,0 +1,170 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-FileCopyrightText: 2025 Tobias Kaminsky + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.ui + +import android.app.Activity +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import com.nextcloud.android.lib.resources.clientintegration.ClientIntegrationUI +import com.nextcloud.android.lib.resources.clientintegration.Element +import com.nextcloud.android.lib.resources.clientintegration.Layout +import com.nextcloud.android.lib.resources.clientintegration.LayoutButton +import com.nextcloud.android.lib.resources.clientintegration.LayoutOrientation +import com.nextcloud.android.lib.resources.clientintegration.LayoutRow +import com.nextcloud.android.lib.resources.clientintegration.LayoutText +import com.nextcloud.android.lib.resources.clientintegration.LayoutURL +import com.nextcloud.utils.extensions.getActivity +import com.owncloud.android.lib.resources.status.OCCapability +import com.owncloud.android.utils.DisplayUtils + +@Composable +fun ClientIntegrationScreen(clientIntegrationUI: ClientIntegrationUI, baseUrl: String) { + val activity = LocalContext.current.getActivity() + val layoutRows = clientIntegrationUI.root?.rows ?: listOf() + + Scaffold(topBar = { + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { + IconButton(onClick = { activity?.finish() }) { + Icon( + imageVector = Icons.Filled.Close, + contentDescription = "Close" + ) + } + } + }, modifier = Modifier.fillMaxSize()) { + when (clientIntegrationUI.root?.orientation) { + LayoutOrientation.VERTICAL -> { + LazyColumn(modifier = Modifier.padding(it)) { + items(layoutRows) { row -> + LazyRow { + items(row.children) { element -> + DisplayElement(element, baseUrl, activity) + } + } + } + } + } + else -> { + LazyRow(modifier = Modifier.padding(it)) { + items(layoutRows) { row -> + LazyColumn { + items(row.children) { element -> + DisplayElement(element, baseUrl, activity) + } + } + } + } + } + } + } +} + +@Composable +private fun DisplayElement(element: Element, baseUrl: String, activity: Activity?) { + when (element) { + is LayoutButton -> Button(onClick = { }) { + Text(element.label) + } + + is LayoutURL -> TextButton({ + openLink(activity, baseUrl, element.url) + }) { Text(element.text) } + + is LayoutText -> Text(element.text) + } +} + +private fun openLink(activity: Activity?, baseUrl: String, relativeUrl: String) { + activity?.let { + DisplayUtils.startLinkIntent(activity, baseUrl + relativeUrl) + } +} + +@Composable +@Preview +private fun ClientIntegrationScreenPreviewVertical() { + val clientIntegrationUI = ClientIntegrationUI( + OCCapability.CLIENT_INTEGRATION_VERSION, + Layout( + LayoutOrientation.VERTICAL, + mutableListOf( + LayoutRow( + listOf(LayoutButton("Click", "Primary"), LayoutText("123")) + ), + LayoutRow( + listOf(LayoutButton("Click2", "Primary")) + ), + LayoutRow( + listOf(LayoutURL("Analytics report created", "https://nextcloud.com")) + ) + ) + ) + ) + + ClientIntegrationScreen( + clientIntegrationUI, + "http://nextcloud.local" + ) +} + +@Composable +@Preview +private fun ClientIntegrationScreenPreviewHorizontal() { + val clientIntegrationUI = ClientIntegrationUI( + OCCapability.CLIENT_INTEGRATION_VERSION, + Layout( + LayoutOrientation.HORIZONTAL, + mutableListOf( + LayoutRow( + listOf(LayoutButton("Click", "Primary"), LayoutText("123")) + ), + LayoutRow( + listOf(LayoutButton("Click2", "Primary")) + ), + LayoutRow( + listOf(LayoutURL("Analytics report created", "https://nextcloud.com")) + ) + ) + ) + ) + + ClientIntegrationScreen(clientIntegrationUI, "http://nextcloud.local") +} + +@Composable +@Preview +private fun ClientIntegrationScreenPreviewEmpty() { + val clientIntegrationUI = ClientIntegrationUI( + OCCapability.CLIENT_INTEGRATION_VERSION, + Layout( + LayoutOrientation.HORIZONTAL, + emptyList() + ) + ) + + ClientIntegrationScreen(clientIntegrationUI, "http://nextcloud.local") +} diff --git a/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt b/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt index 27b295bac9c3..1428ab363b76 100644 --- a/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt +++ b/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt @@ -9,6 +9,7 @@ package com.nextcloud.ui.composeActivity import android.os.Bundle import android.view.MenuItem +import android.view.View import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -25,6 +26,8 @@ import com.nextcloud.client.assistant.repository.local.AssistantLocalRepositoryI import com.nextcloud.client.assistant.repository.remote.AssistantRemoteRepositoryImpl import com.nextcloud.client.database.NextcloudDatabase import com.nextcloud.common.NextcloudClient +import com.nextcloud.ui.ClientIntegrationScreen +import com.nextcloud.utils.extensions.getParcelableArgument import com.owncloud.android.R import com.owncloud.android.databinding.ActivityComposeBinding import com.owncloud.android.ui.activity.DrawerActivity @@ -35,7 +38,6 @@ class ComposeActivity : DrawerActivity() { companion object { const val DESTINATION = "DESTINATION" - const val TITLE = "TITLE" } override fun onCreate(savedInstanceState: Bundle?) { @@ -43,25 +45,37 @@ class ComposeActivity : DrawerActivity() { binding = ActivityComposeBinding.inflate(layoutInflater) setContentView(binding.root) - val destinationId = intent.getIntExtra(DESTINATION, -1) - val titleId = intent.getIntExtra(TITLE, R.string.empty) - - setupDrawer() + val destination = + intent.getParcelableArgument(DESTINATION, ComposeDestination::class.java) ?: throw IllegalArgumentException( + "destination is not exists" + ) - setupToolbarShowOnlyMenuButtonAndTitle(getString(titleId)) { - openDrawer() - } + setupActivityUIFor(destination) binding.composeView.setContent { MaterialTheme( colorScheme = viewThemeUtils.getColorScheme(this), content = { - Content(ComposeDestination.fromId(destinationId)) + Content(destination) } ) } } + private fun setupActivityUIFor(destination: ComposeDestination) { + if (destination is ComposeDestination.AssistantScreen) { + setupDrawer() + setupToolbarShowOnlyMenuButtonAndTitle(destination.title) { + openDrawer() + } + } else { + setSupportActionBar(null) + findViewById(R.id.appbar)?.let { + it.visibility = View.GONE + } + } + } + override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { android.R.id.home -> { toggleDrawer() @@ -104,6 +118,13 @@ class ComposeActivity : DrawerActivity() { capability = capabilities ) } + + is ComposeDestination.ClientIntegrationScreen -> { + binding.bottomNavigation.visibility = View.GONE + val integrationScreen = (currentScreen as ComposeDestination.ClientIntegrationScreen) + ClientIntegrationScreen(integrationScreen.data, nextcloudClient?.baseUri.toString()) + } + else -> Unit } } diff --git a/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeDestination.kt b/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeDestination.kt index 050c8e5a2848..6564d61cd515 100644 --- a/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeDestination.kt +++ b/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeDestination.kt @@ -7,13 +7,25 @@ */ package com.nextcloud.ui.composeActivity -sealed class ComposeDestination(val id: Int) { - data class AssistantScreen(val sessionId: Long?) : ComposeDestination(0) +import android.content.Context +import android.os.Parcelable +import com.nextcloud.android.lib.resources.clientintegration.ClientIntegrationUI +import com.owncloud.android.R +import kotlinx.parcelize.Parcelize + +@Parcelize +sealed class ComposeDestination(val id: Int) : Parcelable { + @Parcelize + data class AssistantScreen(val title: String, val sessionId: Long?) : ComposeDestination(0) + + @Parcelize + data class ClientIntegrationScreen(val title: String, val data: ClientIntegrationUI) : ComposeDestination(1) companion object { - fun fromId(id: Int): ComposeDestination = when (id) { - 0 -> AssistantScreen(null) - else -> throw IllegalArgumentException("Unknown destination: $id") - } + /** + * Creates a assistant screen without selected chat + */ + fun getAssistantScreen(context: Context): AssistantScreen = + AssistantScreen(context.getString(R.string.assistant_screen_top_bar_title), null) } } diff --git a/app/src/main/java/com/nextcloud/ui/fileactions/ClientIntegration.kt b/app/src/main/java/com/nextcloud/ui/fileactions/ClientIntegration.kt new file mode 100644 index 000000000000..0561a66b5178 --- /dev/null +++ b/app/src/main/java/com/nextcloud/ui/fileactions/ClientIntegration.kt @@ -0,0 +1,231 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.ui.fileactions + +import android.content.Context +import android.content.Intent +import android.graphics.Canvas +import android.graphics.drawable.PictureDrawable +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.graphics.createBitmap +import androidx.core.graphics.drawable.toDrawable +import androidx.core.net.toUri +import androidx.lifecycle.lifecycleScope +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.google.gson.JsonElement +import com.google.gson.JsonParser +import com.google.gson.JsonSyntaxException +import com.google.gson.reflect.TypeToken +import com.nextcloud.android.lib.resources.clientintegration.ClientIntegrationUI +import com.nextcloud.android.lib.resources.clientintegration.Element +import com.nextcloud.android.lib.resources.clientintegration.ElementTypeAdapter +import com.nextcloud.android.lib.resources.clientintegration.Endpoint +import com.nextcloud.android.lib.resources.clientintegration.TooltipResponse +import com.nextcloud.client.account.User +import com.nextcloud.common.JSONRequestBody +import com.nextcloud.operations.GetMethod +import com.nextcloud.operations.PostMethod +import com.nextcloud.ui.composeActivity.ComposeActivity +import com.nextcloud.ui.composeActivity.ComposeDestination +import com.nextcloud.utils.GlideHelper +import com.owncloud.android.R +import com.owncloud.android.databinding.FileActionsBottomSheetBinding +import com.owncloud.android.databinding.FileActionsBottomSheetItemBinding +import com.owncloud.android.lib.common.OwnCloudClientManagerFactory +import com.owncloud.android.lib.ocs.ServerResponse +import com.owncloud.android.lib.resources.status.Method +import com.owncloud.android.utils.DisplayUtils +import com.owncloud.android.utils.theme.ViewThemeUtils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import okhttp3.RequestBody +import org.apache.commons.httpclient.HttpStatus +import java.io.IOException + +class ClientIntegration( + private var sheet: FileActionsBottomSheet, + private var user: User, + private var context: Context +) { + + fun inflateClientIntegrationActionView( + endpoint: Endpoint, + layoutInflater: LayoutInflater, + binding: FileActionsBottomSheetBinding, + viewModel: FileActionsViewModel, + viewThemeUtils: ViewThemeUtils + ): View { + val itemBinding = FileActionsBottomSheetItemBinding.inflate(layoutInflater, binding.fileActionsList, false) + .apply { + root.setOnClickListener { + if (viewModel.uiState.value is FileActionsViewModel.UiState.LoadedForSingleFile) { + val singleFile = (viewModel.uiState.value as FileActionsViewModel.UiState.LoadedForSingleFile) + + val fileId = singleFile.titleFile?.localId.toString() + val filePath = singleFile.titleFile?.remotePath.toString() + + requestClientIntegration(endpoint, fileId, filePath) + } else { + requestClientIntegration(endpoint, "", "") + } + } + text.text = endpoint.name + + if (endpoint.icon != null) { + sheet.lifecycleScope.launch(Dispatchers.IO) { + val client = OwnCloudClientManagerFactory.getDefaultSingleton() + .getNextcloudClientFor(user.toOwnCloudAccount(), context) + + val drawable = + GlideHelper.getDrawable(context, client, client.baseUri.toString() + endpoint.icon) + ?.mutate() + + val px = DisplayUtils.convertDpToPixel( + context.resources.getDimension(R.dimen.iconized_single_line_item_icon_size), + context + ) + val returnedBitmap = + createBitmap(drawable?.intrinsicWidth ?: px, drawable?.intrinsicHeight ?: px) + + val canvas = Canvas(returnedBitmap) + canvas.drawPicture((drawable as PictureDrawable).picture) + + val d = returnedBitmap.toDrawable(context.resources) + + val tintedDrawable = viewThemeUtils.platform.tintDrawable( + context, + d + ) + + withContext(Dispatchers.Main) { + icon.setImageDrawable(tintedDrawable) + } + } + } else { + val tintedDrawable = viewThemeUtils.platform.tintDrawable( + context, + AppCompatResources.getDrawable(context, R.drawable.ic_activity)!! + ) + + icon.setImageDrawable(tintedDrawable) + } + } + return itemBinding.root + } + + private fun requestClientIntegration(endpoint: Endpoint, fileId: String, filePath: String) { + sheet.lifecycleScope.launch(Dispatchers.IO) { + val client = OwnCloudClientManagerFactory.getDefaultSingleton() + .getNextcloudClientFor(user.toOwnCloudAccount(), context) + + // construct url + var url = (client.baseUri.toString() + endpoint.url).toUri() + .buildUpon() + .appendQueryParameter("format", "json") + .build() + .toString() + + // Always replace known placeholder in url + url = url.replace("{filePath}", filePath, false) + url = url.replace("{fileId}", fileId, false) + + val method = when (endpoint.method) { + Method.POST -> { + val requestBody = if (endpoint.params?.isNotEmpty() == true) { + val jsonRequestBody = JSONRequestBody() + endpoint.params!!.forEach { + when (it.value) { + "{filePath}" -> jsonRequestBody.put(it.key, filePath) + "{fileId}" -> jsonRequestBody.put(it.key, fileId) + } + } + + jsonRequestBody.get() + } else { + RequestBody.EMPTY + } + + PostMethod(url, true, requestBody) + } + + else -> GetMethod(url, true) + } + + val result = try { + client.execute(method) + } catch (_: IOException) { + showMessage(context.resources.getString(R.string.failed_to_start_action)) + } + val response = method.getResponseBodyAsString() + + try { + val output = parseClientIntegrationResult(response) + if (output.root != null && output.root?.rows != null) { + startClientIntegration(endpoint, output) + } else { + val tooltipResponse = parseTooltipResult(response) + showMessage(tooltipResponse.tooltip) + } + } catch (_: JsonSyntaxException) { + if (result == HttpStatus.SC_OK) { + showMessage(context.resources.getString(R.string.action_triggered)) + } else { + showMessage(context.resources.getString(R.string.failed_to_start_action)) + } + } + sheet.dismiss() + } + } + + private suspend fun showMessage(message: String) = withContext(Dispatchers.Main) { + DisplayUtils.showSnackMessage(sheet.requireActivity(), message) + } + + private fun parseTooltipResult(response: String?): TooltipResponse { + val element: JsonElement = JsonParser.parseString(response) + return Gson() + .fromJson(element, object : TypeToken>() {}) + .ocs + .data + } + + private fun startClientIntegration(endpoint: Endpoint, data: ClientIntegrationUI) { + sheet.lifecycleScope.launch(Dispatchers.IO) { + val integrationScreen = ComposeDestination.ClientIntegrationScreen(endpoint.name, data) + + val bundle = Bundle().apply { + putParcelable(ComposeActivity.DESTINATION, integrationScreen) + } + + val composeActivity = Intent(context, ComposeActivity::class.java).apply { + putExtras(bundle) + } + + context.startActivity(composeActivity) + sheet.dismiss() + } + } + + private fun parseClientIntegrationResult(response: String?): ClientIntegrationUI { + val gson = + GsonBuilder() + .registerTypeHierarchyAdapter(Element::class.java, ElementTypeAdapter()) + .create() + + val element: JsonElement = JsonParser.parseString(response) + return gson + .fromJson(element, object : TypeToken>() {}) + .ocs + .data + } +} diff --git a/app/src/main/java/com/nextcloud/ui/fileactions/FileAction.kt b/app/src/main/java/com/nextcloud/ui/fileactions/FileAction.kt index 6e265a2a7cd8..43f1edc09109 100644 --- a/app/src/main/java/com/nextcloud/ui/fileactions/FileAction.kt +++ b/app/src/main/java/com/nextcloud/ui/fileactions/FileAction.kt @@ -64,6 +64,8 @@ enum class FileAction( // Retry for offline operation RETRY(R.id.action_retry, R.string.retry, R.drawable.ic_retry); + constructor(id: Int, title: Int) : this(id, title, null) + companion object { /** * All file actions, in the order they should be displayed diff --git a/app/src/main/java/com/nextcloud/ui/fileactions/FileActionsBottomSheet.kt b/app/src/main/java/com/nextcloud/ui/fileactions/FileActionsBottomSheet.kt index 7a67b8345b1c..62467f7d9b0f 100644 --- a/app/src/main/java/com/nextcloud/ui/fileactions/FileActionsBottomSheet.kt +++ b/app/src/main/java/com/nextcloud/ui/fileactions/FileActionsBottomSheet.kt @@ -29,6 +29,7 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.nextcloud.android.common.ui.theme.utils.ColorRole +import com.nextcloud.android.lib.resources.clientintegration.Endpoint import com.nextcloud.client.account.CurrentAccountProvider import com.nextcloud.client.di.Injectable import com.nextcloud.client.di.ViewModelFactory @@ -77,6 +78,10 @@ class FileActionsBottomSheet : private val thumbnailAsyncTasks = mutableListOf() + private var endpoints: List? = mutableListOf() + + private lateinit var clientIntegration: ClientIntegration + fun interface ResultListener { fun onResult(@IdRes actionId: Int) } @@ -93,12 +98,16 @@ class FileActionsBottomSheet : viewModel.load(requireArguments(), componentsGetter) + endpoints = arguments?.getParcelableArrayList(FileActionsViewModel.ARG_ENDPOINTS) + val bottomSheetDialog = dialog as BottomSheetDialog bottomSheetDialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED bottomSheetDialog.behavior.skipCollapsed = true viewThemeUtils.platform.colorViewBackground(binding.bottomSheet, ColorRole.SURFACE) + clientIntegration = ClientIntegration(this, currentUserProvider.user, requireContext()) + return binding.root } @@ -199,6 +208,20 @@ class FileActionsBottomSheet : val view = inflateActionView(action) binding.fileActionsList.addView(view) } + + // add client integration + if (endpoints != null) { + for (val e in endpoints) { + val ui = clientIntegration.inflateClientIntegrationActionView( + e, + layoutInflater, + binding, + viewModel, + viewThemeUtils + ) + binding.fileActionsList.addView(ui) + } + } } } @@ -322,7 +345,7 @@ class FileActionsBottomSheet : isOverflow: Boolean, @IdRes additionalToHide: List? = null - ): FileActionsBottomSheet = newInstance(1, listOf(file), isOverflow, additionalToHide, true) + ): FileActionsBottomSheet = newInstance(1, listOf(file), isOverflow, additionalToHide, true, emptyList()) @JvmStatic @JvmOverloads @@ -332,13 +355,15 @@ class FileActionsBottomSheet : isOverflow: Boolean, @IdRes additionalToHide: List? = null, - inSingleFileFragment: Boolean = false + inSingleFileFragment: Boolean = false, + endpoints: List ): FileActionsBottomSheet = FileActionsBottomSheet().apply { val argsBundle = bundleOf( FileActionsViewModel.ARG_ALL_FILES_COUNT to numberOfAllFiles, FileActionsViewModel.ARG_FILES to ArrayList(files), FileActionsViewModel.ARG_IS_OVERFLOW to isOverflow, - FileActionsViewModel.ARG_IN_SINGLE_FILE_FRAGMENT to inSingleFileFragment + FileActionsViewModel.ARG_IN_SINGLE_FILE_FRAGMENT to inSingleFileFragment, + FileActionsViewModel.ARG_ENDPOINTS to endpoints ) additionalToHide?.let { argsBundle.putIntArray(FileActionsViewModel.ARG_ADDITIONAL_FILTER, additionalToHide.toIntArray()) diff --git a/app/src/main/java/com/nextcloud/ui/fileactions/FileActionsViewModel.kt b/app/src/main/java/com/nextcloud/ui/fileactions/FileActionsViewModel.kt index f42015b99518..62b310b010c2 100644 --- a/app/src/main/java/com/nextcloud/ui/fileactions/FileActionsViewModel.kt +++ b/app/src/main/java/com/nextcloud/ui/fileactions/FileActionsViewModel.kt @@ -140,6 +140,7 @@ class FileActionsViewModel @Inject constructor( const val ARG_IS_OVERFLOW = "OVERFLOW" const val ARG_ADDITIONAL_FILTER = "ADDITIONAL_FILTER" const val ARG_IN_SINGLE_FILE_FRAGMENT = "IN_SINGLE_FILE_FRAGMENT" + const val ARG_ENDPOINTS = "ENDPOINTS" private val TAG = FileActionsViewModel::class.simpleName!! } 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 0b0bba6c6812..497776f6babf 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java @@ -2318,6 +2318,8 @@ private ContentValues createContentValues(String accountName, OCCapability capab contentValues.put(ProviderTableMeta.CAPABILITIES_HAS_VALID_SUBSCRIPTION, capability.getHasValidSubscription().getValue()); + contentValues.put(ProviderTableMeta.CAPABILITIES_CLIENT_INTEGRATION_JSON, capability.getClientIntegrationJson()); + return contentValues; } @@ -2503,6 +2505,8 @@ private OCCapability createCapabilityInstance(Cursor cursor) { capability.setDefaultPermissions(getInt(cursor, ProviderTableMeta.CAPABILITIES_DEFAULT_PERMISSIONS)); capability.setHasValidSubscription(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_HAS_VALID_SUBSCRIPTION)); + + capability.setClientIntegrationJson(getString(cursor, ProviderTableMeta.CAPABILITIES_CLIENT_INTEGRATION_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 46905d69231d..6168709a8b08 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 = 96; + public static final int DB_VERSION = 97; private ProviderMeta() { // No instance @@ -292,6 +292,7 @@ static public class ProviderTableMeta implements BaseColumns { public static final String CAPABILITIES_NOTES_FOLDER_PATH = "notes_folder_path"; public static final String CAPABILITIES_DEFAULT_PERMISSIONS = "default_permissions"; public static final String CAPABILITIES_HAS_VALID_SUBSCRIPTION = "has_valid_subscription"; + public static final String CAPABILITIES_CLIENT_INTEGRATION_JSON = "client_integration_json"; //Columns of Uploads table public static final String UPLOADS_LOCAL_PATH = "local_path"; diff --git a/app/src/main/java/com/owncloud/android/operations/GetCapabilitiesOperation.java b/app/src/main/java/com/owncloud/android/operations/GetCapabilitiesOperation.java index 9e86d161a6d0..e895f3b36aa6 100644 --- a/app/src/main/java/com/owncloud/android/operations/GetCapabilitiesOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/GetCapabilitiesOperation.java @@ -36,7 +36,7 @@ protected RemoteOperationResult run(OwnCloudClient client) { currentCapability = storageManager.getCapability(storageManager.getUser().getAccountName()); } - RemoteOperationResult result = new GetCapabilitiesRemoteOperation(currentCapability).execute(client); + RemoteOperationResult result = new GetCapabilitiesRemoteOperation(null).execute(client); if (result.isSuccess() && result.getData() != null && result.getData().size() > 0) { diff --git a/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java index f26047d581a3..7a297de12d4c 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java @@ -431,7 +431,7 @@ private void showTopBanner(ConstraintLayout banner) { moreView.setOnClickListener(v -> LinkHelper.INSTANCE.openAppStore("Nextcloud", true, this)); assistantView.setOnClickListener(v -> { DrawerActivity.menuItemId = Menu.NONE; - startComposeActivity(new ComposeDestination.AssistantScreen(null), R.string.assistant_screen_top_bar_title); + startAssistantScreen(); }); if (getCapabilities() != null && getCapabilities().getAssistant().isTrue()) { assistantView.setVisibility(View.VISIBLE); @@ -583,7 +583,7 @@ private void onNavigationItemClicked(final MenuItem menuItem) { startRecentlyModifiedSearch(menuItem); } else if (itemId == R.id.nav_assistant) { resetOnlyPersonalAndOnDevice(); - startComposeActivity(new ComposeDestination.AssistantScreen(null), R.string.assistant_screen_top_bar_title); + startAssistantScreen(); } else if (itemId == R.id.nav_groupfolders) { resetOnlyPersonalAndOnDevice(); Intent intent = new Intent(getApplicationContext(), FileDisplayActivity.class); @@ -621,7 +621,7 @@ private void handleBottomNavigationViewClicks() { } else if (menuItemId == R.id.nav_favorites) { openFavoritesTab(); } else if (menuItemId == R.id.nav_assistant && !(this instanceof ComposeActivity)) { - startComposeActivity(new ComposeDestination.AssistantScreen(null), R.string.assistant_screen_top_bar_title); + startAssistantScreen(); } else if (menuItemId == R.id.nav_gallery) { openMediaTab(menuItem.getItemId()); } @@ -648,10 +648,12 @@ private void resetFileDepthAndConfigureMenuItem() { } } - private void startComposeActivity(ComposeDestination destination, int titleId) { + private void startAssistantScreen() { + final var destination = ComposeDestination.Companion.getAssistantScreen(this); Intent composeActivity = new Intent(getApplicationContext(), ComposeActivity.class); - composeActivity.putExtra(ComposeActivity.DESTINATION, destination.getId()); - composeActivity.putExtra(ComposeActivity.TITLE, titleId); + final Bundle bundle = new Bundle(); + bundle.putParcelable(ComposeActivity.DESTINATION, destination); + composeActivity.putExtras(bundle); startActivity(composeActivity); } 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 981a234510b0..ae81d56b0427 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 @@ -38,6 +38,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.snackbar.Snackbar; +import com.nextcloud.android.lib.resources.clientintegration.Endpoint; import com.nextcloud.android.lib.resources.files.ToggleFileLockRemoteOperation; import com.nextcloud.client.account.User; import com.nextcloud.client.device.DeviceInfo; @@ -79,6 +80,7 @@ import com.owncloud.android.lib.resources.files.ToggleFavoriteRemoteOperation; import com.owncloud.android.lib.resources.status.E2EVersion; import com.owncloud.android.lib.resources.status.OCCapability; +import com.owncloud.android.lib.resources.status.Type; import com.owncloud.android.ui.activity.DrawerActivity; import com.owncloud.android.ui.activity.FileActivity; import com.owncloud.android.ui.activity.FileDisplayActivity; @@ -643,8 +645,11 @@ public void onOverflowIconClicked(OCFile file, View view) { public void openActionsMenu(final int filesCount, final Set checkedFiles, final boolean isOverflow) { throttler.run("overflowClick", () -> { final var actionsToHide = FileAction.Companion.getFileListActionsToHide(checkedFiles); + + List endpoints = getCapabilities().getClientIntegrationEndpoints(Type.CONTEXT_MENU, checkedFiles.iterator().next().getMimeType()); + final var childFragmentManager = getChildFragmentManager(); - final var actionBottomSheet = FileActionsBottomSheet.newInstance(filesCount, checkedFiles, isOverflow, actionsToHide) + final var actionBottomSheet = FileActionsBottomSheet.newInstance(filesCount, checkedFiles, isOverflow, actionsToHide, endpoints) .setResultListener(childFragmentManager, this, (id) -> onFileActionChosen(id, checkedFiles)); if (FragmentExtensionsKt.isDialogFragmentReady(this)) { diff --git a/app/src/main/res/layout/file_actions_bottom_sheet_item.xml b/app/src/main/res/layout/file_actions_bottom_sheet_item.xml index 77b245fee53b..b2a7a30738ee 100644 --- a/app/src/main/res/layout/file_actions_bottom_sheet_item.xml +++ b/app/src/main/res/layout/file_actions_bottom_sheet_item.xml @@ -7,7 +7,6 @@ ~ SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only --> Failed to create conflict dialog Cannot open file chooser + Failed to start action! + Action triggered diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 827abeebdd08..4e7d3a791625 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ androidCommonLibraryVersion = "0.30.0" androidGifDrawableVersion = "1.2.29" androidImageCropperVersion = "4.7.0" -androidLibraryVersion = "827db94ca661d39ca7fae5c608eab1282b629b84" +androidLibraryVersion = "16fc939b86" androidPluginVersion = '8.13.2' androidsvgVersion = "1.4" androidxMediaVersion = "1.5.1" diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index c9b892b9944d..4230381247dd 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -18939,6 +18939,14 @@ + + + + + + + + @@ -18947,6 +18955,14 @@ + + + + + + + + @@ -18987,6 +19003,14 @@ + + + + + + + + @@ -19155,6 +19179,14 @@ + + + + + + + + @@ -19531,6 +19563,14 @@ + + + + + + + + @@ -19760,6 +19800,14 @@ + + + + + + + +