diff --git a/app/schemas/com.nextcloud.client.database.NextcloudDatabase/85.json b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/85.json new file mode 100644 index 000000000000..5e2a33ba006c --- /dev/null +++ b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/85.json @@ -0,0 +1,1301 @@ +{ + "formatVersion": 1, + "database": { + "version": 85, + "identityHash": "2d24b9210a36150f221156d2e8f59665", + "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": [] + }, + { + "tableName": "offline_operations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `offline_operations_parent_oc_file_id` INTEGER, `offline_operations_path` TEXT, `offline_operations_type` TEXT, `offline_operations_file_name` TEXT, `offline_operations_created_at` INTEGER, `offline_operations_modified_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "parentOCFileId", + "columnName": "offline_operations_parent_oc_file_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "offline_operations_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "offline_operations_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filename", + "columnName": "offline_operations_file_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "offline_operations_created_at", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "modifiedAt", + "columnName": "offline_operations_modified_at", + "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, '2d24b9210a36150f221156d2e8f59665')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/owncloud/android/AbstractIT.java b/app/src/androidTest/java/com/owncloud/android/AbstractIT.java index 4a5481382950..2e2bc846e8bc 100644 --- a/app/src/androidTest/java/com/owncloud/android/AbstractIT.java +++ b/app/src/androidTest/java/com/owncloud/android/AbstractIT.java @@ -377,8 +377,8 @@ public void uploadFile(File file, String remotePath) { public void uploadOCUpload(OCUpload ocUpload) { ConnectivityService connectivityServiceMock = new ConnectivityService() { @Override - public boolean isNetworkAndServerAvailable() throws NetworkOnMainThreadException { - return false; + public void isNetworkAndServerAvailable(@NonNull GenericCallback callback) { + } @Override diff --git a/app/src/androidTest/java/com/owncloud/android/AbstractOnServerIT.java b/app/src/androidTest/java/com/owncloud/android/AbstractOnServerIT.java index 9b5428511b4c..ef79be613ac3 100644 --- a/app/src/androidTest/java/com/owncloud/android/AbstractOnServerIT.java +++ b/app/src/androidTest/java/com/owncloud/android/AbstractOnServerIT.java @@ -189,8 +189,8 @@ public void uploadOCUpload(OCUpload ocUpload) { public void uploadOCUpload(OCUpload ocUpload, int localBehaviour) { ConnectivityService connectivityServiceMock = new ConnectivityService() { @Override - public boolean isNetworkAndServerAvailable() throws NetworkOnMainThreadException { - return false; + public void isNetworkAndServerAvailable(@NonNull GenericCallback callback) { + } @Override diff --git a/app/src/androidTest/java/com/owncloud/android/UploadIT.java b/app/src/androidTest/java/com/owncloud/android/UploadIT.java index 4fa7e58d9392..6e61138aae5c 100644 --- a/app/src/androidTest/java/com/owncloud/android/UploadIT.java +++ b/app/src/androidTest/java/com/owncloud/android/UploadIT.java @@ -59,8 +59,8 @@ public class UploadIT extends AbstractOnServerIT { private ConnectivityService connectivityServiceMock = new ConnectivityService() { @Override - public boolean isNetworkAndServerAvailable() throws NetworkOnMainThreadException { - return false; + public void isNetworkAndServerAvailable(@NonNull GenericCallback callback) { + } @Override @@ -282,8 +282,8 @@ public BatteryStatus getBattery() { public void testUploadOnWifiOnlyButNoWifi() { ConnectivityService connectivityServiceMock = new ConnectivityService() { @Override - public boolean isNetworkAndServerAvailable() throws NetworkOnMainThreadException { - return false; + public void isNetworkAndServerAvailable(@NonNull GenericCallback callback) { + } @Override @@ -371,8 +371,8 @@ public void testUploadOnWifiOnlyAndWifi() { public void testUploadOnWifiOnlyButMeteredWifi() { ConnectivityService connectivityServiceMock = new ConnectivityService() { @Override - public boolean isNetworkAndServerAvailable() throws NetworkOnMainThreadException { - return false; + public void isNetworkAndServerAvailable(@NonNull GenericCallback callback) { + } @Override diff --git a/app/src/androidTest/java/com/owncloud/android/files/services/FileUploaderIT.kt b/app/src/androidTest/java/com/owncloud/android/files/services/FileUploaderIT.kt index a375e451fddd..7b6122a7f1e9 100644 --- a/app/src/androidTest/java/com/owncloud/android/files/services/FileUploaderIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/files/services/FileUploaderIT.kt @@ -34,9 +34,7 @@ abstract class FileUploaderIT : AbstractOnServerIT() { private var uploadsStorageManager: UploadsStorageManager? = null private val connectivityServiceMock: ConnectivityService = object : ConnectivityService { - override fun isNetworkAndServerAvailable(): Boolean { - return false - } + override fun isNetworkAndServerAvailable(callback: ConnectivityService.GenericCallback) = Unit override fun isConnected(): Boolean { return false diff --git a/app/src/androidTest/java/com/owncloud/android/ui/dialog/SyncFileNotEnoughSpaceDialogFragmentTest.java b/app/src/androidTest/java/com/owncloud/android/ui/dialog/SyncFileNotEnoughSpaceDialogFragmentTest.java deleted file mode 100644 index 223a53511812..000000000000 --- a/app/src/androidTest/java/com/owncloud/android/ui/dialog/SyncFileNotEnoughSpaceDialogFragmentTest.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2020 Tobias Kaminsky - * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package com.owncloud.android.ui.dialog; - -import com.owncloud.android.AbstractIT; -import com.owncloud.android.datamodel.OCFile; -import com.owncloud.android.ui.activity.FileDisplayActivity; -import com.owncloud.android.utils.ScreenshotTest; - -import org.junit.Rule; -import org.junit.Test; - -import java.util.Objects; - -import androidx.test.espresso.intent.rule.IntentsTestRule; - -import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; - -public class SyncFileNotEnoughSpaceDialogFragmentTest extends AbstractIT { - @Rule public IntentsTestRule activityRule = new IntentsTestRule<>(FileDisplayActivity.class, - true, - false); - - @Test - @ScreenshotTest - public void showNotEnoughSpaceDialogForFolder() { - FileDisplayActivity test = activityRule.launchActivity(null); - OCFile ocFile = new OCFile("/Document/"); - ocFile.setFileLength(5000000); - ocFile.setFolder(); - - SyncFileNotEnoughSpaceDialogFragment dialog = SyncFileNotEnoughSpaceDialogFragment.newInstance(ocFile, 1000); - dialog.show(test.getListOfFilesFragment().getFragmentManager(), "1"); - - getInstrumentation().waitForIdleSync(); - - screenshot(Objects.requireNonNull(dialog.requireDialog().getWindow()).getDecorView()); - } - - @Test - @ScreenshotTest - public void showNotEnoughSpaceDialogForFile() { - FileDisplayActivity test = activityRule.launchActivity(null); - OCFile ocFile = new OCFile("/Video.mp4"); - ocFile.setFileLength(1000000); - - SyncFileNotEnoughSpaceDialogFragment dialog = SyncFileNotEnoughSpaceDialogFragment.newInstance(ocFile, 2000); - dialog.show(test.getListOfFilesFragment().getFragmentManager(), "2"); - - getInstrumentation().waitForIdleSync(); - - screenshot(Objects.requireNonNull(dialog.requireDialog().getWindow()).getDecorView()); - } -} diff --git a/app/src/androidTest/java/com/owncloud/android/ui/dialog/SyncFileNotEnoughSpaceDialogFragmentTest.kt b/app/src/androidTest/java/com/owncloud/android/ui/dialog/SyncFileNotEnoughSpaceDialogFragmentTest.kt new file mode 100644 index 000000000000..f4f6246e82d6 --- /dev/null +++ b/app/src/androidTest/java/com/owncloud/android/ui/dialog/SyncFileNotEnoughSpaceDialogFragmentTest.kt @@ -0,0 +1,89 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.dialog + +import androidx.annotation.UiThread +import androidx.test.core.app.launchActivity +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isRoot +import com.owncloud.android.AbstractIT +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.ui.activity.FileDisplayActivity +import com.owncloud.android.ui.dialog.SyncFileNotEnoughSpaceDialogFragment.Companion.newInstance +import com.owncloud.android.utils.EspressoIdlingResource +import com.owncloud.android.utils.ScreenshotTest +import org.junit.After +import org.junit.Before +import org.junit.Test + +class SyncFileNotEnoughSpaceDialogFragmentTest : AbstractIT() { + private val testClassName = "com.owncloud.android.ui.dialog.SyncFileNotEnoughSpaceDialogFragmentTest" + + @Before + fun registerIdlingResource() { + IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource) + } + + @After + fun unregisterIdlingResource() { + IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource) + } + + @Test + @ScreenshotTest + @UiThread + fun showNotEnoughSpaceDialogForFolder() { + launchActivity().use { scenario -> + scenario.onActivity { sut -> + val ocFile = OCFile("/Document/").apply { + fileLength = 5000000 + setFolder() + } + + onIdleSync { + EspressoIdlingResource.increment() + newInstance(ocFile, 1000).apply { + show(sut.supportFragmentManager, "1") + } + EspressoIdlingResource.decrement() + + val screenShotName = createName(testClassName + "_" + "showNotEnoughSpaceDialogForFolder", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } + } + } + } + + @Test + @ScreenshotTest + @UiThread + fun showNotEnoughSpaceDialogForFile() { + launchActivity().use { scenario -> + scenario.onActivity { sut -> + val ocFile = OCFile("/Video.mp4").apply { + fileLength = 1000000 + } + + onIdleSync { + EspressoIdlingResource.increment() + newInstance(ocFile, 2000).apply { + show(sut.supportFragmentManager, "2") + } + EspressoIdlingResource.decrement() + + val screenShotName = createName(testClassName + "_" + "showNotEnoughSpaceDialogForFile", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } + } + } + } +} diff --git a/app/src/debug/java/com/nextcloud/test/TestActivity.kt b/app/src/debug/java/com/nextcloud/test/TestActivity.kt index d9a47a42cd0d..ca79eded3f97 100644 --- a/app/src/debug/java/com/nextcloud/test/TestActivity.kt +++ b/app/src/debug/java/com/nextcloud/test/TestActivity.kt @@ -42,6 +42,8 @@ class TestActivity : private lateinit var binding: TestLayoutBinding val connectivityServiceMock: ConnectivityService = object : ConnectivityService { + override fun isNetworkAndServerAvailable(callback: ConnectivityService.GenericCallback) = Unit + override fun isConnected(): Boolean { return false } @@ -53,10 +55,6 @@ class TestActivity : override fun getConnectivity(): Connectivity { return Connectivity.CONNECTED_WIFI } - - override fun isNetworkAndServerAvailable(): Boolean { - return false - } } override fun onCreate(savedInstanceState: Bundle?) { 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 075eb99e9dff..e58a011292a2 100644 --- a/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt +++ b/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt @@ -12,6 +12,7 @@ import androidx.room.AutoMigration import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase +import androidx.room.TypeConverters import com.nextcloud.client.core.Clock import com.nextcloud.client.core.ClockImpl import com.nextcloud.client.database.dao.ArbitraryDataDao @@ -31,6 +32,7 @@ import com.nextcloud.client.database.migrations.DatabaseMigrationUtil import com.nextcloud.client.database.migrations.Migration67to68 import com.nextcloud.client.database.migrations.RoomMigration import com.nextcloud.client.database.migrations.addLegacyMigrations +import com.nextcloud.client.database.typeConverter.OfflineOperationTypeConverter import com.owncloud.android.db.ProviderMeta @Database( @@ -65,11 +67,13 @@ import com.owncloud.android.db.ProviderMeta AutoMigration(from = 80, to = 81), AutoMigration(from = 81, to = 82), AutoMigration(from = 82, to = 83), - AutoMigration(from = 83, to = 84) + AutoMigration(from = 83, to = 84), + AutoMigration(from = 84, to = 85, spec = DatabaseMigrationUtil.DeleteColumnSpec::class) ], exportSchema = true ) @Suppress("Detekt.UnnecessaryAbstractClass") // needed by Room +@TypeConverters(OfflineOperationTypeConverter::class) abstract class NextcloudDatabase : RoomDatabase() { abstract fun arbitraryDataDao(): ArbitraryDataDao @@ -93,6 +97,7 @@ abstract class NextcloudDatabase : RoomDatabase() { instance = Room .databaseBuilder(context, NextcloudDatabase::class.java, ProviderMeta.DB_NAME) .allowMainThreadQueries() + .addTypeConverter(OfflineOperationTypeConverter()) .addLegacyMigrations(clock, context) .addMigrations(RoomMigration()) .addMigrations(Migration67to68()) diff --git a/app/src/main/java/com/nextcloud/client/database/dao/OfflineOperationDao.kt b/app/src/main/java/com/nextcloud/client/database/dao/OfflineOperationDao.kt index 4003e8528403..d798a3533311 100644 --- a/app/src/main/java/com/nextcloud/client/database/dao/OfflineOperationDao.kt +++ b/app/src/main/java/com/nextcloud/client/database/dao/OfflineOperationDao.kt @@ -10,6 +10,7 @@ package com.nextcloud.client.database.dao import androidx.room.Dao import androidx.room.Delete import androidx.room.Insert +import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.Update import com.nextcloud.client.database.entity.OfflineOperationEntity @@ -19,7 +20,7 @@ interface OfflineOperationDao { @Query("SELECT * FROM offline_operations") fun getAll(): List - @Insert + @Insert(onConflict = OnConflictStrategy.REPLACE) fun insert(vararg entity: OfflineOperationEntity) @Update @@ -35,5 +36,8 @@ interface OfflineOperationDao { fun getByPath(path: String): OfflineOperationEntity? @Query("SELECT * FROM offline_operations WHERE offline_operations_parent_oc_file_id = :parentOCFileId") - fun getSubDirectoriesByParentOCFileId(parentOCFileId: Long): List + fun getSubEntitiesByParentOCFileId(parentOCFileId: Long): List + + @Query("DELETE FROM offline_operations") + fun clearTable() } diff --git a/app/src/main/java/com/nextcloud/client/database/entity/OfflineOperationEntity.kt b/app/src/main/java/com/nextcloud/client/database/entity/OfflineOperationEntity.kt index 0ed284722b69..a698cb24e65d 100644 --- a/app/src/main/java/com/nextcloud/client/database/entity/OfflineOperationEntity.kt +++ b/app/src/main/java/com/nextcloud/client/database/entity/OfflineOperationEntity.kt @@ -22,18 +22,18 @@ data class OfflineOperationEntity( @ColumnInfo(name = ProviderTableMeta.OFFLINE_OPERATION_PARENT_OC_FILE_ID) var parentOCFileId: Long? = null, - @ColumnInfo(name = ProviderTableMeta.OFFLINE_OPERATION_PARENT_PATH) - var parentPath: String? = null, + @ColumnInfo(name = ProviderTableMeta.OFFLINE_OPERATION_PATH) + var path: String? = null, @ColumnInfo(name = ProviderTableMeta.OFFLINE_OPERATION_TYPE) var type: OfflineOperationType? = null, - @ColumnInfo(name = ProviderTableMeta.OFFLINE_OPERATION_PATH) - var path: String? = null, - @ColumnInfo(name = ProviderTableMeta.OFFLINE_OPERATION_FILE_NAME) var filename: String? = null, @ColumnInfo(name = ProviderTableMeta.OFFLINE_OPERATION_CREATED_AT) - var createdAt: Long? = null + var createdAt: Long? = null, + + @ColumnInfo(name = ProviderTableMeta.OFFLINE_OPERATION_MODIFIED_AT) + var modifiedAt: Long? = null ) diff --git a/app/src/main/java/com/nextcloud/client/database/migrations/DatabaseMigrationUtil.kt b/app/src/main/java/com/nextcloud/client/database/migrations/DatabaseMigrationUtil.kt index eb1dcb6cd7c0..686a20455f39 100644 --- a/app/src/main/java/com/nextcloud/client/database/migrations/DatabaseMigrationUtil.kt +++ b/app/src/main/java/com/nextcloud/client/database/migrations/DatabaseMigrationUtil.kt @@ -7,6 +7,7 @@ */ package com.nextcloud.client.database.migrations +import androidx.room.DeleteColumn import androidx.room.migration.AutoMigrationSpec import androidx.sqlite.db.SupportSQLiteDatabase @@ -90,4 +91,12 @@ object DatabaseMigrationUtil { super.onPostMigrate(db) } } + + @DeleteColumn.Entries( + DeleteColumn( + tableName = "offline_operations", + columnName = "offline_operations_parent_path" + ) + ) + class DeleteColumnSpec : AutoMigrationSpec } diff --git a/app/src/main/java/com/nextcloud/client/database/typeAdapter/OfflineOperationTypeAdapter.kt b/app/src/main/java/com/nextcloud/client/database/typeAdapter/OfflineOperationTypeAdapter.kt new file mode 100644 index 000000000000..a34c8d7738cd --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/database/typeAdapter/OfflineOperationTypeAdapter.kt @@ -0,0 +1,71 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.database.typeAdapter + +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.google.gson.JsonSerializationContext +import com.google.gson.JsonSerializer +import com.nextcloud.model.OfflineOperationRawType +import com.nextcloud.model.OfflineOperationType + +import java.lang.reflect.Type + +class OfflineOperationTypeAdapter : JsonSerializer, JsonDeserializer { + + override fun serialize( + src: OfflineOperationType?, + typeOfSrc: Type?, + context: JsonSerializationContext? + ): JsonElement { + val jsonObject = JsonObject() + jsonObject.addProperty("type", src?.javaClass?.simpleName) + when (src) { + is OfflineOperationType.CreateFolder -> { + jsonObject.addProperty("type", src.type) + jsonObject.addProperty("path", src.path) + } + + is OfflineOperationType.CreateFile -> { + jsonObject.addProperty("type", src.type) + jsonObject.addProperty("localPath", src.localPath) + jsonObject.addProperty("remotePath", src.remotePath) + jsonObject.addProperty("mimeType", src.mimeType) + } + + null -> Unit + } + return jsonObject + } + + override fun deserialize( + json: JsonElement?, + typeOfT: Type?, + context: JsonDeserializationContext? + ): OfflineOperationType? { + val jsonObject = json?.asJsonObject ?: return null + val type = jsonObject.get("type")?.asString + return when (type) { + OfflineOperationRawType.CreateFolder.name -> OfflineOperationType.CreateFolder( + jsonObject.get("type").asString, + jsonObject.get("path").asString + ) + + OfflineOperationRawType.CreateFile.name -> OfflineOperationType.CreateFile( + jsonObject.get("type").asString, + jsonObject.get("localPath").asString, + jsonObject.get("remotePath").asString, + jsonObject.get("mimeType").asString + ) + + else -> null + } + } +} diff --git a/app/src/main/java/com/nextcloud/client/database/typeConverter/OfflineOperationTypeConverter.kt b/app/src/main/java/com/nextcloud/client/database/typeConverter/OfflineOperationTypeConverter.kt new file mode 100644 index 000000000000..8755c76f7b6d --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/database/typeConverter/OfflineOperationTypeConverter.kt @@ -0,0 +1,33 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.database.typeConverter + +import androidx.room.ProvidedTypeConverter +import androidx.room.TypeConverter +import com.google.gson.Gson +import com.nextcloud.model.OfflineOperationType +import com.google.gson.GsonBuilder +import com.nextcloud.client.database.typeAdapter.OfflineOperationTypeAdapter + +@ProvidedTypeConverter +class OfflineOperationTypeConverter { + + private val gson: Gson = GsonBuilder() + .registerTypeAdapter(OfflineOperationType::class.java, OfflineOperationTypeAdapter()) + .create() + + @TypeConverter + fun fromOfflineOperationType(type: OfflineOperationType?): String? { + return gson.toJson(type) + } + + @TypeConverter + fun toOfflineOperationType(type: String?): OfflineOperationType? { + return gson.fromJson(type, OfflineOperationType::class.java) + } +} 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 43976f1052e6..4eee9aeeb4bb 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt @@ -104,7 +104,13 @@ class BackgroundJobFactory @Inject constructor( } private fun createOfflineOperationsWorker(context: Context, params: WorkerParameters): ListenableWorker { - return OfflineOperationsWorker(accountManager.user, context, connectivityService, viewThemeUtils.get(), params) + return OfflineOperationsWorker( + accountManager.user, + context, + connectivityService, + viewThemeUtils.get(), + params + ) } private fun createFilesExportWork(context: Context, params: WorkerParameters): ListenableWorker { diff --git a/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/OfflineOperationsWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/OfflineOperationsWorker.kt index c974e4f15e99..606ebe3969fd 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/OfflineOperationsWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/OfflineOperationsWorker.kt @@ -23,11 +23,14 @@ import com.owncloud.android.lib.common.OwnCloudClient import com.owncloud.android.lib.common.operations.RemoteOperation import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.files.UploadFileRemoteOperation import com.owncloud.android.operations.CreateFolderOperation import com.owncloud.android.utils.theme.ViewThemeUtils import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.withContext +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine class OfflineOperationsWorker( private val user: User, @@ -48,7 +51,7 @@ class OfflineOperationsWorker( private var repository = OfflineOperationsRepository(fileDataStorageManager) @Suppress("TooGenericExceptionCaught") - override suspend fun doWork(): Result = coroutineScope { + override suspend fun doWork(): Result = withContext(Dispatchers.IO) { val jobName = inputData.getString(JOB_NAME) Log_OC.d( TAG, @@ -57,9 +60,9 @@ class OfflineOperationsWorker( "\n-----------------------------------" ) - if (!connectivityService.isNetworkAndServerAvailable()) { + if (!isNetworkAndServerAvailable()) { Log_OC.d(TAG, "OfflineOperationsWorker cancelled, no internet connection") - return@coroutineScope Result.retry() + return@withContext Result.retry() } val client = clientFactory.create(user) @@ -69,7 +72,7 @@ class OfflineOperationsWorker( val totalOperations = operations.size var currentSuccessfulOperationIndex = 0 - return@coroutineScope try { + return@withContext try { while (operations.isNotEmpty()) { val operation = operations.first() val result = executeOperation(operation, client) @@ -99,27 +102,48 @@ class OfflineOperationsWorker( } } - @Suppress("Deprecation") + private suspend fun isNetworkAndServerAvailable(): Boolean = suspendCoroutine { continuation -> + connectivityService.isNetworkAndServerAvailable { result -> + continuation.resume(result) + } + } + + @Suppress("Deprecation", "MagicNumber") private suspend fun executeOperation( operation: OfflineOperationEntity, client: OwnCloudClient - ): Pair?, RemoteOperation<*>?>? { - return when (operation.type) { - OfflineOperationType.CreateFolder -> { - if (operation.parentPath != null) { - val createFolderOperation = withContext(Dispatchers.IO) { - CreateFolderOperation( - operation.path, - user, - context, - fileDataStorageManager - ) - } - createFolderOperation.execute(client) to createFolderOperation - } else { - Log_OC.d(TAG, "CreateFolder operation incomplete, missing parentPath") - null + ): Pair?, RemoteOperation<*>?>? = withContext(Dispatchers.IO) { + return@withContext when (operation.type) { + is OfflineOperationType.CreateFolder -> { + val createFolderOperation = withContext(NonCancellable) { + val operationType = (operation.type as OfflineOperationType.CreateFolder) + CreateFolderOperation( + operationType.path, + user, + context, + fileDataStorageManager + ) } + createFolderOperation.execute(client) to createFolderOperation + } + + is OfflineOperationType.CreateFile -> { + val createFileOperation = withContext(NonCancellable) { + val operationType = (operation.type as OfflineOperationType.CreateFile) + val lastModificationDate = System.currentTimeMillis() / 1000 + + UploadFileRemoteOperation( + operationType.localPath, + operationType.remotePath, + operationType.mimeType, + "", + operation.modifiedAt ?: lastModificationDate, + operation.createdAt ?: System.currentTimeMillis(), + true + ) + } + + createFileOperation.execute(client) to createFileOperation } else -> { @@ -142,7 +166,7 @@ class OfflineOperationsWorker( } val logMessage = if (result.isSuccess) "Operation completed" else "Operation failed" - Log_OC.d(TAG, "$logMessage path: ${operation.path}, type: ${operation.type}") + Log_OC.d(TAG, "$logMessage filename: ${operation.filename}, type: ${operation.type}") if (result.isSuccess) { repository.updateNextOperations(operation) diff --git a/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/repository/OfflineOperationsRepository.kt b/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/repository/OfflineOperationsRepository.kt index 1d186085b4f6..320967559489 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/repository/OfflineOperationsRepository.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/repository/OfflineOperationsRepository.kt @@ -8,8 +8,11 @@ package com.nextcloud.client.jobs.offlineOperations.repository import com.nextcloud.client.database.entity.OfflineOperationEntity +import com.nextcloud.model.OfflineOperationType import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.utils.MimeType +import com.owncloud.android.utils.MimeTypeUtil class OfflineOperationsRepository( private val fileDataStorageManager: FileDataStorageManager @@ -19,7 +22,7 @@ class OfflineOperationsRepository( private val pathSeparator = '/' @Suppress("NestedBlockDepth") - override fun getAllSubdirectories(fileId: Long): List { + override fun getAllSubEntities(fileId: Long): List { val result = mutableListOf() val queue = ArrayDeque() queue.add(fileId) @@ -31,7 +34,7 @@ class OfflineOperationsRepository( processedIds.add(currentFileId) - val subDirectories = dao.getSubDirectoriesByParentOCFileId(currentFileId) + val subDirectories = dao.getSubEntitiesByParentOCFileId(currentFileId) result.addAll(subDirectories) subDirectories.forEach { @@ -48,15 +51,14 @@ class OfflineOperationsRepository( } override fun deleteOperation(file: OCFile) { - getAllSubdirectories(file.fileId).forEach { - dao.delete(it) + if (file.isFolder) { + getAllSubEntities(file.fileId).forEach { + dao.delete(it) + } } file.decryptedRemotePath?.let { - val entity = dao.getByPath(it) - entity?.let { - dao.delete(entity) - } + dao.deleteByPath(it) } fileDataStorageManager.removeFile(file, true, true) @@ -66,17 +68,28 @@ class OfflineOperationsRepository( val ocFile = fileDataStorageManager.getFileByDecryptedRemotePath(operation.path) val fileId = ocFile?.fileId ?: return - getAllSubdirectories(fileId) + getAllSubEntities(fileId) .mapNotNull { nextOperation -> nextOperation.parentOCFileId?.let { parentId -> fileDataStorageManager.getFileById(parentId)?.let { ocFile -> ocFile.decryptedRemotePath?.let { updatedPath -> - val newParentPath = ocFile.parentRemotePath val newPath = updatedPath + nextOperation.filename + pathSeparator - if (newParentPath != nextOperation.parentPath || newPath != nextOperation.path) { + if (newPath != nextOperation.path) { nextOperation.apply { - parentPath = newParentPath + type = when (type) { + is OfflineOperationType.CreateFile -> + (type as OfflineOperationType.CreateFile).copy( + remotePath = newPath + ) + + is OfflineOperationType.CreateFolder -> + (type as OfflineOperationType.CreateFolder).copy( + path = newPath + ) + + else -> type + } path = newPath } } else { @@ -88,4 +101,15 @@ class OfflineOperationsRepository( } .forEach { dao.update(it) } } + + override fun convertToOCFiles(fileId: Long): List = + dao.getSubEntitiesByParentOCFileId(fileId).map { entity -> + OCFile(entity.path).apply { + mimeType = if (entity.type is OfflineOperationType.CreateFolder) { + MimeType.DIRECTORY + } else { + MimeTypeUtil.getMimeTypeFromPath(entity.path) + } + } + } } diff --git a/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/repository/OfflineOperationsRepositoryType.kt b/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/repository/OfflineOperationsRepositoryType.kt index 4afbf63293e4..b6509093fac9 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/repository/OfflineOperationsRepositoryType.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/repository/OfflineOperationsRepositoryType.kt @@ -11,7 +11,8 @@ import com.nextcloud.client.database.entity.OfflineOperationEntity import com.owncloud.android.datamodel.OCFile interface OfflineOperationsRepositoryType { - fun getAllSubdirectories(fileId: Long): List + fun getAllSubEntities(fileId: Long): List fun deleteOperation(file: OCFile) fun updateNextOperations(operation: OfflineOperationEntity) + fun convertToOCFiles(fileId: Long): List } diff --git a/app/src/main/java/com/nextcloud/client/network/ConnectivityService.java b/app/src/main/java/com/nextcloud/client/network/ConnectivityService.java index 5538e5bf72f5..7da4afe6ea85 100644 --- a/app/src/main/java/com/nextcloud/client/network/ConnectivityService.java +++ b/app/src/main/java/com/nextcloud/client/network/ConnectivityService.java @@ -6,7 +6,8 @@ */ package com.nextcloud.client.network; -import android.os.NetworkOnMainThreadException; + +import androidx.annotation.NonNull; /** * This service provides information about current network connectivity @@ -17,16 +18,12 @@ public interface ConnectivityService { * Checks the availability of the server and the device's internet connection. *

* This method performs a network request to verify if the server is accessible and - * checks if the device has an active internet connection. Due to the network operations involved, - * this method should be executed on a background thread to avoid blocking the main thread. + * checks if the device has an active internet connection. *

* - * @return {@code true} if the server is accessible and the device has an internet connection; - * {@code false} otherwise. - * - * @throws NetworkOnMainThreadException if this function runs on main thread. + * @param callback A callback to handle the result of the network and server availability check. */ - boolean isNetworkAndServerAvailable() throws NetworkOnMainThreadException; + void isNetworkAndServerAvailable(@NonNull GenericCallback callback); boolean isConnected(); @@ -45,4 +42,13 @@ public interface ConnectivityService { * @return Network connectivity status in platform-agnostic format */ Connectivity getConnectivity(); + + /** + * Callback interface for asynchronous results. + * + * @param The type of result returned by the callback. + */ + interface GenericCallback { + void onComplete(T result); + } } diff --git a/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.java b/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.java index 53eaed06ab49..fc5d731bde9c 100644 --- a/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.java +++ b/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.java @@ -13,7 +13,8 @@ import android.net.Network; import android.net.NetworkCapabilities; import android.net.NetworkInfo; -import android.os.NetworkOnMainThreadException; +import android.os.Handler; +import android.os.Looper; import com.nextcloud.client.account.Server; import com.nextcloud.client.account.UserAccountManager; @@ -23,6 +24,7 @@ import org.apache.commons.httpclient.HttpStatus; +import androidx.annotation.NonNull; import androidx.core.net.ConnectivityManagerCompat; import kotlin.jvm.functions.Function1; @@ -36,6 +38,7 @@ class ConnectivityServiceImpl implements ConnectivityService { private final ClientFactory clientFactory; private final GetRequestBuilder requestBuilder; private final WalledCheckCache walledCheckCache; + private final Handler mainThreadHandler = new Handler(Looper.getMainLooper()); static class GetRequestBuilder implements Function1 { @Override @@ -57,16 +60,21 @@ public GetMethod invoke(String url) { } @Override - public boolean isNetworkAndServerAvailable() throws NetworkOnMainThreadException { - Network activeNetwork = platformConnectivityManager.getActiveNetwork(); - NetworkCapabilities networkCapabilities = platformConnectivityManager.getNetworkCapabilities(activeNetwork); - boolean hasInternet = networkCapabilities != null && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); + public void isNetworkAndServerAvailable(@NonNull GenericCallback callback) { + new Thread(() -> { + Network activeNetwork = platformConnectivityManager.getActiveNetwork(); + NetworkCapabilities networkCapabilities = platformConnectivityManager.getNetworkCapabilities(activeNetwork); + boolean hasInternet = networkCapabilities != null && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); - if (!hasInternet) { - return false; - } + boolean result; + if (hasInternet) { + result = !isInternetWalled(); + } else { + result = false; + } - return !isInternetWalled(); + mainThreadHandler.post(() -> callback.onComplete(result)); + }).start(); } @Override diff --git a/app/src/main/java/com/nextcloud/model/OfflineOperationType.kt b/app/src/main/java/com/nextcloud/model/OfflineOperationType.kt index 0b3bcf73131f..65bb25b60899 100644 --- a/app/src/main/java/com/nextcloud/model/OfflineOperationType.kt +++ b/app/src/main/java/com/nextcloud/model/OfflineOperationType.kt @@ -7,6 +7,19 @@ package com.nextcloud.model -enum class OfflineOperationType { - CreateFolder +sealed class OfflineOperationType { + abstract val type: String + + data class CreateFolder(override val type: String, var path: String) : OfflineOperationType() + data class CreateFile( + override val type: String, + val localPath: String, + var remotePath: String, + val mimeType: String + ) : OfflineOperationType() +} + +enum class OfflineOperationRawType { + CreateFolder, + CreateFile } diff --git a/app/src/main/java/com/nextcloud/receiver/NetworkChangeReceiver.kt b/app/src/main/java/com/nextcloud/receiver/NetworkChangeReceiver.kt index fd17eaf7f7f1..d9d7cbea18ef 100644 --- a/app/src/main/java/com/nextcloud/receiver/NetworkChangeReceiver.kt +++ b/app/src/main/java/com/nextcloud/receiver/NetworkChangeReceiver.kt @@ -11,9 +11,6 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import com.nextcloud.client.network.ConnectivityService -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch interface NetworkChangeListener { fun networkAndServerConnectionListener(isNetworkAndServerAvailable: Boolean) @@ -24,15 +21,9 @@ class NetworkChangeReceiver( private val connectivityService: ConnectivityService ) : BroadcastReceiver() { - private val scope = CoroutineScope(Dispatchers.IO) - override fun onReceive(context: Context, intent: Intent?) { - scope.launch { - val isNetworkAndServerAvailable = connectivityService.isNetworkAndServerAvailable() - - launch(Dispatchers.Main) { - listener.networkAndServerConnectionListener(isNetworkAndServerAvailable) - } + connectivityService.isNetworkAndServerAvailable { + listener.networkAndServerConnectionListener(it) } } } diff --git a/app/src/main/java/com/nextcloud/receiver/OfflineOperationActionReceiver.kt b/app/src/main/java/com/nextcloud/receiver/OfflineOperationActionReceiver.kt index 2a4cf8819aa9..df9f5fd15949 100644 --- a/app/src/main/java/com/nextcloud/receiver/OfflineOperationActionReceiver.kt +++ b/app/src/main/java/com/nextcloud/receiver/OfflineOperationActionReceiver.kt @@ -25,6 +25,5 @@ class OfflineOperationActionReceiver : BroadcastReceiver() { val user = intent.getParcelableArgument(USER, User::class.java) ?: return val fileDataStorageManager = FileDataStorageManager(user, context?.contentResolver) fileDataStorageManager.offlineOperationDao.deleteByPath(path) - // TODO Update notification } } diff --git a/app/src/main/java/com/nextcloud/utils/extensions/FragmentExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/FragmentExtensions.kt index e8a71ea9eef9..aa119ddd60a5 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/FragmentExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/FragmentExtensions.kt @@ -16,3 +16,14 @@ inline fun Fragment.typedActivity(): T? { null } } + +/** + * Extension for Java Classes + */ +fun Fragment.getTypedActivity(type: Class): T? { + return if (isAdded && activity != null && type.isInstance(activity)) { + type.cast(activity) + } else { + null + } +} diff --git a/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt b/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt index 516b7d425b90..0c451cebb670 100644 --- a/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt +++ b/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt @@ -32,7 +32,7 @@ object FileNameValidator { * @param existedFileNames Set of existing file names to avoid duplicates. * @return An error message if the filename is invalid, null otherwise. */ - @Suppress("ReturnCount") + @Suppress("ReturnCount", "NestedBlockDepth") fun checkFileName( filename: String, capability: OCCapability, 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 84f7bc6e4bcb..a2963f75cfc3 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java @@ -41,6 +41,7 @@ import com.nextcloud.client.jobs.offlineOperations.repository.OfflineOperationsRepository; import com.nextcloud.client.jobs.offlineOperations.repository.OfflineOperationsRepositoryType; import com.nextcloud.model.OCFileFilterType; +import com.nextcloud.model.OfflineOperationRawType; import com.nextcloud.model.OfflineOperationType; import com.nextcloud.utils.date.DateFormatPattern; import com.nextcloud.utils.extensions.DateExtensionsKt; @@ -107,7 +108,7 @@ public class FileDataStorageManager { public final OfflineOperationDao offlineOperationDao = NextcloudDatabase.getInstance(MainApp.getAppContext()).offlineOperationDao(); private final FileDao fileDao = NextcloudDatabase.getInstance(MainApp.getAppContext()).fileDao(); private final Gson gson = new Gson(); - private final OfflineOperationsRepositoryType offlineOperationsRepository; + public final OfflineOperationsRepositoryType offlineOperationsRepository; public FileDataStorageManager(User user, ContentResolver contentResolver) { this.contentProviderClient = null; @@ -140,33 +141,83 @@ OCFile getFileByDecryptedRemotePath(String path) { return getFileByPath(ProviderTableMeta.FILE_PATH_DECRYPTED, path); } - public OfflineOperationEntity addCreateFolderOfflineOperation(String path, String filename, String parentPath, Long parentOCFileId) { + public void addCreateFileOfflineOperation(String[] localPaths, String[] remotePaths) { + if (localPaths.length != remotePaths.length) { + Log_OC.d(TAG, "Local path and remote path size do not match"); + return; + } + + for (int i = 0; i < localPaths.length; i++) { + String localPath = localPaths[i]; + String remotePath = remotePaths[i]; + String mimeType = MimeTypeUtil.getMimeTypeFromPath(remotePath); + + OfflineOperationEntity entity = new OfflineOperationEntity(); + entity.setPath(remotePath); + entity.setType(new OfflineOperationType.CreateFile(OfflineOperationRawType.CreateFile.name(), localPath, remotePath, mimeType)); + + long createdAt = System.currentTimeMillis(); + long modificationTimestamp = System.currentTimeMillis(); + + entity.setCreatedAt(createdAt); + entity.setModifiedAt(modificationTimestamp / 1000); + entity.setFilename(new File(remotePath).getName()); + + String parentPath = new File(remotePath).getParent() + OCFile.PATH_SEPARATOR; + OCFile parentFile = getFileByDecryptedRemotePath(parentPath); + + if (parentFile != null) { + entity.setParentOCFileId(parentFile.getFileId()); + } + + offlineOperationDao.insert(entity); + createPendingFile(remotePath, mimeType, createdAt, modificationTimestamp); + } + } + + public OfflineOperationEntity addCreateFolderOfflineOperation(String path, String filename, Long parentOCFileId) { OfflineOperationEntity entity = new OfflineOperationEntity(); entity.setFilename(filename); entity.setParentOCFileId(parentOCFileId); + + OfflineOperationType.CreateFolder operationType = new OfflineOperationType.CreateFolder(OfflineOperationRawType.CreateFolder.name(), path); + entity.setType(operationType); entity.setPath(path); - entity.setParentPath(parentPath); - entity.setCreatedAt(System.currentTimeMillis() / 1000L); - entity.setType(OfflineOperationType.CreateFolder); + + long createdAt = System.currentTimeMillis(); + long modificationTimestamp = System.currentTimeMillis(); + + entity.setCreatedAt(createdAt); + entity.setModifiedAt(modificationTimestamp / 1000); offlineOperationDao.insert(entity); - createPendingDirectory(path); + createPendingDirectory(path, createdAt, modificationTimestamp); return entity; } - public void createPendingDirectory(String path) { + public void createPendingFile(String path, String mimeType, long createdAt, long modificationTimestamp) { OCFile file = new OCFile(path); - file.setMimeType(MimeType.DIRECTORY); + file.setMimeType(mimeType); + file.setCreationTimestamp(createdAt); + file.setModificationTimestamp(modificationTimestamp); saveFileWithParent(file, MainApp.getAppContext()); } + public void createPendingDirectory(String path, long createdAt, long modificationTimestamp) { + OCFile directory = new OCFile(path); + directory.setMimeType(MimeType.DIRECTORY); + directory.setCreationTimestamp(createdAt); + directory.setModificationTimestamp(modificationTimestamp); + saveFileWithParent(directory, MainApp.getAppContext()); + } + public void deleteOfflineOperation(OCFile file) { offlineOperationsRepository.deleteOperation(file); } - public void renameCreateFolderOfflineOperation(OCFile file, String newFolderName) { + public void renameOfflineOperation(OCFile file, String newFolderName) { var entity = offlineOperationDao.getByPath(file.getDecryptedRemotePath()); if (entity == null) { return; @@ -178,6 +229,14 @@ public void renameCreateFolderOfflineOperation(OCFile file, String newFolderName } String newPath = parentFolder.getDecryptedRemotePath() + newFolderName + OCFile.PATH_SEPARATOR; + + if (entity.getType() instanceof OfflineOperationType.CreateFolder createFolderType) { + createFolderType.setPath(newPath); + } else if (entity.getType() instanceof OfflineOperationType.CreateFile createFileType) { + createFileType.setRemotePath(newPath); + } + entity.setType(entity.getType()); + entity.setPath(newPath); entity.setFilename(newFolderName); offlineOperationDao.update(entity); 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 b8de6c314ac7..d4bfa5164f2b 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/OCFile.java +++ b/app/src/main/java/com/owncloud/android/datamodel/OCFile.java @@ -788,18 +788,6 @@ public boolean isOfflineOperation() { return getRemoteId() == null; } - public String getOfflineOperationParentPath() { - if (isOfflineOperation()) { - if (Objects.equals(remotePath, OCFile.PATH_SEPARATOR)) { - return OCFile.PATH_SEPARATOR; - } else { - return null; - } - } else { - return getDecryptedRemotePath(); - } - } - public String getEtagInConflict() { return this.etagInConflict; } 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 6a5e9947bd2d..36ba052e262b 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 = 84; + public static final int DB_VERSION = 85; private ProviderMeta() { // No instance @@ -289,9 +289,9 @@ static public class ProviderTableMeta implements BaseColumns { // Columns of offline operation table public static final String OFFLINE_OPERATION_PARENT_OC_FILE_ID = "offline_operations_parent_oc_file_id"; - public static final String OFFLINE_OPERATION_PARENT_PATH = "offline_operations_parent_path"; public static final String OFFLINE_OPERATION_TYPE = "offline_operations_type"; public static final String OFFLINE_OPERATION_PATH = "offline_operations_path"; + public static final String OFFLINE_OPERATION_MODIFIED_AT = "offline_operations_modified_at"; public static final String OFFLINE_OPERATION_CREATED_AT = "offline_operations_created_at"; public static final String OFFLINE_OPERATION_FILE_NAME = "offline_operations_file_name"; diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java index ba8aa8bef4cd..95b726f2f67b 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java @@ -165,8 +165,7 @@ public abstract class FileActivity extends DrawerActivity @Inject UserAccountManager accountManager; - @Inject - ConnectivityService connectivityService; + @Inject public ConnectivityService connectivityService; @Inject BackgroundJobManager backgroundJobManager; @@ -246,6 +245,7 @@ protected void onCreate(Bundle savedInstanceState) { public void networkAndServerConnectionListener(boolean isNetworkAndServerAvailable) { if (isNetworkAndServerAvailable) { hideInfoBox(); + refreshList(); } else { showInfoBox(R.string.offline_mode); } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java index b52eab01ea1b..b7b9b2dc14ac 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java @@ -236,8 +236,6 @@ public class FileDisplayActivity extends FileActivity @Inject AppInfo appInfo; - @Inject ConnectivityService connectivityService; - @Inject InAppReviewHelper inAppReviewHelper; @Inject FastScrollUtils fastScrollUtils; @@ -952,16 +950,21 @@ private void requestUploadOfFilesFromFileSystem(String localBasePath, String[] f default -> FileUploadWorker.LOCAL_BEHAVIOUR_FORGET; }; - FileUploadHelper.Companion.instance().uploadNewFiles(getUser().orElseThrow(RuntimeException::new), - filePaths, - decryptedRemotePaths, - behaviour, - true, - UploadFileOperation.CREATED_BY_USER, - false, - false, - NameCollisionPolicy.ASK_USER); - + connectivityService.isNetworkAndServerAvailable(result -> { + if (result) { + FileUploadHelper.Companion.instance().uploadNewFiles(getUser().orElseThrow(RuntimeException::new), + filePaths, + decryptedRemotePaths, + behaviour, + true, + UploadFileOperation.CREATED_BY_USER, + false, + false, + NameCollisionPolicy.ASK_USER); + } else { + fileDataStorageManager.addCreateFileOfflineOperation(filePaths, decryptedRemotePaths); + } + }); } else { Log_OC.d(TAG, "User clicked on 'Update' with no selection"); DisplayUtils.showSnackMessage(this, R.string.filedisplay_no_file_selected); @@ -1379,7 +1382,13 @@ private void setBackgroundText() { if (MainApp.isOnlyOnDevice()) { ocFileListFragment.setMessageForEmptyList(R.string.file_list_empty_headline, R.string.file_list_empty_on_device, R.drawable.ic_list_empty_folder, true); } else { - ocFileListFragment.setEmptyListMessage(SearchType.NO_SEARCH); + connectivityService.isNetworkAndServerAvailable(result -> { + if (result) { + ocFileListFragment.setEmptyListMessage(SearchType.NO_SEARCH); + } else { + ocFileListFragment.setEmptyListMessage(SearchType.OFFLINE_MODE); + } + }); } } } else { diff --git a/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.java index 161de94c2ea1..f18270acf77d 100755 --- a/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.java @@ -27,7 +27,6 @@ import com.nextcloud.client.jobs.BackgroundJobManager; import com.nextcloud.client.jobs.upload.FileUploadHelper; import com.nextcloud.client.jobs.upload.FileUploadWorker; -import com.nextcloud.client.network.ConnectivityService; import com.nextcloud.client.utils.Throttler; import com.nextcloud.model.WorkerState; import com.nextcloud.model.WorkerStateLiveData; @@ -44,7 +43,6 @@ import com.owncloud.android.ui.decoration.MediaGridItemDecoration; import com.owncloud.android.utils.DisplayUtils; import com.owncloud.android.utils.FilesSyncHelper; -import com.owncloud.android.utils.theme.ViewThemeUtils; import javax.inject.Inject; @@ -73,9 +71,6 @@ public class UploadListActivity extends FileActivity { @Inject UploadsStorageManager uploadsStorageManager; - @Inject - ConnectivityService connectivityService; - @Inject PowerManagementService powerManagementService; diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java index 24b6f2414a29..eba470e3697d 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java @@ -16,9 +16,11 @@ import android.app.Activity; import android.content.ContentValues; import android.content.res.Resources; +import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; +import android.os.Build; import android.os.Handler; import android.os.Looper; import android.text.TextUtils; @@ -31,8 +33,10 @@ import com.elyeproj.loaderviewlibrary.LoaderImageView; import com.nextcloud.android.common.ui.theme.utils.ColorRole; import com.nextcloud.client.account.User; +import com.nextcloud.client.database.entity.OfflineOperationEntity; import com.nextcloud.client.jobs.upload.FileUploadHelper; import com.nextcloud.client.preferences.AppPreferences; +import com.nextcloud.model.OfflineOperationType; import com.nextcloud.model.OCFileFilterType; import com.nextcloud.utils.extensions.ViewExtensionsKt; import com.owncloud.android.MainApp; @@ -66,6 +70,7 @@ import com.owncloud.android.ui.fragment.SearchType; import com.owncloud.android.ui.interfaces.OCFileListFragmentInterface; import com.owncloud.android.ui.preview.PreviewTextFragment; +import com.owncloud.android.utils.BitmapUtils; import com.owncloud.android.utils.DisplayUtils; import com.owncloud.android.utils.FileSortOrder; import com.owncloud.android.utils.FileStorageUtils; @@ -81,8 +86,12 @@ import java.util.Date; import java.util.List; import java.util.Locale; +import java.util.Objects; import java.util.Set; import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.stream.Collectors; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -161,7 +170,7 @@ public OCFileListAdapter( userId = AccountManager .get(activity) .getUserData(this.user.toPlatformAccount(), - com.owncloud.android.lib.common.accounts.AccountUtils.Constants.KEY_USER_ID); + AccountUtils.Constants.KEY_USER_ID); this.viewThemeUtils = viewThemeUtils; @@ -523,7 +532,11 @@ private void bindListGridItemViewHolder(ListGridItemViewHolder holder, OCFile fi } ViewExtensionsKt.setVisibleIf(holder.getShared(), !file.isOfflineOperation()); - setColorFilterForOfflineOperations(holder, file); + if (file.isFolder()) { + setColorFilterForOfflineCreateFolderOperations(holder, file); + } else { + setColorFilterForOfflineCreateFileOperations(holder, file); + } } private void bindListItemViewHolder(ListItemViewHolder holder, OCFile file) { @@ -596,13 +609,14 @@ private void bindListItemViewHolder(ListItemViewHolder holder, OCFile file) { holder.getFileSize().setVisibility(View.VISIBLE); + if (file.isOfflineOperation()) { holder.getFileSize().setText(MainApp.string(R.string.oc_file_list_adapter_offline_operation_description_text)); - holder.getFileSizeSeparator().setVisibility(View.GONE); } else { holder.getFileSize().setText(DisplayUtils.bytesToHumanReadable(localSize)); - holder.getFileSizeSeparator().setVisibility(View.VISIBLE); } + + holder.getFileSizeSeparator().setVisibility(View.VISIBLE); } else { final long fileLength = file.getFileLength(); if (fileLength >= 0) { @@ -610,11 +624,11 @@ private void bindListItemViewHolder(ListItemViewHolder holder, OCFile file) { if (file.isOfflineOperation()) { holder.getFileSize().setText(MainApp.string(R.string.oc_file_list_adapter_offline_operation_description_text)); - holder.getFileSizeSeparator().setVisibility(View.GONE); } else { holder.getFileSize().setText(DisplayUtils.bytesToHumanReadable(fileLength)); - holder.getFileSizeSeparator().setVisibility(View.VISIBLE); } + + holder.getFileSizeSeparator().setVisibility(View.VISIBLE); } else { holder.getFileSize().setVisibility(View.GONE); holder.getFileSizeSeparator().setVisibility(View.GONE); @@ -654,14 +668,40 @@ private void bindListItemViewHolder(ListItemViewHolder holder, OCFile file) { private void applyVisualsForOfflineOperations(ListItemViewHolder holder, OCFile file) { ViewExtensionsKt.setVisibleIf(holder.getShared(), !file.isOfflineOperation()); - setColorFilterForOfflineOperations(holder, file); + + if (file.isFolder()) { + setColorFilterForOfflineCreateFolderOperations(holder, file); + } else { + setColorFilterForOfflineCreateFileOperations(holder, file); + } } - private void setColorFilterForOfflineOperations(ListViewHolder holder, OCFile file) { - if (!file.isFolder()) { + private final ExecutorService executorService = Executors.newCachedThreadPool(); + private final Handler mainHandler = new Handler(Looper.getMainLooper()); + + private void setColorFilterForOfflineCreateFileOperations(ListViewHolder holder, OCFile file) { + if (!file.isOfflineOperation()) { return; } + executorService.execute(() -> { + OfflineOperationEntity entity = mStorageManager.offlineOperationDao.getByPath(file.getDecryptedRemotePath()); + + if (entity != null && entity.getType() != null && entity.getType() instanceof OfflineOperationType.CreateFile createFileOperation) { + Bitmap bitmap = BitmapUtils.decodeSampledBitmapFromFile(createFileOperation.getLocalPath(), holder.getThumbnail().getWidth(), holder.getThumbnail().getHeight()); + if (bitmap == null) return; + + Bitmap thumbnail = BitmapUtils.addColorFilter(bitmap, Color.GRAY,100); + mainHandler.post(() -> holder.getThumbnail().setImageBitmap(thumbnail)); + } + }); + } + + public void onDestroy() { + executorService.shutdown(); + } + + private void setColorFilterForOfflineCreateFolderOperations(ListViewHolder holder, OCFile file) { if (file.isOfflineOperation()) { holder.getThumbnail().setColorFilter(Color.GRAY, PorterDuff.Mode.SRC_IN); } else { @@ -782,6 +822,7 @@ public void swapDirectory( prepareListOfHiddenFiles(); mergeOCFilesForLivePhoto(); mFilesAll.clear(); + addOfflineOperations(directory.getFileId()); mFilesAll.addAll(mFiles); currentDirectory = directory; } else { @@ -790,10 +831,39 @@ public void swapDirectory( } searchType = null; - notifyDataSetChanged(); } + /** + * Converts Offline Operations to OCFiles and adds them to the adapter for visual feedback. + * This function creates pending OCFiles, but they may not consistently appear in the UI. + * The issue arises when {@link RefreshFolderOperation} deletes pending Offline Operations, while some may still exist in the table. + * If only this function is used, it cause crash in {@link FileDisplayActivity mSyncBroadcastReceiver.onReceive}. + *

+ * These function also need to be used: {@link FileDataStorageManager#createPendingDirectory(String, long, long)}, {@link FileDataStorageManager#createPendingFile(String, String, long, long)}. + */ + private void addOfflineOperations(long fileId) { + List offlineOperations = mStorageManager.offlineOperationsRepository.convertToOCFiles(fileId); + if (offlineOperations.isEmpty()) { + return; + } + + List newFiles; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + newFiles = offlineOperations.stream() + .filter(offlineFile -> mFilesAll.stream() + .noneMatch(file -> Objects.equals(file.getDecryptedRemotePath(), offlineFile.getDecryptedRemotePath()))) + .toList(); + } else { + newFiles = offlineOperations.stream() + .filter(offlineFile -> mFilesAll.stream() + .noneMatch(file -> Objects.equals(file.getDecryptedRemotePath(), offlineFile.getDecryptedRemotePath()))) + .collect(Collectors.toList()); + } + + mFilesAll.addAll(newFiles); + } + public void setData(List objects, SearchType searchType, FileDataStorageManager storageManager, diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt index 995e07bf3e3f..186e628e7f8a 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt @@ -16,11 +16,15 @@ object OCShareToOCFileConverter { private const val MILLIS_PER_SECOND = 1000 /** - * Generates a list of incomplete [OCFile] from a list of [OCShare] + * Generates a list of incomplete [OCFile] from a list of [OCShare]. Retrieving OCFile directly by path may fail + * in cases like + * when a shared file is located at a/b/c/d/a.txt. To display a.txt in the shared tab, the device needs the OCFile. + * On first launch, the app may not be aware of the file until the exact path is accessed. * - * This is actually pretty complex as we get one [OCShare] item for each shared instance for the same folder + * Server implementation needed to get file size, thumbnails e.g. : + * ()?.refreshCurrentDirectory() } } } diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.kt index 00ca751ee02e..5c2d1c77dc53 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.kt @@ -146,7 +146,7 @@ class RenameFileDialogFragment : DialogFragment(), DialogInterface.OnClickListen } if (mTargetFile?.isOfflineOperation == true) { - fileDataStorageManager.renameCreateFolderOfflineOperation(mTargetFile, newFileName) + fileDataStorageManager.renameOfflineOperation(mTargetFile, newFileName) if (requireActivity() is FileDisplayActivity) { val activity = requireActivity() as FileDisplayActivity activity.refreshCurrentDirectory() diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/ExtendedListFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/ExtendedListFragment.java index 90d5ff624628..8922e792958b 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/ExtendedListFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/ExtendedListFragment.java @@ -45,6 +45,7 @@ import com.nextcloud.client.di.Injectable; import com.nextcloud.client.preferences.AppPreferences; import com.nextcloud.client.preferences.AppPreferencesImpl; +import com.nextcloud.utils.extensions.FragmentExtensionsKt; import com.owncloud.android.MainApp; import com.owncloud.android.R; import com.owncloud.android.databinding.ListFragmentBinding; @@ -52,6 +53,7 @@ import com.owncloud.android.lib.resources.files.SearchRemoteOperation; import com.owncloud.android.lib.resources.status.OwnCloudVersion; import com.owncloud.android.ui.EmptyRecyclerView; +import com.owncloud.android.ui.activity.FileActivity; import com.owncloud.android.ui.activity.FileDisplayActivity; import com.owncloud.android.ui.activity.FolderPickerActivity; import com.owncloud.android.ui.activity.OnEnforceableRefreshListener; @@ -367,6 +369,10 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, public void onDestroyView() { super.onDestroyView(); binding = null; + var adapter = getRecyclerView().getAdapter(); + if (adapter instanceof OCFileListAdapter ocFileListAdapter) { + ocFileListAdapter.onDestroy(); + } } private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener { @@ -578,69 +584,67 @@ public void setMessageForEmptyList(@StringRes final int headline, @StringRes fin */ public void setMessageForEmptyList(@StringRes final int headline, @StringRes final int message, @DrawableRes final int icon, final boolean tintIcon) { - new Handler(Looper.getMainLooper()).post(new Runnable() { - @Override - public void run() { - - if (mEmptyListContainer != null && mEmptyListMessage != null) { - mEmptyListHeadline.setText(headline); - mEmptyListMessage.setText(message); - - if (tintIcon) { - if (getContext() != null) { - mEmptyListIcon.setImageDrawable( - viewThemeUtils.platform.tintPrimaryDrawable(getContext(), icon)); - } - } else { - mEmptyListIcon.setImageResource(icon); - } + new Handler(Looper.getMainLooper()).post(() -> { - mEmptyListIcon.setVisibility(View.VISIBLE); - mEmptyListMessage.setVisibility(View.VISIBLE); + if (mEmptyListContainer != null && mEmptyListMessage != null) { + mEmptyListHeadline.setText(headline); + mEmptyListMessage.setText(message); + + if (tintIcon) { + if (getContext() != null) { + mEmptyListIcon.setImageDrawable( + viewThemeUtils.platform.tintPrimaryDrawable(getContext(), icon)); + } + } else { + mEmptyListIcon.setImageResource(icon); } + + mEmptyListIcon.setVisibility(View.VISIBLE); + mEmptyListMessage.setVisibility(View.VISIBLE); } }); } public void setEmptyListMessage(final SearchType searchType) { - new Handler(Looper.getMainLooper()).post(new Runnable() { - @Override - public void run() { - - if (searchType == SearchType.NO_SEARCH) { - setMessageForEmptyList(R.string.file_list_empty_headline, - R.string.file_list_empty, - R.drawable.ic_list_empty_folder, - true); - } else if (searchType == SearchType.FILE_SEARCH) { - setMessageForEmptyList(R.string.file_list_empty_headline_server_search, - R.string.file_list_empty, - R.drawable.ic_search_light_grey); - } else if (searchType == SearchType.FAVORITE_SEARCH) { - setMessageForEmptyList(R.string.file_list_empty_favorite_headline, - R.string.file_list_empty_favorites_filter_list, - R.drawable.ic_star_light_yellow); - } else if (searchType == SearchType.RECENTLY_MODIFIED_SEARCH) { - setMessageForEmptyList(R.string.file_list_empty_headline_server_search, - R.string.file_list_empty_recently_modified, - R.drawable.ic_list_empty_recent); - } else if (searchType == SearchType.REGULAR_FILTER) { - setMessageForEmptyList(R.string.file_list_empty_headline_search, - R.string.file_list_empty_search, - R.drawable.ic_search_light_grey); - } else if (searchType == SearchType.SHARED_FILTER) { - setMessageForEmptyList(R.string.file_list_empty_shared_headline, - R.string.file_list_empty_shared, - R.drawable.ic_list_empty_shared); - } else if (searchType == SearchType.GALLERY_SEARCH) { - setMessageForEmptyList(R.string.file_list_empty_headline_server_search, - R.string.file_list_empty_gallery, - R.drawable.file_image); - } else if (searchType == SearchType.LOCAL_SEARCH) { - setMessageForEmptyList(R.string.file_list_empty_headline_server_search, - R.string.file_list_empty_local_search, - R.drawable.ic_search_light_grey); - } + new Handler(Looper.getMainLooper()).post(() -> { + if (searchType == SearchType.OFFLINE_MODE) { + setMessageForEmptyList(R.string.offline_mode_info_title, + R.string.offline_mode_info_description, + R.drawable.ic_cloud_sync, + true); + } else if (searchType == SearchType.NO_SEARCH) { + setMessageForEmptyList(R.string.file_list_empty_headline, + R.string.file_list_empty, + R.drawable.ic_list_empty_folder, + true); + } else if (searchType == SearchType.FILE_SEARCH) { + setMessageForEmptyList(R.string.file_list_empty_headline_server_search, + R.string.file_list_empty, + R.drawable.ic_search_light_grey); + } else if (searchType == SearchType.FAVORITE_SEARCH) { + setMessageForEmptyList(R.string.file_list_empty_favorite_headline, + R.string.file_list_empty_favorites_filter_list, + R.drawable.ic_star_light_yellow); + } else if (searchType == SearchType.RECENTLY_MODIFIED_SEARCH) { + setMessageForEmptyList(R.string.file_list_empty_headline_server_search, + R.string.file_list_empty_recently_modified, + R.drawable.ic_list_empty_recent); + } else if (searchType == SearchType.REGULAR_FILTER) { + setMessageForEmptyList(R.string.file_list_empty_headline_search, + R.string.file_list_empty_search, + R.drawable.ic_search_light_grey); + } else if (searchType == SearchType.SHARED_FILTER) { + setMessageForEmptyList(R.string.file_list_empty_shared_headline, + R.string.file_list_empty_shared, + R.drawable.ic_list_empty_shared); + } else if (searchType == SearchType.GALLERY_SEARCH) { + setMessageForEmptyList(R.string.file_list_empty_headline_server_search, + R.string.file_list_empty_gallery, + R.drawable.file_image); + } else if (searchType == SearchType.LOCAL_SEARCH) { + setMessageForEmptyList(R.string.file_list_empty_headline_server_search, + R.string.file_list_empty_local_search, + R.drawable.ic_search_light_grey); } }); } @@ -650,11 +654,15 @@ public void run() { */ public void setEmptyListLoadingMessage() { new Handler(Looper.getMainLooper()).post(() -> { - if (mEmptyListContainer != null && mEmptyListMessage != null) { - mEmptyListHeadline.setText(R.string.file_list_loading); - mEmptyListMessage.setText(""); - - mEmptyListIcon.setVisibility(View.GONE); + FileActivity fileActivity = FragmentExtensionsKt.getTypedActivity(this, FileActivity.class); + if (fileActivity != null) { + fileActivity.connectivityService.isNetworkAndServerAvailable(result -> { + if (!result || mEmptyListContainer == null || mEmptyListMessage == null) return; + + mEmptyListHeadline.setText(R.string.file_list_loading); + mEmptyListMessage.setText(""); + mEmptyListIcon.setVisibility(View.GONE); + }); } }); } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java index 071faeee4666..6523c6ceaed5 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java @@ -346,7 +346,9 @@ public void copyLink(OCShare share) { @Override @VisibleForTesting public void showSharingMenuActionSheet(OCShare share) { - new FileDetailSharingMenuBottomSheetDialog(fileActivity, this, share, viewThemeUtils).show(); + if (fileActivity != null && !fileActivity.isFinishing()) { + new FileDetailSharingMenuBottomSheetDialog(fileActivity, this, share, viewThemeUtils).show(); + } } /** diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetDialog.java b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetDialog.java index edd5011ff412..f5458da9e546 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetDialog.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetDialog.java @@ -214,19 +214,22 @@ private void setupClickListener() { private void filterActionsForOfflineOperations() { if (file == null) return; - if (!file.isOfflineOperation() || file.isRootDirectory()) { - return; - } + fileActivity.connectivityService.isNetworkAndServerAvailable(result -> { + if (file.isRootDirectory()) { + return; + } - binding.menuCreateRichWorkspace.setVisibility(View.GONE); - binding.menuUploadFromApp.setVisibility(View.GONE); - binding.menuDirectCameraUpload.setVisibility(View.GONE); - binding.menuScanDocUpload.setVisibility(View.GONE); - binding.menuUploadFiles.setVisibility(View.GONE); - binding.menuNewDocument.setVisibility(View.GONE); - binding.menuNewSpreadsheet.setVisibility(View.GONE); - binding.menuNewPresentation.setVisibility(View.GONE); - binding.creatorsContainer.setVisibility(View.GONE); + if (!result || file.isOfflineOperation()) { + binding.menuCreateRichWorkspace.setVisibility(View.GONE); + binding.menuUploadFromApp.setVisibility(View.GONE); + binding.menuDirectCameraUpload.setVisibility(View.GONE); + binding.menuScanDocUpload.setVisibility(View.GONE); + binding.menuNewDocument.setVisibility(View.GONE); + binding.menuNewSpreadsheet.setVisibility(View.GONE); + binding.menuNewPresentation.setVisibility(View.GONE); + binding.creatorsContainer.setVisibility(View.GONE); + } + }); } @Override 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 9dc205647421..812974d98083 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 @@ -638,7 +638,16 @@ public void openActionsMenu(final int filesCount, final Set checkedFiles for (OCFile file : checkedFiles) { if (file.isOfflineOperation()) { toHide = new ArrayList<>( - Arrays.asList(R.id.action_favorite, R.id.action_move_or_copy, R.id.action_sync_file, R.id.action_encrypted, R.id.action_unset_encrypted) + Arrays.asList(R.id.action_favorite, + R.id.action_move_or_copy, + R.id.action_sync_file, + R.id.action_encrypted, + R.id.action_unset_encrypted, + R.id.action_edit, + R.id.action_download_file, + R.id.action_export_file, + R.id.action_set_as_wallpaper + ) ); break; } @@ -1129,10 +1138,16 @@ private void folderOnItemClick(OCFile file, int position) { Log_OC.d(TAG, "no public key for " + user.getAccountName()); FragmentManager fragmentManager = getParentFragmentManager(); - if (fragmentManager.findFragmentByTag(SETUP_ENCRYPTION_DIALOG_TAG) == null) { - SetupEncryptionDialogFragment dialog = SetupEncryptionDialogFragment.newInstance(user, position); - dialog.setTargetFragment(this, SETUP_ENCRYPTION_REQUEST_CODE); - dialog.show(fragmentManager, SETUP_ENCRYPTION_DIALOG_TAG); + if (fragmentManager.findFragmentByTag(SETUP_ENCRYPTION_DIALOG_TAG) == null && requireActivity() instanceof FileActivity fileActivity) { + fileActivity.connectivityService.isNetworkAndServerAvailable(result -> { + if (result) { + SetupEncryptionDialogFragment dialog = SetupEncryptionDialogFragment.newInstance(user, position); + dialog.setTargetFragment(this, SETUP_ENCRYPTION_REQUEST_CODE); + dialog.show(fragmentManager, SETUP_ENCRYPTION_DIALOG_TAG); + } else { + DisplayUtils.showSnackMessage(fileActivity, R.string.internet_connection_required_for_encrypted_folder_setup); + } + }); } } } else { @@ -1143,13 +1158,25 @@ private void folderOnItemClick(OCFile file, int position) { } } - private void fileOnItemClick(OCFile file) { + private Integer checkFileBeforeOpen(OCFile file) { if (isAPKorAAB(Set.of(file))) { + return R.string.gplay_restriction; + } else if (file.isOfflineOperation()) { + return R.string.offline_operations_file_does_not_exists_yet; + } else { + return null; + } + } + + private void fileOnItemClick(OCFile file) { + Integer errorMessageId = checkFileBeforeOpen(file); + if (errorMessageId != null) { Snackbar.make(getRecyclerView(), - R.string.gplay_restriction, + errorMessageId, Snackbar.LENGTH_LONG).show(); return; } + if (PreviewImageFragment.canBePreviewed(file)) { // preview image - it handles the download, if needed if (searchFragment) { diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/SearchType.kt b/app/src/main/java/com/owncloud/android/ui/fragment/SearchType.kt index c28fdb366922..6f32a237a545 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/SearchType.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/SearchType.kt @@ -22,5 +22,6 @@ enum class SearchType : Parcelable { // not a real filter, but nevertheless SHARED_FILTER, - GROUPFOLDER + GROUPFOLDER, + OFFLINE_MODE } diff --git a/app/src/main/java/com/owncloud/android/utils/BitmapUtils.java b/app/src/main/java/com/owncloud/android/utils/BitmapUtils.java index 20ec77f03905..632fbff3defb 100644 --- a/app/src/main/java/com/owncloud/android/utils/BitmapUtils.java +++ b/app/src/main/java/com/owncloud/android/utils/BitmapUtils.java @@ -57,6 +57,26 @@ private BitmapUtils() { // utility class -> private constructor } + public static Bitmap addColorFilter(Bitmap originalBitmap, int filterColor, int opacity) { + int width = originalBitmap.getWidth(); + int height = originalBitmap.getHeight(); + + Bitmap resultBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(resultBitmap); + + canvas.drawBitmap(originalBitmap, 0, 0, null); + + Paint paint = new Paint(); + paint.setColor(filterColor); + + paint.setAlpha(opacity); + + paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP)); + canvas.drawRect(0, 0, width, height, paint); + + return resultBitmap; + } + /** * Decodes a bitmap from a file containing it minimizing the memory use, known that the bitmap will be drawn in a * surface of reqWidth x reqHeight diff --git a/app/src/main/res/drawable/ic_cloud_sync.xml b/app/src/main/res/drawable/ic_cloud_sync.xml new file mode 100644 index 000000000000..64be3ec9ea85 --- /dev/null +++ b/app/src/main/res/drawable/ic_cloud_sync.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0cf0c16c15a6..19f1cec7ec17 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -29,6 +29,7 @@ Base URL Proxy Host Name Proxy Port + File does not exists, yet. Please upload the file first. Delete Offline Folder Conflicted Folder: %s Starting Offline Operations @@ -609,7 +610,8 @@ Do you really want to delete the selected items and their contents? Server is in maintenance mode No internet connection - + You\'re Offline, But Work Continues + Even without an internet connection, you can organize your folders, create files. Once you\'re back online, your pending actions will automatically sync. Awaiting charge Search Auto upload @@ -1175,6 +1177,7 @@ Pin to Home screen Open %1$s Displays your 12 word passphrase + An internet connection is required to set up the encrypted folder Set up end-to-end encryption End-to-end encryption is set up! Remove encryption locally