Browse Source

Merge pull request #13408 from nextcloud/feature/offline-create-folder-operation

Feature - Create Folder When Device Don't Have Internet Connection
Andy Scherzinger 11 months ago
parent
commit
8f42be49cc
100 changed files with 3341 additions and 470 deletions
  1. 1301 0
      app/schemas/com.nextcloud.client.database.NextcloudDatabase/84.json
  2. 6 0
      app/src/androidTest/java/com/owncloud/android/AbstractIT.java
  3. 6 0
      app/src/androidTest/java/com/owncloud/android/AbstractOnServerIT.java
  4. 17 0
      app/src/androidTest/java/com/owncloud/android/UploadIT.java
  5. 4 0
      app/src/androidTest/java/com/owncloud/android/files/services/FileUploaderIT.kt
  6. 6 5
      app/src/androidTest/java/com/owncloud/android/ui/activity/ConflictsResolveActivityIT.java
  7. 4 0
      app/src/debug/java/com/nextcloud/test/TestActivity.kt
  8. 3 0
      app/src/main/AndroidManifest.xml
  9. 6 0
      app/src/main/java/com/nextcloud/client/database/DatabaseModule.kt
  10. 7 2
      app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt
  11. 39 0
      app/src/main/java/com/nextcloud/client/database/dao/OfflineOperationDao.kt
  12. 39 0
      app/src/main/java/com/nextcloud/client/database/entity/OfflineOperationEntity.kt
  13. 6 0
      app/src/main/java/com/nextcloud/client/di/AppModule.java
  14. 4 0
      app/src/main/java/com/nextcloud/client/di/ComponentsModule.java
  15. 6 0
      app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt
  16. 2 0
      app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt
  17. 51 3
      app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt
  18. 6 2
      app/src/main/java/com/nextcloud/client/jobs/notification/WorkerNotificationManager.kt
  19. 135 0
      app/src/main/java/com/nextcloud/client/jobs/offlineOperations/OfflineOperationsNotificationManager.kt
  20. 161 0
      app/src/main/java/com/nextcloud/client/jobs/offlineOperations/OfflineOperationsWorker.kt
  21. 91 0
      app/src/main/java/com/nextcloud/client/jobs/offlineOperations/repository/OfflineOperationsRepository.kt
  22. 17 0
      app/src/main/java/com/nextcloud/client/jobs/offlineOperations/repository/OfflineOperationsRepositoryType.kt
  23. 65 0
      app/src/main/java/com/nextcloud/client/jobs/operation/FileOperationHelper.kt
  24. 17 0
      app/src/main/java/com/nextcloud/client/network/ConnectivityService.java
  25. 14 0
      app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.java
  26. 12 0
      app/src/main/java/com/nextcloud/model/OfflineOperationType.kt
  27. 1 0
      app/src/main/java/com/nextcloud/model/WorkerState.kt
  28. 38 0
      app/src/main/java/com/nextcloud/receiver/NetworkChangeReceiver.kt
  29. 30 0
      app/src/main/java/com/nextcloud/receiver/OfflineOperationActionReceiver.kt
  30. 6 2
      app/src/main/java/com/nextcloud/ui/fileactions/FileAction.kt
  31. 15 0
      app/src/main/java/com/nextcloud/utils/date/DateFormatPattern.kt
  32. 5 1
      app/src/main/java/com/nextcloud/utils/extensions/ActivityExtensions.kt
  33. 18 0
      app/src/main/java/com/nextcloud/utils/extensions/DateExtensions.kt
  34. 20 0
      app/src/main/java/com/nextcloud/utils/extensions/ParcableExtensions.kt
  35. 68 0
      app/src/main/java/com/nextcloud/utils/extensions/RemoteOperationResultExtensions.kt
  36. 6 1
      app/src/main/java/com/nextcloud/utils/extensions/ViewExtensions.kt
  37. 30 1
      app/src/main/java/com/owncloud/android/MainApp.java
  38. 119 34
      app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java
  39. 21 0
      app/src/main/java/com/owncloud/android/datamodel/OCFile.java
  40. 76 66
      app/src/main/java/com/owncloud/android/db/ProviderMeta.java
  41. 7 0
      app/src/main/java/com/owncloud/android/files/FileMenuFilter.java
  42. 54 36
      app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java
  43. 175 71
      app/src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.kt
  44. 22 4
      app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java
  45. 22 3
      app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java
  46. 7 3
      app/src/main/java/com/owncloud/android/ui/activity/ToolbarActivity.java
  47. 55 0
      app/src/main/java/com/owncloud/android/ui/activity/fileDisplayActivity/OfflineFolderConflictManager.kt
  48. 1 1
      app/src/main/java/com/owncloud/android/ui/adapter/ListGridItemViewHolder.kt
  49. 1 1
      app/src/main/java/com/owncloud/android/ui/adapter/ListViewHolder.kt
  50. 75 50
      app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java
  51. 9 9
      app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt
  52. 2 2
      app/src/main/java/com/owncloud/android/ui/adapter/OCFileListViewHolder.kt
  53. 211 96
      app/src/main/java/com/owncloud/android/ui/dialog/ConflictsResolveDialog.kt
  54. 32 7
      app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.kt
  55. 28 5
      app/src/main/java/com/owncloud/android/ui/dialog/RemoveFilesDialogFragment.kt
  56. 10 4
      app/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.kt
  57. 69 0
      app/src/main/java/com/owncloud/android/ui/dialog/parcel/ConflictDialogData.kt
  58. 4 1
      app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java
  59. 17 0
      app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetDialog.java
  60. 21 12
      app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java
  61. 1 0
      app/src/main/java/com/owncloud/android/ui/notifications/NotificationUtils.java
  62. 24 0
      app/src/main/res/drawable/ic_retry.xml
  63. 16 11
      app/src/main/res/layout/conflict_resolve_dialog.xml
  64. 0 1
      app/src/main/res/values-ar/strings.xml
  65. 0 1
      app/src/main/res/values-b+en+001/strings.xml
  66. 0 1
      app/src/main/res/values-bg-rBG/strings.xml
  67. 0 1
      app/src/main/res/values-br/strings.xml
  68. 0 1
      app/src/main/res/values-ca/strings.xml
  69. 0 1
      app/src/main/res/values-cs-rCZ/strings.xml
  70. 0 1
      app/src/main/res/values-da/strings.xml
  71. 0 1
      app/src/main/res/values-de/strings.xml
  72. 0 1
      app/src/main/res/values-el/strings.xml
  73. 0 1
      app/src/main/res/values-es-rAR/strings.xml
  74. 0 1
      app/src/main/res/values-es-rEC/strings.xml
  75. 0 1
      app/src/main/res/values-es-rMX/strings.xml
  76. 0 1
      app/src/main/res/values-es/strings.xml
  77. 0 1
      app/src/main/res/values-eu/strings.xml
  78. 0 1
      app/src/main/res/values-fa/strings.xml
  79. 0 1
      app/src/main/res/values-fi-rFI/strings.xml
  80. 0 1
      app/src/main/res/values-fr/strings.xml
  81. 0 1
      app/src/main/res/values-ga/strings.xml
  82. 0 1
      app/src/main/res/values-gd/strings.xml
  83. 0 1
      app/src/main/res/values-gl/strings.xml
  84. 0 1
      app/src/main/res/values-hr/strings.xml
  85. 0 1
      app/src/main/res/values-hu-rHU/strings.xml
  86. 0 1
      app/src/main/res/values-in/strings.xml
  87. 0 1
      app/src/main/res/values-it/strings.xml
  88. 0 1
      app/src/main/res/values-ja-rJP/strings.xml
  89. 0 1
      app/src/main/res/values-ka/strings.xml
  90. 0 1
      app/src/main/res/values-ko/strings.xml
  91. 0 1
      app/src/main/res/values-lo/strings.xml
  92. 0 1
      app/src/main/res/values-lt-rLT/strings.xml
  93. 0 1
      app/src/main/res/values-lv/strings.xml
  94. 0 1
      app/src/main/res/values-mk/strings.xml
  95. 0 1
      app/src/main/res/values-nb-rNO/strings.xml
  96. 0 1
      app/src/main/res/values-nl/strings.xml
  97. 0 1
      app/src/main/res/values-pl/strings.xml
  98. 0 1
      app/src/main/res/values-pt-rBR/strings.xml
  99. 0 1
      app/src/main/res/values-pt-rPT/strings.xml
  100. 0 1
      app/src/main/res/values-ro/strings.xml

+ 1301 - 0
app/schemas/com.nextcloud.client.database.NextcloudDatabase/84.json

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

+ 6 - 0
app/src/androidTest/java/com/owncloud/android/AbstractIT.java

@@ -17,6 +17,7 @@ import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.os.Build;
 import android.os.Bundle;
+import android.os.NetworkOnMainThreadException;
 import android.text.TextUtils;
 import android.view.View;
 
@@ -375,6 +376,11 @@ public abstract class AbstractIT {
 
     public void uploadOCUpload(OCUpload ocUpload) {
         ConnectivityService connectivityServiceMock = new ConnectivityService() {
+            @Override
+            public boolean isNetworkAndServerAvailable() throws NetworkOnMainThreadException {
+                return false;
+            }
+
             @Override
             public boolean isConnected() {
                 return false;

+ 6 - 0
app/src/androidTest/java/com/owncloud/android/AbstractOnServerIT.java

@@ -13,6 +13,7 @@ import android.accounts.OperationCanceledException;
 import android.content.ActivityNotFoundException;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.NetworkOnMainThreadException;
 
 import com.nextcloud.client.account.User;
 import com.nextcloud.client.account.UserAccountManager;
@@ -187,6 +188,11 @@ public abstract class AbstractOnServerIT extends AbstractIT {
 
     public void uploadOCUpload(OCUpload ocUpload, int localBehaviour) {
         ConnectivityService connectivityServiceMock = new ConnectivityService() {
+            @Override
+            public boolean isNetworkAndServerAvailable() throws NetworkOnMainThreadException {
+                return false;
+            }
+
             @Override
             public boolean isConnected() {
                 return false;

+ 17 - 0
app/src/androidTest/java/com/owncloud/android/UploadIT.java

@@ -8,6 +8,8 @@
  */
 package com.owncloud.android;
 
+import android.os.NetworkOnMainThreadException;
+
 import com.nextcloud.client.account.UserAccountManagerImpl;
 import com.nextcloud.client.device.BatteryStatus;
 import com.nextcloud.client.device.PowerManagementService;
@@ -56,6 +58,11 @@ public class UploadIT extends AbstractOnServerIT {
                                   targetContext.getContentResolver());
 
     private ConnectivityService connectivityServiceMock = new ConnectivityService() {
+        @Override
+        public boolean isNetworkAndServerAvailable() throws NetworkOnMainThreadException {
+            return false;
+        }
+
         @Override
         public boolean isConnected() {
             return false;
@@ -274,6 +281,11 @@ public class UploadIT extends AbstractOnServerIT {
     @Test
     public void testUploadOnWifiOnlyButNoWifi() {
         ConnectivityService connectivityServiceMock = new ConnectivityService() {
+            @Override
+            public boolean isNetworkAndServerAvailable() throws NetworkOnMainThreadException {
+                return false;
+            }
+
             @Override
             public boolean isConnected() {
                 return false;
@@ -358,6 +370,11 @@ public class UploadIT extends AbstractOnServerIT {
     @Test
     public void testUploadOnWifiOnlyButMeteredWifi() {
         ConnectivityService connectivityServiceMock = new ConnectivityService() {
+            @Override
+            public boolean isNetworkAndServerAvailable() throws NetworkOnMainThreadException {
+                return false;
+            }
+
             @Override
             public boolean isConnected() {
                 return false;

+ 4 - 0
app/src/androidTest/java/com/owncloud/android/files/services/FileUploaderIT.kt

@@ -34,6 +34,10 @@ abstract class FileUploaderIT : AbstractOnServerIT() {
     private var uploadsStorageManager: UploadsStorageManager? = null
 
     private val connectivityServiceMock: ConnectivityService = object : ConnectivityService {
+        override fun isNetworkAndServerAvailable(): Boolean {
+            return false
+        }
+
         override fun isConnected(): Boolean {
             return false
         }

+ 6 - 5
app/src/androidTest/java/com/owncloud/android/ui/activity/ConflictsResolveActivityIT.java

@@ -62,7 +62,8 @@ public class ConflictsResolveActivityIT extends AbstractIT {
 
         ConflictsResolveActivity sut = activityRule.launchActivity(intent);
 
-        ConflictsResolveDialog dialog = ConflictsResolveDialog.newInstance(existingFile,
+        ConflictsResolveDialog dialog = ConflictsResolveDialog.newInstance(targetContext,
+                                                                           existingFile,
                                                                            newFile,
                                                                            UserAccountManagerImpl
                                                                                .fromContext(targetContext)
@@ -209,7 +210,7 @@ public class ConflictsResolveActivityIT extends AbstractIT {
 
         getInstrumentation().waitForIdleSync();
 
-        onView(withId(R.id.existing_checkbox)).perform(click());
+        onView(withId(R.id.right_checkbox)).perform(click());
 
         DialogFragment dialog = (DialogFragment) sut.getSupportFragmentManager().findFragmentByTag("conflictDialog");
         screenshot(Objects.requireNonNull(dialog.requireDialog().getWindow()).getDecorView());
@@ -255,7 +256,7 @@ public class ConflictsResolveActivityIT extends AbstractIT {
 
         getInstrumentation().waitForIdleSync();
 
-        onView(withId(R.id.new_checkbox)).perform(click());
+        onView(withId(R.id.left_checkbox)).perform(click());
 
         DialogFragment dialog = (DialogFragment) sut.getSupportFragmentManager().findFragmentByTag("conflictDialog");
         screenshot(Objects.requireNonNull(dialog.requireDialog().getWindow()).getDecorView());
@@ -300,8 +301,8 @@ public class ConflictsResolveActivityIT extends AbstractIT {
 
         getInstrumentation().waitForIdleSync();
 
-        onView(withId(R.id.existing_checkbox)).perform(click());
-        onView(withId(R.id.new_checkbox)).perform(click());
+        onView(withId(R.id.right_checkbox)).perform(click());
+        onView(withId(R.id.left_checkbox)).perform(click());
 
         DialogFragment dialog = (DialogFragment) sut.getSupportFragmentManager().findFragmentByTag("conflictDialog");
         screenshot(Objects.requireNonNull(dialog.requireDialog().getWindow()).getDecorView());

+ 4 - 0
app/src/debug/java/com/nextcloud/test/TestActivity.kt

@@ -53,6 +53,10 @@ class TestActivity :
         override fun getConnectivity(): Connectivity {
             return Connectivity.CONNECTED_WIFI
         }
+
+        override fun isNetworkAndServerAvailable(): Boolean {
+            return false
+        }
     }
 
     override fun onCreate(savedInstanceState: Bundle?) {

+ 3 - 0
app/src/main/AndroidManifest.xml

@@ -266,6 +266,9 @@
             </intent-filter>
         </activity>
 
+        <receiver
+            android:name="com.nextcloud.receiver.OfflineOperationActionReceiver"
+            android:exported="false" />
         <receiver
             android:name="com.nextcloud.client.jobs.MediaFoldersDetectionWork$NotificationReceiver"
             android:exported="false" />

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

@@ -11,6 +11,7 @@ import android.content.Context
 import com.nextcloud.client.core.Clock
 import com.nextcloud.client.database.dao.ArbitraryDataDao
 import com.nextcloud.client.database.dao.FileDao
+import com.nextcloud.client.database.dao.OfflineOperationDao
 import dagger.Module
 import dagger.Provides
 import javax.inject.Singleton
@@ -33,4 +34,9 @@ class DatabaseModule {
     fun fileDao(nextcloudDatabase: NextcloudDatabase): FileDao {
         return nextcloudDatabase.fileDao()
     }
+
+    @Provides
+    fun offlineOperationsDao(nextcloudDatabase: NextcloudDatabase): OfflineOperationDao {
+        return nextcloudDatabase.offlineOperationDao()
+    }
 }

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

@@ -16,11 +16,13 @@ import com.nextcloud.client.core.Clock
 import com.nextcloud.client.core.ClockImpl
 import com.nextcloud.client.database.dao.ArbitraryDataDao
 import com.nextcloud.client.database.dao.FileDao
+import com.nextcloud.client.database.dao.OfflineOperationDao
 import com.nextcloud.client.database.entity.ArbitraryDataEntity
 import com.nextcloud.client.database.entity.CapabilityEntity
 import com.nextcloud.client.database.entity.ExternalLinkEntity
 import com.nextcloud.client.database.entity.FileEntity
 import com.nextcloud.client.database.entity.FilesystemEntity
+import com.nextcloud.client.database.entity.OfflineOperationEntity
 import com.nextcloud.client.database.entity.ShareEntity
 import com.nextcloud.client.database.entity.SyncedFolderEntity
 import com.nextcloud.client.database.entity.UploadEntity
@@ -41,7 +43,8 @@ import com.owncloud.android.db.ProviderMeta
         ShareEntity::class,
         SyncedFolderEntity::class,
         UploadEntity::class,
-        VirtualEntity::class
+        VirtualEntity::class,
+        OfflineOperationEntity::class
     ],
     version = ProviderMeta.DB_VERSION,
     autoMigrations = [
@@ -61,7 +64,8 @@ import com.owncloud.android.db.ProviderMeta
         AutoMigration(from = 79, to = 80),
         AutoMigration(from = 80, to = 81),
         AutoMigration(from = 81, to = 82),
-        AutoMigration(from = 82, to = 83)
+        AutoMigration(from = 82, to = 83),
+        AutoMigration(from = 83, to = 84)
     ],
     exportSchema = true
 )
@@ -70,6 +74,7 @@ abstract class NextcloudDatabase : RoomDatabase() {
 
     abstract fun arbitraryDataDao(): ArbitraryDataDao
     abstract fun fileDao(): FileDao
+    abstract fun offlineOperationDao(): OfflineOperationDao
 
     companion object {
         const val FIRST_ROOM_DB_VERSION = 65

+ 39 - 0
app/src/main/java/com/nextcloud/client/database/dao/OfflineOperationDao.kt

@@ -0,0 +1,39 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package com.nextcloud.client.database.dao
+
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Insert
+import androidx.room.Query
+import androidx.room.Update
+import com.nextcloud.client.database.entity.OfflineOperationEntity
+
+@Dao
+interface OfflineOperationDao {
+    @Query("SELECT * FROM offline_operations")
+    fun getAll(): List<OfflineOperationEntity>
+
+    @Insert
+    fun insert(vararg entity: OfflineOperationEntity)
+
+    @Update
+    fun update(entity: OfflineOperationEntity)
+
+    @Delete
+    fun delete(entity: OfflineOperationEntity)
+
+    @Query("DELETE FROM offline_operations WHERE offline_operations_path = :path")
+    fun deleteByPath(path: String)
+
+    @Query("SELECT * FROM offline_operations WHERE offline_operations_path = :path LIMIT 1")
+    fun getByPath(path: String): OfflineOperationEntity?
+
+    @Query("SELECT * FROM offline_operations WHERE offline_operations_parent_oc_file_id = :parentOCFileId")
+    fun getSubDirectoriesByParentOCFileId(parentOCFileId: Long): List<OfflineOperationEntity>
+}

+ 39 - 0
app/src/main/java/com/nextcloud/client/database/entity/OfflineOperationEntity.kt

@@ -0,0 +1,39 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package com.nextcloud.client.database.entity
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import com.nextcloud.model.OfflineOperationType
+import com.owncloud.android.db.ProviderMeta.ProviderTableMeta
+
+@Entity(tableName = ProviderTableMeta.OFFLINE_OPERATION_TABLE_NAME)
+data class OfflineOperationEntity(
+    @PrimaryKey(autoGenerate = true)
+    @ColumnInfo(name = ProviderTableMeta._ID)
+    val id: Int? = null,
+
+    @ColumnInfo(name = ProviderTableMeta.OFFLINE_OPERATION_PARENT_OC_FILE_ID)
+    var parentOCFileId: Long? = null,
+
+    @ColumnInfo(name = ProviderTableMeta.OFFLINE_OPERATION_PARENT_PATH)
+    var parentPath: String? = null,
+
+    @ColumnInfo(name = ProviderTableMeta.OFFLINE_OPERATION_TYPE)
+    var type: OfflineOperationType? = null,
+
+    @ColumnInfo(name = ProviderTableMeta.OFFLINE_OPERATION_PATH)
+    var path: String? = null,
+
+    @ColumnInfo(name = ProviderTableMeta.OFFLINE_OPERATION_FILE_NAME)
+    var filename: String? = null,
+
+    @ColumnInfo(name = ProviderTableMeta.OFFLINE_OPERATION_CREATED_AT)
+    var createdAt: Long? = null
+)

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

@@ -28,6 +28,7 @@ import com.nextcloud.client.core.ClockImpl;
 import com.nextcloud.client.core.ThreadPoolAsyncRunner;
 import com.nextcloud.client.database.dao.ArbitraryDataDao;
 import com.nextcloud.client.device.DeviceInfo;
+import com.nextcloud.client.jobs.operation.FileOperationHelper;
 import com.nextcloud.client.logger.FileLogHandler;
 import com.nextcloud.client.logger.Logger;
 import com.nextcloud.client.logger.LoggerImpl;
@@ -250,6 +251,11 @@ class AppModule {
         return new PassCodeManager(preferences, clock);
     }
 
+    @Provides
+    FileOperationHelper fileOperationHelper(CurrentAccountProvider currentAccountProvider, Context context) {
+        return new FileOperationHelper(currentAccountProvider.getUser(), context, fileDataStorageManager(currentAccountProvider, context));
+    }
+
     @Provides
     @Singleton
     UsersAndGroupsSearchConfig userAndGroupSearchConfig() {

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

@@ -24,6 +24,7 @@ import com.nextcloud.client.onboarding.WhatsNewActivity;
 import com.nextcloud.client.widget.DashboardWidgetConfigurationActivity;
 import com.nextcloud.client.widget.DashboardWidgetProvider;
 import com.nextcloud.client.widget.DashboardWidgetService;
+import com.nextcloud.receiver.NetworkChangeReceiver;
 import com.nextcloud.ui.ChooseAccountDialogFragment;
 import com.nextcloud.ui.ImageDetailFragment;
 import com.nextcloud.ui.SetStatusDialogFragment;
@@ -313,6 +314,9 @@ abstract class ComponentsModule {
     @ContributesAndroidInjector
     abstract BootupBroadcastReceiver bootupBroadcastReceiver();
 
+    @ContributesAndroidInjector
+    abstract NetworkChangeReceiver networkChangeReceiver();
+
     @ContributesAndroidInjector
     abstract NotificationWork.NotificationReceiver notificationWorkBroadcastReceiver();
 

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

@@ -23,6 +23,7 @@ import com.nextcloud.client.documentscan.GeneratePDFUseCase
 import com.nextcloud.client.documentscan.GeneratePdfFromImagesWork
 import com.nextcloud.client.integrations.deck.DeckApi
 import com.nextcloud.client.jobs.download.FileDownloadWorker
+import com.nextcloud.client.jobs.offlineOperations.OfflineOperationsWorker
 import com.nextcloud.client.jobs.upload.FileUploadWorker
 import com.nextcloud.client.logger.Logger
 import com.nextcloud.client.network.ConnectivityService
@@ -95,12 +96,17 @@ class BackgroundJobFactory @Inject constructor(
                 GeneratePdfFromImagesWork::class -> createPDFGenerateWork(context, workerParameters)
                 HealthStatusWork::class -> createHealthStatusWork(context, workerParameters)
                 TestJob::class -> createTestJob(context, workerParameters)
+                OfflineOperationsWorker::class -> createOfflineOperationsWorker(context, workerParameters)
                 InternalTwoWaySyncWork::class -> createInternalTwoWaySyncWork(context, workerParameters)
                 else -> null // caller falls back to default factory
             }
         }
     }
 
+    private fun createOfflineOperationsWorker(context: Context, params: WorkerParameters): ListenableWorker {
+        return OfflineOperationsWorker(accountManager.user, context, connectivityService, viewThemeUtils.get(), params)
+    }
+
     private fun createFilesExportWork(context: Context, params: WorkerParameters): ListenableWorker {
         return FilesExportWork(
             context,

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

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

+ 51 - 3
app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt

@@ -26,6 +26,7 @@ import com.nextcloud.client.core.Clock
 import com.nextcloud.client.di.Injectable
 import com.nextcloud.client.documentscan.GeneratePdfFromImagesWork
 import com.nextcloud.client.jobs.download.FileDownloadWorker
+import com.nextcloud.client.jobs.offlineOperations.OfflineOperationsWorker
 import com.nextcloud.client.jobs.upload.FileUploadWorker
 import com.nextcloud.client.preferences.AppPreferences
 import com.nextcloud.utils.extensions.isWorkRunning
@@ -80,7 +81,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_OFFLINE_OPERATIONS = "offline_operations"
+        const val JOB_PERIODIC_OFFLINE_OPERATIONS = "periodic_offline_operations"
         const val JOB_PERIODIC_HEALTH_STATUS = "periodic_health_status"
         const val JOB_IMMEDIATE_HEALTH_STATUS = "immediate_health_status"
 
@@ -98,6 +100,7 @@ internal class BackgroundJobManagerImpl(
         const val NOT_SET_VALUE = "not set"
         const val PERIODIC_BACKUP_INTERVAL_MINUTES = 24 * 60L
         const val DEFAULT_PERIODIC_JOB_INTERVAL_MINUTES = 15L
+        const val OFFLINE_OPERATIONS_PERIODIC_JOB_INTERVAL_MINUTES = 5L
         const val DEFAULT_IMMEDIATE_JOB_DELAY_SEC = 3L
 
         private const val KEEP_LOG_MILLIS = 1000 * 60 * 60 * 24 * 3L
@@ -198,13 +201,15 @@ internal class BackgroundJobManagerImpl(
     private fun oneTimeRequestBuilder(
         jobClass: KClass<out ListenableWorker>,
         jobName: String,
-        user: User? = null
+        user: User? = null,
+        constraints: Constraints = Constraints.Builder().build()
     ): OneTimeWorkRequest.Builder {
         val builder = OneTimeWorkRequest.Builder(jobClass.java)
             .addTag(TAG_ALL)
             .addTag(formatNameTag(jobName, user))
             .addTag(formatTimeTag(clock.currentTime))
             .addTag(formatClassTag(jobClass))
+            .setConstraints(constraints)
         user?.let { builder.addTag(formatUserTag(it)) }
         return builder
     }
@@ -217,7 +222,8 @@ internal class BackgroundJobManagerImpl(
         jobName: String,
         intervalMins: Long = DEFAULT_PERIODIC_JOB_INTERVAL_MINUTES,
         flexIntervalMins: Long = DEFAULT_PERIODIC_JOB_INTERVAL_MINUTES,
-        user: User? = null
+        user: User? = null,
+        constraints: Constraints = Constraints.Builder().build()
     ): PeriodicWorkRequest.Builder {
         val builder = PeriodicWorkRequest.Builder(
             jobClass.java,
@@ -230,6 +236,7 @@ internal class BackgroundJobManagerImpl(
             .addTag(formatNameTag(jobName, user))
             .addTag(formatTimeTag(clock.currentTime))
             .addTag(formatClassTag(jobClass))
+            .setConstraints(constraints)
         user?.let { builder.addTag(formatUserTag(it)) }
         return builder
     }
@@ -411,6 +418,47 @@ internal class BackgroundJobManagerImpl(
             workManager.isWorkRunning(JOB_IMMEDIATE_FILES_SYNC + "_" + syncedFolderID)
     }
 
+    override fun startPeriodicallyOfflineOperation() {
+        val inputData = Data.Builder()
+            .putString(OfflineOperationsWorker.JOB_NAME, JOB_PERIODIC_OFFLINE_OPERATIONS)
+            .build()
+
+        val request = periodicRequestBuilder(
+            jobClass = OfflineOperationsWorker::class,
+            jobName = JOB_PERIODIC_OFFLINE_OPERATIONS,
+            intervalMins = OFFLINE_OPERATIONS_PERIODIC_JOB_INTERVAL_MINUTES
+        )
+            .setInputData(inputData)
+            .build()
+
+        workManager.enqueueUniquePeriodicWork(
+            JOB_PERIODIC_OFFLINE_OPERATIONS,
+            ExistingPeriodicWorkPolicy.UPDATE,
+            request
+        )
+    }
+
+    override fun startOfflineOperations() {
+        val inputData = Data.Builder()
+            .putString(OfflineOperationsWorker.JOB_NAME, JOB_OFFLINE_OPERATIONS)
+            .build()
+
+        val constraints = Constraints.Builder()
+            .setRequiredNetworkType(NetworkType.CONNECTED)
+            .build()
+
+        val request =
+            oneTimeRequestBuilder(OfflineOperationsWorker::class, JOB_OFFLINE_OPERATIONS, constraints = constraints)
+                .setInputData(inputData)
+                .build()
+
+        workManager.enqueueUniqueWork(
+            JOB_OFFLINE_OPERATIONS,
+            ExistingWorkPolicy.REPLACE,
+            request
+        )
+    }
+
     override fun schedulePeriodicFilesSyncJob(syncedFolderID: Long) {
         val arguments = Data.Builder()
             .putLong(FilesSyncWork.SYNCED_FOLDER_ID, syncedFolderID)

+ 6 - 2
app/src/main/java/com/nextcloud/client/jobs/notification/WorkerNotificationManager.kt

@@ -30,12 +30,16 @@ open class WorkerNotificationManager(
     val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
 
     var notificationBuilder: NotificationCompat.Builder =
-        NotificationUtils.newNotificationBuilder(context, "WorkerNotificationManager", viewThemeUtils).apply {
+        NotificationUtils.newNotificationBuilder(
+            context,
+            NotificationUtils.NOTIFICATION_CHANNEL_BACKGROUND_OPERATIONS,
+            viewThemeUtils
+        ).apply {
             setTicker(context.getString(tickerId))
             setSmallIcon(R.drawable.notification_icon)
             setLargeIcon(BitmapFactory.decodeResource(context.resources, R.drawable.notification_icon))
             setStyle(NotificationCompat.BigTextStyle())
-            setPriority(NotificationCompat.PRIORITY_LOW)
+            priority = NotificationCompat.PRIORITY_LOW
 
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                 setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_DOWNLOAD)

+ 135 - 0
app/src/main/java/com/nextcloud/client/jobs/offlineOperations/OfflineOperationsNotificationManager.kt

@@ -0,0 +1,135 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package com.nextcloud.client.jobs.offlineOperations
+
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import androidx.core.app.NotificationCompat
+import com.nextcloud.client.account.User
+import com.nextcloud.client.database.entity.OfflineOperationEntity
+import com.nextcloud.client.jobs.notification.WorkerNotificationManager
+import com.nextcloud.receiver.OfflineOperationActionReceiver
+import com.nextcloud.utils.extensions.getErrorMessage
+import com.owncloud.android.R
+import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.lib.common.operations.RemoteOperation
+import com.owncloud.android.lib.common.operations.RemoteOperationResult
+import com.owncloud.android.ui.activity.ConflictsResolveActivity
+import com.owncloud.android.utils.theme.ViewThemeUtils
+
+class OfflineOperationsNotificationManager(private val context: Context, viewThemeUtils: ViewThemeUtils) :
+    WorkerNotificationManager(
+        ID,
+        context,
+        viewThemeUtils,
+        R.string.offline_operations_worker_notification_manager_ticker
+    ) {
+
+    companion object {
+        private const val ID = 121
+        private const val ERROR_ID = 122
+    }
+
+    @Suppress("MagicNumber")
+    fun start() {
+        notificationBuilder.run {
+            setContentTitle(context.getString(R.string.offline_operations_worker_notification_start_text))
+            setProgress(100, 0, false)
+        }
+
+        showNotification()
+    }
+
+    @Suppress("MagicNumber")
+    fun update(totalOperationSize: Int, currentOperationIndex: Int, filename: String) {
+        val title = if (totalOperationSize > 1) {
+            String.format(
+                context.getString(R.string.offline_operations_worker_progress_text),
+                currentOperationIndex,
+                totalOperationSize,
+                filename
+            )
+        } else {
+            filename
+        }
+
+        val progress = (currentOperationIndex * 100) / totalOperationSize
+
+        notificationBuilder.run {
+            setContentTitle(title)
+            setProgress(100, progress, false)
+        }
+
+        showNotification()
+    }
+
+    fun showNewNotification(result: RemoteOperationResult<*>, operation: RemoteOperation<*>) {
+        val reason = (result to operation).getErrorMessage()
+        val text = context.getString(R.string.offline_operations_worker_notification_error_text, reason)
+
+        notificationBuilder.run {
+            setContentTitle(text)
+            setOngoing(false)
+            notificationManager.notify(ERROR_ID, this.build())
+        }
+    }
+
+    fun showConflictResolveNotification(file: OCFile, entity: OfflineOperationEntity?, user: User) {
+        val path = entity?.path
+        val id = entity?.id
+
+        if (path == null || id == null) {
+            return
+        }
+
+        val resolveConflictIntent = ConflictsResolveActivity.createIntent(file, path, context)
+        val resolveConflictPendingIntent = PendingIntent.getActivity(
+            context,
+            id,
+            resolveConflictIntent,
+            PendingIntent.FLAG_IMMUTABLE
+        )
+        val resolveConflictAction = NotificationCompat.Action(
+            R.drawable.ic_cloud_upload,
+            context.getString(R.string.upload_list_resolve_conflict),
+            resolveConflictPendingIntent
+        )
+
+        val deleteIntent = Intent(context, OfflineOperationActionReceiver::class.java).apply {
+            putExtra(OfflineOperationActionReceiver.FILE_PATH, path)
+            putExtra(OfflineOperationActionReceiver.USER, user)
+        }
+        val deletePendingIntent =
+            PendingIntent.getBroadcast(context, 0, deleteIntent, PendingIntent.FLAG_IMMUTABLE)
+        val deleteAction = NotificationCompat.Action(
+            R.drawable.ic_delete,
+            context.getString(R.string.offline_operations_worker_notification_delete_offline_folder),
+            deletePendingIntent
+        )
+
+        val title = context.getString(
+            R.string.offline_operations_worker_notification_conflict_text,
+            file.fileName
+        )
+
+        notificationBuilder
+            .clearActions()
+            .setContentTitle(title)
+            .setContentIntent(resolveConflictPendingIntent)
+            .addAction(deleteAction)
+            .addAction(resolveConflictAction)
+
+        notificationManager.notify(id, notificationBuilder.build())
+    }
+
+    fun dismissNotification(id: Int?) {
+        if (id == null) return
+        notificationManager.cancel(id)
+    }
+}

+ 161 - 0
app/src/main/java/com/nextcloud/client/jobs/offlineOperations/OfflineOperationsWorker.kt

@@ -0,0 +1,161 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package com.nextcloud.client.jobs.offlineOperations
+
+import android.content.Context
+import androidx.work.CoroutineWorker
+import androidx.work.WorkerParameters
+import com.nextcloud.client.account.User
+import com.nextcloud.client.database.entity.OfflineOperationEntity
+import com.nextcloud.client.jobs.offlineOperations.repository.OfflineOperationsRepository
+import com.nextcloud.client.network.ClientFactoryImpl
+import com.nextcloud.client.network.ConnectivityService
+import com.nextcloud.model.OfflineOperationType
+import com.nextcloud.model.WorkerState
+import com.nextcloud.model.WorkerStateLiveData
+import com.owncloud.android.datamodel.FileDataStorageManager
+import com.owncloud.android.lib.common.OwnCloudClient
+import com.owncloud.android.lib.common.operations.RemoteOperation
+import com.owncloud.android.lib.common.operations.RemoteOperationResult
+import com.owncloud.android.lib.common.utils.Log_OC
+import com.owncloud.android.operations.CreateFolderOperation
+import com.owncloud.android.utils.theme.ViewThemeUtils
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.withContext
+
+class OfflineOperationsWorker(
+    private val user: User,
+    private val context: Context,
+    private val connectivityService: ConnectivityService,
+    viewThemeUtils: ViewThemeUtils,
+    params: WorkerParameters
+) : CoroutineWorker(context, params) {
+
+    companion object {
+        private val TAG = OfflineOperationsWorker::class.java.simpleName
+        const val JOB_NAME = "JOB_NAME"
+    }
+
+    private val fileDataStorageManager = FileDataStorageManager(user, context.contentResolver)
+    private val clientFactory = ClientFactoryImpl(context)
+    private val notificationManager = OfflineOperationsNotificationManager(context, viewThemeUtils)
+    private var repository = OfflineOperationsRepository(fileDataStorageManager)
+
+    @Suppress("TooGenericExceptionCaught")
+    override suspend fun doWork(): Result = coroutineScope {
+        val jobName = inputData.getString(JOB_NAME)
+        Log_OC.d(
+            TAG,
+            "$jobName -----------------------------------\n" +
+                "OfflineOperationsWorker started" +
+                "\n-----------------------------------"
+        )
+
+        if (!connectivityService.isNetworkAndServerAvailable()) {
+            Log_OC.d(TAG, "OfflineOperationsWorker cancelled, no internet connection")
+            return@coroutineScope Result.retry()
+        }
+
+        val client = clientFactory.create(user)
+        notificationManager.start()
+
+        var operations = fileDataStorageManager.offlineOperationDao.getAll()
+        val totalOperations = operations.size
+        var currentSuccessfulOperationIndex = 0
+
+        return@coroutineScope try {
+            while (operations.isNotEmpty()) {
+                val operation = operations.first()
+                val result = executeOperation(operation, client)
+                val isSuccess = handleResult(
+                    operation,
+                    totalOperations,
+                    currentSuccessfulOperationIndex,
+                    result?.first,
+                    result?.second
+                )
+
+                operations = if (isSuccess) {
+                    currentSuccessfulOperationIndex++
+                    fileDataStorageManager.offlineOperationDao.getAll()
+                } else {
+                    operations.filter { it != operation }
+                }
+            }
+
+            Log_OC.d(TAG, "OfflineOperationsWorker successfully completed")
+            notificationManager.dismissNotification()
+            WorkerStateLiveData.instance().setWorkState(WorkerState.OfflineOperationsCompleted)
+            Result.success()
+        } catch (e: Exception) {
+            Log_OC.d(TAG, "OfflineOperationsWorker terminated: $e")
+            Result.failure()
+        }
+    }
+
+    @Suppress("Deprecation")
+    private suspend fun executeOperation(
+        operation: OfflineOperationEntity,
+        client: OwnCloudClient
+    ): Pair<RemoteOperationResult<*>?, RemoteOperation<*>?>? {
+        return when (operation.type) {
+            OfflineOperationType.CreateFolder -> {
+                if (operation.parentPath != null) {
+                    val createFolderOperation = withContext(Dispatchers.IO) {
+                        CreateFolderOperation(
+                            operation.path,
+                            user,
+                            context,
+                            fileDataStorageManager
+                        )
+                    }
+                    createFolderOperation.execute(client) to createFolderOperation
+                } else {
+                    Log_OC.d(TAG, "CreateFolder operation incomplete, missing parentPath")
+                    null
+                }
+            }
+
+            else -> {
+                Log_OC.d(TAG, "Unsupported operation type: ${operation.type}")
+                null
+            }
+        }
+    }
+
+    private fun handleResult(
+        operation: OfflineOperationEntity,
+        totalOperations: Int,
+        currentSuccessfulOperationIndex: Int,
+        result: RemoteOperationResult<*>?,
+        remoteOperation: RemoteOperation<*>?
+    ): Boolean {
+        if (result == null) {
+            Log_OC.d(TAG, "Operation not completed, result is null")
+            return false
+        }
+
+        val logMessage = if (result.isSuccess) "Operation completed" else "Operation failed"
+        Log_OC.d(TAG, "$logMessage path: ${operation.path}, type: ${operation.type}")
+
+        if (result.isSuccess) {
+            repository.updateNextOperations(operation)
+            fileDataStorageManager.offlineOperationDao.delete(operation)
+            notificationManager.update(totalOperations, currentSuccessfulOperationIndex, operation.filename ?: "")
+        } else {
+            val excludedErrorCodes = listOf(RemoteOperationResult.ResultCode.FOLDER_ALREADY_EXISTS)
+
+            if (remoteOperation != null && !excludedErrorCodes.contains(result.code)) {
+                notificationManager.showNewNotification(result, remoteOperation)
+            }
+        }
+
+        return result.isSuccess
+    }
+}

+ 91 - 0
app/src/main/java/com/nextcloud/client/jobs/offlineOperations/repository/OfflineOperationsRepository.kt

@@ -0,0 +1,91 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package com.nextcloud.client.jobs.offlineOperations.repository
+
+import com.nextcloud.client.database.entity.OfflineOperationEntity
+import com.owncloud.android.datamodel.FileDataStorageManager
+import com.owncloud.android.datamodel.OCFile
+
+class OfflineOperationsRepository(
+    private val fileDataStorageManager: FileDataStorageManager
+) : OfflineOperationsRepositoryType {
+
+    private val dao = fileDataStorageManager.offlineOperationDao
+    private val pathSeparator = '/'
+
+    @Suppress("NestedBlockDepth")
+    override fun getAllSubdirectories(fileId: Long): List<OfflineOperationEntity> {
+        val result = mutableListOf<OfflineOperationEntity>()
+        val queue = ArrayDeque<Long>()
+        queue.add(fileId)
+        val processedIds = mutableSetOf<Long>()
+
+        while (queue.isNotEmpty()) {
+            val currentFileId = queue.removeFirst()
+            if (currentFileId in processedIds || currentFileId == 1L) continue
+
+            processedIds.add(currentFileId)
+
+            val subDirectories = dao.getSubDirectoriesByParentOCFileId(currentFileId)
+            result.addAll(subDirectories)
+
+            subDirectories.forEach {
+                val ocFile = fileDataStorageManager.getFileByDecryptedRemotePath(it.path)
+                ocFile?.fileId?.let { newFileId ->
+                    if (newFileId != 1L && newFileId !in processedIds) {
+                        queue.add(newFileId)
+                    }
+                }
+            }
+        }
+
+        return result
+    }
+
+    override fun deleteOperation(file: OCFile) {
+        getAllSubdirectories(file.fileId).forEach {
+            dao.delete(it)
+        }
+
+        file.decryptedRemotePath?.let {
+            val entity = dao.getByPath(it)
+            entity?.let {
+                dao.delete(entity)
+            }
+        }
+
+        fileDataStorageManager.removeFile(file, true, true)
+    }
+
+    override fun updateNextOperations(operation: OfflineOperationEntity) {
+        val ocFile = fileDataStorageManager.getFileByDecryptedRemotePath(operation.path)
+        val fileId = ocFile?.fileId ?: return
+
+        getAllSubdirectories(fileId)
+            .mapNotNull { nextOperation ->
+                nextOperation.parentOCFileId?.let { parentId ->
+                    fileDataStorageManager.getFileById(parentId)?.let { ocFile ->
+                        ocFile.decryptedRemotePath?.let { updatedPath ->
+                            val newParentPath = ocFile.parentRemotePath
+                            val newPath = updatedPath + nextOperation.filename + pathSeparator
+
+                            if (newParentPath != nextOperation.parentPath || newPath != nextOperation.path) {
+                                nextOperation.apply {
+                                    parentPath = newParentPath
+                                    path = newPath
+                                }
+                            } else {
+                                null
+                            }
+                        }
+                    }
+                }
+            }
+            .forEach { dao.update(it) }
+    }
+}

+ 17 - 0
app/src/main/java/com/nextcloud/client/jobs/offlineOperations/repository/OfflineOperationsRepositoryType.kt

@@ -0,0 +1,17 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package com.nextcloud.client.jobs.offlineOperations.repository
+
+import com.nextcloud.client.database.entity.OfflineOperationEntity
+import com.owncloud.android.datamodel.OCFile
+
+interface OfflineOperationsRepositoryType {
+    fun getAllSubdirectories(fileId: Long): List<OfflineOperationEntity>
+    fun deleteOperation(file: OCFile)
+    fun updateNextOperations(operation: OfflineOperationEntity)
+}

+ 65 - 0
app/src/main/java/com/nextcloud/client/jobs/operation/FileOperationHelper.kt

@@ -0,0 +1,65 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package com.nextcloud.client.jobs.operation
+
+import android.content.Context
+import com.nextcloud.client.account.User
+import com.nextcloud.client.network.ClientFactoryImpl
+import com.nextcloud.utils.extensions.getErrorMessage
+import com.owncloud.android.datamodel.FileDataStorageManager
+import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.lib.common.utils.Log_OC
+import com.owncloud.android.operations.RemoveFileOperation
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.withContext
+
+class FileOperationHelper(
+    private val user: User,
+    private val context: Context,
+    private val fileDataStorageManager: FileDataStorageManager
+) {
+
+    companion object {
+        private val TAG = FileOperationHelper::class.java.simpleName
+    }
+
+    private val clientFactory = ClientFactoryImpl(context)
+    private val client = clientFactory.create(user)
+
+    @Suppress("TooGenericExceptionCaught", "Deprecation")
+    suspend fun removeFile(file: OCFile, onlyLocalCopy: Boolean, inBackground: Boolean): Boolean {
+        return withContext(Dispatchers.IO) {
+            try {
+                val operation = async {
+                    RemoveFileOperation(
+                        file,
+                        onlyLocalCopy,
+                        user,
+                        inBackground,
+                        context,
+                        fileDataStorageManager
+                    )
+                }
+                val operationResult = operation.await()
+                val result = operationResult.execute(client)
+
+                return@withContext if (result.isSuccess) {
+                    true
+                } else {
+                    val reason = (result to operationResult).getErrorMessage()
+                    Log_OC.e(TAG, "Error occurred while removing file: $reason")
+                    false
+                }
+            } catch (e: Exception) {
+                Log_OC.e(TAG, "Error occurred while removing file: $e")
+                false
+            }
+        }
+    }
+}

+ 17 - 0
app/src/main/java/com/nextcloud/client/network/ConnectivityService.java

@@ -6,11 +6,28 @@
  */
 package com.nextcloud.client.network;
 
+import android.os.NetworkOnMainThreadException;
+
 /**
  * This service provides information about current network connectivity
  * and server reachability.
  */
 public interface ConnectivityService {
+    /**
+     * Checks the availability of the server and the device's internet connection.
+     * <p>
+     * This method performs a network request to verify if the server is accessible and
+     * checks if the device has an active internet connection. Due to the network operations involved,
+     * this method should be executed on a background thread to avoid blocking the main thread.
+     * </p>
+     *
+     * @return {@code true} if the server is accessible and the device has an internet connection;
+     *         {@code false} otherwise.
+     *
+     * @throws NetworkOnMainThreadException if this function runs on main thread.
+     */
+    boolean isNetworkAndServerAvailable() throws NetworkOnMainThreadException;
+
     boolean isConnected();
 
     /**

+ 14 - 0
app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.java

@@ -13,6 +13,7 @@ import android.net.ConnectivityManager;
 import android.net.Network;
 import android.net.NetworkCapabilities;
 import android.net.NetworkInfo;
+import android.os.NetworkOnMainThreadException;
 
 import com.nextcloud.client.account.Server;
 import com.nextcloud.client.account.UserAccountManager;
@@ -55,6 +56,19 @@ class ConnectivityServiceImpl implements ConnectivityService {
         this.walledCheckCache = walledCheckCache;
     }
 
+    @Override
+    public boolean isNetworkAndServerAvailable() throws NetworkOnMainThreadException {
+        Network activeNetwork = platformConnectivityManager.getActiveNetwork();
+        NetworkCapabilities networkCapabilities = platformConnectivityManager.getNetworkCapabilities(activeNetwork);
+        boolean hasInternet = networkCapabilities != null && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+
+        if (!hasInternet) {
+            return false;
+        }
+
+        return !isInternetWalled();
+    }
+
     @Override
     public boolean isConnected() {
         Network nw = platformConnectivityManager.getActiveNetwork();

+ 12 - 0
app/src/main/java/com/nextcloud/model/OfflineOperationType.kt

@@ -0,0 +1,12 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package com.nextcloud.model
+
+enum class OfflineOperationType {
+    CreateFolder
+}

+ 1 - 0
app/src/main/java/com/nextcloud/model/WorkerState.kt

@@ -17,4 +17,5 @@ sealed class WorkerState {
     data class DownloadStarted(var user: User?, var currentDownload: DownloadFileOperation?) : WorkerState()
     data class UploadFinished(var currentFile: OCFile?) : WorkerState()
     data class UploadStarted(var user: User?, var uploads: List<OCUpload>) : WorkerState()
+    data object OfflineOperationsCompleted : WorkerState()
 }

+ 38 - 0
app/src/main/java/com/nextcloud/receiver/NetworkChangeReceiver.kt

@@ -0,0 +1,38 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package com.nextcloud.receiver
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import com.nextcloud.client.network.ConnectivityService
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+interface NetworkChangeListener {
+    fun networkAndServerConnectionListener(isNetworkAndServerAvailable: Boolean)
+}
+
+class NetworkChangeReceiver(
+    private val listener: NetworkChangeListener,
+    private val connectivityService: ConnectivityService
+) : BroadcastReceiver() {
+
+    private val scope = CoroutineScope(Dispatchers.IO)
+
+    override fun onReceive(context: Context, intent: Intent?) {
+        scope.launch {
+            val isNetworkAndServerAvailable = connectivityService.isNetworkAndServerAvailable()
+
+            launch(Dispatchers.Main) {
+                listener.networkAndServerConnectionListener(isNetworkAndServerAvailable)
+            }
+        }
+    }
+}

+ 30 - 0
app/src/main/java/com/nextcloud/receiver/OfflineOperationActionReceiver.kt

@@ -0,0 +1,30 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package com.nextcloud.receiver
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import com.nextcloud.client.account.User
+import com.nextcloud.utils.extensions.getParcelableArgument
+import com.owncloud.android.datamodel.FileDataStorageManager
+
+class OfflineOperationActionReceiver : BroadcastReceiver() {
+    companion object {
+        const val FILE_PATH = "FILE_PATH"
+        const val USER = "USER"
+    }
+
+    override fun onReceive(context: Context?, intent: Intent?) {
+        val path = intent?.getStringExtra(FILE_PATH) ?: return
+        val user = intent.getParcelableArgument(USER, User::class.java) ?: return
+        val fileDataStorageManager = FileDataStorageManager(user, context?.contentResolver)
+        fileDataStorageManager.offlineOperationDao.deleteByPath(path)
+        // TODO Update notification
+    }
+}

+ 6 - 2
app/src/main/java/com/nextcloud/ui/fileactions/FileAction.kt

@@ -52,7 +52,10 @@ enum class FileAction(@IdRes val id: Int, @StringRes val title: Int, @DrawableRe
     LOCK_FILE(R.id.action_lock_file, R.string.lock_file, R.drawable.ic_lock),
 
     // Shortcuts
-    PIN_TO_HOMESCREEN(R.id.action_pin_to_homescreen, R.string.pin_home, R.drawable.add_to_home_screen);
+    PIN_TO_HOMESCREEN(R.id.action_pin_to_homescreen, R.string.pin_home, R.drawable.add_to_home_screen),
+
+    // Retry for offline operation
+    RETRY(R.id.action_retry, R.string.retry, R.drawable.ic_retry);
 
     companion object {
         /**
@@ -82,7 +85,8 @@ enum class FileAction(@IdRes val id: Int, @StringRes val title: Int, @DrawableRe
             UNSET_ENCRYPTED,
             SET_AS_WALLPAPER,
             REMOVE_FILE,
-            PIN_TO_HOMESCREEN
+            PIN_TO_HOMESCREEN,
+            RETRY
         )
     }
 }

+ 15 - 0
app/src/main/java/com/nextcloud/utils/date/DateFormatPattern.kt

@@ -0,0 +1,15 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package com.nextcloud.utils.date
+
+enum class DateFormatPattern(val pattern: String) {
+    /**
+     * e.g. 10.11.2024 - 12:44
+     */
+    FullDateWithHours("dd.MM.yyyy - HH:mm")
+}

+ 5 - 1
app/src/main/java/com/nextcloud/utils/extensions/ActivityExtensions.kt

@@ -10,6 +10,10 @@ package com.nextcloud.utils.extensions
 import androidx.appcompat.app.AppCompatActivity
 import androidx.fragment.app.Fragment
 
-fun AppCompatActivity.isDialogFragmentReady(fragment: Fragment): Boolean = isActive() && !fragment.isStateSaved()
+fun AppCompatActivity.isDialogFragmentReady(fragment: Fragment): Boolean = isActive() && !fragment.isStateSaved
 
 fun AppCompatActivity.isActive(): Boolean = !isFinishing && !isDestroyed
+
+fun AppCompatActivity.fragments(): List<Fragment> = supportFragmentManager.fragments
+
+fun AppCompatActivity.lastFragment(): Fragment = fragments().last()

+ 18 - 0
app/src/main/java/com/nextcloud/utils/extensions/DateExtensions.kt

@@ -0,0 +1,18 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package com.nextcloud.utils.extensions
+
+import android.annotation.SuppressLint
+import com.nextcloud.utils.date.DateFormatPattern
+import java.text.SimpleDateFormat
+import java.util.Date
+
+@SuppressLint("SimpleDateFormat")
+fun Date.currentDateRepresentation(formatPattern: DateFormatPattern): String {
+    return SimpleDateFormat(formatPattern.pattern).format(this)
+}

+ 20 - 0
app/src/main/java/com/nextcloud/utils/extensions/ParcableExtensions.kt

@@ -0,0 +1,20 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package com.nextcloud.utils.extensions
+
+import android.os.Parcel
+import android.os.Parcelable
+
+inline fun <reified T : Parcelable> Parcel.readParcelableCompat(classLoader: ClassLoader?): T? {
+    return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
+        readParcelable(classLoader, T::class.java)
+    } else {
+        @Suppress("DEPRECATION")
+        readParcelable(classLoader)
+    }
+}

+ 68 - 0
app/src/main/java/com/nextcloud/utils/extensions/RemoteOperationResultExtensions.kt

@@ -0,0 +1,68 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package com.nextcloud.utils.extensions
+
+import com.nextcloud.client.database.entity.OfflineOperationEntity
+import com.owncloud.android.MainApp
+import com.owncloud.android.R
+import com.owncloud.android.datamodel.FileDataStorageManager
+import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.lib.common.operations.RemoteOperation
+import com.owncloud.android.lib.common.operations.RemoteOperationResult
+import com.owncloud.android.lib.resources.files.model.RemoteFile
+import com.owncloud.android.utils.ErrorMessageAdapter
+import com.owncloud.android.utils.FileStorageUtils
+
+@Suppress("ReturnCount")
+fun Pair<RemoteOperationResult<*>?, RemoteOperation<*>?>?.getErrorMessage(): String {
+    val result = this?.first ?: return MainApp.string(R.string.unexpected_error_occurred)
+    val operation = this.second ?: return MainApp.string(R.string.unexpected_error_occurred)
+    return ErrorMessageAdapter.getErrorCauseMessage(result, operation, MainApp.getAppContext().resources)
+}
+
+@Suppress("NestedBlockDepth")
+fun RemoteOperationResult<*>?.getConflictedRemoteIdsWithOfflineOperations(
+    offlineOperations: List<OfflineOperationEntity>,
+    fileDataStorageManager: FileDataStorageManager
+): HashMap<String, String>? {
+    val newFiles = toOCFile() ?: return null
+    val result = hashMapOf<String, String>()
+
+    offlineOperations.forEach { operation ->
+        newFiles.forEach { file ->
+            if (fileDataStorageManager.fileExists(operation.path) && operation.filename == file.fileName) {
+                operation.path?.let { path ->
+                    result[file.remoteId] = path
+                }
+            }
+        }
+    }
+
+    return result.ifEmpty { null }
+}
+
+@Suppress("Deprecation")
+fun RemoteOperationResult<*>?.toOCFile(): List<OCFile>? {
+    return if (this?.isSuccess == true) {
+        data?.toOCFileList()
+    } else {
+        null
+    }
+}
+
+private fun ArrayList<Any>.toOCFileList(): List<OCFile> {
+    return this.mapNotNull {
+        val remoteFile = (it as? RemoteFile)
+
+        remoteFile?.let {
+            remoteFile.toOCFile()
+        }
+    }
+}
+
+private fun RemoteFile?.toOCFile(): OCFile = FileStorageUtils.fillOCFile(this)

+ 6 - 1
app/src/main/java/com/nextcloud/utils/extensions/ViewExtensions.kt

@@ -1,7 +1,7 @@
 /*
  * Nextcloud - Android Client
  *
- * SPDX-FileCopyrightText: 2023 Alper Ozturk <alper.ozturk@nextcloud.com>
+ * SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
  * SPDX-FileCopyrightText: 2023 Nextcloud GmbH
  * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
  */
@@ -13,6 +13,11 @@ import android.util.TypedValue
 import android.view.View
 import android.view.ViewOutlineProvider
 
+fun View?.setVisibleIf(condition: Boolean) {
+    if (this == null) return
+    visibility = if (condition) View.VISIBLE else View.GONE
+}
+
 fun createRoundedOutline(context: Context, cornerRadiusValue: Float): ViewOutlineProvider {
     return object : ViewOutlineProvider() {
         override fun getOutline(view: View, outline: Outline) {

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

@@ -31,6 +31,7 @@ import android.content.pm.ActivityInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
+import android.net.ConnectivityManager;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Environment;
@@ -59,6 +60,8 @@ import com.nextcloud.client.onboarding.OnboardingService;
 import com.nextcloud.client.preferences.AppPreferences;
 import com.nextcloud.client.preferences.AppPreferencesImpl;
 import com.nextcloud.client.preferences.DarkMode;
+import com.nextcloud.receiver.NetworkChangeListener;
+import com.nextcloud.receiver.NetworkChangeReceiver;
 import com.nextcloud.utils.extensions.ContextExtensionsKt;
 import com.nmc.android.ui.LauncherActivity;
 import com.owncloud.android.authentication.AuthenticatorActivity;
@@ -129,7 +132,7 @@ import static com.owncloud.android.ui.activity.ContactsPreferenceActivity.PREFER
  * Main Application of the project.
  * Contains methods to build the "static" strings. These strings were before constants in different classes.
  */
-public class MainApp extends Application implements HasAndroidInjector {
+public class MainApp extends Application implements HasAndroidInjector, NetworkChangeListener {
     public static final OwnCloudVersion OUTDATED_SERVER_VERSION = NextcloudVersion.nextcloud_26;
     public static final OwnCloudVersion MINIMUM_SUPPORTED_SERVER_VERSION = OwnCloudVersion.nextcloud_17;
 
@@ -204,6 +207,8 @@ public class MainApp extends Application implements HasAndroidInjector {
 
     private static AppComponent appComponent;
 
+    private NetworkChangeReceiver networkChangeReceiver;
+
     /**
      * Temporary hack
      */
@@ -227,6 +232,11 @@ public class MainApp extends Application implements HasAndroidInjector {
         return powerManagementService;
     }
 
+    private void registerNetworkChangeReceiver() {
+        IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
+        registerReceiver(networkChangeReceiver, filter);
+    }
+
     private String getAppProcessName() {
         String processName = "";
         if(Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
@@ -372,9 +382,12 @@ public class MainApp extends Application implements HasAndroidInjector {
             backgroundJobManager.startMediaFoldersDetectionJob();
             backgroundJobManager.schedulePeriodicHealthStatus();
             backgroundJobManager.scheduleInternal2WaySync();
+            backgroundJobManager.startPeriodicallyOfflineOperation();
         }
 
         registerGlobalPassCodeProtection();
+        networkChangeReceiver = new NetworkChangeReceiver(this, connectivityService);
+        registerNetworkChangeReceiver();
     }
 
     private final LifecycleEventObserver lifecycleEventObserver = ((lifecycleOwner, event) -> {
@@ -670,6 +683,10 @@ public class MainApp extends Application implements HasAndroidInjector {
                               R.string.notification_channel_push_name, R.string
                                   .notification_channel_push_description, context, NotificationManager.IMPORTANCE_DEFAULT);
 
+                createChannel(notificationManager, NotificationUtils.NOTIFICATION_CHANNEL_BACKGROUND_OPERATIONS,
+                              R.string.notification_channel_background_operations_name, R.string
+                                  .notification_channel_background_operations_description, context, NotificationManager.IMPORTANCE_DEFAULT);
+
                 createChannel(notificationManager, NotificationUtils.NOTIFICATION_CHANNEL_GENERAL, R.string
                                   .notification_channel_general_name, R.string.notification_channel_general_description,
                               context, NotificationManager.IMPORTANCE_DEFAULT);
@@ -974,4 +991,16 @@ public class MainApp extends Application implements HasAndroidInjector {
             case SYSTEM -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
         }
     }
+
+    @Override
+    public void networkAndServerConnectionListener(boolean isNetworkAndServerAvailable) {
+        if (backgroundJobManager == null) {
+            Log_OC.d(TAG, "Offline operations terminated, backgroundJobManager cannot be null");
+            return;
+        }
+
+        if (isNetworkAndServerAvailable) {
+            backgroundJobManager.startOfflineOperations();
+        }
+    }
 }

+ 119 - 34
app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java

@@ -13,6 +13,7 @@
  */
 package com.owncloud.android.datamodel;
 
+import android.annotation.SuppressLint;
 import android.content.ContentProviderClient;
 import android.content.ContentProviderOperation;
 import android.content.ContentProviderResult;
@@ -34,7 +35,14 @@ import com.google.gson.JsonSyntaxException;
 import com.nextcloud.client.account.User;
 import com.nextcloud.client.database.NextcloudDatabase;
 import com.nextcloud.client.database.dao.FileDao;
+import com.nextcloud.client.database.dao.OfflineOperationDao;
 import com.nextcloud.client.database.entity.FileEntity;
+import com.nextcloud.client.database.entity.OfflineOperationEntity;
+import com.nextcloud.client.jobs.offlineOperations.repository.OfflineOperationsRepository;
+import com.nextcloud.client.jobs.offlineOperations.repository.OfflineOperationsRepositoryType;
+import com.nextcloud.model.OfflineOperationType;
+import com.nextcloud.utils.date.DateFormatPattern;
+import com.nextcloud.utils.extensions.DateExtensionsKt;
 import com.owncloud.android.MainApp;
 import com.owncloud.android.db.ProviderMeta.ProviderTableMeta;
 import com.owncloud.android.lib.common.network.WebdavEntry;
@@ -65,6 +73,7 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Date;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
@@ -94,19 +103,23 @@ public class FileDataStorageManager {
     private final ContentProviderClient contentProviderClient;
     private final User user;
 
+    public final OfflineOperationDao offlineOperationDao = NextcloudDatabase.getInstance(MainApp.getAppContext()).offlineOperationDao();
     private final FileDao fileDao = NextcloudDatabase.getInstance(MainApp.getAppContext()).fileDao();
     private final Gson gson = new Gson();
+    private final OfflineOperationsRepositoryType offlineOperationsRepository;
 
     public FileDataStorageManager(User user, ContentResolver contentResolver) {
         this.contentProviderClient = null;
         this.contentResolver = contentResolver;
         this.user = user;
+        offlineOperationsRepository = new OfflineOperationsRepository(this);
     }
 
     public FileDataStorageManager(User user, ContentProviderClient contentProviderClient) {
         this.contentProviderClient = contentProviderClient;
         this.contentResolver = null;
         this.user = user;
+        offlineOperationsRepository = new OfflineOperationsRepository(this);
     }
 
     /**
@@ -126,6 +139,73 @@ public class FileDataStorageManager {
         return getFileByPath(ProviderTableMeta.FILE_PATH_DECRYPTED, path);
     }
 
+    public OfflineOperationEntity addCreateFolderOfflineOperation(String path, String filename, String parentPath, Long parentOCFileId) {
+        OfflineOperationEntity entity = new OfflineOperationEntity();
+
+        entity.setFilename(filename);
+        entity.setParentOCFileId(parentOCFileId);
+        entity.setPath(path);
+        entity.setParentPath(parentPath);
+        entity.setCreatedAt(System.currentTimeMillis() / 1000L);
+        entity.setType(OfflineOperationType.CreateFolder);
+
+        offlineOperationDao.insert(entity);
+        createPendingDirectory(path);
+
+        return entity;
+    }
+
+    public void createPendingDirectory(String path) {
+        OCFile file = new OCFile(path);
+        file.setMimeType(MimeType.DIRECTORY);
+        saveFileWithParent(file, MainApp.getAppContext());
+    }
+
+    public void deleteOfflineOperation(OCFile file) {
+        offlineOperationsRepository.deleteOperation(file);
+    }
+
+    public void renameCreateFolderOfflineOperation(OCFile file, String newFolderName) {
+        var entity = offlineOperationDao.getByPath(file.getDecryptedRemotePath());
+        if (entity == null) {
+            return;
+        }
+
+        OCFile parentFolder = getFileById(file.getParentId());
+        if (parentFolder == null) {
+            return;
+        }
+
+        String newPath = parentFolder.getDecryptedRemotePath() + newFolderName + OCFile.PATH_SEPARATOR;
+        entity.setPath(newPath);
+        entity.setFilename(newFolderName);
+        offlineOperationDao.update(entity);
+
+        moveLocalFile(file, newPath, parentFolder.getDecryptedRemotePath());
+    }
+
+    @SuppressLint("SimpleDateFormat")
+    public void keepOfflineOperationAndServerFile(OfflineOperationEntity entity, OCFile file) {
+        if (file == null) return;
+
+        String oldFileName = entity.getFilename();
+        if (oldFileName == null) return;
+
+        Long parentOCFileId = entity.getParentOCFileId();
+        if (parentOCFileId == null) return;
+
+        OCFile parentFolder = getFileById(parentOCFileId);
+        if (parentFolder == null) return;
+
+        DateFormatPattern formatPattern = DateFormatPattern.FullDateWithHours;
+        String currentDateTime = DateExtensionsKt.currentDateRepresentation(new Date(), formatPattern);
+
+        String newFolderName = oldFileName + " - " + currentDateTime;
+        String newPath = parentFolder.getDecryptedRemotePath() + newFolderName + OCFile.PATH_SEPARATOR;
+        moveLocalFile(file, newPath, parentFolder.getDecryptedRemotePath());
+        offlineOperationsRepository.updateNextOperations(entity);
+    }
+
     private @Nullable
     OCFile getFileByPath(String type, String path) {
         final boolean shouldUseEncryptedPath = ProviderTableMeta.FILE_PATH.equals(type);
@@ -171,7 +251,9 @@ public class FileDataStorageManager {
         return null;
     }
 
-    public boolean fileExists(long id) { return fileDao.getFileById(id) != null; }
+    public boolean fileExists(long id) {
+        return fileDao.getFileById(id) != null;
+    }
 
     public boolean fileExists(String path) {
         return fileDao.getFileByEncryptedRemotePath(path, user.getAccountName()) != null;
@@ -364,19 +446,19 @@ public class FileDataStorageManager {
     }
 
     public static void clearTempEncryptedFolder(String accountName) {
-        File tempEncryptedFolder =  new File(FileStorageUtils.getTemporalEncryptedFolderPath(accountName));
+        File tempEncryptedFolder = new File(FileStorageUtils.getTemporalEncryptedFolderPath(accountName));
 
         if (!tempEncryptedFolder.exists()) {
-            Log_OC.d(TAG,"tempEncryptedFolder does not exist");
+            Log_OC.d(TAG, "tempEncryptedFolder does not exist");
             return;
         }
 
         try {
             FileUtils.cleanDirectory(tempEncryptedFolder);
 
-            Log_OC.d(TAG,"tempEncryptedFolder cleared");
+            Log_OC.d(TAG, "tempEncryptedFolder cleared");
         } catch (IOException exception) {
-            Log_OC.d(TAG,"Error caught at clearTempEncryptedFolder: " + exception);
+            Log_OC.d(TAG, "Error caught at clearTempEncryptedFolder: " + exception);
         }
     }
 
@@ -409,7 +491,7 @@ public class FileDataStorageManager {
 
     /**
      * Inserts or updates the list of files contained in a given folder.
-     *
+     * <p>
      * CALLER IS RESPONSIBLE FOR GRANTING RIGHT UPDATE OF INFORMATION, NOT THIS METHOD. HERE ONLY DATA CONSISTENCY
      * SHOULD BE GRANTED
      *
@@ -457,7 +539,7 @@ public class FileDataStorageManager {
                 whereArgs[1] = ocFile.getRemotePath();
                 if (ocFile.isFolder()) {
                     operations.add(ContentProviderOperation.newDelete(
-                        ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_DIR, ocFile.getFileId()))
+                            ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_DIR, ocFile.getFileId()))
                                        .withSelection(where, whereArgs).build());
 
                     File localFolder = new File(FileStorageUtils.getDefaultSavePathFor(user.getAccountName(), ocFile));
@@ -466,7 +548,7 @@ public class FileDataStorageManager {
                     }
                 } else {
                     operations.add(ContentProviderOperation.newDelete(
-                        ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_FILE, ocFile.getFileId()))
+                            ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_FILE, ocFile.getFileId()))
                                        .withSelection(where, whereArgs).build());
 
                     if (ocFile.isDown()) {
@@ -526,6 +608,7 @@ public class FileDataStorageManager {
 
     /**
      * Returns a {@link ContentValues} filled with values that are common to both files and folders
+     *
      * @see #createContentValuesForFile(OCFile)
      * @see #createContentValuesForFolder(OCFile)
      */
@@ -566,6 +649,7 @@ public class FileDataStorageManager {
 
     /**
      * Returns a {@link ContentValues} filled with values for a folder
+     *
      * @see #createContentValuesForFile(OCFile)
      * @see #createContentValuesBase(OCFile)
      */
@@ -577,6 +661,7 @@ public class FileDataStorageManager {
 
     /**
      * Returns a {@link ContentValues} filled with values for a file
+     *
      * @see #createContentValuesForFolder(OCFile)
      * @see #createContentValuesBase(OCFile)
      */
@@ -748,7 +833,7 @@ public class FileDataStorageManager {
 
     /**
      * Updates database and file system for a file or folder that was moved to a different location.
-     *
+     * <p>
      * TODO explore better (faster) implementations TODO throw exceptions up !
      */
     public void moveLocalFile(OCFile ocFile, String targetPath, String targetParentPath) {
@@ -773,7 +858,7 @@ public class FileDataStorageManager {
 
             int lengthOfOldPath = oldPath.length();
             int lengthOfOldStoragePath = defaultSavePath.length() + lengthOfOldPath;
-            for (FileEntity fileEntity: fileEntities) {
+            for (FileEntity fileEntity : fileEntities) {
                 ContentValues contentValues = new ContentValues(); // keep construction in the loop
                 OCFile childFile = createFileInstance(fileEntity);
                 contentValues.put(
@@ -876,8 +961,8 @@ public class FileDataStorageManager {
     }
 
     /**
-     * This method does not require {@link FileDataStorageManager} being initialized
-     * with any specific user. Migration can be performed with {@link com.nextcloud.client.account.AnonymousUser}.
+     * This method does not require {@link FileDataStorageManager} being initialized with any specific user. Migration
+     * can be performed with {@link com.nextcloud.client.account.AnonymousUser}.
      */
     public void migrateStoredFiles(String sourcePath, String destinationPath)
         throws RemoteException, OperationApplicationException {
@@ -909,7 +994,7 @@ public class FileDataStorageManager {
                 ContentValues cv = new ContentValues();
                 fileId[0] = String.valueOf(cursor.getLong(cursor.getColumnIndexOrThrow(ProviderTableMeta._ID)));
                 String oldFileStoragePath =
-                        cursor.getString(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_STORAGE_PATH));
+                    cursor.getString(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_STORAGE_PATH));
 
                 if (oldFileStoragePath.startsWith(sourcePath)) {
 
@@ -940,7 +1025,7 @@ public class FileDataStorageManager {
         List<OCFile> folderContent = new ArrayList<>();
 
         List<FileEntity> files = fileDao.getFolderContent(parentId);
-        for (FileEntity fileEntity: files) {
+        for (FileEntity fileEntity : files) {
             OCFile child = createFileInstance(fileEntity);
             if (!onlyOnDevice || child.existsOnDevice()) {
                 folderContent.add(child);
@@ -1203,7 +1288,7 @@ public class FileDataStorageManager {
                            + ProviderTableMeta.OCSHARES_ACCOUNT_OWNER + "=?",
                        new String[]{value, user.getAccountName()},
                        null
-                );
+                      );
         } else {
             try {
                 cursor = getContentProviderClient().query(
@@ -1212,7 +1297,7 @@ public class FileDataStorageManager {
                     key + AND + ProviderTableMeta.OCSHARES_ACCOUNT_OWNER + "=?",
                     new String[]{value, user.getAccountName()},
                     null
-                );
+                                                         );
             } catch (RemoteException e) {
                 Log_OC.w(TAG, "Could not get details, assuming share does not exist: " + e.getMessage());
                 cursor = null;
@@ -1451,7 +1536,7 @@ public class FileDataStorageManager {
                     ContentProviderOperation.newInsert(ProviderTableMeta.CONTENT_URI_SHARE)
                         .withValues(contentValues)
                         .build()
-                );
+                              );
             }
         }
 
@@ -1611,7 +1696,7 @@ public class FileDataStorageManager {
                     ContentProviderOperation.newDelete(ProviderTableMeta.CONTENT_URI_SHARE).
                         withSelection(where, whereArgs).
                         build()
-                );
+                                      );
             }
         }
         return preparedOperations;
@@ -1629,7 +1714,7 @@ public class FileDataStorageManager {
                 .newDelete(ProviderTableMeta.CONTENT_URI_SHARE)
                 .withSelection(where, whereArgs)
                 .build()
-        );
+                              );
 
         return preparedOperations;
 
@@ -1780,7 +1865,7 @@ public class FileDataStorageManager {
                 cv,
                 ProviderTableMeta._ID + "=?",
                 new String[]{String.valueOf(ocFile.getFileId())}
-            );
+                                                 );
         } else {
             try {
                 updated = getContentProviderClient().update(
@@ -1788,7 +1873,7 @@ public class FileDataStorageManager {
                     cv,
                     ProviderTableMeta._ID + "=?",
                     new String[]{String.valueOf(ocFile.getFileId())}
-                );
+                                                           );
             } catch (RemoteException e) {
                 Log_OC.e(TAG, "Failed saving conflict in database " + e.getMessage(), e);
             }
@@ -1822,7 +1907,7 @@ public class FileDataStorageManager {
                             cv,
                             stringBuilder.toString(),
                             ancestorIds.toArray(new String[]{})
-                        );
+                                                             );
                     } else {
                         try {
                             updated = getContentProviderClient().update(
@@ -1830,7 +1915,7 @@ public class FileDataStorageManager {
                                 cv,
                                 stringBuilder.toString(),
                                 ancestorIds.toArray(new String[]{})
-                            );
+                                                                       );
                         } catch (RemoteException e) {
                             Log_OC.e(TAG, "Failed saving conflict in database " + e.getMessage(), e);
                         }
@@ -1862,7 +1947,7 @@ public class FileDataStorageManager {
                             whereForDescencentsInConflict,
                             new String[]{user.getAccountName(), parentPath + '%'},
                             null
-                        );
+                                                                          );
                     } else {
                         try {
                             descendentsInConflict = getContentProviderClient().query(
@@ -1871,7 +1956,7 @@ public class FileDataStorageManager {
                                 whereForDescencentsInConflict,
                                 new String[]{user.getAccountName(), parentPath + "%"},
                                 null
-                            );
+                                                                                    );
                         } catch (RemoteException e) {
                             Log_OC.e(TAG, "Failed querying for descendents in conflict " + e.getMessage(), e);
                         }
@@ -1886,7 +1971,7 @@ public class FileDataStorageManager {
                                 ProviderTableMeta.FILE_ACCOUNT_OWNER + AND +
                                     ProviderTableMeta.FILE_PATH + "=?",
                                 new String[]{user.getAccountName(), parentPath}
-                            );
+                                                                 );
                         } else {
                             try {
                                 updated = getContentProviderClient().update(
@@ -1895,7 +1980,7 @@ public class FileDataStorageManager {
                                     ProviderTableMeta.FILE_ACCOUNT_OWNER + AND +
                                         ProviderTableMeta.FILE_PATH + "=?"
                                     , new String[]{user.getAccountName(), parentPath}
-                                );
+                                                                           );
                             } catch (RemoteException e) {
                                 Log_OC.e(TAG, "Failed saving conflict in database " + e.getMessage(), e);
                             }
@@ -2285,7 +2370,7 @@ public class FileDataStorageManager {
         Log_OC.d(TAG, "getGalleryItems - query complete, list size: " + fileEntities.size());
 
         List<OCFile> files = new ArrayList<>(fileEntities.size());
-        for (FileEntity fileEntity: fileEntities) {
+        for (FileEntity fileEntity : fileEntities) {
             files.add(createFileInstance(fileEntity));
         }
 
@@ -2306,7 +2391,7 @@ public class FileDataStorageManager {
                     ProviderTableMeta.VIRTUAL_TYPE + "=?",
                     new String[]{String.valueOf(type)},
                     null
-                );
+                                                    );
             } catch (RemoteException e) {
                 Log_OC.e(TAG, e.getMessage(), e);
                 return ocFiles;
@@ -2318,7 +2403,7 @@ public class FileDataStorageManager {
                 ProviderTableMeta.VIRTUAL_TYPE + "=?",
                 new String[]{String.valueOf(type)},
                 null
-            );
+                                          );
         }
 
         if (c != null) {
@@ -2397,7 +2482,7 @@ public class FileDataStorageManager {
         List<OCFile> files = getAllFilesRecursivelyInsideFolder(folder);
         List<Pair<String, String>> decryptedFileNamesAndEncryptedRemotePaths = getDecryptedFileNamesAndEncryptedRemotePaths(files);
 
-        String decryptedFileName = decryptedRemotePath.substring( decryptedRemotePath.lastIndexOf('/') + 1);
+        String decryptedFileName = decryptedRemotePath.substring(decryptedRemotePath.lastIndexOf('/') + 1);
 
         for (Pair<String, String> item : decryptedFileNamesAndEncryptedRemotePaths) {
             if (item.getFirst().equals(decryptedFileName)) {
@@ -2434,7 +2519,7 @@ public class FileDataStorageManager {
         List<FileEntity> fileEntities = fileDao.getAllFiles(user.getAccountName());
         List<OCFile> folderContent = new ArrayList<>(fileEntities.size());
 
-        for (FileEntity fileEntity: fileEntities) {
+        for (FileEntity fileEntity : fileEntities) {
             folderContent.add(createFileInstance(fileEntity));
         }
 
@@ -2483,7 +2568,7 @@ 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());
@@ -2494,7 +2579,7 @@ public class FileDataStorageManager {
 
         return files;
     }
-    
+
     public boolean isPartOfInternalTwoWaySync(OCFile file) {
         if (file.isInternalFolderSync()) {
             return true;

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

@@ -35,6 +35,7 @@ import com.owncloud.android.utils.MimeType;
 import java.io.File;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Objects;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -775,6 +776,26 @@ public class OCFile implements Parcelable, Comparable<OCFile>, ServerFileInterfa
         return this.downloading;
     }
 
+    public boolean isRootDirectory() {
+        return decryptedRemotePath.equals(ROOT_PATH);
+    }
+
+    public boolean isOfflineOperation() {
+        return getRemoteId() == null;
+    }
+
+    public String getOfflineOperationParentPath() {
+        if (isOfflineOperation()) {
+            if (Objects.equals(remotePath, OCFile.PATH_SEPARATOR)) {
+                return OCFile.PATH_SEPARATOR;
+            } else {
+                return null;
+            }
+        } else {
+            return getDecryptedRemotePath();
+        }
+    }
+
     public String getEtagInConflict() {
         return this.etagInConflict;
     }

+ 76 - 66
app/src/main/java/com/owncloud/android/db/ProviderMeta.java

@@ -25,13 +25,14 @@ import java.util.List;
  */
 public class ProviderMeta {
     public static final String DB_NAME = "filelist";
-    public static final int DB_VERSION = 83;
+    public static final int DB_VERSION = 84;
 
     private ProviderMeta() {
         // No instance
     }
 
     static public class ProviderTableMeta implements BaseColumns {
+        public static final String OFFLINE_OPERATION_TABLE_NAME = "offline_operations";
         public static final String FILE_TABLE_NAME = "filelist";
         public static final String OCSHARES_TABLE_NAME = "ocshares";
         public static final String CAPABILITIES_TABLE_NAME = "capabilities";
@@ -47,24 +48,24 @@ public class ProviderMeta {
         private static final String CONTENT_PREFIX = "content://";
 
         public static final Uri CONTENT_URI = Uri.parse(CONTENT_PREFIX
-                + MainApp.getAuthority() + "/");
+                                                            + MainApp.getAuthority() + "/");
         public static final Uri CONTENT_URI_FILE = Uri.parse(CONTENT_PREFIX
-                + MainApp.getAuthority() + "/file");
+                                                                 + MainApp.getAuthority() + "/file");
         public static final Uri CONTENT_URI_DIR = Uri.parse(CONTENT_PREFIX
-                + MainApp.getAuthority() + "/dir");
+                                                                + MainApp.getAuthority() + "/dir");
         public static final Uri CONTENT_URI_SHARE = Uri.parse(CONTENT_PREFIX
-                + MainApp.getAuthority() + "/shares");
+                                                                  + MainApp.getAuthority() + "/shares");
         public static final Uri CONTENT_URI_CAPABILITIES = Uri.parse(CONTENT_PREFIX
-                + MainApp.getAuthority() + "/capabilities");
+                                                                         + MainApp.getAuthority() + "/capabilities");
         public static final Uri CONTENT_URI_UPLOADS = Uri.parse(CONTENT_PREFIX
-                + MainApp.getAuthority() + "/uploads");
+                                                                    + MainApp.getAuthority() + "/uploads");
         public static final Uri CONTENT_URI_SYNCED_FOLDERS = Uri.parse(CONTENT_PREFIX
-                + MainApp.getAuthority() + "/synced_folders");
+                                                                           + MainApp.getAuthority() + "/synced_folders");
         public static final Uri CONTENT_URI_EXTERNAL_LINKS = Uri.parse(CONTENT_PREFIX
-                + MainApp.getAuthority() + "/external_links");
+                                                                           + MainApp.getAuthority() + "/external_links");
         public static final Uri CONTENT_URI_VIRTUAL = Uri.parse(CONTENT_PREFIX + MainApp.getAuthority() + "/virtual");
         public static final Uri CONTENT_URI_FILESYSTEM = Uri.parse(CONTENT_PREFIX
-                + MainApp.getAuthority() + "/filesystem");
+                                                                       + MainApp.getAuthority() + "/filesystem");
 
 
         public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.owncloud.file";
@@ -124,58 +125,58 @@ public class ProviderMeta {
         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,
-                FILE_PARENT,
-                FILE_NAME,
-                FILE_ENCRYPTED_NAME,
-                FILE_CREATION,
-                FILE_MODIFIED,
-                FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA,
-                FILE_CONTENT_LENGTH,
-                FILE_CONTENT_TYPE,
-                FILE_STORAGE_PATH,
-                FILE_PATH,
-                FILE_PATH_DECRYPTED,
-                FILE_ACCOUNT_OWNER,
-                FILE_LAST_SYNC_DATE,
-                FILE_LAST_SYNC_DATE_FOR_DATA,
-                FILE_KEEP_IN_SYNC,
-                FILE_ETAG,
-                FILE_ETAG_ON_SERVER,
-                FILE_SHARED_VIA_LINK,
-                FILE_SHARED_WITH_SHAREE,
-                FILE_PERMISSIONS,
-                FILE_REMOTE_ID,
-                FILE_LOCAL_ID,
-                FILE_UPDATE_THUMBNAIL,
-                FILE_IS_DOWNLOADING,
-                FILE_ETAG_IN_CONFLICT,
-                FILE_FAVORITE,
-                FILE_HIDDEN,
-                FILE_IS_ENCRYPTED,
-                FILE_MOUNT_TYPE,
-                FILE_HAS_PREVIEW,
-                FILE_UNREAD_COMMENTS_COUNT,
-                FILE_OWNER_ID,
-                FILE_OWNER_DISPLAY_NAME,
-                FILE_NOTE,
-                FILE_SHAREES,
-                FILE_RICH_WORKSPACE,
-                FILE_LOCKED,
-                FILE_LOCK_TYPE,
-                FILE_LOCK_OWNER,
-                FILE_LOCK_OWNER_DISPLAY_NAME,
-                FILE_LOCK_OWNER_EDITOR,
-                FILE_LOCK_TIMESTAMP,
-                FILE_LOCK_TIMEOUT,
-                FILE_LOCK_TOKEN,
-                FILE_METADATA_SIZE,
-                FILE_METADATA_LIVE_PHOTO,
-                FILE_E2E_COUNTER,
-                FILE_TAGS,
-                FILE_METADATA_GPS,
-                FILE_INTERNAL_TWO_WAY_SYNC_TIMESTAMP,
-                FILE_INTERNAL_TWO_WAY_SYNC_RESULT));
+            _ID,
+            FILE_PARENT,
+            FILE_NAME,
+            FILE_ENCRYPTED_NAME,
+            FILE_CREATION,
+            FILE_MODIFIED,
+            FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA,
+            FILE_CONTENT_LENGTH,
+            FILE_CONTENT_TYPE,
+            FILE_STORAGE_PATH,
+            FILE_PATH,
+            FILE_PATH_DECRYPTED,
+            FILE_ACCOUNT_OWNER,
+            FILE_LAST_SYNC_DATE,
+            FILE_LAST_SYNC_DATE_FOR_DATA,
+            FILE_KEEP_IN_SYNC,
+            FILE_ETAG,
+            FILE_ETAG_ON_SERVER,
+            FILE_SHARED_VIA_LINK,
+            FILE_SHARED_WITH_SHAREE,
+            FILE_PERMISSIONS,
+            FILE_REMOTE_ID,
+            FILE_LOCAL_ID,
+            FILE_UPDATE_THUMBNAIL,
+            FILE_IS_DOWNLOADING,
+            FILE_ETAG_IN_CONFLICT,
+            FILE_FAVORITE,
+            FILE_HIDDEN,
+            FILE_IS_ENCRYPTED,
+            FILE_MOUNT_TYPE,
+            FILE_HAS_PREVIEW,
+            FILE_UNREAD_COMMENTS_COUNT,
+            FILE_OWNER_ID,
+            FILE_OWNER_DISPLAY_NAME,
+            FILE_NOTE,
+            FILE_SHAREES,
+            FILE_RICH_WORKSPACE,
+            FILE_LOCKED,
+            FILE_LOCK_TYPE,
+            FILE_LOCK_OWNER,
+            FILE_LOCK_OWNER_DISPLAY_NAME,
+            FILE_LOCK_OWNER_EDITOR,
+            FILE_LOCK_TIMESTAMP,
+            FILE_LOCK_TIMEOUT,
+            FILE_LOCK_TOKEN,
+            FILE_METADATA_SIZE,
+            FILE_METADATA_LIVE_PHOTO,
+            FILE_E2E_COUNTER,
+            FILE_TAGS,
+            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
@@ -200,7 +201,7 @@ public class ProviderMeta {
         public static final String OCSHARES_SHARE_LABEL = "share_label";
 
         public static final String OCSHARES_DEFAULT_SORT_ORDER = OCSHARES_FILE_SOURCE
-                + " collate nocase asc";
+            + " collate nocase asc";
 
         // Columns of capabilities table
         public static final String CAPABILITIES_ACCOUNT_NAME = "account";
@@ -217,11 +218,11 @@ public class ProviderMeta {
         public static final String CAPABILITIES_SHARING_PUBLIC_ASK_FOR_OPTIONAL_PASSWORD =
             "sharing_public_ask_for_optional_password";
         public static final String CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_ENABLED =
-                "sharing_public_expire_date_enabled";
+            "sharing_public_expire_date_enabled";
         public static final String CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_DAYS =
-                "sharing_public_expire_date_days";
+            "sharing_public_expire_date_days";
         public static final String CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_ENFORCED =
-                "sharing_public_expire_date_enforced";
+            "sharing_public_expire_date_enforced";
         public static final String CAPABILITIES_SHARING_PUBLIC_SEND_MAIL = "sharing_public_send_mail";
         public static final String CAPABILITIES_SHARING_PUBLIC_UPLOAD = "sharing_public_upload";
         public static final String CAPABILITIES_SHARING_USER_SEND_MAIL = "sharing_user_send_mail";
@@ -286,6 +287,15 @@ public class ProviderMeta {
         public static final String UPLOADS_IS_WIFI_ONLY = "is_wifi_only";
         public static final String UPLOADS_FOLDER_UNLOCK_TOKEN = "folder_unlock_token";
 
+        // Columns of offline operation table
+        public static final String OFFLINE_OPERATION_PARENT_OC_FILE_ID = "offline_operations_parent_oc_file_id";
+        public static final String OFFLINE_OPERATION_PARENT_PATH = "offline_operations_parent_path";
+        public static final String OFFLINE_OPERATION_TYPE = "offline_operations_type";
+        public static final String OFFLINE_OPERATION_PATH = "offline_operations_path";
+        public static final String OFFLINE_OPERATION_CREATED_AT = "offline_operations_created_at";
+        public static final String OFFLINE_OPERATION_FILE_NAME = "offline_operations_file_name";
+
+
         // Columns of synced folder table
         public static final String SYNCED_FOLDER_LOCAL_PATH = "local_path";
         public static final String SYNCED_FOLDER_REMOTE_PATH = "remote_path";

+ 7 - 0
app/src/main/java/com/owncloud/android/files/FileMenuFilter.java

@@ -169,6 +169,7 @@ public class FileMenuFilter {
         filterLock(toHide, fileLockingEnabled);
         filterUnlock(toHide, fileLockingEnabled);
         filterPinToHome(toHide);
+        filterRetry(toHide);
 
         return toHide;
     }
@@ -260,6 +261,12 @@ public class FileMenuFilter {
         }
     }
 
+    private void filterRetry(List<Integer> toHide) {
+        if (!files.iterator().next().isOfflineOperation()) {
+            toHide.add(R.id.action_retry);
+        }
+    }
+
     private void filterEdit(
         List<Integer> toHide,
         OCCapability capability

+ 54 - 36
app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java

@@ -16,6 +16,7 @@ import com.google.gson.Gson;
 import com.nextcloud.android.lib.resources.directediting.DirectEditingObtainRemoteOperation;
 import com.nextcloud.client.account.User;
 import com.nextcloud.common.NextcloudClient;
+import com.nextcloud.utils.extensions.RemoteOperationResultExtensionsKt;
 import com.owncloud.android.datamodel.ArbitraryDataProvider;
 import com.owncloud.android.datamodel.ArbitraryDataProviderImpl;
 import com.owncloud.android.datamodel.FileDataStorageManager;
@@ -42,6 +43,7 @@ import com.owncloud.android.lib.resources.status.E2EVersion;
 import com.owncloud.android.lib.resources.users.GetPredefinedStatusesRemoteOperation;
 import com.owncloud.android.lib.resources.users.PredefinedStatus;
 import com.owncloud.android.syncadapter.FileSyncAdapter;
+import com.owncloud.android.ui.activity.FileDisplayActivity;
 import com.owncloud.android.utils.DataHolderUtil;
 import com.owncloud.android.utils.EncryptionUtils;
 import com.owncloud.android.utils.FileStorageUtils;
@@ -80,7 +82,7 @@ public class RefreshFolderOperation extends RemoteOperation {
     /**
      * Time stamp for the synchronization process in progress
      */
-    private long mCurrentSyncTime;
+    private final long mCurrentSyncTime;
 
     /**
      * Remote folder to synchronize
@@ -90,17 +92,17 @@ public class RefreshFolderOperation extends RemoteOperation {
     /**
      * Access to the local database
      */
-    private FileDataStorageManager mStorageManager;
+    private final FileDataStorageManager fileDataStorageManager;
 
     /**
      * Account where the file to synchronize belongs
      */
-    private User user;
+    private final User user;
 
     /**
      * Android context; necessary to send requests to the download service
      */
-    private Context mContext;
+    private final Context mContext;
 
     /**
      * Files and folders contained in the synchronized folder after a successful operation
@@ -121,12 +123,12 @@ public class RefreshFolderOperation extends RemoteOperation {
      * Map of remote and local paths to files that where locally stored in a location out of the ownCloud folder and
      * couldn't be copied automatically into it
      **/
-    private Map<String, String> mForgottenLocalFiles;
+    private final Map<String, String> mForgottenLocalFiles;
 
     /**
      * 'True' means that this operation is part of a full account synchronization
      */
-    private boolean mSyncFullAccount;
+    private final boolean mSyncFullAccount;
 
     /**
      * 'True' means that the remote folder changed and should be fetched
@@ -136,14 +138,14 @@ public class RefreshFolderOperation extends RemoteOperation {
     /**
      * 'True' means that Etag will be ignored
      */
-    private boolean mIgnoreETag;
+    private final boolean mIgnoreETag;
 
     /**
      * 'True' means that no share and no capabilities will be updated
      */
-    private boolean mOnlyFileMetadata;
+    private final boolean mOnlyFileMetadata;
 
-    private List<SynchronizeFileOperation> mFilesToSyncContents;
+    private final List<SynchronizeFileOperation> mFilesToSyncContents;
     // this will be used for every file when 'folder synchronization' replaces 'folder download'
 
 
@@ -169,7 +171,7 @@ public class RefreshFolderOperation extends RemoteOperation {
         mLocalFolder = folder;
         mCurrentSyncTime = currentSyncTime;
         mSyncFullAccount = syncFullAccount;
-        mStorageManager = dataStorageManager;
+        fileDataStorageManager = dataStorageManager;
         this.user = user;
         mContext = context;
         mForgottenLocalFiles = new HashMap<>();
@@ -190,7 +192,7 @@ public class RefreshFolderOperation extends RemoteOperation {
         mLocalFolder = folder;
         mCurrentSyncTime = currentSyncTime;
         mSyncFullAccount = syncFullAccount;
-        mStorageManager = dataStorageManager;
+        fileDataStorageManager = dataStorageManager;
         this.user = user;
         mContext = context;
         mForgottenLocalFiles = new HashMap<>();
@@ -246,7 +248,7 @@ public class RefreshFolderOperation extends RemoteOperation {
                 // TODO catch IllegalStateException, show properly to user
                 result = fetchAndSyncRemoteFolder(client);
             } else {
-                mChildren = mStorageManager.getFolderContent(mLocalFolder, false);
+                mChildren = fileDataStorageManager.getFolderContent(mLocalFolder, false);
             }
 
             if (result.isSuccess()) {
@@ -257,13 +259,13 @@ public class RefreshFolderOperation extends RemoteOperation {
             }
 
             mLocalFolder.setLastSyncDateForData(System.currentTimeMillis());
-            mStorageManager.saveFile(mLocalFolder);
+            fileDataStorageManager.saveFile(mLocalFolder);
         }
 
+        checkFolderConflictData(result);
+
         if (!mSyncFullAccount && mRemoteFolderChanged) {
-            sendLocalBroadcast(
-                EVENT_SINGLE_FOLDER_CONTENTS_SYNCED, mLocalFolder.getRemotePath(), result
-                              );
+            sendLocalBroadcast(EVENT_SINGLE_FOLDER_CONTENTS_SYNCED, mLocalFolder.getRemotePath(), result);
         }
 
         if (result.isSuccess() && !mSyncFullAccount && !mOnlyFileMetadata) {
@@ -271,13 +273,29 @@ public class RefreshFolderOperation extends RemoteOperation {
         }
 
         if (!mSyncFullAccount) {
-            sendLocalBroadcast(
-                EVENT_SINGLE_FOLDER_SHARES_SYNCED, mLocalFolder.getRemotePath(), result
-                              );
+            sendLocalBroadcast(EVENT_SINGLE_FOLDER_SHARES_SYNCED, mLocalFolder.getRemotePath(), result);
         }
 
         return result;
+    }
+
+    private static HashMap<String, String> lastConflictData = new HashMap<>();
+
+    private void checkFolderConflictData(RemoteOperationResult result) {
+        var offlineOperations = fileDataStorageManager.offlineOperationDao.getAll();
+        if (offlineOperations.isEmpty()) return;
+
+        var conflictData = RemoteOperationResultExtensionsKt.getConflictedRemoteIdsWithOfflineOperations(result, offlineOperations, fileDataStorageManager);
+        if (conflictData != null && !conflictData.equals(lastConflictData)) {
+            lastConflictData = new HashMap<>(conflictData);
+            sendFolderSyncConflictEventBroadcast(conflictData);
+        }
+    }
 
+    private void sendFolderSyncConflictEventBroadcast(HashMap<String, String> conflictData) {
+        Intent intent = new Intent(FileDisplayActivity.FOLDER_SYNC_CONFLICT);
+        intent.putExtra(FileDisplayActivity.FOLDER_SYNC_CONFLICT_ARG_REMOTE_IDS_TO_OPERATION_PATHS, conflictData);
+        LocalBroadcastManager.getInstance(mContext).sendBroadcast(intent);
     }
 
     private void updateOCVersion(OwnCloudClient client) {
@@ -293,7 +311,7 @@ public class RefreshFolderOperation extends RemoteOperation {
         try {
             NextcloudClient nextcloudClient = OwnCloudClientFactory.createNextcloudClient(user, mContext);
 
-            RemoteOperationResult<UserInfo> result = new GetUserProfileOperation(mStorageManager).execute(nextcloudClient);
+            RemoteOperationResult<UserInfo> result = new GetUserProfileOperation(fileDataStorageManager).execute(nextcloudClient);
             if (!result.isSuccess()) {
                 Log_OC.w(TAG, "Couldn't update user profile from server");
             } else {
@@ -309,9 +327,9 @@ public class RefreshFolderOperation extends RemoteOperation {
         String oldDirectEditingEtag = arbitraryDataProvider.getValue(user,
                                                                      ArbitraryDataProvider.DIRECT_EDITING_ETAG);
 
-        RemoteOperationResult result = new GetCapabilitiesOperation(mStorageManager).execute(mContext);
+        RemoteOperationResult result = new GetCapabilitiesOperation(fileDataStorageManager).execute(mContext);
         if (result.isSuccess()) {
-            String newDirectEditingEtag = mStorageManager.getCapability(user.getAccountName()).getDirectEditingEtag();
+            String newDirectEditingEtag = fileDataStorageManager.getCapability(user.getAccountName()).getDirectEditingEtag();
 
             if (!oldDirectEditingEtag.equalsIgnoreCase(newDirectEditingEtag)) {
                 updateDirectEditing(arbitraryDataProvider, newDirectEditingEtag);
@@ -430,13 +448,13 @@ public class RefreshFolderOperation extends RemoteOperation {
     }
 
     private void removeLocalFolder() {
-        if (mStorageManager.fileExists(mLocalFolder.getFileId())) {
+        if (fileDataStorageManager.fileExists(mLocalFolder.getFileId())) {
             String currentSavePath = FileStorageUtils.getSavePath(user.getAccountName());
-            mStorageManager.removeFolder(
+            fileDataStorageManager.removeFolder(
                 mLocalFolder,
                 true,
                 mLocalFolder.isDown() && mLocalFolder.getStoragePath().startsWith(currentSavePath)
-                                        );
+                                               );
         }
     }
 
@@ -451,7 +469,7 @@ public class RefreshFolderOperation extends RemoteOperation {
      */
     private void synchronizeData(List<Object> folderAndFiles) {
         // get 'fresh data' from the database
-        mLocalFolder = mStorageManager.getFileByPath(mLocalFolder.getRemotePath());
+        mLocalFolder = fileDataStorageManager.getFileByPath(mLocalFolder.getRemotePath());
 
         if (mLocalFolder == null) {
             Log_OC.d(TAG,"mLocalFolder cannot be null");
@@ -469,7 +487,7 @@ public class RefreshFolderOperation extends RemoteOperation {
         mFilesToSyncContents.clear();
 
         // if local folder is encrypted, download fresh metadata
-        boolean encryptedAncestor = FileStorageUtils.checkEncryptionStatus(mLocalFolder, mStorageManager);
+        boolean encryptedAncestor = FileStorageUtils.checkEncryptionStatus(mLocalFolder, fileDataStorageManager);
         mLocalFolder.setEncrypted(encryptedAncestor);
 
         // update permission
@@ -505,11 +523,11 @@ public class RefreshFolderOperation extends RemoteOperation {
         if (object instanceof DecryptedFolderMetadataFileV1) {
             e2EVersion = E2EVersion.V1_2;
             localFilesMap = prefillLocalFilesMap((DecryptedFolderMetadataFileV1) object,
-                                                 mStorageManager.getFolderContent(mLocalFolder, false));
+                                                 fileDataStorageManager.getFolderContent(mLocalFolder, false));
         } else {
             e2EVersion = E2EVersion.V2_0;
             localFilesMap = prefillLocalFilesMap((DecryptedFolderMetadataFile) object,
-                                                 mStorageManager.getFolderContent(mLocalFolder, false));
+                                                 fileDataStorageManager.getFolderContent(mLocalFolder, false));
 
             // update counter
             if (object != null) {
@@ -537,7 +555,7 @@ public class RefreshFolderOperation extends RemoteOperation {
 
             // TODO better implementation is needed
             if (localFile == null) {
-                localFile = mStorageManager.getFileByPath(updatedFile.getRemotePath());
+                localFile = fileDataStorageManager.getFileByPath(updatedFile.getRemotePath());
             }
 
             // add to updatedFile data about LOCAL STATE (not existing in server)
@@ -556,11 +574,11 @@ public class RefreshFolderOperation extends RemoteOperation {
 
             // update file name for encrypted files
             if (e2EVersion == E2EVersion.V1_2) {
-                updateFileNameForEncryptedFileV1(mStorageManager,
+                updateFileNameForEncryptedFileV1(fileDataStorageManager,
                                                  (DecryptedFolderMetadataFileV1) object,
                                                  updatedFile);
             } else {
-                updateFileNameForEncryptedFile(mStorageManager,
+                updateFileNameForEncryptedFile(fileDataStorageManager,
                                                (DecryptedFolderMetadataFile) object,
                                                updatedFile);
                 if (localFile != null) {
@@ -579,15 +597,15 @@ public class RefreshFolderOperation extends RemoteOperation {
         // save updated contents in local database
         // update file name for encrypted files
         if (e2EVersion == E2EVersion.V1_2) {
-            updateFileNameForEncryptedFileV1(mStorageManager,
+            updateFileNameForEncryptedFileV1(fileDataStorageManager,
                                              (DecryptedFolderMetadataFileV1) object,
                                              mLocalFolder);
         } else {
-            updateFileNameForEncryptedFile(mStorageManager,
+            updateFileNameForEncryptedFile(fileDataStorageManager,
                                            (DecryptedFolderMetadataFile) object,
                                            mLocalFolder);
         }
-        mStorageManager.saveFolder(remoteFolder, updatedFiles, localFilesMap.values());
+        fileDataStorageManager.saveFolder(remoteFolder, updatedFiles, localFilesMap.values());
 
         mChildren = updatedFiles;
     }
@@ -817,7 +835,7 @@ public class RefreshFolderOperation extends RemoteOperation {
                     shares.add(share);
                 }
             }
-            mStorageManager.saveSharesInFolder(shares, mLocalFolder);
+            fileDataStorageManager.saveSharesInFolder(shares, mLocalFolder);
         }
 
         return result;

+ 175 - 71
app/src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.kt

@@ -9,12 +9,18 @@
  */
 package com.owncloud.android.ui.activity
 
+import android.annotation.SuppressLint
 import android.content.Context
 import android.content.Intent
 import android.os.Bundle
 import android.widget.Toast
+import androidx.fragment.app.FragmentTransaction
+import androidx.lifecycle.lifecycleScope
 import com.nextcloud.client.account.User
+import com.nextcloud.client.database.entity.OfflineOperationEntity
 import com.nextcloud.client.jobs.download.FileDownloadHelper
+import com.nextcloud.client.jobs.offlineOperations.OfflineOperationsNotificationManager
+import com.nextcloud.client.jobs.operation.FileOperationHelper
 import com.nextcloud.client.jobs.upload.FileUploadHelper
 import com.nextcloud.client.jobs.upload.FileUploadWorker
 import com.nextcloud.client.jobs.upload.UploadNotificationManager
@@ -22,7 +28,6 @@ import com.nextcloud.model.HTTPStatusCodes
 import com.nextcloud.utils.extensions.getParcelableArgument
 import com.nextcloud.utils.extensions.logFileSize
 import com.owncloud.android.R
-import com.owncloud.android.datamodel.FileDataStorageManager
 import com.owncloud.android.datamodel.OCFile
 import com.owncloud.android.datamodel.UploadsStorageManager
 import com.owncloud.android.db.OCUpload
@@ -34,24 +39,26 @@ import com.owncloud.android.ui.dialog.ConflictsResolveDialog
 import com.owncloud.android.ui.dialog.ConflictsResolveDialog.Decision
 import com.owncloud.android.ui.dialog.ConflictsResolveDialog.OnConflictDecisionMadeListener
 import com.owncloud.android.utils.FileStorageUtils
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
 import javax.inject.Inject
 
 /**
  * Wrapper activity which will be launched if keep-in-sync file will be modified by external application.
  */
 class ConflictsResolveActivity : FileActivity(), OnConflictDecisionMadeListener {
-    @JvmField
     @Inject
-    var uploadsStorageManager: UploadsStorageManager? = null
+    lateinit var uploadsStorageManager: UploadsStorageManager
 
-    @JvmField
     @Inject
-    var fileStorageManager: FileDataStorageManager? = null
+    lateinit var fileOperationHelper: FileOperationHelper
 
     private var conflictUploadId: Long = 0
+    private var offlineOperationPath: String? = null
     private var existingFile: OCFile? = null
     private var newFile: OCFile? = null
     private var localBehaviour = FileUploadWorker.LOCAL_BEHAVIOUR_FORGET
+    private lateinit var offlineOperationNotificationManager: OfflineOperationsNotificationManager
 
     @JvmField
     var listener: OnConflictDecisionMadeListener? = null
@@ -61,7 +68,7 @@ class ConflictsResolveActivity : FileActivity(), OnConflictDecisionMadeListener
 
         getArguments(savedInstanceState)
 
-        val upload = uploadsStorageManager?.getUploadById(conflictUploadId)
+        val upload = uploadsStorageManager.getUploadById(conflictUploadId)
         if (upload != null) {
             localBehaviour = upload.localAction
         }
@@ -69,6 +76,7 @@ class ConflictsResolveActivity : FileActivity(), OnConflictDecisionMadeListener
         // new file was modified locally in file system
         newFile = file
         setupOnConflictDecisionMadeListener(upload)
+        offlineOperationNotificationManager = OfflineOperationsNotificationManager(this, viewThemeUtils)
     }
 
     private fun getArguments(savedInstanceState: Bundle?) {
@@ -76,7 +84,9 @@ class ConflictsResolveActivity : FileActivity(), OnConflictDecisionMadeListener
             conflictUploadId = savedInstanceState.getLong(EXTRA_CONFLICT_UPLOAD_ID)
             existingFile = savedInstanceState.getParcelableArgument(EXTRA_EXISTING_FILE, OCFile::class.java)
             localBehaviour = savedInstanceState.getInt(EXTRA_LOCAL_BEHAVIOUR)
+            offlineOperationPath = savedInstanceState.getString(EXTRA_OFFLINE_OPERATION_PATH)
         } else {
+            offlineOperationPath = intent.getStringExtra(EXTRA_OFFLINE_OPERATION_PATH)
             conflictUploadId = intent.getLongExtra(EXTRA_CONFLICT_UPLOAD_ID, -1)
             existingFile = intent.getParcelableArgument(EXTRA_EXISTING_FILE, OCFile::class.java)
             localBehaviour = intent.getIntExtra(EXTRA_LOCAL_BEHAVIOUR, localBehaviour)
@@ -85,69 +95,121 @@ class ConflictsResolveActivity : FileActivity(), OnConflictDecisionMadeListener
 
     private fun setupOnConflictDecisionMadeListener(upload: OCUpload?) {
         listener = OnConflictDecisionMadeListener { decision: Decision? ->
-            val file = newFile // local file got changed, so either upload it or replace it again by server
+
+            // local file got changed, so either upload it or replace it again by server
+            val file = newFile
+
             // version
             val user = user.orElseThrow { RuntimeException() }
+
+            val offlineOperation = if (offlineOperationPath != null) {
+                fileDataStorageManager.offlineOperationDao.getByPath(offlineOperationPath!!)
+            } else {
+                null
+            }
+
             when (decision) {
-                Decision.CANCEL -> {}
-                Decision.KEEP_LOCAL -> {
-                    upload?.let {
-                        FileUploadHelper.instance().removeFileUpload(it.remotePath, it.accountName)
-                    }
-                    FileUploadHelper.instance().uploadUpdatedFile(
-                        user,
-                        arrayOf(file),
-                        localBehaviour,
-                        NameCollisionPolicy.OVERWRITE
-                    )
-                }
+                Decision.KEEP_LOCAL -> keepLocal(file, upload, user)
+                Decision.KEEP_BOTH -> keepBoth(file, upload, user)
+                Decision.KEEP_SERVER -> keepServer(file, upload)
+                Decision.KEEP_OFFLINE_FOLDER -> keepOfflineFolder(newFile, offlineOperation)
+                Decision.KEEP_SERVER_FOLDER -> keepServerFile(offlineOperation)
+                Decision.KEEP_BOTH_FOLDER -> keepBothFolder(offlineOperation, newFile)
+                else -> Unit
+            }
 
-                Decision.KEEP_BOTH -> {
-                    upload?.let {
-                        FileUploadHelper.instance().removeFileUpload(it.remotePath, it.accountName)
-                    }
-                    FileUploadHelper.instance().uploadUpdatedFile(
-                        user,
-                        arrayOf(file),
-                        localBehaviour,
-                        NameCollisionPolicy.RENAME
-                    )
-                }
+            finish()
+        }
+    }
 
-                Decision.KEEP_SERVER -> {
-                    if (!shouldDeleteLocal()) {
-                        // Overwrite local file
-                        file?.let {
-                            FileDownloadHelper.instance().downloadFile(
-                                getUser().orElseThrow { RuntimeException() },
-                                file,
-                                conflictUploadId = conflictUploadId
-                            )
-                        }
-                    }
+    private fun keepBothFolder(offlineOperation: OfflineOperationEntity?, serverFile: OCFile?) {
+        offlineOperation ?: return
+        fileDataStorageManager.keepOfflineOperationAndServerFile(offlineOperation, serverFile)
+        backgroundJobManager.startOfflineOperations()
+        offlineOperationNotificationManager.dismissNotification(offlineOperation.id)
+    }
 
-                    upload?.let {
-                        FileUploadHelper.instance().removeFileUpload(it.remotePath, it.accountName)
+    private fun keepServerFile(offlineOperation: OfflineOperationEntity?) {
+        offlineOperation ?: return
+        fileDataStorageManager.offlineOperationDao.delete(offlineOperation)
 
-                        UploadNotificationManager(
-                            applicationContext,
-                            viewThemeUtils
-                        ).dismissOldErrorNotification(it.remotePath, it.localPath)
-                    }
+        val id = offlineOperation.id ?: return
+        offlineOperationNotificationManager.dismissNotification(id)
+    }
+
+    private fun keepOfflineFolder(serverFile: OCFile?, offlineOperation: OfflineOperationEntity?) {
+        serverFile ?: return
+        offlineOperation ?: return
+
+        lifecycleScope.launch(Dispatchers.IO) {
+            val isSuccess = fileOperationHelper.removeFile(serverFile, false, false)
+            if (isSuccess) {
+                backgroundJobManager.startOfflineOperations()
+
+                launch(Dispatchers.Main) {
+                    offlineOperationNotificationManager.dismissNotification(offlineOperation.id)
                 }
+            }
+        }
+    }
+
+    private fun keepLocal(file: OCFile?, upload: OCUpload?, user: User) {
+        upload?.let {
+            FileUploadHelper.instance().removeFileUpload(it.remotePath, it.accountName)
+        }
+
+        FileUploadHelper.instance().uploadUpdatedFile(
+            user,
+            arrayOf(file),
+            localBehaviour,
+            NameCollisionPolicy.OVERWRITE
+        )
+    }
+
+    private fun keepBoth(file: OCFile?, upload: OCUpload?, user: User) {
+        upload?.let {
+            FileUploadHelper.instance().removeFileUpload(it.remotePath, it.accountName)
+        }
 
-                else -> {}
+        FileUploadHelper.instance().uploadUpdatedFile(
+            user,
+            arrayOf(file),
+            localBehaviour,
+            NameCollisionPolicy.RENAME
+        )
+    }
+
+    private fun keepServer(file: OCFile?, upload: OCUpload?) {
+        if (!shouldDeleteLocal()) {
+            // Overwrite local file
+            file?.let {
+                FileDownloadHelper.instance().downloadFile(
+                    user.orElseThrow { RuntimeException() },
+                    file,
+                    conflictUploadId = conflictUploadId
+                )
             }
-            finish()
+        }
+
+        upload?.let {
+            FileUploadHelper.instance().removeFileUpload(it.remotePath, it.accountName)
+
+            UploadNotificationManager(
+                applicationContext,
+                viewThemeUtils
+            ).dismissOldErrorNotification(it.remotePath, it.localPath)
         }
     }
 
     override fun onSaveInstanceState(outState: Bundle) {
         super.onSaveInstanceState(outState)
         existingFile.logFileSize(TAG)
-        outState.putLong(EXTRA_CONFLICT_UPLOAD_ID, conflictUploadId)
-        outState.putParcelable(EXTRA_EXISTING_FILE, existingFile)
-        outState.putInt(EXTRA_LOCAL_BEHAVIOUR, localBehaviour)
+
+        outState.run {
+            putLong(EXTRA_CONFLICT_UPLOAD_ID, conflictUploadId)
+            putParcelable(EXTRA_EXISTING_FILE, existingFile)
+            putInt(EXTRA_LOCAL_BEHAVIOUR, localBehaviour)
+        }
     }
 
     override fun conflictDecisionMade(decision: Decision?) {
@@ -157,23 +219,46 @@ class ConflictsResolveActivity : FileActivity(), OnConflictDecisionMadeListener
     @Suppress("ReturnCount")
     override fun onStart() {
         super.onStart()
+
         if (account == null) {
             finish()
             return
         }
+
         if (newFile == null) {
             Log_OC.e(TAG, "No file received")
             finish()
             return
         }
+
+        offlineOperationPath?.let { path ->
+            newFile?.let { ocFile ->
+                val offlineOperation = fileDataStorageManager.offlineOperationDao.getByPath(path)
+
+                if (offlineOperation == null) {
+                    showErrorAndFinish()
+                    return
+                }
+
+                val (ft, _) = prepareDialog()
+                val dialog = ConflictsResolveDialog.newInstance(
+                    this,
+                    offlineOperation,
+                    ocFile
+                )
+                dialog.show(ft, "conflictDialog")
+                return
+            }
+        }
+
         if (existingFile == null) {
-            val remotePath = fileStorageManager?.retrieveRemotePathConsideringEncryption(newFile) ?: return
+            val remotePath = fileDataStorageManager.retrieveRemotePathConsideringEncryption(newFile) ?: return
             val operation = ReadFileRemoteOperation(remotePath)
 
             @Suppress("TooGenericExceptionCaught")
-            Thread {
+            lifecycleScope.launch(Dispatchers.IO) {
                 try {
-                    val result = operation.execute(account, this)
+                    val result = operation.execute(account, this@ConflictsResolveActivity)
                     if (result.isSuccess) {
                         existingFile = FileStorageUtils.fillOCFile(result.data[0] as RemoteFile)
                         existingFile?.lastSyncDateForProperties = System.currentTimeMillis()
@@ -186,14 +271,15 @@ class ConflictsResolveActivity : FileActivity(), OnConflictDecisionMadeListener
                     Log_OC.e(TAG, "Error when trying to fetch remote file", e)
                     showErrorAndFinish()
                 }
-            }.start()
+            }
         } else {
-            val remotePath = fileStorageManager?.retrieveRemotePathConsideringEncryption(existingFile) ?: return
+            val remotePath = fileDataStorageManager.retrieveRemotePathConsideringEncryption(existingFile) ?: return
             startDialog(remotePath)
         }
     }
 
-    private fun startDialog(remotePath: String) {
+    @SuppressLint("CommitTransaction")
+    private fun prepareDialog(): Pair<FragmentTransaction, User> {
         val userOptional = user
         if (!userOptional.isPresent) {
             Log_OC.e(TAG, "User not present")
@@ -206,13 +292,21 @@ class ConflictsResolveActivity : FileActivity(), OnConflictDecisionMadeListener
         if (prev != null) {
             fragmentTransaction.remove(prev)
         }
+
+        return fragmentTransaction to user.get()
+    }
+
+    private fun startDialog(remotePath: String) {
+        val (ft, user) = prepareDialog()
+
         if (existingFile != null && storageManager.fileExists(remotePath) && newFile != null) {
             val dialog = ConflictsResolveDialog.newInstance(
-                existingFile,
+                this,
                 newFile!!,
-                userOptional.get()
+                existingFile!!,
+                user
             )
-            dialog.show(fragmentTransaction, "conflictDialog")
+            dialog.show(ft, "conflictDialog")
         } else {
             // Account was changed to a different one - just finish
             Log_OC.e(TAG, "Account was changed, finishing")
@@ -222,8 +316,8 @@ class ConflictsResolveActivity : FileActivity(), OnConflictDecisionMadeListener
 
     private fun showErrorAndFinish(code: Int? = null) {
         val message = parseErrorMessage(code)
-        runOnUiThread {
-            Toast.makeText(this, message, Toast.LENGTH_LONG).show()
+        lifecycleScope.launch(Dispatchers.Main) {
+            Toast.makeText(this@ConflictsResolveActivity, message, Toast.LENGTH_LONG).show()
             finish()
         }
     }
@@ -254,18 +348,28 @@ class ConflictsResolveActivity : FileActivity(), OnConflictDecisionMadeListener
          */
         const val EXTRA_LOCAL_BEHAVIOUR = "LOCAL_BEHAVIOUR"
         const val EXTRA_EXISTING_FILE = "EXISTING_FILE"
+        private const val EXTRA_OFFLINE_OPERATION_PATH = "EXTRA_OFFLINE_OPERATION_PATH"
+
         private val TAG = ConflictsResolveActivity::class.java.simpleName
 
         @JvmStatic
         fun createIntent(file: OCFile?, user: User?, conflictUploadId: Long, flag: Int?, context: Context?): Intent {
-            val intent = Intent(context, ConflictsResolveActivity::class.java)
-            if (flag != null) {
-                intent.flags = intent.flags or flag
+            return Intent(context, ConflictsResolveActivity::class.java).apply {
+                if (flag != null) {
+                    flags = flags or flag
+                }
+                putExtra(EXTRA_FILE, file)
+                putExtra(EXTRA_USER, user)
+                putExtra(EXTRA_CONFLICT_UPLOAD_ID, conflictUploadId)
+            }
+        }
+
+        @JvmStatic
+        fun createIntent(file: OCFile, offlineOperationPath: String, context: Context): Intent {
+            return Intent(context, ConflictsResolveActivity::class.java).apply {
+                putExtra(EXTRA_FILE, file)
+                putExtra(EXTRA_OFFLINE_OPERATION_PATH, offlineOperationPath)
             }
-            intent.putExtra(EXTRA_FILE, file)
-            intent.putExtra(EXTRA_USER, user)
-            intent.putExtra(EXTRA_CONFLICT_UPLOAD_ID, conflictUploadId)
-            return intent
         }
     }
 }

+ 22 - 4
app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java

@@ -21,8 +21,10 @@ import android.app.Activity;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.ServiceConnection;
 import android.content.pm.PackageManager;
+import android.net.ConnectivityManager;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
@@ -36,6 +38,8 @@ import com.nextcloud.client.jobs.BackgroundJobManager;
 import com.nextcloud.client.jobs.download.FileDownloadWorker;
 import com.nextcloud.client.jobs.upload.FileUploadHelper;
 import com.nextcloud.client.network.ConnectivityService;
+import com.nextcloud.receiver.NetworkChangeListener;
+import com.nextcloud.receiver.NetworkChangeReceiver;
 import com.nextcloud.utils.EditorUtils;
 import com.nextcloud.utils.extensions.ActivityExtensionsKt;
 import com.nextcloud.utils.extensions.BundleExtensionsKt;
@@ -112,7 +116,7 @@ import static com.owncloud.android.ui.activity.FileDisplayActivity.TAG_PUBLIC_LI
  */
 public abstract class FileActivity extends DrawerActivity
     implements OnRemoteOperationListener, ComponentsGetter, SslUntrustedCertDialog.OnSslUntrustedCertListener,
-    LoadingVersionNumberTask.VersionDevInterface, FileDetailSharingFragment.OnEditShareListener {
+    LoadingVersionNumberTask.VersionDevInterface, FileDetailSharingFragment.OnEditShareListener, NetworkChangeListener {
 
     public static final String EXTRA_FILE = "com.owncloud.android.ui.activity.FILE";
     public static final String EXTRA_LIVE_PHOTO_FILE = "com.owncloud.android.ui.activity.LIVE.PHOTO.FILE";
@@ -176,6 +180,13 @@ public abstract class FileActivity extends DrawerActivity
     @Inject
     ArbitraryDataProvider arbitraryDataProvider;
 
+    private NetworkChangeReceiver networkChangeReceiver;
+
+    private void registerNetworkChangeReceiver() {
+        IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
+        registerReceiver(networkChangeReceiver, filter);
+    }
+
     @Override
     public void showFiles(boolean onDeviceOnly, boolean personalFiles) {
         // must be specialized in subclasses
@@ -198,10 +209,11 @@ public abstract class FileActivity extends DrawerActivity
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        networkChangeReceiver = new NetworkChangeReceiver(this, connectivityService);
         usersAndGroupsSearchConfig.reset();
         mHandler = new Handler();
         mFileOperationsHelper = new FileOperationsHelper(this, getUserAccountManager(), connectivityService, editorUtils);
-        User user = null;
+        User user;
 
         if (savedInstanceState != null) {
             mFile = BundleExtensionsKt.getParcelableArgument(savedInstanceState, FileActivity.EXTRA_FILE, OCFile.class);
@@ -227,11 +239,15 @@ public abstract class FileActivity extends DrawerActivity
         mOperationsServiceConnection = new OperationsServiceConnection();
         bindService(new Intent(this, OperationsService.class), mOperationsServiceConnection,
                     Context.BIND_AUTO_CREATE);
+        registerNetworkChangeReceiver();
     }
 
-    public void checkInternetConnection() {
-        if (connectivityService != null && connectivityService.isConnected()) {
+    @Override
+    public void networkAndServerConnectionListener(boolean isNetworkAndServerAvailable) {
+        if (isNetworkAndServerAvailable) {
             hideInfoBox();
+        } else {
+            showInfoBox(R.string.offline_mode);
         }
     }
 
@@ -266,6 +282,8 @@ public abstract class FileActivity extends DrawerActivity
             mOperationsServiceBinder = null;
         }
 
+        unregisterReceiver(networkChangeReceiver);
+
         super.onDestroy();
     }
 

+ 22 - 3
app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java

@@ -93,6 +93,7 @@ import com.owncloud.android.operations.RenameFileOperation;
 import com.owncloud.android.operations.SynchronizeFileOperation;
 import com.owncloud.android.operations.UploadFileOperation;
 import com.owncloud.android.syncadapter.FileSyncAdapter;
+import com.owncloud.android.ui.activity.fileDisplayActivity.OfflineFolderConflictManager;
 import com.owncloud.android.ui.asynctasks.CheckAvailableSpaceTask;
 import com.owncloud.android.ui.asynctasks.FetchRemoteFileTask;
 import com.owncloud.android.ui.asynctasks.GetRemoteFileTask;
@@ -171,6 +172,8 @@ public class FileDisplayActivity extends FileActivity
     public static final String LIST_GROUPFOLDERS = "LIST_GROUPFOLDERS";
     public static final int SINGLE_USER_SIZE = 1;
     public static final String OPEN_FILE = "NC_OPEN_FILE";
+    public static final String FOLDER_SYNC_CONFLICT = "FOLDER_SYNC_CONFLICT";
+    public static final String FOLDER_SYNC_CONFLICT_ARG_REMOTE_IDS_TO_OPERATION_PATHS = "FOLDER_SYNC_CONFLICT_ARG_REMOTE_IDS_TO_OPERATION_PATHS";
 
     private FilesBinding binding;
 
@@ -278,6 +281,9 @@ public class FileDisplayActivity extends FileActivity
         initSyncBroadcastReceiver();
         observeWorkerState();
         registerRefreshFolderEventReceiver();
+
+        OfflineFolderConflictManager offlineFolderConflictManager = new OfflineFolderConflictManager(this);
+        offlineFolderConflictManager.registerRefreshSearchEventReceiver();
     }
 
     @SuppressWarnings("unchecked")
@@ -871,7 +877,7 @@ public class FileDisplayActivity extends FileActivity
                     if (hasEnoughSpaceAvailable) {
                         File file = new File(filesToUpload[0]);
                         File renamedFile;
-                        if(requestCode == REQUEST_CODE__UPLOAD_FROM_CAMERA) {
+                        if (requestCode == REQUEST_CODE__UPLOAD_FROM_CAMERA) {
                             renamedFile = new File(file.getParent() + PATH_SEPARATOR + FileOperationsHelper.getCapturedImageName());
                         } else {
                             renamedFile = new File(file.getParent() + PATH_SEPARATOR + FileOperationsHelper.getCapturedVideoName());
@@ -1315,7 +1321,6 @@ public class FileDisplayActivity extends FileActivity
 
                         Log_OC.d(TAG, "Setting progress visibility to " + mSyncInProgress);
 
-
                         OCFileListFragment ocFileListFragment = getListOfFilesFragment();
                         if (ocFileListFragment != null) {
                             ocFileListFragment.setLoading(mSyncInProgress);
@@ -1597,10 +1602,24 @@ public class FileDisplayActivity extends FileActivity
                 fileDownloadProgressListener = null;
             } else if (state instanceof WorkerState.UploadFinished) {
                 refreshList();
+            } else if (state instanceof  WorkerState.OfflineOperationsCompleted) {
+                refreshCurrentDirectory();
             }
         });
     }
 
+    public void refreshCurrentDirectory() {
+        OCFile currentDir = (getCurrentDir() != null) ?
+            getStorageManager().getFileByDecryptedRemotePath(getCurrentDir().getRemotePath()) : null;
+
+        OCFileListFragment fileListFragment =
+            (ActivityExtensionsKt.lastFragment(this) instanceof OCFileListFragment fragment) ? fragment : getListOfFilesFragment();
+
+        if (fileListFragment != null) {
+            fileListFragment.listDirectory(currentDir, false, false);
+        }
+    }
+
     private void handleDownloadWorkerState() {
         if (mWaitingToPreview != null && getStorageManager() != null) {
             mWaitingToPreview = getStorageManager().getFileById(mWaitingToPreview.getFileId());
@@ -2216,7 +2235,7 @@ public class FileDisplayActivity extends FileActivity
         syncAndUpdateFolder(true);
     }
 
-    private void syncAndUpdateFolder(boolean ignoreETag) {
+    public void syncAndUpdateFolder(boolean ignoreETag) {
         syncAndUpdateFolder(ignoreETag, false);
     }
 

+ 7 - 3
app/src/main/java/com/owncloud/android/ui/activity/ToolbarActivity.java

@@ -215,15 +215,19 @@ public abstract class ToolbarActivity extends BaseActivity implements Injectable
      * @param text the text to be displayed
      */
     protected final void showInfoBox(@StringRes int text) {
-        mInfoBox.setVisibility(View.VISIBLE);
-        mInfoBoxMessage.setText(text);
+        if (mInfoBox != null && mInfoBoxMessage != null) {
+            mInfoBox.setVisibility(View.VISIBLE);
+            mInfoBoxMessage.setText(text);
+        }
     }
 
     /**
      * Hides the toolbar's info box.
      */
     public final void hideInfoBox() {
-        mInfoBox.setVisibility(View.GONE);
+        if (mInfoBox != null) {
+            mInfoBox.setVisibility(View.GONE);
+        }
     }
 
     public void setPreviewImageVisibility(boolean isVisibility) {

+ 55 - 0
app/src/main/java/com/owncloud/android/ui/activity/fileDisplayActivity/OfflineFolderConflictManager.kt

@@ -0,0 +1,55 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package com.owncloud.android.ui.activity.fileDisplayActivity
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import androidx.localbroadcastmanager.content.LocalBroadcastManager
+import com.nextcloud.client.jobs.offlineOperations.OfflineOperationsNotificationManager
+import com.nextcloud.utils.extensions.getSerializableArgument
+import com.owncloud.android.ui.activity.FileDisplayActivity
+
+class OfflineFolderConflictManager(private val activity: FileDisplayActivity) {
+
+    private val notificationManager = OfflineOperationsNotificationManager(activity, activity.viewThemeUtils)
+
+    fun registerRefreshSearchEventReceiver() {
+        val filter = IntentFilter(FileDisplayActivity.FOLDER_SYNC_CONFLICT)
+        LocalBroadcastManager.getInstance(activity).registerReceiver(folderSyncConflictEventReceiver, filter)
+    }
+
+    private val folderSyncConflictEventReceiver = object : BroadcastReceiver() {
+        override fun onReceive(context: Context, intent: Intent) {
+            intent.run {
+                @Suppress("UNCHECKED_CAST")
+                val map = getSerializableArgument(
+                    FileDisplayActivity.FOLDER_SYNC_CONFLICT_ARG_REMOTE_IDS_TO_OPERATION_PATHS,
+                    HashMap::class.java
+                ) as? HashMap<String, String>
+
+                if (!map.isNullOrEmpty()) {
+                    showFolderSyncConflictNotifications(map)
+                }
+            }
+        }
+    }
+
+    private fun showFolderSyncConflictNotifications(remoteIdsToOperationPaths: HashMap<String, String>) {
+        remoteIdsToOperationPaths.forEach { (remoteId, path) ->
+            val file = activity.storageManager.getFileByRemoteId(remoteId)
+            file?.let {
+                val entity = activity.storageManager.offlineOperationDao.getByPath(path)
+                if (activity.user.isPresent) {
+                    notificationManager.showConflictResolveNotification(file, entity, activity.user.get())
+                }
+            }
+        }
+    }
+}

+ 1 - 1
app/src/main/java/com/owncloud/android/ui/adapter/ListGridItemViewHolder.kt

@@ -9,6 +9,6 @@ package com.owncloud.android.ui.adapter
 
 import android.widget.TextView
 
-internal interface ListGridItemViewHolder : ListGridImageViewHolder {
+internal interface ListGridItemViewHolder : ListViewHolder {
     val fileName: TextView
 }

+ 1 - 1
app/src/main/java/com/owncloud/android/ui/adapter/ListGridImageViewHolder.kt → app/src/main/java/com/owncloud/android/ui/adapter/ListViewHolder.kt

@@ -14,7 +14,7 @@ import android.widget.LinearLayout
 import android.widget.TextView
 import com.elyeproj.loaderviewlibrary.LoaderImageView
 
-interface ListGridImageViewHolder {
+interface ListViewHolder {
     val thumbnail: ImageView
     fun showVideoOverlay()
     val shimmerThumbnail: LoaderImageView

+ 75 - 50
app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java

@@ -16,6 +16,8 @@ import android.annotation.SuppressLint;
 import android.app.Activity;
 import android.content.ContentValues;
 import android.content.res.Resources;
+import android.graphics.Color;
+import android.graphics.PorterDuff;
 import android.graphics.drawable.Drawable;
 import android.os.Handler;
 import android.os.Looper;
@@ -23,7 +25,6 @@ import android.text.TextUtils;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
-import android.widget.ImageButton;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 
@@ -32,6 +33,7 @@ import com.nextcloud.android.common.ui.theme.utils.ColorRole;
 import com.nextcloud.client.account.User;
 import com.nextcloud.client.jobs.upload.FileUploadHelper;
 import com.nextcloud.client.preferences.AppPreferences;
+import com.nextcloud.utils.extensions.ViewExtensionsKt;
 import com.owncloud.android.MainApp;
 import com.owncloud.android.R;
 import com.owncloud.android.databinding.GridImageBinding;
@@ -96,25 +98,26 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
 
     private static final int showFilenameColumnThreshold = 4;
     private final String userId;
-    private Activity activity;
-    private AppPreferences preferences;
+    private final Activity activity;
+    private final AppPreferences preferences;
     private List<OCFile> mFiles = new ArrayList<>();
-    private List<OCFile> mFilesAll = new ArrayList<>();
-    private boolean hideItemOptions;
+    private final List<OCFile> mFilesAll = new ArrayList<>();
+    private final boolean hideItemOptions;
     private long lastTimestamp;
     private boolean gridView;
     public ArrayList<String> listOfHiddenFiles = new ArrayList<>();
     private FileDataStorageManager mStorageManager;
     private User user;
-    private OCFileListFragmentInterface ocFileListFragmentInterface;
+    private final OCFileListFragmentInterface ocFileListFragmentInterface;
+
 
     private OCFile currentDirectory;
     private static final String TAG = OCFileListAdapter.class.getSimpleName();
 
-    private static final int VIEWTYPE_FOOTER = 0;
-    private static final int VIEWTYPE_ITEM = 1;
-    private static final int VIEWTYPE_IMAGE = 2;
-    private static final int VIEWTYPE_HEADER = 3;
+    private static final int VIEW_TYPE_FOOTER = 0;
+    private static final int VIEW_TYPE_ITEM = 1;
+    private static final int VIEW_TYPE_IMAGE = 2;
+    private static final int VIEW_TYPE_HEADER = 3;
 
     private boolean onlyOnDevice;
     private final OCFileListDelegate ocFileListDelegate;
@@ -339,23 +342,23 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
     @Override
     public int getItemViewType(int position) {
         if (shouldShowHeader() && position == 0) {
-            return VIEWTYPE_HEADER;
+            return VIEW_TYPE_HEADER;
         }
 
         if (shouldShowHeader() && position == mFiles.size() + 1 ||
             (!shouldShowHeader() && position == mFiles.size())) {
-            return VIEWTYPE_FOOTER;
+            return VIEW_TYPE_FOOTER;
         }
 
         OCFile item = getItem(position);
         if (item == null) {
-            return VIEWTYPE_ITEM;
+            return VIEW_TYPE_ITEM;
         }
 
         if (MimeTypeUtil.isImageOrVideo(item)) {
-            return VIEWTYPE_IMAGE;
+            return VIEW_TYPE_IMAGE;
         } else {
-            return VIEWTYPE_ITEM;
+            return VIEW_TYPE_ITEM;
         }
     }
 
@@ -378,9 +381,9 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
                     );
                 }
             }
-            case VIEWTYPE_IMAGE -> {
+            case VIEW_TYPE_IMAGE -> {
                 if (gridView) {
-                    return new OCFileListGridImageViewHolder(
+                    return new OCFileListViewHolder(
                         GridImageBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)
                     );
                 } else {
@@ -389,12 +392,12 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
                     );
                 }
             }
-            case VIEWTYPE_FOOTER -> {
+            case VIEW_TYPE_FOOTER -> {
                 return new OCFileListFooterViewHolder(
                     ListFooterBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)
                 );
             }
-            case VIEWTYPE_HEADER -> {
+            case VIEW_TYPE_HEADER -> {
                 ListHeaderBinding binding = ListHeaderBinding.inflate(
                     LayoutInflater.from(parent.getContext()),
                     parent,
@@ -421,7 +424,7 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
             PreviewTextFragment.setText(headerViewHolder.getHeaderText(), text, null, activity, true, true, viewThemeUtils);
             headerViewHolder.getHeaderView().setOnClickListener(v -> ocFileListFragmentInterface.onHeaderClicked());
         } else {
-            ListGridImageViewHolder gridViewHolder = (ListGridImageViewHolder) holder;
+            ListViewHolder gridViewHolder = (ListViewHolder) holder;
             OCFile file = getItem(position);
 
             if (file == null) {
@@ -430,24 +433,24 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
             }
 
             ocFileListDelegate.bindGridViewHolder(gridViewHolder, file, currentDirectory, searchType);
-            checkVisibilityOfMoreButtons(gridViewHolder);
+            ViewExtensionsKt.setVisibleIf(gridViewHolder.getMore(), !isMultiSelect());
             checkVisibilityOfFileFeaturesLayout(gridViewHolder);
 
-            if (holder instanceof ListItemViewHolder) {
-                bindListItemViewHolder((ListItemViewHolder) gridViewHolder, file);
+            if (holder instanceof ListItemViewHolder itemViewHolder) {
+                bindListItemViewHolder(itemViewHolder, file);
             }
 
-            if (holder instanceof ListGridItemViewHolder) {
-                bindListGridItemViewHolder((ListGridItemViewHolder) holder, file);
-                checkVisibilityOfMoreButtons((ListGridItemViewHolder) holder);
-                checkVisibilityOfFileFeaturesLayout((ListGridItemViewHolder) holder);
+            if (holder instanceof ListGridItemViewHolder gridItemViewHolder) {
+                bindListGridItemViewHolder(gridItemViewHolder, file);
+                ViewExtensionsKt.setVisibleIf(gridItemViewHolder.getMore(), !isMultiSelect());
+                checkVisibilityOfFileFeaturesLayout(gridItemViewHolder);
             }
 
             updateLivePhotoIndicators(gridViewHolder, file);
         }
     }
 
-    private void checkVisibilityOfFileFeaturesLayout(ListGridImageViewHolder holder) {
+    private void checkVisibilityOfFileFeaturesLayout(ListViewHolder holder) {
         int fileFeaturesVisibility = View.GONE;
         LinearLayout fileFeaturesLayout = holder.getFileFeaturesLayout();
 
@@ -465,19 +468,6 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
         fileFeaturesLayout.setVisibility(fileFeaturesVisibility);
     }
 
-    private void checkVisibilityOfMoreButtons(ListGridImageViewHolder holder) {
-        ImageButton moreButton = holder.getMore();
-        if (moreButton == null) {
-            return;
-        }
-
-        if (isMultiSelect()) {
-            moreButton.setVisibility(View.GONE);
-        } else {
-            moreButton.setVisibility(View.VISIBLE);
-        }
-    }
-
     private void mergeOCFilesForLivePhoto() {
         List<OCFile> filesToRemove = new ArrayList<>();
 
@@ -505,13 +495,13 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
         filesToRemove.clear();
     }
 
-    private void updateLivePhotoIndicators(ListGridImageViewHolder holder, OCFile file) {
+    private void updateLivePhotoIndicators(ListViewHolder holder, OCFile file) {
         boolean isLivePhoto = file.getLinkedFileIdForLivePhoto() != null;
 
         if (holder instanceof OCFileListItemViewHolder) {
             holder.getLivePhotoIndicator().setVisibility(isLivePhoto ? (View.VISIBLE) : (View.GONE));
             holder.getLivePhotoIndicatorSeparator().setVisibility(isLivePhoto ? (View.VISIBLE) : (View.GONE));
-        } else if (holder instanceof OCFileListGridImageViewHolder) {
+        } else if (holder instanceof OCFileListViewHolder) {
             holder.getGridLivePhotoIndicator().setVisibility(isLivePhoto ? (View.VISIBLE) : (View.GONE));
         }
     }
@@ -529,6 +519,9 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
                 holder.getFileName().setVisibility(View.VISIBLE);
             }
         }
+
+        ViewExtensionsKt.setVisibleIf(holder.getShared(), !file.isOfflineOperation());
+        setColorFilterForOfflineOperations(holder, file);
     }
 
     private void bindListItemViewHolder(ListItemViewHolder holder, OCFile file) {
@@ -599,15 +592,27 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
                 localSize = localFile.length();
             }
 
-            holder.getFileSize().setText(DisplayUtils.bytesToHumanReadable(localSize));
             holder.getFileSize().setVisibility(View.VISIBLE);
-            holder.getFileSizeSeparator().setVisibility(View.VISIBLE);
+
+            if (file.isOfflineOperation()) {
+                holder.getFileSize().setText(MainApp.string(R.string.oc_file_list_adapter_offline_operation_description_text));
+                holder.getFileSizeSeparator().setVisibility(View.GONE);
+            } else {
+                holder.getFileSize().setText(DisplayUtils.bytesToHumanReadable(localSize));
+                holder.getFileSizeSeparator().setVisibility(View.VISIBLE);
+            }
         } else {
             final long fileLength = file.getFileLength();
             if (fileLength >= 0) {
-                holder.getFileSize().setText(DisplayUtils.bytesToHumanReadable(fileLength));
                 holder.getFileSize().setVisibility(View.VISIBLE);
-                holder.getFileSizeSeparator().setVisibility(View.VISIBLE);
+
+                if (file.isOfflineOperation()) {
+                    holder.getFileSize().setText(MainApp.string(R.string.oc_file_list_adapter_offline_operation_description_text));
+                    holder.getFileSizeSeparator().setVisibility(View.GONE);
+                } else {
+                    holder.getFileSize().setText(DisplayUtils.bytesToHumanReadable(fileLength));
+                    holder.getFileSizeSeparator().setVisibility(View.VISIBLE);
+                }
             } else {
                 holder.getFileSize().setVisibility(View.GONE);
                 holder.getFileSizeSeparator().setVisibility(View.GONE);
@@ -641,12 +646,32 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
         } else {
             holder.getOverflowMenu().setImageResource(R.drawable.ic_dots_vertical);
         }
+
+        applyVisualsForOfflineOperations(holder, file);
+    }
+
+    private void applyVisualsForOfflineOperations(ListItemViewHolder holder, OCFile file) {
+        ViewExtensionsKt.setVisibleIf(holder.getShared(), !file.isOfflineOperation());
+        setColorFilterForOfflineOperations(holder, file);
+    }
+
+    private void setColorFilterForOfflineOperations(ListViewHolder holder, OCFile file) {
+        if (!file.isFolder()) {
+            return;
+        }
+
+        if (file.isOfflineOperation()) {
+            holder.getThumbnail().setColorFilter(Color.GRAY, PorterDuff.Mode.SRC_IN);
+        } else {
+            Drawable drawable = viewThemeUtils.platform.tintDrawable(MainApp.getAppContext(), holder.getThumbnail().getDrawable(), ColorRole.PRIMARY);
+            holder.getThumbnail().setImageDrawable(drawable);
+        }
     }
 
     @Override
     public void onViewAttachedToWindow(@NonNull RecyclerView.ViewHolder holder) {
-        if (holder instanceof ListGridImageViewHolder) {
-            LoaderImageView thumbnailShimmer = ((ListGridImageViewHolder) holder).getShimmerThumbnail();
+        if (holder instanceof ListViewHolder) {
+            LoaderImageView thumbnailShimmer = ((ListViewHolder) holder).getShimmerThumbnail();
             if (thumbnailShimmer.getVisibility() == View.VISIBLE) {
                 thumbnailShimmer.setImageResource(R.drawable.background);
                 thumbnailShimmer.resetLoader();
@@ -1018,7 +1043,7 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
     @Override
     public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) {
         super.onViewRecycled(holder);
-        if (holder instanceof ListGridImageViewHolder listGridImageViewHolder) {
+        if (holder instanceof ListViewHolder listGridImageViewHolder) {
             LoaderImageView thumbnailShimmer = listGridImageViewHolder.getShimmerThumbnail();
             DisplayUtils.stopShimmer(thumbnailShimmer,  listGridImageViewHolder.getThumbnail());
         }

+ 9 - 9
app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt

@@ -195,7 +195,7 @@ class OCFileListDelegate(
     }
 
     fun bindGridViewHolder(
-        gridViewHolder: ListGridImageViewHolder,
+        gridViewHolder: ListViewHolder,
         file: OCFile,
         currentDirectory: OCFile?,
         searchType: SearchType?
@@ -253,7 +253,7 @@ class OCFileListDelegate(
         }
     }
 
-    private fun bindUnreadComments(file: OCFile, gridViewHolder: ListGridImageViewHolder) {
+    private fun bindUnreadComments(file: OCFile, gridViewHolder: ListViewHolder) {
         if (file.unreadCommentsCount > 0) {
             gridViewHolder.unreadComments.visibility = View.VISIBLE
             gridViewHolder.unreadComments.setOnClickListener {
@@ -265,7 +265,7 @@ class OCFileListDelegate(
         }
     }
 
-    private fun bindGridItemLayout(file: OCFile, gridViewHolder: ListGridImageViewHolder) {
+    private fun bindGridItemLayout(file: OCFile, gridViewHolder: ListViewHolder) {
         setItemLayoutBackgroundColor(file, gridViewHolder)
         setCheckBoxImage(file, gridViewHolder)
         setItemLayoutOnClickListeners(file, gridViewHolder)
@@ -275,7 +275,7 @@ class OCFileListDelegate(
         }
     }
 
-    private fun setItemLayoutOnClickListeners(file: OCFile, gridViewHolder: ListGridImageViewHolder) {
+    private fun setItemLayoutOnClickListeners(file: OCFile, gridViewHolder: ListViewHolder) {
         gridViewHolder.itemLayout.setOnClickListener { ocFileListFragmentInterface.onItemClicked(file) }
 
         if (!hideItemOptions) {
@@ -290,7 +290,7 @@ class OCFileListDelegate(
         }
     }
 
-    private fun setItemLayoutBackgroundColor(file: OCFile, gridViewHolder: ListGridImageViewHolder) {
+    private fun setItemLayoutBackgroundColor(file: OCFile, gridViewHolder: ListViewHolder) {
         val cornerRadius = context.resources.getDimension(R.dimen.selected_grid_container_radius)
 
         val isDarkModeActive = (syncFolderProvider?.preferences?.isDarkModeEnabled == true)
@@ -313,7 +313,7 @@ class OCFileListDelegate(
         }
     }
 
-    private fun setCheckBoxImage(file: OCFile, gridViewHolder: ListGridImageViewHolder) {
+    private fun setCheckBoxImage(file: OCFile, gridViewHolder: ListViewHolder) {
         if (isCheckedFile(file)) {
             gridViewHolder.checkbox.setImageDrawable(
                 viewThemeUtils.platform.tintDrawable(context, R.drawable.ic_checkbox_marked, ColorRole.PRIMARY)
@@ -323,7 +323,7 @@ class OCFileListDelegate(
         }
     }
 
-    private fun bindGridMetadataViews(file: OCFile, gridViewHolder: ListGridImageViewHolder) {
+    private fun bindGridMetadataViews(file: OCFile, gridViewHolder: ListViewHolder) {
         if (showMetadata) {
             showLocalFileIndicator(file, gridViewHolder)
             gridViewHolder.favorite.visibility = if (file.isFavorite) View.VISIBLE else View.GONE
@@ -333,7 +333,7 @@ class OCFileListDelegate(
         }
     }
 
-    private fun showLocalFileIndicator(file: OCFile, gridViewHolder: ListGridImageViewHolder) {
+    private fun showLocalFileIndicator(file: OCFile, gridViewHolder: ListViewHolder) {
         val operationsServiceBinder = transferServiceGetter.operationsServiceBinder
 
         val icon: Int? = when {
@@ -365,7 +365,7 @@ class OCFileListDelegate(
         }
     }
 
-    private fun showShareIcon(gridViewHolder: ListGridImageViewHolder, file: OCFile) {
+    private fun showShareIcon(gridViewHolder: ListViewHolder, file: OCFile) {
         val sharedIconView = gridViewHolder.shared
         if (gridViewHolder is OCFileListItemViewHolder || file.unreadCommentsCount == 0) {
             sharedIconView.visibility = View.VISIBLE

+ 2 - 2
app/src/main/java/com/owncloud/android/ui/adapter/OCFileListGridImageViewHolder.kt → app/src/main/java/com/owncloud/android/ui/adapter/OCFileListViewHolder.kt

@@ -16,11 +16,11 @@ import androidx.recyclerview.widget.RecyclerView
 import com.elyeproj.loaderviewlibrary.LoaderImageView
 import com.owncloud.android.databinding.GridImageBinding
 
-internal class OCFileListGridImageViewHolder(var binding: GridImageBinding) :
+internal class OCFileListViewHolder(var binding: GridImageBinding) :
     RecyclerView.ViewHolder(
         binding.root
     ),
-    ListGridImageViewHolder {
+    ListViewHolder {
 
     override val thumbnail: ImageView
         get() = binding.thumbnail

+ 211 - 96
app/src/main/java/com/owncloud/android/ui/dialog/ConflictsResolveDialog.kt

@@ -1,11 +1,8 @@
 /*
  * Nextcloud - Android Client
  *
- * SPDX-FileCopyrightText: 2023 Alper Ozturk <alper.ozturk@nextcloud.com>
- * SPDX-FileCopyrightText: 2020-2022 Tobias Kaminsky <tobias@kaminsky.me>
- * SPDX-FileCopyrightText: 2015 ownCloud Inc.
- * SPDX-FileCopyrightText: 2012 Bartosz Przybylski <bart.p.pl@gmail.com>
- * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only)
+ * SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
  */
 package com.owncloud.android.ui.dialog
 
@@ -21,10 +18,10 @@ import androidx.fragment.app.DialogFragment
 import com.google.android.material.button.MaterialButton
 import com.google.android.material.dialog.MaterialAlertDialogBuilder
 import com.nextcloud.client.account.User
+import com.nextcloud.client.database.entity.OfflineOperationEntity
 import com.nextcloud.client.di.Injectable
 import com.nextcloud.utils.extensions.getParcelableArgument
 import com.nextcloud.utils.extensions.getSerializableArgument
-import com.nextcloud.utils.extensions.logFileSize
 import com.owncloud.android.R
 import com.owncloud.android.databinding.ConflictResolveDialogBinding
 import com.owncloud.android.datamodel.FileDataStorageManager
@@ -33,7 +30,10 @@ import com.owncloud.android.datamodel.SyncedFolderProvider
 import com.owncloud.android.datamodel.ThumbnailsCacheManager.ThumbnailGenerationTask
 import com.owncloud.android.lib.common.utils.Log_OC
 import com.owncloud.android.ui.adapter.LocalFileListAdapter
+import com.owncloud.android.ui.dialog.parcel.ConflictDialogData
+import com.owncloud.android.ui.dialog.parcel.ConflictFileData
 import com.owncloud.android.utils.DisplayUtils
+import com.owncloud.android.utils.MimeTypeUtil
 import com.owncloud.android.utils.theme.ViewThemeUtils
 import java.io.File
 import javax.inject.Inject
@@ -44,24 +44,32 @@ import javax.inject.Inject
 class ConflictsResolveDialog : DialogFragment(), Injectable {
     private lateinit var binding: ConflictResolveDialogBinding
 
-    private var existingFile: OCFile? = null
-    private var newFile: File? = null
     var listener: OnConflictDecisionMadeListener? = null
-    private var user: User? = null
     private val asyncTasks: MutableList<ThumbnailGenerationTask> = ArrayList()
     private var positiveButton: MaterialButton? = null
 
+    private var data: ConflictDialogData? = null
+    private var user: User? = null
+    private var leftDataFile: File? = null
+    private var rightDataFile: OCFile? = null
+
     @Inject
     lateinit var viewThemeUtils: ViewThemeUtils
 
     @Inject
     lateinit var syncedFolderProvider: SyncedFolderProvider
 
+    @Inject
+    lateinit var fileDataStorageManager: FileDataStorageManager
+
     enum class Decision {
         CANCEL,
         KEEP_BOTH,
         KEEP_LOCAL,
-        KEEP_SERVER
+        KEEP_SERVER,
+        KEEP_OFFLINE_FOLDER,
+        KEEP_SERVER_FOLDER,
+        KEEP_BOTH_FOLDER
     }
 
     override fun onAttach(context: Context) {
@@ -98,14 +106,13 @@ class ConflictsResolveDialog : DialogFragment(), Injectable {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
 
-        if (savedInstanceState != null) {
-            existingFile = savedInstanceState.getParcelableArgument(KEY_EXISTING_FILE, OCFile::class.java)
-            newFile = savedInstanceState.getSerializableArgument(KEY_NEW_FILE, File::class.java)
-            user = savedInstanceState.getParcelableArgument(KEY_USER, User::class.java)
-        } else if (arguments != null) {
-            existingFile = arguments.getParcelableArgument(KEY_EXISTING_FILE, OCFile::class.java)
-            newFile = arguments.getSerializableArgument(KEY_NEW_FILE, File::class.java)
-            user = arguments.getParcelableArgument(KEY_USER, User::class.java)
+        val bundle = savedInstanceState ?: arguments
+
+        if (bundle != null) {
+            data = bundle.getParcelableArgument(ARG_CONFLICT_DATA, ConflictDialogData::class.java)
+            leftDataFile = bundle.getSerializableArgument(ARG_LEFT_FILE, File::class.java)
+            rightDataFile = bundle.getParcelableArgument(ARG_RIGHT_FILE, OCFile::class.java)
+            user = bundle.getParcelableArgument(ARG_USER, User::class.java)
         } else {
             Toast.makeText(context, "Failed to create conflict dialog", Toast.LENGTH_LONG).show()
         }
@@ -113,80 +120,116 @@ class ConflictsResolveDialog : DialogFragment(), Injectable {
 
     override fun onSaveInstanceState(outState: Bundle) {
         super.onSaveInstanceState(outState)
-        existingFile.logFileSize(TAG)
-        newFile.logFileSize(TAG)
-        outState.putParcelable(KEY_EXISTING_FILE, existingFile)
-        outState.putSerializable(KEY_NEW_FILE, newFile)
-        outState.putParcelable(KEY_USER, user)
+        outState.run {
+            putParcelable(ARG_CONFLICT_DATA, data)
+        }
     }
 
     override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
         binding = ConflictResolveDialogBinding.inflate(requireActivity().layoutInflater)
 
-        viewThemeUtils.platform.themeCheckbox(binding.newCheckbox)
-        viewThemeUtils.platform.themeCheckbox(binding.existingCheckbox)
+        val builder = createDialogBuilder()
 
-        val builder = MaterialAlertDialogBuilder(requireActivity())
-        builder.setView(binding.root)
+        setupUI()
+        setOnClickListeners()
+
+        viewThemeUtils.run {
+            platform.themeCheckbox(binding.leftCheckbox)
+            platform.themeCheckbox(binding.rightCheckbox)
+            dialog.colorMaterialAlertDialogBackground(requireContext(), builder)
+        }
+
+        return builder.create()
+    }
+
+    private fun createDialogBuilder(): MaterialAlertDialogBuilder {
+        return MaterialAlertDialogBuilder(requireContext())
+            .setView(binding.root)
             .setPositiveButton(R.string.common_ok) { _: DialogInterface?, _: Int ->
-                if (binding.newCheckbox.isChecked && binding.existingCheckbox.isChecked) {
-                    listener?.conflictDecisionMade(Decision.KEEP_BOTH)
-                } else if (binding.newCheckbox.isChecked) {
-                    listener?.conflictDecisionMade(Decision.KEEP_LOCAL)
-                } else if (binding.existingCheckbox.isChecked) {
-                    listener?.conflictDecisionMade(Decision.KEEP_SERVER)
-                }
+                okButtonClick()
             }
             .setNegativeButton(R.string.common_cancel) { _: DialogInterface?, _: Int ->
                 listener?.conflictDecisionMade(Decision.CANCEL)
             }
-            .setTitle(String.format(getString(R.string.conflict_file_headline), existingFile?.fileName))
+            .setTitle(data?.folderName)
+    }
 
-        setupUI()
-        setOnClickListeners()
+    private fun okButtonClick() {
+        binding.run {
+            val isFolderNameNotExists = (data?.folderName == null)
+            val decision = when {
+                leftCheckbox.isChecked && rightCheckbox.isChecked ->
+                    if (isFolderNameNotExists) Decision.KEEP_BOTH_FOLDER else Decision.KEEP_BOTH
 
-        viewThemeUtils.dialog.colorMaterialAlertDialogBackground(binding.existingFileContainer.context, builder)
+                leftCheckbox.isChecked ->
+                    if (isFolderNameNotExists) Decision.KEEP_OFFLINE_FOLDER else Decision.KEEP_LOCAL
 
-        return builder.create()
+                rightCheckbox.isChecked ->
+                    if (isFolderNameNotExists) Decision.KEEP_SERVER_FOLDER else Decision.KEEP_SERVER
+
+                else -> null
+            }
+
+            decision?.let { listener?.conflictDecisionMade(it) }
+        }
     }
 
     private fun setupUI() {
-        val parentFile = existingFile?.remotePath?.let { File(it).parentFile }
-        if (parentFile != null) {
-            binding.`in`.text = String.format(getString(R.string.in_folder), parentFile.absolutePath)
-        } else {
-            binding.`in`.visibility = View.GONE
+        binding.run {
+            data?.let {
+                val (leftData, rightData) = it.checkboxData
+
+                folderName.visibility = if (it.folderName == null) {
+                    View.GONE
+                } else {
+                    View.VISIBLE
+                }
+                folderName.text = it.folderName
+
+                title.visibility = if (it.title == null) {
+                    View.GONE
+                } else {
+                    View.VISIBLE
+                }
+                title.text = it.title
+
+                description.text = it.description
+
+                leftCheckbox.text = leftData.title
+                leftTimestamp.text = leftData.timestamp
+                leftFileSize.text = leftData.fileSize
+
+                rightCheckbox.text = rightData.title
+                rightTimestamp.text = rightData.timestamp
+                rightFileSize.text = rightData.fileSize
+
+                if (leftDataFile != null && rightDataFile != null && user != null) {
+                    setThumbnailsForFileConflicts()
+                } else {
+                    val folderIcon = MimeTypeUtil.getDefaultFolderIcon(requireContext(), viewThemeUtils)
+                    leftThumbnail.setImageDrawable(folderIcon)
+                    rightThumbnail.setImageDrawable(folderIcon)
+                }
+            }
         }
+    }
+
+    private fun setThumbnailsForFileConflicts() {
+        binding.leftThumbnail.tag = leftDataFile.hashCode()
+        binding.rightThumbnail.tag = rightDataFile.hashCode()
 
-        // set info for new file
-        binding.newSize.text = newFile?.length()?.let { DisplayUtils.bytesToHumanReadable(it) }
-        binding.newTimestamp.text = newFile?.lastModified()?.let { DisplayUtils.getRelativeTimestamp(context, it) }
-        binding.newThumbnail.tag = newFile.hashCode()
         LocalFileListAdapter.setThumbnail(
-            newFile,
-            binding.newThumbnail,
+            leftDataFile,
+            binding.leftThumbnail,
             context,
             viewThemeUtils
         )
 
-        // set info for existing file
-        binding.existingSize.text = existingFile?.fileLength?.let { DisplayUtils.bytesToHumanReadable(it) }
-        binding.existingTimestamp.text = existingFile?.modificationTimestamp?.let {
-            DisplayUtils.getRelativeTimestamp(
-                context,
-                it
-            )
-        }
-
-        binding.existingThumbnail.tag = existingFile?.fileId
         DisplayUtils.setThumbnail(
-            existingFile,
-            binding.existingThumbnail,
+            rightDataFile,
+            binding.rightThumbnail,
             user,
-            FileDataStorageManager(
-                user,
-                requireContext().contentResolver
-            ),
+            fileDataStorageManager,
             asyncTasks,
             false,
             context,
@@ -198,31 +241,35 @@ class ConflictsResolveDialog : DialogFragment(), Injectable {
     }
 
     private fun setOnClickListeners() {
-        val checkBoxClickListener = View.OnClickListener {
-            positiveButton?.isEnabled = binding.newCheckbox.isChecked || binding.existingCheckbox.isChecked
-        }
+        binding.run {
+            val checkBoxClickListener = View.OnClickListener {
+                positiveButton?.isEnabled = (leftCheckbox.isChecked || rightCheckbox.isChecked)
+            }
 
-        binding.newCheckbox.setOnClickListener(checkBoxClickListener)
-        binding.existingCheckbox.setOnClickListener(checkBoxClickListener)
+            leftCheckbox.setOnClickListener(checkBoxClickListener)
+            rightCheckbox.setOnClickListener(checkBoxClickListener)
 
-        binding.newFileContainer.setOnClickListener {
-            binding.newCheckbox.isChecked = !binding.newCheckbox.isChecked
-            positiveButton?.isEnabled = binding.newCheckbox.isChecked || binding.existingCheckbox.isChecked
-        }
-        binding.existingFileContainer.setOnClickListener {
-            binding.existingCheckbox.isChecked = !binding.existingCheckbox.isChecked
-            positiveButton?.isEnabled = binding.newCheckbox.isChecked || binding.existingCheckbox.isChecked
+            leftFileContainer.setOnClickListener {
+                leftCheckbox.toggle()
+                positiveButton?.isEnabled = (leftCheckbox.isChecked || rightCheckbox.isChecked)
+            }
+
+            rightFileContainer.setOnClickListener {
+                rightCheckbox.toggle()
+                positiveButton?.isEnabled = (leftCheckbox.isChecked || rightCheckbox.isChecked)
+            }
         }
     }
 
     fun showDialog(activity: AppCompatActivity) {
         val prev = activity.supportFragmentManager.findFragmentByTag("dialog")
-        val ft = activity.supportFragmentManager.beginTransaction()
-        if (prev != null) {
-            ft.remove(prev)
+        activity.supportFragmentManager.beginTransaction().run {
+            if (prev != null) {
+                this.remove(prev)
+            }
+            addToBackStack(null)
+            show(this, "dialog")
         }
-        ft.addToBackStack(null)
-        show(ft, "dialog")
     }
 
     override fun onCancel(dialog: DialogInterface) {
@@ -236,35 +283,103 @@ class ConflictsResolveDialog : DialogFragment(), Injectable {
     override fun onStop() {
         super.onStop()
 
-        for (task in asyncTasks) {
-            task.cancel(true)
+        asyncTasks.forEach {
+            it.cancel(true)
             Log_OC.d(this, "cancel: abort get method directly")
-            task.getMethod?.abort()
+            it.getMethod?.abort()
         }
 
         asyncTasks.clear()
     }
 
     companion object {
-        private const val TAG = "ConflictsResolveDialog"
-        private const val KEY_NEW_FILE = "file"
-        private const val KEY_EXISTING_FILE = "ocfile"
-        private const val KEY_USER = "user"
+        private const val ARG_CONFLICT_DATA = "CONFLICT_DATA"
+        private const val ARG_LEFT_FILE = "LEFT_FILE"
+        private const val ARG_RIGHT_FILE = "RIGHT_FILE"
+        private const val ARG_USER = "USER"
+
+        @JvmStatic
+        fun newInstance(context: Context, leftFile: OCFile, rightFile: OCFile, user: User?): ConflictsResolveDialog {
+            val file = File(leftFile.storagePath)
+            val conflictData = getFileConflictData(file, rightFile, context)
+
+            val bundle = Bundle().apply {
+                putParcelable(ARG_CONFLICT_DATA, conflictData)
+                putSerializable(ARG_LEFT_FILE, file)
+                putParcelable(ARG_RIGHT_FILE, rightFile)
+                putParcelable(ARG_USER, user)
+            }
+
+            return ConflictsResolveDialog().apply {
+                arguments = bundle
+            }
+        }
 
         @JvmStatic
-        fun newInstance(existingFile: OCFile?, newFile: OCFile, user: User?): ConflictsResolveDialog {
-            val file = File(newFile.storagePath)
-            file.logFileSize(TAG)
+        fun newInstance(
+            context: Context,
+            offlineOperation: OfflineOperationEntity,
+            rightFile: OCFile
+        ): ConflictsResolveDialog {
+            val conflictData = getFolderConflictData(offlineOperation, rightFile, context)
 
             val bundle = Bundle().apply {
-                putParcelable(KEY_EXISTING_FILE, existingFile)
-                putSerializable(KEY_NEW_FILE, file)
-                putParcelable(KEY_USER, user)
+                putParcelable(ARG_CONFLICT_DATA, conflictData)
+                putParcelable(ARG_RIGHT_FILE, rightFile)
             }
 
             return ConflictsResolveDialog().apply {
                 arguments = bundle
             }
         }
+
+        @Suppress("MagicNumber")
+        @JvmStatic
+        private fun getFolderConflictData(
+            offlineOperation: OfflineOperationEntity,
+            rightFile: OCFile,
+            context: Context
+        ): ConflictDialogData {
+            val folderName = null
+
+            val leftTitle = context.getString(R.string.prefs_synced_folders_local_path_title)
+            val leftTimestamp =
+                DisplayUtils.getRelativeTimestamp(context, offlineOperation.createdAt?.times(1000L) ?: 0)
+            val leftFileSize = DisplayUtils.bytesToHumanReadable(0)
+            val leftCheckBoxData = ConflictFileData(leftTitle, leftTimestamp.toString(), leftFileSize)
+
+            val rightTitle = context.getString(R.string.prefs_synced_folders_remote_path_title)
+            val rightTimestamp = DisplayUtils.getRelativeTimestamp(context, rightFile.modificationTimestamp)
+            val rightFileSize = DisplayUtils.bytesToHumanReadable(rightFile.fileLength)
+            val rightCheckBoxData = ConflictFileData(rightTitle, rightTimestamp.toString(), rightFileSize)
+
+            val title = context.getString(R.string.conflict_folder_headline)
+            val description = context.getString(R.string.conflict_message_description_for_folder)
+            return ConflictDialogData(folderName, title, description, Pair(leftCheckBoxData, rightCheckBoxData))
+        }
+
+        @JvmStatic
+        private fun getFileConflictData(file: File, rightFile: OCFile, context: Context): ConflictDialogData {
+            val parentFile = File(rightFile.remotePath).parentFile
+            val folderName = if (parentFile != null) {
+                String.format(context.getString(R.string.in_folder), parentFile.absolutePath)
+            } else {
+                null
+            }
+
+            val leftTitle = context.getString(R.string.conflict_local_file)
+            val leftTimestamp = DisplayUtils.getRelativeTimestamp(context, file.lastModified())
+            val leftFileSize = DisplayUtils.bytesToHumanReadable(file.length())
+            val leftCheckBoxData = ConflictFileData(leftTitle, leftTimestamp.toString(), leftFileSize)
+
+            val rightTitle = context.getString(R.string.conflict_server_file)
+            val rightTimestamp = DisplayUtils.getRelativeTimestamp(context, rightFile.modificationTimestamp)
+            val rightFileSize = DisplayUtils.bytesToHumanReadable(rightFile.fileLength)
+            val rightCheckBoxData = ConflictFileData(rightTitle, rightTimestamp.toString(), rightFileSize)
+
+            val title = context.getString(R.string.choose_which_file)
+            val description = context.getString(R.string.conflict_message_description)
+            return ConflictDialogData(folderName, title, description, Pair(leftCheckBoxData, rightCheckBoxData))
+        }
     }
 }

+ 32 - 7
app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.kt

@@ -19,22 +19,28 @@ import android.view.View
 import android.widget.TextView
 import androidx.appcompat.app.AlertDialog
 import androidx.fragment.app.DialogFragment
+import androidx.lifecycle.lifecycleScope
 import com.google.android.material.button.MaterialButton
 import com.google.android.material.dialog.MaterialAlertDialogBuilder
 import com.google.common.collect.Sets
 import com.nextcloud.client.account.CurrentAccountProvider
 import com.nextcloud.client.di.Injectable
+import com.nextcloud.client.network.ConnectivityService
 import com.nextcloud.utils.extensions.getParcelableArgument
 import com.nextcloud.utils.fileNameValidator.FileNameValidator
 import com.owncloud.android.R
 import com.owncloud.android.databinding.EditBoxDialogBinding
 import com.owncloud.android.datamodel.FileDataStorageManager
 import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.lib.common.utils.Log_OC
 import com.owncloud.android.lib.resources.status.OCCapability
 import com.owncloud.android.ui.activity.ComponentsGetter
+import com.owncloud.android.ui.activity.FileDisplayActivity
 import com.owncloud.android.utils.DisplayUtils
 import com.owncloud.android.utils.KeyboardUtils
 import com.owncloud.android.utils.theme.ViewThemeUtils
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
 import javax.inject.Inject
 
 /**
@@ -55,9 +61,12 @@ class CreateFolderDialogFragment : DialogFragment(), DialogInterface.OnClickList
     lateinit var keyboardUtils: KeyboardUtils
 
     @Inject
-    lateinit var currentAccount: CurrentAccountProvider
+    lateinit var connectivityService: ConnectivityService
 
-    private var mParentFolder: OCFile? = null
+    @Inject
+    lateinit var accountProvider: CurrentAccountProvider
+
+    private var parentFolder: OCFile? = null
     private var positiveButton: MaterialButton? = null
 
     private lateinit var binding: EditBoxDialogBinding
@@ -92,7 +101,7 @@ class CreateFolderDialogFragment : DialogFragment(), DialogInterface.OnClickList
 
     @Suppress("EmptyFunctionBlock")
     override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
-        mParentFolder = arguments?.getParcelableArgument(ARG_PARENT_FOLDER, OCFile::class.java)
+        parentFolder = arguments?.getParcelableArgument(ARG_PARENT_FOLDER, OCFile::class.java)
 
         val inflater = requireActivity().layoutInflater
         binding = EditBoxDialogBinding.inflate(inflater, null, false)
@@ -121,7 +130,7 @@ class CreateFolderDialogFragment : DialogFragment(), DialogInterface.OnClickList
         return builder.create()
     }
 
-    private fun getOCCapability(): OCCapability = fileDataStorageManager.getCapability(currentAccount.user.accountName)
+    private fun getOCCapability(): OCCapability = fileDataStorageManager.getCapability(accountProvider.user.accountName)
 
     private fun checkFileNameAfterEachType(fileNames: MutableSet<String>) {
         val newFileName = binding.userInput.text?.toString()?.trim() ?: ""
@@ -173,14 +182,30 @@ class CreateFolderDialogFragment : DialogFragment(), DialogInterface.OnClickList
                 return
             }
 
-            val path = mParentFolder?.decryptedRemotePath + newFolderName + OCFile.PATH_SEPARATOR
-            if (requireActivity() is ComponentsGetter) {
-                (requireActivity() as ComponentsGetter).fileOperationsHelper.createFolder(path)
+            val path = parentFolder?.decryptedRemotePath + newFolderName + OCFile.PATH_SEPARATOR
+            lifecycleScope.launch(Dispatchers.IO) {
+                if (connectivityService.isNetworkAndServerAvailable()) {
+                    (requireActivity() as ComponentsGetter).fileOperationsHelper.createFolder(path)
+                } else {
+                    Log_OC.d(TAG, "Network not available, creating offline operation")
+                    fileDataStorageManager.addCreateFolderOfflineOperation(
+                        path,
+                        newFolderName,
+                        parentFolder?.offlineOperationParentPath,
+                        parentFolder?.fileId
+                    )
+
+                    launch(Dispatchers.Main) {
+                        val fileDisplayActivity = requireActivity() as? FileDisplayActivity
+                        fileDisplayActivity?.syncAndUpdateFolder(true)
+                    }
+                }
             }
         }
     }
 
     companion object {
+        private const val TAG = "CreateFolderDialogFragment"
         private const val ARG_PARENT_FOLDER = "PARENT_FOLDER"
         const val CREATE_FOLDER_FRAGMENT = "CREATE_FOLDER_FRAGMENT"
 

+ 28 - 5
app/src/main/java/com/owncloud/android/ui/dialog/RemoveFilesDialogFragment.kt

@@ -18,9 +18,12 @@ import androidx.appcompat.app.AlertDialog
 import com.google.android.material.button.MaterialButton
 import com.nextcloud.client.di.Injectable
 import com.owncloud.android.R
+import com.owncloud.android.datamodel.FileDataStorageManager
 import com.owncloud.android.datamodel.OCFile
 import com.owncloud.android.ui.activity.ComponentsGetter
+import com.owncloud.android.ui.activity.FileDisplayActivity
 import com.owncloud.android.ui.dialog.ConfirmationDialogFragment.ConfirmationDialogFragmentListener
+import javax.inject.Inject
 
 /**
  * Dialog requiring confirmation before removing a collection of given OCFiles.
@@ -30,14 +33,19 @@ class RemoveFilesDialogFragment : ConfirmationDialogFragment(), ConfirmationDial
     private var mTargetFiles: Collection<OCFile>? = null
     private var actionMode: ActionMode? = null
 
+    @Inject
+    lateinit var fileDataStorageManager: FileDataStorageManager
+
+    private var positiveButton: MaterialButton? = null
+
     override fun onStart() {
         super.onStart()
 
         val alertDialog = dialog as AlertDialog? ?: return
 
-        val positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE) as? MaterialButton
+        positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE) as? MaterialButton
         positiveButton?.let {
-            viewThemeUtils?.material?.colorMaterialButtonPrimaryTonal(positiveButton)
+            viewThemeUtils?.material?.colorMaterialButtonPrimaryTonal(it)
         }
 
         val negativeButton = alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE) as? MaterialButton
@@ -76,8 +84,22 @@ class RemoveFilesDialogFragment : ConfirmationDialogFragment(), ConfirmationDial
     }
 
     private fun removeFiles(onlyLocalCopy: Boolean) {
-        val cg = activity as ComponentsGetter?
-        cg?.fileOperationsHelper?.removeFiles(mTargetFiles, onlyLocalCopy, false)
+        val (offlineFiles, files) = mTargetFiles?.partition { it.isOfflineOperation } ?: Pair(emptyList(), emptyList())
+
+        offlineFiles.forEach {
+            fileDataStorageManager.deleteOfflineOperation(it)
+        }
+
+        if (files.isNotEmpty()) {
+            val cg = activity as ComponentsGetter?
+            cg?.fileOperationsHelper?.removeFiles(files, onlyLocalCopy, false)
+        }
+
+        if (offlineFiles.isNotEmpty()) {
+            val activity = requireActivity() as? FileDisplayActivity
+            activity?.refreshCurrentDirectory()
+        }
+
         finishActionMode()
     }
 
@@ -151,7 +173,8 @@ class RemoveFilesDialogFragment : ConfirmationDialogFragment(), ConfirmationDial
 
                 putInt(ARG_POSITIVE_BTN_RES, R.string.file_delete)
 
-                if (containsFolder || containsDown) {
+                val isAnyFileOffline = files.any { it.isOfflineOperation }
+                if ((containsFolder || containsDown) && !isAnyFileOffline) {
                     putInt(ARG_NEGATIVE_BTN_RES, R.string.confirmation_remove_local)
                 }
 

+ 10 - 4
app/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.kt

@@ -24,14 +24,15 @@ import com.google.common.collect.Sets
 import com.nextcloud.client.account.CurrentAccountProvider
 import com.nextcloud.client.di.Injectable
 import com.nextcloud.utils.extensions.getParcelableArgument
-import com.nextcloud.utils.fileNameValidator.FileNameValidator.isFileHidden
 import com.nextcloud.utils.fileNameValidator.FileNameValidator.checkFileName
+import com.nextcloud.utils.fileNameValidator.FileNameValidator.isFileHidden
 import com.owncloud.android.R
 import com.owncloud.android.databinding.EditBoxDialogBinding
 import com.owncloud.android.datamodel.FileDataStorageManager
 import com.owncloud.android.datamodel.OCFile
 import com.owncloud.android.lib.resources.status.OCCapability
 import com.owncloud.android.ui.activity.ComponentsGetter
+import com.owncloud.android.ui.activity.FileDisplayActivity
 import com.owncloud.android.utils.DisplayUtils
 import com.owncloud.android.utils.KeyboardUtils
 import com.owncloud.android.utils.theme.ViewThemeUtils
@@ -144,9 +145,14 @@ class RenameFileDialogFragment : DialogFragment(), DialogInterface.OnClickListen
                 return
             }
 
-            if (requireActivity() is ComponentsGetter) {
-                val componentsGetter = requireActivity() as ComponentsGetter
-                componentsGetter.getFileOperationsHelper().renameFile(mTargetFile, newFileName)
+            if (mTargetFile?.isOfflineOperation == true) {
+                fileDataStorageManager.renameCreateFolderOfflineOperation(mTargetFile, newFileName)
+                if (requireActivity() is FileDisplayActivity) {
+                    val activity = requireActivity() as FileDisplayActivity
+                    activity.refreshCurrentDirectory()
+                }
+            } else {
+                (requireActivity() as ComponentsGetter).fileOperationsHelper.renameFile(mTargetFile, newFileName)
             }
         }
     }

+ 69 - 0
app/src/main/java/com/owncloud/android/ui/dialog/parcel/ConflictDialogData.kt

@@ -0,0 +1,69 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package com.owncloud.android.ui.dialog.parcel
+
+import android.os.Parcel
+import android.os.Parcelable
+import com.nextcloud.utils.extensions.readParcelableCompat
+
+data class ConflictDialogData(
+    val folderName: String?,
+    val title: String?,
+    val description: String,
+    val checkboxData: Pair<ConflictFileData, ConflictFileData>
+) : Parcelable {
+    constructor(parcel: Parcel) : this(
+        parcel.readString() ?: "",
+        parcel.readString() ?: "",
+        parcel.readString() ?: "",
+        checkboxData = Pair(
+            parcel.readParcelableCompat(ConflictFileData::class.java.classLoader) ?: ConflictFileData("", "", ""),
+            parcel.readParcelableCompat(ConflictFileData::class.java.classLoader) ?: ConflictFileData("", "", "")
+        )
+    )
+
+    override fun writeToParcel(parcel: Parcel, flags: Int) {
+        parcel.writeString(folderName)
+        parcel.writeString(title)
+        parcel.writeString(description)
+        parcel.writeParcelable(checkboxData.first, flags)
+        parcel.writeParcelable(checkboxData.second, flags)
+    }
+
+    override fun describeContents(): Int = 0
+
+    companion object CREATOR : Parcelable.Creator<ConflictDialogData> {
+        override fun createFromParcel(parcel: Parcel): ConflictDialogData = ConflictDialogData(parcel)
+        override fun newArray(size: Int): Array<ConflictDialogData?> = arrayOfNulls(size)
+    }
+}
+
+data class ConflictFileData(
+    val title: String,
+    val timestamp: String,
+    val fileSize: String
+) : Parcelable {
+    constructor(parcel: Parcel) : this(
+        parcel.readString() ?: "",
+        parcel.readString() ?: "",
+        parcel.readString() ?: ""
+    )
+
+    override fun writeToParcel(parcel: Parcel, flags: Int) {
+        parcel.writeString(title)
+        parcel.writeString(timestamp)
+        parcel.writeString(fileSize)
+    }
+
+    override fun describeContents(): Int = 0
+
+    companion object CREATOR : Parcelable.Creator<ConflictFileData> {
+        override fun createFromParcel(parcel: Parcel): ConflictFileData = ConflictFileData(parcel)
+        override fun newArray(size: Int): Array<ConflictFileData?> = arrayOfNulls(size)
+    }
+}

+ 4 - 1
app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java

@@ -452,7 +452,10 @@ public class FileDetailFragment extends FileFragment implements OnClickListener,
                                                                     backgroundJobManager);
         } else if (itemId == R.id.action_set_as_wallpaper) {
             containerActivity.getFileOperationsHelper().setPictureAs(getFile(), getView());
-        } else if (itemId == R.id.action_encrypted) {// TODO implement or remove
+        } else if (itemId == R.id.action_retry) {
+            backgroundJobManager.startOfflineOperations();
+        } else if (itemId == R.id.action_encrypted) {
+            // TODO implement or remove
         } else if (itemId == R.id.action_unset_encrypted) {// TODO implement or remove
         }
     }

+ 17 - 0
app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetDialog.java

@@ -157,6 +157,7 @@ public class OCFileListBottomSheetDialog extends BottomSheetDialog implements In
         }
 
         setupClickListener();
+        filterActionsForOfflineOperations();
     }
 
     private void setupClickListener() {
@@ -210,6 +211,22 @@ public class OCFileListBottomSheetDialog extends BottomSheetDialog implements In
         });
     }
 
+    private void filterActionsForOfflineOperations() {
+        if (!file.isOfflineOperation() || file.isRootDirectory()) {
+            return;
+        }
+
+        binding.menuCreateRichWorkspace.setVisibility(View.GONE);
+        binding.menuUploadFromApp.setVisibility(View.GONE);
+        binding.menuDirectCameraUpload.setVisibility(View.GONE);
+        binding.menuScanDocUpload.setVisibility(View.GONE);
+        binding.menuUploadFiles.setVisibility(View.GONE);
+        binding.menuNewDocument.setVisibility(View.GONE);
+        binding.menuNewSpreadsheet.setVisibility(View.GONE);
+        binding.menuNewPresentation.setVisibility(View.GONE);
+        binding.creatorsContainer.setVisibility(View.GONE);
+    }
+
     @Override
     protected void onStop() {
         super.onStop();

+ 21 - 12
app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java

@@ -123,6 +123,7 @@ import org.greenrobot.eventbus.ThreadMode;
 
 import java.io.File;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.List;
@@ -628,22 +629,28 @@ public class OCFileListFragment extends ExtendedListFragment implements
 
     public void openActionsMenu(final int filesCount, final Set<OCFile> checkedFiles, final boolean isOverflow) {
         throttler.run("overflowClick", () -> {
-            final FragmentManager childFragmentManager = getChildFragmentManager();
-
             List<Integer> toHide = new ArrayList<>();
+
+            for (OCFile file : checkedFiles) {
+                if (file.isOfflineOperation()) {
+                    toHide = new ArrayList<>(
+                        Arrays.asList(R.id.action_favorite, R.id.action_move_or_copy, R.id.action_sync_file, R.id.action_encrypted, R.id.action_unset_encrypted)
+                    );
+                    break;
+                }
+            }
+
             if (isAPKorAAB(checkedFiles)) {
                 toHide.add(R.id.action_send_share_file);
                 toHide.add(R.id.action_export_file);
                 toHide.add(R.id.action_sync_file);
                 toHide.add(R.id.action_download_file);
             }
-            
+
+            final FragmentManager childFragmentManager = getChildFragmentManager();
             FileActionsBottomSheet.newInstance(filesCount, checkedFiles, isOverflow, toHide)
-                .setResultListener(childFragmentManager, this, (id) -> {
-                    onFileActionChosen(id, checkedFiles);
-                })
+                .setResultListener(childFragmentManager, this, (id) -> onFileActionChosen(id, checkedFiles))
                 .show(childFragmentManager, "actions");
-            ;
         });
     }
 
@@ -1108,10 +1115,6 @@ public class OCFileListFragment extends ExtendedListFragment implements
     @Override
     @OptIn(markerClass = UnstableApi.class)
     public void onItemClicked(OCFile file) {
-        if (mContainerActivity != null && mContainerActivity instanceof FileActivity fileActivity) {
-            fileActivity.checkInternetConnection();
-        }
-
         if (getCommonAdapter() != null && getCommonAdapter().isMultiSelect()) {
             toggleItemToCheckedList(file);
         } else {
@@ -1240,6 +1243,9 @@ public class OCFileListFragment extends ExtendedListFragment implements
             } else if (itemId == R.id.action_pin_to_homescreen) {
                 shortcutUtil.addShortcutToHomescreen(singleFile, viewThemeUtils, accountManager.getUser(), syncedFolderProvider);
                 return true;
+            } else if (itemId == R.id.action_retry) {
+                backgroundJobManager.startOfflineOperations();
+                return true;
             }
         }
 
@@ -1478,7 +1484,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
         }
 
         // FAB
-        setFabEnabled(mFile != null && mFile.canWrite());
+        setFabEnabled(mFile != null && (mFile.canWrite() || mFile.isOfflineOperation()));
 
         invalidateActionMode();
     }
@@ -1980,6 +1986,9 @@ public class OCFileListFragment extends ExtendedListFragment implements
      */
     public void selectAllFiles(boolean select) {
         OCFileListAdapter ocFileListAdapter = (OCFileListAdapter) getRecyclerView().getAdapter();
+        if (ocFileListAdapter == null) {
+            return;
+        }
 
         if (select) {
             ocFileListAdapter.addAllFilesToCheckedFiles();

+ 1 - 0
app/src/main/java/com/owncloud/android/ui/notifications/NotificationUtils.java

@@ -33,6 +33,7 @@ public final class NotificationUtils {
     public static final String NOTIFICATION_CHANNEL_FILE_SYNC = "NOTIFICATION_CHANNEL_FILE_SYNC";
     public static final String NOTIFICATION_CHANNEL_FILE_OBSERVER = "NOTIFICATION_CHANNEL_FILE_OBSERVER";
     public static final String NOTIFICATION_CHANNEL_PUSH = "NOTIFICATION_CHANNEL_PUSH";
+    public static final String NOTIFICATION_CHANNEL_BACKGROUND_OPERATIONS = "NOTIFICATION_CHANNEL_BACKGROUND_OPERATIONS";
 
     private NotificationUtils() {
         // utility class -> private constructor

+ 24 - 0
app/src/main/res/drawable/ic_retry.xml

@@ -0,0 +1,24 @@
+<!--
+  ~ Nextcloud - Android Client
+  ~
+  ~ SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
+  ~ SPDX-FileCopyrightText: 2024 Nextcloud GmbH
+  ~ SPDX-FileCopyrightText: 2018-2024 Google LLC
+  ~ SPDX-License-Identifier: Apache-2.0
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="#000000"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M12,5V2L8,6l4,4V7c3.31,0 6,2.69 6,6c0,2.97 -2.17,5.43 -5,5.91v2.02c3.95,-0.49 7,-3.85 7,-7.93C20,8.58 16.42,5 12,5z" />
+
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M6,13c0,-1.65 0.67,-3.15 1.76,-4.24L6.34,7.34C4.9,8.79 4,10.79 4,13c0,4.08 3.05,7.44 7,7.93v-2.02C8.17,18.43 6,15.97 6,13z" />
+
+</vector>

+ 16 - 11
app/src/main/res/layout/conflict_resolve_dialog.xml

@@ -17,44 +17,49 @@
     android:paddingBottom="@dimen/standard_padding">
 
     <TextView
-        android:id="@+id/in"
+        android:id="@+id/folder_name"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:text="@string/in_folder"
         android:paddingBottom="@dimen/standard_padding" />
 
     <TextView
+        android:id="@+id/title"
+        android:layout_marginTop="@dimen/standard_margin"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:text="@string/choose_which_file"
         android:textStyle="bold" />
 
     <TextView
+        android:id="@+id/description"
+        android:layout_marginTop="@dimen/standard_margin"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:text="@string/conflict_message_description" />
 
     <LinearLayout
+        android:layout_marginTop="@dimen/standard_margin"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:orientation="horizontal"
         android:baselineAligned="false">
 
         <LinearLayout
-            android:id="@+id/newFileContainer"
+            android:id="@+id/leftFileContainer"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             android:layout_weight="1"
             android:orientation="vertical">
 
             <androidx.appcompat.widget.AppCompatCheckBox
-                android:id="@+id/new_checkbox"
+                android:id="@+id/left_checkbox"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:text="@string/conflict_local_file" />
 
             <ImageView
-                android:id="@+id/new_thumbnail"
+                android:id="@+id/left_thumbnail"
                 android:layout_width="match_parent"
                 android:layout_height="80dp"
                 android:layout_margin="@dimen/standard_half_margin"
@@ -62,33 +67,33 @@
                 android:contentDescription="@string/thumbnail_for_new_file_desc" />
 
             <TextView
-                android:id="@+id/new_timestamp"
+                android:id="@+id/left_timestamp"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 tools:text="12. Dec 2020 - 23:10:20" />
 
             <TextView
-                android:id="@+id/new_size"
+                android:id="@+id/left_file_size"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 tools:text="5 Mb" />
         </LinearLayout>
 
         <LinearLayout
-            android:id="@+id/existingFileContainer"
+            android:id="@+id/rightFileContainer"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             android:layout_weight="1"
             android:orientation="vertical">
 
             <androidx.appcompat.widget.AppCompatCheckBox
-                android:id="@+id/existing_checkbox"
+                android:id="@+id/right_checkbox"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:text="@string/conflict_server_file" />
 
             <ImageView
-                android:id="@+id/existing_thumbnail"
+                android:id="@+id/right_thumbnail"
                 android:layout_width="match_parent"
                 android:layout_height="80dp"
                 android:layout_margin="@dimen/standard_half_margin"
@@ -96,13 +101,13 @@
                 android:contentDescription="@string/thumbnail_for_existing_file_description" />
 
             <TextView
-                android:id="@+id/existing_timestamp"
+                android:id="@+id/right_timestamp"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 tools:text="10. Dec 2020 - 10:10:10" />
 
             <TextView
-                android:id="@+id/existing_size"
+                android:id="@+id/right_file_size"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 tools:text="3 Mb" />

+ 0 - 1
app/src/main/res/values-ar/strings.xml

@@ -179,7 +179,6 @@
     <string name="confirmation_remove_folders_alert">هل توَدُّ حقاً حذف العناصر المختارة وما يحتوّه؟</string>
     <string name="confirmation_remove_local">محلياً فقط</string>
     <string name="conflict_dialog_error">تعذّر إنشاء نافذة حوار حل التعارضات</string>
-    <string name="conflict_file_headline">ملف متضارب %1$s </string>
     <string name="conflict_local_file">ملف محلي</string>
     <string name="conflict_message_description">إذا قمت باختيار كلا الاصدارين, الملف المحلي سيحتوي على رقم ملحق باسم الملف.</string>
     <string name="conflict_server_file">ملف على الخادم</string>

+ 0 - 1
app/src/main/res/values-b+en+001/strings.xml

@@ -179,7 +179,6 @@
     <string name="confirmation_remove_folders_alert">Do you really want to delete the selected items and their contents?</string>
     <string name="confirmation_remove_local">Local only</string>
     <string name="conflict_dialog_error">Conflict resolver dialog cannot be created</string>
-    <string name="conflict_file_headline">Conflicting file %1$s</string>
     <string name="conflict_local_file">Local file</string>
     <string name="conflict_message_description">If you select both versions, the local file will have a number appended to its name.</string>
     <string name="conflict_server_file">Server file</string>

+ 0 - 1
app/src/main/res/values-bg-rBG/strings.xml

@@ -153,7 +153,6 @@
     <string name="confirmation_remove_folder_alert">Наистина ли желаете %1$s и съдържанието ѝ да бъдат изтрито?</string>
     <string name="confirmation_remove_folders_alert">Наистина ли желаете избраните елементи и съдържанието им да бъдат премахнати?</string>
     <string name="confirmation_remove_local">Само локално</string>
-    <string name="conflict_file_headline">Несъвместим файл %1$s</string>
     <string name="conflict_local_file">Локален файл</string>
     <string name="conflict_message_description">Ако изберете и двете версии, ще бъде добавен номер към името на локалния файл.</string>
     <string name="conflict_server_file">Файл на сървъра</string>

+ 0 - 1
app/src/main/res/values-br/strings.xml

@@ -179,7 +179,6 @@
     <string name="confirmation_remove_folders_alert">Ha sur oc\'h e fell deoc\'h dilemel an elfennoù diuzet ha kement tra zo e-barzh . </string>
     <string name="confirmation_remove_local">Lec\'hel hepken</string>
     <string name="conflict_dialog_error">N\'eus ket bet gallet krouiñ un egorenn diskoulmañ tabutoù</string>
-    <string name="conflict_file_headline">Restr kudennek %1$s</string>
     <string name="conflict_local_file">Restr lec\'hel</string>
     <string name="conflict_message_description">Ma tibabit an daou stumm e vo staget un niverenn ouzh anv ar restr lec\'hel</string>
     <string name="conflict_server_file">Restr ar servijer</string>

+ 0 - 1
app/src/main/res/values-ca/strings.xml

@@ -152,7 +152,6 @@
     <string name="confirmation_remove_folder_alert">Esteu segur que voleu suprimir %1$s i els seus continguts?</string>
     <string name="confirmation_remove_folders_alert">Esteu segur que voleu suprimir els elements seleccionats i el seu contingut?</string>
     <string name="confirmation_remove_local">Només local</string>
-    <string name="conflict_file_headline">Fitxer en conflicte %1$s</string>
     <string name="conflict_local_file">Fitxer local</string>
     <string name="conflict_message_description">Si seleccioneu ambdues versions, s\'afegirà un numero al nom del fitxer local.</string>
     <string name="conflict_server_file">Fitxer del servidor</string>

+ 0 - 1
app/src/main/res/values-cs-rCZ/strings.xml

@@ -170,7 +170,6 @@
     <string name="confirmation_remove_folder_alert">Opravdu chcete %1$s a jeho obsah odstranit?</string>
     <string name="confirmation_remove_folders_alert">Opravdu chcete vybrané položky a jejich obsah odstranit?</string>
     <string name="confirmation_remove_local">Pouze místní</string>
-    <string name="conflict_file_headline">Kolidující soubor %1$s</string>
     <string name="conflict_local_file">Místní soubor</string>
     <string name="conflict_message_description">Pokud zvolíte obě verze, k názvu místního souboru bude připojeno číslo.</string>
     <string name="conflict_server_file">Soubor na serveru</string>

+ 0 - 1
app/src/main/res/values-da/strings.xml

@@ -164,7 +164,6 @@
     <string name="confirmation_remove_folder_alert">Er du sikker på at du vil slette %1$s med indhold?</string>
     <string name="confirmation_remove_folders_alert">Er du sikker på at du vil slette de valgte artikler med indhold?</string>
     <string name="confirmation_remove_local">Kun lokal</string>
-    <string name="conflict_file_headline">Fil i konflikt %1$s</string>
     <string name="conflict_local_file">Lokal fil</string>
     <string name="conflict_message_description">Hvis du vælger begge versioner, vil den lokale fil få tilføjet et nummer til sit navn.</string>
     <string name="conflict_server_file">Server fil</string>

+ 0 - 1
app/src/main/res/values-de/strings.xml

@@ -179,7 +179,6 @@
     <string name="confirmation_remove_folders_alert">Möchten Sie die ausgewählten Elemente und deren inhalt wirklich löschen?</string>
     <string name="confirmation_remove_local">Nur lokal</string>
     <string name="conflict_dialog_error">Konfliktlösungsdialog konnte nicht erstellt werden</string>
-    <string name="conflict_file_headline">Konflikt-Datei %1$s</string>
     <string name="conflict_local_file">Lokale Datei</string>
     <string name="conflict_message_description">Falls beide Versionen gewählt werden, wird bei der lokalen Datei eine Zahl am Ende des Dateinamens hinzugefügt.</string>
     <string name="conflict_server_file">Server-Datei</string>

+ 0 - 1
app/src/main/res/values-el/strings.xml

@@ -151,7 +151,6 @@
     <string name="confirmation_remove_folder_alert">Θέλετε σίγουρα να διαγράψετε το %1$s και τα περιεχόμενά του;</string>
     <string name="confirmation_remove_folders_alert">Θέλετε να διαγράψετε τα επιλεγμένα αντικείμενα και τα περιεχόμενά τους;</string>
     <string name="confirmation_remove_local">Μόνο τοπικά</string>
-    <string name="conflict_file_headline">Αρχείο σε αντίφαση %1$s</string>
     <string name="conflict_local_file">Τοπικό αρχείο</string>
     <string name="conflict_message_description">Εάν επιλέξετε και τις δύο εκδόσεις, στο όνομα του τοπικού αρχείου θα προστεθεί ένας αριθμός.</string>
     <string name="conflict_server_file">Αρχείο διακομιστή</string>

+ 0 - 1
app/src/main/res/values-es-rAR/strings.xml

@@ -179,7 +179,6 @@
     <string name="confirmation_remove_folders_alert">¿Realmente desea eliminar los elementos seleccionados y sus contenidos?</string>
     <string name="confirmation_remove_local">Sólo local</string>
     <string name="conflict_dialog_error">El diálogo de resolución de conflictos no puede ser creado</string>
-    <string name="conflict_file_headline">Archivo en conflicto %1$s</string>
     <string name="conflict_local_file">Archivo local</string>
     <string name="conflict_message_description">Si selecciona ambas versiones, se le agregará un número al nombre del archivo copiado.</string>
     <string name="conflict_server_file">Archivo del servidor</string>

+ 0 - 1
app/src/main/res/values-es-rEC/strings.xml

@@ -153,7 +153,6 @@
     <string name="confirmation_remove_folder_alert">¿Realmente quieres eliminar %1$s y sus contenidos? </string>
     <string name="confirmation_remove_folders_alert">¿Reamente deseas eliminar los elementos seleccionados y sus contenidos?</string>
     <string name="confirmation_remove_local">Sólo local</string>
-    <string name="conflict_file_headline">Archivo en conflicto %1$s</string>
     <string name="conflict_local_file">Archivo local</string>
     <string name="conflict_message_description">Si selecciona ambas versiones, el archivo local tendrá un número agregado a su nombre.</string>
     <string name="conflict_server_file">Archivo del servidor</string>

+ 0 - 1
app/src/main/res/values-es-rMX/strings.xml

@@ -179,7 +179,6 @@
     <string name="confirmation_remove_folders_alert">¿Reamente deseas eliminar los elementos seleccionados y sus contenidos?</string>
     <string name="confirmation_remove_local">Sólo local</string>
     <string name="conflict_dialog_error">El diálogo de resolución de conflictos no puede ser creado</string>
-    <string name="conflict_file_headline">Archivo conflictivo %1$s</string>
     <string name="conflict_local_file">Archivo local</string>
     <string name="conflict_message_description">Si seleccionas ambas versiones, el archivo local tendrá un número al final del nombre.</string>
     <string name="conflict_server_file">Archivo del servidor</string>

+ 0 - 1
app/src/main/res/values-es/strings.xml

@@ -178,7 +178,6 @@
     <string name="confirmation_remove_folders_alert">¿Estás seguro de que quieres eliminar los elementos seleccionados y sus contenidos?</string>
     <string name="confirmation_remove_local">Solo local</string>
     <string name="conflict_dialog_error">El diálogo de resolución de conflictos no puede ser creado</string>
-    <string name="conflict_file_headline">Conflicto en archivo %1$s</string>
     <string name="conflict_local_file">Archivo local</string>
     <string name="conflict_message_description">Si seleccionas ambas versiones, el archivo local tendrá un número añadido a su nombre.</string>
     <string name="conflict_server_file">Archivo del servidor</string>

+ 0 - 1
app/src/main/res/values-eu/strings.xml

@@ -179,7 +179,6 @@
     <string name="confirmation_remove_folders_alert">Ziur zaude hautatutako elementuak eta beren edukiak ezabatu nahi dituzula?</string>
     <string name="confirmation_remove_local">Lokala bakarrik</string>
     <string name="conflict_dialog_error">Gatazkak konpontzeko elkarrizketa-koadroa ezin da sortu</string>
-    <string name="conflict_file_headline">%1$sfitxategi gatazkatsua</string>
     <string name="conflict_local_file">Fitxategi lokala</string>
     <string name="conflict_message_description">Bi bertsioak hautatzen badituzu, fitxategi lokalaren izenari zenbaki bat gehituko zaio.</string>
     <string name="conflict_server_file">Zerbitzariko fitxategia</string>

+ 0 - 1
app/src/main/res/values-fa/strings.xml

@@ -158,7 +158,6 @@
     <string name="confirmation_remove_folder_alert">آیا واقعا می خواهید %1$s و محتویات آن را حذف کنید؟</string>
     <string name="confirmation_remove_folders_alert">آیا واقعاً می‌خواهید موارد انتخاب شده و محتوای آنها حذف شود؟</string>
     <string name="confirmation_remove_local">فقط محلی</string>
-    <string name="conflict_file_headline">فایل متناقض %1$s</string>
     <string name="conflict_local_file">پروندهٔ محلّی</string>
     <string name="conflict_message_description">اگر هردو نسخه را انتخاب کنید، یک شماره به نام فایل محلی اضافه خواهد شد.</string>
     <string name="conflict_server_file">پروندهٔ کارساز</string>

+ 0 - 1
app/src/main/res/values-fi-rFI/strings.xml

@@ -158,7 +158,6 @@
     <string name="confirmation_remove_folder_alert">Haluatko varmasti poistaa kohteen %1$s ja sen sisällön?</string>
     <string name="confirmation_remove_folders_alert">Haluatko varmasti poistaa valitut kohteet ja niiden sisällön?</string>
     <string name="confirmation_remove_local">Vain paikallisen</string>
-    <string name="conflict_file_headline">Ristiriitainen kohde %1$s</string>
     <string name="conflict_local_file">Paikallinen tiedosto</string>
     <string name="conflict_message_description">Jos valitset molemmat versiot, paikallisen tiedoston nimeen lisätään numero.</string>
     <string name="conflict_server_file">Palvelintiedosto</string>

+ 0 - 1
app/src/main/res/values-fr/strings.xml

@@ -179,7 +179,6 @@
     <string name="confirmation_remove_folders_alert">Souhaitez-vous vraiment supprimer les éléments sélectionnés ainsi que leurs contenus ?</string>
     <string name="confirmation_remove_local">Local seulement</string>
     <string name="conflict_dialog_error">Erreur lors de la création de la boîte de dialogue de conflit !</string>
-    <string name="conflict_file_headline">Fichier %1$s en conflit</string>
     <string name="conflict_local_file">fichier local</string>
     <string name="conflict_message_description">Si vous sélectionnez les deux versions, le fichier local aura un numéro ajouté à son nom.</string>
     <string name="conflict_server_file">Fichier serveur</string>

+ 0 - 1
app/src/main/res/values-ga/strings.xml

@@ -179,7 +179,6 @@
     <string name="confirmation_remove_folders_alert">An bhfuil tú cinnte gur mhaith leat na míreanna roghnaithe agus a bhfuil iontu a scriosadh?</string>
     <string name="confirmation_remove_local">Áitiúil amháin</string>
     <string name="conflict_dialog_error">Ní féidir dialóg réititheora coinbhleachta a chruthú</string>
-    <string name="conflict_file_headline">Comhad contrártha %1$s</string>
     <string name="conflict_local_file">Comhad áitiúil</string>
     <string name="conflict_message_description">Má roghnaíonn tú an dá leagan, beidh uimhir ag gabháil leis an gcomhad áitiúil lena ainm.</string>
     <string name="conflict_server_file">Comhad freastalaí</string>

+ 0 - 1
app/src/main/res/values-gd/strings.xml

@@ -137,7 +137,6 @@
     <string name="confirmation_remove_folder_alert">A bheil thu cinnteach gu bheil thu airson %1$s ’s a shusbaint a sguabadh às?</string>
     <string name="confirmation_remove_folders_alert">A bheil thu cinnteach gu bheil thu airson na nithean a thagh thu ’s an susbaint a sguabadh às?</string>
     <string name="confirmation_remove_local">Ionadail a-mhàin</string>
-    <string name="conflict_file_headline">Faidhle %1$s ann an còmhstri</string>
     <string name="conflict_message_description">Ma thaghas tu an dà thionndadh, thèid àireamh a chur ri ainm an fhaidhle ionadail.</string>
     <string name="contactlist_item_icon">Ìomhaigheag a’ chleachdaiche air liosta an luchd-aithne</string>
     <string name="contactlist_no_permission">Cha deach cead a thoirt ’s cha deach càil ion-phortadh.</string>

+ 0 - 1
app/src/main/res/values-gl/strings.xml

@@ -179,7 +179,6 @@
     <string name="confirmation_remove_folders_alert">Confirma que quere eliminar os elementos seleccionados e o seu contido?</string>
     <string name="confirmation_remove_local">Só local</string>
     <string name="conflict_dialog_error">Non é posíbel crear o diálogo de resolución de conflitos</string>
-    <string name="conflict_file_headline">Ficheiro en conflito %1$s</string>
     <string name="conflict_local_file">Ficheiro local</string>
     <string name="conflict_message_description">Se selecciona ambas versións, o ficheiro local terá un número engadido ao nome.</string>
     <string name="conflict_server_file">Ficheiro do servidor</string>

+ 0 - 1
app/src/main/res/values-hr/strings.xml

@@ -145,7 +145,6 @@
     <string name="confirmation_remove_folder_alert">Želite li zaista izbrisati %1$s i pripadajući sadržaj?</string>
     <string name="confirmation_remove_folders_alert">Želite li zaista izbrisati odabrane stavke i pripadajući sadržaj?</string>
     <string name="confirmation_remove_local">Samo lokalno</string>
-    <string name="conflict_file_headline">Nepodudarna datoteka %1$s</string>
     <string name="conflict_local_file">Lokalna datoteka</string>
     <string name="conflict_message_description">Ako odaberete obje inačice, lokalna će datoteka uz naziv imati i broj.</string>
     <string name="conflict_server_file">Datoteka na poslužitelju</string>

+ 0 - 1
app/src/main/res/values-hu-rHU/strings.xml

@@ -155,7 +155,6 @@
     <string name="confirmation_remove_folder_alert">Biztos, hogy törli ezt: %1$s és a tartalmát?</string>
     <string name="confirmation_remove_folders_alert">Biztos, hogy törli a kiválasztott elemeket és tartalmukat?</string>
     <string name="confirmation_remove_local">Csak a helyi példány</string>
-    <string name="conflict_file_headline">Ütköző fájl: %1$s</string>
     <string name="conflict_local_file">Helyi fájl</string>
     <string name="conflict_message_description">Amennyiben mindkét verziót kiválasztja, a helyi fájl nevéhez egy szám lesz hozzáfűzve.</string>
     <string name="conflict_server_file">Kiszolgálón lévő fájl</string>

+ 0 - 1
app/src/main/res/values-in/strings.xml

@@ -173,7 +173,6 @@ Otomatis unggah hanya bekerja dengan baik apabila Anda mengeluarkan aplikasi ini
     <string name="confirmation_remove_folders_alert">Apa anda yakin ingin menghapus item yang terpilih beserta isinya?</string>
     <string name="confirmation_remove_local">Lokal saja</string>
     <string name="conflict_dialog_error">Dialog penyelesaian konflik tidak dapat dibuat</string>
-    <string name="conflict_file_headline">File konflik %1$s</string>
     <string name="conflict_local_file">File lokal</string>
     <string name="conflict_message_description">Jika Anda memilih kedua versi, nama dari berkas lokal akan ditambahi angka.</string>
     <string name="conflict_server_file">File server</string>

+ 0 - 1
app/src/main/res/values-it/strings.xml

@@ -169,7 +169,6 @@
     <string name="confirmation_remove_folder_alert">Vuoi davvero rimuovere %1$s e il relativo contenuto?</string>
     <string name="confirmation_remove_folders_alert">Vuoi davvero eliminare gli elementi selezionati e il loro contenuto?</string>
     <string name="confirmation_remove_local">Solo localmente</string>
-    <string name="conflict_file_headline">File %1$s in conflitto</string>
     <string name="conflict_local_file">File locale</string>
     <string name="conflict_message_description">Se selezioni entrambe le versioni, il file locale ha un numero aggiunto al suo nome.</string>
     <string name="conflict_server_file">File su server</string>

+ 0 - 1
app/src/main/res/values-ja-rJP/strings.xml

@@ -179,7 +179,6 @@
     <string name="confirmation_remove_folders_alert">本当に選択したアイテムとその内容を削除しますか?</string>
     <string name="confirmation_remove_local">ローカルのみ</string>
     <string name="conflict_dialog_error">競合解決ダイアログを作成できません</string>
-    <string name="conflict_file_headline">%1$sはすでに存在します</string>
     <string name="conflict_local_file">ローカルファイル</string>
     <string name="conflict_message_description">両方のバージョンを選択した場合、ローカルファイルはファイル名に数字が追加されます。</string>
     <string name="conflict_server_file">サーバーファイル</string>

+ 0 - 1
app/src/main/res/values-ka/strings.xml

@@ -151,7 +151,6 @@
     <string name="confirmation_remove_folder_alert">Do you really want to delete %1$s and the contents thereof?</string>
     <string name="confirmation_remove_folders_alert">Do you really want to delete the selected items and their contents?</string>
     <string name="confirmation_remove_local">Local only</string>
-    <string name="conflict_file_headline">Conflicting file %1$s</string>
     <string name="conflict_local_file">Local file</string>
     <string name="conflict_message_description">If you select both versions, the local file will have a number appended to its name.</string>
     <string name="conflict_server_file">Server file</string>

+ 0 - 1
app/src/main/res/values-ko/strings.xml

@@ -178,7 +178,6 @@
     <string name="confirmation_remove_folders_alert">선택한 항목과 포함된 내용을 삭제하시겠습니까?</string>
     <string name="confirmation_remove_local">로컬만</string>
     <string name="conflict_dialog_error">충돌 해결 프로그램 대화 상자를 만들 수 없습니다.</string>
-    <string name="conflict_file_headline">충돌하는 파일 %1$s</string>
     <string name="conflict_local_file">로컬 파일</string>
     <string name="conflict_message_description">두 버전을 모두 선택하면 기존 파일 이름에 번호가 추가됩니다.</string>
     <string name="conflict_server_file">서버 파일</string>

+ 0 - 1
app/src/main/res/values-lo/strings.xml

@@ -144,7 +144,6 @@
     <string name="confirmation_remove_folder_alert">ທ່ານຕ້ອງການ ລຶບ%1$s ແລະ ເນື້ອຫາບໍ?</string>
     <string name="confirmation_remove_folders_alert">ທ່ານຕ້ອງການລຶບລາຍການທີ່ເລືອກ ແລະ ເນື້ອຫາແທ້ບໍ?</string>
     <string name="confirmation_remove_local">ຊ່ອງເກັບຢ່າງດຽວ</string>
-    <string name="conflict_file_headline">ຟາຍຜິດພາດ%1$s</string>
     <string name="conflict_message_description">ຖ້າທ່ານເລືອກເອົາທັງສອງເວີຊັ້ນ, ບ່ອນເກັບຟາຍຈະມີຈໍານວນສະສົມ</string>
     <string name="contactlist_item_icon">ລາຍການໄອຄອນຜູ້ຕິດຕໍ່</string>
     <string name="contactlist_no_permission">ບໍ່ໄດ້ຮັບອະນຸຍາດ, ບໍ່ມີຫຍັງນໍາເຂົ້າ.</string>

+ 0 - 1
app/src/main/res/values-lt-rLT/strings.xml

@@ -147,7 +147,6 @@
     <string name="confirmation_remove_folder_alert">Ar tikrai norite ištrinti %1$s ir jo turinį?</string>
     <string name="confirmation_remove_folders_alert">Ar tikrai norite ištrinti pažymėtus elementus ir jų turinį?</string>
     <string name="confirmation_remove_local">Tik vietiniai</string>
-    <string name="conflict_file_headline">Nesuderinamas failas%1$s</string>
     <string name="conflict_local_file">Vietinis failas</string>
     <string name="conflict_message_description">Jei pasirinksite abi versijas, vietinis failas prie pavadinimo turės numerį.</string>
     <string name="conflict_server_file">Failas iš serverio</string>

+ 0 - 1
app/src/main/res/values-lv/strings.xml

@@ -132,7 +132,6 @@
     <string name="confirmation_remove_folder_alert">Vai tiešām vēlaties izdzēst %1$s un tā saturu?</string>
     <string name="confirmation_remove_folders_alert">Vai tiešām vēlies dzēst izvēlētos objektus un to saturu?</string>
     <string name="confirmation_remove_local">Tikai lokālos</string>
-    <string name="conflict_file_headline">Konfliktējošs fails %1$s</string>
     <string name="contactlist_item_icon">Lietotāja ikona kontaktpersonu sarakstam</string>
     <string name="contactlist_no_permission">Nav dota atļauja, importēšana neizdevās</string>
     <string name="contacts">Kontakti</string>

+ 0 - 1
app/src/main/res/values-mk/strings.xml

@@ -144,7 +144,6 @@
     <string name="confirmation_remove_folder_alert">али си сигурен дека сакаш да ја избришеш %1$s и содржината во истата?</string>
     <string name="confirmation_remove_folders_alert">Дали си сигурен дека сакаш да ја избришеш означената ставкаи содржината во неа?</string>
     <string name="confirmation_remove_local">Само локално</string>
-    <string name="conflict_file_headline">Датотеки со конфликт %1$s</string>
     <string name="conflict_local_file">Локална датотека</string>
     <string name="conflict_message_description">Ако ги одберете и двете верзии, локалната датотека ќе има број додаден на нејзиното име.</string>
     <string name="conflict_server_file">Серверска датотека</string>

+ 0 - 1
app/src/main/res/values-nb-rNO/strings.xml

@@ -179,7 +179,6 @@
     <string name="confirmation_remove_folders_alert">Vil du virkelig fjerne de valgte elementene og dets innhold?</string>
     <string name="confirmation_remove_local">Kun lokalt</string>
     <string name="conflict_dialog_error">Dialogboksen Konfliktløser kan ikke opprettes</string>
-    <string name="conflict_file_headline">Konflikt med%1$s</string>
     <string name="conflict_local_file">Lokal fil</string>
     <string name="conflict_message_description">Hvis du velger begge versjonene vil den lokale filen få et tall lagt til på slutten av navnet.</string>
     <string name="conflict_server_file">Server fil</string>

+ 0 - 1
app/src/main/res/values-nl/strings.xml

@@ -178,7 +178,6 @@
     <string name="confirmation_remove_folders_alert">Wil je de geselecteerde objecten en hun inhoud echt verwijderen?</string>
     <string name="confirmation_remove_local">Alleen lokaal</string>
     <string name="conflict_dialog_error">Conflictoplossingsvenster kan niet geladen worden</string>
-    <string name="conflict_file_headline">Conflicterend bestand%1$s</string>
     <string name="conflict_local_file">Lokaal bestand</string>
     <string name="conflict_message_description">Als je beide versies selecteert, zal het lokale bestand een nummer aan de naam toegevoegd krijgen.</string>
     <string name="conflict_server_file">Serverbestand</string>

+ 0 - 1
app/src/main/res/values-pl/strings.xml

@@ -176,7 +176,6 @@
     <string name="confirmation_remove_folders_alert">Czy na pewno chcesz usunąć wybrane pozycje i ich zawartość?</string>
     <string name="confirmation_remove_local">Tylko lokalnie</string>
     <string name="conflict_dialog_error">Nie można utworzyć okna dialogowego rozwiązywania konfliktów</string>
-    <string name="conflict_file_headline">Plik powodujący konflikt %1$s</string>
     <string name="conflict_local_file">Plik lokalny</string>
     <string name="conflict_message_description">Jeśli wybierzesz obie wersje, to do nazwy pliku lokalnego zostanie dodany numer.</string>
     <string name="conflict_server_file">Plik z serwera</string>

+ 0 - 1
app/src/main/res/values-pt-rBR/strings.xml

@@ -179,7 +179,6 @@
     <string name="confirmation_remove_folders_alert">Quer realmente excluir os itens selecionados e seus conteúdos?</string>
     <string name="confirmation_remove_local">Somente local</string>
     <string name="conflict_dialog_error">A caixa de diálogo de resolução de conflitos não pode ser criada</string>
-    <string name="conflict_file_headline">Arquivo conflitante %1$s</string>
     <string name="conflict_local_file">Arquivo local</string>
     <string name="conflict_message_description">Se você selecionar as duas versões, o arquivo local terá um número anexado ao seu nome.</string>
     <string name="conflict_server_file">Arquivo do servidor</string>

+ 0 - 1
app/src/main/res/values-pt-rPT/strings.xml

@@ -149,7 +149,6 @@
     <string name="confirmation_remove_folder_alert">Deseja realmente apagar %1$s e o seu conteúdo?</string>
     <string name="confirmation_remove_folders_alert">Quer realmente apagar os itens seleccionados e os seus conteúdos?</string>
     <string name="confirmation_remove_local">Apenas localmente</string>
-    <string name="conflict_file_headline">Ficheiro em conflito %1$s</string>
     <string name="conflict_local_file">Ficheiro local</string>
     <string name="conflict_message_description">Se selecionou ambas as versões, o ficheiro local terá um número acrescentado ao seu nome.</string>
     <string name="conflict_server_file">Ficheiro do servidor</string>

+ 0 - 1
app/src/main/res/values-ro/strings.xml

@@ -174,7 +174,6 @@
     <string name="confirmation_remove_folder_alert">Sigur vreți să eliminați %1$s și conținutul său?</string>
     <string name="confirmation_remove_folders_alert">Doriți să ștergeți elementele selectate și conținutul lor?</string>
     <string name="confirmation_remove_local">Doar local</string>
-    <string name="conflict_file_headline">Conflict cu fișierul %1$s</string>
     <string name="conflict_local_file">Fișier local</string>
     <string name="conflict_message_description">Dacă selectezi ambele variante, atunci fișierul local va avea un număr adăugat la numele său.</string>
     <string name="conflict_server_file">Fișier pe server</string>

Some files were not shown because too many files changed in this diff