Browse Source

Report client health

Signed-off-by: tobiasKaminsky <tobias@kaminsky.me>
tobiasKaminsky 1 year ago
parent
commit
b336d01b4b
25 changed files with 1522 additions and 44 deletions
  1. 1179 0
      app/schemas/com.nextcloud.client.database.NextcloudDatabase/76.json
  2. 20 0
      app/src/androidTest/java/com/owncloud/android/datamodel/ArbitraryDataProviderIT.kt
  3. 6 1
      app/src/androidTest/java/com/owncloud/android/util/EncryptionTestIT.java
  4. 2 1
      app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt
  5. 3 0
      app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt
  6. 3 1
      app/src/main/java/com/nextcloud/client/database/entity/CapabilityEntity.kt
  7. 2 2
      app/src/main/java/com/nextcloud/client/di/AppModule.java
  8. 12 2
      app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt
  9. 2 0
      app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt
  10. 23 0
      app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt
  11. 131 0
      app/src/main/java/com/nextcloud/client/jobs/HealthStatusWork.kt
  12. 2 0
      app/src/main/java/com/owncloud/android/MainApp.java
  13. 4 0
      app/src/main/java/com/owncloud/android/datamodel/ArbitraryDataProvider.kt
  14. 11 0
      app/src/main/java/com/owncloud/android/datamodel/ArbitraryDataProviderImpl.java
  15. 14 1
      app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java
  16. 25 21
      app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.java
  17. 2 1
      app/src/main/java/com/owncloud/android/db/ProviderMeta.java
  18. 3 1
      app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java
  19. 7 1
      app/src/main/java/com/owncloud/android/operations/DownloadFileOperation.java
  20. 3 1
      app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java
  21. 2 0
      app/src/main/java/com/owncloud/android/ui/dialog/SetupEncryptionDialogFragment.kt
  22. 2 2
      app/src/main/java/com/owncloud/android/ui/fragment/GroupfolderListFragment.kt
  23. 3 1
      app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java
  24. 2 2
      app/src/main/java/com/owncloud/android/ui/fragment/SharedListFragment.kt
  25. 59 6
      app/src/main/java/com/owncloud/android/utils/EncryptionUtils.java

+ 1179 - 0
app/schemas/com.nextcloud.client.database.NextcloudDatabase/76.json

@@ -0,0 +1,1179 @@
+{
+    "formatVersion": 1,
+    "database": {
+        "version": 76,
+        "identityHash": "0d639ab041aa87e6c2ef9504395545f7",
+        "entities": [
+            {
+                "tableName": "arbitrary_data",
+                "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `cloud_id` TEXT, `key` TEXT, `value` TEXT)",
+                "fields": [
+                    {
+                        "fieldPath": "id",
+                        "columnName": "_id",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "cloudId",
+                        "columnName": "cloud_id",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "key",
+                        "columnName": "key",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "value",
+                        "columnName": "value",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    }
+                ],
+                "primaryKey": {
+                    "autoGenerate": true,
+                    "columnNames": [
+                        "_id"
+                    ]
+                },
+                "indices": [],
+                "foreignKeys": []
+            },
+            {
+                "tableName": "capabilities",
+                "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `account` TEXT, `version_mayor` INTEGER, `version_minor` INTEGER, `version_micro` INTEGER, `version_string` TEXT, `version_edition` TEXT, `extended_support` INTEGER, `core_pollinterval` INTEGER, `sharing_api_enabled` INTEGER, `sharing_public_enabled` INTEGER, `sharing_public_password_enforced` INTEGER, `sharing_public_expire_date_enabled` INTEGER, `sharing_public_expire_date_days` INTEGER, `sharing_public_expire_date_enforced` INTEGER, `sharing_public_send_mail` INTEGER, `sharing_public_upload` INTEGER, `sharing_user_send_mail` INTEGER, `sharing_resharing` INTEGER, `sharing_federation_outgoing` INTEGER, `sharing_federation_incoming` INTEGER, `files_bigfilechunking` INTEGER, `files_undelete` INTEGER, `files_versioning` INTEGER, `external_links` INTEGER, `server_name` TEXT, `server_color` TEXT, `server_text_color` TEXT, `server_element_color` TEXT, `server_slogan` TEXT, `server_logo` TEXT, `background_url` TEXT, `end_to_end_encryption` INTEGER, `end_to_end_encryption_keys_exist` INTEGER, `activity` INTEGER, `background_default` INTEGER, `background_plain` INTEGER, `richdocument` INTEGER, `richdocument_mimetype_list` TEXT, `richdocument_direct_editing` INTEGER, `richdocument_direct_templates` INTEGER, `richdocument_optional_mimetype_list` TEXT, `sharing_public_ask_for_optional_password` INTEGER, `richdocument_product_name` TEXT, `direct_editing_etag` TEXT, `user_status` INTEGER, `user_status_supports_emoji` INTEGER, `etag` TEXT, `files_locking_version` TEXT, `groupfolders` INTEGER, `drop_account` INTEGER, `security_guard` INTEGER)",
+                "fields": [
+                    {
+                        "fieldPath": "id",
+                        "columnName": "_id",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "accountName",
+                        "columnName": "account",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "versionMajor",
+                        "columnName": "version_mayor",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "versionMinor",
+                        "columnName": "version_minor",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "versionMicro",
+                        "columnName": "version_micro",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "versionString",
+                        "columnName": "version_string",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "versionEditor",
+                        "columnName": "version_edition",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "extendedSupport",
+                        "columnName": "extended_support",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "corePollinterval",
+                        "columnName": "core_pollinterval",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "sharingApiEnabled",
+                        "columnName": "sharing_api_enabled",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "sharingPublicEnabled",
+                        "columnName": "sharing_public_enabled",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "sharingPublicPasswordEnforced",
+                        "columnName": "sharing_public_password_enforced",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "sharingPublicExpireDateEnabled",
+                        "columnName": "sharing_public_expire_date_enabled",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "sharingPublicExpireDateDays",
+                        "columnName": "sharing_public_expire_date_days",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "sharingPublicExpireDateEnforced",
+                        "columnName": "sharing_public_expire_date_enforced",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "sharingPublicSendMail",
+                        "columnName": "sharing_public_send_mail",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "sharingPublicUpload",
+                        "columnName": "sharing_public_upload",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "sharingUserSendMail",
+                        "columnName": "sharing_user_send_mail",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "sharingResharing",
+                        "columnName": "sharing_resharing",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "sharingFederationOutgoing",
+                        "columnName": "sharing_federation_outgoing",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "sharingFederationIncoming",
+                        "columnName": "sharing_federation_incoming",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "filesBigfilechunking",
+                        "columnName": "files_bigfilechunking",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "filesUndelete",
+                        "columnName": "files_undelete",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "filesVersioning",
+                        "columnName": "files_versioning",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "externalLinks",
+                        "columnName": "external_links",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "serverName",
+                        "columnName": "server_name",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "serverColor",
+                        "columnName": "server_color",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "serverTextColor",
+                        "columnName": "server_text_color",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "serverElementColor",
+                        "columnName": "server_element_color",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "serverSlogan",
+                        "columnName": "server_slogan",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "serverLogo",
+                        "columnName": "server_logo",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "serverBackgroundUrl",
+                        "columnName": "background_url",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "endToEndEncryption",
+                        "columnName": "end_to_end_encryption",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "endToEndEncryptionKeysExist",
+                        "columnName": "end_to_end_encryption_keys_exist",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "activity",
+                        "columnName": "activity",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "serverBackgroundDefault",
+                        "columnName": "background_default",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "serverBackgroundPlain",
+                        "columnName": "background_plain",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "richdocument",
+                        "columnName": "richdocument",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "richdocumentMimetypeList",
+                        "columnName": "richdocument_mimetype_list",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "richdocumentDirectEditing",
+                        "columnName": "richdocument_direct_editing",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "richdocumentTemplates",
+                        "columnName": "richdocument_direct_templates",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "richdocumentOptionalMimetypeList",
+                        "columnName": "richdocument_optional_mimetype_list",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "sharingPublicAskForOptionalPassword",
+                        "columnName": "sharing_public_ask_for_optional_password",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "richdocumentProductName",
+                        "columnName": "richdocument_product_name",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "directEditingEtag",
+                        "columnName": "direct_editing_etag",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "userStatus",
+                        "columnName": "user_status",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "userStatusSupportsEmoji",
+                        "columnName": "user_status_supports_emoji",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "etag",
+                        "columnName": "etag",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "filesLockingVersion",
+                        "columnName": "files_locking_version",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "groupfolders",
+                        "columnName": "groupfolders",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "dropAccount",
+                        "columnName": "drop_account",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "securityGuard",
+                        "columnName": "security_guard",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    }
+                ],
+                "primaryKey": {
+                    "autoGenerate": true,
+                    "columnNames": [
+                        "_id"
+                    ]
+                },
+                "indices": [],
+                "foreignKeys": []
+            },
+            {
+                "tableName": "external_links",
+                "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `icon_url` TEXT, `language` TEXT, `type` INTEGER, `name` TEXT, `url` TEXT, `redirect` INTEGER)",
+                "fields": [
+                    {
+                        "fieldPath": "id",
+                        "columnName": "_id",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "iconUrl",
+                        "columnName": "icon_url",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "language",
+                        "columnName": "language",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "type",
+                        "columnName": "type",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "name",
+                        "columnName": "name",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "url",
+                        "columnName": "url",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "redirect",
+                        "columnName": "redirect",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    }
+                ],
+                "primaryKey": {
+                    "autoGenerate": true,
+                    "columnNames": [
+                        "_id"
+                    ]
+                },
+                "indices": [],
+                "foreignKeys": []
+            },
+            {
+                "tableName": "filelist",
+                "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `filename` TEXT, `encrypted_filename` TEXT, `path` TEXT, `path_decrypted` TEXT, `parent` INTEGER, `created` INTEGER, `modified` INTEGER, `content_type` TEXT, `content_length` INTEGER, `media_path` TEXT, `file_owner` TEXT, `last_sync_date` INTEGER, `last_sync_date_for_data` INTEGER, `modified_at_last_sync_for_data` INTEGER, `etag` TEXT, `etag_on_server` TEXT, `share_by_link` INTEGER, `permissions` TEXT, `remote_id` TEXT, `local_id` INTEGER NOT NULL DEFAULT -1, `update_thumbnail` INTEGER, `is_downloading` INTEGER, `favorite` INTEGER, `hidden` INTEGER, `is_encrypted` INTEGER, `etag_in_conflict` TEXT, `shared_via_users` INTEGER, `mount_type` INTEGER, `has_preview` INTEGER, `unread_comments_count` INTEGER, `owner_id` TEXT, `owner_display_name` TEXT, `note` TEXT, `sharees` TEXT, `rich_workspace` TEXT, `metadata_size` TEXT, `metadata_live_photo` TEXT, `locked` INTEGER, `lock_type` INTEGER, `lock_owner` TEXT, `lock_owner_display_name` TEXT, `lock_owner_editor` TEXT, `lock_timestamp` INTEGER, `lock_timeout` INTEGER, `lock_token` TEXT, `tags` TEXT, `metadata_gps` TEXT)",
+                "fields": [
+                    {
+                        "fieldPath": "id",
+                        "columnName": "_id",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "name",
+                        "columnName": "filename",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "encryptedName",
+                        "columnName": "encrypted_filename",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "path",
+                        "columnName": "path",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "pathDecrypted",
+                        "columnName": "path_decrypted",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "parent",
+                        "columnName": "parent",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "creation",
+                        "columnName": "created",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "modified",
+                        "columnName": "modified",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "contentType",
+                        "columnName": "content_type",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "contentLength",
+                        "columnName": "content_length",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "storagePath",
+                        "columnName": "media_path",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "accountOwner",
+                        "columnName": "file_owner",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "lastSyncDate",
+                        "columnName": "last_sync_date",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "lastSyncDateForData",
+                        "columnName": "last_sync_date_for_data",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "modifiedAtLastSyncForData",
+                        "columnName": "modified_at_last_sync_for_data",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "etag",
+                        "columnName": "etag",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "etagOnServer",
+                        "columnName": "etag_on_server",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "sharedViaLink",
+                        "columnName": "share_by_link",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "permissions",
+                        "columnName": "permissions",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "remoteId",
+                        "columnName": "remote_id",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "localId",
+                        "columnName": "local_id",
+                        "affinity": "INTEGER",
+                        "notNull": true,
+                        "defaultValue": "-1"
+                    },
+                    {
+                        "fieldPath": "updateThumbnail",
+                        "columnName": "update_thumbnail",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "isDownloading",
+                        "columnName": "is_downloading",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "favorite",
+                        "columnName": "favorite",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "hidden",
+                        "columnName": "hidden",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "isEncrypted",
+                        "columnName": "is_encrypted",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "etagInConflict",
+                        "columnName": "etag_in_conflict",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "sharedWithSharee",
+                        "columnName": "shared_via_users",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "mountType",
+                        "columnName": "mount_type",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "hasPreview",
+                        "columnName": "has_preview",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "unreadCommentsCount",
+                        "columnName": "unread_comments_count",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "ownerId",
+                        "columnName": "owner_id",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "ownerDisplayName",
+                        "columnName": "owner_display_name",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "note",
+                        "columnName": "note",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "sharees",
+                        "columnName": "sharees",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "richWorkspace",
+                        "columnName": "rich_workspace",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "metadataSize",
+                        "columnName": "metadata_size",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "metadataLivePhoto",
+                        "columnName": "metadata_live_photo",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "locked",
+                        "columnName": "locked",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "lockType",
+                        "columnName": "lock_type",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "lockOwner",
+                        "columnName": "lock_owner",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "lockOwnerDisplayName",
+                        "columnName": "lock_owner_display_name",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "lockOwnerEditor",
+                        "columnName": "lock_owner_editor",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "lockTimestamp",
+                        "columnName": "lock_timestamp",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "lockTimeout",
+                        "columnName": "lock_timeout",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "lockToken",
+                        "columnName": "lock_token",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "tags",
+                        "columnName": "tags",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "metadataGPS",
+                        "columnName": "metadata_gps",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    }
+                ],
+                "primaryKey": {
+                    "autoGenerate": true,
+                    "columnNames": [
+                        "_id"
+                    ]
+                },
+                "indices": [],
+                "foreignKeys": []
+            },
+            {
+                "tableName": "filesystem",
+                "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `is_folder` INTEGER, `found_at` INTEGER, `upload_triggered` INTEGER, `syncedfolder_id` TEXT, `crc32` TEXT, `modified_at` INTEGER)",
+                "fields": [
+                    {
+                        "fieldPath": "id",
+                        "columnName": "_id",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "localPath",
+                        "columnName": "local_path",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "fileIsFolder",
+                        "columnName": "is_folder",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "fileFoundRecently",
+                        "columnName": "found_at",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "fileSentForUpload",
+                        "columnName": "upload_triggered",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "syncedFolderId",
+                        "columnName": "syncedfolder_id",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "crc32",
+                        "columnName": "crc32",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "fileModified",
+                        "columnName": "modified_at",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    }
+                ],
+                "primaryKey": {
+                    "autoGenerate": true,
+                    "columnNames": [
+                        "_id"
+                    ]
+                },
+                "indices": [],
+                "foreignKeys": []
+            },
+            {
+                "tableName": "ocshares",
+                "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `file_source` INTEGER, `item_source` INTEGER, `share_type` INTEGER, `shate_with` TEXT, `path` TEXT, `permissions` INTEGER, `shared_date` INTEGER, `expiration_date` INTEGER, `token` TEXT, `shared_with_display_name` TEXT, `is_directory` INTEGER, `user_id` INTEGER, `id_remote_shared` INTEGER, `owner_share` TEXT, `is_password_protected` INTEGER, `note` TEXT, `hide_download` INTEGER, `share_link` TEXT, `share_label` TEXT)",
+                "fields": [
+                    {
+                        "fieldPath": "id",
+                        "columnName": "_id",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "fileSource",
+                        "columnName": "file_source",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "itemSource",
+                        "columnName": "item_source",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "shareType",
+                        "columnName": "share_type",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "shareWith",
+                        "columnName": "shate_with",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "path",
+                        "columnName": "path",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "permissions",
+                        "columnName": "permissions",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "sharedDate",
+                        "columnName": "shared_date",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "expirationDate",
+                        "columnName": "expiration_date",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "token",
+                        "columnName": "token",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "shareWithDisplayName",
+                        "columnName": "shared_with_display_name",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "isDirectory",
+                        "columnName": "is_directory",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "userId",
+                        "columnName": "user_id",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "idRemoteShared",
+                        "columnName": "id_remote_shared",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "accountOwner",
+                        "columnName": "owner_share",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "isPasswordProtected",
+                        "columnName": "is_password_protected",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "note",
+                        "columnName": "note",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "hideDownload",
+                        "columnName": "hide_download",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "shareLink",
+                        "columnName": "share_link",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "shareLabel",
+                        "columnName": "share_label",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    }
+                ],
+                "primaryKey": {
+                    "autoGenerate": true,
+                    "columnNames": [
+                        "_id"
+                    ]
+                },
+                "indices": [],
+                "foreignKeys": []
+            },
+            {
+                "tableName": "synced_folders",
+                "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `wifi_only` INTEGER, `charging_only` INTEGER, `existing` INTEGER, `enabled` INTEGER, `enabled_timestamp_ms` INTEGER, `subfolder_by_date` INTEGER, `account` TEXT, `upload_option` INTEGER, `name_collision_policy` INTEGER, `type` INTEGER, `hidden` INTEGER, `sub_folder_rule` INTEGER)",
+                "fields": [
+                    {
+                        "fieldPath": "id",
+                        "columnName": "_id",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "localPath",
+                        "columnName": "local_path",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "remotePath",
+                        "columnName": "remote_path",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "wifiOnly",
+                        "columnName": "wifi_only",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "chargingOnly",
+                        "columnName": "charging_only",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "existing",
+                        "columnName": "existing",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "enabled",
+                        "columnName": "enabled",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "enabledTimestampMs",
+                        "columnName": "enabled_timestamp_ms",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "subfolderByDate",
+                        "columnName": "subfolder_by_date",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "account",
+                        "columnName": "account",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "uploadAction",
+                        "columnName": "upload_option",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "nameCollisionPolicy",
+                        "columnName": "name_collision_policy",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "type",
+                        "columnName": "type",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "hidden",
+                        "columnName": "hidden",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "subFolderRule",
+                        "columnName": "sub_folder_rule",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    }
+                ],
+                "primaryKey": {
+                    "autoGenerate": true,
+                    "columnNames": [
+                        "_id"
+                    ]
+                },
+                "indices": [],
+                "foreignKeys": []
+            },
+            {
+                "tableName": "list_of_uploads",
+                "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `account_name` TEXT, `file_size` INTEGER, `status` INTEGER, `local_behaviour` INTEGER, `upload_time` INTEGER, `name_collision_policy` INTEGER, `is_create_remote_folder` INTEGER, `upload_end_timestamp` INTEGER, `last_result` INTEGER, `is_while_charging_only` INTEGER, `is_wifi_only` INTEGER, `created_by` INTEGER, `folder_unlock_token` TEXT)",
+                "fields": [
+                    {
+                        "fieldPath": "id",
+                        "columnName": "_id",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "localPath",
+                        "columnName": "local_path",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "remotePath",
+                        "columnName": "remote_path",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "accountName",
+                        "columnName": "account_name",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "fileSize",
+                        "columnName": "file_size",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "status",
+                        "columnName": "status",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "localBehaviour",
+                        "columnName": "local_behaviour",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "uploadTime",
+                        "columnName": "upload_time",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "nameCollisionPolicy",
+                        "columnName": "name_collision_policy",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "isCreateRemoteFolder",
+                        "columnName": "is_create_remote_folder",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "uploadEndTimestamp",
+                        "columnName": "upload_end_timestamp",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "lastResult",
+                        "columnName": "last_result",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "isWhileChargingOnly",
+                        "columnName": "is_while_charging_only",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "isWifiOnly",
+                        "columnName": "is_wifi_only",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "createdBy",
+                        "columnName": "created_by",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "folderUnlockToken",
+                        "columnName": "folder_unlock_token",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    }
+                ],
+                "primaryKey": {
+                    "autoGenerate": true,
+                    "columnNames": [
+                        "_id"
+                    ]
+                },
+                "indices": [],
+                "foreignKeys": []
+            },
+            {
+                "tableName": "virtual",
+                "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `type` TEXT, `ocfile_id` INTEGER)",
+                "fields": [
+                    {
+                        "fieldPath": "id",
+                        "columnName": "_id",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "type",
+                        "columnName": "type",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "ocFileId",
+                        "columnName": "ocfile_id",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    }
+                ],
+                "primaryKey": {
+                    "autoGenerate": true,
+                    "columnNames": [
+                        "_id"
+                    ]
+                },
+                "indices": [],
+                "foreignKeys": []
+            }
+        ],
+        "views": [],
+        "setupQueries": [
+            "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+            "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0d639ab041aa87e6c2ef9504395545f7')"
+        ]
+    }
+}

+ 20 - 0
app/src/androidTest/java/com/owncloud/android/datamodel/ArbitraryDataProviderIT.kt

@@ -75,4 +75,24 @@ class ArbitraryDataProviderIT : AbstractIT() {
         arbitraryDataProvider.storeOrUpdateKeyValue(user.accountName, key, value.toString())
         assertEquals(value, arbitraryDataProvider.getIntegerValue(user.accountName, key))
     }
+
+    @Test
+    fun testIncrement() {
+        val key = "INCREMENT"
+
+        // key does not exist
+        assertEquals(-1, arbitraryDataProvider.getIntegerValue(user.accountName, key))
+
+        // increment -> 1
+        arbitraryDataProvider.incrementValue(user.accountName, key)
+        assertEquals(1, arbitraryDataProvider.getIntegerValue(user.accountName, key))
+
+        // increment -> 2
+        arbitraryDataProvider.incrementValue(user.accountName, key)
+        assertEquals(2, arbitraryDataProvider.getIntegerValue(user.accountName, key))
+
+        // delete
+        arbitraryDataProvider.deleteKeyForAccount(user.accountName, key)
+        assertEquals(-1, arbitraryDataProvider.getIntegerValue(user.accountName, key))
+    }
 }

+ 6 - 1
app/src/androidTest/java/com/owncloud/android/util/EncryptionTestIT.java

@@ -765,7 +765,12 @@ public class EncryptionTestIT extends AbstractIT {
         // verify authentication tag
         assertTrue(Arrays.equals(expectedAuthTag, authenticationTag));
 
-        byte[] decryptedBytes = decryptFile(encryptedTempFile, key, iv, authenticationTag);
+        byte[] decryptedBytes = decryptFile(encryptedTempFile,
+                                            key,
+                                            iv,
+                                            authenticationTag,
+                                            new ArbitraryDataProviderImpl(targetContext),
+                                            user);
 
         File decryptedFile = File.createTempFile("file", "dec");
         FileOutputStream fileOutputStream1 = new FileOutputStream(decryptedFile);

+ 2 - 1
app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt

@@ -68,7 +68,8 @@ import com.owncloud.android.db.ProviderMeta
         AutoMigration(from = 71, to = 72),
         AutoMigration(from = 72, to = 73),
         AutoMigration(from = 73, to = 74, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class),
-        AutoMigration(from = 74, to = 75)
+        AutoMigration(from = 74, to = 75),
+        AutoMigration(from = 75, to = 76)
     ],
     exportSchema = true
 )

+ 3 - 0
app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt

@@ -61,4 +61,7 @@ interface FileDao {
 
     @Query("SELECT * FROM filelist WHERE path LIKE :pathPattern AND file_owner = :fileOwner ORDER BY path ASC")
     fun getFolderWithDescendants(pathPattern: String, fileOwner: String): List<FileEntity>
+
+    @Query("SELECT * FROM filelist where file_owner = :fileOwner AND etag_in_conflict IS NOT NULL")
+    fun getFilesWithSyncConflict(fileOwner: String): List<FileEntity>
 }

+ 3 - 1
app/src/main/java/com/nextcloud/client/database/entity/CapabilityEntity.kt

@@ -131,5 +131,7 @@ data class CapabilityEntity(
     @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_GROUPFOLDERS)
     val groupfolders: Int?,
     @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_DROP_ACCOUNT)
-    val dropAccount: Int?
+    val dropAccount: Int?,
+    @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_SECURITY_GUARD)
+    val securityGuard: Int?
 )

+ 2 - 2
app/src/main/java/com/nextcloud/client/di/AppModule.java

@@ -145,8 +145,8 @@ class AppModule {
     }
 
     @Provides
-    UploadsStorageManager uploadsStorageManager(Context context,
-                                                CurrentAccountProvider currentAccountProvider) {
+    UploadsStorageManager uploadsStorageManager(CurrentAccountProvider currentAccountProvider,
+                                                Context context) {
         return new UploadsStorageManager(currentAccountProvider, context.getContentResolver());
     }
 

+ 12 - 2
app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt

@@ -62,7 +62,7 @@ class BackgroundJobFactory @Inject constructor(
     private val deviceInfo: DeviceInfo,
     private val accountManager: UserAccountManager,
     private val resources: Resources,
-    private val dataProvider: ArbitraryDataProvider,
+    private val arbitraryDataProvider: ArbitraryDataProvider,
     private val uploadsStorageManager: UploadsStorageManager,
     private val connectivityService: ConnectivityService,
     private val notificationManager: NotificationManager,
@@ -103,6 +103,7 @@ class BackgroundJobFactory @Inject constructor(
                 FilesExportWork::class -> createFilesExportWork(context, workerParameters)
                 FilesUploadWorker::class -> createFilesUploadWorker(context, workerParameters)
                 GeneratePdfFromImagesWork::class -> createPDFGenerateWork(context, workerParameters)
+                HealthStatusWork::class -> createHealthStatusWork(context, workerParameters)
                 else -> null // caller falls back to default factory
             }
         }
@@ -139,7 +140,7 @@ class BackgroundJobFactory @Inject constructor(
             context,
             params,
             resources,
-            dataProvider,
+            arbitraryDataProvider,
             contentResolver,
             accountManager
         )
@@ -260,4 +261,13 @@ class BackgroundJobFactory @Inject constructor(
             params = params
         )
     }
+
+    private fun createHealthStatusWork(context: Context, params: WorkerParameters): HealthStatusWork {
+        return HealthStatusWork(
+            context,
+            params,
+            accountManager,
+            arbitraryDataProvider
+        )
+    }
 }

+ 2 - 0
app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt

@@ -147,4 +147,6 @@ interface BackgroundJobManager {
 
     fun pruneJobs()
     fun cancelAllJobs()
+    fun schedulePeriodicHealthStatus()
+    fun startHealthStatus()
 }

+ 23 - 0
app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt

@@ -82,6 +82,8 @@ internal class BackgroundJobManagerImpl(
         const val JOB_PDF_GENERATION = "pdf_generation"
         const val JOB_IMMEDIATE_CALENDAR_BACKUP = "immediate_calendar_backup"
         const val JOB_IMMEDIATE_FILES_EXPORT = "immediate_files_export"
+        const val JOB_PERIODIC_HEALTH_STATUS = "periodic_health_status"
+        const val JOB_IMMEDIATE_HEALTH_STATUS = "immediate_health_status"
 
         const val JOB_TEST = "test_job"
 
@@ -507,4 +509,25 @@ internal class BackgroundJobManagerImpl(
     override fun cancelAllJobs() {
         workManager.cancelAllWorkByTag(TAG_ALL)
     }
+
+    override fun schedulePeriodicHealthStatus() {
+        val request = periodicRequestBuilder(
+            jobClass = HealthStatusWork::class,
+            jobName = JOB_PERIODIC_HEALTH_STATUS,
+            intervalMins = PERIODIC_BACKUP_INTERVAL_MINUTES
+        ).build()
+
+        workManager.enqueueUniquePeriodicWork(JOB_PERIODIC_HEALTH_STATUS, ExistingPeriodicWorkPolicy.KEEP, request)
+    }
+
+    override fun startHealthStatus() {
+        val request = oneTimeRequestBuilder(HealthStatusWork::class, JOB_IMMEDIATE_HEALTH_STATUS)
+            .build()
+
+        workManager.enqueueUniqueWork(
+            JOB_IMMEDIATE_HEALTH_STATUS,
+            ExistingWorkPolicy.KEEP,
+            request
+        )
+    }
 }

+ 131 - 0
app/src/main/java/com/nextcloud/client/jobs/HealthStatusWork.kt

@@ -0,0 +1,131 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2023 Tobias Kaminsky
+ * Copyright (C) 2023 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.client.jobs
+
+import android.content.Context
+import androidx.work.Worker
+import androidx.work.WorkerParameters
+import com.nextcloud.client.account.User
+import com.nextcloud.client.account.UserAccountManager
+import com.owncloud.android.datamodel.ArbitraryDataProvider
+import com.owncloud.android.datamodel.FileDataStorageManager
+import com.owncloud.android.datamodel.UploadsStorageManager
+import com.owncloud.android.db.UploadResult
+import com.owncloud.android.lib.common.OwnCloudClientManagerFactory
+import com.owncloud.android.lib.common.utils.Log_OC
+import com.owncloud.android.lib.resources.status.Problem
+import com.owncloud.android.lib.resources.status.SendClientDiagnosticRemoteOperation
+import com.owncloud.android.utils.EncryptionUtils
+import com.owncloud.android.utils.theme.CapabilityUtils
+
+class HealthStatusWork(
+    private val context: Context,
+    params: WorkerParameters,
+    private val userAccountManager: UserAccountManager,
+    private val arbitraryDataProvider: ArbitraryDataProvider
+) : Worker(context, params) {
+    override fun doWork(): Result {
+        for (user in userAccountManager.allUsers) {
+            // only if security guard is enabled
+            if (!CapabilityUtils.getCapability(user, context).securityGuard.isTrue) {
+                continue
+            }
+
+            val syncConflicts = collectSyncConflicts(user)
+
+            val problems = mutableListOf<Problem>().apply {
+                addAll(
+                    collectUploadProblems(
+                        user,
+                        listOf(
+                            UploadResult.CREDENTIAL_ERROR,
+                            UploadResult.CANNOT_CREATE_FILE,
+                            UploadResult.FOLDER_ERROR,
+                            UploadResult.SERVICE_INTERRUPTED
+                        )
+                    )
+                )
+            }
+
+            val virusDetected = collectUploadProblems(user, listOf(UploadResult.VIRUS_DETECTED)).firstOrNull()
+
+            val e2eErrors = EncryptionUtils.readE2eError(arbitraryDataProvider, user)
+
+            val nextcloudClient = OwnCloudClientManagerFactory.getDefaultSingleton()
+                .getNextcloudClientFor(user.toOwnCloudAccount(), context)
+            val result =
+                SendClientDiagnosticRemoteOperation(
+                    syncConflicts,
+                    problems,
+                    virusDetected,
+                    e2eErrors
+                ).execute(
+                    nextcloudClient
+                )
+
+            if (!result.isSuccess) {
+                if (result.exception == null) {
+                    Log_OC.e(TAG, "Update client health NOT successful!")
+                } else {
+                    Log_OC.e(TAG, "Update client health NOT successful!", result.exception)
+                }
+            }
+        }
+
+        return Result.success()
+    }
+
+    private fun collectSyncConflicts(user: User): Problem? {
+        val fileDataStorageManager = FileDataStorageManager(user, context.contentResolver)
+
+        val conflicts = fileDataStorageManager.getFilesWithSyncConflict(user)
+
+        return if (conflicts.isEmpty()) {
+            null
+        } else {
+            Problem("sync_conflicts", conflicts.size, conflicts.minOf { it.lastSyncDateForData })
+        }
+    }
+
+    private fun collectUploadProblems(user: User, errorCodes: List<UploadResult>): List<Problem> {
+        val uploadsStorageManager = UploadsStorageManager(userAccountManager, context.contentResolver)
+
+        val problems = uploadsStorageManager
+            .getUploadsForAccount(user.accountName)
+            .filter {
+                errorCodes.contains(it.lastResult)
+            }.groupBy { it.lastResult }
+
+        return if (problems.isEmpty()) {
+            emptyList()
+        } else {
+            return problems.map { problem ->
+                Problem(problem.key.toString(), problem.value.size, problem.value.minOf { it.uploadEndTimestamp })
+            }
+        }
+    }
+
+    companion object {
+        private const val TAG = "Health Status"
+    }
+}

+ 2 - 0
app/src/main/java/com/owncloud/android/MainApp.java

@@ -349,6 +349,8 @@ public class MainApp extends MultiDexApplication implements HasAndroidInjector {
         backgroundJobManager.scheduleMediaFoldersDetectionJob();
         backgroundJobManager.startMediaFoldersDetectionJob();
 
+        backgroundJobManager.schedulePeriodicHealthStatus();
+
         registerGlobalPassCodeProtection();
     }
 

+ 4 - 0
app/src/main/java/com/owncloud/android/datamodel/ArbitraryDataProvider.kt

@@ -28,6 +28,8 @@ interface ArbitraryDataProvider {
     fun deleteKeyForAccount(account: String, key: String)
 
     fun storeOrUpdateKeyValue(accountName: String, key: String, newValue: Long)
+
+    fun incrementValue(accountName: String, key: String)
     fun storeOrUpdateKeyValue(accountName: String, key: String, newValue: Boolean)
     fun storeOrUpdateKeyValue(accountName: String, key: String, newValue: String)
 
@@ -43,5 +45,7 @@ interface ArbitraryDataProvider {
         const val DIRECT_EDITING = "DIRECT_EDITING"
         const val DIRECT_EDITING_ETAG = "DIRECT_EDITING_ETAG"
         const val PREDEFINED_STATUS = "PREDEFINED_STATUS"
+        const val E2E_ERRORS = "E2E_ERRORS"
+        const val E2E_ERRORS_TIMESTAMP = "E2E_ERRORS_TIMESTAMP"
     }
 }

+ 11 - 0
app/src/main/java/com/owncloud/android/datamodel/ArbitraryDataProviderImpl.java

@@ -63,6 +63,17 @@ public class ArbitraryDataProviderImpl implements ArbitraryDataProvider {
         storeOrUpdateKeyValue(accountName, key, String.valueOf(newValue));
     }
 
+    @Override
+    public void incrementValue(@NonNull String accountName, @NonNull String key) {
+        int oldValue = getIntegerValue(accountName, key);
+
+        int value = 1;
+        if (oldValue > 0) {
+            value = oldValue + 1;
+        }
+        storeOrUpdateKeyValue(accountName, key, value);
+    }
+
     @Override
     public void storeOrUpdateKeyValue(@NonNull final String accountName, @NonNull final String key, final boolean newValue) {
         storeOrUpdateKeyValue(accountName, key, String.valueOf(newValue));

+ 14 - 1
app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java

@@ -1954,6 +1954,7 @@ public class FileDataStorageManager {
                           capability.getFilesLockingVersion());
         contentValues.put(ProviderTableMeta.CAPABILITIES_GROUPFOLDERS, capability.getGroupfolders().getValue());
         contentValues.put(ProviderTableMeta.CAPABILITIES_DROP_ACCOUNT, capability.getDropAccount().getValue());
+        contentValues.put(ProviderTableMeta.CAPABILITIES_SECURITY_GUARD, capability.getSecurityGuard().getValue());
 
         return contentValues;
     }
@@ -2111,6 +2112,7 @@ public class FileDataStorageManager {
                 getString(cursor, ProviderTableMeta.CAPABILITIES_FILES_LOCKING_VERSION));
             capability.setGroupfolders(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_GROUPFOLDERS));
             capability.setDropAccount(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_DROP_ACCOUNT));
+            capability.setSecurityGuard(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_SECURITY_GUARD));
         }
         return capability;
     }
@@ -2287,7 +2289,18 @@ public class FileDataStorageManager {
         return user;
     }
 
-    public OCFile getDefaultRootPath(){
+    public OCFile getDefaultRootPath() {
         return new OCFile(OCFile.ROOT_PATH);
     }
+
+    public List<OCFile> getFilesWithSyncConflict(User user) {
+        List<FileEntity> fileEntities = fileDao.getFilesWithSyncConflict(user.getAccountName());
+        List<OCFile> files = new ArrayList<>(fileEntities.size());
+
+        for (FileEntity fileEntity : fileEntities) {
+            files.add(createFileInstance(fileEntity));
+        }
+
+        return files;
+    }
 }

+ 25 - 21
app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.java

@@ -567,6 +567,10 @@ public class UploadsStorageManager extends Observable {
             , String.valueOf(UploadStatus.UPLOAD_FAILED.value));
     }
 
+    public OCUpload[] getUploadsForAccount(final @NonNull String accountName) {
+        return getUploads(ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "== ?", accountName);
+    }
+
     public OCUpload[] getFinishedUploadsForCurrentAccount() {
         User user = currentAccountProvider.getUser();
 
@@ -586,14 +590,14 @@ public class UploadsStorageManager extends Observable {
         User user = currentAccountProvider.getUser();
 
         return getUploads(ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_FAILED.value +
-                        AND + ProviderTableMeta.UPLOADS_LAST_RESULT +
-                        "<>" + UploadResult.DELAYED_FOR_WIFI.getValue() +
-                        AND + ProviderTableMeta.UPLOADS_LAST_RESULT +
-                        "<>" + UploadResult.LOCK_FAILED.getValue() +
-                        AND + ProviderTableMeta.UPLOADS_LAST_RESULT +
-                        "<>" + UploadResult.DELAYED_FOR_CHARGING.getValue() +
-                        AND + ProviderTableMeta.UPLOADS_LAST_RESULT +
-                        "<>" + UploadResult.DELAYED_IN_POWER_SAVE_MODE.getValue() +
+                              AND + ProviderTableMeta.UPLOADS_LAST_RESULT +
+                              "<>" + UploadResult.DELAYED_FOR_WIFI.getValue() +
+                              AND + ProviderTableMeta.UPLOADS_LAST_RESULT +
+                              "<>" + UploadResult.LOCK_FAILED.getValue() +
+                              AND + ProviderTableMeta.UPLOADS_LAST_RESULT +
+                              "<>" + UploadResult.DELAYED_FOR_CHARGING.getValue() +
+                              AND + ProviderTableMeta.UPLOADS_LAST_RESULT +
+                              "<>" + UploadResult.DELAYED_IN_POWER_SAVE_MODE.getValue() +
                         AND + ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "== ?",
                         user.getAccountName());
     }
@@ -624,15 +628,15 @@ public class UploadsStorageManager extends Observable {
     public long clearFailedButNotDelayedUploads() {
         User user = currentAccountProvider.getUser();
         final long deleted = getDB().delete(
-                ProviderTableMeta.CONTENT_URI_UPLOADS,
-                ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_FAILED.value +
-                        AND + ProviderTableMeta.UPLOADS_LAST_RESULT +
-                        "<>" + UploadResult.LOCK_FAILED.getValue() +
-                        AND + ProviderTableMeta.UPLOADS_LAST_RESULT +
-                        "<>" + UploadResult.DELAYED_FOR_WIFI.getValue() +
-                        AND + ProviderTableMeta.UPLOADS_LAST_RESULT +
-                        "<>" + UploadResult.DELAYED_FOR_CHARGING.getValue() +
-                        AND + ProviderTableMeta.UPLOADS_LAST_RESULT +
+            ProviderTableMeta.CONTENT_URI_UPLOADS,
+            ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_FAILED.value +
+                AND + ProviderTableMeta.UPLOADS_LAST_RESULT +
+                "<>" + UploadResult.LOCK_FAILED.getValue() +
+                AND + ProviderTableMeta.UPLOADS_LAST_RESULT +
+                "<>" + UploadResult.DELAYED_FOR_WIFI.getValue() +
+                AND + ProviderTableMeta.UPLOADS_LAST_RESULT +
+                "<>" + UploadResult.DELAYED_FOR_CHARGING.getValue() +
+                AND + ProviderTableMeta.UPLOADS_LAST_RESULT +
                         "<>" + UploadResult.DELAYED_IN_POWER_SAVE_MODE.getValue() +
                         AND + ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "== ?",
                 new String[]{user.getAccountName()}
@@ -647,10 +651,10 @@ public class UploadsStorageManager extends Observable {
     public long clearSuccessfulUploads() {
         User user = currentAccountProvider.getUser();
         final long deleted = getDB().delete(
-                ProviderTableMeta.CONTENT_URI_UPLOADS,
-                ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_SUCCEEDED.value + AND +
-                        ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "== ?", new String[]{user.getAccountName()}
-        );
+            ProviderTableMeta.CONTENT_URI_UPLOADS,
+            ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_SUCCEEDED.value + AND +
+                ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "== ?", new String[]{user.getAccountName()}
+                                           );
 
         Log_OC.d(TAG, "delete all successful uploads");
         if (deleted > 0) {

+ 2 - 1
app/src/main/java/com/owncloud/android/db/ProviderMeta.java

@@ -35,7 +35,7 @@ import java.util.List;
  */
 public class ProviderMeta {
     public static final String DB_NAME = "filelist";
-    public static final int DB_VERSION = 75;
+    public static final int DB_VERSION = 76;
 
     private ProviderMeta() {
         // No instance
@@ -264,6 +264,7 @@ public class ProviderMeta {
         public static final String CAPABILITIES_USER_STATUS_SUPPORTS_EMOJI = "user_status_supports_emoji";
         public static final String CAPABILITIES_GROUPFOLDERS = "groupfolders";
         public static final String CAPABILITIES_DROP_ACCOUNT = "drop_account";
+        public static final String CAPABILITIES_SECURITY_GUARD = "security_guard";
 
         //Columns of Uploads table
         public static final String UPLOADS_LOCAL_PATH = "local_path";

+ 3 - 1
app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java

@@ -163,7 +163,9 @@ public class CreateFolderOperation extends SyncOperation implements OnRemoteOper
                                                serializedFolderMetadata,
                                                token,
                                                client,
-                                               metadataExists);
+                                               metadataExists,
+                                               arbitraryDataProvider,
+                                               user);
 
                 // unlock folder
                 if (token != null) {

+ 7 - 1
app/src/main/java/com/owncloud/android/operations/DownloadFileOperation.java

@@ -26,6 +26,7 @@ import android.text.TextUtils;
 import android.webkit.MimeTypeMap;
 
 import com.nextcloud.client.account.User;
+import com.owncloud.android.datamodel.ArbitraryDataProviderImpl;
 import com.owncloud.android.datamodel.DecryptedFolderMetadata;
 import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
@@ -213,7 +214,12 @@ public class DownloadFileOperation extends RemoteOperation {
                         .get(file.getEncryptedFileName()).getAuthenticationTag());
 
                 try {
-                    byte[] decryptedBytes = EncryptionUtils.decryptFile(tmpFile, key, iv, authenticationTag);
+                    byte[] decryptedBytes = EncryptionUtils.decryptFile(tmpFile,
+                                                                        key,
+                                                                        iv,
+                                                                        authenticationTag,
+                                                                        new ArbitraryDataProviderImpl(context),
+                                                                        user);
 
                     try (FileOutputStream fileOutputStream = new FileOutputStream(tmpFile)) {
                         fileOutputStream.write(decryptedBytes);

+ 3 - 1
app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java

@@ -638,7 +638,9 @@ public class UploadFileOperation extends SyncOperation {
                                                serializedFolderMetadata,
                                                token,
                                                client,
-                                               metadataExists);
+                                               metadataExists,
+                                               arbitraryDataProvider,
+                                               user);
 
                 // unlock
                 result = EncryptionUtils.unlockFolder(parentFile, client, token);

+ 2 - 0
app/src/main/java/com/owncloud/android/ui/dialog/SetupEncryptionDialogFragment.kt

@@ -223,6 +223,7 @@ class SetupEncryptionDialogFragment : DialogFragment(), Injectable {
             val secondKey = EncryptionUtils.decodeStringToBase64Bytes(decryptedString)
 
             if (!Arrays.equals(firstKey, secondKey)) {
+                EncryptionUtils.reportE2eError(arbitraryDataProvider, user)
                 throw Exception("Keys do not match")
             }
 
@@ -404,6 +405,7 @@ class SetupEncryptionDialogFragment : DialogFragment(), Injectable {
                 if (result.isSuccess) {
                     publicKeyString = result.data[0] as String
                     if (!EncryptionUtils.isMatchingKeys(keyPair, publicKeyString)) {
+                        EncryptionUtils.reportE2eError(arbitraryDataProvider, user)
                         throw RuntimeException("Wrong CSR returned")
                     }
                     Log_OC.d(TAG, "public key success")

+ 2 - 2
app/src/main/java/com/owncloud/android/ui/fragment/GroupfolderListFragment.kt

@@ -125,8 +125,8 @@ class GroupfolderListFragment : OCFileListFragment(), Injectable, GroupfolderLis
             val fetchResult = ReadFileRemoteOperation(partialFile.remotePath).execute(user, context)
             if (!fetchResult.isSuccess) {
                 logger.e(SHARED_TAG, "Error fetching file")
-                if (fetchResult.isException) {
-                    logger.e(SHARED_TAG, "exception: ", fetchResult.exception)
+                if (fetchResult.isException && fetchResult.exception != null) {
+                    logger.e(SHARED_TAG, "exception: ", fetchResult.exception!!)
                 }
                 null
             } else {

+ 3 - 1
app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java

@@ -1766,7 +1766,9 @@ public class OCFileListFragment extends ExtendedListFragment implements
                                                serializedFolderMetadata,
                                                token,
                                                client,
-                                               metadataExists);
+                                               metadataExists,
+                                               arbitraryDataProvider,
+                                               user);
 
                 // unlock folder
                 EncryptionUtils.unlockFolder(folder, client, token);

+ 2 - 2
app/src/main/java/com/owncloud/android/ui/fragment/SharedListFragment.kt

@@ -87,8 +87,8 @@ class SharedListFragment : OCFileListFragment(), Injectable {
             val fetchResult = ReadFileRemoteOperation(partialFile.remotePath).execute(user, context)
             if (!fetchResult.isSuccess) {
                 logger.e(SHARED_TAG, "Error fetching file")
-                if (fetchResult.isException) {
-                    logger.e(SHARED_TAG, "exception: ", fetchResult.exception)
+                if (fetchResult.isException && fetchResult.exception != null) {
+                    logger.e(SHARED_TAG, "exception: ", fetchResult.exception!!)
                 }
                 null
             } else {

+ 59 - 6
app/src/main/java/com/owncloud/android/utils/EncryptionUtils.java

@@ -47,6 +47,8 @@ import com.owncloud.android.lib.resources.e2ee.StoreMetadataRemoteOperation;
 import com.owncloud.android.lib.resources.e2ee.UnlockFileRemoteOperation;
 import com.owncloud.android.lib.resources.e2ee.UpdateMetadataRemoteOperation;
 import com.owncloud.android.lib.resources.status.NextcloudVersion;
+import com.owncloud.android.lib.resources.status.Problem;
+import com.owncloud.android.lib.resources.status.SendClientDiagnosticRemoteOperation;
 import com.owncloud.android.operations.UploadException;
 
 import org.apache.commons.httpclient.HttpStatus;
@@ -326,10 +328,12 @@ public final class EncryptionUtils {
 
         if (TextUtils.isEmpty(decryptedFolderChecksum) &&
             isFolderMigrated(remoteId, user, arbitraryDataProvider)) {
+            reportE2eError(arbitraryDataProvider, user);
             throw new IllegalStateException("Possible downgrade attack detected!");
         }
 
         if (!TextUtils.isEmpty(decryptedFolderChecksum) && !decryptedFolderChecksum.equals(checksum)) {
+            reportE2eError(arbitraryDataProvider, user);
             throw new IllegalStateException("Wrong checksum!");
         }
 
@@ -349,7 +353,9 @@ public final class EncryptionUtils {
                     encryptedFile.getEncrypted(),
                     decodeStringToBase64Bytes(encryptedKey),
                     decodeStringToBase64Bytes(encryptedFile.getEncryptedInitializationVector()),
-                    decodeStringToBase64Bytes(encryptedFile.getEncryptedTag())
+                    decodeStringToBase64Bytes(encryptedFile.getEncryptedTag()),
+                    arbitraryDataProvider,
+                    user
                                                              );
 
                 DecryptedFolderMetadata.DecryptedFile decryptedFile = new DecryptedFolderMetadata.DecryptedFile();
@@ -430,7 +436,9 @@ public final class EncryptionUtils {
                                                serializedFolderMetadata,
                                                token,
                                                client,
-                                               true);
+                                               true,
+                                               arbitraryDataProvider,
+                                               user);
 
                 // unlock folder
                 RemoteOperationResult unlockFolderResult = EncryptionUtils.unlockFolder(folder, client, token);
@@ -534,7 +542,12 @@ public final class EncryptionUtils {
      * @param authenticationTag  authenticationTag from metadata
      * @return decrypted byte[]
      */
-    public static byte[] decryptFile(File file, byte[] encryptionKeyBytes, byte[] iv, byte[] authenticationTag)
+    public static byte[] decryptFile(File file,
+                                     byte[] encryptionKeyBytes,
+                                     byte[] iv,
+                                     byte[] authenticationTag,
+                                     ArbitraryDataProvider arbitraryDataProvider,
+                                     User user)
         throws NoSuchAlgorithmException,
         InvalidAlgorithmParameterException, NoSuchPaddingException, InvalidKeyException,
         BadPaddingException, IllegalBlockSizeException, IOException {
@@ -554,6 +567,7 @@ public final class EncryptionUtils {
                                                                fileBytes.length - (128 / 8), fileBytes.length);
 
         if (!Arrays.equals(extractedAuthenticationTag, authenticationTag)) {
+            reportE2eError(arbitraryDataProvider, user);
             throw new SecurityException("Tag not correct");
         }
 
@@ -713,7 +727,9 @@ public final class EncryptionUtils {
     public static String decryptStringSymmetric(String string,
                                                 byte[] encryptionKeyBytes,
                                                 byte[] iv,
-                                                byte[] authenticationTag)
+                                                byte[] authenticationTag,
+                                                ArbitraryDataProvider arbitraryDataProvider,
+                                                User user)
         throws NoSuchPaddingException,
         NoSuchAlgorithmException,
         InvalidAlgorithmParameterException,
@@ -733,6 +749,7 @@ public final class EncryptionUtils {
                                                                bytes.length);
 
         if (!Arrays.equals(extractedAuthenticationTag, authenticationTag)) {
+            reportE2eError(arbitraryDataProvider, user);
             throw new SecurityException("Tag not correct");
         }
 
@@ -1057,7 +1074,7 @@ public final class EncryptionUtils {
 
             return new Pair<>(Boolean.FALSE, metadata);
         } else {
-            // TODO error
+            reportE2eError(arbitraryDataProvider, user);
             throw new UploadException("something wrong");
         }
     }
@@ -1066,7 +1083,9 @@ public final class EncryptionUtils {
                                       String serializedFolderMetadata,
                                       String token,
                                       OwnCloudClient client,
-                                      boolean metadataExists) throws UploadException {
+                                      boolean metadataExists,
+                                      ArbitraryDataProvider arbitraryDataProvider,
+                                      User user) throws UploadException {
         RemoteOperationResult uploadMetadataOperationResult;
         if (metadataExists) {
             // update metadata
@@ -1081,6 +1100,7 @@ public final class EncryptionUtils {
         }
 
         if (!uploadMetadataOperationResult.isSuccess()) {
+            reportE2eError(arbitraryDataProvider, user);
             throw new UploadException("Storing/updating metadata was not successful");
         }
     }
@@ -1207,4 +1227,37 @@ public final class EncryptionUtils {
 
         return arrayList.contains(id);
     }
+
+    public static void reportE2eError(ArbitraryDataProvider arbitraryDataProvider, User user) {
+        arbitraryDataProvider.incrementValue(user.getAccountName(), ArbitraryDataProvider.E2E_ERRORS);
+
+        if (arbitraryDataProvider.getLongValue(user.getAccountName(),
+                                               ArbitraryDataProvider.E2E_ERRORS_TIMESTAMP) == -1L) {
+            arbitraryDataProvider.storeOrUpdateKeyValue(
+                user.getAccountName(),
+                ArbitraryDataProvider.E2E_ERRORS_TIMESTAMP,
+                System.currentTimeMillis() / 1000
+                                                       );
+        }
+    }
+
+    @Nullable
+    public static Problem readE2eError(ArbitraryDataProvider arbitraryDataProvider, User user) {
+        int value = arbitraryDataProvider.getIntegerValue(user.getAccountName(),
+                                                          ArbitraryDataProvider.E2E_ERRORS);
+        long timestamp = arbitraryDataProvider.getLongValue(user.getAccountName(),
+                                                            ArbitraryDataProvider.E2E_ERRORS_TIMESTAMP);
+
+        arbitraryDataProvider.deleteKeyForAccount(user.getAccountName(),
+                                                  ArbitraryDataProvider.E2E_ERRORS);
+
+        arbitraryDataProvider.deleteKeyForAccount(user.getAccountName(),
+                                                  ArbitraryDataProvider.E2E_ERRORS_TIMESTAMP);
+
+        if (value > 0 && timestamp > 0) {
+            return new Problem(SendClientDiagnosticRemoteOperation.E2E_ERRORS, value, timestamp);
+        } else {
+            return null;
+        }
+    }
 }