diff --git a/app/schemas/com.nextcloud.client.database.NextcloudDatabase/81.json b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/81.json index 10d076c10d38..3dd2580e23ed 100644 --- a/app/schemas/com.nextcloud.client.database.NextcloudDatabase/81.json +++ b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/81.json @@ -1206,4 +1206,4 @@ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '082a63031678a67879428f688f02d3b5')" ] } -} \ No newline at end of file +} diff --git a/app/schemas/com.nextcloud.client.database.NextcloudDatabase/83.json b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/83.json new file mode 100644 index 000000000000..c27bba8f602e --- /dev/null +++ b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/83.json @@ -0,0 +1,1245 @@ +{ + "formatVersion": 1, + "database": { + "version": 83, + "identityHash": "365a8731a100a61ae5029beb74acd02e", + "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, `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)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "assistant", + "columnName": "assistant", + "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": "endToEndEncryptionApiVersion", + "columnName": "end_to_end_encryption_api_version", + "affinity": "TEXT", + "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 + }, + { + "fieldPath": "forbiddenFileNameCharacters", + "columnName": "forbidden_filename_characters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forbiddenFileNames", + "columnName": "forbidden_filenames", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forbiddenFileNameExtensions", + "columnName": "forbidden_filename_extensions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forbiddenFilenameBaseNames", + "columnName": "forbidden_filename_basenames", + "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, `e2e_counter` INTEGER, `internal_two_way_sync_timestamp` INTEGER, `internal_two_way_sync_result` 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 + }, + { + "fieldPath": "e2eCounter", + "columnName": "e2e_counter", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "internalTwoWaySync", + "columnName": "internal_two_way_sync_timestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "internalTwoWaySyncResult", + "columnName": "internal_two_way_sync_result", + "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` TEXT, `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": "TEXT", + "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, `exclude_hidden` INTEGER, `last_scan_timestamp_ms` 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 + }, + { + "fieldPath": "excludeHidden", + "columnName": "exclude_hidden", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastScanTimestampMs", + "columnName": "last_scan_timestamp_ms", + "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, '365a8731a100a61ae5029beb74acd02e')" + ] + } +} \ No newline at end of file diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.SettingsActivityIT_open.png b/app/screenshots/gplay/debug/com.nextcloud.client.SettingsActivityIT_open.png index a5159c8740aa..9d35b9c6b572 100644 Binary files a/app/screenshots/gplay/debug/com.nextcloud.client.SettingsActivityIT_open.png and b/app/screenshots/gplay/debug/com.nextcloud.client.SettingsActivityIT_open.png differ diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.SettingsActivityIT_showMnemonic_Error.png b/app/screenshots/gplay/debug/com.nextcloud.client.SettingsActivityIT_showMnemonic_Error.png index f2432977ce8e..4e7c8e82caad 100644 Binary files a/app/screenshots/gplay/debug/com.nextcloud.client.SettingsActivityIT_showMnemonic_Error.png and b/app/screenshots/gplay/debug/com.nextcloud.client.SettingsActivityIT_showMnemonic_Error.png differ diff --git a/app/src/androidTest/java/com/owncloud/android/FileIT.java b/app/src/androidTest/java/com/owncloud/android/FileIT.java index d5c24d3269cb..9f251d65c857 100644 --- a/app/src/androidTest/java/com/owncloud/android/FileIT.java +++ b/app/src/androidTest/java/com/owncloud/android/FileIT.java @@ -106,7 +106,6 @@ public void testRenameFolder() throws IOException { assertTrue(new SynchronizeFolderOperation(targetContext, folderPath, user, - System.currentTimeMillis(), fileDataStorageManager) .execute(targetContext) .isSuccess()); diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index af9fd5fae0f3..6f89c63ac939 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -255,6 +255,9 @@ + @@ -627,4 +630,4 @@ - \ No newline at end of file + diff --git a/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt b/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt index bed77c0436a9..a4e0f74b8117 100644 --- a/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt +++ b/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt @@ -60,7 +60,8 @@ import com.owncloud.android.db.ProviderMeta AutoMigration(from = 78, to = 79, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class), AutoMigration(from = 79, to = 80), AutoMigration(from = 80, to = 81), - AutoMigration(from = 81, to = 82) + AutoMigration(from = 81, to = 82), + AutoMigration(from = 82, to = 83) ], 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 4dc78224018f..37ef4568c5be 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 @@ -49,4 +49,10 @@ interface FileDao { @Query("SELECT * FROM filelist where file_owner = :fileOwner AND etag_in_conflict IS NOT NULL") fun getFilesWithSyncConflict(fileOwner: String): List + + @Query( + "SELECT * FROM filelist where file_owner = :fileOwner AND internal_two_way_sync_timestamp >= 0 " + + "ORDER BY internal_two_way_sync_timestamp DESC" + ) + fun getInternalTwoWaySyncFolders(fileOwner: String): List } diff --git a/app/src/main/java/com/nextcloud/client/database/entity/FileEntity.kt b/app/src/main/java/com/nextcloud/client/database/entity/FileEntity.kt index 2281a06ea9da..d223172bb91f 100644 --- a/app/src/main/java/com/nextcloud/client/database/entity/FileEntity.kt +++ b/app/src/main/java/com/nextcloud/client/database/entity/FileEntity.kt @@ -115,5 +115,9 @@ data class FileEntity( @ColumnInfo(name = ProviderTableMeta.FILE_METADATA_GPS) val metadataGPS: String?, @ColumnInfo(name = ProviderTableMeta.FILE_E2E_COUNTER) - val e2eCounter: Long? + val e2eCounter: Long?, + @ColumnInfo(name = ProviderTableMeta.FILE_INTERNAL_TWO_WAY_SYNC_TIMESTAMP) + val internalTwoWaySync: Long?, + @ColumnInfo(name = ProviderTableMeta.FILE_INTERNAL_TWO_WAY_SYNC_RESULT) + val internalTwoWaySyncResult: String? ) diff --git a/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java b/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java index a6ba1bcf15ec..37b1b10d98d4 100644 --- a/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java +++ b/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java @@ -54,6 +54,7 @@ import com.owncloud.android.ui.activity.FileDisplayActivity; import com.owncloud.android.ui.activity.FilePickerActivity; import com.owncloud.android.ui.activity.FolderPickerActivity; +import com.owncloud.android.ui.activity.InternalTwoWaySyncActivity; import com.owncloud.android.ui.activity.ManageAccountsActivity; import com.owncloud.android.ui.activity.ManageSpaceActivity; import com.owncloud.android.ui.activity.NotificationsActivity; @@ -476,4 +477,7 @@ abstract class ComponentsModule { @ContributesAndroidInjector abstract TestJob testJob(); + + @ContributesAndroidInjector + abstract InternalTwoWaySyncActivity internalTwoWaySyncActivity(); } 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 8bfc81c0301c..b42d1ef25d5e 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt @@ -95,6 +95,7 @@ class BackgroundJobFactory @Inject constructor( GeneratePdfFromImagesWork::class -> createPDFGenerateWork(context, workerParameters) HealthStatusWork::class -> createHealthStatusWork(context, workerParameters) TestJob::class -> createTestJob(context, workerParameters) + InternalTwoWaySyncWork::class -> createInternalTwoWaySyncWork(context, workerParameters) else -> null // caller falls back to default factory } } @@ -277,4 +278,14 @@ class BackgroundJobFactory @Inject constructor( backgroundJobManager.get() ) } + + private fun createInternalTwoWaySyncWork(context: Context, params: WorkerParameters): InternalTwoWaySyncWork { + return InternalTwoWaySyncWork( + context, + params, + accountManager, + powerManagementService, + connectivityService + ) + } } 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 ae6d35a5c39a..d460fd623f7c 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt @@ -168,4 +168,5 @@ interface BackgroundJobManager { fun schedulePeriodicHealthStatus() fun startHealthStatus() fun bothFilesSyncJobsRunning(syncedFolderID: Long): Boolean + fun scheduleInternal2WaySync() } 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 76a9b836b8eb..af345d832bfd 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt @@ -84,6 +84,8 @@ internal class BackgroundJobManagerImpl( const val JOB_PERIODIC_HEALTH_STATUS = "periodic_health_status" const val JOB_IMMEDIATE_HEALTH_STATUS = "immediate_health_status" + const val JOB_INTERNAL_TWO_WAY_SYNC = "internal_two_way_sync" + const val JOB_TEST = "test_job" const val MAX_CONTENT_TRIGGER_DELAY_MS = 10000L @@ -647,4 +649,13 @@ internal class BackgroundJobManagerImpl( request ) } + + override fun scheduleInternal2WaySync() { + val request = periodicRequestBuilder( + jobClass = InternalTwoWaySyncWork::class, + jobName = JOB_INTERNAL_TWO_WAY_SYNC + ).build() + + workManager.enqueueUniquePeriodicWork(JOB_INTERNAL_TWO_WAY_SYNC, ExistingPeriodicWorkPolicy.KEEP, request) + } } diff --git a/app/src/main/java/com/nextcloud/client/jobs/InternalTwoWaySyncWork.kt b/app/src/main/java/com/nextcloud/client/jobs/InternalTwoWaySyncWork.kt new file mode 100644 index 000000000000..18d32c7491a5 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/jobs/InternalTwoWaySyncWork.kt @@ -0,0 +1,91 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Tobias Kaminsky + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.jobs + +import android.content.Context +import androidx.work.Worker +import androidx.work.WorkerParameters +import com.nextcloud.client.account.UserAccountManager +import com.nextcloud.client.device.PowerManagementService +import com.nextcloud.client.network.ConnectivityService +import com.owncloud.android.MainApp +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.operations.SynchronizeFolderOperation +import com.owncloud.android.utils.FileStorageUtils +import java.io.File + +@Suppress("Detekt.NestedBlockDepth") +class InternalTwoWaySyncWork( + private val context: Context, + params: WorkerParameters, + private val userAccountManager: UserAccountManager, + private val powerManagementService: PowerManagementService, + private val connectivityService: ConnectivityService +) : Worker(context, params) { + override fun doWork(): Result { + Log_OC.d(TAG, "Worker started!") + + var result = true + + if (powerManagementService.isPowerSavingEnabled || + !connectivityService.isConnected || connectivityService.isInternetWalled + ) { + Log_OC.d(TAG, "Not starting due to constraints!") + return Result.success() + } + + val users = userAccountManager.allUsers + + for (user in users) { + val fileDataStorageManager = FileDataStorageManager(user, context.contentResolver) + val folders = fileDataStorageManager.getInternalTwoWaySyncFolders(user) + + for (folder in folders) { + val freeSpaceLeft = File(folder.storagePath).getFreeSpace() + val localFolderSize = FileStorageUtils.getFolderSize(File(folder.storagePath, MainApp.getDataFolder())) + val remoteFolderSize = folder.fileLength + + if (freeSpaceLeft < (remoteFolderSize - localFolderSize)) { + Log_OC.d(TAG, "Not enough space left!") + result = false + } + + Log_OC.d(TAG, "Folder ${folder.remotePath}: started!") + val operation = SynchronizeFolderOperation(context, folder.remotePath, user, fileDataStorageManager) + .execute(context) + + if (operation.isSuccess) { + Log_OC.d(TAG, "Folder ${folder.remotePath}: finished!") + } else { + Log_OC.d(TAG, "Folder ${folder.remotePath} failed!") + result = false + } + + folder.apply { + internalFolderSyncResult = operation.code.toString() + internalFolderSyncTimestamp = System.currentTimeMillis() + } + + fileDataStorageManager.saveFile(folder) + } + } + + return if (result) { + Log_OC.d(TAG, "Worker finished with success!") + Result.success() + } else { + Log_OC.d(TAG, "Worker finished with failure!") + Result.failure() + } + } + + companion object { + const val TAG = "InternalTwoWaySyncWork" + } +} diff --git a/app/src/main/java/com/owncloud/android/MainApp.java b/app/src/main/java/com/owncloud/android/MainApp.java index 9d8507b7b4b0..9fd3e2cb7abe 100644 --- a/app/src/main/java/com/owncloud/android/MainApp.java +++ b/app/src/main/java/com/owncloud/android/MainApp.java @@ -371,6 +371,7 @@ public void onCreate() { backgroundJobManager.scheduleMediaFoldersDetectionJob(); backgroundJobManager.startMediaFoldersDetectionJob(); backgroundJobManager.schedulePeriodicHealthStatus(); + backgroundJobManager.scheduleInternal2WaySync(); } registerGlobalPassCodeProtection(); 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 95653322a517..5390b5d5c6e1 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java @@ -77,6 +77,7 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import kotlin.Pair; +@SuppressFBWarnings("CE") public class FileDataStorageManager { private static final String TAG = FileDataStorageManager.class.getSimpleName(); @@ -558,6 +559,8 @@ private ContentValues createContentValuesBase(OCFile fileOrFolder) { cv.put(ProviderTableMeta.FILE_SHAREES, gson.toJson(fileOrFolder.getSharees())); cv.put(ProviderTableMeta.FILE_TAGS, gson.toJson(fileOrFolder.getTags())); cv.put(ProviderTableMeta.FILE_RICH_WORKSPACE, fileOrFolder.getRichWorkspace()); + cv.put(ProviderTableMeta.FILE_INTERNAL_TWO_WAY_SYNC_TIMESTAMP, fileOrFolder.getInternalFolderSyncTimestamp()); + cv.put(ProviderTableMeta.FILE_INTERNAL_TWO_WAY_SYNC_RESULT, fileOrFolder.getInternalFolderSyncResult()); return cv; } @@ -602,6 +605,8 @@ private ContentValues createContentValuesForFile(OCFile file) { cv.put(ProviderTableMeta.FILE_METADATA_GPS, gson.toJson(file.getGeoLocation())); cv.put(ProviderTableMeta.FILE_METADATA_LIVE_PHOTO, file.getLinkedFileIdForLivePhoto()); cv.put(ProviderTableMeta.FILE_E2E_COUNTER, file.getE2eCounter()); + cv.put(ProviderTableMeta.FILE_INTERNAL_TWO_WAY_SYNC_TIMESTAMP, file.getInternalFolderSyncTimestamp()); + cv.put(ProviderTableMeta.FILE_INTERNAL_TWO_WAY_SYNC_RESULT, file.getInternalFolderSyncResult()); return cv; } @@ -1035,6 +1040,7 @@ private OCFile createFileInstance(FileEntity fileEntity) { ocFile.setLivePhoto(fileEntity.getMetadataLivePhoto()); ocFile.setHidden(nullToZero(fileEntity.getHidden()) == 1); ocFile.setE2eCounter(fileEntity.getE2eCounter()); + ocFile.setInternalFolderSyncTimestamp(fileEntity.getInternalTwoWaySync()); String sharees = fileEntity.getSharees(); // Surprisingly JSON deserialization causes significant overhead. @@ -2477,4 +2483,29 @@ public List getFilesWithSyncConflict(User user) { return files; } + + public List getInternalTwoWaySyncFolders(User user) { + List fileEntities = fileDao.getInternalTwoWaySyncFolders(user.getAccountName()); + List files = new ArrayList<>(fileEntities.size()); + + for (FileEntity fileEntity : fileEntities) { + files.add(createFileInstance(fileEntity)); + } + + return files; + } + + public boolean isPartOfInternalTwoWaySync(OCFile file) { + if (file.isInternalFolderSync()) { + return true; + } + + while (file != null && !OCFile.ROOT_PATH.equals(file.getDecryptedRemotePath())) { + if (file.isInternalFolderSync()) { + return true; + } + file = getFileById(file.getParentId()); + } + return false; + } } diff --git a/app/src/main/java/com/owncloud/android/datamodel/OCFile.java b/app/src/main/java/com/owncloud/android/datamodel/OCFile.java index 566b1c06d3c9..5577bc182a32 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/OCFile.java +++ b/app/src/main/java/com/owncloud/android/datamodel/OCFile.java @@ -117,6 +117,8 @@ public class OCFile implements Parcelable, Comparable, ServerFileInterfa @Nullable private GeoLocation geolocation; private List tags = new ArrayList<>(); + private Long internalFolderSyncTimestamp = -1L; + private String internalFolderSyncResult = ""; /** * URI to the local path of the file contents, if stored in the device; cached after first call to @@ -1051,6 +1053,26 @@ public void setE2eCounter(@Nullable Long e2eCounter) { this.e2eCounter = e2eCounter; } } + + public boolean isInternalFolderSync() { + return internalFolderSyncTimestamp >= 0; + } + + public Long getInternalFolderSyncTimestamp() { + return internalFolderSyncTimestamp; + } + + public void setInternalFolderSyncTimestamp(Long internalFolderSyncTimestamp) { + this.internalFolderSyncTimestamp = internalFolderSyncTimestamp; + } + + public String getInternalFolderSyncResult() { + return internalFolderSyncResult; + } + + public void setInternalFolderSyncResult(String internalFolderSyncResult) { + this.internalFolderSyncResult = internalFolderSyncResult; + } public boolean isAPKorAAB() { if ("gplay".equals(BuildConfig.FLAVOR)) { 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 579c81fe4e21..3ea8d4b7e196 100644 --- a/app/src/main/java/com/owncloud/android/db/ProviderMeta.java +++ b/app/src/main/java/com/owncloud/android/db/ProviderMeta.java @@ -25,7 +25,7 @@ */ public class ProviderMeta { public static final String DB_NAME = "filelist"; - public static final int DB_VERSION = 82; + public static final int DB_VERSION = 83; private ProviderMeta() { // No instance @@ -120,6 +120,8 @@ static public class ProviderTableMeta implements BaseColumns { public static final String FILE_LOCK_TOKEN = "lock_token"; public static final String FILE_TAGS = "tags"; public static final String FILE_E2E_COUNTER = "e2e_counter"; + public static final String FILE_INTERNAL_TWO_WAY_SYNC_TIMESTAMP = "internal_two_way_sync_timestamp"; + public static final String FILE_INTERNAL_TWO_WAY_SYNC_RESULT = "internal_two_way_sync_result"; public static final List FILE_ALL_COLUMNS = Collections.unmodifiableList(Arrays.asList( _ID, @@ -171,7 +173,9 @@ static public class ProviderTableMeta implements BaseColumns { FILE_METADATA_LIVE_PHOTO, FILE_E2E_COUNTER, FILE_TAGS, - FILE_METADATA_GPS)); + FILE_METADATA_GPS, + FILE_INTERNAL_TWO_WAY_SYNC_TIMESTAMP, + FILE_INTERNAL_TWO_WAY_SYNC_RESULT)); public static final String FILE_DEFAULT_SORT_ORDER = FILE_NAME + " collate nocase asc"; // Columns of ocshares table diff --git a/app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java b/app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java index aa76b4935e8f..086006d40559 100644 --- a/app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java @@ -697,6 +697,7 @@ private void setLocalFileDataOnUpdatedFile(OCFile remoteFile, OCFile localFile, if (localFile != null) { updatedFile.setFileId(localFile.getFileId()); updatedFile.setLastSyncDateForData(localFile.getLastSyncDateForData()); + updatedFile.setInternalFolderSyncTimestamp(localFile.getInternalFolderSyncTimestamp()); updatedFile.setModificationTimestampAtLastSyncForData( localFile.getModificationTimestampAtLastSyncForData() ); diff --git a/app/src/main/java/com/owncloud/android/operations/SynchronizeFileOperation.java b/app/src/main/java/com/owncloud/android/operations/SynchronizeFileOperation.java index 58ca7cd8432e..6caf81f4714b 100644 --- a/app/src/main/java/com/owncloud/android/operations/SynchronizeFileOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/SynchronizeFileOperation.java @@ -295,6 +295,8 @@ private void requestForUpload(OCFile file) { } private void requestForDownload(OCFile file) { + Log_OC.d("InternalTwoWaySyncWork", "download file: " + file.getFileName()); + FileDownloadHelper.Companion.instance().downloadFile( mUser, file); diff --git a/app/src/main/java/com/owncloud/android/operations/SynchronizeFolderOperation.java b/app/src/main/java/com/owncloud/android/operations/SynchronizeFolderOperation.java index 861a29e75838..9f48c9ce92a7 100644 --- a/app/src/main/java/com/owncloud/android/operations/SynchronizeFolderOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/SynchronizeFolderOperation.java @@ -55,9 +55,6 @@ public class SynchronizeFolderOperation extends SyncOperation { private static final String TAG = SynchronizeFolderOperation.class.getSimpleName(); - /** Time stamp for the synchronization process in progress */ - private long mCurrentSyncTime; - /** Remote path of the folder to synchronize */ private String mRemotePath; @@ -95,17 +92,14 @@ public class SynchronizeFolderOperation extends SyncOperation { * @param context Application context. * @param remotePath Path to synchronize. * @param user Nextcloud account where the folder is located. - * @param currentSyncTime Time stamp for the synchronization process in progress. */ public SynchronizeFolderOperation(Context context, String remotePath, User user, - long currentSyncTime, FileDataStorageManager storageManager) { super(storageManager); mRemotePath = remotePath; - mCurrentSyncTime = currentSyncTime; this.user = user; mContext = context; mRemoteFolderChanged = false; @@ -365,7 +359,7 @@ private void synchronizeData(List folderAndFiles) throws OperationCancel } private void updateLocalStateData(OCFile remoteFile, OCFile localFile, OCFile updatedFile) { - updatedFile.setLastSyncDateForProperties(mCurrentSyncTime); + updatedFile.setLastSyncDateForProperties(System.currentTimeMillis()); if (localFile != null) { updatedFile.setFileId(localFile.getFileId()); updatedFile.setLastSyncDateForData(localFile.getLastSyncDateForData()); @@ -393,8 +387,19 @@ private void updateLocalStateData(OCFile remoteFile, OCFile localFile, OCFile up } } - private void classifyFileForLaterSyncOrDownload(OCFile remoteFile, OCFile localFile) { - if (!remoteFile.isFolder()) { + @SuppressFBWarnings("JLM") + private void classifyFileForLaterSyncOrDownload(OCFile remoteFile, OCFile localFile) throws OperationCancelledException { + if (remoteFile.isFolder()) { + /// to download children files recursively + synchronized (mCancellationRequested) { + if (mCancellationRequested.get()) { + throw new OperationCancelledException(); + } + startSyncFolderOperation(remoteFile.getRemotePath()); + } + + } else { + /// prepare content synchronization for files (any file, not just favorites) SynchronizeFileOperation operation = new SynchronizeFileOperation( localFile, remoteFile, diff --git a/app/src/main/java/com/owncloud/android/services/OperationsService.java b/app/src/main/java/com/owncloud/android/services/OperationsService.java index 372a3c88e089..648c7ce1b444 100644 --- a/app/src/main/java/com/owncloud/android/services/OperationsService.java +++ b/app/src/main/java/com/owncloud/android/services/OperationsService.java @@ -707,7 +707,6 @@ private Pair newOperation(Intent operationIntent) { this, // TODO remove this dependency from construction time remotePath, user, - System.currentTimeMillis(), // TODO remove this dependency from construction time fileDataStorageManager ); break; diff --git a/app/src/main/java/com/owncloud/android/ui/activity/InternalTwoWaySyncActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/InternalTwoWaySyncActivity.kt new file mode 100644 index 000000000000..e5489e6d9468 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/activity/InternalTwoWaySyncActivity.kt @@ -0,0 +1,30 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Tobias Kaminsky + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.ui.activity + +import android.os.Bundle +import androidx.recyclerview.widget.LinearLayoutManager +import com.nextcloud.client.di.Injectable +import com.owncloud.android.databinding.InternalTwoWaySyncLayoutBinding +import com.owncloud.android.ui.adapter.InternalTwoWaySyncAdapter + +class InternalTwoWaySyncActivity : BaseActivity(), Injectable { + lateinit var binding: InternalTwoWaySyncLayoutBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding = InternalTwoWaySyncLayoutBinding.inflate(layoutInflater) + setContentView(binding.root) + + binding.list.apply { + adapter = InternalTwoWaySyncAdapter(fileDataStorageManager, user.get(), context) + layoutManager = LinearLayoutManager(context) + } + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java index 88f4e90c9d88..0dbf9ca99e92 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java @@ -171,6 +171,9 @@ public void onCreate(Bundle savedInstanceState) { // Details setupDetailsCategory(preferenceScreen); + // Sync + setupSyncCategory(); + // More setupMoreCategory(); @@ -310,13 +313,19 @@ private void setupAboutCategory(String appVersion) { } } } + + private void setupSyncCategory() { + final PreferenceCategory preferenceCategorySync = (PreferenceCategory) findPreference("sync"); + viewThemeUtils.files.themePreferenceCategory(preferenceCategorySync); + + setupAutoUploadPreference(preferenceCategorySync); + setupInternalTwoWaySyncPreference(preferenceCategorySync); + } private void setupMoreCategory() { final PreferenceCategory preferenceCategoryMore = (PreferenceCategory) findPreference("more"); viewThemeUtils.files.themePreferenceCategory(preferenceCategoryMore); - setupAutoUploadPreference(preferenceCategoryMore); - setupCalendarPreference(preferenceCategoryMore); setupBackupPreference(); @@ -548,6 +557,16 @@ private void setupAutoUploadPreference(PreferenceCategory preferenceCategoryMore }); } } + + private void setupInternalTwoWaySyncPreference(PreferenceCategory preferenceCategorySync) { + Preference twoWaySync = findPreference("internal_two_way_sync"); + + twoWaySync.setOnPreferenceClickListener(preference -> { + Intent intent = new Intent(this, InternalTwoWaySyncActivity.class); + startActivity(intent); + return true; + }); + } private void setupBackupPreference() { Preference pContactsBackup = findPreference("backup"); diff --git a/app/src/main/java/com/owncloud/android/ui/activity/StorageMigration.java b/app/src/main/java/com/owncloud/android/ui/activity/StorageMigration.java index 5574e8ffa1d7..00ab1b25cfe5 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/StorageMigration.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/StorageMigration.java @@ -424,8 +424,12 @@ private void checkDestinationAvailability() throws MigrationException { throw new MigrationException(R.string.file_migration_failed_dir_already_exists); } - if (dstFile.getFreeSpace() < FileStorageUtils.getFolderSize(new File(srcFile, MainApp.getDataFolder()))) { - throw new MigrationException(R.string.file_migration_failed_not_enough_space); + try { + if (dstFile.getFreeSpace() < FileStorageUtils.getFolderSize(new File(srcFile, MainApp.getDataFolder()))) { + throw new MigrationException(R.string.file_migration_failed_not_enough_space); + } + } catch (MigrationException e) { + throw new RuntimeException(e); } } diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/InternalTwoWaySyncAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/InternalTwoWaySyncAdapter.kt new file mode 100644 index 000000000000..b432add035ef --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/adapter/InternalTwoWaySyncAdapter.kt @@ -0,0 +1,43 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Tobias Kaminsky + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.ui.adapter + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.nextcloud.client.account.User +import com.owncloud.android.databinding.InternalTwoWaySyncViewHolderBinding +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.datamodel.OCFile + +class InternalTwoWaySyncAdapter( + dataStorageManager: FileDataStorageManager, + user: User, + val context: Context +) : RecyclerView.Adapter() { + var folders: List = dataStorageManager.getInternalTwoWaySyncFolders(user) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): InternalTwoWaySyncViewHolder { + return InternalTwoWaySyncViewHolder( + InternalTwoWaySyncViewHolderBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + override fun getItemCount(): Int { + return folders.size + } + + override fun onBindViewHolder(holder: InternalTwoWaySyncViewHolder, position: Int) { + holder.bind(folders[position], context) + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/InternalTwoWaySyncViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/InternalTwoWaySyncViewHolder.kt new file mode 100644 index 000000000000..978b8c73172a --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/adapter/InternalTwoWaySyncViewHolder.kt @@ -0,0 +1,44 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Tobias Kaminsky + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.ui.adapter + +import android.content.Context +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import com.owncloud.android.R +import com.owncloud.android.databinding.InternalTwoWaySyncViewHolderBinding +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.utils.DisplayUtils + +class InternalTwoWaySyncViewHolder(val binding: InternalTwoWaySyncViewHolderBinding) : + RecyclerView.ViewHolder(binding.root) { + fun bind(folder: OCFile, context: Context) { + binding.run { + size.text = DisplayUtils.bytesToHumanReadable(folder.fileLength) + name.text = folder.decryptedFileName + + if (folder.internalFolderSyncResult.isEmpty()) { + syncResult.visibility = View.GONE + syncResultDivider.visibility = View.GONE + } else { + syncResult.visibility = View.VISIBLE + syncResultDivider.visibility = View.VISIBLE + syncResult.text = folder.internalFolderSyncResult + } + + if (folder.internalFolderSyncTimestamp == 0L) { + syncTimestamp.text = context.getString(R.string.internal_two_way_sync_not_yet) + } else { + syncTimestamp.text = DisplayUtils.getRelativeTimestamp( + context, + folder.internalFolderSyncTimestamp + ) + } + } + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java index 0b27de1bc378..b02a85ae5022 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java @@ -261,6 +261,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat binding.favorite.setOnClickListener(this); binding.overflowMenu.setOnClickListener(this); binding.lastModificationTimestamp.setOnClickListener(this); + binding.folderSyncButton.setOnClickListener(this); updateFileDetails(false, false); } @@ -471,8 +472,14 @@ public void onClick(View v) { boolean showDetailedTimestamp = !preferences.isShowDetailedTimestampEnabled(); preferences.setShowDetailedTimestampEnabled(showDetailedTimestamp); setFileModificationTimestamp(getFile(), showDetailedTimestamp); - - Log_OC.e(TAG, "Incorrect view clicked!"); + } else if (id == R.id.folder_sync_button) { + if (binding.folderSyncButton.isChecked()) { + getFile().setInternalFolderSyncTimestamp(0L); + } else { + getFile().setInternalFolderSyncTimestamp(-1L); + } + + storageManager.saveFile(getFile()); } else { Log_OC.e(TAG, "Incorrect view clicked!"); } @@ -556,6 +563,17 @@ public void updateFileDetails(boolean transferring, boolean refresh) { if (fabMain != null) { fabMain.hide(); } + + binding.syncBlock.setVisibility(file.isFolder() ? View.VISIBLE : View.GONE); + + if (file.isInternalFolderSync()) { + binding.folderSyncButton.setChecked(file.isInternalFolderSync()); + } else { + if (storageManager.isPartOfInternalTwoWaySync(file)) { + binding.folderSyncButton.setChecked(true); + binding.folderSyncButton.setEnabled(false); + } + } } setupViewPager(); diff --git a/app/src/main/res/layout/file_details_fragment.xml b/app/src/main/res/layout/file_details_fragment.xml index c00d53999a43..9466d4638d35 100644 --- a/app/src/main/res/layout/file_details_fragment.xml +++ b/app/src/main/res/layout/file_details_fragment.xml @@ -170,6 +170,39 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/internal_two_way_sync_view_holder.xml b/app/src/main/res/layout/internal_two_way_sync_view_holder.xml new file mode 100644 index 000000000000..4304150df226 --- /dev/null +++ b/app/src/main/res/layout/internal_two_way_sync_view_holder.xml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d20254961e9f..71b8e5459c93 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1218,13 +1218,15 @@ Year/Month/Day Secure sharing is not set up for this user Resharing is not allowed during secure file drop + Sync + Internal two way sync + Manage internal folders for two way sync + Not yet, soon to be synced Google restricted downloading APK/AAB files! No file or folder matching your search - Event not found, you can always sync to update. Redirecting to web… Contact not found, you can always sync to update. Redirecting to web… Permissions are required to open search result otherwise it will redirected to web… - Current folder name is invalid, please rename the folder. Redirecting to root %s. Please rename the file before moving or copying Some contents cannot able to uploaded due to contains reserved names or invalid character @@ -1233,4 +1235,5 @@ %s is a forbidden name .%s is a forbidden file extension Name ends with a space or a period + Sync diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 061f947aeba6..aa1e05dc5515 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -53,13 +53,23 @@ android:key="show_media_scan_notifications"/> + + + + + + -