From acc6157704588d428d97de6f008c5d39ab93ab18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Corentin=20No=C3=ABl?= Date: Thu, 5 Nov 2026 15:11:41 +0000 Subject: [PATCH] feat(auto-upload): allow delaying local copy deletion after 60 days MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a "deleted after 60 days" entry to the synced folder "Original file will be…" option. The number 60 is a good know value from other implementations but I've made purposely made it flexible for future-proofing. Assisted-by: Copilot:claude-sonnet-5.5 Signed-off-by: Corentin Noël --- .../107.json | 1394 +++++++++++++++++ .../nextcloud/client/AutoUploadFragmentIT.kt | 3 +- .../android/utils/SyncedFolderUtilsTest.kt | 6 +- .../client/database/NextcloudDatabase.kt | 3 +- .../database/entity/SyncedFolderEntity.kt | 8 +- .../client/jobs/BackgroundJobManager.kt | 2 +- .../client/jobs/BackgroundJobManagerImpl.kt | 8 +- .../AutoUploadLocalDeletionWorker.kt | 44 +- .../jobs/autoUpload/AutoUploadRescanWorker.kt | 7 + .../android/datamodel/SyncedFolder.java | 27 +- .../datamodel/SyncedFolderDisplayItem.java | 12 +- .../android/datamodel/SyncedFolderProvider.kt | 7 +- .../com/owncloud/android/db/ProviderMeta.java | 3 +- .../upload/DeleteUploadedFileOperation.kt | 11 +- .../SyncedFolderPreferencesDialogFragment.kt | 15 +- .../dialog/parcel/SyncedFolderParcelable.java | 28 + .../data/AutoUploadDisplayItemFactory.kt | 13 +- app/src/main/res/values/attrs.xml | 1 + app/src/main/res/values/strings.xml | 4 + .../SyncedFolderDisplayItemExtensionsTests.kt | 9 +- .../SyncedFolderDisplayItemExtensionsTest.kt | 3 +- .../upload/DeleteUploadedFileOperationTest.kt | 50 + .../parcel/SyncedFolderParcelableTest.kt | 43 + .../utils/AutoUploadFileMatchingTests.kt | 3 +- .../android/utils/AutoUploadHelperTest.kt | 3 +- 25 files changed, 1660 insertions(+), 47 deletions(-) create mode 100644 app/schemas/com.nextcloud.client.database.NextcloudDatabase/107.json create mode 100644 app/src/test/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperationTest.kt create mode 100644 app/src/test/java/com/owncloud/android/ui/dialog/parcel/SyncedFolderParcelableTest.kt diff --git a/app/schemas/com.nextcloud.client.database.NextcloudDatabase/107.json b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/107.json new file mode 100644 index 000000000000..db03788bb8cd --- /dev/null +++ b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/107.json @@ -0,0 +1,1394 @@ +{ + "formatVersion": 1, + "database": { + "version": 107, + "identityHash": "614444a5d483e4d0a18e6db69d457a4f", + "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, `sharing_json` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "assistant", + "columnName": "assistant", + "affinity": "INTEGER" + }, + { + "fieldPath": "accountName", + "columnName": "account", + "affinity": "TEXT" + }, + { + "fieldPath": "versionMajor", + "columnName": "version_mayor", + "affinity": "INTEGER" + }, + { + "fieldPath": "versionMinor", + "columnName": "version_minor", + "affinity": "INTEGER" + }, + { + "fieldPath": "versionMicro", + "columnName": "version_micro", + "affinity": "INTEGER" + }, + { + "fieldPath": "versionString", + "columnName": "version_string", + "affinity": "TEXT" + }, + { + "fieldPath": "versionEditor", + "columnName": "version_edition", + "affinity": "TEXT" + }, + { + "fieldPath": "extendedSupport", + "columnName": "extended_support", + "affinity": "INTEGER" + }, + { + "fieldPath": "corePollinterval", + "columnName": "core_pollinterval", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingApiEnabled", + "columnName": "sharing_api_enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicEnabled", + "columnName": "sharing_public_enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicPasswordEnforced", + "columnName": "sharing_public_password_enforced", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicExpireDateEnabled", + "columnName": "sharing_public_expire_date_enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicExpireDateDays", + "columnName": "sharing_public_expire_date_days", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicExpireDateEnforced", + "columnName": "sharing_public_expire_date_enforced", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicSendMail", + "columnName": "sharing_public_send_mail", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicUpload", + "columnName": "sharing_public_upload", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingUserSendMail", + "columnName": "sharing_user_send_mail", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingResharing", + "columnName": "sharing_resharing", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingFederationOutgoing", + "columnName": "sharing_federation_outgoing", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingFederationIncoming", + "columnName": "sharing_federation_incoming", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesBigfilechunking", + "columnName": "files_bigfilechunking", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesUndelete", + "columnName": "files_undelete", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesVersioning", + "columnName": "files_versioning", + "affinity": "INTEGER" + }, + { + "fieldPath": "externalLinks", + "columnName": "external_links", + "affinity": "INTEGER" + }, + { + "fieldPath": "serverName", + "columnName": "server_name", + "affinity": "TEXT" + }, + { + "fieldPath": "serverColor", + "columnName": "server_color", + "affinity": "TEXT" + }, + { + "fieldPath": "serverTextColor", + "columnName": "server_text_color", + "affinity": "TEXT" + }, + { + "fieldPath": "serverElementColor", + "columnName": "server_element_color", + "affinity": "TEXT" + }, + { + "fieldPath": "serverSlogan", + "columnName": "server_slogan", + "affinity": "TEXT" + }, + { + "fieldPath": "serverLogo", + "columnName": "server_logo", + "affinity": "TEXT" + }, + { + "fieldPath": "serverBackgroundUrl", + "columnName": "background_url", + "affinity": "TEXT" + }, + { + "fieldPath": "endToEndEncryption", + "columnName": "end_to_end_encryption", + "affinity": "INTEGER" + }, + { + "fieldPath": "endToEndEncryptionKeysExist", + "columnName": "end_to_end_encryption_keys_exist", + "affinity": "INTEGER" + }, + { + "fieldPath": "endToEndEncryptionApiVersion", + "columnName": "end_to_end_encryption_api_version", + "affinity": "TEXT" + }, + { + "fieldPath": "activity", + "columnName": "activity", + "affinity": "INTEGER" + }, + { + "fieldPath": "serverBackgroundDefault", + "columnName": "background_default", + "affinity": "INTEGER" + }, + { + "fieldPath": "serverBackgroundPlain", + "columnName": "background_plain", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocument", + "columnName": "richdocument", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentMimetypeList", + "columnName": "richdocument_mimetype_list", + "affinity": "TEXT" + }, + { + "fieldPath": "richdocumentDirectEditing", + "columnName": "richdocument_direct_editing", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentTemplates", + "columnName": "richdocument_direct_templates", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentOptionalMimetypeList", + "columnName": "richdocument_optional_mimetype_list", + "affinity": "TEXT" + }, + { + "fieldPath": "sharingPublicAskForOptionalPassword", + "columnName": "sharing_public_ask_for_optional_password", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentProductName", + "columnName": "richdocument_product_name", + "affinity": "TEXT" + }, + { + "fieldPath": "directEditingEtag", + "columnName": "direct_editing_etag", + "affinity": "TEXT" + }, + { + "fieldPath": "userStatus", + "columnName": "user_status", + "affinity": "INTEGER" + }, + { + "fieldPath": "userStatusSupportsEmoji", + "columnName": "user_status_supports_emoji", + "affinity": "INTEGER" + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT" + }, + { + "fieldPath": "filesLockingVersion", + "columnName": "files_locking_version", + "affinity": "TEXT" + }, + { + "fieldPath": "groupfolders", + "columnName": "groupfolders", + "affinity": "INTEGER" + }, + { + "fieldPath": "dropAccount", + "columnName": "drop_account", + "affinity": "INTEGER" + }, + { + "fieldPath": "securityGuard", + "columnName": "security_guard", + "affinity": "INTEGER" + }, + { + "fieldPath": "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" + }, + { + "fieldPath": "sharingJson", + "columnName": "sharing_json", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "external_links", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `icon_url` TEXT, `language` TEXT, `type` INTEGER, `name` TEXT, `url` TEXT, `redirect` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "iconUrl", + "columnName": "icon_url", + "affinity": "TEXT" + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT" + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT" + }, + { + "fieldPath": "redirect", + "columnName": "redirect", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "filelist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `filename` TEXT, `encrypted_filename` TEXT, `path` TEXT, `path_decrypted` TEXT, `parent` INTEGER, `created` INTEGER, `modified` INTEGER, `content_type` TEXT, `content_length` INTEGER, `media_path` TEXT, `file_owner` TEXT, `last_sync_date` INTEGER, `last_sync_date_for_data` INTEGER, `modified_at_last_sync_for_data` INTEGER, `etag` TEXT, `etag_on_server` TEXT, `share_by_link` INTEGER, `permissions` TEXT, `remote_id` TEXT, `local_id` INTEGER NOT NULL DEFAULT -1, `update_thumbnail` INTEGER, `is_downloading` INTEGER, `favorite` INTEGER, `hidden` INTEGER, `is_encrypted` INTEGER, `etag_in_conflict` TEXT, `shared_via_users` INTEGER, `mount_type` INTEGER, `has_preview` INTEGER, `unread_comments_count` INTEGER, `owner_id` TEXT, `owner_display_name` TEXT, `note` TEXT, `sharees` TEXT, `rich_workspace` TEXT, `metadata_size` TEXT, `metadata_live_photo` TEXT, `locked` INTEGER, `lock_type` INTEGER, `lock_owner` TEXT, `lock_owner_display_name` TEXT, `lock_owner_editor` TEXT, `lock_timestamp` INTEGER, `lock_timeout` INTEGER, `lock_token` TEXT, `tags` TEXT, `metadata_gps` TEXT, `e2e_counter` INTEGER, `internal_two_way_sync_timestamp` INTEGER, `internal_two_way_sync_result` TEXT, `uploaded` INTEGER, `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, `auto_delete_local_after_ms` INTEGER DEFAULT 0)", + "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" + }, + { + "fieldPath": "autoDeleteLocalAfterMs", + "columnName": "auto_delete_local_after_ms", + "affinity": "INTEGER", + "defaultValue": "0" + } + ], + "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, '614444a5d483e4d0a18e6db69d457a4f')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/nextcloud/client/AutoUploadFragmentIT.kt b/app/src/androidTest/java/com/nextcloud/client/AutoUploadFragmentIT.kt index ccfc8243d98d..776834b69989 100644 --- a/app/src/androidTest/java/com/nextcloud/client/AutoUploadFragmentIT.kt +++ b/app/src/androidTest/java/com/nextcloud/client/AutoUploadFragmentIT.kt @@ -78,7 +78,8 @@ class AutoUploadFragmentIT : AbstractIT() { false, SubFolderRule.YEAR_MONTH, false, - SyncedFolder.NOT_SCANNED_YET + SyncedFolder.NOT_SCANNED_YET, + SyncedFolder.AUTO_DELETE_LOCAL_DISABLED ) launch { scenario -> diff --git a/app/src/androidTest/java/com/owncloud/android/utils/SyncedFolderUtilsTest.kt b/app/src/androidTest/java/com/owncloud/android/utils/SyncedFolderUtilsTest.kt index 310affdba943..35528461b758 100644 --- a/app/src/androidTest/java/com/owncloud/android/utils/SyncedFolderUtilsTest.kt +++ b/app/src/androidTest/java/com/owncloud/android/utils/SyncedFolderUtilsTest.kt @@ -176,7 +176,8 @@ class SyncedFolderUtilsTest : AbstractIT() { false, SubFolderRule.YEAR_MONTH, false, - SyncedFolder.NOT_SCANNED_YET + SyncedFolder.NOT_SCANNED_YET, + SyncedFolder.AUTO_DELETE_LOCAL_DISABLED ) Assert.assertFalse(SyncedFolderUtils.isQualifyingMediaFolder(folder)) } @@ -201,7 +202,8 @@ class SyncedFolderUtilsTest : AbstractIT() { false, SubFolderRule.YEAR_MONTH, false, - SyncedFolder.NOT_SCANNED_YET + SyncedFolder.NOT_SCANNED_YET, + SyncedFolder.AUTO_DELETE_LOCAL_DISABLED ) Assert.assertFalse(SyncedFolderUtils.isQualifyingMediaFolder(folder)) } 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 b6676caf0a1b..77d8360d8ce0 100644 --- a/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt +++ b/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt @@ -105,7 +105,8 @@ import com.owncloud.android.db.ProviderMeta AutoMigration(from = 102, to = 103, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class), AutoMigration(from = 103, to = 104), AutoMigration(from = 104, to = 105), - AutoMigration(from = 105, to = 106, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class) + AutoMigration(from = 105, to = 106, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class), + AutoMigration(from = 106, to = 107) ], exportSchema = true ) diff --git a/app/src/main/java/com/nextcloud/client/database/entity/SyncedFolderEntity.kt b/app/src/main/java/com/nextcloud/client/database/entity/SyncedFolderEntity.kt index 18b0911209ed..cd7fece6a792 100644 --- a/app/src/main/java/com/nextcloud/client/database/entity/SyncedFolderEntity.kt +++ b/app/src/main/java/com/nextcloud/client/database/entity/SyncedFolderEntity.kt @@ -51,7 +51,9 @@ data class SyncedFolderEntity( @ColumnInfo(name = ProviderTableMeta.SYNCED_FOLDER_EXCLUDE_HIDDEN) val excludeHidden: Int?, @ColumnInfo(name = ProviderTableMeta.SYNCED_FOLDER_LAST_SCAN_TIMESTAMP_MS) - val lastScanTimestampMs: Long? + val lastScanTimestampMs: Long?, + @ColumnInfo(name = ProviderTableMeta.SYNCED_FOLDER_AUTO_DELETE_LOCAL_AFTER_MS, defaultValue = "0") + val autoDeleteLocalAfterMs: Long? = 0 ) fun SyncedFolderEntity.toSyncedFolder(): SyncedFolder = SyncedFolder( @@ -88,5 +90,7 @@ fun SyncedFolderEntity.toSyncedFolder(): SyncedFolder = SyncedFolder( // excludeHidden this.excludeHidden == 1, // lastScanTimestampMs - this.lastScanTimestampMs ?: SyncedFolder.NOT_SCANNED_YET + this.lastScanTimestampMs ?: SyncedFolder.NOT_SCANNED_YET, + // autoDeleteLocalAfterMs + this.autoDeleteLocalAfterMs ?: SyncedFolder.AUTO_DELETE_LOCAL_DISABLED ) 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 7f4a0a3184cc..924812fd26c6 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt @@ -179,5 +179,5 @@ interface BackgroundJobManager { fun startMetadataSyncJob(currentDirPath: String, folderAlreadySynced: Boolean = false) fun downloadFolder(folder: OCFile, accountName: String) fun cancelFolderDownload() - fun locallyDeleteAutoUploadedFiles(syncedFolders: List) + fun locallyDeleteAutoUploadedFiles(syncedFolders: List, minAgeMs: Long = 0L) } 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 54f3852d8763..c4968bdfcb47 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt @@ -939,20 +939,24 @@ internal class BackgroundJobManagerImpl( workManager.cancelAllWorkByTag(JOB_DOWNLOAD_FOLDER) } - override fun locallyDeleteAutoUploadedFiles(syncedFolders: List) { + override fun locallyDeleteAutoUploadedFiles(syncedFolders: List, minAgeMs: Long) { val syncedFolderIDs = syncedFolders .filter { it.isEnabled } .map { it.id } + if (syncedFolderIDs.isEmpty()) { + return + } val arguments = Data.Builder() .putLongArray(AutoUploadLocalDeletionWorker.SYNCED_FOLDER_IDS, syncedFolderIDs.toLongArray()) + .putLong(AutoUploadLocalDeletionWorker.MIN_AGE_MS, minAgeMs) .build() val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() - val jobName = JOB_AUTO_UPLOAD_LOCAL_DELETION + "_" + syncedFolderIDs.joinToString("-") + val jobName = JOB_AUTO_UPLOAD_LOCAL_DELETION + "_" + minAgeMs + "_" + syncedFolderIDs.joinToString("-") val request = oneTimeRequestBuilder( jobClass = AutoUploadLocalDeletionWorker::class, jobName = jobName diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt index ecba6c0a465b..76a7c4c9a395 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadLocalDeletionWorker.kt @@ -36,6 +36,7 @@ class AutoUploadLocalDeletionWorker( companion object { const val SYNCED_FOLDER_IDS = "synced_folder_IDs" + const val MIN_AGE_MS = "min_age_ms" const val NOTIFICATION_ID = 267 private const val TAG = "AutoUploadLocalDeletionWorker" @@ -50,9 +51,13 @@ class AutoUploadLocalDeletionWorker( ) override suspend fun doWork(): Result { - showNotification( - createNotification(context.getString(R.string.autoupload_delete_uploaded_notif_started_title)) - ) + val minAgeMs = inputData.getLong(MIN_AGE_MS, 0L) + val isAutomaticRun = minAgeMs > 0 + if (!isAutomaticRun) { + showNotification( + createNotification(context.getString(R.string.autoupload_delete_uploaded_notif_started_title)) + ) + } Log_OC.d(TAG, "Started") val syncedFolderIDs = inputData.getLongArray(SYNCED_FOLDER_IDS) @@ -77,14 +82,19 @@ class AutoUploadLocalDeletionWorker( val op = DeleteUploadedFileOperation( it, context, - fileDataStorageManager + fileDataStorageManager, + minAgeMs ) val res = op.run() if (res.code != RemoteOperationResult.ResultCode.OK) { Log_OC.d(TAG, "Failed") - showNotification( - createNotification(context.getString(R.string.autoupload_delete_uploaded_notif_error_title)) - ) + if (!isAutomaticRun) { + showNotification( + createNotification( + context.getString(R.string.autoupload_delete_uploaded_notif_error_title) + ) + ) + } return Result.failure() } foldersAnalyzed++ @@ -94,16 +104,18 @@ class AutoUploadLocalDeletionWorker( } val runTimeMs = System.currentTimeMillis() - timeStarted - showNotification( - createSuccessNotification( - users.size, - foldersAnalyzed, - filesRemoved, - filesPreserved, - spaceFreed, - runTimeMs + if (!isAutomaticRun) { + showNotification( + createSuccessNotification( + users.size, + foldersAnalyzed, + filesRemoved, + filesPreserved, + spaceFreed, + runTimeMs + ) ) - ) + } Log_OC.d( TAG, "Success: users=$users, foldersAnalyzed=$foldersAnalyzed, filesPreserved=$filesPreserved, " + 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 index 4ac19f358735..c5dd154a11c8 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadRescanWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadRescanWorker.kt @@ -40,6 +40,13 @@ class AutoUploadRescanWorker( Log_OC.d(TAG, "requested auto upload for $requestedFolders folder(s)") + syncedFolderProvider.syncedFolders + .filter { it.isAutoDeleteLocal } + .groupBy { it.autoDeleteLocalAfterMs } + .forEach { (retentionMs, folders) -> + backgroundJobManager.locallyDeleteAutoUploadedFiles(folders, retentionMs) + } + Result.success() } } diff --git a/app/src/main/java/com/owncloud/android/datamodel/SyncedFolder.java b/app/src/main/java/com/owncloud/android/datamodel/SyncedFolder.java index aa564d9b737c..1bf8ddd66cd7 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/SyncedFolder.java +++ b/app/src/main/java/com/owncloud/android/datamodel/SyncedFolder.java @@ -19,6 +19,7 @@ import java.io.File; import java.io.Serializable; +import java.util.concurrent.TimeUnit; /** * Synced folder entity containing all information per synced folder. @@ -27,6 +28,8 @@ public class SyncedFolder implements Serializable, Cloneable { public static final long UNPERSISTED_ID = Long.MIN_VALUE; public static final long EMPTY_ENABLED_TIMESTAMP_MS = -1; public static final long NOT_SCANNED_YET = -1; + public static final long AUTO_DELETE_LOCAL_DISABLED = 0; + public static final long DEFAULT_AUTO_DELETE_LOCAL_AFTER_MS = TimeUnit.DAYS.toMillis(60); private static final long serialVersionUID = -793476118299906429L; @@ -48,6 +51,7 @@ public class SyncedFolder implements Serializable, Cloneable { private SubFolderRule subfolderRule; private boolean excludeHidden; private long lastScanTimestampMs; + private long autoDeleteLocalAfterMs; /** * constructor for new, to be persisted entity. @@ -67,6 +71,7 @@ public class SyncedFolder implements Serializable, Cloneable { * @param hidden hide item flag * @param subFolderRule whether to filter subFolder by year/month/day * @param excludeHidden exclude hidden file or folder, for {@link MediaFolderType#CUSTOM} only + * @param autoDeleteLocalAfterMs age in milliseconds after which uploaded local files are deleted, {@link #AUTO_DELETE_LOCAL_DISABLED} to keep them */ public SyncedFolder(String localPath, String remotePath, @@ -83,7 +88,8 @@ public SyncedFolder(String localPath, boolean hidden, SubFolderRule subFolderRule, boolean excludeHidden, - long lastScanTimestampMs) { + long lastScanTimestampMs, + long autoDeleteLocalAfterMs) { this(UNPERSISTED_ID, localPath, remotePath, @@ -100,7 +106,8 @@ public SyncedFolder(String localPath, hidden, subFolderRule, excludeHidden, - lastScanTimestampMs); + lastScanTimestampMs, + autoDeleteLocalAfterMs); } /** @@ -124,7 +131,8 @@ public SyncedFolder(long id, boolean hidden, SubFolderRule subFolderRule, boolean excludeHidden, - long lastScanTimestampMs) { + long lastScanTimestampMs, + long autoDeleteLocalAfterMs) { this.id = id; this.localPath = localPath; this.remotePath = remotePath; @@ -141,6 +149,7 @@ public SyncedFolder(long id, this.subfolderRule = subFolderRule; this.excludeHidden = excludeHidden; this.lastScanTimestampMs = lastScanTimestampMs; + this.autoDeleteLocalAfterMs = autoDeleteLocalAfterMs; } /** @@ -332,6 +341,18 @@ public boolean isFileInFolderWithCorrectMediaType(File file, String filePath) { return isInLocalDirectory && isCorrectMediaType; } + public long getAutoDeleteLocalAfterMs() { + return autoDeleteLocalAfterMs; + } + + public void setAutoDeleteLocalAfterMs(long autoDeleteLocalAfterMs) { + this.autoDeleteLocalAfterMs = autoDeleteLocalAfterMs; + } + + public boolean isAutoDeleteLocal() { + return autoDeleteLocalAfterMs > AUTO_DELETE_LOCAL_DISABLED; + } + public long getLastScanTimestampMs() { return lastScanTimestampMs; } public void setLastScanTimestampMs(long lastScanTimestampMs) { this.lastScanTimestampMs = lastScanTimestampMs; } diff --git a/app/src/main/java/com/owncloud/android/datamodel/SyncedFolderDisplayItem.java b/app/src/main/java/com/owncloud/android/datamodel/SyncedFolderDisplayItem.java index cbbf4453aadb..9fd9b95747a5 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/SyncedFolderDisplayItem.java +++ b/app/src/main/java/com/owncloud/android/datamodel/SyncedFolderDisplayItem.java @@ -61,7 +61,8 @@ public SyncedFolderDisplayItem(long id, boolean hidden, SubFolderRule subFolderRule, boolean excludeHidden, - long lastScanTimestampMs) { + long lastScanTimestampMs, + long autoDeleteLocalAfterMs) { super(id, localPath, remotePath, @@ -78,7 +79,8 @@ public SyncedFolderDisplayItem(long id, hidden, subFolderRule, excludeHidden, - lastScanTimestampMs); + lastScanTimestampMs, + autoDeleteLocalAfterMs); this.filePaths = filePaths; this.folderName = folderName; this.numberOfFiles = numberOfFiles; @@ -101,7 +103,8 @@ public SyncedFolderDisplayItem(long id, boolean hidden, SubFolderRule subFolderRule, boolean excludeHidden, - long lastScanTimestampMs) { + long lastScanTimestampMs, + long autoDeleteLocalAfterMs) { super(id, localPath, remotePath, @@ -118,7 +121,8 @@ public SyncedFolderDisplayItem(long id, hidden, subFolderRule, excludeHidden, - lastScanTimestampMs); + lastScanTimestampMs, + autoDeleteLocalAfterMs); this.folderName = folderName; } diff --git a/app/src/main/java/com/owncloud/android/datamodel/SyncedFolderProvider.kt b/app/src/main/java/com/owncloud/android/datamodel/SyncedFolderProvider.kt index 0e13d030531e..9e667200cd5e 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/SyncedFolderProvider.kt +++ b/app/src/main/java/com/owncloud/android/datamodel/SyncedFolderProvider.kt @@ -199,7 +199,8 @@ class SyncedFolderProvider( bool(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_HIDDEN), SubFolderRule.entries[int(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_SUBFOLDER_RULE)], bool(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_EXCLUDE_HIDDEN), - long(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_LAST_SCAN_TIMESTAMP_MS) + long(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_LAST_SCAN_TIMESTAMP_MS), + long(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_AUTO_DELETE_LOCAL_AFTER_MS) ) } @@ -226,5 +227,9 @@ class SyncedFolderProvider( ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_LAST_SCAN_TIMESTAMP_MS, syncedFolder.lastScanTimestampMs ) + put( + ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_AUTO_DELETE_LOCAL_AFTER_MS, + syncedFolder.autoDeleteLocalAfterMs + ) } } 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 3886a51210d6..03988bdfa434 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 = 106; + public static final int DB_VERSION = 107; private ProviderMeta() { // No instance @@ -347,6 +347,7 @@ static public class ProviderTableMeta implements BaseColumns { public static final String SYNCED_FOLDER_SUBFOLDER_RULE = "sub_folder_rule"; public static final String SYNCED_FOLDER_EXCLUDE_HIDDEN = "exclude_hidden"; public static final String SYNCED_FOLDER_LAST_SCAN_TIMESTAMP_MS = "last_scan_timestamp_ms"; + public static final String SYNCED_FOLDER_AUTO_DELETE_LOCAL_AFTER_MS = "auto_delete_local_after_ms"; // Columns of external links table public static final String EXTERNAL_LINKS_ICON_URL = "icon_url"; diff --git a/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt b/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt index b81137979d24..51c9e5a19937 100644 --- a/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt +++ b/app/src/main/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperation.kt @@ -24,7 +24,8 @@ import com.nextcloud.utils.AppConstants.MILLIS_PER_SECOND class DeleteUploadedFileOperation( private val syncedFolder: SyncedFolder, private val context: Context, - private val storageManager: FileDataStorageManager + private val storageManager: FileDataStorageManager, + private val minAgeMs: Long = 0L ) { companion object { @@ -92,7 +93,15 @@ class DeleteUploadedFileOperation( return result } + internal fun isOldEnough(localFile: File): Boolean = + System.currentTimeMillis() - localFile.lastModified() >= minAgeMs + private suspend fun fileCanBeDeleted(localFile: File, folder: OCFile): Boolean { + if (!isOldEnough(localFile)) { + Log_OC.d(TAG, "File ${localFile.name} is newer than the retention period, leaving in place") + return false + } + val remotePath = syncFolderHelper.getAutoUploadRemotePath(syncedFolder, localFile) val ocFile = storageManager.getFileByRemotePath(remotePath) diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/SyncedFolderPreferencesDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/SyncedFolderPreferencesDialogFragment.kt index e88b2622fb62..4d6cd920bd8e 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/SyncedFolderPreferencesDialogFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/SyncedFolderPreferencesDialogFragment.kt @@ -38,6 +38,7 @@ import com.owncloud.android.utils.theme.ViewThemeUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlin.time.Duration.Companion.milliseconds import java.io.File import javax.inject.Inject @@ -86,7 +87,7 @@ class SyncedFolderPreferencesDialogFragment : syncedFolder = arguments.getParcelableArgument(SYNCED_FOLDER_PARCELABLE, SyncedFolderParcelable::class.java) } - uploadBehaviorItemStrings = resources.getTextArray(R.array.pref_behaviour_entries) + uploadBehaviorItemStrings = behaviourEntries() nameCollisionPolicyItemStrings = resources.getTextArray(R.array.pref_name_collision_policy_entries) } @@ -391,11 +392,21 @@ class SyncedFolderPreferencesDialogFragment : binding.settingInstantNameCollisionPolicyContainer.setOnClickListener { showNameCollisionPolicyDialog() } } + private fun behaviourEntries(): Array { + val retentionDays = SyncedFolder.DEFAULT_AUTO_DELETE_LOCAL_AFTER_MS.milliseconds.inWholeDays.toInt() + val deleteAfterDelayEntry = resources.getQuantityString( + R.plurals.pref_behaviour_entries_delete_file_after_days, + retentionDays, + retentionDays + ) + return resources.getTextArray(R.array.pref_behaviour_entries) + deleteAfterDelayEntry + } + private fun showBehaviourDialog() { val builder = MaterialAlertDialogBuilder(requireActivity()) syncedFolder?.let { - val behaviourEntries = resources.getTextArray(R.array.pref_behaviour_entries) + val behaviourEntries = behaviourEntries() val behaviourEntryValues = resources.getTextArray(R.array.pref_behaviour_entryValues) builder.setTitle(R.string.prefs_instant_behaviour_dialogTitle) .setSingleChoiceItems(behaviourEntries, it.uploadActionInteger) { dialog: DialogInterface, which: Int -> diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/parcel/SyncedFolderParcelable.java b/app/src/main/java/com/owncloud/android/ui/dialog/parcel/SyncedFolderParcelable.java index 5e64943f31b4..ae7b41be2d1e 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/parcel/SyncedFolderParcelable.java +++ b/app/src/main/java/com/owncloud/android/ui/dialog/parcel/SyncedFolderParcelable.java @@ -13,6 +13,7 @@ import com.nextcloud.client.jobs.upload.FileUploadWorker; import com.nextcloud.client.preferences.SubFolderRule; import com.owncloud.android.datamodel.MediaFolderType; +import com.owncloud.android.datamodel.SyncedFolder; import com.owncloud.android.datamodel.SyncedFolderDisplayItem; import com.owncloud.android.files.services.NameCollisionPolicy; @@ -20,6 +21,7 @@ * Parcelable for {@link SyncedFolderDisplayItem} objects to transport them from/to dialog fragments. */ public class SyncedFolderParcelable implements Parcelable { + private static final int BEHAVIOUR_INDEX_DELETE_AFTER_DELAY = 3; private String folderName; private String localPath; private String remotePath; @@ -37,6 +39,7 @@ public class SyncedFolderParcelable implements Parcelable { private int section; private SubFolderRule subFolderRule; private boolean excludeHidden; + private long autoDeleteLocalAfterMs; public SyncedFolderParcelable(SyncedFolderDisplayItem syncedFolderDisplayItem, int section) { id = syncedFolderDisplayItem.getId(); @@ -57,6 +60,7 @@ public SyncedFolderParcelable(SyncedFolderDisplayItem syncedFolderDisplayItem, i hidden = syncedFolderDisplayItem.isHidden(); subFolderRule = syncedFolderDisplayItem.getSubfolderRule(); excludeHidden = syncedFolderDisplayItem.isExcludeHidden(); + autoDeleteLocalAfterMs = syncedFolderDisplayItem.getAutoDeleteLocalAfterMs(); } private SyncedFolderParcelable(Parcel read) { @@ -77,6 +81,7 @@ private SyncedFolderParcelable(Parcel read) { hidden = read.readInt() != 0; subFolderRule = SubFolderRule.values()[read.readInt()]; excludeHidden = read.readInt() != 0; + autoDeleteLocalAfterMs = read.readLong(); } public SyncedFolderParcelable() { @@ -102,6 +107,7 @@ public void writeToParcel(Parcel dest, int flags) { dest.writeInt(hidden ? 1 : 0); dest.writeInt(subFolderRule.ordinal()); dest.writeInt(excludeHidden ? 1 : 0); + dest.writeLong(autoDeleteLocalAfterMs); } public static final Creator CREATOR = @@ -124,6 +130,10 @@ public int describeContents() { } public Integer getUploadActionInteger() { + if (isAutoDeleteLocal()) { + return BEHAVIOUR_INDEX_DELETE_AFTER_DELAY; + } + return switch (uploadAction) { case FileUploadWorker.LOCAL_BEHAVIOUR_FORGET -> 0; case FileUploadWorker.LOCAL_BEHAVIOUR_MOVE -> 1; @@ -133,6 +143,8 @@ public Integer getUploadActionInteger() { } public void setUploadAction(String uploadAction) { + autoDeleteLocalAfterMs = SyncedFolder.AUTO_DELETE_LOCAL_DISABLED; + switch (uploadAction) { case "LOCAL_BEHAVIOUR_FORGET": this.uploadAction = FileUploadWorker.LOCAL_BEHAVIOUR_FORGET; @@ -143,6 +155,10 @@ public void setUploadAction(String uploadAction) { case "LOCAL_BEHAVIOUR_DELETE": this.uploadAction = FileUploadWorker.LOCAL_BEHAVIOUR_DELETE; break; + case "LOCAL_BEHAVIOUR_DELETE_AFTER_DELAY": + this.uploadAction = FileUploadWorker.LOCAL_BEHAVIOUR_FORGET; + autoDeleteLocalAfterMs = SyncedFolder.DEFAULT_AUTO_DELETE_LOCAL_AFTER_MS; + break; default: // do nothing break; @@ -268,6 +284,18 @@ public void setSection(int section) { } public void setSubFolderRule(SubFolderRule subFolderRule) { this.subFolderRule = subFolderRule; } + public long getAutoDeleteLocalAfterMs() { + return autoDeleteLocalAfterMs; + } + + public void setAutoDeleteLocalAfterMs(long autoDeleteLocalAfterMs) { + this.autoDeleteLocalAfterMs = autoDeleteLocalAfterMs; + } + + public boolean isAutoDeleteLocal() { + return autoDeleteLocalAfterMs > SyncedFolder.AUTO_DELETE_LOCAL_DISABLED; + } + public boolean isExcludeHidden() { return excludeHidden; } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/autoUpload/data/AutoUploadDisplayItemFactory.kt b/app/src/main/java/com/owncloud/android/ui/fragment/autoUpload/data/AutoUploadDisplayItemFactory.kt index 26efc5256347..de9dabe608b4 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/autoUpload/data/AutoUploadDisplayItemFactory.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/autoUpload/data/AutoUploadDisplayItemFactory.kt @@ -66,7 +66,8 @@ class AutoUploadDisplayItemFactory( false, SubFolderRule.YEAR_MONTH, false, - SyncedFolder.NOT_SCANNED_YET + SyncedFolder.NOT_SCANNED_YET, + SyncedFolder.AUTO_DELETE_LOCAL_DISABLED ) fun createEmptyCustomFolder(accountName: String): SyncedFolderDisplayItem = SyncedFolderDisplayItem( @@ -87,7 +88,8 @@ class AutoUploadDisplayItemFactory( false, SubFolderRule.YEAR_MONTH, false, - SyncedFolder.NOT_SCANNED_YET + SyncedFolder.NOT_SCANNED_YET, + SyncedFolder.AUTO_DELETE_LOCAL_DISABLED ) fun createNewCustomFolder(syncedFolder: SyncedFolderParcelable): SyncedFolderDisplayItem = SyncedFolderDisplayItem( @@ -108,7 +110,8 @@ class AutoUploadDisplayItemFactory( syncedFolder.isHidden, syncedFolder.subFolderRule, syncedFolder.isExcludeHidden, - SyncedFolder.NOT_SCANNED_YET + SyncedFolder.NOT_SCANNED_YET, + syncedFolder.autoDeleteLocalAfterMs ) fun updateAutoUploadFolderItem(item: SyncedFolderDisplayItem, syncedFolder: SyncedFolderParcelable) { @@ -125,6 +128,7 @@ class AutoUploadDisplayItemFactory( setEnabled(syncedFolder.isEnabled, clock.currentTime) setSubFolderRule(syncedFolder.subFolderRule) setExcludeHidden(syncedFolder.isExcludeHidden) + autoDeleteLocalAfterMs = syncedFolder.autoDeleteLocalAfterMs } } @@ -154,7 +158,8 @@ class AutoUploadDisplayItemFactory( syncedFolder.isHidden, syncedFolder.subfolderRule, syncedFolder.isExcludeHidden, - syncedFolder.lastScanTimestampMs + syncedFolder.lastScanTimestampMs, + syncedFolder.autoDeleteLocalAfterMs ) private fun getDisplayFilePaths(files: List): List? = diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 8641f4e9de73..d65d542bd49c 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -17,6 +17,7 @@ LOCAL_BEHAVIOUR_FORGET LOCAL_BEHAVIOUR_MOVE LOCAL_BEHAVIOUR_DELETE + LOCAL_BEHAVIOUR_DELETE_AFTER_DELAY diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 31901ff1e355..42b5331723c4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -747,6 +747,10 @@ kept in original folder moved to app folder deleted + + deleted after %d day + deleted after %d days + Data storage location Manage data storage location diff --git a/app/src/test/java/com/nextcloud/client/utils/SyncedFolderDisplayItemExtensionsTests.kt b/app/src/test/java/com/nextcloud/client/utils/SyncedFolderDisplayItemExtensionsTests.kt index 54540c769c9c..2eab3691fb7f 100644 --- a/app/src/test/java/com/nextcloud/client/utils/SyncedFolderDisplayItemExtensionsTests.kt +++ b/app/src/test/java/com/nextcloud/client/utils/SyncedFolderDisplayItemExtensionsTests.kt @@ -38,7 +38,8 @@ class SyncedFolderDisplayItemExtensionsTests { false, SubFolderRule.YEAR_MONTH, false, - SyncedFolder.NOT_SCANNED_YET + SyncedFolder.NOT_SCANNED_YET, + SyncedFolder.AUTO_DELETE_LOCAL_DISABLED ), SyncedFolderDisplayItem( 2, @@ -58,7 +59,8 @@ class SyncedFolderDisplayItemExtensionsTests { false, SubFolderRule.YEAR_MONTH, false, - SyncedFolder.NOT_SCANNED_YET + SyncedFolder.NOT_SCANNED_YET, + SyncedFolder.AUTO_DELETE_LOCAL_DISABLED ), SyncedFolderDisplayItem( 3, @@ -78,7 +80,8 @@ class SyncedFolderDisplayItemExtensionsTests { false, SubFolderRule.YEAR_MONTH, false, - SyncedFolder.NOT_SCANNED_YET + SyncedFolder.NOT_SCANNED_YET, + SyncedFolder.AUTO_DELETE_LOCAL_DISABLED ) ) diff --git a/app/src/test/java/com/nextcloud/utils/extensions/SyncedFolderDisplayItemExtensionsTest.kt b/app/src/test/java/com/nextcloud/utils/extensions/SyncedFolderDisplayItemExtensionsTest.kt index d8ce488776ac..5d451294bdcc 100644 --- a/app/src/test/java/com/nextcloud/utils/extensions/SyncedFolderDisplayItemExtensionsTest.kt +++ b/app/src/test/java/com/nextcloud/utils/extensions/SyncedFolderDisplayItemExtensionsTest.kt @@ -111,6 +111,7 @@ class SyncedFolderDisplayItemExtensionsTest { false, SubFolderRule.YEAR_MONTH, true, - SyncedFolder.NOT_SCANNED_YET + SyncedFolder.NOT_SCANNED_YET, + SyncedFolder.AUTO_DELETE_LOCAL_DISABLED ) } diff --git a/app/src/test/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperationTest.kt b/app/src/test/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperationTest.kt new file mode 100644 index 000000000000..34ca86ea60d8 --- /dev/null +++ b/app/src/test/java/com/owncloud/android/operations/upload/DeleteUploadedFileOperationTest.kt @@ -0,0 +1,50 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.operations.upload + +import android.content.Context +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.datamodel.SyncedFolder +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.mockito.kotlin.mock +import java.io.File +import java.util.concurrent.TimeUnit + +class DeleteUploadedFileOperationTest { + + private val retentionMs = TimeUnit.DAYS.toMillis(RETENTION_DAYS) + + private fun operation(minAgeMs: Long) = + DeleteUploadedFileOperation(mock(), mock(), mock(), minAgeMs) + + private fun fileModifiedDaysAgo(days: Long): File = File.createTempFile("old", null).apply { + deleteOnExit() + setLastModified(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(days)) + } + + @Test + fun fileYoungerThanRetentionIsNotOldEnough() { + assertFalse(operation(retentionMs).isOldEnough(fileModifiedDaysAgo(RETENTION_DAYS - 1))) + } + + @Test + fun fileOlderThanRetentionIsOldEnough() { + assertTrue(operation(retentionMs).isOldEnough(fileModifiedDaysAgo(RETENTION_DAYS + 1))) + } + + @Test + fun noRetentionAcceptsAnyFile() { + assertTrue(operation(SyncedFolder.AUTO_DELETE_LOCAL_DISABLED).isOldEnough(fileModifiedDaysAgo(0))) + } + + private companion object { + const val RETENTION_DAYS = 60L + } +} diff --git a/app/src/test/java/com/owncloud/android/ui/dialog/parcel/SyncedFolderParcelableTest.kt b/app/src/test/java/com/owncloud/android/ui/dialog/parcel/SyncedFolderParcelableTest.kt new file mode 100644 index 000000000000..602002c9c039 --- /dev/null +++ b/app/src/test/java/com/owncloud/android/ui/dialog/parcel/SyncedFolderParcelableTest.kt @@ -0,0 +1,43 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.ui.dialog.parcel + +import com.nextcloud.client.jobs.upload.FileUploadWorker +import com.owncloud.android.datamodel.SyncedFolder +import org.junit.Assert.assertEquals +import org.junit.Test + +class SyncedFolderParcelableTest { + + @Test + fun deleteAfterDelayKeepsFilesAndSetsDefaultRetention() { + val parcelable = SyncedFolderParcelable() + + parcelable.setUploadAction(DELETE_AFTER_DELAY) + + assertEquals(FileUploadWorker.LOCAL_BEHAVIOUR_FORGET, parcelable.uploadAction) + assertEquals(SyncedFolder.DEFAULT_AUTO_DELETE_LOCAL_AFTER_MS, parcelable.autoDeleteLocalAfterMs) + assertEquals(DELETE_AFTER_DELAY_INDEX, parcelable.uploadActionInteger) + } + + @Test + fun otherBehavioursResetRetention() { + val parcelable = SyncedFolderParcelable() + parcelable.setUploadAction(DELETE_AFTER_DELAY) + + parcelable.setUploadAction("LOCAL_BEHAVIOUR_MOVE") + + assertEquals(SyncedFolder.AUTO_DELETE_LOCAL_DISABLED, parcelable.autoDeleteLocalAfterMs) + assertEquals(FileUploadWorker.LOCAL_BEHAVIOUR_MOVE, parcelable.uploadAction) + } + + private companion object { + const val DELETE_AFTER_DELAY = "LOCAL_BEHAVIOUR_DELETE_AFTER_DELAY" + const val DELETE_AFTER_DELAY_INDEX = 3 + } +} diff --git a/app/src/test/java/com/owncloud/android/utils/AutoUploadFileMatchingTests.kt b/app/src/test/java/com/owncloud/android/utils/AutoUploadFileMatchingTests.kt index db102b1ac131..fc023cfbb8df 100644 --- a/app/src/test/java/com/owncloud/android/utils/AutoUploadFileMatchingTests.kt +++ b/app/src/test/java/com/owncloud/android/utils/AutoUploadFileMatchingTests.kt @@ -72,7 +72,8 @@ class AutoUploadFileMatchingTests { false, com.nextcloud.client.preferences.SubFolderRule.YEAR_MONTH, false, - System.currentTimeMillis() + System.currentTimeMillis(), + SyncedFolder.AUTO_DELETE_LOCAL_DISABLED ) // endregion diff --git a/app/src/test/java/com/owncloud/android/utils/AutoUploadHelperTest.kt b/app/src/test/java/com/owncloud/android/utils/AutoUploadHelperTest.kt index 9ec1652ce070..1dea5f72644e 100644 --- a/app/src/test/java/com/owncloud/android/utils/AutoUploadHelperTest.kt +++ b/app/src/test/java/com/owncloud/android/utils/AutoUploadHelperTest.kt @@ -77,7 +77,8 @@ class AutoUploadHelperTest { false, SubFolderRule.YEAR_MONTH, excludeHidden, - lastScan + lastScan, + SyncedFolder.AUTO_DELETE_LOCAL_DISABLED ) @Test