diff --git a/app/schemas/com.nextcloud.client.database.NextcloudDatabase/76.json b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/76.json new file mode 100644 index 0000000000..b907f637f9 --- /dev/null +++ b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/76.json @@ -0,0 +1,1179 @@ +{ + "formatVersion": 1, + "database": { + "version": 76, + "identityHash": "0d639ab041aa87e6c2ef9504395545f7", + "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", + "notNull": false + }, + { + "fieldPath": "cloudId", + "columnName": "cloud_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "capabilities", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `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, `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)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "accountName", + "columnName": "account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "versionMajor", + "columnName": "version_mayor", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionMinor", + "columnName": "version_minor", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionMicro", + "columnName": "version_micro", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionString", + "columnName": "version_string", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "versionEditor", + "columnName": "version_edition", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "extendedSupport", + "columnName": "extended_support", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "corePollinterval", + "columnName": "core_pollinterval", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingApiEnabled", + "columnName": "sharing_api_enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicEnabled", + "columnName": "sharing_public_enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicPasswordEnforced", + "columnName": "sharing_public_password_enforced", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicExpireDateEnabled", + "columnName": "sharing_public_expire_date_enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicExpireDateDays", + "columnName": "sharing_public_expire_date_days", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicExpireDateEnforced", + "columnName": "sharing_public_expire_date_enforced", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicSendMail", + "columnName": "sharing_public_send_mail", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicUpload", + "columnName": "sharing_public_upload", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingUserSendMail", + "columnName": "sharing_user_send_mail", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingResharing", + "columnName": "sharing_resharing", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingFederationOutgoing", + "columnName": "sharing_federation_outgoing", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingFederationIncoming", + "columnName": "sharing_federation_incoming", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filesBigfilechunking", + "columnName": "files_bigfilechunking", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filesUndelete", + "columnName": "files_undelete", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filesVersioning", + "columnName": "files_versioning", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "externalLinks", + "columnName": "external_links", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "serverName", + "columnName": "server_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverColor", + "columnName": "server_color", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverTextColor", + "columnName": "server_text_color", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverElementColor", + "columnName": "server_element_color", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverSlogan", + "columnName": "server_slogan", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverLogo", + "columnName": "server_logo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverBackgroundUrl", + "columnName": "background_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endToEndEncryption", + "columnName": "end_to_end_encryption", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "endToEndEncryptionKeysExist", + "columnName": "end_to_end_encryption_keys_exist", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "activity", + "columnName": "activity", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "serverBackgroundDefault", + "columnName": "background_default", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "serverBackgroundPlain", + "columnName": "background_plain", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocument", + "columnName": "richdocument", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocumentMimetypeList", + "columnName": "richdocument_mimetype_list", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "richdocumentDirectEditing", + "columnName": "richdocument_direct_editing", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocumentTemplates", + "columnName": "richdocument_direct_templates", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocumentOptionalMimetypeList", + "columnName": "richdocument_optional_mimetype_list", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharingPublicAskForOptionalPassword", + "columnName": "sharing_public_ask_for_optional_password", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocumentProductName", + "columnName": "richdocument_product_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "directEditingEtag", + "columnName": "direct_editing_etag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userStatus", + "columnName": "user_status", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userStatusSupportsEmoji", + "columnName": "user_status_supports_emoji", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filesLockingVersion", + "columnName": "files_locking_version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "groupfolders", + "columnName": "groupfolders", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "dropAccount", + "columnName": "drop_account", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "securityGuard", + "columnName": "security_guard", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "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", + "notNull": false + }, + { + "fieldPath": "iconUrl", + "columnName": "icon_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "redirect", + "columnName": "redirect", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "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)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "filename", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "encryptedName", + "columnName": "encrypted_filename", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pathDecrypted", + "columnName": "path_decrypted", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parent", + "columnName": "parent", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "creation", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "modified", + "columnName": "modified", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentLength", + "columnName": "content_length", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "storagePath", + "columnName": "media_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "accountOwner", + "columnName": "file_owner", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastSyncDate", + "columnName": "last_sync_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastSyncDateForData", + "columnName": "last_sync_date_for_data", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "modifiedAtLastSyncForData", + "columnName": "modified_at_last_sync_for_data", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "etagOnServer", + "columnName": "etag_on_server", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharedViaLink", + "columnName": "share_by_link", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remoteId", + "columnName": "remote_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "localId", + "columnName": "local_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "updateThumbnail", + "columnName": "update_thumbnail", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isDownloading", + "columnName": "is_downloading", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "favorite", + "columnName": "favorite", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isEncrypted", + "columnName": "is_encrypted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "etagInConflict", + "columnName": "etag_in_conflict", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharedWithSharee", + "columnName": "shared_via_users", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mountType", + "columnName": "mount_type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hasPreview", + "columnName": "has_preview", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "unreadCommentsCount", + "columnName": "unread_comments_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "ownerId", + "columnName": "owner_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ownerDisplayName", + "columnName": "owner_display_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharees", + "columnName": "sharees", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "richWorkspace", + "columnName": "rich_workspace", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "metadataSize", + "columnName": "metadata_size", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "metadataLivePhoto", + "columnName": "metadata_live_photo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockType", + "columnName": "lock_type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockOwner", + "columnName": "lock_owner", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockOwnerDisplayName", + "columnName": "lock_owner_display_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockOwnerEditor", + "columnName": "lock_owner_editor", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockTimestamp", + "columnName": "lock_timestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockTimeout", + "columnName": "lock_timeout", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockToken", + "columnName": "lock_token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "metadataGPS", + "columnName": "metadata_gps", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "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", + "notNull": false + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileIsFolder", + "columnName": "is_folder", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "fileFoundRecently", + "columnName": "found_at", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "fileSentForUpload", + "columnName": "upload_triggered", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "syncedFolderId", + "columnName": "syncedfolder_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "crc32", + "columnName": "crc32", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileModified", + "columnName": "modified_at", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "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` INTEGER, `id_remote_shared` INTEGER, `owner_share` TEXT, `is_password_protected` INTEGER, `note` TEXT, `hide_download` INTEGER, `share_link` TEXT, `share_label` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "fileSource", + "columnName": "file_source", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "itemSource", + "columnName": "item_source", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "shareType", + "columnName": "share_type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "shareWith", + "columnName": "shate_with", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharedDate", + "columnName": "shared_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expirationDate", + "columnName": "expiration_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shareWithDisplayName", + "columnName": "shared_with_display_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDirectory", + "columnName": "is_directory", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "idRemoteShared", + "columnName": "id_remote_shared", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "accountOwner", + "columnName": "owner_share", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isPasswordProtected", + "columnName": "is_password_protected", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hideDownload", + "columnName": "hide_download", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "shareLink", + "columnName": "share_link", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shareLabel", + "columnName": "share_label", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "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)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wifiOnly", + "columnName": "wifi_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "chargingOnly", + "columnName": "charging_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "existing", + "columnName": "existing", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enabledTimestampMs", + "columnName": "enabled_timestamp_ms", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "subfolderByDate", + "columnName": "subfolder_by_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uploadAction", + "columnName": "upload_option", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "nameCollisionPolicy", + "columnName": "name_collision_policy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "subFolderRule", + "columnName": "sub_folder_rule", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "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", + "notNull": false + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "accountName", + "columnName": "account_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileSize", + "columnName": "file_size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localBehaviour", + "columnName": "local_behaviour", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "uploadTime", + "columnName": "upload_time", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "nameCollisionPolicy", + "columnName": "name_collision_policy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isCreateRemoteFolder", + "columnName": "is_create_remote_folder", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "uploadEndTimestamp", + "columnName": "upload_end_timestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastResult", + "columnName": "last_result", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isWhileChargingOnly", + "columnName": "is_while_charging_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isWifiOnly", + "columnName": "is_wifi_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "createdBy", + "columnName": "created_by", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "folderUnlockToken", + "columnName": "folder_unlock_token", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "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", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ocFileId", + "columnName": "ocfile_id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "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, '0d639ab041aa87e6c2ef9504395545f7')" + ] + } +} diff --git a/app/src/androidTest/java/com/owncloud/android/datamodel/ArbitraryDataProviderIT.kt b/app/src/androidTest/java/com/owncloud/android/datamodel/ArbitraryDataProviderIT.kt index dad2be82ba..76c174e66a 100644 --- a/app/src/androidTest/java/com/owncloud/android/datamodel/ArbitraryDataProviderIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/datamodel/ArbitraryDataProviderIT.kt @@ -75,4 +75,24 @@ class ArbitraryDataProviderIT : AbstractIT() { arbitraryDataProvider.storeOrUpdateKeyValue(user.accountName, key, value.toString()) assertEquals(value, arbitraryDataProvider.getIntegerValue(user.accountName, key)) } + + @Test + fun testIncrement() { + val key = "INCREMENT" + + // key does not exist + assertEquals(-1, arbitraryDataProvider.getIntegerValue(user.accountName, key)) + + // increment -> 1 + arbitraryDataProvider.incrementValue(user.accountName, key) + assertEquals(1, arbitraryDataProvider.getIntegerValue(user.accountName, key)) + + // increment -> 2 + arbitraryDataProvider.incrementValue(user.accountName, key) + assertEquals(2, arbitraryDataProvider.getIntegerValue(user.accountName, key)) + + // delete + arbitraryDataProvider.deleteKeyForAccount(user.accountName, key) + assertEquals(-1, arbitraryDataProvider.getIntegerValue(user.accountName, key)) + } } diff --git a/app/src/androidTest/java/com/owncloud/android/util/EncryptionTestIT.java b/app/src/androidTest/java/com/owncloud/android/util/EncryptionTestIT.java index a31944bf0e..b35ec8ab0d 100644 --- a/app/src/androidTest/java/com/owncloud/android/util/EncryptionTestIT.java +++ b/app/src/androidTest/java/com/owncloud/android/util/EncryptionTestIT.java @@ -765,7 +765,12 @@ public class EncryptionTestIT extends AbstractIT { // verify authentication tag assertTrue(Arrays.equals(expectedAuthTag, authenticationTag)); - byte[] decryptedBytes = decryptFile(encryptedTempFile, key, iv, authenticationTag); + byte[] decryptedBytes = decryptFile(encryptedTempFile, + key, + iv, + authenticationTag, + new ArbitraryDataProviderImpl(targetContext), + user); File decryptedFile = File.createTempFile("file", "dec"); FileOutputStream fileOutputStream1 = new FileOutputStream(decryptedFile); 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 d0682d9547..45b3da6464 100644 --- a/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt +++ b/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt @@ -68,7 +68,8 @@ import com.owncloud.android.db.ProviderMeta AutoMigration(from = 71, to = 72), AutoMigration(from = 72, to = 73), AutoMigration(from = 73, to = 74, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class), - AutoMigration(from = 74, to = 75) + AutoMigration(from = 74, to = 75), + AutoMigration(from = 75, to = 76) ], exportSchema = true ) diff --git a/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt b/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt index 21a7e34d70..c895dad97e 100644 --- a/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt +++ b/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt @@ -61,4 +61,7 @@ interface FileDao { @Query("SELECT * FROM filelist WHERE path LIKE :pathPattern AND file_owner = :fileOwner ORDER BY path ASC") fun getFolderWithDescendants(pathPattern: String, fileOwner: String): List + + @Query("SELECT * FROM filelist where file_owner = :fileOwner AND etag_in_conflict IS NOT NULL") + fun getFilesWithSyncConflict(fileOwner: String): List } 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 910fba0517..20f9cb9571 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 @@ -131,5 +131,7 @@ data class CapabilityEntity( @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_GROUPFOLDERS) val groupfolders: Int?, @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_DROP_ACCOUNT) - val dropAccount: Int? + val dropAccount: Int?, + @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_SECURITY_GUARD) + val securityGuard: Int? ) diff --git a/app/src/main/java/com/nextcloud/client/di/AppModule.java b/app/src/main/java/com/nextcloud/client/di/AppModule.java index a6c2e67e1a..d7d32b25d0 100644 --- a/app/src/main/java/com/nextcloud/client/di/AppModule.java +++ b/app/src/main/java/com/nextcloud/client/di/AppModule.java @@ -145,8 +145,8 @@ class AppModule { } @Provides - UploadsStorageManager uploadsStorageManager(Context context, - CurrentAccountProvider currentAccountProvider) { + UploadsStorageManager uploadsStorageManager(CurrentAccountProvider currentAccountProvider, + Context context) { return new UploadsStorageManager(currentAccountProvider, context.getContentResolver()); } diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt index 50deb4beb9..1fb333f5ce 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt @@ -62,7 +62,7 @@ class BackgroundJobFactory @Inject constructor( private val deviceInfo: DeviceInfo, private val accountManager: UserAccountManager, private val resources: Resources, - private val dataProvider: ArbitraryDataProvider, + private val arbitraryDataProvider: ArbitraryDataProvider, private val uploadsStorageManager: UploadsStorageManager, private val connectivityService: ConnectivityService, private val notificationManager: NotificationManager, @@ -103,6 +103,7 @@ class BackgroundJobFactory @Inject constructor( FilesExportWork::class -> createFilesExportWork(context, workerParameters) FilesUploadWorker::class -> createFilesUploadWorker(context, workerParameters) GeneratePdfFromImagesWork::class -> createPDFGenerateWork(context, workerParameters) + HealthStatusWork::class -> createHealthStatusWork(context, workerParameters) else -> null // caller falls back to default factory } } @@ -139,7 +140,7 @@ class BackgroundJobFactory @Inject constructor( context, params, resources, - dataProvider, + arbitraryDataProvider, contentResolver, accountManager ) @@ -260,4 +261,13 @@ class BackgroundJobFactory @Inject constructor( params = params ) } + + private fun createHealthStatusWork(context: Context, params: WorkerParameters): HealthStatusWork { + return HealthStatusWork( + context, + params, + accountManager, + arbitraryDataProvider + ) + } } diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt index d42b0568b5..cc8bc6a52f 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt @@ -147,4 +147,6 @@ interface BackgroundJobManager { fun pruneJobs() fun cancelAllJobs() + fun schedulePeriodicHealthStatus() + fun startHealthStatus() } diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt index 9da528a7fc..7e954a0b29 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt @@ -82,6 +82,8 @@ internal class BackgroundJobManagerImpl( const val JOB_PDF_GENERATION = "pdf_generation" const val JOB_IMMEDIATE_CALENDAR_BACKUP = "immediate_calendar_backup" const val JOB_IMMEDIATE_FILES_EXPORT = "immediate_files_export" + const val JOB_PERIODIC_HEALTH_STATUS = "periodic_health_status" + const val JOB_IMMEDIATE_HEALTH_STATUS = "immediate_health_status" const val JOB_TEST = "test_job" @@ -507,4 +509,25 @@ internal class BackgroundJobManagerImpl( override fun cancelAllJobs() { workManager.cancelAllWorkByTag(TAG_ALL) } + + override fun schedulePeriodicHealthStatus() { + val request = periodicRequestBuilder( + jobClass = HealthStatusWork::class, + jobName = JOB_PERIODIC_HEALTH_STATUS, + intervalMins = PERIODIC_BACKUP_INTERVAL_MINUTES + ).build() + + workManager.enqueueUniquePeriodicWork(JOB_PERIODIC_HEALTH_STATUS, ExistingPeriodicWorkPolicy.KEEP, request) + } + + override fun startHealthStatus() { + val request = oneTimeRequestBuilder(HealthStatusWork::class, JOB_IMMEDIATE_HEALTH_STATUS) + .build() + + workManager.enqueueUniqueWork( + JOB_IMMEDIATE_HEALTH_STATUS, + ExistingWorkPolicy.KEEP, + request + ) + } } diff --git a/app/src/main/java/com/nextcloud/client/jobs/HealthStatusWork.kt b/app/src/main/java/com/nextcloud/client/jobs/HealthStatusWork.kt new file mode 100644 index 0000000000..ba2945dc71 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/jobs/HealthStatusWork.kt @@ -0,0 +1,131 @@ +/* + * + * Nextcloud Android client application + * + * @author Tobias Kaminsky + * Copyright (C) 2023 Tobias Kaminsky + * Copyright (C) 2023 Nextcloud GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.client.jobs + +import android.content.Context +import androidx.work.Worker +import androidx.work.WorkerParameters +import com.nextcloud.client.account.User +import com.nextcloud.client.account.UserAccountManager +import com.owncloud.android.datamodel.ArbitraryDataProvider +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.datamodel.UploadsStorageManager +import com.owncloud.android.db.UploadResult +import com.owncloud.android.lib.common.OwnCloudClientManagerFactory +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.status.Problem +import com.owncloud.android.lib.resources.status.SendClientDiagnosticRemoteOperation +import com.owncloud.android.utils.EncryptionUtils +import com.owncloud.android.utils.theme.CapabilityUtils + +class HealthStatusWork( + private val context: Context, + params: WorkerParameters, + private val userAccountManager: UserAccountManager, + private val arbitraryDataProvider: ArbitraryDataProvider +) : Worker(context, params) { + override fun doWork(): Result { + for (user in userAccountManager.allUsers) { + // only if security guard is enabled + if (!CapabilityUtils.getCapability(user, context).securityGuard.isTrue) { + continue + } + + val syncConflicts = collectSyncConflicts(user) + + val problems = mutableListOf().apply { + addAll( + collectUploadProblems( + user, + listOf( + UploadResult.CREDENTIAL_ERROR, + UploadResult.CANNOT_CREATE_FILE, + UploadResult.FOLDER_ERROR, + UploadResult.SERVICE_INTERRUPTED + ) + ) + ) + } + + val virusDetected = collectUploadProblems(user, listOf(UploadResult.VIRUS_DETECTED)).firstOrNull() + + val e2eErrors = EncryptionUtils.readE2eError(arbitraryDataProvider, user) + + val nextcloudClient = OwnCloudClientManagerFactory.getDefaultSingleton() + .getNextcloudClientFor(user.toOwnCloudAccount(), context) + val result = + SendClientDiagnosticRemoteOperation( + syncConflicts, + problems, + virusDetected, + e2eErrors + ).execute( + nextcloudClient + ) + + if (!result.isSuccess) { + if (result.exception == null) { + Log_OC.e(TAG, "Update client health NOT successful!") + } else { + Log_OC.e(TAG, "Update client health NOT successful!", result.exception) + } + } + } + + return Result.success() + } + + private fun collectSyncConflicts(user: User): Problem? { + val fileDataStorageManager = FileDataStorageManager(user, context.contentResolver) + + val conflicts = fileDataStorageManager.getFilesWithSyncConflict(user) + + return if (conflicts.isEmpty()) { + null + } else { + Problem("sync_conflicts", conflicts.size, conflicts.minOf { it.lastSyncDateForData }) + } + } + + private fun collectUploadProblems(user: User, errorCodes: List): List { + val uploadsStorageManager = UploadsStorageManager(userAccountManager, context.contentResolver) + + val problems = uploadsStorageManager + .getUploadsForAccount(user.accountName) + .filter { + errorCodes.contains(it.lastResult) + }.groupBy { it.lastResult } + + return if (problems.isEmpty()) { + emptyList() + } else { + return problems.map { problem -> + Problem(problem.key.toString(), problem.value.size, problem.value.minOf { it.uploadEndTimestamp }) + } + } + } + + companion object { + private const val TAG = "Health Status" + } +} diff --git a/app/src/main/java/com/owncloud/android/MainApp.java b/app/src/main/java/com/owncloud/android/MainApp.java index 4fa3d54fce..4911950ed5 100644 --- a/app/src/main/java/com/owncloud/android/MainApp.java +++ b/app/src/main/java/com/owncloud/android/MainApp.java @@ -349,6 +349,8 @@ public class MainApp extends MultiDexApplication implements HasAndroidInjector { backgroundJobManager.scheduleMediaFoldersDetectionJob(); backgroundJobManager.startMediaFoldersDetectionJob(); + backgroundJobManager.schedulePeriodicHealthStatus(); + registerGlobalPassCodeProtection(); } diff --git a/app/src/main/java/com/owncloud/android/datamodel/ArbitraryDataProvider.kt b/app/src/main/java/com/owncloud/android/datamodel/ArbitraryDataProvider.kt index 782c48a0ff..d4e0fbc537 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/ArbitraryDataProvider.kt +++ b/app/src/main/java/com/owncloud/android/datamodel/ArbitraryDataProvider.kt @@ -28,6 +28,8 @@ interface ArbitraryDataProvider { fun deleteKeyForAccount(account: String, key: String) fun storeOrUpdateKeyValue(accountName: String, key: String, newValue: Long) + + fun incrementValue(accountName: String, key: String) fun storeOrUpdateKeyValue(accountName: String, key: String, newValue: Boolean) fun storeOrUpdateKeyValue(accountName: String, key: String, newValue: String) @@ -43,5 +45,7 @@ interface ArbitraryDataProvider { const val DIRECT_EDITING = "DIRECT_EDITING" const val DIRECT_EDITING_ETAG = "DIRECT_EDITING_ETAG" const val PREDEFINED_STATUS = "PREDEFINED_STATUS" + const val E2E_ERRORS = "E2E_ERRORS" + const val E2E_ERRORS_TIMESTAMP = "E2E_ERRORS_TIMESTAMP" } } diff --git a/app/src/main/java/com/owncloud/android/datamodel/ArbitraryDataProviderImpl.java b/app/src/main/java/com/owncloud/android/datamodel/ArbitraryDataProviderImpl.java index f44c842c08..338b7cda17 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/ArbitraryDataProviderImpl.java +++ b/app/src/main/java/com/owncloud/android/datamodel/ArbitraryDataProviderImpl.java @@ -63,6 +63,17 @@ public class ArbitraryDataProviderImpl implements ArbitraryDataProvider { storeOrUpdateKeyValue(accountName, key, String.valueOf(newValue)); } + @Override + public void incrementValue(@NonNull String accountName, @NonNull String key) { + int oldValue = getIntegerValue(accountName, key); + + int value = 1; + if (oldValue > 0) { + value = oldValue + 1; + } + storeOrUpdateKeyValue(accountName, key, value); + } + @Override public void storeOrUpdateKeyValue(@NonNull final String accountName, @NonNull final String key, final boolean newValue) { storeOrUpdateKeyValue(accountName, key, String.valueOf(newValue)); 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 f72343457b..a3c7afb7b5 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java @@ -1954,6 +1954,7 @@ public class FileDataStorageManager { capability.getFilesLockingVersion()); contentValues.put(ProviderTableMeta.CAPABILITIES_GROUPFOLDERS, capability.getGroupfolders().getValue()); contentValues.put(ProviderTableMeta.CAPABILITIES_DROP_ACCOUNT, capability.getDropAccount().getValue()); + contentValues.put(ProviderTableMeta.CAPABILITIES_SECURITY_GUARD, capability.getSecurityGuard().getValue()); return contentValues; } @@ -2111,6 +2112,7 @@ public class FileDataStorageManager { getString(cursor, ProviderTableMeta.CAPABILITIES_FILES_LOCKING_VERSION)); capability.setGroupfolders(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_GROUPFOLDERS)); capability.setDropAccount(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_DROP_ACCOUNT)); + capability.setSecurityGuard(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_SECURITY_GUARD)); } return capability; } @@ -2287,7 +2289,18 @@ public class FileDataStorageManager { return user; } - public OCFile getDefaultRootPath(){ + public OCFile getDefaultRootPath() { return new OCFile(OCFile.ROOT_PATH); } + + public List getFilesWithSyncConflict(User user) { + List fileEntities = fileDao.getFilesWithSyncConflict(user.getAccountName()); + List files = new ArrayList<>(fileEntities.size()); + + for (FileEntity fileEntity : fileEntities) { + files.add(createFileInstance(fileEntity)); + } + + return files; + } } diff --git a/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.java b/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.java index b6cc8a5dde..e442595695 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.java @@ -567,6 +567,10 @@ public class UploadsStorageManager extends Observable { , String.valueOf(UploadStatus.UPLOAD_FAILED.value)); } + public OCUpload[] getUploadsForAccount(final @NonNull String accountName) { + return getUploads(ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "== ?", accountName); + } + public OCUpload[] getFinishedUploadsForCurrentAccount() { User user = currentAccountProvider.getUser(); @@ -586,14 +590,14 @@ public class UploadsStorageManager extends Observable { User user = currentAccountProvider.getUser(); return getUploads(ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_FAILED.value + - AND + ProviderTableMeta.UPLOADS_LAST_RESULT + - "<>" + UploadResult.DELAYED_FOR_WIFI.getValue() + - AND + ProviderTableMeta.UPLOADS_LAST_RESULT + - "<>" + UploadResult.LOCK_FAILED.getValue() + - AND + ProviderTableMeta.UPLOADS_LAST_RESULT + - "<>" + UploadResult.DELAYED_FOR_CHARGING.getValue() + - AND + ProviderTableMeta.UPLOADS_LAST_RESULT + - "<>" + UploadResult.DELAYED_IN_POWER_SAVE_MODE.getValue() + + AND + ProviderTableMeta.UPLOADS_LAST_RESULT + + "<>" + UploadResult.DELAYED_FOR_WIFI.getValue() + + AND + ProviderTableMeta.UPLOADS_LAST_RESULT + + "<>" + UploadResult.LOCK_FAILED.getValue() + + AND + ProviderTableMeta.UPLOADS_LAST_RESULT + + "<>" + UploadResult.DELAYED_FOR_CHARGING.getValue() + + AND + ProviderTableMeta.UPLOADS_LAST_RESULT + + "<>" + UploadResult.DELAYED_IN_POWER_SAVE_MODE.getValue() + AND + ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "== ?", user.getAccountName()); } @@ -624,15 +628,15 @@ public class UploadsStorageManager extends Observable { public long clearFailedButNotDelayedUploads() { User user = currentAccountProvider.getUser(); final long deleted = getDB().delete( - ProviderTableMeta.CONTENT_URI_UPLOADS, - ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_FAILED.value + - AND + ProviderTableMeta.UPLOADS_LAST_RESULT + - "<>" + UploadResult.LOCK_FAILED.getValue() + - AND + ProviderTableMeta.UPLOADS_LAST_RESULT + - "<>" + UploadResult.DELAYED_FOR_WIFI.getValue() + - AND + ProviderTableMeta.UPLOADS_LAST_RESULT + - "<>" + UploadResult.DELAYED_FOR_CHARGING.getValue() + - AND + ProviderTableMeta.UPLOADS_LAST_RESULT + + ProviderTableMeta.CONTENT_URI_UPLOADS, + ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_FAILED.value + + AND + ProviderTableMeta.UPLOADS_LAST_RESULT + + "<>" + UploadResult.LOCK_FAILED.getValue() + + AND + ProviderTableMeta.UPLOADS_LAST_RESULT + + "<>" + UploadResult.DELAYED_FOR_WIFI.getValue() + + AND + ProviderTableMeta.UPLOADS_LAST_RESULT + + "<>" + UploadResult.DELAYED_FOR_CHARGING.getValue() + + AND + ProviderTableMeta.UPLOADS_LAST_RESULT + "<>" + UploadResult.DELAYED_IN_POWER_SAVE_MODE.getValue() + AND + ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "== ?", new String[]{user.getAccountName()} @@ -647,10 +651,10 @@ public class UploadsStorageManager extends Observable { public long clearSuccessfulUploads() { User user = currentAccountProvider.getUser(); final long deleted = getDB().delete( - ProviderTableMeta.CONTENT_URI_UPLOADS, - ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_SUCCEEDED.value + AND + - ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "== ?", new String[]{user.getAccountName()} - ); + ProviderTableMeta.CONTENT_URI_UPLOADS, + ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_SUCCEEDED.value + AND + + ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "== ?", new String[]{user.getAccountName()} + ); Log_OC.d(TAG, "delete all successful uploads"); if (deleted > 0) { 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 e165fce1a9..eafc497902 100644 --- a/app/src/main/java/com/owncloud/android/db/ProviderMeta.java +++ b/app/src/main/java/com/owncloud/android/db/ProviderMeta.java @@ -35,7 +35,7 @@ import java.util.List; */ public class ProviderMeta { public static final String DB_NAME = "filelist"; - public static final int DB_VERSION = 75; + public static final int DB_VERSION = 76; private ProviderMeta() { // No instance @@ -264,6 +264,7 @@ public class ProviderMeta { public static final String CAPABILITIES_USER_STATUS_SUPPORTS_EMOJI = "user_status_supports_emoji"; public static final String CAPABILITIES_GROUPFOLDERS = "groupfolders"; public static final String CAPABILITIES_DROP_ACCOUNT = "drop_account"; + public static final String CAPABILITIES_SECURITY_GUARD = "security_guard"; //Columns of Uploads table public static final String UPLOADS_LOCAL_PATH = "local_path"; diff --git a/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java b/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java index f5d2f1f43a..cb1dab978e 100644 --- a/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java @@ -163,7 +163,9 @@ public class CreateFolderOperation extends SyncOperation implements OnRemoteOper serializedFolderMetadata, token, client, - metadataExists); + metadataExists, + arbitraryDataProvider, + user); // unlock folder if (token != null) { diff --git a/app/src/main/java/com/owncloud/android/operations/DownloadFileOperation.java b/app/src/main/java/com/owncloud/android/operations/DownloadFileOperation.java index ff1a682b5d..9da4a78e20 100644 --- a/app/src/main/java/com/owncloud/android/operations/DownloadFileOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/DownloadFileOperation.java @@ -26,6 +26,7 @@ import android.text.TextUtils; import android.webkit.MimeTypeMap; import com.nextcloud.client.account.User; +import com.owncloud.android.datamodel.ArbitraryDataProviderImpl; import com.owncloud.android.datamodel.DecryptedFolderMetadata; import com.owncloud.android.datamodel.FileDataStorageManager; import com.owncloud.android.datamodel.OCFile; @@ -213,7 +214,12 @@ public class DownloadFileOperation extends RemoteOperation { .get(file.getEncryptedFileName()).getAuthenticationTag()); try { - byte[] decryptedBytes = EncryptionUtils.decryptFile(tmpFile, key, iv, authenticationTag); + byte[] decryptedBytes = EncryptionUtils.decryptFile(tmpFile, + key, + iv, + authenticationTag, + new ArbitraryDataProviderImpl(context), + user); try (FileOutputStream fileOutputStream = new FileOutputStream(tmpFile)) { fileOutputStream.write(decryptedBytes); diff --git a/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java b/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java index d0ee77afc1..d2a8b982ca 100644 --- a/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java @@ -638,7 +638,9 @@ public class UploadFileOperation extends SyncOperation { serializedFolderMetadata, token, client, - metadataExists); + metadataExists, + arbitraryDataProvider, + user); // unlock result = EncryptionUtils.unlockFolder(parentFile, client, token); diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/SetupEncryptionDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/SetupEncryptionDialogFragment.kt index c78f4ed771..30a193d930 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/SetupEncryptionDialogFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/SetupEncryptionDialogFragment.kt @@ -223,6 +223,7 @@ class SetupEncryptionDialogFragment : DialogFragment(), Injectable { val secondKey = EncryptionUtils.decodeStringToBase64Bytes(decryptedString) if (!Arrays.equals(firstKey, secondKey)) { + EncryptionUtils.reportE2eError(arbitraryDataProvider, user) throw Exception("Keys do not match") } @@ -404,6 +405,7 @@ class SetupEncryptionDialogFragment : DialogFragment(), Injectable { if (result.isSuccess) { publicKeyString = result.data[0] as String if (!EncryptionUtils.isMatchingKeys(keyPair, publicKeyString)) { + EncryptionUtils.reportE2eError(arbitraryDataProvider, user) throw RuntimeException("Wrong CSR returned") } Log_OC.d(TAG, "public key success") diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/GroupfolderListFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/GroupfolderListFragment.kt index 6252a57414..cefcc0b648 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/GroupfolderListFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/GroupfolderListFragment.kt @@ -125,8 +125,8 @@ class GroupfolderListFragment : OCFileListFragment(), Injectable, GroupfolderLis val fetchResult = ReadFileRemoteOperation(partialFile.remotePath).execute(user, context) if (!fetchResult.isSuccess) { logger.e(SHARED_TAG, "Error fetching file") - if (fetchResult.isException) { - logger.e(SHARED_TAG, "exception: ", fetchResult.exception) + if (fetchResult.isException && fetchResult.exception != null) { + logger.e(SHARED_TAG, "exception: ", fetchResult.exception!!) } null } else { 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 945bd41ec9..93b65101f1 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 @@ -1766,7 +1766,9 @@ public class OCFileListFragment extends ExtendedListFragment implements serializedFolderMetadata, token, client, - metadataExists); + metadataExists, + arbitraryDataProvider, + user); // unlock folder EncryptionUtils.unlockFolder(folder, client, token); diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/SharedListFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/SharedListFragment.kt index dfd4a2952f..a2f1c28f3d 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/SharedListFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/SharedListFragment.kt @@ -87,8 +87,8 @@ class SharedListFragment : OCFileListFragment(), Injectable { val fetchResult = ReadFileRemoteOperation(partialFile.remotePath).execute(user, context) if (!fetchResult.isSuccess) { logger.e(SHARED_TAG, "Error fetching file") - if (fetchResult.isException) { - logger.e(SHARED_TAG, "exception: ", fetchResult.exception) + if (fetchResult.isException && fetchResult.exception != null) { + logger.e(SHARED_TAG, "exception: ", fetchResult.exception!!) } null } else { diff --git a/app/src/main/java/com/owncloud/android/utils/EncryptionUtils.java b/app/src/main/java/com/owncloud/android/utils/EncryptionUtils.java index 7bf42ccb73..988b4db277 100644 --- a/app/src/main/java/com/owncloud/android/utils/EncryptionUtils.java +++ b/app/src/main/java/com/owncloud/android/utils/EncryptionUtils.java @@ -47,6 +47,8 @@ import com.owncloud.android.lib.resources.e2ee.StoreMetadataRemoteOperation; import com.owncloud.android.lib.resources.e2ee.UnlockFileRemoteOperation; import com.owncloud.android.lib.resources.e2ee.UpdateMetadataRemoteOperation; import com.owncloud.android.lib.resources.status.NextcloudVersion; +import com.owncloud.android.lib.resources.status.Problem; +import com.owncloud.android.lib.resources.status.SendClientDiagnosticRemoteOperation; import com.owncloud.android.operations.UploadException; import org.apache.commons.httpclient.HttpStatus; @@ -326,10 +328,12 @@ public final class EncryptionUtils { if (TextUtils.isEmpty(decryptedFolderChecksum) && isFolderMigrated(remoteId, user, arbitraryDataProvider)) { + reportE2eError(arbitraryDataProvider, user); throw new IllegalStateException("Possible downgrade attack detected!"); } if (!TextUtils.isEmpty(decryptedFolderChecksum) && !decryptedFolderChecksum.equals(checksum)) { + reportE2eError(arbitraryDataProvider, user); throw new IllegalStateException("Wrong checksum!"); } @@ -349,7 +353,9 @@ public final class EncryptionUtils { encryptedFile.getEncrypted(), decodeStringToBase64Bytes(encryptedKey), decodeStringToBase64Bytes(encryptedFile.getEncryptedInitializationVector()), - decodeStringToBase64Bytes(encryptedFile.getEncryptedTag()) + decodeStringToBase64Bytes(encryptedFile.getEncryptedTag()), + arbitraryDataProvider, + user ); DecryptedFolderMetadata.DecryptedFile decryptedFile = new DecryptedFolderMetadata.DecryptedFile(); @@ -430,7 +436,9 @@ public final class EncryptionUtils { serializedFolderMetadata, token, client, - true); + true, + arbitraryDataProvider, + user); // unlock folder RemoteOperationResult unlockFolderResult = EncryptionUtils.unlockFolder(folder, client, token); @@ -534,7 +542,12 @@ public final class EncryptionUtils { * @param authenticationTag authenticationTag from metadata * @return decrypted byte[] */ - public static byte[] decryptFile(File file, byte[] encryptionKeyBytes, byte[] iv, byte[] authenticationTag) + public static byte[] decryptFile(File file, + byte[] encryptionKeyBytes, + byte[] iv, + byte[] authenticationTag, + ArbitraryDataProvider arbitraryDataProvider, + User user) throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, NoSuchPaddingException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, IOException { @@ -554,6 +567,7 @@ public final class EncryptionUtils { fileBytes.length - (128 / 8), fileBytes.length); if (!Arrays.equals(extractedAuthenticationTag, authenticationTag)) { + reportE2eError(arbitraryDataProvider, user); throw new SecurityException("Tag not correct"); } @@ -713,7 +727,9 @@ public final class EncryptionUtils { public static String decryptStringSymmetric(String string, byte[] encryptionKeyBytes, byte[] iv, - byte[] authenticationTag) + byte[] authenticationTag, + ArbitraryDataProvider arbitraryDataProvider, + User user) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, @@ -733,6 +749,7 @@ public final class EncryptionUtils { bytes.length); if (!Arrays.equals(extractedAuthenticationTag, authenticationTag)) { + reportE2eError(arbitraryDataProvider, user); throw new SecurityException("Tag not correct"); } @@ -1057,7 +1074,7 @@ public final class EncryptionUtils { return new Pair<>(Boolean.FALSE, metadata); } else { - // TODO error + reportE2eError(arbitraryDataProvider, user); throw new UploadException("something wrong"); } } @@ -1066,7 +1083,9 @@ public final class EncryptionUtils { String serializedFolderMetadata, String token, OwnCloudClient client, - boolean metadataExists) throws UploadException { + boolean metadataExists, + ArbitraryDataProvider arbitraryDataProvider, + User user) throws UploadException { RemoteOperationResult uploadMetadataOperationResult; if (metadataExists) { // update metadata @@ -1081,6 +1100,7 @@ public final class EncryptionUtils { } if (!uploadMetadataOperationResult.isSuccess()) { + reportE2eError(arbitraryDataProvider, user); throw new UploadException("Storing/updating metadata was not successful"); } } @@ -1207,4 +1227,37 @@ public final class EncryptionUtils { return arrayList.contains(id); } + + public static void reportE2eError(ArbitraryDataProvider arbitraryDataProvider, User user) { + arbitraryDataProvider.incrementValue(user.getAccountName(), ArbitraryDataProvider.E2E_ERRORS); + + if (arbitraryDataProvider.getLongValue(user.getAccountName(), + ArbitraryDataProvider.E2E_ERRORS_TIMESTAMP) == -1L) { + arbitraryDataProvider.storeOrUpdateKeyValue( + user.getAccountName(), + ArbitraryDataProvider.E2E_ERRORS_TIMESTAMP, + System.currentTimeMillis() / 1000 + ); + } + } + + @Nullable + public static Problem readE2eError(ArbitraryDataProvider arbitraryDataProvider, User user) { + int value = arbitraryDataProvider.getIntegerValue(user.getAccountName(), + ArbitraryDataProvider.E2E_ERRORS); + long timestamp = arbitraryDataProvider.getLongValue(user.getAccountName(), + ArbitraryDataProvider.E2E_ERRORS_TIMESTAMP); + + arbitraryDataProvider.deleteKeyForAccount(user.getAccountName(), + ArbitraryDataProvider.E2E_ERRORS); + + arbitraryDataProvider.deleteKeyForAccount(user.getAccountName(), + ArbitraryDataProvider.E2E_ERRORS_TIMESTAMP); + + if (value > 0 && timestamp > 0) { + return new Problem(SendClientDiagnosticRemoteOperation.E2E_ERRORS, value, timestamp); + } else { + return null; + } + } }