diff --git a/app/schemas/com.nextcloud.client.database.NextcloudDatabase/105.json b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/105.json new file mode 100644 index 000000000000..3e57df891978 --- /dev/null +++ b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/105.json @@ -0,0 +1,1383 @@ +{ + "formatVersion": 1, + "database": { + "version": 105, + "identityHash": "e25bb2d1d844acd91607d0e638b9800f", + "entities": [ + { + "tableName": "arbitrary_data", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `cloud_id` TEXT, `key` TEXT, `value` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "cloudId", + "columnName": "cloud_id", + "affinity": "TEXT" + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "capabilities", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `assistant` INTEGER, `account` TEXT, `version_mayor` INTEGER, `version_minor` INTEGER, `version_micro` INTEGER, `version_string` TEXT, `version_edition` TEXT, `extended_support` INTEGER, `core_pollinterval` INTEGER, `sharing_api_enabled` INTEGER, `sharing_public_enabled` INTEGER, `sharing_public_password_enforced` INTEGER, `sharing_public_expire_date_enabled` INTEGER, `sharing_public_expire_date_days` INTEGER, `sharing_public_expire_date_enforced` INTEGER, `sharing_public_send_mail` INTEGER, `sharing_public_upload` INTEGER, `sharing_user_send_mail` INTEGER, `sharing_resharing` INTEGER, `sharing_federation_outgoing` INTEGER, `sharing_federation_incoming` INTEGER, `files_bigfilechunking` INTEGER, `files_undelete` INTEGER, `files_versioning` INTEGER, `external_links` INTEGER, `server_name` TEXT, `server_color` TEXT, `server_text_color` TEXT, `server_element_color` TEXT, `server_slogan` TEXT, `server_logo` TEXT, `background_url` TEXT, `end_to_end_encryption` INTEGER, `end_to_end_encryption_keys_exist` INTEGER, `end_to_end_encryption_api_version` TEXT, `activity` INTEGER, `background_default` INTEGER, `background_plain` INTEGER, `richdocument` INTEGER, `richdocument_mimetype_list` TEXT, `richdocument_direct_editing` INTEGER, `richdocument_direct_templates` INTEGER, `richdocument_optional_mimetype_list` TEXT, `sharing_public_ask_for_optional_password` INTEGER, `richdocument_product_name` TEXT, `direct_editing_etag` TEXT, `user_status` INTEGER, `user_status_supports_emoji` INTEGER, `etag` TEXT, `files_locking_version` TEXT, `groupfolders` INTEGER, `drop_account` INTEGER, `security_guard` INTEGER, `governance` INTEGER, `forbidden_filename_characters` TEXT, `forbidden_filenames` TEXT, `forbidden_filename_extensions` TEXT, `forbidden_filename_basenames` TEXT, `files_download_limit` INTEGER, `files_download_limit_default` INTEGER, `recommendation` INTEGER, `notes_folder_path` TEXT, `default_permissions` INTEGER, `user_status_supports_busy` INTEGER, `windows_compatible_filenames` INTEGER, `has_valid_subscription` INTEGER, `client_integration_json` TEXT, `mod_rewrite_working` INTEGER, `chunked_upload_max_size` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "assistant", + "columnName": "assistant", + "affinity": "INTEGER" + }, + { + "fieldPath": "accountName", + "columnName": "account", + "affinity": "TEXT" + }, + { + "fieldPath": "versionMajor", + "columnName": "version_mayor", + "affinity": "INTEGER" + }, + { + "fieldPath": "versionMinor", + "columnName": "version_minor", + "affinity": "INTEGER" + }, + { + "fieldPath": "versionMicro", + "columnName": "version_micro", + "affinity": "INTEGER" + }, + { + "fieldPath": "versionString", + "columnName": "version_string", + "affinity": "TEXT" + }, + { + "fieldPath": "versionEditor", + "columnName": "version_edition", + "affinity": "TEXT" + }, + { + "fieldPath": "extendedSupport", + "columnName": "extended_support", + "affinity": "INTEGER" + }, + { + "fieldPath": "corePollinterval", + "columnName": "core_pollinterval", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingApiEnabled", + "columnName": "sharing_api_enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicEnabled", + "columnName": "sharing_public_enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicPasswordEnforced", + "columnName": "sharing_public_password_enforced", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicExpireDateEnabled", + "columnName": "sharing_public_expire_date_enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicExpireDateDays", + "columnName": "sharing_public_expire_date_days", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicExpireDateEnforced", + "columnName": "sharing_public_expire_date_enforced", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicSendMail", + "columnName": "sharing_public_send_mail", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicUpload", + "columnName": "sharing_public_upload", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingUserSendMail", + "columnName": "sharing_user_send_mail", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingResharing", + "columnName": "sharing_resharing", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingFederationOutgoing", + "columnName": "sharing_federation_outgoing", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingFederationIncoming", + "columnName": "sharing_federation_incoming", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesBigfilechunking", + "columnName": "files_bigfilechunking", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesUndelete", + "columnName": "files_undelete", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesVersioning", + "columnName": "files_versioning", + "affinity": "INTEGER" + }, + { + "fieldPath": "externalLinks", + "columnName": "external_links", + "affinity": "INTEGER" + }, + { + "fieldPath": "serverName", + "columnName": "server_name", + "affinity": "TEXT" + }, + { + "fieldPath": "serverColor", + "columnName": "server_color", + "affinity": "TEXT" + }, + { + "fieldPath": "serverTextColor", + "columnName": "server_text_color", + "affinity": "TEXT" + }, + { + "fieldPath": "serverElementColor", + "columnName": "server_element_color", + "affinity": "TEXT" + }, + { + "fieldPath": "serverSlogan", + "columnName": "server_slogan", + "affinity": "TEXT" + }, + { + "fieldPath": "serverLogo", + "columnName": "server_logo", + "affinity": "TEXT" + }, + { + "fieldPath": "serverBackgroundUrl", + "columnName": "background_url", + "affinity": "TEXT" + }, + { + "fieldPath": "endToEndEncryption", + "columnName": "end_to_end_encryption", + "affinity": "INTEGER" + }, + { + "fieldPath": "endToEndEncryptionKeysExist", + "columnName": "end_to_end_encryption_keys_exist", + "affinity": "INTEGER" + }, + { + "fieldPath": "endToEndEncryptionApiVersion", + "columnName": "end_to_end_encryption_api_version", + "affinity": "TEXT" + }, + { + "fieldPath": "activity", + "columnName": "activity", + "affinity": "INTEGER" + }, + { + "fieldPath": "serverBackgroundDefault", + "columnName": "background_default", + "affinity": "INTEGER" + }, + { + "fieldPath": "serverBackgroundPlain", + "columnName": "background_plain", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocument", + "columnName": "richdocument", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentMimetypeList", + "columnName": "richdocument_mimetype_list", + "affinity": "TEXT" + }, + { + "fieldPath": "richdocumentDirectEditing", + "columnName": "richdocument_direct_editing", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentTemplates", + "columnName": "richdocument_direct_templates", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentOptionalMimetypeList", + "columnName": "richdocument_optional_mimetype_list", + "affinity": "TEXT" + }, + { + "fieldPath": "sharingPublicAskForOptionalPassword", + "columnName": "sharing_public_ask_for_optional_password", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentProductName", + "columnName": "richdocument_product_name", + "affinity": "TEXT" + }, + { + "fieldPath": "directEditingEtag", + "columnName": "direct_editing_etag", + "affinity": "TEXT" + }, + { + "fieldPath": "userStatus", + "columnName": "user_status", + "affinity": "INTEGER" + }, + { + "fieldPath": "userStatusSupportsEmoji", + "columnName": "user_status_supports_emoji", + "affinity": "INTEGER" + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT" + }, + { + "fieldPath": "filesLockingVersion", + "columnName": "files_locking_version", + "affinity": "TEXT" + }, + { + "fieldPath": "groupfolders", + "columnName": "groupfolders", + "affinity": "INTEGER" + }, + { + "fieldPath": "dropAccount", + "columnName": "drop_account", + "affinity": "INTEGER" + }, + { + "fieldPath": "securityGuard", + "columnName": "security_guard", + "affinity": "INTEGER" + }, + { + "fieldPath": "governance", + "columnName": "governance", + "affinity": "INTEGER" + }, + { + "fieldPath": "forbiddenFileNameCharacters", + "columnName": "forbidden_filename_characters", + "affinity": "TEXT" + }, + { + "fieldPath": "forbiddenFileNames", + "columnName": "forbidden_filenames", + "affinity": "TEXT" + }, + { + "fieldPath": "forbiddenFileNameExtensions", + "columnName": "forbidden_filename_extensions", + "affinity": "TEXT" + }, + { + "fieldPath": "forbiddenFilenameBaseNames", + "columnName": "forbidden_filename_basenames", + "affinity": "TEXT" + }, + { + "fieldPath": "filesDownloadLimit", + "columnName": "files_download_limit", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesDownloadLimitDefault", + "columnName": "files_download_limit_default", + "affinity": "INTEGER" + }, + { + "fieldPath": "recommendation", + "columnName": "recommendation", + "affinity": "INTEGER" + }, + { + "fieldPath": "notesFolderPath", + "columnName": "notes_folder_path", + "affinity": "TEXT" + }, + { + "fieldPath": "defaultPermissions", + "columnName": "default_permissions", + "affinity": "INTEGER" + }, + { + "fieldPath": "userStatusSupportsBusy", + "columnName": "user_status_supports_busy", + "affinity": "INTEGER" + }, + { + "fieldPath": "isWCFEnabled", + "columnName": "windows_compatible_filenames", + "affinity": "INTEGER" + }, + { + "fieldPath": "hasValidSubscription", + "columnName": "has_valid_subscription", + "affinity": "INTEGER" + }, + { + "fieldPath": "clientIntegrationJson", + "columnName": "client_integration_json", + "affinity": "TEXT" + }, + { + "fieldPath": "modRewriteWorking", + "columnName": "mod_rewrite_working", + "affinity": "INTEGER" + }, + { + "fieldPath": "chunkedUploadMaxSize", + "columnName": "chunked_upload_max_size", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "external_links", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `icon_url` TEXT, `language` TEXT, `type` INTEGER, `name` TEXT, `url` TEXT, `redirect` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "iconUrl", + "columnName": "icon_url", + "affinity": "TEXT" + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT" + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT" + }, + { + "fieldPath": "redirect", + "columnName": "redirect", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "filelist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `filename` TEXT, `encrypted_filename` TEXT, `path` TEXT, `path_decrypted` TEXT, `parent` INTEGER, `created` INTEGER, `modified` INTEGER, `content_type` TEXT, `content_length` INTEGER, `media_path` TEXT, `file_owner` TEXT, `last_sync_date` INTEGER, `last_sync_date_for_data` INTEGER, `modified_at_last_sync_for_data` INTEGER, `etag` TEXT, `etag_on_server` TEXT, `share_by_link` INTEGER, `permissions` TEXT, `remote_id` TEXT, `local_id` INTEGER NOT NULL DEFAULT -1, `update_thumbnail` INTEGER, `is_downloading` INTEGER, `favorite` INTEGER, `hidden` INTEGER, `is_encrypted` INTEGER, `etag_in_conflict` TEXT, `shared_via_users` INTEGER, `mount_type` INTEGER, `has_preview` INTEGER, `unread_comments_count` INTEGER, `owner_id` TEXT, `owner_display_name` TEXT, `note` TEXT, `sharees` TEXT, `rich_workspace` TEXT, `metadata_size` TEXT, `metadata_live_photo` TEXT, `locked` INTEGER, `lock_type` INTEGER, `lock_owner` TEXT, `lock_owner_display_name` TEXT, `lock_owner_editor` TEXT, `lock_timestamp` INTEGER, `lock_timeout` INTEGER, `lock_token` TEXT, `tags` TEXT, `metadata_gps` TEXT, `e2e_counter` INTEGER, `internal_two_way_sync_timestamp` INTEGER, `internal_two_way_sync_result` TEXT, `uploaded` INTEGER, `is_read_only` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "name", + "columnName": "filename", + "affinity": "TEXT" + }, + { + "fieldPath": "encryptedName", + "columnName": "encrypted_filename", + "affinity": "TEXT" + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT" + }, + { + "fieldPath": "pathDecrypted", + "columnName": "path_decrypted", + "affinity": "TEXT" + }, + { + "fieldPath": "parent", + "columnName": "parent", + "affinity": "INTEGER" + }, + { + "fieldPath": "creation", + "columnName": "created", + "affinity": "INTEGER" + }, + { + "fieldPath": "modified", + "columnName": "modified", + "affinity": "INTEGER" + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT" + }, + { + "fieldPath": "contentLength", + "columnName": "content_length", + "affinity": "INTEGER" + }, + { + "fieldPath": "storagePath", + "columnName": "media_path", + "affinity": "TEXT" + }, + { + "fieldPath": "accountOwner", + "columnName": "file_owner", + "affinity": "TEXT" + }, + { + "fieldPath": "lastSyncDate", + "columnName": "last_sync_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastSyncDateForData", + "columnName": "last_sync_date_for_data", + "affinity": "INTEGER" + }, + { + "fieldPath": "modifiedAtLastSyncForData", + "columnName": "modified_at_last_sync_for_data", + "affinity": "INTEGER" + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT" + }, + { + "fieldPath": "etagOnServer", + "columnName": "etag_on_server", + "affinity": "TEXT" + }, + { + "fieldPath": "sharedViaLink", + "columnName": "share_by_link", + "affinity": "INTEGER" + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "TEXT" + }, + { + "fieldPath": "remoteId", + "columnName": "remote_id", + "affinity": "TEXT" + }, + { + "fieldPath": "localId", + "columnName": "local_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "updateThumbnail", + "columnName": "update_thumbnail", + "affinity": "INTEGER" + }, + { + "fieldPath": "isDownloading", + "columnName": "is_downloading", + "affinity": "INTEGER" + }, + { + "fieldPath": "favorite", + "columnName": "favorite", + "affinity": "INTEGER" + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER" + }, + { + "fieldPath": "isEncrypted", + "columnName": "is_encrypted", + "affinity": "INTEGER" + }, + { + "fieldPath": "etagInConflict", + "columnName": "etag_in_conflict", + "affinity": "TEXT" + }, + { + "fieldPath": "sharedWithSharee", + "columnName": "shared_via_users", + "affinity": "INTEGER" + }, + { + "fieldPath": "mountType", + "columnName": "mount_type", + "affinity": "INTEGER" + }, + { + "fieldPath": "hasPreview", + "columnName": "has_preview", + "affinity": "INTEGER" + }, + { + "fieldPath": "unreadCommentsCount", + "columnName": "unread_comments_count", + "affinity": "INTEGER" + }, + { + "fieldPath": "ownerId", + "columnName": "owner_id", + "affinity": "TEXT" + }, + { + "fieldPath": "ownerDisplayName", + "columnName": "owner_display_name", + "affinity": "TEXT" + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT" + }, + { + "fieldPath": "sharees", + "columnName": "sharees", + "affinity": "TEXT" + }, + { + "fieldPath": "richWorkspace", + "columnName": "rich_workspace", + "affinity": "TEXT" + }, + { + "fieldPath": "metadataSize", + "columnName": "metadata_size", + "affinity": "TEXT" + }, + { + "fieldPath": "metadataLivePhoto", + "columnName": "metadata_live_photo", + "affinity": "TEXT" + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockType", + "columnName": "lock_type", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockOwner", + "columnName": "lock_owner", + "affinity": "TEXT" + }, + { + "fieldPath": "lockOwnerDisplayName", + "columnName": "lock_owner_display_name", + "affinity": "TEXT" + }, + { + "fieldPath": "lockOwnerEditor", + "columnName": "lock_owner_editor", + "affinity": "TEXT" + }, + { + "fieldPath": "lockTimestamp", + "columnName": "lock_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockTimeout", + "columnName": "lock_timeout", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockToken", + "columnName": "lock_token", + "affinity": "TEXT" + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT" + }, + { + "fieldPath": "metadataGPS", + "columnName": "metadata_gps", + "affinity": "TEXT" + }, + { + "fieldPath": "e2eCounter", + "columnName": "e2e_counter", + "affinity": "INTEGER" + }, + { + "fieldPath": "internalTwoWaySync", + "columnName": "internal_two_way_sync_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "internalTwoWaySyncResult", + "columnName": "internal_two_way_sync_result", + "affinity": "TEXT" + }, + { + "fieldPath": "uploaded", + "columnName": "uploaded", + "affinity": "INTEGER" + }, + { + "fieldPath": "isReadOnly", + "columnName": "is_read_only", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "filesystem", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `is_folder` INTEGER, `found_at` INTEGER, `upload_triggered` INTEGER, `syncedfolder_id` TEXT, `crc32` TEXT, `modified_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT" + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT" + }, + { + "fieldPath": "fileIsFolder", + "columnName": "is_folder", + "affinity": "INTEGER" + }, + { + "fieldPath": "fileFoundRecently", + "columnName": "found_at", + "affinity": "INTEGER" + }, + { + "fieldPath": "fileSentForUpload", + "columnName": "upload_triggered", + "affinity": "INTEGER" + }, + { + "fieldPath": "syncedFolderId", + "columnName": "syncedfolder_id", + "affinity": "TEXT" + }, + { + "fieldPath": "crc32", + "columnName": "crc32", + "affinity": "TEXT" + }, + { + "fieldPath": "fileModified", + "columnName": "modified_at", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [ + { + "name": "index_filesystem_local_path_syncedfolder_id", + "unique": false, + "columnNames": [ + "local_path", + "syncedfolder_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_filesystem_local_path_syncedfolder_id` ON `${TABLE_NAME}` (`local_path`, `syncedfolder_id`)" + }, + { + "name": "index_filesystem_syncedfolder_id_upload_triggered_is_folder", + "unique": false, + "columnNames": [ + "syncedfolder_id", + "upload_triggered", + "is_folder" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_filesystem_syncedfolder_id_upload_triggered_is_folder` ON `${TABLE_NAME}` (`syncedfolder_id`, `upload_triggered`, `is_folder`)" + } + ] + }, + { + "tableName": "ocshares", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `file_source` INTEGER, `item_source` INTEGER, `share_type` INTEGER, `shate_with` TEXT, `path` TEXT, `permissions` INTEGER, `shared_date` INTEGER, `expiration_date` INTEGER, `token` TEXT, `shared_with_display_name` TEXT, `is_directory` INTEGER, `user_id` TEXT, `id_remote_shared` INTEGER, `owner_share` TEXT, `is_password_protected` INTEGER, `note` TEXT, `hide_download` INTEGER, `share_link` TEXT, `share_label` TEXT, `download_limit_limit` INTEGER, `download_limit_count` INTEGER, `attributes` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "fileSource", + "columnName": "file_source", + "affinity": "INTEGER" + }, + { + "fieldPath": "itemSource", + "columnName": "item_source", + "affinity": "INTEGER" + }, + { + "fieldPath": "shareType", + "columnName": "share_type", + "affinity": "INTEGER" + }, + { + "fieldPath": "shareWith", + "columnName": "shate_with", + "affinity": "TEXT" + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT" + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharedDate", + "columnName": "shared_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "expirationDate", + "columnName": "expiration_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT" + }, + { + "fieldPath": "shareWithDisplayName", + "columnName": "shared_with_display_name", + "affinity": "TEXT" + }, + { + "fieldPath": "isDirectory", + "columnName": "is_directory", + "affinity": "INTEGER" + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT" + }, + { + "fieldPath": "idRemoteShared", + "columnName": "id_remote_shared", + "affinity": "INTEGER" + }, + { + "fieldPath": "accountOwner", + "columnName": "owner_share", + "affinity": "TEXT" + }, + { + "fieldPath": "isPasswordProtected", + "columnName": "is_password_protected", + "affinity": "INTEGER" + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT" + }, + { + "fieldPath": "hideDownload", + "columnName": "hide_download", + "affinity": "INTEGER" + }, + { + "fieldPath": "shareLink", + "columnName": "share_link", + "affinity": "TEXT" + }, + { + "fieldPath": "shareLabel", + "columnName": "share_label", + "affinity": "TEXT" + }, + { + "fieldPath": "downloadLimitLimit", + "columnName": "download_limit_limit", + "affinity": "INTEGER" + }, + { + "fieldPath": "downloadLimitCount", + "columnName": "download_limit_count", + "affinity": "INTEGER" + }, + { + "fieldPath": "attributes", + "columnName": "attributes", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "synced_folders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `wifi_only` INTEGER, `charging_only` INTEGER, `existing` INTEGER, `enabled` INTEGER, `enabled_timestamp_ms` INTEGER, `subfolder_by_date` INTEGER, `account` TEXT, `upload_option` INTEGER, `name_collision_policy` INTEGER, `type` INTEGER, `hidden` INTEGER, `sub_folder_rule` INTEGER, `exclude_hidden` INTEGER, `last_scan_timestamp_ms` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT" + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT" + }, + { + "fieldPath": "wifiOnly", + "columnName": "wifi_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "chargingOnly", + "columnName": "charging_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "existing", + "columnName": "existing", + "affinity": "INTEGER" + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "enabledTimestampMs", + "columnName": "enabled_timestamp_ms", + "affinity": "INTEGER" + }, + { + "fieldPath": "subfolderByDate", + "columnName": "subfolder_by_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "TEXT" + }, + { + "fieldPath": "uploadAction", + "columnName": "upload_option", + "affinity": "INTEGER" + }, + { + "fieldPath": "nameCollisionPolicy", + "columnName": "name_collision_policy", + "affinity": "INTEGER" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER" + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER" + }, + { + "fieldPath": "subFolderRule", + "columnName": "sub_folder_rule", + "affinity": "INTEGER" + }, + { + "fieldPath": "excludeHidden", + "columnName": "exclude_hidden", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastScanTimestampMs", + "columnName": "last_scan_timestamp_ms", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "list_of_uploads", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `account_name` TEXT, `file_size` INTEGER, `status` INTEGER, `local_behaviour` INTEGER, `upload_time` INTEGER, `name_collision_policy` INTEGER, `is_create_remote_folder` INTEGER, `upload_end_timestamp` INTEGER, `upload_end_timestamp_long` INTEGER, `last_result` INTEGER, `is_while_charging_only` INTEGER, `is_wifi_only` INTEGER, `created_by` INTEGER, `folder_unlock_token` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT" + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT" + }, + { + "fieldPath": "accountName", + "columnName": "account_name", + "affinity": "TEXT" + }, + { + "fieldPath": "fileSize", + "columnName": "file_size", + "affinity": "INTEGER" + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER" + }, + { + "fieldPath": "localBehaviour", + "columnName": "local_behaviour", + "affinity": "INTEGER" + }, + { + "fieldPath": "uploadTime", + "columnName": "upload_time", + "affinity": "INTEGER" + }, + { + "fieldPath": "nameCollisionPolicy", + "columnName": "name_collision_policy", + "affinity": "INTEGER" + }, + { + "fieldPath": "isCreateRemoteFolder", + "columnName": "is_create_remote_folder", + "affinity": "INTEGER" + }, + { + "fieldPath": "uploadEndTimestamp", + "columnName": "upload_end_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "uploadEndTimestampLong", + "columnName": "upload_end_timestamp_long", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastResult", + "columnName": "last_result", + "affinity": "INTEGER" + }, + { + "fieldPath": "isWhileChargingOnly", + "columnName": "is_while_charging_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "isWifiOnly", + "columnName": "is_wifi_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "createdBy", + "columnName": "created_by", + "affinity": "INTEGER" + }, + { + "fieldPath": "folderUnlockToken", + "columnName": "folder_unlock_token", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [ + { + "name": "index_list_of_uploads_account_name_status", + "unique": false, + "columnNames": [ + "account_name", + "status" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_list_of_uploads_account_name_status` ON `${TABLE_NAME}` (`account_name`, `status`)" + }, + { + "name": "index_list_of_uploads_remote_path_account_name", + "unique": false, + "columnNames": [ + "remote_path", + "account_name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_list_of_uploads_remote_path_account_name` ON `${TABLE_NAME}` (`remote_path`, `account_name`)" + }, + { + "name": "index_list_of_uploads_account_name_local_path", + "unique": false, + "columnNames": [ + "account_name", + "local_path" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_list_of_uploads_account_name_local_path` ON `${TABLE_NAME}` (`account_name`, `local_path`)" + } + ] + }, + { + "tableName": "virtual", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `type` TEXT, `ocfile_id` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT" + }, + { + "fieldPath": "ocFileId", + "columnName": "ocfile_id", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "offline_operations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `offline_operations_parent_oc_file_id` INTEGER, `offline_operations_path` TEXT, `offline_operations_type` TEXT, `offline_operations_file_name` TEXT, `offline_operations_created_at` INTEGER, `offline_operations_modified_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "parentOCFileId", + "columnName": "offline_operations_parent_oc_file_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "path", + "columnName": "offline_operations_path", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "offline_operations_type", + "affinity": "TEXT" + }, + { + "fieldPath": "filename", + "columnName": "offline_operations_file_name", + "affinity": "TEXT" + }, + { + "fieldPath": "createdAt", + "columnName": "offline_operations_created_at", + "affinity": "INTEGER" + }, + { + "fieldPath": "modifiedAt", + "columnName": "offline_operations_modified_at", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "recommended_files", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `directory` TEXT NOT NULL, `extension` TEXT NOT NULL, `mime_type` TEXT NOT NULL, `has_preview` INTEGER NOT NULL, `reason` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `account_name` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "directory", + "columnName": "directory", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "extension", + "columnName": "extension", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mimeType", + "columnName": "mime_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasPreview", + "columnName": "has_preview", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reason", + "columnName": "reason", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountName", + "columnName": "account_name", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "assistant", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountName` TEXT, `type` TEXT, `status` TEXT, `userId` TEXT, `appId` TEXT, `input` TEXT, `output` TEXT, `completionExpectedAt` INTEGER, `progress` INTEGER, `lastUpdated` INTEGER, `scheduledAt` INTEGER, `endedAt` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountName", + "columnName": "accountName", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT" + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT" + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT" + }, + { + "fieldPath": "appId", + "columnName": "appId", + "affinity": "TEXT" + }, + { + "fieldPath": "input", + "columnName": "input", + "affinity": "TEXT" + }, + { + "fieldPath": "output", + "columnName": "output", + "affinity": "TEXT" + }, + { + "fieldPath": "completionExpectedAt", + "columnName": "completionExpectedAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "progress", + "columnName": "progress", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "INTEGER" + }, + { + "fieldPath": "scheduledAt", + "columnName": "scheduledAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "endedAt", + "columnName": "endedAt", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e25bb2d1d844acd91607d0e638b9800f')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/nextcloud/utils/PowerManagementFactory.kt b/app/src/androidTest/java/com/nextcloud/utils/PowerManagementFactory.kt new file mode 100644 index 000000000000..d953d091b13b --- /dev/null +++ b/app/src/androidTest/java/com/nextcloud/utils/PowerManagementFactory.kt @@ -0,0 +1,38 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils + +import com.nextcloud.client.device.BatteryStatus +import com.nextcloud.client.device.PowerManagementService + +object PowerManagementFactory { + @JvmStatic + val mock: PowerManagementService = object : PowerManagementService { + override val isIgnoringOptimization: Boolean + get() = true + override val isPowerSavingEnabled: Boolean + get() = false + override val battery: BatteryStatus + get() = BatteryStatus(false, 0) + override val blocksAutoUpload: Boolean + get() = false + } + + @JvmStatic + @Suppress("MagicNumber") + val mockCharging: PowerManagementService = object : PowerManagementService { + override val isIgnoringOptimization: Boolean + get() = true + override val isPowerSavingEnabled: Boolean + get() = false + override val battery: BatteryStatus + get() = BatteryStatus(true, 100) + override val blocksAutoUpload: Boolean + get() = false + } +} diff --git a/app/src/androidTest/java/com/owncloud/android/AbstractIT.java b/app/src/androidTest/java/com/owncloud/android/AbstractIT.java index 5230036f5bff..0640a8cee639 100644 --- a/app/src/androidTest/java/com/owncloud/android/AbstractIT.java +++ b/app/src/androidTest/java/com/owncloud/android/AbstractIT.java @@ -26,8 +26,7 @@ import com.nextcloud.client.account.User; import com.nextcloud.client.account.UserAccountManager; import com.nextcloud.client.account.UserAccountManagerImpl; -import com.nextcloud.client.device.BatteryStatus; -import com.nextcloud.client.device.PowerManagementService; +import com.nextcloud.utils.PowerManagementFactory; import com.nextcloud.client.jobs.upload.FileUploadWorker; import com.nextcloud.client.network.ConnectivityManagerFactory; import com.nextcloud.client.network.ConnectivityService; @@ -73,7 +72,6 @@ import java.util.Objects; import java.util.Optional; -import androidx.annotation.NonNull; import androidx.fragment.app.DialogFragment; import androidx.test.espresso.contrib.DrawerActions; import androidx.test.espresso.intent.rule.IntentsTestRule; @@ -367,24 +365,7 @@ public void uploadFile(File file, String remotePath) { } public void uploadOCUpload(OCUpload ocUpload) { - PowerManagementService powerManagementServiceMock = new PowerManagementService() { - @Override - public boolean isIgnoringOptimization() { - return true; - } - - @NonNull - @Override - public BatteryStatus getBattery() { - return new BatteryStatus(); - } - - @Override - public boolean isPowerSavingEnabled() { - return false; - } - }; - + final var powerManagementServiceMock = PowerManagementFactory.getMock(); UserAccountManager accountManager = UserAccountManagerImpl.fromContext(targetContext); UploadsStorageManager uploadsStorageManager = new UploadsStorageManager(accountManager, targetContext.getContentResolver()); diff --git a/app/src/androidTest/java/com/owncloud/android/AbstractOnServerIT.java b/app/src/androidTest/java/com/owncloud/android/AbstractOnServerIT.java index a1924f00e162..e7e1e2b05bf2 100644 --- a/app/src/androidTest/java/com/owncloud/android/AbstractOnServerIT.java +++ b/app/src/androidTest/java/com/owncloud/android/AbstractOnServerIT.java @@ -16,8 +16,7 @@ import com.nextcloud.client.account.User; import com.nextcloud.client.account.UserAccountManager; import com.nextcloud.client.account.UserAccountManagerImpl; -import com.nextcloud.client.device.BatteryStatus; -import com.nextcloud.client.device.PowerManagementService; +import com.nextcloud.utils.PowerManagementFactory; import com.nextcloud.client.jobs.upload.FileUploadWorker; import com.owncloud.android.datamodel.OCFile; import com.owncloud.android.datamodel.UploadsStorageManager; @@ -45,7 +44,6 @@ import java.util.Objects; import java.util.Optional; -import androidx.annotation.NonNull; import androidx.test.platform.app.InstrumentationRegistry; import static org.junit.Assert.assertNotNull; @@ -205,24 +203,7 @@ public void uploadOCUpload(OCUpload ocUpload) { } public void uploadOCUpload(OCUpload ocUpload, int localBehaviour) { - PowerManagementService powerManagementServiceMock = new PowerManagementService() { - @Override - public boolean isIgnoringOptimization() { - return true; - } - - @NonNull - @Override - public BatteryStatus getBattery() { - return new BatteryStatus(); - } - - @Override - public boolean isPowerSavingEnabled() { - return false; - } - }; - + final var powerManagementServiceMock = PowerManagementFactory.getMock(); UserAccountManager accountManager = UserAccountManagerImpl.fromContext(targetContext); UploadsStorageManager uploadsStorageManager = new UploadsStorageManager(accountManager, targetContext.getContentResolver()); diff --git a/app/src/androidTest/java/com/owncloud/android/GrantFolderExistenceTests.kt b/app/src/androidTest/java/com/owncloud/android/GrantFolderExistenceTests.kt index a5c296340fd4..82d771add397 100644 --- a/app/src/androidTest/java/com/owncloud/android/GrantFolderExistenceTests.kt +++ b/app/src/androidTest/java/com/owncloud/android/GrantFolderExistenceTests.kt @@ -7,8 +7,7 @@ package com.owncloud.android import com.nextcloud.client.account.UserAccountManagerImpl -import com.nextcloud.client.device.BatteryStatus -import com.nextcloud.client.device.PowerManagementService +import com.nextcloud.utils.PowerManagementFactory import com.nextcloud.client.jobs.upload.FileUploadWorker import com.owncloud.android.datamodel.UploadsStorageManager import com.owncloud.android.db.OCUpload @@ -36,11 +35,7 @@ class GrantFolderExistenceTests : AbstractOnServerIT() { targetContext.contentResolver ) - private val powerManagementServiceMock = object : PowerManagementService { - override val isPowerSavingEnabled = false - override val isIgnoringOptimization = true - override val battery = BatteryStatus(false, 0) - } + private val powerManagementServiceMock = PowerManagementFactory.mock @Before @Throws(IOException::class) diff --git a/app/src/androidTest/java/com/owncloud/android/UploadIT.java b/app/src/androidTest/java/com/owncloud/android/UploadIT.java index 7b5b416bc10f..0dfdc09d2748 100644 --- a/app/src/androidTest/java/com/owncloud/android/UploadIT.java +++ b/app/src/androidTest/java/com/owncloud/android/UploadIT.java @@ -9,7 +9,7 @@ package com.owncloud.android; import com.nextcloud.client.account.UserAccountManagerImpl; -import com.nextcloud.client.device.BatteryStatus; +import com.nextcloud.utils.PowerManagementFactory; import com.nextcloud.client.device.PowerManagementService; import com.nextcloud.client.jobs.upload.FileUploadWorker; import com.nextcloud.client.network.ConnectivityManagerFactory; @@ -38,8 +38,6 @@ import java.util.List; import java.util.concurrent.TimeUnit; -import androidx.annotation.NonNull; - import static junit.framework.TestCase.assertEquals; import static junit.framework.TestCase.assertFalse; import static junit.framework.TestCase.assertNotNull; @@ -55,22 +53,7 @@ public class UploadIT extends AbstractOnServerIT { new UploadsStorageManager(UserAccountManagerImpl.fromContext(targetContext), targetContext.getContentResolver()); - private PowerManagementService powerManagementServiceMock = new PowerManagementService() { - @Override - public boolean isIgnoringOptimization() { - return true; - } - - @Override - public boolean isPowerSavingEnabled() { - return false; - } - @NonNull - @Override - public BatteryStatus getBattery() { - return new BatteryStatus(false, 0); - } - }; + private final PowerManagementService powerManagementServiceMock = PowerManagementFactory.getMock(); @Before public void before() throws IOException { @@ -208,24 +191,7 @@ public void testUploadOnChargingOnlyButNotCharging() { @Test public void testUploadOnChargingOnlyAndCharging() { - PowerManagementService powerManagementServiceMock = new PowerManagementService() { - @Override - public boolean isIgnoringOptimization() { - return true; - } - - @Override - public boolean isPowerSavingEnabled() { - return false; - } - - @NonNull - @Override - public BatteryStatus getBattery() { - return new BatteryStatus(true, 100); - } - }; - + final var powerManagementServiceMock = PowerManagementFactory.getMockCharging(); OCUpload ocUpload = new OCUpload(FileStorageUtils.getTemporalPath(account.name) + "/empty.txt", FOLDER + "charging.txt", account.name); ocUpload.setWhileChargingOnly(true); 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 244a2aa63ba4..25a80c87ac3d 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 @@ -10,8 +10,7 @@ package com.owncloud.android.files.services import com.nextcloud.client.account.UserAccountManagerImpl -import com.nextcloud.client.device.BatteryStatus -import com.nextcloud.client.device.PowerManagementService +import com.nextcloud.utils.PowerManagementFactory import com.nextcloud.client.jobs.upload.FileUploadWorker import com.owncloud.android.AbstractOnServerIT import com.owncloud.android.datamodel.UploadsStorageManager @@ -30,11 +29,7 @@ class FileUploaderIT : AbstractOnServerIT() { private lateinit var uploadsStorageManager: UploadsStorageManager - private val powerManagementServiceMock = object : PowerManagementService { - override val isIgnoringOptimization = true - override val isPowerSavingEnabled = false - override val battery = BatteryStatus() - } + private val powerManagementServiceMock = PowerManagementFactory.mock @Before fun setUp() { 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 49b0763400da..3f1e4be006b0 100644 --- a/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt +++ b/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt @@ -103,7 +103,8 @@ import com.owncloud.android.db.ProviderMeta AutoMigration(from = 100, to = 101, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class), AutoMigration(from = 101, to = 102, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class), AutoMigration(from = 102, to = 103, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class), - AutoMigration(from = 103, to = 104) + AutoMigration(from = 103, to = 104), + AutoMigration(from = 104, to = 105) ], exportSchema = true ) diff --git a/app/src/main/java/com/nextcloud/client/database/dao/UploadDao.kt b/app/src/main/java/com/nextcloud/client/database/dao/UploadDao.kt index 07db239a7dd6..0261666c35ce 100644 --- a/app/src/main/java/com/nextcloud/client/database/dao/UploadDao.kt +++ b/app/src/main/java/com/nextcloud/client/database/dao/UploadDao.kt @@ -95,9 +95,11 @@ interface UploadDao { SELECT * FROM ${ProviderTableMeta.UPLOADS_TABLE_NAME} WHERE ${ProviderTableMeta.UPLOADS_STATUS} = :status AND (:nameCollisionPolicy IS NULL OR ${ProviderTableMeta.UPLOADS_NAME_COLLISION_POLICY} = :nameCollisionPolicy) + ORDER BY ${ProviderTableMeta._ID} DESC + LIMIT :limit """ ) - suspend fun getUploadsByStatus(status: Int, nameCollisionPolicy: Int? = null): List + suspend fun getUploadsByStatus(status: Int, nameCollisionPolicy: Int? = null, limit: Int): List @Query( """ @@ -105,11 +107,14 @@ interface UploadDao { WHERE ${ProviderTableMeta.UPLOADS_ACCOUNT_NAME} = :accountName AND ${ProviderTableMeta.UPLOADS_STATUS} = :status AND (:nameCollisionPolicy IS NULL OR ${ProviderTableMeta.UPLOADS_NAME_COLLISION_POLICY} = :nameCollisionPolicy) + ORDER BY ${ProviderTableMeta._ID} DESC + LIMIT :limit """ ) suspend fun getUploadsByAccountNameAndStatus( accountName: String, status: Int, - nameCollisionPolicy: Int? = null + nameCollisionPolicy: Int? = null, + limit: Int ): List } diff --git a/app/src/main/java/com/nextcloud/client/database/entity/FilesystemEntity.kt b/app/src/main/java/com/nextcloud/client/database/entity/FilesystemEntity.kt index 031fc52baeb3..c66876d06131 100644 --- a/app/src/main/java/com/nextcloud/client/database/entity/FilesystemEntity.kt +++ b/app/src/main/java/com/nextcloud/client/database/entity/FilesystemEntity.kt @@ -9,10 +9,28 @@ package com.nextcloud.client.database.entity import androidx.room.ColumnInfo import androidx.room.Entity +import androidx.room.Index import androidx.room.PrimaryKey import com.owncloud.android.db.ProviderMeta.ProviderTableMeta -@Entity(tableName = ProviderTableMeta.FILESYSTEM_TABLE_NAME) +@Entity( + tableName = ProviderTableMeta.FILESYSTEM_TABLE_NAME, + indices = [ + Index( + value = [ + ProviderTableMeta.FILESYSTEM_FILE_LOCAL_PATH, + ProviderTableMeta.FILESYSTEM_SYNCED_FOLDER_ID + ] + ), + Index( + value = [ + ProviderTableMeta.FILESYSTEM_SYNCED_FOLDER_ID, + ProviderTableMeta.FILESYSTEM_FILE_SENT_FOR_UPLOAD, + ProviderTableMeta.FILESYSTEM_FILE_IS_FOLDER + ] + ) + ] +) data class FilesystemEntity( @PrimaryKey(autoGenerate = true) @ColumnInfo(name = ProviderTableMeta._ID) diff --git a/app/src/main/java/com/nextcloud/client/database/entity/UploadEntity.kt b/app/src/main/java/com/nextcloud/client/database/entity/UploadEntity.kt index ff4ff9e91c6f..bc9636bc797f 100644 --- a/app/src/main/java/com/nextcloud/client/database/entity/UploadEntity.kt +++ b/app/src/main/java/com/nextcloud/client/database/entity/UploadEntity.kt @@ -10,6 +10,7 @@ package com.nextcloud.client.database.entity import androidx.room.ColumnInfo import androidx.room.Entity +import androidx.room.Index import androidx.room.PrimaryKey import com.nextcloud.utils.autoRename.AutoRename import com.owncloud.android.datamodel.UploadsStorageManager @@ -21,7 +22,14 @@ import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.lib.resources.status.OCCapability import java.lang.IllegalArgumentException -@Entity(tableName = ProviderTableMeta.UPLOADS_TABLE_NAME) +@Entity( + tableName = ProviderTableMeta.UPLOADS_TABLE_NAME, + indices = [ + Index(value = [ProviderTableMeta.UPLOADS_ACCOUNT_NAME, ProviderTableMeta.UPLOADS_STATUS]), + Index(value = [ProviderTableMeta.UPLOADS_REMOTE_PATH, ProviderTableMeta.UPLOADS_ACCOUNT_NAME]), + Index(value = [ProviderTableMeta.UPLOADS_ACCOUNT_NAME, ProviderTableMeta.UPLOADS_LOCAL_PATH]) + ] +) data class UploadEntity( @PrimaryKey(autoGenerate = true) @ColumnInfo(name = ProviderTableMeta._ID) diff --git a/app/src/main/java/com/nextcloud/client/device/PowerManagementService.kt b/app/src/main/java/com/nextcloud/client/device/PowerManagementService.kt index 5b51acaf0518..c5563e46d1a5 100644 --- a/app/src/main/java/com/nextcloud/client/device/PowerManagementService.kt +++ b/app/src/main/java/com/nextcloud/client/device/PowerManagementService.kt @@ -30,4 +30,6 @@ interface PowerManagementService { * Checks current battery status using platform [android.os.BatteryManager] */ val battery: BatteryStatus + + val blocksAutoUpload: Boolean } diff --git a/app/src/main/java/com/nextcloud/client/device/PowerManagementServiceImpl.kt b/app/src/main/java/com/nextcloud/client/device/PowerManagementServiceImpl.kt index 8d3c9ad89645..d28943b1849d 100644 --- a/app/src/main/java/com/nextcloud/client/device/PowerManagementServiceImpl.kt +++ b/app/src/main/java/com/nextcloud/client/device/PowerManagementServiceImpl.kt @@ -38,10 +38,13 @@ internal class PowerManagementServiceImpl( return platformPowerManager.isPowerSaveMode } + override val blocksAutoUpload: Boolean + get() = isPowerSavingEnabled && !isIgnoringOptimization + @Suppress("MagicNumber") // 100% is 100, we're not doing Cobol override val battery: BatteryStatus get() { - val intent: Intent? = context.registerBroadcastReceiver( + val intent = context.registerBroadcastReceiver( null, IntentFilter(Intent.ACTION_BATTERY_CHANGED), ReceiverFlag.NotExported @@ -54,7 +57,7 @@ internal class PowerManagementServiceImpl( else -> false } } ?: false - val level = intent?.let { it -> + val level = intent?.let { val level: Int = it.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) val scale: Int = it.getIntExtra(BatteryManager.EXTRA_SCALE, -1) (level * 100 / scale.toFloat()).toInt() 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 06fda4b00a25..1fe963ad7c34 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt @@ -25,6 +25,7 @@ import com.nextcloud.client.documentscan.GeneratePdfFromImagesWork import com.nextcloud.client.integrations.deck.DeckApi import com.nextcloud.client.jobs.autoUpload.AutoUploadHelper import com.nextcloud.client.jobs.autoUpload.AutoUploadLocalDeletionWorker +import com.nextcloud.client.jobs.autoUpload.AutoUploadRescanWorker import com.nextcloud.client.jobs.autoUpload.AutoUploadWorker import com.nextcloud.client.jobs.autoUpload.FileSystemRepository import com.nextcloud.client.jobs.download.FileDownloadWorker @@ -94,6 +95,7 @@ class BackgroundJobFactory @Inject constructor( ContactsBackupWork::class -> createContactsBackupWork(context, workerParameters) ContactsImportWork::class -> createContactsImportWork(context, workerParameters) AutoUploadWorker::class -> createAutoUploadWorker(context, workerParameters) + AutoUploadRescanWorker::class -> createAutoUploadRescanWorker(context, workerParameters) OfflineSyncWork::class -> createOfflineSyncWork(context, workerParameters) MediaFoldersDetectionWork::class -> createMediaFoldersDetectionWork(context, workerParameters) NotificationWork::class -> createNotificationWork(context, workerParameters) @@ -194,9 +196,18 @@ class BackgroundJobFactory @Inject constructor( localBroadcastManager = localBroadcastManager.get(), autoUploadHelper = AutoUploadHelper( FileSystemRepository(dao = database.fileSystemDao(), uploadsStorageManager, context) - ) + ), + preferences = preferences ) + private fun createAutoUploadRescanWorker(context: Context, params: WorkerParameters): AutoUploadRescanWorker = + AutoUploadRescanWorker( + context = context, + params = params, + syncedFolderProvider = syncedFolderProvider, + backgroundJobManager = backgroundJobManager.get() + ) + private fun createOfflineSyncWork(context: Context, params: WorkerParameters): OfflineSyncWork = OfflineSyncWork( context = context, params = params, diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt index 4aa562956c40..030359d57e31 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt @@ -147,6 +147,7 @@ interface BackgroundJobManager { fun cancelFilesUploadJob(user: User) fun isStartFileUploadJobScheduled(accountName: String): Boolean fun isAutoUploadIgnoringPowerSavingScheduled(syncedFolderID: Long): Boolean + fun schedulePeriodicAutoUpload() fun cancelFilesDownloadJob(accountName: String, fileId: Long) @Suppress("LongParameterList") diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt index daba20131e20..d913d32fc104 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt @@ -29,6 +29,7 @@ import com.nextcloud.client.core.Clock import com.nextcloud.client.di.Injectable import com.nextcloud.client.documentscan.GeneratePdfFromImagesWork import com.nextcloud.client.jobs.autoUpload.AutoUploadLocalDeletionWorker +import com.nextcloud.client.jobs.autoUpload.AutoUploadRescanWorker import com.nextcloud.client.jobs.autoUpload.AutoUploadWorker import com.nextcloud.client.jobs.download.FileDownloadWorker import com.nextcloud.client.jobs.folderDownload.FolderDownloadWorker @@ -496,6 +497,16 @@ internal class BackgroundJobManagerImpl( override fun isAutoUploadIgnoringPowerSavingScheduled(syncedFolderID: Long): Boolean = workManager.isWorkScheduled(autoUploadIgnorePowerSavingTag(syncedFolderID)) + override fun schedulePeriodicAutoUpload() { + val request = periodicRequestBuilder( + jobClass = AutoUploadRescanWorker::class, + jobName = JOB_PERIODIC_FILES_SYNC, + constraints = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build() + ).build() + + workManager.enqueueUniquePeriodicWork(JOB_PERIODIC_FILES_SYNC, ExistingPeriodicWorkPolicy.KEEP, request) + } + override fun startAutoUpload(syncedFolder: SyncedFolder, overridePowerSaving: Boolean) { val syncedFolderID = syncedFolder.id diff --git a/app/src/main/java/com/nextcloud/client/jobs/ContentObserverWork.kt b/app/src/main/java/com/nextcloud/client/jobs/ContentObserverWork.kt index 214b85239b43..9229fd297018 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/ContentObserverWork.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/ContentObserverWork.kt @@ -72,7 +72,7 @@ class ContentObserverWork( } private suspend fun checkAndTriggerAutoUpload() = withContext(Dispatchers.IO) { - if (powerManagementService.isPowerSavingEnabled) { + if (powerManagementService.blocksAutoUpload) { Log_OC.w(TAG, "⚑ Power saving mode active β€” skipping file sync.") return@withContext } diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadRescanWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadRescanWorker.kt new file mode 100644 index 000000000000..a512a6102ae2 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadRescanWorker.kt @@ -0,0 +1,42 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.jobs.autoUpload + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import com.nextcloud.client.jobs.BackgroundJobManager +import com.owncloud.android.datamodel.SyncedFolderProvider +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.utils.FilesSyncHelper +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class AutoUploadRescanWorker( + context: Context, + params: WorkerParameters, + private val syncedFolderProvider: SyncedFolderProvider, + private val backgroundJobManager: BackgroundJobManager +) : CoroutineWorker(context, params) { + + companion object { + private const val TAG = "πŸ”„πŸ“€" + "AutoUploadRescan" + } + + override suspend fun doWork(): Result = withContext(Dispatchers.IO) { + val requestedFolders = FilesSyncHelper.startAutoUploadForEnabledSyncedFolders( + syncedFolderProvider, + backgroundJobManager, + false + ) + + Log_OC.d(TAG, "requested auto upload for $requestedFolders folder(s)") + + Result.success() + } +} diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadWorker.kt index 56f89c47cf94..1851113ebab2 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadWorker.kt @@ -24,6 +24,7 @@ import com.nextcloud.client.jobs.upload.FileUploadWorker import com.nextcloud.client.jobs.upload.UploadDelayPolicy import com.nextcloud.client.jobs.utils.UploadErrorNotificationManager import com.nextcloud.client.network.ConnectivityService +import com.nextcloud.client.preferences.AppPreferences import com.nextcloud.utils.extensions.getLog import com.nextcloud.utils.extensions.isConflict import com.nextcloud.utils.extensions.isNonRetryable @@ -54,6 +55,7 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext import java.io.File import kotlin.coroutines.cancellation.CancellationException +import kotlin.time.Duration.Companion.milliseconds @Suppress("LongParameterList", "TooManyFunctions", "TooGenericExceptionCaught") class AutoUploadWorker( @@ -67,7 +69,8 @@ class AutoUploadWorker( private val repository: FileSystemRepository, val viewThemeUtils: ViewThemeUtils, localBroadcastManager: LocalBroadcastManager, - private val autoUploadHelper: AutoUploadHelper + private val autoUploadHelper: AutoUploadHelper, + private val preferences: AppPreferences ) : CoroutineWorker(context, params) { companion object { @@ -98,7 +101,7 @@ class AutoUploadWorker( Log_OC.d(TAG, syncedFolder.getLog()) - if (canExitEarly(syncFolderId)) { + if (isBlocked(syncFolderId)) { return Result.success() } @@ -106,8 +109,14 @@ class AutoUploadWorker( Log_OC.w(TAG, "power saving mode enabled - override power saving: $overridePowerSaving") } - // insert entries based on selected local storage path + // The scan has to run before the scan interval decides there is nothing to do, otherwise files added + // since the previous run stay invisible until some other trigger happens to scan them in. autoUploadHelper.insertEntries(syncedFolder) + + if (hasNothingToDo()) { + return Result.success() + } + uploadFiles(syncedFolder) // only update last scan time after uploading files @@ -187,38 +196,44 @@ class AutoUploadWorker( } @Suppress("ReturnCount") - private suspend fun canExitEarly(syncedFolderID: Long): Boolean { - if ((powerManagementService.isPowerSavingEnabled && !overridePowerSaving)) { + private fun isBlocked(syncedFolderID: Long): Boolean { + if (powerManagementService.blocksAutoUpload && !overridePowerSaving) { Log_OC.w(TAG, "⚑ Skipping: device is in power saving mode") return true } + if (preferences.isGlobalUploadPaused) { + Log_OC.w(TAG, "⏸️ Skipping: uploads are paused") + return true + } + if (syncedFolderID < 0) { Log_OC.e(TAG, "invalid sync folder id") return true } - val hasPendingFiles = repository.hasPendingFiles(syncedFolder) - if (hasPendingFiles) { + return false + } + + private suspend fun hasNothingToDo(): Boolean { + if (repository.hasPendingFiles(syncedFolder)) { Log_OC.d(TAG, "pending files found, starting...") return false } val totalScanInterval = syncedFolder.getTotalScanInterval(connectivityService, powerManagementService) - val currentTime = System.currentTimeMillis() - val passedScanInterval = totalScanInterval <= currentTime + val waitingForScanInterval = totalScanInterval > System.currentTimeMillis() && !overridePowerSaving - if (!passedScanInterval && !overridePowerSaving) { + if (waitingForScanInterval) { Log_OC.w( TAG, "skipped since started before scan interval and nothing todo: " + syncedFolder.localPath ) - return true + } else { + Log_OC.d(TAG, "starting ...") } - Log_OC.d(TAG, "starting ...") - - return false + return waitingForScanInterval } private fun getUserOrReturn(syncedFolder: SyncedFolder): User? { @@ -277,18 +292,29 @@ class AutoUploadWorker( filePathsWithIds.forEachIndexed { batchIndex, (path, id) -> ensureActive() - delay(retryPolicy.getDelay()) + if (preferences.isGlobalUploadPaused) { + Log_OC.w(TAG, "⏸️ uploads paused, stopping auto upload") + return@withContext + } + + delay(retryPolicy.getDelay().milliseconds) val file = File(path) val localPath = file.absolutePath val remotePath = syncFolderHelper.getAutoUploadRemotePath(syncedFolder, file) try { + // A camera app can register the file with MediaStore before it finished writing it. + if (file.isFile && file.length() == 0L) { + Log_OC.w(TAG, "skipping empty file, still being written: $localPath") + return@forEachIndexed + } + val entityResult = getEntityResult(user, localPath, remotePath, capability) if (entityResult !is AutoUploadEntityResult.Success) { repository.markFileAsHandled(localPath, syncedFolder) Log_OC.d(TAG, "marked file as handled: $localPath") - continue + return@forEachIndexed } var (uploadEntity, upload) = entityResult.data @@ -297,7 +323,7 @@ class AutoUploadWorker( if (path.isEmpty() || !file.exists()) { Log_OC.w(TAG, "detected non-existing local file, removing entity") deleteNonExistingFile(path, id, upload) - continue + return@forEachIndexed } try { @@ -311,7 +337,12 @@ class AutoUploadWorker( val operation = createUploadFileOperation(upload, user) Log_OC.d(TAG, "πŸ•’ uploading: $localPath, id: $generatedId") - val result = operation.execute(client) + FileUploadWorker.registerActiveUpload(operation) + val result = try { + operation.execute(client) + } finally { + FileUploadWorker.unregisterActiveUpload(operation.ocUploadId) + } fileUploadEventBroadcaster.sendUploadStarted(operation, context) UploadErrorNotificationManager.handleResult( @@ -363,7 +394,7 @@ class AutoUploadWorker( if (path.isEmpty() || !file.exists()) { Log_OC.w(TAG, "detected non-existing local file, removing entity") deleteNonExistingFile(path, id, upload) - continue + return@forEachIndexed } } } catch (e: Exception) { @@ -385,6 +416,15 @@ class AutoUploadWorker( uploadsStorageManager.removeUpload(upload) } + private fun isResolvedByCurrentCollisionPolicy(lastUploadResult: UploadResult): Boolean { + val conflictResults = setOf( + UploadResult.SYNC_CONFLICT, + UploadResult.CONFLICT_ERROR + ) + + return lastUploadResult in conflictResults && syncedFolder.nameCollisionPolicy != NameCollisionPolicy.ASK_USER + } + @Suppress("ReturnCount") private fun getEntityResult( user: User, @@ -403,7 +443,7 @@ class AutoUploadWorker( ) val lastUploadResult = uploadEntity?.lastResult?.let { UploadResult.fromValue(it) } - if (lastUploadResult?.isNonRetryable() == true) { + if (lastUploadResult?.isNonRetryable() == true && !isResolvedByCurrentCollisionPolicy(lastUploadResult)) { Log_OC.w( TAG, "last upload failed with ${lastUploadResult.value}, skipping auto-upload: $localPath" @@ -458,7 +498,7 @@ class AutoUploadWorker( true, FileDataStorageManager(user, context.contentResolver) ).apply { - if (overridePowerSaving) { + if (overridePowerSaving || !powerManagementService.blocksAutoUpload) { isIgnoringPowerSaveMode = true } } diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt index a9b678592512..425c717649b4 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt @@ -90,6 +90,8 @@ class FileUploadHelper { const val MAX_FILE_COUNT = 500 + const val MAX_UPLOADS_PER_QUERY = 500 + val mBoundListeners = HashMap() private val retryInProgress = AtomicBoolean(false) @@ -377,6 +379,10 @@ class FileUploadHelper { * If `null`, uploads matching the given [status] from all accounts are returned. * @param status The [UploadStatus] to filter uploads by (e.g., `UPLOAD_FAILED`). * @param nameCollisionPolicy The [NameCollisionPolicy] to filter uploads by (e.g., `SKIP`). + * + * At most [MAX_UPLOADS_PER_QUERY] of the newest uploads are returned. A history that grew into the tens of + * thousands of rows cannot be held in memory at once, and neither the upload list nor a retry pass needs more + * than a page of it. */ suspend fun getUploadsByStatus( accountName: String?, @@ -386,9 +392,14 @@ class FileUploadHelper { ): List { val dao = uploadsStorageManager.uploadDao return if (accountName != null) { - dao.getUploadsByAccountNameAndStatus(accountName, status.value, nameCollisionPolicy?.serialize()) + dao.getUploadsByAccountNameAndStatus( + accountName, + status.value, + nameCollisionPolicy?.serialize(), + MAX_UPLOADS_PER_QUERY + ) } else { - dao.getUploadsByStatus(status.value, nameCollisionPolicy?.serialize()) + dao.getUploadsByStatus(status.value, nameCollisionPolicy?.serialize(), MAX_UPLOADS_PER_QUERY) }.mapNotNull { it.toOCUpload(capability) } diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt index ad16103230d7..541e48c4827d 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt @@ -125,6 +125,19 @@ class FileUploadWorker( it.remotePath == remotePath && it.user.accountName == accountName } + /** + * [activeOperations] tracks every upload running in this process, not only the ones this worker started. + * Auto upload has to register here too, otherwise a retry pass sees its in-progress row as abandoned and + * reschedules it, and the cancel action in the upload list has nothing to cancel. + */ + fun registerActiveUpload(operation: UploadFileOperation) { + activeOperations[operation.ocUploadId] = operation + } + + fun unregisterActiveUpload(uploadId: Long) { + activeOperations.remove(uploadId) + } + fun getUploadAction(action: String): Int = when (action) { "LOCAL_BEHAVIOUR_FORGET" -> LOCAL_BEHAVIOUR_FORGET "LOCAL_BEHAVIOUR_MOVE" -> LOCAL_BEHAVIOUR_MOVE diff --git a/app/src/main/java/com/nextcloud/ui/component/UploadWarningCard.kt b/app/src/main/java/com/nextcloud/ui/component/UploadWarningCard.kt index d668deb22cea..1e25dfe17bd0 100644 --- a/app/src/main/java/com/nextcloud/ui/component/UploadWarningCard.kt +++ b/app/src/main/java/com/nextcloud/ui/component/UploadWarningCard.kt @@ -39,7 +39,7 @@ class UploadWarningCard( private val viewThemeUtils: ViewThemeUtils ) { fun bind(binding: UploadWarningCardBinding) { - val isBatterySaver = powerManagementService.isPowerSavingEnabled + val isBatterySaver = powerManagementService.blocksAutoUpload val isIgnoringOptimization = powerManagementService.isIgnoringOptimization binding.root.setVisibleIf(isBatterySaver || !isIgnoringOptimization) diff --git a/app/src/main/java/com/nextcloud/utils/extensions/UploadResultExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/UploadResultExtensions.kt index 62f36ebc36eb..bedf7b922973 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/UploadResultExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/UploadResultExtensions.kt @@ -23,14 +23,11 @@ fun UploadResult.isNonRetryable(): Boolean = when (this) { UploadResult.QUOTA_EXCEEDED, UploadResult.PRIVILEGES_ERROR, - // most cases covered and mapped from RemoteOperationResult. Most likely UploadResult.UNKNOWN this error will - // occur again - UploadResult.UNKNOWN, - // user's choice UploadResult.CANCELLED -> true - // everything else may succeed after retry + // Everything else may succeed after retry. UNKNOWN in particular must stay retryable: it is the catch-all for + // unmapped transport failures else -> false } diff --git a/app/src/main/java/com/owncloud/android/MainApp.java b/app/src/main/java/com/owncloud/android/MainApp.java index f08bde45fee9..2afd5ade94ef 100644 --- a/app/src/main/java/com/owncloud/android/MainApp.java +++ b/app/src/main/java/com/owncloud/android/MainApp.java @@ -353,6 +353,7 @@ public void onCreate() { if (backgroundJobManager != null) { backgroundJobManager.scheduleMediaFoldersDetectionJob(); backgroundJobManager.startMediaFoldersDetectionJob(); + backgroundJobManager.schedulePeriodicAutoUpload(); backgroundJobManager.schedulePeriodicHealthStatus(); if (preferences.isTwoWaySyncEnabled()) { 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 173b81fb9a19..b3a91ca55c87 100644 --- a/app/src/main/java/com/owncloud/android/db/ProviderMeta.java +++ b/app/src/main/java/com/owncloud/android/db/ProviderMeta.java @@ -24,7 +24,7 @@ */ public class ProviderMeta { public static final String DB_NAME = "filelist"; - public static final int DB_VERSION = 104; + public static final int DB_VERSION = 105; private ProviderMeta() { // No instance diff --git a/app/src/main/java/com/owncloud/android/files/BootupBroadcastReceiver.java b/app/src/main/java/com/owncloud/android/files/BootupBroadcastReceiver.java index 8eb62f17a9c9..22cae6e42047 100644 --- a/app/src/main/java/com/owncloud/android/files/BootupBroadcastReceiver.java +++ b/app/src/main/java/com/owncloud/android/files/BootupBroadcastReceiver.java @@ -72,6 +72,7 @@ public void onReceive(Context context, Intent intent) { walledCheckCache); Log_OC.d(TAG, "scheduleContentObserverJob, called"); backgroundJobManager.scheduleContentObserverJob(); + backgroundJobManager.schedulePeriodicAutoUpload(); MainApp.initContactsBackup(accountManager, backgroundJobManager); } else { Log_OC.d(TAG, "Getting wrong intent: " + intent.getAction()); diff --git a/app/src/main/java/com/owncloud/android/utils/FileStorageUtils.java b/app/src/main/java/com/owncloud/android/utils/FileStorageUtils.java index 014f4e12d64e..a067f9f1f6bc 100644 --- a/app/src/main/java/com/owncloud/android/utils/FileStorageUtils.java +++ b/app/src/main/java/com/owncloud/android/utils/FileStorageUtils.java @@ -272,6 +272,37 @@ private static String getSubPathFromDate(long date, Locale currentLocale, SubFol return df.format(d); } + /** + * Removes the synced folder from the beginning of a local file path, leaving only the subfolders that have to be + * mirrored below the remote folder. + *

+ * Files are selected with a SQL {@code LIKE} on the synced folder path, which is case insensitive, so a folder + * configured as {@code DCIM/camera} also collects the files MediaStore stores under {@code DCIM/Camera}. The + * prefix therefore has to be removed case insensitively too, or the whole local path ends up on the server. + */ + private static String stripSyncedFolderPrefix(String absolutePath, String syncedFolderLocalPath) { + if (syncedFolderLocalPath == null || syncedFolderLocalPath.isEmpty()) { + return absolutePath; + } + + String prefix = syncedFolderLocalPath.endsWith(OCFile.PATH_SEPARATOR) + ? syncedFolderLocalPath.substring(0, syncedFolderLocalPath.length() - 1) + : syncedFolderLocalPath; + + if (absolutePath.startsWith(prefix)) { + return absolutePath.substring(prefix.length()); + } + + if (absolutePath.regionMatches(true, 0, prefix, 0, prefix.length())) { + Log_OC.w(TAG, "local path differs from the synced folder in letter case only, stripping anyway"); + return absolutePath.substring(prefix.length()); + } + + Log_OC.e(TAG, "local file is not below its synced folder, dropping the local subfolders"); + + return OCFile.PATH_SEPARATOR + new File(absolutePath).getName(); + } + /** * Returns the InstantUploadFilePath on the nextcloud instance * @@ -291,7 +322,7 @@ public static String getInstantUploadFilePath(File file, } Log_OC.w(TAG, "FileStorageUtils:getInstantUploadFilePath subfolderByDate: " + subfolderByDate); - File parentFile = new File(file.getAbsolutePath().replace(syncedFolderLocalPath, "")).getParentFile(); + File parentFile = new File(stripSyncedFolderPrefix(file.getAbsolutePath(), syncedFolderLocalPath)).getParentFile(); String relativeSubfolderPath = ""; if (parentFile == null) { diff --git a/app/src/test/java/com/nextcloud/client/device/TestPowerManagementService.kt b/app/src/test/java/com/nextcloud/client/device/TestPowerManagementService.kt index 366f3fcef218..87d1ec7c0418 100644 --- a/app/src/test/java/com/nextcloud/client/device/TestPowerManagementService.kt +++ b/app/src/test/java/com/nextcloud/client/device/TestPowerManagementService.kt @@ -91,6 +91,35 @@ class TestPowerManagementService { // power saving is disabled assertFalse(powerManagementService.isPowerSavingEnabled) } + + private fun givenBatteryOptimizationIsIgnored(ignored: Boolean) { + whenever(context.getSystemService(Context.POWER_SERVICE)).thenReturn(platformPowerManager) + whenever(context.packageName).thenReturn("com.nextcloud.client") + whenever(platformPowerManager.isIgnoringBatteryOptimizations(any())).thenReturn(ignored) + } + + @Test + fun `auto upload is blocked while the system still restricts the app`() { + whenever(platformPowerManager.isPowerSaveMode).thenReturn(true) + givenBatteryOptimizationIsIgnored(false) + + assertTrue(powerManagementService.blocksAutoUpload) + } + + @Test + fun `auto upload is not blocked once the app is excluded from battery optimization`() { + whenever(platformPowerManager.isPowerSaveMode).thenReturn(true) + givenBatteryOptimizationIsIgnored(true) + + assertFalse(powerManagementService.blocksAutoUpload) + } + + @Test + fun `auto upload is not blocked while battery saver is off`() { + whenever(platformPowerManager.isPowerSaveMode).thenReturn(false) + + assertFalse(powerManagementService.blocksAutoUpload) + } } @SuppressLint("UnspecifiedRegisterReceiverFlag") diff --git a/app/src/test/java/com/nextcloud/client/jobs/ContentObserverWorkTest.kt b/app/src/test/java/com/nextcloud/client/jobs/ContentObserverWorkTest.kt index f8698c15a4d6..342129b8fa63 100644 --- a/app/src/test/java/com/nextcloud/client/jobs/ContentObserverWorkTest.kt +++ b/app/src/test/java/com/nextcloud/client/jobs/ContentObserverWorkTest.kt @@ -98,7 +98,7 @@ class ContentObserverWorkTest { @Test fun power_saving_stops_a_run_before_folders_are_read() { runBlocking { - whenever(powerManagementService.isPowerSavingEnabled).thenReturn(true) + whenever(powerManagementService.blocksAutoUpload).thenReturn(true) worker.doWork() diff --git a/app/src/test/java/com/nextcloud/client/utils/FileStorageUtilsTest.kt b/app/src/test/java/com/nextcloud/client/utils/FileStorageUtilsTest.kt index 56441d2da535..79e3a13ab64d 100644 --- a/app/src/test/java/com/nextcloud/client/utils/FileStorageUtilsTest.kt +++ b/app/src/test/java/com/nextcloud/client/utils/FileStorageUtilsTest.kt @@ -210,6 +210,66 @@ class FileStorageUtilsTest { assertEquals(expected, result) } + @Test + fun instantUploadPathIgnoresLetterCaseOfTheSyncedFolder() { + val result = FileStorageUtils.getInstantUploadFilePath( + File("/storage/emulated/0/DCIM/camera/IMG_20260101_120000.jpg"), + Locale.ROOT, + "/Autoupload/Camera", + "/storage/emulated/0/DCIM/Camera", + 123123123L, + false, + SubFolderRule.YEAR_MONTH + ) + + assertEquals("/Autoupload/Camera/IMG_20260101_120000.jpg", result) + } + + @Test + fun instantUploadPathNeverMirrorsALocalPathThatIsNotBelowTheSyncedFolder() { + val result = FileStorageUtils.getInstantUploadFilePath( + File("/storage/emulated/0/Pictures/Other/file.jpg"), + Locale.ROOT, + "/Autoupload/Camera", + "/storage/emulated/0/DCIM/Camera", + 123123123L, + false, + SubFolderRule.YEAR_MONTH + ) + + assertEquals("/Autoupload/Camera/file.jpg", result) + } + + @Test + fun instantUploadPathStripsTheSyncedFolderOnlyFromTheStart() { + val result = FileStorageUtils.getInstantUploadFilePath( + File("/sdcard/DCIM/DCIM/file.jpg"), + Locale.ROOT, + "/Camera", + "/sdcard/DCIM", + 123123123L, + false, + SubFolderRule.YEAR_MONTH + ) + + assertEquals("/Camera/DCIM/file.jpg", result) + } + + @Test + fun instantUploadPathToleratesATrailingSeparatorOnTheSyncedFolder() { + val result = FileStorageUtils.getInstantUploadFilePath( + File("/sdcard/DCIM/file.jpg"), + Locale.ROOT, + "/Camera", + "/sdcard/DCIM/", + 123123123L, + false, + SubFolderRule.YEAR_MONTH + ) + + assertEquals("/Camera/file.jpg", result) + } + @Test fun testGetFilenameAndExtensionWhenGivenInvalidFilenamesWithSpecialChars() { val result = FileStorageUtils.getFilenameAndExtension("invoice\u202Ecod.exe", false, false)