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 tháng trước cách đây
mục cha
commit
8f42be49cc
100 tập tin đã thay đổi với 3341 bổ sung470 xóa
  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>

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác