Browse Source

Merge pull request #13501 from nextcloud/feature/offline-file-creation

Feature - Offline File Creation
Tobias Kaminsky 7 tháng trước cách đây
mục cha
commit
c530e22931
43 tập tin đã thay đổi với 2036 bổ sung303 xóa
  1. 1301 0
      app/schemas/com.nextcloud.client.database.NextcloudDatabase/85.json
  2. 2 2
      app/src/androidTest/java/com/owncloud/android/AbstractIT.java
  3. 2 2
      app/src/androidTest/java/com/owncloud/android/AbstractOnServerIT.java
  4. 6 6
      app/src/androidTest/java/com/owncloud/android/UploadIT.java
  5. 1 3
      app/src/androidTest/java/com/owncloud/android/files/services/FileUploaderIT.kt
  6. 0 59
      app/src/androidTest/java/com/owncloud/android/ui/dialog/SyncFileNotEnoughSpaceDialogFragmentTest.java
  7. 89 0
      app/src/androidTest/java/com/owncloud/android/ui/dialog/SyncFileNotEnoughSpaceDialogFragmentTest.kt
  8. 2 4
      app/src/debug/java/com/nextcloud/test/TestActivity.kt
  9. 6 1
      app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt
  10. 6 2
      app/src/main/java/com/nextcloud/client/database/dao/OfflineOperationDao.kt
  11. 6 6
      app/src/main/java/com/nextcloud/client/database/entity/OfflineOperationEntity.kt
  12. 9 0
      app/src/main/java/com/nextcloud/client/database/migrations/DatabaseMigrationUtil.kt
  13. 71 0
      app/src/main/java/com/nextcloud/client/database/typeAdapter/OfflineOperationTypeAdapter.kt
  14. 33 0
      app/src/main/java/com/nextcloud/client/database/typeConverter/OfflineOperationTypeConverter.kt
  15. 7 1
      app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt
  16. 47 23
      app/src/main/java/com/nextcloud/client/jobs/offlineOperations/OfflineOperationsWorker.kt
  17. 36 12
      app/src/main/java/com/nextcloud/client/jobs/offlineOperations/repository/OfflineOperationsRepository.kt
  18. 2 1
      app/src/main/java/com/nextcloud/client/jobs/offlineOperations/repository/OfflineOperationsRepositoryType.kt
  19. 14 8
      app/src/main/java/com/nextcloud/client/network/ConnectivityService.java
  20. 17 9
      app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.java
  21. 15 2
      app/src/main/java/com/nextcloud/model/OfflineOperationType.kt
  22. 2 11
      app/src/main/java/com/nextcloud/receiver/NetworkChangeReceiver.kt
  23. 0 1
      app/src/main/java/com/nextcloud/receiver/OfflineOperationActionReceiver.kt
  24. 11 0
      app/src/main/java/com/nextcloud/utils/extensions/FragmentExtensions.kt
  25. 1 1
      app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt
  26. 68 9
      app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java
  27. 0 12
      app/src/main/java/com/owncloud/android/datamodel/OCFile.java
  28. 2 2
      app/src/main/java/com/owncloud/android/db/ProviderMeta.java
  29. 2 2
      app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java
  30. 22 13
      app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java
  31. 0 5
      app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.java
  32. 80 10
      app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java
  33. 7 3
      app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt
  34. 3 9
      app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.kt
  35. 1 1
      app/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.kt
  36. 69 61
      app/src/main/java/com/owncloud/android/ui/fragment/ExtendedListFragment.java
  37. 3 1
      app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java
  38. 15 12
      app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetDialog.java
  39. 34 7
      app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java
  40. 2 1
      app/src/main/java/com/owncloud/android/ui/fragment/SearchType.kt
  41. 20 0
      app/src/main/java/com/owncloud/android/utils/BitmapUtils.java
  42. 18 0
      app/src/main/res/drawable/ic_cloud_sync.xml
  43. 4 1
      app/src/main/res/values/strings.xml

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

@@ -0,0 +1,1301 @@
+{
+  "formatVersion": 1,
+  "database": {
+    "version": 85,
+    "identityHash": "2d24b9210a36150f221156d2e8f59665",
+    "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_path` TEXT, `offline_operations_type` TEXT, `offline_operations_file_name` TEXT, `offline_operations_created_at` INTEGER, `offline_operations_modified_at` INTEGER)",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "_id",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "parentOCFileId",
+            "columnName": "offline_operations_parent_oc_file_id",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "path",
+            "columnName": "offline_operations_path",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "type",
+            "columnName": "offline_operations_type",
+            "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
+          },
+          {
+            "fieldPath": "modifiedAt",
+            "columnName": "offline_operations_modified_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, '2d24b9210a36150f221156d2e8f59665')"
+    ]
+  }
+}

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

@@ -377,8 +377,8 @@ public abstract class AbstractIT {
     public void uploadOCUpload(OCUpload ocUpload) {
         ConnectivityService connectivityServiceMock = new ConnectivityService() {
             @Override
-            public boolean isNetworkAndServerAvailable() throws NetworkOnMainThreadException {
-                return false;
+            public void isNetworkAndServerAvailable(@NonNull GenericCallback<Boolean> callback) {
+
             }
 
             @Override

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

@@ -189,8 +189,8 @@ 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;
+            public void isNetworkAndServerAvailable(@NonNull GenericCallback<Boolean> callback) {
+
             }
 
             @Override

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

@@ -59,8 +59,8 @@ public class UploadIT extends AbstractOnServerIT {
 
     private ConnectivityService connectivityServiceMock = new ConnectivityService() {
         @Override
-        public boolean isNetworkAndServerAvailable() throws NetworkOnMainThreadException {
-            return false;
+        public void isNetworkAndServerAvailable(@NonNull GenericCallback<Boolean> callback) {
+
         }
 
         @Override
@@ -282,8 +282,8 @@ public class UploadIT extends AbstractOnServerIT {
     public void testUploadOnWifiOnlyButNoWifi() {
         ConnectivityService connectivityServiceMock = new ConnectivityService() {
             @Override
-            public boolean isNetworkAndServerAvailable() throws NetworkOnMainThreadException {
-                return false;
+            public void isNetworkAndServerAvailable(@NonNull GenericCallback<Boolean> callback) {
+
             }
 
             @Override
@@ -371,8 +371,8 @@ public class UploadIT extends AbstractOnServerIT {
     public void testUploadOnWifiOnlyButMeteredWifi() {
         ConnectivityService connectivityServiceMock = new ConnectivityService() {
             @Override
-            public boolean isNetworkAndServerAvailable() throws NetworkOnMainThreadException {
-                return false;
+            public void isNetworkAndServerAvailable(@NonNull GenericCallback<Boolean> callback) {
+
             }
 
             @Override

+ 1 - 3
app/src/androidTest/java/com/owncloud/android/files/services/FileUploaderIT.kt

@@ -34,9 +34,7 @@ abstract class FileUploaderIT : AbstractOnServerIT() {
     private var uploadsStorageManager: UploadsStorageManager? = null
 
     private val connectivityServiceMock: ConnectivityService = object : ConnectivityService {
-        override fun isNetworkAndServerAvailable(): Boolean {
-            return false
-        }
+        override fun isNetworkAndServerAvailable(callback: ConnectivityService.GenericCallback<Boolean>) = Unit
 
         override fun isConnected(): Boolean {
             return false

+ 0 - 59
app/src/androidTest/java/com/owncloud/android/ui/dialog/SyncFileNotEnoughSpaceDialogFragmentTest.java

@@ -1,59 +0,0 @@
-/*
- * Nextcloud - Android Client
- *
- * SPDX-FileCopyrightText: 2020 Tobias Kaminsky <tobias@kaminsky.me>
- * SPDX-FileCopyrightText: 2020 Nextcloud GmbH
- * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
- */
-package com.owncloud.android.ui.dialog;
-
-import com.owncloud.android.AbstractIT;
-import com.owncloud.android.datamodel.OCFile;
-import com.owncloud.android.ui.activity.FileDisplayActivity;
-import com.owncloud.android.utils.ScreenshotTest;
-
-import org.junit.Rule;
-import org.junit.Test;
-
-import java.util.Objects;
-
-import androidx.test.espresso.intent.rule.IntentsTestRule;
-
-import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
-
-public class SyncFileNotEnoughSpaceDialogFragmentTest extends AbstractIT {
-    @Rule public IntentsTestRule<FileDisplayActivity> activityRule = new IntentsTestRule<>(FileDisplayActivity.class,
-                                                                                           true,
-                                                                                           false);
-
-    @Test
-    @ScreenshotTest
-    public void showNotEnoughSpaceDialogForFolder() {
-        FileDisplayActivity test = activityRule.launchActivity(null);
-        OCFile ocFile = new OCFile("/Document/");
-        ocFile.setFileLength(5000000);
-        ocFile.setFolder();
-
-        SyncFileNotEnoughSpaceDialogFragment dialog = SyncFileNotEnoughSpaceDialogFragment.newInstance(ocFile, 1000);
-        dialog.show(test.getListOfFilesFragment().getFragmentManager(), "1");
-
-        getInstrumentation().waitForIdleSync();
-
-        screenshot(Objects.requireNonNull(dialog.requireDialog().getWindow()).getDecorView());
-    }
-
-    @Test
-    @ScreenshotTest
-    public void showNotEnoughSpaceDialogForFile() {
-        FileDisplayActivity test = activityRule.launchActivity(null);
-        OCFile ocFile = new OCFile("/Video.mp4");
-        ocFile.setFileLength(1000000);
-
-        SyncFileNotEnoughSpaceDialogFragment dialog = SyncFileNotEnoughSpaceDialogFragment.newInstance(ocFile, 2000);
-        dialog.show(test.getListOfFilesFragment().getFragmentManager(), "2");
-
-        getInstrumentation().waitForIdleSync();
-
-        screenshot(Objects.requireNonNull(dialog.requireDialog().getWindow()).getDecorView());
-    }
-}

+ 89 - 0
app/src/androidTest/java/com/owncloud/android/ui/dialog/SyncFileNotEnoughSpaceDialogFragmentTest.kt

@@ -0,0 +1,89 @@
+/*
+ * 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
+
+import androidx.annotation.UiThread
+import androidx.test.core.app.launchActivity
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.IdlingRegistry
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.isRoot
+import com.owncloud.android.AbstractIT
+import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.ui.activity.FileDisplayActivity
+import com.owncloud.android.ui.dialog.SyncFileNotEnoughSpaceDialogFragment.Companion.newInstance
+import com.owncloud.android.utils.EspressoIdlingResource
+import com.owncloud.android.utils.ScreenshotTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+
+class SyncFileNotEnoughSpaceDialogFragmentTest : AbstractIT() {
+    private val testClassName = "com.owncloud.android.ui.dialog.SyncFileNotEnoughSpaceDialogFragmentTest"
+
+    @Before
+    fun registerIdlingResource() {
+        IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource)
+    }
+
+    @After
+    fun unregisterIdlingResource() {
+        IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource)
+    }
+
+    @Test
+    @ScreenshotTest
+    @UiThread
+    fun showNotEnoughSpaceDialogForFolder() {
+        launchActivity<FileDisplayActivity>().use { scenario ->
+            scenario.onActivity { sut ->
+                val ocFile = OCFile("/Document/").apply {
+                    fileLength = 5000000
+                    setFolder()
+                }
+
+                onIdleSync {
+                    EspressoIdlingResource.increment()
+                    newInstance(ocFile, 1000).apply {
+                        show(sut.supportFragmentManager, "1")
+                    }
+                    EspressoIdlingResource.decrement()
+
+                    val screenShotName = createName(testClassName + "_" + "showNotEnoughSpaceDialogForFolder", "")
+                    onView(isRoot()).check(matches(isDisplayed()))
+                    screenshotViaName(sut, screenShotName)
+                }
+            }
+        }
+    }
+
+    @Test
+    @ScreenshotTest
+    @UiThread
+    fun showNotEnoughSpaceDialogForFile() {
+        launchActivity<FileDisplayActivity>().use { scenario ->
+            scenario.onActivity { sut ->
+                val ocFile = OCFile("/Video.mp4").apply {
+                    fileLength = 1000000
+                }
+
+                onIdleSync {
+                    EspressoIdlingResource.increment()
+                    newInstance(ocFile, 2000).apply {
+                        show(sut.supportFragmentManager, "2")
+                    }
+                    EspressoIdlingResource.decrement()
+
+                    val screenShotName = createName(testClassName + "_" + "showNotEnoughSpaceDialogForFile", "")
+                    onView(isRoot()).check(matches(isDisplayed()))
+                    screenshotViaName(sut, screenShotName)
+                }
+            }
+        }
+    }
+}

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

@@ -42,6 +42,8 @@ class TestActivity :
     private lateinit var binding: TestLayoutBinding
 
     val connectivityServiceMock: ConnectivityService = object : ConnectivityService {
+        override fun isNetworkAndServerAvailable(callback: ConnectivityService.GenericCallback<Boolean>) = Unit
+
         override fun isConnected(): Boolean {
             return false
         }
@@ -53,10 +55,6 @@ class TestActivity :
         override fun getConnectivity(): Connectivity {
             return Connectivity.CONNECTED_WIFI
         }
-
-        override fun isNetworkAndServerAvailable(): Boolean {
-            return false
-        }
     }
 
     override fun onCreate(savedInstanceState: Bundle?) {

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

@@ -12,6 +12,7 @@ import androidx.room.AutoMigration
 import androidx.room.Database
 import androidx.room.Room
 import androidx.room.RoomDatabase
+import androidx.room.TypeConverters
 import com.nextcloud.client.core.Clock
 import com.nextcloud.client.core.ClockImpl
 import com.nextcloud.client.database.dao.ArbitraryDataDao
@@ -31,6 +32,7 @@ import com.nextcloud.client.database.migrations.DatabaseMigrationUtil
 import com.nextcloud.client.database.migrations.Migration67to68
 import com.nextcloud.client.database.migrations.RoomMigration
 import com.nextcloud.client.database.migrations.addLegacyMigrations
+import com.nextcloud.client.database.typeConverter.OfflineOperationTypeConverter
 import com.owncloud.android.db.ProviderMeta
 
 @Database(
@@ -65,11 +67,13 @@ import com.owncloud.android.db.ProviderMeta
         AutoMigration(from = 80, to = 81),
         AutoMigration(from = 81, to = 82),
         AutoMigration(from = 82, to = 83),
-        AutoMigration(from = 83, to = 84)
+        AutoMigration(from = 83, to = 84),
+        AutoMigration(from = 84, to = 85, spec = DatabaseMigrationUtil.DeleteColumnSpec::class)
     ],
     exportSchema = true
 )
 @Suppress("Detekt.UnnecessaryAbstractClass") // needed by Room
+@TypeConverters(OfflineOperationTypeConverter::class)
 abstract class NextcloudDatabase : RoomDatabase() {
 
     abstract fun arbitraryDataDao(): ArbitraryDataDao
@@ -93,6 +97,7 @@ abstract class NextcloudDatabase : RoomDatabase() {
                 instance = Room
                     .databaseBuilder(context, NextcloudDatabase::class.java, ProviderMeta.DB_NAME)
                     .allowMainThreadQueries()
+                    .addTypeConverter(OfflineOperationTypeConverter())
                     .addLegacyMigrations(clock, context)
                     .addMigrations(RoomMigration())
                     .addMigrations(Migration67to68())

+ 6 - 2
app/src/main/java/com/nextcloud/client/database/dao/OfflineOperationDao.kt

@@ -10,6 +10,7 @@ package com.nextcloud.client.database.dao
 import androidx.room.Dao
 import androidx.room.Delete
 import androidx.room.Insert
+import androidx.room.OnConflictStrategy
 import androidx.room.Query
 import androidx.room.Update
 import com.nextcloud.client.database.entity.OfflineOperationEntity
@@ -19,7 +20,7 @@ interface OfflineOperationDao {
     @Query("SELECT * FROM offline_operations")
     fun getAll(): List<OfflineOperationEntity>
 
-    @Insert
+    @Insert(onConflict = OnConflictStrategy.REPLACE)
     fun insert(vararg entity: OfflineOperationEntity)
 
     @Update
@@ -35,5 +36,8 @@ interface OfflineOperationDao {
     fun getByPath(path: String): OfflineOperationEntity?
 
     @Query("SELECT * FROM offline_operations WHERE offline_operations_parent_oc_file_id = :parentOCFileId")
-    fun getSubDirectoriesByParentOCFileId(parentOCFileId: Long): List<OfflineOperationEntity>
+    fun getSubEntitiesByParentOCFileId(parentOCFileId: Long): List<OfflineOperationEntity>
+
+    @Query("DELETE FROM offline_operations")
+    fun clearTable()
 }

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

@@ -22,18 +22,18 @@ data class OfflineOperationEntity(
     @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_PATH)
+    var path: 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
+    var createdAt: Long? = null,
+
+    @ColumnInfo(name = ProviderTableMeta.OFFLINE_OPERATION_MODIFIED_AT)
+    var modifiedAt: Long? = null
 )

+ 9 - 0
app/src/main/java/com/nextcloud/client/database/migrations/DatabaseMigrationUtil.kt

@@ -7,6 +7,7 @@
  */
 package com.nextcloud.client.database.migrations
 
+import androidx.room.DeleteColumn
 import androidx.room.migration.AutoMigrationSpec
 import androidx.sqlite.db.SupportSQLiteDatabase
 
@@ -90,4 +91,12 @@ object DatabaseMigrationUtil {
             super.onPostMigrate(db)
         }
     }
+
+    @DeleteColumn.Entries(
+        DeleteColumn(
+            tableName = "offline_operations",
+            columnName = "offline_operations_parent_path"
+        )
+    )
+    class DeleteColumnSpec : AutoMigrationSpec
 }

+ 71 - 0
app/src/main/java/com/nextcloud/client/database/typeAdapter/OfflineOperationTypeAdapter.kt

@@ -0,0 +1,71 @@
+/*
+ * 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.typeAdapter
+
+import com.google.gson.JsonDeserializationContext
+import com.google.gson.JsonDeserializer
+import com.google.gson.JsonElement
+import com.google.gson.JsonObject
+import com.google.gson.JsonSerializationContext
+import com.google.gson.JsonSerializer
+import com.nextcloud.model.OfflineOperationRawType
+import com.nextcloud.model.OfflineOperationType
+
+import java.lang.reflect.Type
+
+class OfflineOperationTypeAdapter : JsonSerializer<OfflineOperationType>, JsonDeserializer<OfflineOperationType> {
+
+    override fun serialize(
+        src: OfflineOperationType?,
+        typeOfSrc: Type?,
+        context: JsonSerializationContext?
+    ): JsonElement {
+        val jsonObject = JsonObject()
+        jsonObject.addProperty("type", src?.javaClass?.simpleName)
+        when (src) {
+            is OfflineOperationType.CreateFolder -> {
+                jsonObject.addProperty("type", src.type)
+                jsonObject.addProperty("path", src.path)
+            }
+
+            is OfflineOperationType.CreateFile -> {
+                jsonObject.addProperty("type", src.type)
+                jsonObject.addProperty("localPath", src.localPath)
+                jsonObject.addProperty("remotePath", src.remotePath)
+                jsonObject.addProperty("mimeType", src.mimeType)
+            }
+
+            null -> Unit
+        }
+        return jsonObject
+    }
+
+    override fun deserialize(
+        json: JsonElement?,
+        typeOfT: Type?,
+        context: JsonDeserializationContext?
+    ): OfflineOperationType? {
+        val jsonObject = json?.asJsonObject ?: return null
+        val type = jsonObject.get("type")?.asString
+        return when (type) {
+            OfflineOperationRawType.CreateFolder.name -> OfflineOperationType.CreateFolder(
+                jsonObject.get("type").asString,
+                jsonObject.get("path").asString
+            )
+
+            OfflineOperationRawType.CreateFile.name -> OfflineOperationType.CreateFile(
+                jsonObject.get("type").asString,
+                jsonObject.get("localPath").asString,
+                jsonObject.get("remotePath").asString,
+                jsonObject.get("mimeType").asString
+            )
+
+            else -> null
+        }
+    }
+}

+ 33 - 0
app/src/main/java/com/nextcloud/client/database/typeConverter/OfflineOperationTypeConverter.kt

@@ -0,0 +1,33 @@
+/*
+ * 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.typeConverter
+
+import androidx.room.ProvidedTypeConverter
+import androidx.room.TypeConverter
+import com.google.gson.Gson
+import com.nextcloud.model.OfflineOperationType
+import com.google.gson.GsonBuilder
+import com.nextcloud.client.database.typeAdapter.OfflineOperationTypeAdapter
+
+@ProvidedTypeConverter
+class OfflineOperationTypeConverter {
+
+    private val gson: Gson = GsonBuilder()
+        .registerTypeAdapter(OfflineOperationType::class.java, OfflineOperationTypeAdapter())
+        .create()
+
+    @TypeConverter
+    fun fromOfflineOperationType(type: OfflineOperationType?): String? {
+        return gson.toJson(type)
+    }
+
+    @TypeConverter
+    fun toOfflineOperationType(type: String?): OfflineOperationType? {
+        return gson.fromJson(type, OfflineOperationType::class.java)
+    }
+}

+ 7 - 1
app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt

@@ -104,7 +104,13 @@ class BackgroundJobFactory @Inject constructor(
     }
 
     private fun createOfflineOperationsWorker(context: Context, params: WorkerParameters): ListenableWorker {
-        return OfflineOperationsWorker(accountManager.user, context, connectivityService, viewThemeUtils.get(), params)
+        return OfflineOperationsWorker(
+            accountManager.user,
+            context,
+            connectivityService,
+            viewThemeUtils.get(),
+            params
+        )
     }
 
     private fun createFilesExportWork(context: Context, params: WorkerParameters): ListenableWorker {

+ 47 - 23
app/src/main/java/com/nextcloud/client/jobs/offlineOperations/OfflineOperationsWorker.kt

@@ -23,11 +23,14 @@ 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.lib.resources.files.UploadFileRemoteOperation
 import com.owncloud.android.operations.CreateFolderOperation
 import com.owncloud.android.utils.theme.ViewThemeUtils
 import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.NonCancellable
 import kotlinx.coroutines.withContext
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
 
 class OfflineOperationsWorker(
     private val user: User,
@@ -48,7 +51,7 @@ class OfflineOperationsWorker(
     private var repository = OfflineOperationsRepository(fileDataStorageManager)
 
     @Suppress("TooGenericExceptionCaught")
-    override suspend fun doWork(): Result = coroutineScope {
+    override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
         val jobName = inputData.getString(JOB_NAME)
         Log_OC.d(
             TAG,
@@ -57,9 +60,9 @@ class OfflineOperationsWorker(
                 "\n-----------------------------------"
         )
 
-        if (!connectivityService.isNetworkAndServerAvailable()) {
+        if (!isNetworkAndServerAvailable()) {
             Log_OC.d(TAG, "OfflineOperationsWorker cancelled, no internet connection")
-            return@coroutineScope Result.retry()
+            return@withContext Result.retry()
         }
 
         val client = clientFactory.create(user)
@@ -69,7 +72,7 @@ class OfflineOperationsWorker(
         val totalOperations = operations.size
         var currentSuccessfulOperationIndex = 0
 
-        return@coroutineScope try {
+        return@withContext try {
             while (operations.isNotEmpty()) {
                 val operation = operations.first()
                 val result = executeOperation(operation, client)
@@ -99,27 +102,48 @@ class OfflineOperationsWorker(
         }
     }
 
-    @Suppress("Deprecation")
+    private suspend fun isNetworkAndServerAvailable(): Boolean = suspendCoroutine { continuation ->
+        connectivityService.isNetworkAndServerAvailable { result ->
+            continuation.resume(result)
+        }
+    }
+
+    @Suppress("Deprecation", "MagicNumber")
     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
+    ): Pair<RemoteOperationResult<*>?, RemoteOperation<*>?>? = withContext(Dispatchers.IO) {
+        return@withContext when (operation.type) {
+            is OfflineOperationType.CreateFolder -> {
+                val createFolderOperation = withContext(NonCancellable) {
+                    val operationType = (operation.type as OfflineOperationType.CreateFolder)
+                    CreateFolderOperation(
+                        operationType.path,
+                        user,
+                        context,
+                        fileDataStorageManager
+                    )
                 }
+                createFolderOperation.execute(client) to createFolderOperation
+            }
+
+            is OfflineOperationType.CreateFile -> {
+                val createFileOperation = withContext(NonCancellable) {
+                    val operationType = (operation.type as OfflineOperationType.CreateFile)
+                    val lastModificationDate = System.currentTimeMillis() / 1000
+
+                    UploadFileRemoteOperation(
+                        operationType.localPath,
+                        operationType.remotePath,
+                        operationType.mimeType,
+                        "",
+                        operation.modifiedAt ?: lastModificationDate,
+                        operation.createdAt ?: System.currentTimeMillis(),
+                        true
+                    )
+                }
+
+                createFileOperation.execute(client) to createFileOperation
             }
 
             else -> {
@@ -142,7 +166,7 @@ class OfflineOperationsWorker(
         }
 
         val logMessage = if (result.isSuccess) "Operation completed" else "Operation failed"
-        Log_OC.d(TAG, "$logMessage path: ${operation.path}, type: ${operation.type}")
+        Log_OC.d(TAG, "$logMessage filename: ${operation.filename}, type: ${operation.type}")
 
         if (result.isSuccess) {
             repository.updateNextOperations(operation)

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

@@ -8,8 +8,11 @@
 package com.nextcloud.client.jobs.offlineOperations.repository
 
 import com.nextcloud.client.database.entity.OfflineOperationEntity
+import com.nextcloud.model.OfflineOperationType
 import com.owncloud.android.datamodel.FileDataStorageManager
 import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.utils.MimeType
+import com.owncloud.android.utils.MimeTypeUtil
 
 class OfflineOperationsRepository(
     private val fileDataStorageManager: FileDataStorageManager
@@ -19,7 +22,7 @@ class OfflineOperationsRepository(
     private val pathSeparator = '/'
 
     @Suppress("NestedBlockDepth")
-    override fun getAllSubdirectories(fileId: Long): List<OfflineOperationEntity> {
+    override fun getAllSubEntities(fileId: Long): List<OfflineOperationEntity> {
         val result = mutableListOf<OfflineOperationEntity>()
         val queue = ArrayDeque<Long>()
         queue.add(fileId)
@@ -31,7 +34,7 @@ class OfflineOperationsRepository(
 
             processedIds.add(currentFileId)
 
-            val subDirectories = dao.getSubDirectoriesByParentOCFileId(currentFileId)
+            val subDirectories = dao.getSubEntitiesByParentOCFileId(currentFileId)
             result.addAll(subDirectories)
 
             subDirectories.forEach {
@@ -48,15 +51,14 @@ class OfflineOperationsRepository(
     }
 
     override fun deleteOperation(file: OCFile) {
-        getAllSubdirectories(file.fileId).forEach {
-            dao.delete(it)
+        if (file.isFolder) {
+            getAllSubEntities(file.fileId).forEach {
+                dao.delete(it)
+            }
         }
 
         file.decryptedRemotePath?.let {
-            val entity = dao.getByPath(it)
-            entity?.let {
-                dao.delete(entity)
-            }
+            dao.deleteByPath(it)
         }
 
         fileDataStorageManager.removeFile(file, true, true)
@@ -66,17 +68,28 @@ class OfflineOperationsRepository(
         val ocFile = fileDataStorageManager.getFileByDecryptedRemotePath(operation.path)
         val fileId = ocFile?.fileId ?: return
 
-        getAllSubdirectories(fileId)
+        getAllSubEntities(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) {
+                            if (newPath != nextOperation.path) {
                                 nextOperation.apply {
-                                    parentPath = newParentPath
+                                    type = when (type) {
+                                        is OfflineOperationType.CreateFile ->
+                                            (type as OfflineOperationType.CreateFile).copy(
+                                                remotePath = newPath
+                                            )
+
+                                        is OfflineOperationType.CreateFolder ->
+                                            (type as OfflineOperationType.CreateFolder).copy(
+                                                path = newPath
+                                            )
+
+                                        else -> type
+                                    }
                                     path = newPath
                                 }
                             } else {
@@ -88,4 +101,15 @@ class OfflineOperationsRepository(
             }
             .forEach { dao.update(it) }
     }
+
+    override fun convertToOCFiles(fileId: Long): List<OCFile> =
+        dao.getSubEntitiesByParentOCFileId(fileId).map { entity ->
+            OCFile(entity.path).apply {
+                mimeType = if (entity.type is OfflineOperationType.CreateFolder) {
+                    MimeType.DIRECTORY
+                } else {
+                    MimeTypeUtil.getMimeTypeFromPath(entity.path)
+                }
+            }
+        }
 }

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

@@ -11,7 +11,8 @@ import com.nextcloud.client.database.entity.OfflineOperationEntity
 import com.owncloud.android.datamodel.OCFile
 
 interface OfflineOperationsRepositoryType {
-    fun getAllSubdirectories(fileId: Long): List<OfflineOperationEntity>
+    fun getAllSubEntities(fileId: Long): List<OfflineOperationEntity>
     fun deleteOperation(file: OCFile)
     fun updateNextOperations(operation: OfflineOperationEntity)
+    fun convertToOCFiles(fileId: Long): List<OCFile>
 }

+ 14 - 8
app/src/main/java/com/nextcloud/client/network/ConnectivityService.java

@@ -6,7 +6,8 @@
  */
 package com.nextcloud.client.network;
 
-import android.os.NetworkOnMainThreadException;
+
+import androidx.annotation.NonNull;
 
 /**
  * This service provides information about current network connectivity
@@ -17,16 +18,12 @@ 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.
+     * checks if the device has an active internet connection.
      * </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.
+     * @param callback A callback to handle the result of the network and server availability check.
      */
-    boolean isNetworkAndServerAvailable() throws NetworkOnMainThreadException;
+    void isNetworkAndServerAvailable(@NonNull GenericCallback<Boolean> callback);
 
     boolean isConnected();
 
@@ -45,4 +42,13 @@ public interface ConnectivityService {
      * @return Network connectivity status in platform-agnostic format
      */
     Connectivity getConnectivity();
+
+    /**
+     * Callback interface for asynchronous results.
+     *
+     * @param <T> The type of result returned by the callback.
+     */
+    interface GenericCallback<T> {
+        void onComplete(T result);
+    }
 }

+ 17 - 9
app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.java

@@ -13,7 +13,8 @@ import android.net.ConnectivityManager;
 import android.net.Network;
 import android.net.NetworkCapabilities;
 import android.net.NetworkInfo;
-import android.os.NetworkOnMainThreadException;
+import android.os.Handler;
+import android.os.Looper;
 
 import com.nextcloud.client.account.Server;
 import com.nextcloud.client.account.UserAccountManager;
@@ -23,6 +24,7 @@ import com.owncloud.android.lib.common.utils.Log_OC;
 
 import org.apache.commons.httpclient.HttpStatus;
 
+import androidx.annotation.NonNull;
 import androidx.core.net.ConnectivityManagerCompat;
 import kotlin.jvm.functions.Function1;
 
@@ -36,6 +38,7 @@ class ConnectivityServiceImpl implements ConnectivityService {
     private final ClientFactory clientFactory;
     private final GetRequestBuilder requestBuilder;
     private final WalledCheckCache walledCheckCache;
+    private final Handler mainThreadHandler = new Handler(Looper.getMainLooper());
 
     static class GetRequestBuilder implements Function1<String, GetMethod> {
         @Override
@@ -57,16 +60,21 @@ class ConnectivityServiceImpl implements ConnectivityService {
     }
 
     @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);
+    public void isNetworkAndServerAvailable(@NonNull GenericCallback<Boolean> callback) {
+        new Thread(() -> {
+            Network activeNetwork = platformConnectivityManager.getActiveNetwork();
+            NetworkCapabilities networkCapabilities = platformConnectivityManager.getNetworkCapabilities(activeNetwork);
+            boolean hasInternet = networkCapabilities != null && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
 
-        if (!hasInternet) {
-            return false;
-        }
+            boolean result;
+            if (hasInternet) {
+                result = !isInternetWalled();
+            } else {
+                result = false;
+            }
 
-        return !isInternetWalled();
+            mainThreadHandler.post(() -> callback.onComplete(result));
+        }).start();
     }
 
     @Override

+ 15 - 2
app/src/main/java/com/nextcloud/model/OfflineOperationType.kt

@@ -7,6 +7,19 @@
 
 package com.nextcloud.model
 
-enum class OfflineOperationType {
-    CreateFolder
+sealed class OfflineOperationType {
+    abstract val type: String
+
+    data class CreateFolder(override val type: String, var path: String) : OfflineOperationType()
+    data class CreateFile(
+        override val type: String,
+        val localPath: String,
+        var remotePath: String,
+        val mimeType: String
+    ) : OfflineOperationType()
+}
+
+enum class OfflineOperationRawType {
+    CreateFolder,
+    CreateFile
 }

+ 2 - 11
app/src/main/java/com/nextcloud/receiver/NetworkChangeReceiver.kt

@@ -11,9 +11,6 @@ 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)
@@ -24,15 +21,9 @@ class NetworkChangeReceiver(
     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)
-            }
+        connectivityService.isNetworkAndServerAvailable {
+            listener.networkAndServerConnectionListener(it)
         }
     }
 }

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

@@ -25,6 +25,5 @@ class OfflineOperationActionReceiver : BroadcastReceiver() {
         val user = intent.getParcelableArgument(USER, User::class.java) ?: return
         val fileDataStorageManager = FileDataStorageManager(user, context?.contentResolver)
         fileDataStorageManager.offlineOperationDao.deleteByPath(path)
-        // TODO Update notification
     }
 }

+ 11 - 0
app/src/main/java/com/nextcloud/utils/extensions/FragmentExtensions.kt

@@ -16,3 +16,14 @@ inline fun <reified T : Any> Fragment.typedActivity(): T? {
         null
     }
 }
+
+/**
+ * Extension for Java Classes
+ */
+fun <T : Any> Fragment.getTypedActivity(type: Class<T>): T? {
+    return if (isAdded && activity != null && type.isInstance(activity)) {
+        type.cast(activity)
+    } else {
+        null
+    }
+}

+ 1 - 1
app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt

@@ -32,7 +32,7 @@ object FileNameValidator {
      * @param existedFileNames Set of existing file names to avoid duplicates.
      * @return An error message if the filename is invalid, null otherwise.
      */
-    @Suppress("ReturnCount")
+    @Suppress("ReturnCount", "NestedBlockDepth")
     fun checkFileName(
         filename: String,
         capability: OCCapability,

+ 68 - 9
app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java

@@ -41,6 +41,7 @@ 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.OCFileFilterType;
+import com.nextcloud.model.OfflineOperationRawType;
 import com.nextcloud.model.OfflineOperationType;
 import com.nextcloud.utils.date.DateFormatPattern;
 import com.nextcloud.utils.extensions.DateExtensionsKt;
@@ -107,7 +108,7 @@ public class FileDataStorageManager {
     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 final OfflineOperationsRepositoryType offlineOperationsRepository;
 
     public FileDataStorageManager(User user, ContentResolver contentResolver) {
         this.contentProviderClient = null;
@@ -140,33 +141,83 @@ public class FileDataStorageManager {
         return getFileByPath(ProviderTableMeta.FILE_PATH_DECRYPTED, path);
     }
 
-    public OfflineOperationEntity addCreateFolderOfflineOperation(String path, String filename, String parentPath, Long parentOCFileId) {
+    public void addCreateFileOfflineOperation(String[] localPaths, String[] remotePaths) {
+        if (localPaths.length != remotePaths.length) {
+            Log_OC.d(TAG, "Local path and remote path size do not match");
+            return;
+        }
+
+        for (int i = 0; i < localPaths.length; i++) {
+            String localPath = localPaths[i];
+            String remotePath = remotePaths[i];
+            String mimeType = MimeTypeUtil.getMimeTypeFromPath(remotePath);
+
+            OfflineOperationEntity entity = new OfflineOperationEntity();
+            entity.setPath(remotePath);
+            entity.setType(new OfflineOperationType.CreateFile(OfflineOperationRawType.CreateFile.name(), localPath, remotePath, mimeType));
+
+            long createdAt = System.currentTimeMillis();
+            long modificationTimestamp = System.currentTimeMillis();
+
+            entity.setCreatedAt(createdAt);
+            entity.setModifiedAt(modificationTimestamp / 1000);
+            entity.setFilename(new File(remotePath).getName());
+
+            String parentPath = new File(remotePath).getParent() + OCFile.PATH_SEPARATOR;
+            OCFile parentFile = getFileByDecryptedRemotePath(parentPath);
+
+            if (parentFile != null) {
+                entity.setParentOCFileId(parentFile.getFileId());
+            }
+
+            offlineOperationDao.insert(entity);
+            createPendingFile(remotePath, mimeType, createdAt, modificationTimestamp);
+        }
+    }
+
+    public OfflineOperationEntity addCreateFolderOfflineOperation(String path, String filename, Long parentOCFileId) {
         OfflineOperationEntity entity = new OfflineOperationEntity();
 
         entity.setFilename(filename);
         entity.setParentOCFileId(parentOCFileId);
+
+        OfflineOperationType.CreateFolder operationType = new OfflineOperationType.CreateFolder(OfflineOperationRawType.CreateFolder.name(), path);
+        entity.setType(operationType);
         entity.setPath(path);
-        entity.setParentPath(parentPath);
-        entity.setCreatedAt(System.currentTimeMillis() / 1000L);
-        entity.setType(OfflineOperationType.CreateFolder);
+
+        long createdAt = System.currentTimeMillis();
+        long modificationTimestamp = System.currentTimeMillis();
+
+        entity.setCreatedAt(createdAt);
+        entity.setModifiedAt(modificationTimestamp / 1000);
 
         offlineOperationDao.insert(entity);
-        createPendingDirectory(path);
+        createPendingDirectory(path, createdAt, modificationTimestamp);
 
         return entity;
     }
 
-    public void createPendingDirectory(String path) {
+    public void createPendingFile(String path, String mimeType, long createdAt, long modificationTimestamp) {
         OCFile file = new OCFile(path);
-        file.setMimeType(MimeType.DIRECTORY);
+        file.setMimeType(mimeType);
+        file.setCreationTimestamp(createdAt);
+        file.setModificationTimestamp(modificationTimestamp);
         saveFileWithParent(file, MainApp.getAppContext());
     }
 
+    public void createPendingDirectory(String path, long createdAt, long modificationTimestamp) {
+        OCFile directory = new OCFile(path);
+        directory.setMimeType(MimeType.DIRECTORY);
+        directory.setCreationTimestamp(createdAt);
+        directory.setModificationTimestamp(modificationTimestamp);
+        saveFileWithParent(directory, MainApp.getAppContext());
+    }
+
     public void deleteOfflineOperation(OCFile file) {
         offlineOperationsRepository.deleteOperation(file);
     }
 
-    public void renameCreateFolderOfflineOperation(OCFile file, String newFolderName) {
+    public void renameOfflineOperation(OCFile file, String newFolderName) {
         var entity = offlineOperationDao.getByPath(file.getDecryptedRemotePath());
         if (entity == null) {
             return;
@@ -178,6 +229,14 @@ public class FileDataStorageManager {
         }
 
         String newPath = parentFolder.getDecryptedRemotePath() + newFolderName + OCFile.PATH_SEPARATOR;
+
+        if (entity.getType() instanceof OfflineOperationType.CreateFolder createFolderType) {
+            createFolderType.setPath(newPath);
+        } else if (entity.getType() instanceof OfflineOperationType.CreateFile createFileType) {
+            createFileType.setRemotePath(newPath);
+        }
+        entity.setType(entity.getType());
+
         entity.setPath(newPath);
         entity.setFilename(newFolderName);
         offlineOperationDao.update(entity);

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

@@ -788,18 +788,6 @@ public class OCFile implements Parcelable, Comparable<OCFile>, ServerFileInterfa
         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;
     }

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

@@ -25,7 +25,7 @@ import java.util.List;
  */
 public class ProviderMeta {
     public static final String DB_NAME = "filelist";
-    public static final int DB_VERSION = 84;
+    public static final int DB_VERSION = 85;
 
     private ProviderMeta() {
         // No instance
@@ -289,9 +289,9 @@ public class ProviderMeta {
 
         // 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_MODIFIED_AT = "offline_operations_modified_at";
         public static final String OFFLINE_OPERATION_CREATED_AT = "offline_operations_created_at";
         public static final String OFFLINE_OPERATION_FILE_NAME = "offline_operations_file_name";
 

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

@@ -165,8 +165,7 @@ public abstract class FileActivity extends DrawerActivity
     @Inject
     UserAccountManager accountManager;
 
-    @Inject
-    ConnectivityService connectivityService;
+    @Inject public ConnectivityService connectivityService;
 
     @Inject
     BackgroundJobManager backgroundJobManager;
@@ -246,6 +245,7 @@ public abstract class FileActivity extends DrawerActivity
     public void networkAndServerConnectionListener(boolean isNetworkAndServerAvailable) {
         if (isNetworkAndServerAvailable) {
             hideInfoBox();
+            refreshList();
         } else {
             showInfoBox(R.string.offline_mode);
         }

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

@@ -236,8 +236,6 @@ public class FileDisplayActivity extends FileActivity
 
     @Inject AppInfo appInfo;
 
-    @Inject ConnectivityService connectivityService;
-
     @Inject InAppReviewHelper inAppReviewHelper;
 
     @Inject FastScrollUtils fastScrollUtils;
@@ -952,16 +950,21 @@ public class FileDisplayActivity extends FileActivity
                 default -> FileUploadWorker.LOCAL_BEHAVIOUR_FORGET;
             };
 
-            FileUploadHelper.Companion.instance().uploadNewFiles(getUser().orElseThrow(RuntimeException::new),
-                                                                 filePaths,
-                                                                 decryptedRemotePaths,
-                                                                 behaviour,
-                                                                 true,
-                                                                 UploadFileOperation.CREATED_BY_USER,
-                                                                 false,
-                                                                 false,
-                                                                 NameCollisionPolicy.ASK_USER);
-
+            connectivityService.isNetworkAndServerAvailable(result -> {
+                if (result) {
+                    FileUploadHelper.Companion.instance().uploadNewFiles(getUser().orElseThrow(RuntimeException::new),
+                                                                         filePaths,
+                                                                         decryptedRemotePaths,
+                                                                         behaviour,
+                                                                         true,
+                                                                         UploadFileOperation.CREATED_BY_USER,
+                                                                         false,
+                                                                         false,
+                                                                         NameCollisionPolicy.ASK_USER);
+                } else {
+                    fileDataStorageManager.addCreateFileOfflineOperation(filePaths, decryptedRemotePaths);
+                }
+            });
         } else {
             Log_OC.d(TAG, "User clicked on 'Update' with no selection");
             DisplayUtils.showSnackMessage(this, R.string.filedisplay_no_file_selected);
@@ -1379,7 +1382,13 @@ public class FileDisplayActivity extends FileActivity
                 if (MainApp.isOnlyOnDevice()) {
                     ocFileListFragment.setMessageForEmptyList(R.string.file_list_empty_headline, R.string.file_list_empty_on_device, R.drawable.ic_list_empty_folder, true);
                 } else {
-                    ocFileListFragment.setEmptyListMessage(SearchType.NO_SEARCH);
+                    connectivityService.isNetworkAndServerAvailable(result -> {
+                        if (result) {
+                            ocFileListFragment.setEmptyListMessage(SearchType.NO_SEARCH);
+                        } else {
+                            ocFileListFragment.setEmptyListMessage(SearchType.OFFLINE_MODE);
+                        }
+                    });
                 }
             }
         } else {

+ 0 - 5
app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.java

@@ -27,7 +27,6 @@ import com.nextcloud.client.device.PowerManagementService;
 import com.nextcloud.client.jobs.BackgroundJobManager;
 import com.nextcloud.client.jobs.upload.FileUploadHelper;
 import com.nextcloud.client.jobs.upload.FileUploadWorker;
-import com.nextcloud.client.network.ConnectivityService;
 import com.nextcloud.client.utils.Throttler;
 import com.nextcloud.model.WorkerState;
 import com.nextcloud.model.WorkerStateLiveData;
@@ -44,7 +43,6 @@ import com.owncloud.android.ui.adapter.UploadListAdapter;
 import com.owncloud.android.ui.decoration.MediaGridItemDecoration;
 import com.owncloud.android.utils.DisplayUtils;
 import com.owncloud.android.utils.FilesSyncHelper;
-import com.owncloud.android.utils.theme.ViewThemeUtils;
 
 import javax.inject.Inject;
 
@@ -73,9 +71,6 @@ public class UploadListActivity extends FileActivity {
     @Inject
     UploadsStorageManager uploadsStorageManager;
 
-    @Inject
-    ConnectivityService connectivityService;
-
     @Inject
     PowerManagementService powerManagementService;
 

+ 80 - 10
app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java

@@ -16,9 +16,11 @@ import android.annotation.SuppressLint;
 import android.app.Activity;
 import android.content.ContentValues;
 import android.content.res.Resources;
+import android.graphics.Bitmap;
 import android.graphics.Color;
 import android.graphics.PorterDuff;
 import android.graphics.drawable.Drawable;
+import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
 import android.text.TextUtils;
@@ -31,8 +33,10 @@ import android.widget.LinearLayout;
 import com.elyeproj.loaderviewlibrary.LoaderImageView;
 import com.nextcloud.android.common.ui.theme.utils.ColorRole;
 import com.nextcloud.client.account.User;
+import com.nextcloud.client.database.entity.OfflineOperationEntity;
 import com.nextcloud.client.jobs.upload.FileUploadHelper;
 import com.nextcloud.client.preferences.AppPreferences;
+import com.nextcloud.model.OfflineOperationType;
 import com.nextcloud.model.OCFileFilterType;
 import com.nextcloud.utils.extensions.ViewExtensionsKt;
 import com.owncloud.android.MainApp;
@@ -66,6 +70,7 @@ import com.owncloud.android.ui.activity.FileDisplayActivity;
 import com.owncloud.android.ui.fragment.SearchType;
 import com.owncloud.android.ui.interfaces.OCFileListFragmentInterface;
 import com.owncloud.android.ui.preview.PreviewTextFragment;
+import com.owncloud.android.utils.BitmapUtils;
 import com.owncloud.android.utils.DisplayUtils;
 import com.owncloud.android.utils.FileSortOrder;
 import com.owncloud.android.utils.FileStorageUtils;
@@ -81,8 +86,12 @@ import java.util.Collections;
 import java.util.Date;
 import java.util.List;
 import java.util.Locale;
+import java.util.Objects;
 import java.util.Set;
 import java.util.UUID;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -161,7 +170,7 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
         userId = AccountManager
             .get(activity)
             .getUserData(this.user.toPlatformAccount(),
-                         com.owncloud.android.lib.common.accounts.AccountUtils.Constants.KEY_USER_ID);
+                         AccountUtils.Constants.KEY_USER_ID);
 
         this.viewThemeUtils = viewThemeUtils;
 
@@ -523,7 +532,11 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
         }
 
         ViewExtensionsKt.setVisibleIf(holder.getShared(), !file.isOfflineOperation());
-        setColorFilterForOfflineOperations(holder, file);
+        if (file.isFolder()) {
+            setColorFilterForOfflineCreateFolderOperations(holder, file);
+        } else {
+            setColorFilterForOfflineCreateFileOperations(holder, file);
+        }
     }
 
     private void bindListItemViewHolder(ListItemViewHolder holder, OCFile file) {
@@ -596,13 +609,14 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
 
             holder.getFileSize().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);
             }
+
+            holder.getFileSizeSeparator().setVisibility(View.VISIBLE);
         } else {
             final long fileLength = file.getFileLength();
             if (fileLength >= 0) {
@@ -610,11 +624,11 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
 
                 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);
                 }
+
+                holder.getFileSizeSeparator().setVisibility(View.VISIBLE);
             } else {
                 holder.getFileSize().setVisibility(View.GONE);
                 holder.getFileSizeSeparator().setVisibility(View.GONE);
@@ -654,14 +668,40 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
 
     private void applyVisualsForOfflineOperations(ListItemViewHolder holder, OCFile file) {
         ViewExtensionsKt.setVisibleIf(holder.getShared(), !file.isOfflineOperation());
-        setColorFilterForOfflineOperations(holder, file);
+
+        if (file.isFolder()) {
+            setColorFilterForOfflineCreateFolderOperations(holder, file);
+        } else {
+            setColorFilterForOfflineCreateFileOperations(holder, file);
+        }
     }
 
-    private void setColorFilterForOfflineOperations(ListViewHolder holder, OCFile file) {
-        if (!file.isFolder()) {
+    private final ExecutorService executorService = Executors.newCachedThreadPool();
+    private final Handler mainHandler = new Handler(Looper.getMainLooper());
+
+    private void setColorFilterForOfflineCreateFileOperations(ListViewHolder holder, OCFile file) {
+        if (!file.isOfflineOperation()) {
             return;
         }
 
+        executorService.execute(() -> {
+            OfflineOperationEntity entity = mStorageManager.offlineOperationDao.getByPath(file.getDecryptedRemotePath());
+
+            if (entity != null && entity.getType() != null && entity.getType() instanceof OfflineOperationType.CreateFile createFileOperation) {
+                Bitmap bitmap = BitmapUtils.decodeSampledBitmapFromFile(createFileOperation.getLocalPath(), holder.getThumbnail().getWidth(), holder.getThumbnail().getHeight());
+                if (bitmap == null) return;
+
+                Bitmap thumbnail = BitmapUtils.addColorFilter(bitmap, Color.GRAY,100);
+                mainHandler.post(() -> holder.getThumbnail().setImageBitmap(thumbnail));
+            }
+        });
+    }
+
+    public void onDestroy() {
+        executorService.shutdown();
+    }
+
+    private void setColorFilterForOfflineCreateFolderOperations(ListViewHolder holder, OCFile file) {
         if (file.isOfflineOperation()) {
             holder.getThumbnail().setColorFilter(Color.GRAY, PorterDuff.Mode.SRC_IN);
         } else {
@@ -782,6 +822,7 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
             prepareListOfHiddenFiles();
             mergeOCFilesForLivePhoto();
             mFilesAll.clear();
+            addOfflineOperations(directory.getFileId());
             mFilesAll.addAll(mFiles);
             currentDirectory = directory;
         } else {
@@ -790,10 +831,39 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
         }
 
         searchType = null;
-
         notifyDataSetChanged();
     }
 
+    /**
+     * Converts Offline Operations to OCFiles and adds them to the adapter for visual feedback.
+     * This function creates pending OCFiles, but they may not consistently appear in the UI.
+     * The issue arises when  {@link RefreshFolderOperation} deletes pending Offline Operations, while some may still exist in the table.
+     * If only this function is used, it cause crash in {@link FileDisplayActivity mSyncBroadcastReceiver.onReceive}.
+     * <p>
+     * These function also need to be used: {@link FileDataStorageManager#createPendingDirectory(String, long, long)}, {@link FileDataStorageManager#createPendingFile(String, String, long, long)}.
+     */
+    private void addOfflineOperations(long fileId) {
+        List<OCFile> offlineOperations = mStorageManager.offlineOperationsRepository.convertToOCFiles(fileId);
+        if (offlineOperations.isEmpty()) {
+            return;
+        }
+
+        List<OCFile> newFiles;
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+            newFiles = offlineOperations.stream()
+                .filter(offlineFile -> mFilesAll.stream()
+                    .noneMatch(file -> Objects.equals(file.getDecryptedRemotePath(), offlineFile.getDecryptedRemotePath())))
+                .toList();
+        } else {
+            newFiles = offlineOperations.stream()
+                .filter(offlineFile -> mFilesAll.stream()
+                    .noneMatch(file -> Objects.equals(file.getDecryptedRemotePath(), offlineFile.getDecryptedRemotePath())))
+                .collect(Collectors.toList());
+        }
+
+        mFilesAll.addAll(newFiles);
+    }
+
     public void setData(List<Object> objects,
                         SearchType searchType,
                         FileDataStorageManager storageManager,

+ 7 - 3
app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt

@@ -16,11 +16,15 @@ object OCShareToOCFileConverter {
     private const val MILLIS_PER_SECOND = 1000
 
     /**
-     * Generates a list of incomplete [OCFile] from a list of [OCShare]
+     * Generates a list of incomplete [OCFile] from a list of [OCShare]. Retrieving OCFile directly by path may fail
+     * in cases like
+     * when a shared file is located at a/b/c/d/a.txt. To display a.txt in the shared tab, the device needs the OCFile.
+     * On first launch, the app may not be aware of the file until the exact path is accessed.
      *
-     * This is actually pretty complex as we get one [OCShare] item for each shared instance for the same folder
+     * Server implementation needed to get file size, thumbnails e.g. :
+     * <a href="https://github.com/nextcloud/server/issues/4456g</a>.
      *
-     * **THIS ONLY WORKS WITH FILES SHARED *BY* THE USER, NOT FOR SHARES *WITH* THE USER**
+     * Note: This works only for files shared *by* the user, not files shared *with* the user.
      */
     @JvmStatic
     fun buildOCFilesFromShares(shares: List<OCShare>): List<OCFile> {

+ 3 - 9
app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.kt

@@ -19,7 +19,6 @@ 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
@@ -40,8 +39,6 @@ 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
 
 /**
@@ -184,21 +181,18 @@ class CreateFolderDialogFragment : DialogFragment(), DialogInterface.OnClickList
             }
 
             val path = parentFolder?.decryptedRemotePath + newFolderName + OCFile.PATH_SEPARATOR
-            lifecycleScope.launch(Dispatchers.IO) {
-                if (connectivityService.isNetworkAndServerAvailable()) {
+            connectivityService.isNetworkAndServerAvailable { result ->
+                if (result) {
                     typedActivity<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) {
-                        (requireActivity() as? FileDisplayActivity)?.syncAndUpdateFolder(true)
-                    }
+                    typedActivity<FileDisplayActivity>()?.refreshCurrentDirectory()
                 }
             }
         }

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

@@ -146,7 +146,7 @@ class RenameFileDialogFragment : DialogFragment(), DialogInterface.OnClickListen
             }
 
             if (mTargetFile?.isOfflineOperation == true) {
-                fileDataStorageManager.renameCreateFolderOfflineOperation(mTargetFile, newFileName)
+                fileDataStorageManager.renameOfflineOperation(mTargetFile, newFileName)
                 if (requireActivity() is FileDisplayActivity) {
                     val activity = requireActivity() as FileDisplayActivity
                     activity.refreshCurrentDirectory()

+ 69 - 61
app/src/main/java/com/owncloud/android/ui/fragment/ExtendedListFragment.java

@@ -45,6 +45,7 @@ import com.nextcloud.client.account.UserAccountManager;
 import com.nextcloud.client.di.Injectable;
 import com.nextcloud.client.preferences.AppPreferences;
 import com.nextcloud.client.preferences.AppPreferencesImpl;
+import com.nextcloud.utils.extensions.FragmentExtensionsKt;
 import com.owncloud.android.MainApp;
 import com.owncloud.android.R;
 import com.owncloud.android.databinding.ListFragmentBinding;
@@ -52,6 +53,7 @@ import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.lib.resources.files.SearchRemoteOperation;
 import com.owncloud.android.lib.resources.status.OwnCloudVersion;
 import com.owncloud.android.ui.EmptyRecyclerView;
+import com.owncloud.android.ui.activity.FileActivity;
 import com.owncloud.android.ui.activity.FileDisplayActivity;
 import com.owncloud.android.ui.activity.FolderPickerActivity;
 import com.owncloud.android.ui.activity.OnEnforceableRefreshListener;
@@ -367,6 +369,10 @@ public class ExtendedListFragment extends Fragment implements
     public void onDestroyView() {
         super.onDestroyView();
         binding = null;
+        var adapter = getRecyclerView().getAdapter();
+        if (adapter instanceof OCFileListAdapter ocFileListAdapter) {
+            ocFileListAdapter.onDestroy();
+        }
     }
 
     private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
@@ -578,69 +584,67 @@ public class ExtendedListFragment extends Fragment implements
      */
     public void setMessageForEmptyList(@StringRes final int headline, @StringRes final int message,
                                        @DrawableRes final int icon, final boolean tintIcon) {
-        new Handler(Looper.getMainLooper()).post(new Runnable() {
-            @Override
-            public void run() {
-
-                if (mEmptyListContainer != null && mEmptyListMessage != null) {
-                    mEmptyListHeadline.setText(headline);
-                    mEmptyListMessage.setText(message);
-
-                    if (tintIcon) {
-                        if (getContext() != null) {
-                            mEmptyListIcon.setImageDrawable(
-                                viewThemeUtils.platform.tintPrimaryDrawable(getContext(), icon));
-                        }
-                    } else {
-                        mEmptyListIcon.setImageResource(icon);
-                    }
+        new Handler(Looper.getMainLooper()).post(() -> {
 
-                    mEmptyListIcon.setVisibility(View.VISIBLE);
-                    mEmptyListMessage.setVisibility(View.VISIBLE);
+            if (mEmptyListContainer != null && mEmptyListMessage != null) {
+                mEmptyListHeadline.setText(headline);
+                mEmptyListMessage.setText(message);
+
+                if (tintIcon) {
+                    if (getContext() != null) {
+                        mEmptyListIcon.setImageDrawable(
+                            viewThemeUtils.platform.tintPrimaryDrawable(getContext(), icon));
+                    }
+                } else {
+                    mEmptyListIcon.setImageResource(icon);
                 }
+
+                mEmptyListIcon.setVisibility(View.VISIBLE);
+                mEmptyListMessage.setVisibility(View.VISIBLE);
             }
         });
     }
 
     public void setEmptyListMessage(final SearchType searchType) {
-        new Handler(Looper.getMainLooper()).post(new Runnable() {
-            @Override
-            public void run() {
-
-                if (searchType == SearchType.NO_SEARCH) {
-                    setMessageForEmptyList(R.string.file_list_empty_headline,
-                                           R.string.file_list_empty,
-                                           R.drawable.ic_list_empty_folder,
-                                           true);
-                } else if (searchType == SearchType.FILE_SEARCH) {
-                    setMessageForEmptyList(R.string.file_list_empty_headline_server_search,
-                                           R.string.file_list_empty,
-                                           R.drawable.ic_search_light_grey);
-                } else if (searchType == SearchType.FAVORITE_SEARCH) {
-                    setMessageForEmptyList(R.string.file_list_empty_favorite_headline,
-                                           R.string.file_list_empty_favorites_filter_list,
-                                           R.drawable.ic_star_light_yellow);
-                } else if (searchType == SearchType.RECENTLY_MODIFIED_SEARCH) {
-                    setMessageForEmptyList(R.string.file_list_empty_headline_server_search,
-                                           R.string.file_list_empty_recently_modified,
-                                           R.drawable.ic_list_empty_recent);
-                } else if (searchType == SearchType.REGULAR_FILTER) {
-                    setMessageForEmptyList(R.string.file_list_empty_headline_search,
-                                           R.string.file_list_empty_search,
-                                           R.drawable.ic_search_light_grey);
-                } else if (searchType == SearchType.SHARED_FILTER) {
-                    setMessageForEmptyList(R.string.file_list_empty_shared_headline,
-                                           R.string.file_list_empty_shared,
-                                           R.drawable.ic_list_empty_shared);
-                } else if (searchType == SearchType.GALLERY_SEARCH) {
-                    setMessageForEmptyList(R.string.file_list_empty_headline_server_search,
-                                           R.string.file_list_empty_gallery,
-                                           R.drawable.file_image);
-                } else if (searchType == SearchType.LOCAL_SEARCH) {
-                    setMessageForEmptyList(R.string.file_list_empty_headline_server_search,
-                                           R.string.file_list_empty_local_search,
-                                           R.drawable.ic_search_light_grey);
-                }
+        new Handler(Looper.getMainLooper()).post(() -> {
+            if (searchType == SearchType.OFFLINE_MODE) {
+                setMessageForEmptyList(R.string.offline_mode_info_title,
+                                       R.string.offline_mode_info_description,
+                                       R.drawable.ic_cloud_sync,
+                                       true);
+            } else if (searchType == SearchType.NO_SEARCH) {
+                setMessageForEmptyList(R.string.file_list_empty_headline,
+                                       R.string.file_list_empty,
+                                       R.drawable.ic_list_empty_folder,
+                                       true);
+            } else if (searchType == SearchType.FILE_SEARCH) {
+                setMessageForEmptyList(R.string.file_list_empty_headline_server_search,
+                                       R.string.file_list_empty,
+                                       R.drawable.ic_search_light_grey);
+            } else if (searchType == SearchType.FAVORITE_SEARCH) {
+                setMessageForEmptyList(R.string.file_list_empty_favorite_headline,
+                                       R.string.file_list_empty_favorites_filter_list,
+                                       R.drawable.ic_star_light_yellow);
+            } else if (searchType == SearchType.RECENTLY_MODIFIED_SEARCH) {
+                setMessageForEmptyList(R.string.file_list_empty_headline_server_search,
+                                       R.string.file_list_empty_recently_modified,
+                                       R.drawable.ic_list_empty_recent);
+            } else if (searchType == SearchType.REGULAR_FILTER) {
+                setMessageForEmptyList(R.string.file_list_empty_headline_search,
+                                       R.string.file_list_empty_search,
+                                       R.drawable.ic_search_light_grey);
+            } else if (searchType == SearchType.SHARED_FILTER) {
+                setMessageForEmptyList(R.string.file_list_empty_shared_headline,
+                                       R.string.file_list_empty_shared,
+                                       R.drawable.ic_list_empty_shared);
+            } else if (searchType == SearchType.GALLERY_SEARCH) {
+                setMessageForEmptyList(R.string.file_list_empty_headline_server_search,
+                                       R.string.file_list_empty_gallery,
+                                       R.drawable.file_image);
+            } else if (searchType == SearchType.LOCAL_SEARCH) {
+                setMessageForEmptyList(R.string.file_list_empty_headline_server_search,
+                                       R.string.file_list_empty_local_search,
+                                       R.drawable.ic_search_light_grey);
             }
         });
     }
@@ -650,11 +654,15 @@ public class ExtendedListFragment extends Fragment implements
      */
     public void setEmptyListLoadingMessage() {
         new Handler(Looper.getMainLooper()).post(() -> {
-            if (mEmptyListContainer != null && mEmptyListMessage != null) {
-                mEmptyListHeadline.setText(R.string.file_list_loading);
-                mEmptyListMessage.setText("");
-
-                mEmptyListIcon.setVisibility(View.GONE);
+            FileActivity fileActivity = FragmentExtensionsKt.getTypedActivity(this, FileActivity.class);
+            if (fileActivity != null) {
+                fileActivity.connectivityService.isNetworkAndServerAvailable(result -> {
+                    if (!result || mEmptyListContainer == null || mEmptyListMessage == null) return;
+
+                    mEmptyListHeadline.setText(R.string.file_list_loading);
+                    mEmptyListMessage.setText("");
+                    mEmptyListIcon.setVisibility(View.GONE);
+                });
             }
         });
     }

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

@@ -346,7 +346,9 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
     @Override
     @VisibleForTesting
     public void showSharingMenuActionSheet(OCShare share) {
-        new FileDetailSharingMenuBottomSheetDialog(fileActivity, this, share, viewThemeUtils).show();
+        if (fileActivity != null && !fileActivity.isFinishing()) {
+            new FileDetailSharingMenuBottomSheetDialog(fileActivity, this, share, viewThemeUtils).show();
+        }
     }
 
     /**

+ 15 - 12
app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetDialog.java

@@ -214,19 +214,22 @@ public class OCFileListBottomSheetDialog extends BottomSheetDialog implements In
     private void filterActionsForOfflineOperations() {
         if (file == null) return;
 
-        if (!file.isOfflineOperation() || file.isRootDirectory()) {
-            return;
-        }
+        fileActivity.connectivityService.isNetworkAndServerAvailable(result -> {
+            if (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);
+            if (!result || file.isOfflineOperation()) {
+                binding.menuCreateRichWorkspace.setVisibility(View.GONE);
+                binding.menuUploadFromApp.setVisibility(View.GONE);
+                binding.menuDirectCameraUpload.setVisibility(View.GONE);
+                binding.menuScanDocUpload.setVisibility(View.GONE);
+                binding.menuNewDocument.setVisibility(View.GONE);
+                binding.menuNewSpreadsheet.setVisibility(View.GONE);
+                binding.menuNewPresentation.setVisibility(View.GONE);
+                binding.creatorsContainer.setVisibility(View.GONE);
+            }
+        });
     }
 
     @Override

+ 34 - 7
app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java

@@ -638,7 +638,16 @@ public class OCFileListFragment extends ExtendedListFragment implements
             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)
+                        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,
+                                      R.id.action_edit,
+                                      R.id.action_download_file,
+                                      R.id.action_export_file,
+                                      R.id.action_set_as_wallpaper
+                                     )
                     );
                     break;
                 }
@@ -1129,10 +1138,16 @@ public class OCFileListFragment extends ExtendedListFragment implements
                 Log_OC.d(TAG, "no public key for " + user.getAccountName());
 
                 FragmentManager fragmentManager = getParentFragmentManager();
-                if (fragmentManager.findFragmentByTag(SETUP_ENCRYPTION_DIALOG_TAG) == null) {
-                    SetupEncryptionDialogFragment dialog = SetupEncryptionDialogFragment.newInstance(user, position);
-                    dialog.setTargetFragment(this, SETUP_ENCRYPTION_REQUEST_CODE);
-                    dialog.show(fragmentManager, SETUP_ENCRYPTION_DIALOG_TAG);
+                if (fragmentManager.findFragmentByTag(SETUP_ENCRYPTION_DIALOG_TAG) == null && requireActivity() instanceof FileActivity fileActivity) {
+                    fileActivity.connectivityService.isNetworkAndServerAvailable(result -> {
+                        if (result) {
+                            SetupEncryptionDialogFragment dialog = SetupEncryptionDialogFragment.newInstance(user, position);
+                            dialog.setTargetFragment(this, SETUP_ENCRYPTION_REQUEST_CODE);
+                            dialog.show(fragmentManager, SETUP_ENCRYPTION_DIALOG_TAG);
+                        } else {
+                            DisplayUtils.showSnackMessage(fileActivity, R.string.internet_connection_required_for_encrypted_folder_setup);
+                        }
+                    });
                 }
             }
         } else {
@@ -1143,13 +1158,25 @@ public class OCFileListFragment extends ExtendedListFragment implements
         }
     }
 
-    private void fileOnItemClick(OCFile file) {
+    private Integer checkFileBeforeOpen(OCFile file) {
         if (isAPKorAAB(Set.of(file))) {
+            return R.string.gplay_restriction;
+        } else if (file.isOfflineOperation()) {
+            return R.string.offline_operations_file_does_not_exists_yet;
+        } else {
+            return null;
+        }
+    }
+
+    private void fileOnItemClick(OCFile file) {
+        Integer errorMessageId = checkFileBeforeOpen(file);
+        if (errorMessageId != null) {
             Snackbar.make(getRecyclerView(),
-                          R.string.gplay_restriction,
+                          errorMessageId,
                           Snackbar.LENGTH_LONG).show();
             return;
         }
+
         if (PreviewImageFragment.canBePreviewed(file)) {
             // preview image - it handles the download, if needed
             if (searchFragment) {

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

@@ -22,5 +22,6 @@ enum class SearchType : Parcelable {
 
     // not a real filter, but nevertheless
     SHARED_FILTER,
-    GROUPFOLDER
+    GROUPFOLDER,
+    OFFLINE_MODE
 }

+ 20 - 0
app/src/main/java/com/owncloud/android/utils/BitmapUtils.java

@@ -57,6 +57,26 @@ public final class BitmapUtils {
         // utility class -> private constructor
     }
 
+    public static Bitmap addColorFilter(Bitmap originalBitmap, int filterColor, int opacity) {
+        int width = originalBitmap.getWidth();
+        int height = originalBitmap.getHeight();
+
+        Bitmap resultBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+        Canvas canvas = new Canvas(resultBitmap);
+
+        canvas.drawBitmap(originalBitmap, 0, 0, null);
+
+        Paint paint = new Paint();
+        paint.setColor(filterColor);
+
+        paint.setAlpha(opacity);
+
+        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP));
+        canvas.drawRect(0, 0, width, height, paint);
+
+        return resultBitmap;
+    }
+
     /**
      * Decodes a bitmap from a file containing it minimizing the memory use, known that the bitmap will be drawn in a
      * surface of reqWidth x reqHeight

+ 18 - 0
app/src/main/res/drawable/ic_cloud_sync.xml

@@ -0,0 +1,18 @@
+<!--
+  ~ Nextcloud - Android Client
+  ~
+  ~ 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="M21.5,14.98c-0.02,0 -0.03,0 -0.05,0.01C21.2,13.3 19.76,12 18,12c-1.4,0 -2.6,0.83 -3.16,2.02C13.26,14.1 12,15.4 12,17c0,1.66 1.34,3 3,3l6.5,-0.02c1.38,0 2.5,-1.12 2.5,-2.5S22.88,14.98 21.5,14.98zM10,4.26v2.09C7.67,7.18 6,9.39 6,12c0,1.77 0.78,3.34 2,4.44V14h2v6H4v-2h2.73C5.06,16.54 4,14.4 4,12C4,8.27 6.55,5.15 10,4.26zM20,6h-2.73c1.43,1.26 2.41,3.01 2.66,5l-2.02,0C17.68,9.64 16.98,8.45 16,7.56V10h-2V4h6V6z" />
+
+</vector>

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

@@ -29,6 +29,7 @@
     <string name="app_config_base_url_title">Base URL</string>
     <string name="app_config_proxy_host_title">Proxy Host Name</string>
     <string name="app_config_proxy_port_title">Proxy Port</string>
+    <string name="offline_operations_file_does_not_exists_yet">File does not exists, yet. Please upload the file first.</string>
     <string name="offline_operations_worker_notification_delete_offline_folder">Delete Offline Folder</string>
     <string name="offline_operations_worker_notification_conflict_text">Conflicted Folder: %s</string>
     <string name="offline_operations_worker_notification_start_text">Starting Offline Operations</string>
@@ -609,7 +610,8 @@
     <string name="confirmation_remove_folders_alert">Do you really want to delete the selected items and their contents?</string>
     <string name="maintenance_mode">Server is in maintenance mode</string>
     <string name="offline_mode">No internet connection</string>
-
+    <string name="offline_mode_info_title">You\'re Offline, But Work Continues</string>
+    <string name="offline_mode_info_description">Even without an internet connection, you can organize your folders, create files. Once you\'re back online, your pending actions will automatically sync.</string>
     <string name="uploads_view_upload_status_waiting_for_charging">Awaiting charge</string>
     <string name="actionbar_search">Search</string>
     <string name="drawer_synced_folders">Auto upload</string>
@@ -1175,6 +1177,7 @@
     <string name="pin_home">Pin to Home screen</string>
     <string name="pin_shortcut_label">Open %1$s</string>
     <string name="displays_mnemonic">Displays your 12 word passphrase</string>
+    <string name="internet_connection_required_for_encrypted_folder_setup">An internet connection is required to set up the encrypted folder</string>
     <string name="prefs_setup_e2e">Set up end-to-end encryption</string>
     <string name="prefs_e2e_active">End-to-end encryption is set up!</string>
     <string name="prefs_remove_e2e">Remove encryption locally</string>