فهرست منبع

Internal two way sync

Signed-off-by: tobiasKaminsky <tobias@kaminsky.me>
tobiasKaminsky 10 ماه پیش
والد
کامیت
82c6956566
33فایلهای تغییر یافته به همراه1789 افزوده شده و 29 حذف شده
  1. 1 1
      app/schemas/com.nextcloud.client.database.NextcloudDatabase/81.json
  2. 1245 0
      app/schemas/com.nextcloud.client.database.NextcloudDatabase/83.json
  3. BIN
      app/screenshots/gplay/debug/com.nextcloud.client.SettingsActivityIT_open.png
  4. BIN
      app/screenshots/gplay/debug/com.nextcloud.client.SettingsActivityIT_showMnemonic_Error.png
  5. 0 1
      app/src/androidTest/java/com/owncloud/android/FileIT.java
  6. 4 1
      app/src/main/AndroidManifest.xml
  7. 2 1
      app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt
  8. 6 0
      app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt
  9. 5 1
      app/src/main/java/com/nextcloud/client/database/entity/FileEntity.kt
  10. 4 0
      app/src/main/java/com/nextcloud/client/di/ComponentsModule.java
  11. 11 0
      app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt
  12. 1 0
      app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt
  13. 11 0
      app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt
  14. 91 0
      app/src/main/java/com/nextcloud/client/jobs/InternalTwoWaySyncWork.kt
  15. 1 0
      app/src/main/java/com/owncloud/android/MainApp.java
  16. 31 0
      app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java
  17. 22 0
      app/src/main/java/com/owncloud/android/datamodel/OCFile.java
  18. 6 2
      app/src/main/java/com/owncloud/android/db/ProviderMeta.java
  19. 1 0
      app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java
  20. 2 0
      app/src/main/java/com/owncloud/android/operations/SynchronizeFileOperation.java
  21. 14 9
      app/src/main/java/com/owncloud/android/operations/SynchronizeFolderOperation.java
  22. 0 1
      app/src/main/java/com/owncloud/android/services/OperationsService.java
  23. 30 0
      app/src/main/java/com/owncloud/android/ui/activity/InternalTwoWaySyncActivity.kt
  24. 21 2
      app/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java
  25. 6 2
      app/src/main/java/com/owncloud/android/ui/activity/StorageMigration.java
  26. 43 0
      app/src/main/java/com/owncloud/android/ui/adapter/InternalTwoWaySyncAdapter.kt
  27. 44 0
      app/src/main/java/com/owncloud/android/ui/adapter/InternalTwoWaySyncViewHolder.kt
  28. 20 2
      app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java
  29. 33 0
      app/src/main/res/layout/file_details_fragment.xml
  30. 18 0
      app/src/main/res/layout/internal_two_way_sync_layout.xml
  31. 97 0
      app/src/main/res/layout/internal_two_way_sync_view_holder.xml
  32. 5 2
      app/src/main/res/values/strings.xml
  33. 14 4
      app/src/main/res/xml/preferences.xml

+ 1 - 1
app/schemas/com.nextcloud.client.database.NextcloudDatabase/81.json

@@ -1206,4 +1206,4 @@
       "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '082a63031678a67879428f688f02d3b5')"
     ]
   }
-}
+}

+ 1245 - 0
app/schemas/com.nextcloud.client.database.NextcloudDatabase/83.json

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

BIN
app/screenshots/gplay/debug/com.nextcloud.client.SettingsActivityIT_open.png


BIN
app/screenshots/gplay/debug/com.nextcloud.client.SettingsActivityIT_showMnemonic_Error.png


+ 0 - 1
app/src/androidTest/java/com/owncloud/android/FileIT.java

@@ -106,7 +106,6 @@ public class FileIT extends AbstractOnServerIT {
         assertTrue(new SynchronizeFolderOperation(targetContext,
                                                   folderPath,
                                                   user,
-                                                  System.currentTimeMillis(),
                                                   fileDataStorageManager)
                        .execute(targetContext)
                        .isSuccess());

+ 4 - 1
app/src/main/AndroidManifest.xml

@@ -255,6 +255,9 @@
         <activity
             android:name=".ui.activity.SyncedFoldersActivity"
             android:exported="false" />
+        <activity
+            android:name=".ui.activity.InternalTwoWaySyncActivity"
+            android:exported="false" />
         <activity
             android:name="com.nextcloud.client.widget.DashboardWidgetConfigurationActivity"
             android:exported="false">
@@ -627,4 +630,4 @@
         </activity>
     </application>
 
-</manifest>
+</manifest>

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

@@ -60,7 +60,8 @@ import com.owncloud.android.db.ProviderMeta
         AutoMigration(from = 78, to = 79, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class),
         AutoMigration(from = 79, to = 80),
         AutoMigration(from = 80, to = 81),
-        AutoMigration(from = 81, to = 82)
+        AutoMigration(from = 81, to = 82),
+        AutoMigration(from = 82, to = 83)
     ],
     exportSchema = true
 )

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

@@ -49,4 +49,10 @@ interface FileDao {
 
     @Query("SELECT * FROM filelist where file_owner = :fileOwner AND etag_in_conflict IS NOT NULL")
     fun getFilesWithSyncConflict(fileOwner: String): List<FileEntity>
+
+    @Query(
+        "SELECT * FROM filelist where file_owner = :fileOwner AND internal_two_way_sync_timestamp >= 0 " +
+            "ORDER BY internal_two_way_sync_timestamp DESC"
+    )
+    fun getInternalTwoWaySyncFolders(fileOwner: String): List<FileEntity>
 }

+ 5 - 1
app/src/main/java/com/nextcloud/client/database/entity/FileEntity.kt

@@ -115,5 +115,9 @@ data class FileEntity(
     @ColumnInfo(name = ProviderTableMeta.FILE_METADATA_GPS)
     val metadataGPS: String?,
     @ColumnInfo(name = ProviderTableMeta.FILE_E2E_COUNTER)
-    val e2eCounter: Long?
+    val e2eCounter: Long?,
+    @ColumnInfo(name = ProviderTableMeta.FILE_INTERNAL_TWO_WAY_SYNC_TIMESTAMP)
+    val internalTwoWaySync: Long?,
+    @ColumnInfo(name = ProviderTableMeta.FILE_INTERNAL_TWO_WAY_SYNC_RESULT)
+    val internalTwoWaySyncResult: String?
 )

+ 4 - 0
app/src/main/java/com/nextcloud/client/di/ComponentsModule.java

@@ -54,6 +54,7 @@ import com.owncloud.android.ui.activity.FileActivity;
 import com.owncloud.android.ui.activity.FileDisplayActivity;
 import com.owncloud.android.ui.activity.FilePickerActivity;
 import com.owncloud.android.ui.activity.FolderPickerActivity;
+import com.owncloud.android.ui.activity.InternalTwoWaySyncActivity;
 import com.owncloud.android.ui.activity.ManageAccountsActivity;
 import com.owncloud.android.ui.activity.ManageSpaceActivity;
 import com.owncloud.android.ui.activity.NotificationsActivity;
@@ -476,4 +477,7 @@ abstract class ComponentsModule {
 
     @ContributesAndroidInjector
     abstract TestJob testJob();
+    
+    @ContributesAndroidInjector
+    abstract InternalTwoWaySyncActivity internalTwoWaySyncActivity();
 }

+ 11 - 0
app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt

@@ -95,6 +95,7 @@ class BackgroundJobFactory @Inject constructor(
                 GeneratePdfFromImagesWork::class -> createPDFGenerateWork(context, workerParameters)
                 HealthStatusWork::class -> createHealthStatusWork(context, workerParameters)
                 TestJob::class -> createTestJob(context, workerParameters)
+                InternalTwoWaySyncWork::class -> createInternalTwoWaySyncWork(context, workerParameters)
                 else -> null // caller falls back to default factory
             }
         }
@@ -277,4 +278,14 @@ class BackgroundJobFactory @Inject constructor(
             backgroundJobManager.get()
         )
     }
+
+    private fun createInternalTwoWaySyncWork(context: Context, params: WorkerParameters): InternalTwoWaySyncWork {
+        return InternalTwoWaySyncWork(
+            context,
+            params,
+            accountManager,
+            powerManagementService,
+            connectivityService
+        )
+    }
 }

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

@@ -168,4 +168,5 @@ interface BackgroundJobManager {
     fun schedulePeriodicHealthStatus()
     fun startHealthStatus()
     fun bothFilesSyncJobsRunning(syncedFolderID: Long): Boolean
+    fun scheduleInternal2WaySync()
 }

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

@@ -84,6 +84,8 @@ internal class BackgroundJobManagerImpl(
         const val JOB_PERIODIC_HEALTH_STATUS = "periodic_health_status"
         const val JOB_IMMEDIATE_HEALTH_STATUS = "immediate_health_status"
 
+        const val JOB_INTERNAL_TWO_WAY_SYNC = "internal_two_way_sync"
+
         const val JOB_TEST = "test_job"
 
         const val MAX_CONTENT_TRIGGER_DELAY_MS = 10000L
@@ -647,4 +649,13 @@ internal class BackgroundJobManagerImpl(
             request
         )
     }
+
+    override fun scheduleInternal2WaySync() {
+        val request = periodicRequestBuilder(
+            jobClass = InternalTwoWaySyncWork::class,
+            jobName = JOB_INTERNAL_TWO_WAY_SYNC
+        ).build()
+
+        workManager.enqueueUniquePeriodicWork(JOB_INTERNAL_TWO_WAY_SYNC, ExistingPeriodicWorkPolicy.KEEP, request)
+    }
 }

+ 91 - 0
app/src/main/java/com/nextcloud/client/jobs/InternalTwoWaySyncWork.kt

@@ -0,0 +1,91 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Tobias Kaminsky <tobias.kaminsky@nextcloud.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package com.nextcloud.client.jobs
+
+import android.content.Context
+import androidx.work.Worker
+import androidx.work.WorkerParameters
+import com.nextcloud.client.account.UserAccountManager
+import com.nextcloud.client.device.PowerManagementService
+import com.nextcloud.client.network.ConnectivityService
+import com.owncloud.android.MainApp
+import com.owncloud.android.datamodel.FileDataStorageManager
+import com.owncloud.android.lib.common.utils.Log_OC
+import com.owncloud.android.operations.SynchronizeFolderOperation
+import com.owncloud.android.utils.FileStorageUtils
+import java.io.File
+
+@Suppress("Detekt.NestedBlockDepth")
+class InternalTwoWaySyncWork(
+    private val context: Context,
+    params: WorkerParameters,
+    private val userAccountManager: UserAccountManager,
+    private val powerManagementService: PowerManagementService,
+    private val connectivityService: ConnectivityService
+) : Worker(context, params) {
+    override fun doWork(): Result {
+        Log_OC.d(TAG, "Worker started!")
+
+        var result = true
+
+        if (powerManagementService.isPowerSavingEnabled ||
+            !connectivityService.isConnected || connectivityService.isInternetWalled
+        ) {
+            Log_OC.d(TAG, "Not starting due to constraints!")
+            return Result.success()
+        }
+
+        val users = userAccountManager.allUsers
+
+        for (user in users) {
+            val fileDataStorageManager = FileDataStorageManager(user, context.contentResolver)
+            val folders = fileDataStorageManager.getInternalTwoWaySyncFolders(user)
+
+            for (folder in folders) {
+                val freeSpaceLeft = File(folder.storagePath).getFreeSpace()
+                val localFolderSize = FileStorageUtils.getFolderSize(File(folder.storagePath, MainApp.getDataFolder()))
+                val remoteFolderSize = folder.fileLength
+
+                if (freeSpaceLeft < (remoteFolderSize - localFolderSize)) {
+                    Log_OC.d(TAG, "Not enough space left!")
+                    result = false
+                }
+
+                Log_OC.d(TAG, "Folder ${folder.remotePath}: started!")
+                val operation = SynchronizeFolderOperation(context, folder.remotePath, user, fileDataStorageManager)
+                    .execute(context)
+
+                if (operation.isSuccess) {
+                    Log_OC.d(TAG, "Folder ${folder.remotePath}: finished!")
+                } else {
+                    Log_OC.d(TAG, "Folder ${folder.remotePath} failed!")
+                    result = false
+                }
+
+                folder.apply {
+                    internalFolderSyncResult = operation.code.toString()
+                    internalFolderSyncTimestamp = System.currentTimeMillis()
+                }
+
+                fileDataStorageManager.saveFile(folder)
+            }
+        }
+
+        return if (result) {
+            Log_OC.d(TAG, "Worker finished with success!")
+            Result.success()
+        } else {
+            Log_OC.d(TAG, "Worker finished with failure!")
+            Result.failure()
+        }
+    }
+
+    companion object {
+        const val TAG = "InternalTwoWaySyncWork"
+    }
+}

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

@@ -371,6 +371,7 @@ public class MainApp extends Application implements HasAndroidInjector {
             backgroundJobManager.scheduleMediaFoldersDetectionJob();
             backgroundJobManager.startMediaFoldersDetectionJob();
             backgroundJobManager.schedulePeriodicHealthStatus();
+            backgroundJobManager.scheduleInternal2WaySync();
         }
 
         registerGlobalPassCodeProtection();

+ 31 - 0
app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java

@@ -77,6 +77,7 @@ import androidx.annotation.VisibleForTesting;
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 import kotlin.Pair;
 
+@SuppressFBWarnings("CE")
 public class FileDataStorageManager {
     private static final String TAG = FileDataStorageManager.class.getSimpleName();
 
@@ -558,6 +559,8 @@ public class FileDataStorageManager {
         cv.put(ProviderTableMeta.FILE_SHAREES, gson.toJson(fileOrFolder.getSharees()));
         cv.put(ProviderTableMeta.FILE_TAGS, gson.toJson(fileOrFolder.getTags()));
         cv.put(ProviderTableMeta.FILE_RICH_WORKSPACE, fileOrFolder.getRichWorkspace());
+        cv.put(ProviderTableMeta.FILE_INTERNAL_TWO_WAY_SYNC_TIMESTAMP, fileOrFolder.getInternalFolderSyncTimestamp());
+        cv.put(ProviderTableMeta.FILE_INTERNAL_TWO_WAY_SYNC_RESULT, fileOrFolder.getInternalFolderSyncResult());
         return cv;
     }
 
@@ -602,6 +605,8 @@ public class FileDataStorageManager {
         cv.put(ProviderTableMeta.FILE_METADATA_GPS, gson.toJson(file.getGeoLocation()));
         cv.put(ProviderTableMeta.FILE_METADATA_LIVE_PHOTO, file.getLinkedFileIdForLivePhoto());
         cv.put(ProviderTableMeta.FILE_E2E_COUNTER, file.getE2eCounter());
+        cv.put(ProviderTableMeta.FILE_INTERNAL_TWO_WAY_SYNC_TIMESTAMP, file.getInternalFolderSyncTimestamp());
+        cv.put(ProviderTableMeta.FILE_INTERNAL_TWO_WAY_SYNC_RESULT, file.getInternalFolderSyncResult());
 
         return cv;
     }
@@ -1035,6 +1040,7 @@ public class FileDataStorageManager {
         ocFile.setLivePhoto(fileEntity.getMetadataLivePhoto());
         ocFile.setHidden(nullToZero(fileEntity.getHidden()) == 1);
         ocFile.setE2eCounter(fileEntity.getE2eCounter());
+        ocFile.setInternalFolderSyncTimestamp(fileEntity.getInternalTwoWaySync());
 
         String sharees = fileEntity.getSharees();
         // Surprisingly JSON deserialization causes significant overhead.
@@ -2477,4 +2483,29 @@ public class FileDataStorageManager {
 
         return files;
     }
+    
+    public List<OCFile> getInternalTwoWaySyncFolders(User user) {
+        List<FileEntity> fileEntities = fileDao.getInternalTwoWaySyncFolders(user.getAccountName());
+        List<OCFile> files = new ArrayList<>(fileEntities.size());
+
+        for (FileEntity fileEntity : fileEntities) {
+            files.add(createFileInstance(fileEntity));
+        }
+
+        return files;
+    }
+    
+    public boolean isPartOfInternalTwoWaySync(OCFile file) {
+        if (file.isInternalFolderSync()) {
+            return true;
+        }
+
+        while (file != null && !OCFile.ROOT_PATH.equals(file.getDecryptedRemotePath())) {
+            if (file.isInternalFolderSync()) {
+                return true;
+            }
+            file = getFileById(file.getParentId());
+        }
+        return false;
+    }
 }

+ 22 - 0
app/src/main/java/com/owncloud/android/datamodel/OCFile.java

@@ -117,6 +117,8 @@ public class OCFile implements Parcelable, Comparable<OCFile>, ServerFileInterfa
     @Nullable
     private GeoLocation geolocation;
     private List<String> tags = new ArrayList<>();
+    private Long internalFolderSyncTimestamp = -1L;
+    private String internalFolderSyncResult = "";
 
     /**
      * URI to the local path of the file contents, if stored in the device; cached after first call to
@@ -1051,6 +1053,26 @@ public class OCFile implements Parcelable, Comparable<OCFile>, ServerFileInterfa
             this.e2eCounter = e2eCounter;
         }
     }
+
+    public boolean isInternalFolderSync() {
+        return internalFolderSyncTimestamp >= 0;
+    }
+    
+    public Long getInternalFolderSyncTimestamp() {
+        return internalFolderSyncTimestamp;
+    }
+
+    public void setInternalFolderSyncTimestamp(Long internalFolderSyncTimestamp) {
+        this.internalFolderSyncTimestamp = internalFolderSyncTimestamp;
+    }
+
+    public String getInternalFolderSyncResult() {
+        return internalFolderSyncResult;
+    }
+
+    public void setInternalFolderSyncResult(String internalFolderSyncResult) {
+        this.internalFolderSyncResult = internalFolderSyncResult;
+    }
     
     public boolean isAPKorAAB() {
         if ("gplay".equals(BuildConfig.FLAVOR)) {

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

@@ -25,7 +25,7 @@ import java.util.List;
  */
 public class ProviderMeta {
     public static final String DB_NAME = "filelist";
-    public static final int DB_VERSION = 82;
+    public static final int DB_VERSION = 83;
 
     private ProviderMeta() {
         // No instance
@@ -120,6 +120,8 @@ public class ProviderMeta {
         public static final String FILE_LOCK_TOKEN = "lock_token";
         public static final String FILE_TAGS = "tags";
         public static final String FILE_E2E_COUNTER = "e2e_counter";
+        public static final String FILE_INTERNAL_TWO_WAY_SYNC_TIMESTAMP = "internal_two_way_sync_timestamp";
+        public static final String FILE_INTERNAL_TWO_WAY_SYNC_RESULT = "internal_two_way_sync_result";
 
         public static final List<String> FILE_ALL_COLUMNS = Collections.unmodifiableList(Arrays.asList(
                 _ID,
@@ -171,7 +173,9 @@ public class ProviderMeta {
                 FILE_METADATA_LIVE_PHOTO,
                 FILE_E2E_COUNTER,
                 FILE_TAGS,
-                FILE_METADATA_GPS));
+                FILE_METADATA_GPS,
+                FILE_INTERNAL_TWO_WAY_SYNC_TIMESTAMP,
+                FILE_INTERNAL_TWO_WAY_SYNC_RESULT));
         public static final String FILE_DEFAULT_SORT_ORDER = FILE_NAME + " collate nocase asc";
 
         // Columns of ocshares table

+ 1 - 0
app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java

@@ -697,6 +697,7 @@ public class RefreshFolderOperation extends RemoteOperation {
         if (localFile != null) {
             updatedFile.setFileId(localFile.getFileId());
             updatedFile.setLastSyncDateForData(localFile.getLastSyncDateForData());
+            updatedFile.setInternalFolderSyncTimestamp(localFile.getInternalFolderSyncTimestamp());
             updatedFile.setModificationTimestampAtLastSyncForData(
                 localFile.getModificationTimestampAtLastSyncForData()
                                                                  );

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

@@ -295,6 +295,8 @@ public class SynchronizeFileOperation extends SyncOperation {
     }
 
     private void requestForDownload(OCFile file) {
+        Log_OC.d("InternalTwoWaySyncWork", "download file: " + file.getFileName());
+        
         FileDownloadHelper.Companion.instance().downloadFile(
             mUser,
             file);

+ 14 - 9
app/src/main/java/com/owncloud/android/operations/SynchronizeFolderOperation.java

@@ -55,9 +55,6 @@ public class SynchronizeFolderOperation extends SyncOperation {
 
     private static final String TAG = SynchronizeFolderOperation.class.getSimpleName();
 
-    /** Time stamp for the synchronization process in progress */
-    private long mCurrentSyncTime;
-
     /** Remote path of the folder to synchronize */
     private String mRemotePath;
 
@@ -95,17 +92,14 @@ public class SynchronizeFolderOperation extends SyncOperation {
      * @param context         Application context.
      * @param remotePath      Path to synchronize.
      * @param user            Nextcloud account where the folder is located.
-     * @param currentSyncTime Time stamp for the synchronization process in progress.
      */
     public SynchronizeFolderOperation(Context context,
                                       String remotePath,
                                       User user,
-                                      long currentSyncTime,
                                       FileDataStorageManager storageManager) {
         super(storageManager);
 
         mRemotePath = remotePath;
-        mCurrentSyncTime = currentSyncTime;
         this.user = user;
         mContext = context;
         mRemoteFolderChanged = false;
@@ -365,7 +359,7 @@ public class SynchronizeFolderOperation extends SyncOperation {
     }
 
     private void updateLocalStateData(OCFile remoteFile, OCFile localFile, OCFile updatedFile) {
-        updatedFile.setLastSyncDateForProperties(mCurrentSyncTime);
+        updatedFile.setLastSyncDateForProperties(System.currentTimeMillis());
         if (localFile != null) {
             updatedFile.setFileId(localFile.getFileId());
             updatedFile.setLastSyncDateForData(localFile.getLastSyncDateForData());
@@ -393,8 +387,19 @@ public class SynchronizeFolderOperation extends SyncOperation {
         }
     }
 
-    private void classifyFileForLaterSyncOrDownload(OCFile remoteFile, OCFile localFile) {
-        if (!remoteFile.isFolder()) {
+    @SuppressFBWarnings("JLM")
+    private void classifyFileForLaterSyncOrDownload(OCFile remoteFile, OCFile localFile) throws OperationCancelledException {
+        if (remoteFile.isFolder()) {
+            /// to download children files recursively
+            synchronized (mCancellationRequested) {
+                if (mCancellationRequested.get()) {
+                    throw new OperationCancelledException();
+                }
+                startSyncFolderOperation(remoteFile.getRemotePath());
+            }
+
+        } else {
+            /// prepare content synchronization for files (any file, not just favorites)
             SynchronizeFileOperation operation = new SynchronizeFileOperation(
                 localFile,
                 remoteFile,

+ 0 - 1
app/src/main/java/com/owncloud/android/services/OperationsService.java

@@ -707,7 +707,6 @@ public class OperationsService extends Service {
                             this,                       // TODO remove this dependency from construction time
                             remotePath,
                             user,
-                            System.currentTimeMillis(),  // TODO remove this dependency from construction time
                             fileDataStorageManager
                         );
                         break;

+ 30 - 0
app/src/main/java/com/owncloud/android/ui/activity/InternalTwoWaySyncActivity.kt

@@ -0,0 +1,30 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Tobias Kaminsky <tobias.kaminsky@nextcloud.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package com.owncloud.android.ui.activity
+
+import android.os.Bundle
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.nextcloud.client.di.Injectable
+import com.owncloud.android.databinding.InternalTwoWaySyncLayoutBinding
+import com.owncloud.android.ui.adapter.InternalTwoWaySyncAdapter
+
+class InternalTwoWaySyncActivity : BaseActivity(), Injectable {
+    lateinit var binding: InternalTwoWaySyncLayoutBinding
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        binding = InternalTwoWaySyncLayoutBinding.inflate(layoutInflater)
+        setContentView(binding.root)
+
+        binding.list.apply {
+            adapter = InternalTwoWaySyncAdapter(fileDataStorageManager, user.get(), context)
+            layoutManager = LinearLayoutManager(context)
+        }
+    }
+}

+ 21 - 2
app/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java

@@ -171,6 +171,9 @@ public class SettingsActivity extends PreferenceActivity
         // Details
         setupDetailsCategory(preferenceScreen);
 
+        // Sync
+        setupSyncCategory();
+        
         // More
         setupMoreCategory();
 
@@ -310,13 +313,19 @@ public class SettingsActivity extends PreferenceActivity
             }
         }
     }
+    
+    private void setupSyncCategory() {
+        final PreferenceCategory preferenceCategorySync = (PreferenceCategory) findPreference("sync");
+        viewThemeUtils.files.themePreferenceCategory(preferenceCategorySync);
+
+        setupAutoUploadPreference(preferenceCategorySync);
+        setupInternalTwoWaySyncPreference(preferenceCategorySync);
+    }
 
     private void setupMoreCategory() {
         final PreferenceCategory preferenceCategoryMore = (PreferenceCategory) findPreference("more");
         viewThemeUtils.files.themePreferenceCategory(preferenceCategoryMore);
 
-        setupAutoUploadPreference(preferenceCategoryMore);
-
         setupCalendarPreference(preferenceCategoryMore);
 
         setupBackupPreference();
@@ -548,6 +557,16 @@ public class SettingsActivity extends PreferenceActivity
             });
         }
     }
+    
+    private void setupInternalTwoWaySyncPreference(PreferenceCategory preferenceCategorySync) {
+        Preference twoWaySync = findPreference("internal_two_way_sync");
+        
+        twoWaySync.setOnPreferenceClickListener(preference -> {
+            Intent intent = new Intent(this, InternalTwoWaySyncActivity.class);
+            startActivity(intent);
+            return true;
+        });
+    }
 
     private void setupBackupPreference() {
         Preference pContactsBackup = findPreference("backup");

+ 6 - 2
app/src/main/java/com/owncloud/android/ui/activity/StorageMigration.java

@@ -424,8 +424,12 @@ public class StorageMigration {
                 throw new MigrationException(R.string.file_migration_failed_dir_already_exists);
             }
 
-            if (dstFile.getFreeSpace() < FileStorageUtils.getFolderSize(new File(srcFile, MainApp.getDataFolder()))) {
-                throw new MigrationException(R.string.file_migration_failed_not_enough_space);
+            try {
+                if (dstFile.getFreeSpace() < FileStorageUtils.getFolderSize(new File(srcFile, MainApp.getDataFolder()))) {
+                    throw new MigrationException(R.string.file_migration_failed_not_enough_space);
+                }
+            } catch (MigrationException e) {
+                throw new RuntimeException(e);
             }
         }
 

+ 43 - 0
app/src/main/java/com/owncloud/android/ui/adapter/InternalTwoWaySyncAdapter.kt

@@ -0,0 +1,43 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Tobias Kaminsky <tobias.kaminsky@nextcloud.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package com.owncloud.android.ui.adapter
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import com.nextcloud.client.account.User
+import com.owncloud.android.databinding.InternalTwoWaySyncViewHolderBinding
+import com.owncloud.android.datamodel.FileDataStorageManager
+import com.owncloud.android.datamodel.OCFile
+
+class InternalTwoWaySyncAdapter(
+    dataStorageManager: FileDataStorageManager,
+    user: User,
+    val context: Context
+) : RecyclerView.Adapter<InternalTwoWaySyncViewHolder>() {
+    var folders: List<OCFile> = dataStorageManager.getInternalTwoWaySyncFolders(user)
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): InternalTwoWaySyncViewHolder {
+        return InternalTwoWaySyncViewHolder(
+            InternalTwoWaySyncViewHolderBinding.inflate(
+                LayoutInflater.from(parent.context),
+                parent,
+                false
+            )
+        )
+    }
+
+    override fun getItemCount(): Int {
+        return folders.size
+    }
+
+    override fun onBindViewHolder(holder: InternalTwoWaySyncViewHolder, position: Int) {
+        holder.bind(folders[position], context)
+    }
+}

+ 44 - 0
app/src/main/java/com/owncloud/android/ui/adapter/InternalTwoWaySyncViewHolder.kt

@@ -0,0 +1,44 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Tobias Kaminsky <tobias.kaminsky@nextcloud.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package com.owncloud.android.ui.adapter
+
+import android.content.Context
+import android.view.View
+import androidx.recyclerview.widget.RecyclerView
+import com.owncloud.android.R
+import com.owncloud.android.databinding.InternalTwoWaySyncViewHolderBinding
+import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.utils.DisplayUtils
+
+class InternalTwoWaySyncViewHolder(val binding: InternalTwoWaySyncViewHolderBinding) :
+    RecyclerView.ViewHolder(binding.root) {
+    fun bind(folder: OCFile, context: Context) {
+        binding.run {
+            size.text = DisplayUtils.bytesToHumanReadable(folder.fileLength)
+            name.text = folder.decryptedFileName
+
+            if (folder.internalFolderSyncResult.isEmpty()) {
+                syncResult.visibility = View.GONE
+                syncResultDivider.visibility = View.GONE
+            } else {
+                syncResult.visibility = View.VISIBLE
+                syncResultDivider.visibility = View.VISIBLE
+                syncResult.text = folder.internalFolderSyncResult
+            }
+
+            if (folder.internalFolderSyncTimestamp == 0L) {
+                syncTimestamp.text = context.getString(R.string.internal_two_way_sync_not_yet)
+            } else {
+                syncTimestamp.text = DisplayUtils.getRelativeTimestamp(
+                    context,
+                    folder.internalFolderSyncTimestamp
+                )
+            }
+        }
+    }
+}

+ 20 - 2
app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java

@@ -261,6 +261,7 @@ public class FileDetailFragment extends FileFragment implements OnClickListener,
             binding.favorite.setOnClickListener(this);
             binding.overflowMenu.setOnClickListener(this);
             binding.lastModificationTimestamp.setOnClickListener(this);
+            binding.folderSyncButton.setOnClickListener(this);
 
             updateFileDetails(false, false);
         }
@@ -471,8 +472,14 @@ public class FileDetailFragment extends FileFragment implements OnClickListener,
             boolean showDetailedTimestamp = !preferences.isShowDetailedTimestampEnabled();
             preferences.setShowDetailedTimestampEnabled(showDetailedTimestamp);
             setFileModificationTimestamp(getFile(), showDetailedTimestamp);
-
-            Log_OC.e(TAG, "Incorrect view clicked!");
+        } else if (id == R.id.folder_sync_button) {
+            if (binding.folderSyncButton.isChecked()) {
+                getFile().setInternalFolderSyncTimestamp(0L);    
+            } else {
+                getFile().setInternalFolderSyncTimestamp(-1L);
+            }
+            
+            storageManager.saveFile(getFile());
         } else {
             Log_OC.e(TAG, "Incorrect view clicked!");
         }
@@ -556,6 +563,17 @@ public class FileDetailFragment extends FileFragment implements OnClickListener,
             if (fabMain != null) {
                 fabMain.hide();
             }
+            
+            binding.syncBlock.setVisibility(file.isFolder() ? View.VISIBLE : View.GONE);
+            
+            if (file.isInternalFolderSync()) {
+                binding.folderSyncButton.setChecked(file.isInternalFolderSync());    
+            } else {
+                if (storageManager.isPartOfInternalTwoWaySync(file)) {
+                    binding.folderSyncButton.setChecked(true);
+                    binding.folderSyncButton.setEnabled(false);
+                }
+            }
         }
 
         setupViewPager();

+ 33 - 0
app/src/main/res/layout/file_details_fragment.xml

@@ -170,6 +170,39 @@
 
     </LinearLayout>
 
+    <LinearLayout
+        android:id="@+id/syncBlock"
+        android:layout_width="match_parent"
+        android:orientation="vertical"
+        android:layout_height="wrap_content">
+        
+        <View
+            android:layout_width="match_parent"
+            android:layout_height="1dp"
+            android:background="@color/list_divider_background" />
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal"
+            android:paddingStart="@dimen/standard_padding"
+            android:paddingTop="@dimen/standard_half_padding"
+            android:paddingEnd="@dimen/zero"
+            android:paddingBottom="@dimen/standard_half_padding">
+
+            <CheckBox
+                android:id="@+id/folder_sync_button"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content" />
+
+            <TextView
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:text="@string/sync" />
+        </LinearLayout>
+        
+    </LinearLayout>
+
     <View
         android:layout_width="match_parent"
         android:layout_height="1dp"

+ 18 - 0
app/src/main/res/layout/internal_two_way_sync_layout.xml

@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Nextcloud - Android Client
+  ~
+  ~ SPDX-FileCopyrightText: 2024 Tobias Kaminsky <tobias@kaminsky.me>
+  ~ SPDX-License-Identifier: AGPL-3.0-or-later
+  -->
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/list"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 97 - 0
app/src/main/res/layout/internal_two_way_sync_view_holder.xml

@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Nextcloud - Android Client
+  ~
+  ~ SPDX-FileCopyrightText: 2024 Andy Scherzinger <info@andy-scherzinger.de>
+  ~ SPDX-FileCopyrightText: 2024 Tobias Kaminsky <tobias@kaminsky.me>
+  ~ SPDX-License-Identifier: AGPL-3.0-or-later
+  -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="@dimen/min_list_item_size"
+    android:orientation="horizontal"
+    android:paddingStart="@dimen/standard_half_padding"
+    android:paddingEnd="@dimen/standard_half_padding">
+
+    <ImageView
+        android:layout_width="@dimen/file_icon_size"
+        android:layout_height="@dimen/file_icon_size"
+        android:layout_gravity="center_vertical"
+        android:contentDescription="@null"
+        android:src="@drawable/folder" />
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/min_list_item_size"
+        android:layout_marginStart="@dimen/standard_half_margin"
+        android:gravity="center_vertical"
+        android:orientation="vertical">
+
+        <TextView
+            android:id="@+id/name"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:ellipsize="middle"
+            android:gravity="center_vertical"
+            android:singleLine="true"
+            android:textColor="@color/text_color"
+            android:textSize="@dimen/two_line_primary_text_size"
+            tools:text="Folder abc" />
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content">
+
+            <TextView
+                android:id="@+id/size"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:gravity="center_vertical"
+                android:textColor="@color/list_item_lastmod_and_filesize_text"
+                android:textSize="@dimen/two_line_secondary_text_size"
+                tools:text="241 Mb" />
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:gravity="end"
+                android:paddingStart="@dimen/zero"
+                android:paddingEnd="@dimen/standard_quarter_padding"
+                android:text="@string/info_separator"
+                android:textColor="@color/list_item_lastmod_and_filesize_text"
+                android:textSize="@dimen/two_line_secondary_text_size" />
+
+            <TextView
+                android:id="@+id/sync_timestamp"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:gravity="center_vertical"
+                android:textColor="@color/list_item_lastmod_and_filesize_text"
+                android:textSize="@dimen/two_line_secondary_text_size"
+                tools:text="5 min ago" />
+
+            <TextView
+                android:id="@+id/sync_result_divider"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:gravity="end"
+                android:paddingStart="@dimen/zero"
+                android:paddingEnd="@dimen/standard_quarter_padding"
+                android:text="@string/info_separator"
+                android:textColor="@color/list_item_lastmod_and_filesize_text"
+                android:textSize="@dimen/two_line_secondary_text_size" />
+
+            <TextView
+                android:id="@+id/sync_result"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:gravity="center_vertical"
+                android:textColor="@color/list_item_lastmod_and_filesize_text"
+                android:textSize="@dimen/two_line_secondary_text_size"
+                tools:text="Success" />
+
+        </LinearLayout>
+    </LinearLayout>
+
+</LinearLayout>

+ 5 - 2
app/src/main/res/values/strings.xml

@@ -1218,13 +1218,15 @@
     <string name="sub_folder_rule_day">Year/Month/Day</string>
     <string name="secure_share_not_set_up">Secure sharing is not set up for this user</string>
     <string name="share_not_allowed_when_file_drop">Resharing is not allowed during secure file drop</string>
+    <string name="prefs_category_sync">Sync</string>
+    <string name="internal_two_way_sync">Internal two way sync</string>
+    <string name="prefs_two_way_sync_summary">Manage internal folders for two way sync</string>
+    <string name="internal_two_way_sync_not_yet">Not yet, soon to be synced</string>
     <string name="gplay_restriction">Google restricted downloading APK/AAB files!</string>
     <string name="file_list_empty_local_search">No file or folder matching your search</string>
-
     <string name="unified_search_fragment_calendar_event_not_found">Event not found, you can always sync to update. Redirecting to web…</string>
     <string name="unified_search_fragment_contact_not_found">Contact not found, you can always sync to update. Redirecting to web…</string>
     <string name="unified_search_fragment_permission_needed">Permissions are required to open search result otherwise it will redirected to web…</string>
-
     <string name="file_name_validator_current_path_is_invalid">Current folder name is invalid, please rename the folder. Redirecting to root</string>
     <string name="file_name_validator_rename_before_move_or_copy">%s. Please rename the file before moving or copying</string>
     <string name="file_name_validator_upload_content_error">Some contents cannot able to uploaded due to contains reserved names or invalid character</string>
@@ -1233,4 +1235,5 @@
     <string name="file_name_validator_error_reserved_names">%s is a forbidden name</string>
     <string name="file_name_validator_error_forbidden_file_extensions">.%s is a forbidden file extension</string>
     <string name="file_name_validator_error_ends_with_space_period">Name ends with a space or a period</string>
+    <string name="sync">Sync</string>
 </resources>

+ 14 - 4
app/src/main/res/xml/preferences.xml

@@ -53,13 +53,23 @@
             android:key="show_media_scan_notifications"/>
 	</PreferenceCategory>
 
+    <PreferenceCategory
+        android:title="@string/prefs_category_sync"
+        android:key="sync">
+    <Preference
+        android:title="@string/drawer_synced_folders"
+        android:key="syncedFolders"
+        android:summary="@string/prefs_sycned_folders_summary" />
+
+        <Preference
+            android:title="@string/internal_two_way_sync"
+            android:key="internal_two_way_sync"
+            android:summary="@string/prefs_two_way_sync_summary" />
+    </PreferenceCategory>
+
 	<PreferenceCategory
         android:title="@string/prefs_category_more"
         android:key="more">
-        <Preference
-            android:title="@string/drawer_synced_folders"
-            android:key="syncedFolders"
-            android:summary="@string/prefs_sycned_folders_summary" />
         <Preference
             android:title="@string/prefs_calendar_contacts"
             android:key="calendar_contacts"