Ver código fonte

Merge pull request #13220 from nextcloud/feature/compatible-file-names

Feature Compatible File Names
Tobias Kaminsky 10 meses atrás
pai
commit
6d324936e7
37 arquivos alterados com 2532 adições e 620 exclusões
  1. 1233 0
      app/schemas/com.nextcloud.client.database.NextcloudDatabase/82.json
  2. BIN
      app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testNewFolderDialog.png
  3. 193 0
      app/src/androidTest/java/com/nextcloud/utils/FileNameValidatorTests.kt
  4. 1 1
      app/src/androidTest/java/com/owncloud/android/utils/EspressoIdlingResource.kt
  5. 1 0
      app/src/main/java/com/nextcloud/client/assistant/repository/AssistantMockRepository.kt
  6. 2 1
      app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt
  7. 9 1
      app/src/main/java/com/nextcloud/client/database/entity/CapabilityEntity.kt
  8. 33 0
      app/src/main/java/com/nextcloud/utils/extensions/OCCapabilityExtensions.kt
  9. 15 0
      app/src/main/java/com/nextcloud/utils/extensions/StringExtensions.kt
  10. 157 0
      app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt
  11. 11 0
      app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java
  12. 5 1
      app/src/main/java/com/owncloud/android/db/ProviderMeta.java
  13. 52 5
      app/src/main/java/com/owncloud/android/providers/DocumentsStorageProvider.java
  14. 1 0
      app/src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.kt
  15. 34 6
      app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt
  16. 20 5
      app/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java
  17. 23 2
      app/src/main/java/com/owncloud/android/ui/activity/UploadFilesActivity.java
  18. 10 6
      app/src/main/java/com/owncloud/android/ui/adapter/TemplateAdapter.java
  19. 44 27
      app/src/main/java/com/owncloud/android/ui/dialog/ChooseRichDocumentsTemplateDialogFragment.kt
  20. 147 130
      app/src/main/java/com/owncloud/android/ui/dialog/ChooseTemplateDialogFragment.kt
  21. 76 67
      app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.kt
  22. 0 219
      app/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.java
  23. 205 0
      app/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.kt
  24. 0 126
      app/src/main/java/com/owncloud/android/ui/dialog/RenamePublicShareDialogFragment.java
  25. 120 0
      app/src/main/java/com/owncloud/android/ui/dialog/RenamePublicShareDialogFragment.kt
  26. 55 12
      app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java
  27. 23 3
      app/src/main/java/com/owncloud/android/ui/helpers/UriUploader.kt
  28. 2 1
      app/src/main/java/com/owncloud/android/utils/glide/HttpStreamFetcher.kt
  29. 11 0
      app/src/main/res/color/card_border_selector.xml
  30. 12 0
      app/src/main/res/drawable/rounded_rect_8dp.xml
  31. 14 3
      app/src/main/res/layout/template_button.xml
  32. 11 0
      app/src/main/res/values/strings.xml
  33. 1 1
      build.gradle
  34. 1 1
      gradle.properties
  35. 8 0
      gradle/verification-metadata.xml
  36. 1 1
      scripts/analysis/detectWrongSettings.sh
  37. 1 1
      scripts/analysis/lint-results.txt

+ 1233 - 0
app/schemas/com.nextcloud.client.database.NextcloudDatabase/82.json

@@ -0,0 +1,1233 @@
+{
+  "formatVersion": 1,
+  "database": {
+    "version": 82,
+    "identityHash": "e78b1402db9da7caff78c46fff585672",
+    "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)",
+        "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
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": true,
+          "columnNames": [
+            "_id"
+          ]
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "filesystem",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `is_folder` INTEGER, `found_at` INTEGER, `upload_triggered` INTEGER, `syncedfolder_id` TEXT, `crc32` TEXT, `modified_at` INTEGER)",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "_id",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "localPath",
+            "columnName": "local_path",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "fileIsFolder",
+            "columnName": "is_folder",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "fileFoundRecently",
+            "columnName": "found_at",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "fileSentForUpload",
+            "columnName": "upload_triggered",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "syncedFolderId",
+            "columnName": "syncedfolder_id",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "crc32",
+            "columnName": "crc32",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "fileModified",
+            "columnName": "modified_at",
+            "affinity": "INTEGER",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": true,
+          "columnNames": [
+            "_id"
+          ]
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "ocshares",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `file_source` INTEGER, `item_source` INTEGER, `share_type` INTEGER, `shate_with` TEXT, `path` TEXT, `permissions` INTEGER, `shared_date` INTEGER, `expiration_date` INTEGER, `token` TEXT, `shared_with_display_name` TEXT, `is_directory` INTEGER, `user_id` TEXT, `id_remote_shared` INTEGER, `owner_share` TEXT, `is_password_protected` INTEGER, `note` TEXT, `hide_download` INTEGER, `share_link` TEXT, `share_label` TEXT)",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "_id",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "fileSource",
+            "columnName": "file_source",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "itemSource",
+            "columnName": "item_source",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "shareType",
+            "columnName": "share_type",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "shareWith",
+            "columnName": "shate_with",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "path",
+            "columnName": "path",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "permissions",
+            "columnName": "permissions",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "sharedDate",
+            "columnName": "shared_date",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "expirationDate",
+            "columnName": "expiration_date",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "token",
+            "columnName": "token",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "shareWithDisplayName",
+            "columnName": "shared_with_display_name",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "isDirectory",
+            "columnName": "is_directory",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "userId",
+            "columnName": "user_id",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "idRemoteShared",
+            "columnName": "id_remote_shared",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "accountOwner",
+            "columnName": "owner_share",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "isPasswordProtected",
+            "columnName": "is_password_protected",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "note",
+            "columnName": "note",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "hideDownload",
+            "columnName": "hide_download",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "shareLink",
+            "columnName": "share_link",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "shareLabel",
+            "columnName": "share_label",
+            "affinity": "TEXT",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": true,
+          "columnNames": [
+            "_id"
+          ]
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "synced_folders",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `wifi_only` INTEGER, `charging_only` INTEGER, `existing` INTEGER, `enabled` INTEGER, `enabled_timestamp_ms` INTEGER, `subfolder_by_date` INTEGER, `account` TEXT, `upload_option` INTEGER, `name_collision_policy` INTEGER, `type` INTEGER, `hidden` INTEGER, `sub_folder_rule` INTEGER, `exclude_hidden` INTEGER, `last_scan_timestamp_ms` INTEGER)",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "_id",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "localPath",
+            "columnName": "local_path",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "remotePath",
+            "columnName": "remote_path",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "wifiOnly",
+            "columnName": "wifi_only",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "chargingOnly",
+            "columnName": "charging_only",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "existing",
+            "columnName": "existing",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "enabled",
+            "columnName": "enabled",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "enabledTimestampMs",
+            "columnName": "enabled_timestamp_ms",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "subfolderByDate",
+            "columnName": "subfolder_by_date",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "account",
+            "columnName": "account",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "uploadAction",
+            "columnName": "upload_option",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "nameCollisionPolicy",
+            "columnName": "name_collision_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "type",
+            "columnName": "type",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "hidden",
+            "columnName": "hidden",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "subFolderRule",
+            "columnName": "sub_folder_rule",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "excludeHidden",
+            "columnName": "exclude_hidden",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "lastScanTimestampMs",
+            "columnName": "last_scan_timestamp_ms",
+            "affinity": "INTEGER",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": true,
+          "columnNames": [
+            "_id"
+          ]
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "list_of_uploads",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `account_name` TEXT, `file_size` INTEGER, `status` INTEGER, `local_behaviour` INTEGER, `upload_time` INTEGER, `name_collision_policy` INTEGER, `is_create_remote_folder` INTEGER, `upload_end_timestamp` INTEGER, `last_result` INTEGER, `is_while_charging_only` INTEGER, `is_wifi_only` INTEGER, `created_by` INTEGER, `folder_unlock_token` TEXT)",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "_id",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "localPath",
+            "columnName": "local_path",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "remotePath",
+            "columnName": "remote_path",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "accountName",
+            "columnName": "account_name",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "fileSize",
+            "columnName": "file_size",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "status",
+            "columnName": "status",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "localBehaviour",
+            "columnName": "local_behaviour",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "uploadTime",
+            "columnName": "upload_time",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "nameCollisionPolicy",
+            "columnName": "name_collision_policy",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "isCreateRemoteFolder",
+            "columnName": "is_create_remote_folder",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "uploadEndTimestamp",
+            "columnName": "upload_end_timestamp",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "lastResult",
+            "columnName": "last_result",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "isWhileChargingOnly",
+            "columnName": "is_while_charging_only",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "isWifiOnly",
+            "columnName": "is_wifi_only",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "createdBy",
+            "columnName": "created_by",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "folderUnlockToken",
+            "columnName": "folder_unlock_token",
+            "affinity": "TEXT",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": true,
+          "columnNames": [
+            "_id"
+          ]
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "virtual",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `type` TEXT, `ocfile_id` INTEGER)",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "_id",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "type",
+            "columnName": "type",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "ocFileId",
+            "columnName": "ocfile_id",
+            "affinity": "INTEGER",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": true,
+          "columnNames": [
+            "_id"
+          ]
+        },
+        "indices": [],
+        "foreignKeys": []
+      }
+    ],
+    "views": [],
+    "setupQueries": [
+      "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e78b1402db9da7caff78c46fff585672')"
+    ]
+  }
+}

BIN
app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testNewFolderDialog.png


+ 193 - 0
app/src/androidTest/java/com/nextcloud/utils/FileNameValidatorTests.kt

@@ -0,0 +1,193 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package com.nextcloud.utils
+
+import com.nextcloud.utils.fileNameValidator.FileNameValidator
+import com.owncloud.android.AbstractIT
+import com.owncloud.android.R
+import com.owncloud.android.lib.resources.status.OCCapability
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+
+@Suppress("TooManyFunctions")
+class FileNameValidatorTests : AbstractIT() {
+
+    private var capability: OCCapability = fileDataStorageManager.getCapability(account.name)
+
+    @Before
+    fun setup() {
+        capability = capability.apply {
+            forbiddenFilenamesJson = """[".htaccess",".htaccess"]"""
+            forbiddenFilenameBaseNamesJson = """
+                                    ["con", "prn", "aux", "nul", "com0", "com1", "com2", "com3", "com4", 
+                                    "com5", "com6", "com7", "com8", "com9", "com¹", "com²", "com³", 
+                                    "lpt0", "lpt1", "lpt2", "lpt3", "lpt4", "lpt5", "lpt6", "lpt7", 
+                                    "lpt8", "lpt9", "lpt¹", "lpt²", "lpt³"]
+                                    """
+            forbiddenFilenameExtensionJson = """[".filepart",".part"]"""
+            forbiddenFilenameCharactersJson = """["<", ">", ":", "\\\\", "/", "|", "?", "*", "&"]"""
+        }
+    }
+
+    @Test
+    fun testInvalidCharacter() {
+        val result = FileNameValidator.checkFileName("file<name", capability, targetContext)
+        assertEquals(
+            String.format(targetContext.getString(R.string.file_name_validator_error_invalid_character), "<"),
+            result
+        )
+    }
+
+    @Test
+    fun testReservedName() {
+        val result = FileNameValidator.checkFileName("CON", capability, targetContext)
+        assertEquals(targetContext.getString(R.string.file_name_validator_error_reserved_names, "CON"), result)
+    }
+
+    @Test
+    fun testForbiddenFilenameExtension() {
+        val result = FileNameValidator.checkFileName("my_fav_file.filepart", capability, targetContext)
+        assertEquals(
+            targetContext.getString(R.string.file_name_validator_error_forbidden_file_extensions, "filepart"),
+            result
+        )
+    }
+
+    @Test
+    fun testEndsWithSpaceOrPeriod() {
+        val result = FileNameValidator.checkFileName("filename ", capability, targetContext)
+        assertEquals(targetContext.getString(R.string.file_name_validator_error_ends_with_space_period), result)
+
+        val result2 = FileNameValidator.checkFileName("filename.", capability, targetContext)
+        assertEquals(targetContext.getString(R.string.file_name_validator_error_ends_with_space_period), result2)
+    }
+
+    @Test
+    fun testEmptyFileName() {
+        val result = FileNameValidator.checkFileName("", capability, targetContext)
+        assertEquals(targetContext.getString(R.string.filename_empty), result)
+    }
+
+    @Test
+    fun testFileAlreadyExists() {
+        val existingFiles = mutableSetOf("existingFile")
+        val result = FileNameValidator.checkFileName("existingFile", capability, targetContext, existingFiles)
+        assertEquals(targetContext.getString(R.string.file_already_exists), result)
+    }
+
+    @Test
+    fun testValidFileName() {
+        val result = FileNameValidator.checkFileName("validFileName", capability, targetContext)
+        assertNull(result)
+    }
+
+    @Test
+    fun testIsFileHidden() {
+        assertTrue(FileNameValidator.isFileHidden(".hiddenFile"))
+        assertFalse(FileNameValidator.isFileHidden("visibleFile"))
+    }
+
+    @Test
+    fun testIsFileNameAlreadyExist() {
+        val existingFiles = mutableSetOf("existingFile")
+        assertTrue(FileNameValidator.isFileNameAlreadyExist("existingFile", existingFiles))
+        assertFalse(FileNameValidator.isFileNameAlreadyExist("newFile", existingFiles))
+    }
+
+    @Test
+    fun testValidFolderAndFilePaths() {
+        val folderPath = "validFolder"
+        val filePaths = listOf("file1.txt", "file2.doc", "file3.jpg")
+
+        val result = FileNameValidator.checkFolderAndFilePaths(folderPath, filePaths, capability, targetContext)
+        assertTrue(result)
+    }
+
+    @Test
+    fun testFolderPathWithReservedName() {
+        val folderPath = "CON"
+        val filePaths = listOf("file1.txt", "file2.doc", "file3.jpg")
+
+        val result = FileNameValidator.checkFolderAndFilePaths(folderPath, filePaths, capability, targetContext)
+        assertFalse(result)
+    }
+
+    @Test
+    fun testFilePathWithReservedName() {
+        val folderPath = "validFolder"
+        val filePaths = listOf("file1.txt", "PRN.doc", "file3.jpg")
+
+        val result = FileNameValidator.checkFolderAndFilePaths(folderPath, filePaths, capability, targetContext)
+        assertFalse(result)
+    }
+
+    @Test
+    fun testFolderPathWithInvalidCharacter() {
+        val folderPath = "invalid<Folder"
+        val filePaths = listOf("file1.txt", "file2.doc", "file3.jpg")
+
+        val result = FileNameValidator.checkFolderAndFilePaths(folderPath, filePaths, capability, targetContext)
+        assertFalse(result)
+    }
+
+    @Test
+    fun testFilePathWithInvalidCharacter() {
+        val folderPath = "validFolder"
+        val filePaths = listOf("file1.txt", "file|2.doc", "file3.jpg")
+
+        val result = FileNameValidator.checkFolderAndFilePaths(folderPath, filePaths, capability, targetContext)
+        assertFalse(result)
+    }
+
+    @Test
+    fun testFolderPathEndingWithSpace() {
+        val folderPath = "folderWithSpace "
+        val filePaths = listOf("file1.txt", "file2.doc", "file3.jpg")
+
+        val result = FileNameValidator.checkFolderAndFilePaths(folderPath, filePaths, capability, targetContext)
+        assertFalse(result)
+    }
+
+    @Test
+    fun testFilePathEndingWithPeriod() {
+        val folderPath = "validFolder"
+        val filePaths = listOf("file1.txt", "file2.doc", "file3.")
+
+        val result = FileNameValidator.checkFolderAndFilePaths(folderPath, filePaths, capability, targetContext)
+        assertFalse(result)
+    }
+
+    @Test
+    fun testFilePathWithNestedFolder() {
+        val folderPath = "validFolder\\secondValidFolder\\CON"
+        val filePaths = listOf("file1.txt", "file2.doc", "file3.")
+
+        val result = FileNameValidator.checkFolderAndFilePaths(folderPath, filePaths, capability, targetContext)
+        assertFalse(result)
+    }
+
+    @Test
+    fun testOnlyFolderPath() {
+        val folderPath = "/A1/Aaaww/W/C2/"
+
+        val result = FileNameValidator.checkFolderAndFilePaths(folderPath, listOf(), capability, targetContext)
+        assertTrue(result)
+    }
+
+    @Test
+    fun testOnlyFolderPathWithOneReservedName() {
+        val folderPath = "/A1/Aaaww/CON/W/C2/"
+
+        val result = FileNameValidator.checkFolderAndFilePaths(folderPath, listOf(), capability, targetContext)
+        assertFalse(result)
+    }
+}

+ 1 - 1
app/src/androidTest/java/com/owncloud/android/utils/EspressoIdlingResource.kt

@@ -1,7 +1,7 @@
 /*
  * Nextcloud - Android Client
  *
- * SPDX-FileCopyrightText: 2024 Your Name <your@email.com>
+ * SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
  * SPDX-License-Identifier: AGPL-3.0-or-later
  */
 

+ 1 - 0
app/src/main/java/com/nextcloud/client/assistant/repository/AssistantMockRepository.kt

@@ -31,6 +31,7 @@ class AssistantMockRepository(private val giveEmptyTasks: Boolean = false) : Ass
         return RemoteOperationResult<Void>(RemoteOperationResult.ResultCode.OK)
     }
 
+    @Suppress("LongMethod")
     override fun getTaskList(appId: String): RemoteOperationResult<TaskList> {
         val taskList = if (giveEmptyTasks) {
             TaskList(listOf())

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

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

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

@@ -122,5 +122,13 @@ data class CapabilityEntity(
     @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_DROP_ACCOUNT)
     val dropAccount: Int?,
     @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_SECURITY_GUARD)
-    val securityGuard: Int?
+    val securityGuard: Int?,
+    @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_FORBIDDEN_FILENAME_CHARACTERS)
+    val forbiddenFileNameCharacters: Int?,
+    @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_FORBIDDEN_FILENAMES)
+    val forbiddenFileNames: Int?,
+    @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_FORBIDDEN_FORBIDDEN_FILENAME_EXTENSIONS)
+    val forbiddenFileNameExtensions: Int?,
+    @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_FORBIDDEN_FORBIDDEN_FILENAME_BASE_NAMES)
+    val forbiddenFilenameBaseNames: Int?
 )

+ 33 - 0
app/src/main/java/com/nextcloud/utils/extensions/OCCapabilityExtensions.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.utils.extensions
+
+import com.google.gson.Gson
+import com.owncloud.android.lib.resources.status.OCCapability
+import org.json.JSONException
+
+private val gson = Gson()
+
+fun OCCapability.forbiddenFilenames(): List<String> = jsonToList(forbiddenFilenamesJson)
+
+fun OCCapability.forbiddenFilenameCharacters(): List<String> = jsonToList(forbiddenFilenameCharactersJson)
+
+fun OCCapability.forbiddenFilenameExtension(): List<String> = jsonToList(forbiddenFilenameExtensionJson)
+
+fun OCCapability.forbiddenFilenameBaseNames(): List<String> = jsonToList(forbiddenFilenameBaseNamesJson)
+
+@Suppress("ReturnCount")
+private fun jsonToList(json: String?): List<String> {
+    if (json == null) return emptyList()
+
+    return try {
+        return gson.fromJson(json, Array<String>::class.java).toList()
+    } catch (e: JSONException) {
+        emptyList()
+    }
+}

+ 15 - 0
app/src/main/java/com/nextcloud/utils/extensions/StringExtensions.kt

@@ -15,3 +15,18 @@ fun String.getRandomString(length: Int): String {
 
     return this + result
 }
+
+fun String.removeFileExtension(): String {
+    val dotIndex = lastIndexOf('.')
+    return if (dotIndex != -1) {
+        substring(0, dotIndex)
+    } else {
+        this
+    }
+}
+
+object StringConstants {
+    const val SLASH = "/"
+    const val DOT = "."
+    const val SPACE = " "
+}

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

@@ -0,0 +1,157 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package com.nextcloud.utils.fileNameValidator
+
+import android.content.Context
+import android.text.TextUtils
+import com.nextcloud.utils.extensions.StringConstants
+import com.nextcloud.utils.extensions.forbiddenFilenameBaseNames
+import com.nextcloud.utils.extensions.forbiddenFilenameCharacters
+import com.nextcloud.utils.extensions.forbiddenFilenameExtension
+import com.nextcloud.utils.extensions.forbiddenFilenames
+import com.nextcloud.utils.extensions.removeFileExtension
+import com.owncloud.android.R
+import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.lib.resources.status.OCCapability
+
+object FileNameValidator {
+
+    /**
+     * Checks the validity of a file name.
+     *
+     * @param filename The name of the file to validate.
+     * @param capability The capabilities affecting the validation criteria
+     * such as forbiddenFilenames, forbiddenCharacters.
+     * @param context The context used for retrieving error messages.
+     * @param existedFileNames Set of existing file names to avoid duplicates.
+     * @return An error message if the filename is invalid, null otherwise.
+     */
+    @Suppress("ReturnCount")
+    fun checkFileName(
+        filename: String,
+        capability: OCCapability,
+        context: Context,
+        existedFileNames: MutableSet<String>? = null
+    ): String? {
+        if (TextUtils.isEmpty(filename)) {
+            return context.getString(R.string.filename_empty)
+        }
+
+        existedFileNames?.let {
+            if (isFileNameAlreadyExist(filename, existedFileNames)) {
+                return context.getString(R.string.file_already_exists)
+            }
+        }
+
+        if (filename.endsWith(StringConstants.SPACE) || filename.endsWith(StringConstants.DOT)) {
+            return context.getString(R.string.file_name_validator_error_ends_with_space_period)
+        }
+
+        checkInvalidCharacters(filename, capability, context)?.let {
+            return it
+        }
+
+        capability.forbiddenFilenameBaseNames().let {
+            val forbiddenFilenameBaseNames = capability.forbiddenFilenameBaseNames().map { it.lowercase() }
+
+            if (forbiddenFilenameBaseNames.contains(filename.lowercase()) || forbiddenFilenameBaseNames.contains(
+                    filename.removeFileExtension().lowercase()
+                )
+            ) {
+                return context.getString(
+                    R.string.file_name_validator_error_reserved_names,
+                    filename.substringBefore(StringConstants.DOT)
+                )
+            }
+        }
+
+        capability.forbiddenFilenamesJson?.let {
+            val forbiddenFilenames = capability.forbiddenFilenames().map { it.lowercase() }
+
+            if (forbiddenFilenames.contains(filename.uppercase()) || forbiddenFilenames.contains(
+                    filename.removeFileExtension().uppercase()
+                )
+            ) {
+                return context.getString(
+                    R.string.file_name_validator_error_reserved_names,
+                    filename.substringBefore(StringConstants.DOT)
+                )
+            }
+        }
+
+        capability.forbiddenFilenameExtensionJson?.let {
+            val forbiddenFilenameExtension = capability.forbiddenFilenameExtension()
+
+            if (forbiddenFilenameExtension.any { filename.endsWith(it, ignoreCase = true) }) {
+                return context.getString(
+                    R.string.file_name_validator_error_forbidden_file_extensions,
+                    filename.substringAfter(StringConstants.DOT)
+                )
+            }
+        }
+
+        return null
+    }
+
+    /**
+     * Checks the validity of file paths wanted to move or copied inside the folder.
+     *
+     * @param folderPath Target folder to be used for move or copy.
+     * @param filePaths The list of file paths to move or copy to folderPath.
+     * @param capability The capabilities affecting the validation criteria.
+     * @param context The context used for retrieving error messages.
+     * @return True if folder path and file paths are valid, false otherwise.
+     */
+    fun checkFolderAndFilePaths(
+        folderPath: String,
+        filePaths: List<String>,
+        capability: OCCapability,
+        context: Context
+    ): Boolean {
+        return checkFolderPath(folderPath, capability, context) && checkFilePaths(filePaths, capability, context)
+    }
+
+    fun checkParentRemotePaths(filePaths: List<OCFile>, capability: OCCapability, context: Context): Boolean {
+        return filePaths.all {
+            if (it.parentRemotePath != StringConstants.SLASH) {
+                val parentFolderName = it.parentRemotePath.replace(StringConstants.SLASH, "")
+                checkFileName(parentFolderName, capability, context) == null
+            } else {
+                true
+            }
+        }
+    }
+
+    private fun checkFilePaths(filePaths: List<String>, capability: OCCapability, context: Context): Boolean {
+        return filePaths.all { checkFileName(it, capability, context) == null }
+    }
+
+    fun checkFolderPath(folderPath: String, capability: OCCapability, context: Context): Boolean {
+        return folderPath.split("[/\\\\]".toRegex())
+            .none { it.isNotEmpty() && checkFileName(it, capability, context) != null }
+    }
+
+    @Suppress("ReturnCount")
+    private fun checkInvalidCharacters(name: String, capability: OCCapability, context: Context): String? {
+        capability.forbiddenFilenameCharactersJson?.let {
+            val forbiddenFilenameCharacters = capability.forbiddenFilenameCharacters()
+
+            val invalidCharacter = forbiddenFilenameCharacters.firstOrNull { name.contains(it) }
+
+            if (invalidCharacter == null) return null
+
+            return context.getString(R.string.file_name_validator_error_invalid_character, invalidCharacter)
+        }
+
+        return null
+    }
+
+    fun isFileHidden(name: String): Boolean = !TextUtils.isEmpty(name) && name[0] == '.'
+
+    fun isFileNameAlreadyExist(name: String, fileNames: MutableSet<String>): Boolean = fileNames.contains(name)
+}

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

@@ -2053,6 +2053,11 @@ public class FileDataStorageManager {
         contentValues.put(ProviderTableMeta.CAPABILITIES_DROP_ACCOUNT, capability.getDropAccount().getValue());
         contentValues.put(ProviderTableMeta.CAPABILITIES_SECURITY_GUARD, capability.getSecurityGuard().getValue());
 
+        contentValues.put(ProviderTableMeta.CAPABILITIES_FORBIDDEN_FILENAME_CHARACTERS, capability.getForbiddenFilenameCharactersJson());
+        contentValues.put(ProviderTableMeta.CAPABILITIES_FORBIDDEN_FILENAMES, capability.getForbiddenFilenamesJson());
+        contentValues.put(ProviderTableMeta.CAPABILITIES_FORBIDDEN_FORBIDDEN_FILENAME_EXTENSIONS, capability.getForbiddenFilenameExtensionJson());
+        contentValues.put(ProviderTableMeta.CAPABILITIES_FORBIDDEN_FORBIDDEN_FILENAME_BASE_NAMES, capability.getForbiddenFilenameBaseNamesJson());
+
         return contentValues;
     }
 
@@ -2221,7 +2226,13 @@ public class FileDataStorageManager {
             capability.setGroupfolders(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_GROUPFOLDERS));
             capability.setDropAccount(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_DROP_ACCOUNT));
             capability.setSecurityGuard(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_SECURITY_GUARD));
+
+            capability.setForbiddenFilenameCharactersJson(getString(cursor, ProviderTableMeta.CAPABILITIES_FORBIDDEN_FILENAME_CHARACTERS));
+            capability.setForbiddenFilenamesJson(getString(cursor, ProviderTableMeta.CAPABILITIES_FORBIDDEN_FILENAMES));
+            capability.setForbiddenFilenameExtensionJson(getString(cursor, ProviderTableMeta.CAPABILITIES_FORBIDDEN_FORBIDDEN_FILENAME_EXTENSIONS));
+            capability.setForbiddenFilenameBaseNamesJson(getString(cursor, ProviderTableMeta.CAPABILITIES_FORBIDDEN_FORBIDDEN_FILENAME_BASE_NAMES));
         }
+
         return capability;
     }
 

+ 5 - 1
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 = 81;
+    public static final int DB_VERSION = 82;
 
     private ProviderMeta() {
         // No instance
@@ -259,6 +259,10 @@ public class ProviderMeta {
         public static final String CAPABILITIES_GROUPFOLDERS = "groupfolders";
         public static final String CAPABILITIES_DROP_ACCOUNT = "drop_account";
         public static final String CAPABILITIES_SECURITY_GUARD = "security_guard";
+        public static final String CAPABILITIES_FORBIDDEN_FILENAME_CHARACTERS = "forbidden_filename_characters";
+        public static final String CAPABILITIES_FORBIDDEN_FILENAMES = "forbidden_filenames";
+        public static final String CAPABILITIES_FORBIDDEN_FORBIDDEN_FILENAME_EXTENSIONS = "forbidden_filename_extensions";
+        public static final String CAPABILITIES_FORBIDDEN_FORBIDDEN_FILENAME_BASE_NAMES = "forbidden_filename_basenames";
 
         //Columns of Uploads table
         public static final String UPLOADS_LOCAL_PATH = "local_path";

+ 52 - 5
app/src/main/java/com/owncloud/android/providers/DocumentsStorageProvider.java

@@ -34,6 +34,8 @@ import com.nextcloud.client.jobs.upload.FileUploadWorker;
 import com.nextcloud.client.preferences.AppPreferences;
 import com.nextcloud.client.preferences.AppPreferencesImpl;
 import com.nextcloud.client.utils.HashUtil;
+import com.nextcloud.utils.extensions.ContextExtensionsKt;
+import com.nextcloud.utils.fileNameValidator.FileNameValidator;
 import com.owncloud.android.MainApp;
 import com.owncloud.android.R;
 import com.owncloud.android.datamodel.FileDataStorageManager;
@@ -47,6 +49,7 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.lib.resources.files.CheckEtagRemoteOperation;
 import com.owncloud.android.lib.resources.files.UploadFileRemoteOperation;
+import com.owncloud.android.lib.resources.status.OCCapability;
 import com.owncloud.android.operations.CopyFileOperation;
 import com.owncloud.android.operations.CreateFolderOperation;
 import com.owncloud.android.operations.DownloadFileOperation;
@@ -59,6 +62,7 @@ import com.owncloud.android.ui.helpers.FileOperationsHelper;
 import com.owncloud.android.utils.FileStorageUtils;
 import com.owncloud.android.utils.FileUtil;
 import com.owncloud.android.utils.MimeTypeUtil;
+import com.owncloud.android.utils.theme.CapabilityUtils;
 
 import org.nextcloud.providers.cursors.FileCursor;
 import org.nextcloud.providers.cursors.RootCursor;
@@ -95,6 +99,8 @@ public class DocumentsStorageProvider extends DocumentsProvider {
 
     @Inject UserAccountManager accountManager;
 
+    private boolean isFolderPathValid = true;
+
     @VisibleForTesting
     static final String DOCUMENTID_SEPARATOR = "/";
     private static final int DOCUMENTID_PARTS = 2;
@@ -187,6 +193,11 @@ public class DocumentsStorageProvider extends DocumentsProvider {
         throws FileNotFoundException {
         Log_OC.d(TAG, "openDocument(), id=" + documentId);
 
+        if (!isFolderPathValid) {
+            Log_OC.d(TAG, "Folder path is not valid, operation is cancelled");
+            return null;
+        }
+
         Document document = toDocument(documentId);
         Context context = getNonNullContext();
 
@@ -347,8 +358,13 @@ public class DocumentsStorageProvider extends DocumentsProvider {
     public String renameDocument(String documentId, String displayName) throws FileNotFoundException {
         Log_OC.d(TAG, "renameDocument(), id=" + documentId);
 
-        Document document = toDocument(documentId);
+        String errorMessage = checkFileName(displayName);
+        if (errorMessage != null) {
+            ContextExtensionsKt.showToast(getNonNullContext(), errorMessage);
+            return null;
+        }
 
+        Document document = toDocument(documentId);
         RemoteOperationResult result = new RenameFileOperation(document.getRemotePath(),
                                                                displayName,
                                                                document.getStorageManager())
@@ -370,11 +386,17 @@ public class DocumentsStorageProvider extends DocumentsProvider {
     public String copyDocument(String sourceDocumentId, String targetParentDocumentId) throws FileNotFoundException {
         Log_OC.d(TAG, "copyDocument(), id=" + sourceDocumentId);
 
-        Document document = toDocument(sourceDocumentId);
-
-        FileDataStorageManager storageManager = document.getStorageManager();
         Document targetFolder = toDocument(targetParentDocumentId);
 
+        String filename = targetFolder.getFile().getFileName();
+        isFolderPathValid = checkFolderPath(filename);
+        if (!isFolderPathValid) {
+            ContextExtensionsKt.showToast(getNonNullContext(), R.string.file_name_validator_error_contains_reserved_names_or_invalid_characters);
+            return null;
+        }
+
+        Document document = toDocument(sourceDocumentId);
+        FileDataStorageManager storageManager = document.getStorageManager();
         RemoteOperationResult result = new CopyFileOperation(document.getRemotePath(),
                                                              targetFolder.getRemotePath(),
                                                              document.getStorageManager())
@@ -422,9 +444,16 @@ public class DocumentsStorageProvider extends DocumentsProvider {
         throws FileNotFoundException {
         Log_OC.d(TAG, "moveDocument(), id=" + sourceDocumentId);
 
-        Document document = toDocument(sourceDocumentId);
         Document targetFolder = toDocument(targetParentDocumentId);
 
+        String filename = targetFolder.getFile().getFileName();
+        isFolderPathValid = checkFolderPath(filename);
+        if (!isFolderPathValid) {
+            ContextExtensionsKt.showToast(getNonNullContext(), R.string.file_name_validator_error_contains_reserved_names_or_invalid_characters);
+            return null;
+        }
+
+        Document document = toDocument(sourceDocumentId);
         RemoteOperationResult result = new MoveFileOperation(document.getRemotePath(),
                                                              targetFolder.getRemotePath(),
                                                              document.getStorageManager())
@@ -463,10 +492,28 @@ public class DocumentsStorageProvider extends DocumentsProvider {
         return result;
     }
 
+    private OCCapability getCapabilities() {
+        return CapabilityUtils.getCapability(accountManager.getUser(), getNonNullContext());
+    }
+
+    private boolean checkFolderPath(String filename) {
+        return FileNameValidator.INSTANCE.checkFolderPath(filename, getCapabilities(), getNonNullContext());
+    }
+
+    private String checkFileName(String filename) {
+        return FileNameValidator.INSTANCE.checkFileName(filename, getCapabilities(), getNonNullContext(),null);
+    }
+
     @Override
     public String createDocument(String documentId, String mimeType, String displayName) throws FileNotFoundException {
         Log_OC.d(TAG, "createDocument(), id=" + documentId);
 
+        String errorMessage = checkFileName(displayName);
+        if (errorMessage != null) {
+            ContextExtensionsKt.showToast(getNonNullContext(), errorMessage);
+            return null;
+        }
+
         Document folderDocument = toDocument(documentId);
 
         if (DocumentsContract.Document.MIME_TYPE_DIR.equalsIgnoreCase(mimeType)) {

+ 1 - 0
app/src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.kt

@@ -154,6 +154,7 @@ class ConflictsResolveActivity : FileActivity(), OnConflictDecisionMadeListener
         listener?.conflictDecisionMade(decision)
     }
 
+    @Suppress("ReturnCount")
     override fun onStart() {
         super.onStart()
         if (account == null) {

+ 34 - 6
app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt

@@ -23,6 +23,7 @@ import android.view.View
 import androidx.activity.OnBackPressedCallback
 import androidx.localbroadcastmanager.content.LocalBroadcastManager
 import com.nextcloud.client.di.Injectable
+import com.nextcloud.utils.fileNameValidator.FileNameValidator
 import com.owncloud.android.R
 import com.owncloud.android.databinding.FilesFolderPickerBinding
 import com.owncloud.android.databinding.FilesPickerBinding
@@ -379,14 +380,36 @@ open class FolderPickerActivity :
     private fun toggleChooseEnabled() {
         if (this is FilePickerActivity) {
             return
+        }
+
+        val selectedFolderPathTitle = getSelectedFolderPathTitle()
+        val isFolderPathValid = if (selectedFolderPathTitle != null) {
+            FileNameValidator.checkFolderPath(selectedFolderPathTitle, capabilities, this)
         } else {
-            folderPickerBinding.folderPickerBtnCopy.isEnabled = checkFolderSelectable()
-            folderPickerBinding.folderPickerBtnMove.isEnabled = checkFolderSelectable()
+            true
+        }
+
+        checkButtonStates(isFolderPathValid)
+
+        if (!isFolderPathValid) {
+            DisplayUtils.showSnackMessage(
+                this,
+                R.string.file_name_validator_error_contains_reserved_names_or_invalid_characters
+            )
+            return
+        }
+    }
+
+    private fun checkButtonStates(isConditionMet: Boolean) {
+        folderPickerBinding.run {
+            folderPickerBtnChoose.isEnabled = isConditionMet
+            folderPickerBtnCopy.isEnabled = isFolderSelectable() && isConditionMet
+            folderPickerBtnMove.isEnabled = isFolderSelectable() && isConditionMet
         }
     }
 
     // for copy and move, disable selecting parent folder of target files
-    private fun checkFolderSelectable(): Boolean {
+    private fun isFolderSelectable(): Boolean {
         return when {
             action != MOVE_OR_COPY -> true
             targetFilePaths.isNullOrEmpty() -> true
@@ -407,13 +430,17 @@ open class FolderPickerActivity :
             val atRoot = (currentDir == null || currentDir.parentId == 0L)
             actionBar.setDisplayHomeAsUpEnabled(!atRoot)
             actionBar.setHomeButtonEnabled(!atRoot)
-            val title = if (atRoot) captionText ?: "" else currentDir?.fileName
-            title?.let {
-                viewThemeUtils.files.themeActionBar(this, actionBar, title)
+            getSelectedFolderPathTitle()?.let {
+                viewThemeUtils.files.themeActionBar(this, actionBar, it)
             }
         }
     }
 
+    private fun getSelectedFolderPathTitle(): String? {
+        val atRoot = (currentDir == null || currentDir.parentId == 0L)
+        return if (atRoot) captionText ?: "" else currentDir?.fileName
+    }
+
     private fun initControls() {
         if (this is FilePickerActivity) {
             viewThemeUtils.material.colorMaterialButtonPrimaryFilled(filesPickerBinding.folderPickerBtnCancel)
@@ -441,6 +468,7 @@ open class FolderPickerActivity :
         }
     }
 
+    @Suppress("MagicNumber")
     private fun processOperation(action: String?) {
         val i = intent
         val resultData = Intent()

+ 20 - 5
app/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java

@@ -55,6 +55,7 @@ import com.nextcloud.client.preferences.AppPreferences;
 import com.nextcloud.utils.extensions.BundleExtensionsKt;
 import com.nextcloud.utils.extensions.FileExtensionsKt;
 import com.nextcloud.utils.extensions.IntentExtensionsKt;
+import com.nextcloud.utils.fileNameValidator.FileNameValidator;
 import com.owncloud.android.MainApp;
 import com.owncloud.android.R;
 import com.owncloud.android.databinding.ReceiveExternalFilesBinding;
@@ -295,6 +296,12 @@ public class ReceiveExternalFilesActivity extends FileActivity
     @Override
     public void selectFile(OCFile file) {
         if (file.isFolder()) {
+            String filenameErrorMessage = FileNameValidator.INSTANCE.checkFileName(file.getFileName(), getCapabilities(), this, null);
+            if (filenameErrorMessage != null) {
+                DisplayUtils.showSnackMessage(this, filenameErrorMessage);
+                return;
+            }
+
             if (file.isEncrypted() &&
                 !FileOperationsHelper.isEndToEndEncryptionSetup(this, getUser().orElseThrow(IllegalAccessError::new))) {
                 DisplayUtils.showSnackMessage(this, R.string.e2e_not_yet_setup);
@@ -661,8 +668,17 @@ public class ReceiveExternalFilesActivity extends FileActivity
         if (id == R.id.uploader_choose_folder) {
             mUploadPath = "";   // first element in mParents is root dir, represented by "";
             // init mUploadPath with "/" results in a "//" prefix
+
+            StringBuilder stringBuilder = new StringBuilder();
             for (String p : mParents) {
-                mUploadPath += p + OCFile.PATH_SEPARATOR;
+                stringBuilder.append(p).append(OCFile.PATH_SEPARATOR);
+            }
+            mUploadPath = stringBuilder.toString();
+
+            boolean isPathValid = FileNameValidator.INSTANCE.checkFolderPath(mUploadPath, getCapabilities(), this);
+            if (!isPathValid) {
+                DisplayUtils.showSnackMessage(this, R.string.file_name_validator_error_contains_reserved_names_or_invalid_characters);
+                return;
             }
 
             if (mUploadFromTmpFile) {
@@ -938,12 +954,11 @@ public class ReceiveExternalFilesActivity extends FileActivity
                 messageResId = R.string.uploader_error_message_read_permission_not_granted;
             } else if (resultCode == UriUploader.UriUploaderResultCode.ERROR_UNKNOWN) {
                 messageResId = R.string.common_error_unknown;
+            } else if (resultCode == UriUploader.UriUploaderResultCode.INVALID_FILE_NAME) {
+                messageResId = R.string.file_name_validator_upload_content_error;
             }
 
-            showErrorDialog(
-                messageResId,
-                messageResTitle
-                           );
+            showErrorDialog(messageResId, messageResTitle);
         }
     }
 

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

@@ -35,6 +35,7 @@ import com.nextcloud.client.jobs.upload.FileUploadWorker;
 import com.nextcloud.client.preferences.AppPreferences;
 import com.nextcloud.utils.extensions.ActivityExtensionsKt;
 import com.nextcloud.utils.extensions.FileExtensionsKt;
+import com.nextcloud.utils.fileNameValidator.FileNameValidator;
 import com.owncloud.android.R;
 import com.owncloud.android.databinding.UploadFilesLayoutBinding;
 import com.owncloud.android.lib.common.utils.Log_OC;
@@ -642,8 +643,15 @@ public class UploadFilesActivity extends DrawerActivity implements LocalFileList
 
                     finish();
                 } else {
-                    new CheckAvailableSpaceTask(this, mFileListFragment.getCheckedFilePaths())
-                        .execute(binding.uploadFilesSpinnerBehaviour.getSelectedItemPosition() == 0);
+                    String[] selectedFilePaths = mFileListFragment.getCheckedFilePaths();
+                    String filenameErrorMessage = checkFileNameBeforeUpload(selectedFilePaths);
+                    if (filenameErrorMessage != null) {
+                        DisplayUtils.showSnackMessage(this, filenameErrorMessage);
+                        return;
+                    }
+
+                    boolean isPositionZero = (binding.uploadFilesSpinnerBehaviour.getSelectedItemPosition() == 0);
+                    new CheckAvailableSpaceTask(this, selectedFilePaths).execute(isPositionZero);
                 }
             } else {
                 requestPermissions();
@@ -651,6 +659,19 @@ public class UploadFilesActivity extends DrawerActivity implements LocalFileList
         }
     }
 
+    private String checkFileNameBeforeUpload(String[] selectedFilePaths) {
+        for (String filePath : selectedFilePaths) {
+            File file = new File(filePath);
+            String filenameErrorMessage = FileNameValidator.INSTANCE.checkFileName(file.getName(), getCapabilities(), this, null);
+
+            if (filenameErrorMessage != null) {
+                return filenameErrorMessage;
+            }
+        }
+
+        return null;
+    }
+
     @Override
     public void onConfirmation(String callerTag) {
         Log_OC.d(TAG, "Positive button in dialog was clicked; dialog tag is " + callerTag);

+ 10 - 6
app/src/main/java/com/owncloud/android/ui/adapter/TemplateAdapter.java

@@ -1,6 +1,7 @@
 /*
  * Nextcloud - Android Client
  *
+ * SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
  * SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
  * SPDX-FileCopyrightText: 2018 Tobias Kaminsky <tobias@kaminsky.me>
  * SPDX-FileCopyrightText: 2018 Nextcloud GmbH
@@ -8,6 +9,7 @@
  */
 package com.owncloud.android.ui.adapter;
 
+import android.annotation.SuppressLint;
 import android.content.Context;
 import android.graphics.drawable.Drawable;
 import android.view.LayoutInflater;
@@ -35,11 +37,11 @@ import androidx.recyclerview.widget.RecyclerView;
 public class TemplateAdapter extends RecyclerView.Adapter<TemplateAdapter.ViewHolder> {
 
     private TemplateList templateList = new TemplateList();
-    private ClickListener clickListener;
-    private Context context;
-    private CurrentAccountProvider currentAccountProvider;
-    private ClientFactory clientFactory;
-    private String mimetype;
+    private final ClickListener clickListener;
+    private final Context context;
+    private final CurrentAccountProvider currentAccountProvider;
+    private final ClientFactory clientFactory;
+    private final String mimetype;
     private Template selectedTemplate;
     private final ViewThemeUtils viewThemeUtils;
 
@@ -78,6 +80,7 @@ public class TemplateAdapter extends RecyclerView.Adapter<TemplateAdapter.ViewHo
         this.templateList = templateList;
     }
 
+    @SuppressLint("NotifyDataSetChanged")
     public void setTemplateAsActive(Template template) {
         selectedTemplate = template;
         notifyDataSetChanged();
@@ -101,7 +104,8 @@ public class TemplateAdapter extends RecyclerView.Adapter<TemplateAdapter.ViewHo
             super(binding.getRoot());
             this.binding = binding;
             viewThemeUtils.files.themeTemplateCardView(this.binding.templateContainer);
-            itemView.setOnClickListener(this);
+            binding.templateLayout.setOnClickListener(this);
+            binding.templateContainer.setOnClickListener(this);
         }
 
         @Override

+ 44 - 27
app/src/main/java/com/owncloud/android/ui/dialog/ChooseRichDocumentsTemplateDialogFragment.kt

@@ -27,6 +27,7 @@ import com.nextcloud.client.di.Injectable
 import com.nextcloud.client.network.ClientFactory
 import com.nextcloud.client.network.ClientFactory.CreationException
 import com.nextcloud.utils.extensions.getParcelableArgument
+import com.nextcloud.utils.fileNameValidator.FileNameValidator
 import com.owncloud.android.MainApp
 import com.owncloud.android.R
 import com.owncloud.android.databinding.ChooseTemplateBinding
@@ -211,22 +212,23 @@ class ChooseRichDocumentsTemplateDialogFragment :
             Type.PRESENTATION -> {
                 R.string.create_new_presentation
             }
-
-            else -> R.string.select_template
         }
     }
 
     @Suppress("DEPRECATION")
     private fun createFromTemplate(template: Template, path: String) {
-        waitDialog = newInstance(R.string.wait_a_moment, false)
-        waitDialog?.show(parentFragmentManager, WAIT_DIALOG_TAG)
+        waitDialog = newInstance(R.string.wait_a_moment, false).also {
+            it.show(parentFragmentManager, WAIT_DIALOG_TAG)
+        }
         CreateFileFromTemplateTask(this, client, template, path, currentAccount.user).execute()
     }
 
     @SuppressLint("NotifyDataSetChanged")
     fun setTemplateList(templateList: List<Template>?) {
-        adapter?.setTemplateList(templateList)
-        adapter?.notifyDataSetChanged()
+        adapter?.let {
+            it.setTemplateList(templateList)
+            it.notifyDataSetChanged()
+        }
     }
 
     private val fileNameText: String
@@ -248,9 +250,18 @@ class ChooseRichDocumentsTemplateDialogFragment :
 
         val selectedTemplate = adapter?.selectedTemplate
 
+        val errorMessage = FileNameValidator.checkFileName(
+            name,
+            fileDataStorageManager.getCapability(currentAccount.user),
+            requireContext(),
+            fileNames
+        )
+
         if (selectedTemplate == null) {
             DisplayUtils.showSnackMessage(binding.list, R.string.select_one_template)
-        } else if (name.isEmpty() || name.equals(DOT + selectedTemplate.extension, ignoreCase = true)) {
+        } else if (errorMessage != null) {
+            DisplayUtils.showSnackMessage(requireActivity(), errorMessage)
+        } else if (name.equals(DOT + selectedTemplate.extension, ignoreCase = true)) {
             DisplayUtils.showSnackMessage(binding.list, R.string.enter_filename)
         } else if (!name.endsWith(selectedTemplate.extension)) {
             createFromTemplate(selectedTemplate, path + DOT + selectedTemplate.extension)
@@ -283,31 +294,37 @@ class ChooseRichDocumentsTemplateDialogFragment :
     }
 
     private fun checkEnablingCreateButton() {
-        positiveButton?.let {
-            val selectedTemplate = adapter?.selectedTemplate
-            val name = fileNameText
-            val isNameJustExtension = selectedTemplate != null && name.equals(
-                DOT + selectedTemplate.extension,
-                ignoreCase = true
-            )
-            val isNameEmpty = name.isEmpty() || isNameJustExtension
-            val state = selectedTemplate != null && !isNameEmpty && fileNames?.contains(name) == false
+        if (positiveButton == null) {
+            return
+        }
 
-            it.isEnabled = selectedTemplate != null && name.isNotEmpty() && !name.equals(
+        val selectedTemplate = adapter?.selectedTemplate
+        val name = fileNameText
+        val errorMessage = FileNameValidator.checkFileName(
+            name,
+            fileDataStorageManager.getCapability(currentAccount.user),
+            requireContext(),
+            fileNames
+        )
+        val isExtension = (
+            selectedTemplate == null || !name.equals(
                 DOT + selectedTemplate.extension,
                 ignoreCase = true
             )
-            it.isEnabled = state
-            it.isClickable = state
+            )
+        val isEnable = isExtension && errorMessage == null
 
-            binding.filenameContainer.isErrorEnabled = !state
+        positiveButton?.let {
+            it.isEnabled = isEnable
+            it.isClickable = isEnable
+        }
 
-            if (!state) {
-                if (isNameEmpty) {
-                    binding.filenameContainer.error = getText(R.string.filename_empty)
-                } else {
-                    binding.filenameContainer.error = getText(R.string.file_already_exists)
-                }
+        binding.filenameContainer.run {
+            isErrorEnabled = !isEnable
+            error = if (!isEnable) {
+                errorMessage ?: getText(R.string.filename_empty)
+            } else {
+                null
             }
         }
     }
@@ -418,7 +435,7 @@ class ChooseRichDocumentsTemplateDialogFragment :
         @Deprecated("Deprecated in Java")
         override fun onPostExecute(templateList: List<Template>) {
             val fragment = chooseTemplateDialogFragmentWeakReference.get()
-            if (fragment == null) {
+            if (fragment == null || !fragment.isAdded) {
                 Log_OC.e(TAG, "Error streaming file: no previewMediaFragment!")
                 return
             }

+ 147 - 130
app/src/main/java/com/owncloud/android/ui/dialog/ChooseTemplateDialogFragment.kt

@@ -1,6 +1,7 @@
 /*
  * Nextcloud - Android Client
  *
+ * SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
  * SPDX-FileCopyrightText: 2023 TSI-mc
  * SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro.brey@nextcloud.com>
  * SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
@@ -10,6 +11,7 @@
  */
 package com.owncloud.android.ui.dialog
 
+import android.annotation.SuppressLint
 import android.app.Dialog
 import android.content.Intent
 import android.os.AsyncTask
@@ -30,6 +32,7 @@ import com.nextcloud.client.di.Injectable
 import com.nextcloud.client.network.ClientFactory
 import com.nextcloud.client.network.ClientFactory.CreationException
 import com.nextcloud.utils.extensions.getParcelableArgument
+import com.nextcloud.utils.fileNameValidator.FileNameValidator
 import com.owncloud.android.MainApp
 import com.owncloud.android.R
 import com.owncloud.android.databinding.ChooseTemplateBinding
@@ -41,6 +44,7 @@ import com.owncloud.android.lib.common.TemplateList
 import com.owncloud.android.lib.common.utils.Log_OC
 import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation
 import com.owncloud.android.lib.resources.files.model.RemoteFile
+import com.owncloud.android.lib.resources.status.OCCapability
 import com.owncloud.android.ui.activity.ExternalSiteWebView
 import com.owncloud.android.ui.activity.TextEditorWebView
 import com.owncloud.android.ui.adapter.TemplateAdapter
@@ -79,12 +83,6 @@ class ChooseTemplateDialogFragment : DialogFragment(), View.OnClickListener, Tem
     private var positiveButton: MaterialButton? = null
     private var creator: Creator? = null
 
-    enum class Type {
-        DOCUMENT,
-        SPREADSHEET,
-        PRESENTATION
-    }
-
     private var _binding: ChooseTemplateBinding? = null
     val binding get() = _binding!!
 
@@ -92,18 +90,21 @@ class ChooseTemplateDialogFragment : DialogFragment(), View.OnClickListener, Tem
         super.onStart()
         val alertDialog = dialog as AlertDialog
 
-        val positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE) as MaterialButton
-        viewThemeUtils.material.colorMaterialButtonPrimaryTonal(positiveButton)
-
-        val negativeButton = alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE) as MaterialButton
-        viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(negativeButton)
+        val negativeButton = alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE) as? MaterialButton
+        negativeButton?.let {
+            viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(negativeButton)
+        }
 
-        positiveButton.setOnClickListener(this)
-        positiveButton.isEnabled = false
-        positiveButton.isClickable = false
+        val positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE) as? MaterialButton
+        positiveButton?.let {
+            viewThemeUtils.material.colorMaterialButtonPrimaryTonal(positiveButton)
+            positiveButton.setOnClickListener(this)
+            positiveButton.isEnabled = false
+            positiveButton.isClickable = false
+            this.positiveButton = positiveButton
+        }
 
-        this.positiveButton = positiveButton
-        checkEnablingCreateButton()
+        checkFileNameAfterEachType()
     }
 
     override fun onResume() {
@@ -126,10 +127,8 @@ class ChooseTemplateDialogFragment : DialogFragment(), View.OnClickListener, Tem
 
         fileNames = fileDataStorageManager.getFolderContent(parentFolder, false).map { it.fileName }
 
-        // Inflate the layout for the dialog
         val inflater = requireActivity().layoutInflater
         _binding = ChooseTemplateBinding.inflate(inflater, null, false)
-        val view: View = binding.root
 
         viewThemeUtils.material.colorTextInputLayout(
             binding.filenameContainer
@@ -137,13 +136,9 @@ class ChooseTemplateDialogFragment : DialogFragment(), View.OnClickListener, Tem
 
         binding.filename.addTextChangedListener(object : TextWatcher {
             override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit
-
-            override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
-                // not needed
-            }
-
+            override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) = Unit
             override fun afterTextChanged(s: Editable) {
-                checkEnablingCreateButton()
+                checkFileNameAfterEachType()
             }
         })
 
@@ -152,7 +147,7 @@ class ChooseTemplateDialogFragment : DialogFragment(), View.OnClickListener, Tem
         binding.list.setHasFixedSize(true)
         binding.list.layoutManager = GridLayoutManager(activity, 2)
         adapter = TemplateAdapter(
-            creator!!.mimetype,
+            creator?.mimetype,
             this,
             context,
             currentAccount,
@@ -161,9 +156,8 @@ class ChooseTemplateDialogFragment : DialogFragment(), View.OnClickListener, Tem
         )
         binding.list.adapter = adapter
 
-        // Build the dialog
         val builder = MaterialAlertDialogBuilder(activity)
-        builder.setView(view)
+            .setView(binding.root)
             .setPositiveButton(R.string.create, null)
             .setNegativeButton(R.string.common_cancel, null)
             .setTitle(title)
@@ -173,7 +167,7 @@ class ChooseTemplateDialogFragment : DialogFragment(), View.OnClickListener, Tem
         return builder.create()
     }
 
-    @Suppress("TooGenericExceptionCaught") // legacy code
+    @Suppress("TooGenericExceptionCaught")
     private fun fetchTemplate() {
         try {
             val user = currentAccount.user
@@ -197,6 +191,7 @@ class ChooseTemplateDialogFragment : DialogFragment(), View.OnClickListener, Tem
         CreateFileFromTemplateTask(this, clientFactory, currentAccount.user, template, path, creator).execute()
     }
 
+    @SuppressLint("NotifyDataSetChanged")
     fun setTemplateList(templateList: TemplateList?) {
         adapter?.setTemplateList(templateList)
         adapter?.notifyDataSetChanged()
@@ -207,9 +202,9 @@ class ChooseTemplateDialogFragment : DialogFragment(), View.OnClickListener, Tem
     }
 
     private fun onTemplateChosen(template: Template) {
-        adapter!!.setTemplateAsActive(template)
+        adapter?.setTemplateAsActive(template)
         prefillFilenameIfEmpty(template)
-        checkEnablingCreateButton()
+        checkFileNameAfterEachType()
     }
 
     private fun prefillFilenameIfEmpty(template: Template) {
@@ -222,73 +217,92 @@ class ChooseTemplateDialogFragment : DialogFragment(), View.OnClickListener, Tem
         }
     }
 
+    private fun getOCCapability(): OCCapability = fileDataStorageManager.getCapability(currentAccount.user.accountName)
+
     override fun onClick(v: View) {
         val name = binding.filename.text.toString()
-        val path = parentFolder!!.remotePath + name
-
-        val selectedTemplate = adapter!!.selectedTemplate
-
-        if (selectedTemplate == null) {
-            DisplayUtils.showSnackMessage(binding.list, R.string.select_one_template)
-        } else if (name.isEmpty() || name.equals(DOT + selectedTemplate.extension, ignoreCase = true)) {
-            DisplayUtils.showSnackMessage(binding.list, R.string.enter_filename)
-        } else if (!name.endsWith(selectedTemplate.extension)) {
-            createFromTemplate(selectedTemplate, path + DOT + selectedTemplate.extension)
-        } else {
-            createFromTemplate(selectedTemplate, path)
-        }
-    }
+        val path = parentFolder?.remotePath + name
+        val selectedTemplate = adapter?.selectedTemplate
+
+        val errorMessage = FileNameValidator.checkFileName(name, getOCCapability(), requireContext())
 
-    private fun checkEnablingCreateButton() {
-        if (positiveButton != null) {
-            val selectedTemplate = adapter!!.selectedTemplate
-            val name = binding.filename.text.toString().trim()
-            val isNameJustExtension = selectedTemplate != null && name.equals(
-                DOT + selectedTemplate.extension,
-                ignoreCase = true
-            )
-            val isNameEmpty = name.isEmpty() || isNameJustExtension
-            val state = selectedTemplate != null && !isNameEmpty && !fileNames.contains(name)
-
-            positiveButton?.isEnabled = state
-            positiveButton?.isClickable = state
-            binding.filenameContainer.isErrorEnabled = !state
-
-            if (!state) {
-                if (isNameEmpty) {
-                    binding.filenameContainer.error = getText(R.string.filename_empty)
+        when {
+            selectedTemplate == null -> {
+                DisplayUtils.showSnackMessage(binding.list, R.string.select_one_template)
+            }
+            errorMessage != null -> {
+                DisplayUtils.showSnackMessage(requireActivity(), errorMessage)
+            }
+            name.equals(DOT + selectedTemplate.extension, ignoreCase = true) -> {
+                DisplayUtils.showSnackMessage(binding.list, R.string.enter_filename)
+            }
+            else -> {
+                val fullPath = if (!name.endsWith(selectedTemplate.extension)) {
+                    path + DOT + selectedTemplate.extension
                 } else {
-                    binding.filenameContainer.error = getText(R.string.file_already_exists)
+                    path
                 }
+                createFromTemplate(selectedTemplate, fullPath)
             }
         }
     }
 
-    @Suppress("LongParameterList") // legacy code
+    private fun checkFileNameAfterEachType() {
+        if (positiveButton == null) return
+
+        val selectedTemplate = adapter?.selectedTemplate
+        val name = binding.filename.text.toString().trim()
+        val isNameJustExtension = selectedTemplate != null && name.equals(
+            DOT + selectedTemplate.extension,
+            ignoreCase = true
+        )
+        val fileNameValidatorResult = FileNameValidator.checkFileName(name, getOCCapability(), requireContext())
+
+        val errorMessage = when {
+            isNameJustExtension -> null
+            fileNameValidatorResult != null -> fileNameValidatorResult
+            else -> null
+        }
+
+        val isNameValid = (errorMessage == null) && !name.equals(DOT + selectedTemplate?.extension, ignoreCase = true)
+        val isHiddenFileName = FileNameValidator.isFileHidden(name)
+
+        binding.filenameContainer.isErrorEnabled = !isNameValid || isHiddenFileName
+        binding.filenameContainer.error = when {
+            !isNameValid -> errorMessage ?: getString(R.string.enter_filename)
+            isHiddenFileName -> getText(R.string.hidden_file_name_warning)
+            else -> null
+        }
+
+        positiveButton?.apply {
+            isEnabled = isNameValid && !isHiddenFileName
+            isClickable = isEnabled
+        }
+    }
+
+    @Suppress("LongParameterList", "DEPRECATION")
     private class CreateFileFromTemplateTask(
         chooseTemplateDialogFragment: ChooseTemplateDialogFragment,
         private val clientFactory: ClientFactory?,
-        user: User,
-        template: Template,
-        path: String,
-        creator: Creator?
-    ) : AsyncTask<Void, Void, String>() {
-        private val chooseTemplateDialogFragmentWeakReference: WeakReference<ChooseTemplateDialogFragment>
-        private val template: Template
-        private val path: String
+        private val user: User,
+        private val template: Template,
+        private val path: String,
         private val creator: Creator?
-        private val user: User
+    ) : AsyncTask<Void, Void, String>() {
+        private val chooseTemplateDialogFragmentWeakReference: WeakReference<ChooseTemplateDialogFragment> =
+            WeakReference(chooseTemplateDialogFragment)
         private var file: OCFile? = null
 
+        @Deprecated("Deprecated in Java")
         @Suppress("ReturnCount") // legacy code
         override fun doInBackground(vararg params: Void): String {
             return try {
-                val client = clientFactory!!.create(user)
+                val client = clientFactory?.create(user) ?: return ""
                 val nextcloudClient = clientFactory.createNextcloudClient(user)
                 val result = DirectEditingCreateFileRemoteOperation(
                     path,
-                    creator!!.editor,
-                    creator.id,
+                    creator?.editor,
+                    creator?.id,
                     template.title
                 ).execute(nextcloudClient)
                 if (!result.isSuccess) {
@@ -316,51 +330,51 @@ class ChooseTemplateDialogFragment : DialogFragment(), View.OnClickListener, Tem
             }
         }
 
+        @Deprecated("Deprecated in Java")
         override fun onPostExecute(url: String) {
             val fragment = chooseTemplateDialogFragmentWeakReference.get()
-            if (fragment != null && fragment.isAdded) {
-                if (url.isEmpty()) {
-                    DisplayUtils.showSnackMessage(fragment.binding.list, R.string.error_creating_file_from_template)
-                } else {
-                    val editorWebView = Intent(MainApp.getAppContext(), TextEditorWebView::class.java)
-                    editorWebView.putExtra(ExternalSiteWebView.EXTRA_TITLE, "Text")
-                    editorWebView.putExtra(ExternalSiteWebView.EXTRA_URL, url)
-                    editorWebView.putExtra(ExternalSiteWebView.EXTRA_FILE, file)
-                    editorWebView.putExtra(ExternalSiteWebView.EXTRA_SHOW_SIDEBAR, false)
-                    fragment.startActivity(editorWebView)
-                    fragment.dismiss()
-                }
-            } else {
+            if (fragment == null || !fragment.isAdded) {
                 Log_OC.e(TAG, "Error creating file from template!")
+                return
             }
-        }
 
-        init {
-            chooseTemplateDialogFragmentWeakReference = WeakReference(chooseTemplateDialogFragment)
-            this.template = template
-            this.path = path
-            this.creator = creator
-            this.user = user
+            if (url.isEmpty()) {
+                DisplayUtils.showSnackMessage(fragment.binding.list, R.string.error_creating_file_from_template)
+                return
+            }
+
+            val editorWebView = Intent(MainApp.getAppContext(), TextEditorWebView::class.java).apply {
+                putExtra(ExternalSiteWebView.EXTRA_TITLE, "Text")
+                putExtra(ExternalSiteWebView.EXTRA_URL, url)
+                putExtra(ExternalSiteWebView.EXTRA_FILE, file)
+                putExtra(ExternalSiteWebView.EXTRA_SHOW_SIDEBAR, false)
+            }
+
+            fragment.run {
+                startActivity(editorWebView)
+                dismiss()
+            }
         }
     }
 
+    @Suppress("DEPRECATION")
     private class FetchTemplateTask(
         chooseTemplateDialogFragment: ChooseTemplateDialogFragment,
         private val clientFactory: ClientFactory?,
         private val user: User,
-        creator: Creator?
-    ) : AsyncTask<Void, Void, TemplateList>() {
-        private val chooseTemplateDialogFragmentWeakReference: WeakReference<ChooseTemplateDialogFragment>
         private val creator: Creator?
+    ) : AsyncTask<Void, Void, TemplateList>() {
+        private val chooseTemplateDialogFragmentWeakReference: WeakReference<ChooseTemplateDialogFragment> =
+            WeakReference(chooseTemplateDialogFragment)
 
+        @Deprecated("Deprecated in Java")
         override fun doInBackground(vararg voids: Void): TemplateList {
             return try {
-                val client = clientFactory!!.createNextcloudClient(user)
+                val client = clientFactory?.createNextcloudClient(user) ?: return TemplateList()
                 val result = DirectEditingObtainListOfTemplatesRemoteOperation(
-                    creator!!.editor,
-                    creator.id
-                )
-                    .execute(client)
+                    creator?.editor,
+                    creator?.id
+                ).execute(client)
                 if (!result.isSuccess) {
                     TemplateList()
                 } else {
@@ -372,30 +386,31 @@ class ChooseTemplateDialogFragment : DialogFragment(), View.OnClickListener, Tem
             }
         }
 
+        @Deprecated("Deprecated in Java")
         override fun onPostExecute(templateList: TemplateList) {
             val fragment = chooseTemplateDialogFragmentWeakReference.get()
-            if (fragment != null && fragment.isAdded) {
-                if (templateList.templates.isEmpty()) {
-                    DisplayUtils.showSnackMessage(fragment.binding.list, R.string.error_retrieving_templates)
-                } else {
-                    if (templateList.templates.size == SINGLE_TEMPLATE) {
-                        fragment.onTemplateChosen(templateList.templates.values.iterator().next())
-                        fragment.binding.list.visibility = View.GONE
-                    } else {
-                        val name = DOT + templateList.templates.values.iterator().next().extension
-                        fragment.binding.filename.setText(name)
-                        fragment.binding.helperText.visibility = View.VISIBLE
-                    }
-                    fragment.setTemplateList(templateList)
-                }
-            } else {
+            if (fragment == null || !fragment.isAdded) {
                 Log_OC.e(TAG, "Error streaming file: no previewMediaFragment!")
+                return
             }
-        }
 
-        init {
-            chooseTemplateDialogFragmentWeakReference = WeakReference(chooseTemplateDialogFragment)
-            this.creator = creator
+            if (templateList.templates.isEmpty()) {
+                DisplayUtils.showSnackMessage(fragment.binding.list, R.string.error_retrieving_templates)
+                return
+            }
+
+            fragment.run {
+                if (templateList.templates.size == SINGLE_TEMPLATE) {
+                    onTemplateChosen(templateList.templates.values.iterator().next())
+                    binding.list.visibility = View.GONE
+                } else {
+                    val name = DOT + templateList.templates.values.iterator().next().extension
+                    binding.filename.setText(name)
+                    binding.helperText.visibility = View.VISIBLE
+                }
+
+                setTemplateList(templateList)
+            }
         }
     }
 
@@ -409,13 +424,15 @@ class ChooseTemplateDialogFragment : DialogFragment(), View.OnClickListener, Tem
 
         @JvmStatic
         fun newInstance(parentFolder: OCFile?, creator: Creator?, headline: String?): ChooseTemplateDialogFragment {
-            val frag = ChooseTemplateDialogFragment()
-            val args = Bundle()
-            args.putParcelable(ARG_PARENT_FOLDER, parentFolder)
-            args.putParcelable(ARG_CREATOR, creator)
-            args.putString(ARG_HEADLINE, headline)
-            frag.arguments = args
-            return frag
+            val bundle = Bundle().apply {
+                putParcelable(ARG_PARENT_FOLDER, parentFolder)
+                putParcelable(ARG_CREATOR, creator)
+                putString(ARG_HEADLINE, headline)
+            }
+
+            return ChooseTemplateDialogFragment().apply {
+                arguments = bundle
+            }
         }
     }
 }

+ 76 - 67
app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.kt

@@ -14,7 +14,6 @@ import android.app.Dialog
 import android.content.DialogInterface
 import android.os.Bundle
 import android.text.Editable
-import android.text.TextUtils
 import android.text.TextWatcher
 import android.view.View
 import android.widget.TextView
@@ -23,13 +22,15 @@ import androidx.fragment.app.DialogFragment
 import com.google.android.material.button.MaterialButton
 import com.google.android.material.dialog.MaterialAlertDialogBuilder
 import com.google.common.collect.Sets
+import com.nextcloud.client.account.CurrentAccountProvider
 import com.nextcloud.client.di.Injectable
 import com.nextcloud.utils.extensions.getParcelableArgument
+import com.nextcloud.utils.fileNameValidator.FileNameValidator
 import com.owncloud.android.R
 import com.owncloud.android.databinding.EditBoxDialogBinding
 import com.owncloud.android.datamodel.FileDataStorageManager
 import com.owncloud.android.datamodel.OCFile
-import com.owncloud.android.lib.resources.files.FileUtils
+import com.owncloud.android.lib.resources.status.OCCapability
 import com.owncloud.android.ui.activity.ComponentsGetter
 import com.owncloud.android.utils.DisplayUtils
 import com.owncloud.android.utils.KeyboardUtils
@@ -43,17 +44,19 @@ import javax.inject.Inject
  * Triggers the folder creation when name is confirmed.
  */
 class CreateFolderDialogFragment : DialogFragment(), DialogInterface.OnClickListener, Injectable {
-    @JvmField
+
+    @Inject
+    lateinit var fileDataStorageManager: FileDataStorageManager
+
     @Inject
-    var fileDataStorageManager: FileDataStorageManager? = null
+    lateinit var viewThemeUtils: ViewThemeUtils
 
-    @JvmField
     @Inject
-    var viewThemeUtils: ViewThemeUtils? = null
+    lateinit var keyboardUtils: KeyboardUtils
 
-    @JvmField
     @Inject
-    var keyboardUtils: KeyboardUtils? = null
+    lateinit var currentAccount: CurrentAccountProvider
+
     private var mParentFolder: OCFile? = null
     private var positiveButton: MaterialButton? = null
 
@@ -68,108 +71,112 @@ class CreateFolderDialogFragment : DialogFragment(), DialogInterface.OnClickList
         val dialog = dialog
 
         if (dialog is AlertDialog) {
-            positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE) as MaterialButton
-            val negativeButton = dialog.getButton(AlertDialog.BUTTON_NEGATIVE) as MaterialButton
+            positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE) as? MaterialButton
+            positiveButton?.let {
+                it.isEnabled = false
+                viewThemeUtils.material.colorMaterialButtonPrimaryTonal(it)
+            }
 
-            viewThemeUtils?.material?.colorMaterialButtonPrimaryTonal(positiveButton!!)
-            viewThemeUtils?.material?.colorMaterialButtonPrimaryBorderless(negativeButton)
+            val negativeButton = dialog.getButton(AlertDialog.BUTTON_NEGATIVE) as? MaterialButton
+            negativeButton?.let {
+                viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(it)
+            }
         }
     }
 
     override fun onResume() {
         super.onResume()
         bindButton()
-        keyboardUtils!!.showKeyboardForEditText(requireDialog().window, binding.userInput)
+        keyboardUtils.showKeyboardForEditText(requireDialog().window, binding.userInput)
     }
 
     @Suppress("EmptyFunctionBlock")
     override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
         mParentFolder = arguments?.getParcelableArgument(ARG_PARENT_FOLDER, OCFile::class.java)
 
-        // Inflate the layout for the dialog
         val inflater = requireActivity().layoutInflater
         binding = EditBoxDialogBinding.inflate(inflater, null, false)
-        val view: View = binding.root
 
-        // Setup layout
         binding.userInput.setText(R.string.empty)
-        viewThemeUtils?.material?.colorTextInputLayout(binding.userInputContainer)
+        viewThemeUtils.material.colorTextInputLayout(binding.userInputContainer)
 
         val parentFolder = requireArguments().getParcelableArgument(ARG_PARENT_FOLDER, OCFile::class.java)
 
-        val folderContent = fileDataStorageManager!!.getFolderContent(parentFolder, false)
+        val folderContent = fileDataStorageManager.getFolderContent(parentFolder, false)
         val fileNames: MutableSet<String> = Sets.newHashSetWithExpectedSize(folderContent.size)
         for (file in folderContent) {
             fileNames.add(file.fileName)
         }
 
-        // Add TextChangedListener to handle showing/hiding the input warning message
         binding.userInput.addTextChangedListener(object : TextWatcher {
             override fun afterTextChanged(s: Editable) {}
             override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
-
-            /**
-             * When user enters a hidden file name, the 'hidden file' message is shown. Otherwise,
-             * the message is ensured to be hidden.
-             */
             override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
-                var newFileName = ""
-                if (binding.userInput.text != null) {
-                    newFileName = binding.userInput.text.toString().trim { it <= ' ' }
-                }
-                if (!TextUtils.isEmpty(newFileName) && newFileName[0] == '.') {
-                    binding.userInputContainer.error = getText(R.string.hidden_file_name_warning)
-                } else if (TextUtils.isEmpty(newFileName)) {
-                    binding.userInputContainer.error = getString(R.string.filename_empty)
-                    if (positiveButton == null) {
-                        bindButton()
-                    }
-                    positiveButton!!.isEnabled = false
-                } else if (!FileUtils.isValidName(newFileName)) {
-                    binding.userInputContainer.error = getString(R.string.filename_forbidden_charaters_from_server)
-                    positiveButton!!.isEnabled = false
-                } else if (fileNames.contains(newFileName)) {
-                    binding.userInputContainer.error = getText(R.string.file_already_exists)
-                    positiveButton!!.isEnabled = false
-                } else if (binding.userInputContainer.error != null) {
-                    binding.userInputContainer.error = null
-                    // Called to remove extra padding
-                    binding.userInputContainer.isErrorEnabled = false
-                    positiveButton!!.isEnabled = true
-                }
+                checkFileNameAfterEachType(fileNames)
             }
         })
 
-        // Build the dialog
-        val builder = buildMaterialAlertDialog(view)
-        viewThemeUtils?.dialog?.colorMaterialAlertDialogBackground(binding.userInputContainer.context, builder)
+        val builder = buildMaterialAlertDialog(binding.root)
+        viewThemeUtils.dialog.colorMaterialAlertDialogBackground(binding.userInputContainer.context, builder)
         return builder.create()
     }
 
+    private fun getOCCapability(): OCCapability = fileDataStorageManager.getCapability(currentAccount.user.accountName)
+
+    private fun checkFileNameAfterEachType(fileNames: MutableSet<String>) {
+        val newFileName = binding.userInput.text?.toString()?.trim() ?: ""
+
+        val fileNameValidatorResult: String? =
+            FileNameValidator.checkFileName(newFileName, getOCCapability(), requireContext(), fileNames)
+
+        val errorMessage = when {
+            newFileName.isEmpty() -> getString(R.string.folder_name_empty)
+            fileNameValidatorResult != null -> fileNameValidatorResult
+            else -> null
+        }
+
+        if (errorMessage != null) {
+            binding.userInputContainer.error = errorMessage
+            positiveButton?.isEnabled = false
+            if (positiveButton == null) {
+                bindButton()
+            }
+        } else if (FileNameValidator.isFileHidden(newFileName)) {
+            binding.userInputContainer.error = requireContext().getString(R.string.hidden_file_name_warning)
+            binding.userInputContainer.isErrorEnabled = true
+            positiveButton?.isEnabled = true
+        } else {
+            binding.userInputContainer.error = null
+            binding.userInputContainer.isErrorEnabled = false
+            positiveButton?.isEnabled = true
+        }
+    }
+
     private fun buildMaterialAlertDialog(view: View): MaterialAlertDialogBuilder {
-        val builder = MaterialAlertDialogBuilder(requireActivity())
-        builder
+        return MaterialAlertDialogBuilder(requireActivity())
             .setView(view)
             .setPositiveButton(R.string.folder_confirm_create, this)
             .setNegativeButton(R.string.common_cancel, this)
             .setTitle(R.string.uploader_info_dirname)
-        return builder
     }
 
     override fun onClick(dialog: DialogInterface, which: Int) {
         if (which == AlertDialog.BUTTON_POSITIVE) {
-            val newFolderName = (getDialog()!!.findViewById<View>(R.id.user_input) as TextView)
+            val newFolderName = (getDialog()?.findViewById<View>(R.id.user_input) as TextView)
                 .text.toString().trim { it <= ' ' }
-            if (TextUtils.isEmpty(newFolderName)) {
-                DisplayUtils.showSnackMessage(requireActivity(), R.string.filename_empty)
+
+            val errorMessage: String? =
+                FileNameValidator.checkFileName(newFolderName, getOCCapability(), requireContext())
+
+            if (errorMessage != null) {
+                DisplayUtils.showSnackMessage(requireActivity(), errorMessage)
                 return
             }
-            if (!FileUtils.isValidName(newFolderName)) {
-                DisplayUtils.showSnackMessage(requireActivity(), R.string.filename_forbidden_charaters_from_server)
-                return
+
+            val path = mParentFolder?.decryptedRemotePath + newFolderName + OCFile.PATH_SEPARATOR
+            if (requireActivity() is ComponentsGetter) {
+                (requireActivity() as ComponentsGetter).fileOperationsHelper.createFolder(path)
             }
-            val path = mParentFolder!!.decryptedRemotePath + newFolderName + OCFile.PATH_SEPARATOR
-            (requireActivity() as ComponentsGetter).fileOperationsHelper.createFolder(path)
         }
     }
 
@@ -185,11 +192,13 @@ class CreateFolderDialogFragment : DialogFragment(), DialogInterface.OnClickList
          */
         @JvmStatic
         fun newInstance(parentFolder: OCFile?): CreateFolderDialogFragment {
-            val frag = CreateFolderDialogFragment()
-            val args = Bundle()
-            args.putParcelable(ARG_PARENT_FOLDER, parentFolder)
-            frag.arguments = args
-            return frag
+            val bundle = Bundle().apply {
+                putParcelable(ARG_PARENT_FOLDER, parentFolder)
+            }
+
+            return CreateFolderDialogFragment().apply {
+                arguments = bundle
+            }
         }
     }
 }

+ 0 - 219
app/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.java

@@ -1,219 +0,0 @@
-/*
- * Nextcloud - Android Client
- *
- * SPDX-FileCopyrightText: 2023 Alper Ozturk <alper.ozturk@nextcloud.com>
- * SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de>
- * SPDX-FileCopyrightText: 2017-2022 Tobias Kaminsky <tobias@kaminsky.me>
- * SPDX-FileCopyrightText: 2014 ownCloud Inc.
- * SPDX-FileCopyrightText: 2014 David A. Velasco <dvelasco@solidgear.es>
- * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only)
- */
-package com.owncloud.android.ui.dialog;
-
-import android.app.Dialog;
-import android.content.DialogInterface;
-import android.os.Bundle;
-import android.text.Editable;
-import android.text.TextUtils;
-import android.text.TextWatcher;
-import android.view.LayoutInflater;
-import android.view.View;
-
-import com.google.android.material.button.MaterialButton;
-import com.google.android.material.dialog.MaterialAlertDialogBuilder;
-import com.google.common.collect.Sets;
-import com.nextcloud.client.di.Injectable;
-import com.nextcloud.utils.extensions.BundleExtensionsKt;
-import com.owncloud.android.R;
-import com.owncloud.android.databinding.EditBoxDialogBinding;
-import com.owncloud.android.datamodel.FileDataStorageManager;
-import com.owncloud.android.datamodel.OCFile;
-import com.owncloud.android.lib.resources.files.FileUtils;
-import com.owncloud.android.ui.activity.ComponentsGetter;
-import com.owncloud.android.utils.DisplayUtils;
-import com.owncloud.android.utils.KeyboardUtils;
-import com.owncloud.android.utils.theme.ViewThemeUtils;
-
-import java.util.List;
-import java.util.Set;
-
-import javax.inject.Inject;
-
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AlertDialog;
-import androidx.fragment.app.DialogFragment;
-
-/**
- *  Dialog to input a new name for an {@link OCFile} being renamed.
- *  Triggers the rename operation.
- */
-public class RenameFileDialogFragment
-    extends DialogFragment implements DialogInterface.OnClickListener, TextWatcher, Injectable {
-
-    private static final String ARG_TARGET_FILE = "TARGET_FILE";
-    private static final String ARG_PARENT_FOLDER = "PARENT_FOLDER";
-
-    @Inject ViewThemeUtils viewThemeUtils;
-    @Inject FileDataStorageManager fileDataStorageManager;
-    @Inject KeyboardUtils keyboardUtils;
-
-    private EditBoxDialogBinding binding;
-    private OCFile mTargetFile;
-    private MaterialButton positiveButton;
-    private Set<String> fileNames;
-
-    /**
-     * Public factory method to create new RenameFileDialogFragment instances.
-     *
-     * @param file File to rename.
-     * @return Dialog ready to show.
-     */
-    public static RenameFileDialogFragment newInstance(OCFile file, OCFile parentFolder) {
-        RenameFileDialogFragment frag = new RenameFileDialogFragment();
-        Bundle args = new Bundle();
-        args.putParcelable(ARG_TARGET_FILE, file);
-        args.putParcelable(ARG_PARENT_FOLDER, parentFolder);
-        frag.setArguments(args);
-        return frag;
-    }
-
-    @Override
-    public void onStart() {
-        super.onStart();
-        initAlertDialog();
-    }
-
-    @Override
-    public void onResume() {
-        super.onResume();
-        keyboardUtils.showKeyboardForEditText(requireDialog().getWindow(), binding.userInput);
-    }
-
-    @NonNull
-    @Override
-    public Dialog onCreateDialog(Bundle savedInstanceState) {
-        mTargetFile = BundleExtensionsKt.getParcelableArgument(requireArguments(), ARG_TARGET_FILE, OCFile.class);
-
-        // Inflate the layout for the dialog
-        LayoutInflater inflater = requireActivity().getLayoutInflater();
-        binding = EditBoxDialogBinding.inflate(inflater, null, false);
-        View view = binding.getRoot();
-
-        // Setup layout
-        String currentName = mTargetFile.getFileName();
-        binding.userInput.setText(currentName);
-        viewThemeUtils.material.colorTextInputLayout(binding.userInputContainer);
-        int extensionStart = mTargetFile.isFolder() ? -1 : currentName.lastIndexOf('.');
-        int selectionEnd = extensionStart >= 0 ? extensionStart : currentName.length();
-        binding.userInput.setSelection(0, selectionEnd);
-
-        OCFile parentFolder = BundleExtensionsKt.getParcelableArgument(getArguments(), ARG_PARENT_FOLDER, OCFile.class);
-        List<OCFile> folderContent = fileDataStorageManager.getFolderContent(parentFolder, false);
-        fileNames = Sets.newHashSetWithExpectedSize(folderContent.size());
-
-        for (OCFile file : folderContent) {
-            fileNames.add(file.getFileName());
-        }
-
-        // Add TextChangedListener to handle showing/hiding the input warning message
-        binding.userInput.addTextChangedListener(this);
-
-        // Build the dialog
-        MaterialAlertDialogBuilder builder = buildMaterialAlertDialog(view);
-
-        viewThemeUtils.dialog.colorMaterialAlertDialogBackground(binding.userInputContainer.getContext(), builder);
-
-        return builder.create();
-    }
-
-    private MaterialAlertDialogBuilder buildMaterialAlertDialog(View view) {
-        MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity());
-
-        builder
-            .setView(view)
-            .setPositiveButton(R.string.file_rename, this)
-            .setNegativeButton(R.string.common_cancel, this)
-            .setTitle(R.string.rename_dialog_title);
-
-        return builder;
-    }
-
-    private void initAlertDialog() {
-        AlertDialog alertDialog = (AlertDialog) getDialog();
-
-        if (alertDialog != null) {
-            positiveButton = (MaterialButton) alertDialog.getButton(AlertDialog.BUTTON_POSITIVE);
-            MaterialButton negativeButton = (MaterialButton) alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE);
-
-            viewThemeUtils.material.colorMaterialButtonPrimaryTonal(positiveButton);
-            viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(negativeButton);
-        }
-    }
-
-    @Override
-    public void onClick(DialogInterface dialog, int which) {
-        if (which == AlertDialog.BUTTON_POSITIVE) {
-            String newFileName = "";
-
-            if (binding.userInput.getText() != null) {
-                newFileName = binding.userInput.getText().toString().trim();
-            }
-
-            if (TextUtils.isEmpty(newFileName)) {
-                DisplayUtils.showSnackMessage(requireActivity(), R.string.filename_empty);
-                return;
-            }
-
-            if (!FileUtils.isValidName(newFileName)) {
-                DisplayUtils.showSnackMessage(requireActivity(), R.string.filename_forbidden_charaters_from_server);
-
-                return;
-            }
-
-            ((ComponentsGetter) requireActivity()).getFileOperationsHelper().renameFile(mTargetFile, newFileName);
-        }
-    }
-
-    @Override
-    public void onDestroyView() {
-        super.onDestroyView();
-        binding = null;
-    }
-
-    @Override
-    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
-
-    }
-
-    /**
-     * When user enters a hidden file name, the 'hidden file' message is shown.
-     * Otherwise, the message is ensured to be hidden.
-     */
-    @Override
-    public void onTextChanged(CharSequence s, int start, int before, int count) {
-        String newFileName = "";
-        if (binding.userInput.getText() != null) {
-            newFileName = binding.userInput.getText().toString().trim();
-        }
-
-        if (!TextUtils.isEmpty(newFileName) && newFileName.charAt(0) == '.') {
-            binding.userInputContainer.setError(getText(R.string.hidden_file_name_warning));
-        } else if (TextUtils.isEmpty(newFileName)) {
-            binding.userInputContainer.setError(getString(R.string.filename_empty));
-            positiveButton.setEnabled(false);
-        } else if (fileNames.contains(newFileName)) {
-            binding.userInputContainer.setError(getText(R.string.file_already_exists));
-            positiveButton.setEnabled(false);
-        } else if (binding.userInputContainer.getError() != null) {
-            binding.userInputContainer.setError(null);
-            // Called to remove extra padding
-            binding.userInputContainer.setErrorEnabled(false);
-            positiveButton.setEnabled(true);
-        }
-    }
-
-    @Override
-    public void afterTextChanged(Editable s) {
-
-    }
-}

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

@@ -0,0 +1,205 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2023 Alper Ozturk <alper.ozturk@nextcloud.com>
+ * SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de>
+ * SPDX-FileCopyrightText: 2017-2022 Tobias Kaminsky <tobias@kaminsky.me>
+ * SPDX-FileCopyrightText: 2014 ownCloud Inc.
+ * SPDX-FileCopyrightText: 2014 David A. Velasco <dvelasco@solidgear.es>
+ * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only)
+ */
+package com.owncloud.android.ui.dialog
+
+import android.app.Dialog
+import android.content.DialogInterface
+import android.os.Bundle
+import android.text.Editable
+import android.text.TextWatcher
+import android.view.View
+import androidx.appcompat.app.AlertDialog
+import androidx.fragment.app.DialogFragment
+import com.google.android.material.button.MaterialButton
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.google.common.collect.Sets
+import com.nextcloud.client.account.CurrentAccountProvider
+import com.nextcloud.client.di.Injectable
+import com.nextcloud.utils.extensions.getParcelableArgument
+import com.nextcloud.utils.fileNameValidator.FileNameValidator.isFileHidden
+import com.nextcloud.utils.fileNameValidator.FileNameValidator.checkFileName
+import com.owncloud.android.R
+import com.owncloud.android.databinding.EditBoxDialogBinding
+import com.owncloud.android.datamodel.FileDataStorageManager
+import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.lib.resources.status.OCCapability
+import com.owncloud.android.ui.activity.ComponentsGetter
+import com.owncloud.android.utils.DisplayUtils
+import com.owncloud.android.utils.KeyboardUtils
+import com.owncloud.android.utils.theme.ViewThemeUtils
+import javax.inject.Inject
+
+/**
+ * Dialog to input a new name for an [OCFile] being renamed.
+ * Triggers the rename operation.
+ */
+class RenameFileDialogFragment : DialogFragment(), DialogInterface.OnClickListener, TextWatcher, Injectable {
+    @Inject
+    lateinit var viewThemeUtils: ViewThemeUtils
+
+    @Inject
+    lateinit var fileDataStorageManager: FileDataStorageManager
+
+    @Inject
+    lateinit var keyboardUtils: KeyboardUtils
+
+    @Inject
+    lateinit var currentAccount: CurrentAccountProvider
+
+    private lateinit var binding: EditBoxDialogBinding
+    private var mTargetFile: OCFile? = null
+    private var positiveButton: MaterialButton? = null
+    private var fileNames: MutableSet<String>? = null
+
+    override fun onStart() {
+        super.onStart()
+        initAlertDialog()
+    }
+
+    override fun onResume() {
+        super.onResume()
+        keyboardUtils.showKeyboardForEditText(requireDialog().window, binding.userInput)
+    }
+
+    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+        mTargetFile = requireArguments().getParcelableArgument(ARG_TARGET_FILE, OCFile::class.java)
+
+        val inflater = requireActivity().layoutInflater
+        binding = EditBoxDialogBinding.inflate(inflater, null, false)
+
+        val currentName = mTargetFile?.fileName
+        binding.userInput.setText(currentName)
+        viewThemeUtils.material.colorTextInputLayout(binding.userInputContainer)
+        val extensionStart = if (mTargetFile?.isFolder == true) -1 else currentName?.lastIndexOf('.')
+        val selectionEnd = if ((extensionStart ?: -1) >= 0) extensionStart else currentName?.length
+        if (selectionEnd != null) {
+            binding.userInput.setSelection(0, selectionEnd)
+        }
+
+        val parentFolder = arguments.getParcelableArgument(ARG_PARENT_FOLDER, OCFile::class.java)
+        val folderContent = fileDataStorageManager.getFolderContent(parentFolder, false)
+        fileNames = Sets.newHashSetWithExpectedSize(folderContent.size)
+
+        for (file in folderContent) {
+            fileNames?.add(file.fileName)
+        }
+
+        binding.userInput.addTextChangedListener(this)
+
+        val builder = buildMaterialAlertDialog(binding.root)
+
+        viewThemeUtils.dialog.colorMaterialAlertDialogBackground(binding.userInputContainer.context, builder)
+
+        return builder.create()
+    }
+
+    private fun buildMaterialAlertDialog(view: View): MaterialAlertDialogBuilder {
+        val builder = MaterialAlertDialogBuilder(requireActivity())
+
+        builder
+            .setView(view)
+            .setPositiveButton(R.string.file_rename, this)
+            .setNegativeButton(R.string.common_cancel, this)
+            .setTitle(R.string.rename_dialog_title)
+
+        return builder
+    }
+
+    private fun initAlertDialog() {
+        val alertDialog = dialog as AlertDialog?
+
+        if (alertDialog != null) {
+            positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE) as MaterialButton
+            val negativeButton = alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE) as MaterialButton
+
+            positiveButton?.let {
+                viewThemeUtils.material.colorMaterialButtonPrimaryTonal(it)
+            }
+            viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(negativeButton)
+        }
+    }
+
+    private val oCCapability: OCCapability
+        get() = fileDataStorageManager.getCapability(currentAccount.user.accountName)
+
+    override fun onClick(dialog: DialogInterface, which: Int) {
+        if (which == AlertDialog.BUTTON_POSITIVE) {
+            var newFileName = ""
+
+            if (binding.userInput.text != null) {
+                newFileName = binding.userInput.text.toString().trim { it <= ' ' }
+            }
+
+            val errorMessage = checkFileName(newFileName, oCCapability, requireContext(), null)
+            if (errorMessage != null) {
+                DisplayUtils.showSnackMessage(requireActivity(), errorMessage)
+                return
+            }
+
+            if (requireActivity() is ComponentsGetter) {
+                val componentsGetter = requireActivity() as ComponentsGetter
+                componentsGetter.getFileOperationsHelper().renameFile(mTargetFile, newFileName)
+            }
+        }
+    }
+
+    override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit
+
+    /**
+     * When user enters a hidden file name, the 'hidden file' message is shown.
+     * Otherwise, the message is ensured to be hidden.
+     */
+    override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
+        var newFileName = ""
+        if (binding.userInput.text != null) {
+            newFileName = binding.userInput.text.toString().trim { it <= ' ' }
+        }
+
+        val errorMessage = checkFileName(newFileName, oCCapability, requireContext(), fileNames)
+
+        if (isFileHidden(newFileName)) {
+            binding.userInputContainer.error = getText(R.string.hidden_file_name_warning)
+        } else if (errorMessage != null) {
+            binding.userInputContainer.error = errorMessage
+            positiveButton?.isEnabled = false
+        } else if (binding.userInputContainer.error != null) {
+            binding.userInputContainer.error = null
+            // Called to remove extra padding
+            binding.userInputContainer.isErrorEnabled = false
+            positiveButton?.isEnabled = true
+        }
+    }
+
+    override fun afterTextChanged(s: Editable) = Unit
+
+    companion object {
+        private const val ARG_TARGET_FILE = "TARGET_FILE"
+        private const val ARG_PARENT_FOLDER = "PARENT_FOLDER"
+
+        /**
+         * Public factory method to create new RenameFileDialogFragment instances.
+         *
+         * @param file File to rename.
+         * @return Dialog ready to show.
+         */
+        @JvmStatic
+        fun newInstance(file: OCFile?, parentFolder: OCFile?): RenameFileDialogFragment {
+            val bundle = Bundle().apply {
+                putParcelable(ARG_TARGET_FILE, file)
+                putParcelable(ARG_PARENT_FOLDER, parentFolder)
+            }
+
+            return RenameFileDialogFragment().apply {
+                arguments = bundle
+            }
+        }
+    }
+}

+ 0 - 126
app/src/main/java/com/owncloud/android/ui/dialog/RenamePublicShareDialogFragment.java

@@ -1,126 +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 android.app.Dialog;
-import android.content.DialogInterface;
-import android.os.Bundle;
-import android.text.TextUtils;
-import android.view.LayoutInflater;
-import android.view.View;
-
-import com.google.android.material.button.MaterialButton;
-import com.google.android.material.dialog.MaterialAlertDialogBuilder;
-import com.nextcloud.client.di.Injectable;
-import com.nextcloud.utils.extensions.BundleExtensionsKt;
-import com.owncloud.android.R;
-import com.owncloud.android.databinding.EditBoxDialogBinding;
-import com.owncloud.android.lib.resources.shares.OCShare;
-import com.owncloud.android.ui.activity.ComponentsGetter;
-import com.owncloud.android.utils.DisplayUtils;
-import com.owncloud.android.utils.KeyboardUtils;
-import com.owncloud.android.utils.theme.ViewThemeUtils;
-
-import javax.inject.Inject;
-
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AlertDialog;
-import androidx.fragment.app.DialogFragment;
-
-/**
- * Dialog to rename a public share.
- */
-public class RenamePublicShareDialogFragment
-    extends DialogFragment implements DialogInterface.OnClickListener, Injectable {
-
-    private static final String ARG_PUBLIC_SHARE = "PUBLIC_SHARE";
-
-    @Inject ViewThemeUtils viewThemeUtils;
-    @Inject KeyboardUtils keyboardUtils;
-
-    private EditBoxDialogBinding binding;
-    private OCShare publicShare;
-
-    public static RenamePublicShareDialogFragment newInstance(OCShare share) {
-        RenamePublicShareDialogFragment frag = new RenamePublicShareDialogFragment();
-        Bundle args = new Bundle();
-        args.putParcelable(ARG_PUBLIC_SHARE, share);
-        frag.setArguments(args);
-        return frag;
-    }
-
-    @Override
-    public void onStart() {
-        super.onStart();
-
-        AlertDialog alertDialog = (AlertDialog) getDialog();
-
-        if (alertDialog != null) {
-            MaterialButton positiveButton = (MaterialButton) alertDialog.getButton(AlertDialog.BUTTON_POSITIVE);
-            MaterialButton negativeButton = (MaterialButton) alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE);
-            viewThemeUtils.material.colorMaterialButtonPrimaryTonal(positiveButton);
-            viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(negativeButton);
-        }
-    }
-
-    @Override
-    public void onResume() {
-        super.onResume();
-        keyboardUtils.showKeyboardForEditText(requireDialog().getWindow(), binding.userInput);
-    }
-
-    @NonNull
-    @Override
-    public Dialog onCreateDialog(Bundle savedInstanceState) {
-        publicShare = BundleExtensionsKt.getParcelableArgument(requireArguments(), ARG_PUBLIC_SHARE, OCShare.class);
-
-        // Inflate the layout for the dialog
-        LayoutInflater inflater = requireActivity().getLayoutInflater();
-        binding = EditBoxDialogBinding.inflate(inflater, null, false);
-        View view = binding.getRoot();
-
-        // Setup layout
-        viewThemeUtils.material.colorTextInputLayout(binding.userInputContainer);
-        binding.userInput.setText(publicShare.getLabel());
-
-        // Build the dialog
-        MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(view.getContext());
-        builder.setView(view)
-            .setPositiveButton(R.string.file_rename, this)
-            .setNegativeButton(R.string.common_cancel, this)
-            .setTitle(R.string.public_share_name);
-
-        viewThemeUtils.dialog.colorMaterialAlertDialogBackground(binding.userInput.getContext(), builder);
-
-        return builder.create();
-    }
-
-    @Override
-    public void onClick(DialogInterface dialog, int which) {
-        if (which == AlertDialog.BUTTON_POSITIVE) {
-            String newName = "";
-            if (binding.userInput.getText() != null) {
-                newName = binding.userInput.getText().toString().trim();
-            }
-
-            if (TextUtils.isEmpty(newName)) {
-                DisplayUtils.showSnackMessage(requireActivity(), R.string.label_empty);
-                return;
-            }
-
-            ((ComponentsGetter) requireActivity()).getFileOperationsHelper().setLabelToPublicShare(publicShare,
-                                                                                                   newName);
-        }
-    }
-
-    @Override
-    public void onDestroyView() {
-        super.onDestroyView();
-        binding = null;
-    }
-}

+ 120 - 0
app/src/main/java/com/owncloud/android/ui/dialog/RenamePublicShareDialogFragment.kt

@@ -0,0 +1,120 @@
+/*
+ * 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 android.app.Dialog
+import android.content.DialogInterface
+import android.os.Bundle
+import android.text.TextUtils
+import android.view.View
+import androidx.appcompat.app.AlertDialog
+import androidx.fragment.app.DialogFragment
+import com.google.android.material.button.MaterialButton
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.nextcloud.client.di.Injectable
+import com.nextcloud.utils.extensions.getParcelableArgument
+import com.owncloud.android.R
+import com.owncloud.android.databinding.EditBoxDialogBinding
+import com.owncloud.android.lib.resources.shares.OCShare
+import com.owncloud.android.ui.activity.ComponentsGetter
+import com.owncloud.android.utils.DisplayUtils
+import com.owncloud.android.utils.KeyboardUtils
+import com.owncloud.android.utils.theme.ViewThemeUtils
+import javax.inject.Inject
+
+/**
+ * Dialog to rename a public share.
+ */
+class RenamePublicShareDialogFragment : DialogFragment(), DialogInterface.OnClickListener, Injectable {
+    @Inject
+    lateinit var viewThemeUtils: ViewThemeUtils
+
+    @Inject
+    lateinit var keyboardUtils: KeyboardUtils
+
+    private lateinit var binding: EditBoxDialogBinding
+    private var publicShare: OCShare? = null
+
+    override fun onStart() {
+        super.onStart()
+
+        val alertDialog = dialog as AlertDialog? ?: return
+
+        val positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE) as? MaterialButton
+        val negativeButton = alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE) as? MaterialButton
+
+        positiveButton?.let {
+            viewThemeUtils.material.colorMaterialButtonPrimaryTonal(positiveButton)
+        }
+
+        negativeButton?.let {
+            viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(negativeButton)
+        }
+    }
+
+    override fun onResume() {
+        super.onResume()
+        keyboardUtils.showKeyboardForEditText(requireDialog().window, binding.userInput)
+    }
+
+    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+        publicShare = requireArguments().getParcelableArgument(ARG_PUBLIC_SHARE, OCShare::class.java)
+
+        val inflater = requireActivity().layoutInflater
+        binding = EditBoxDialogBinding.inflate(inflater, null, false)
+        val view: View = binding.root
+
+        viewThemeUtils.material.colorTextInputLayout(binding.userInputContainer)
+        binding.userInput.setText(publicShare?.label)
+
+        val builder = MaterialAlertDialogBuilder(view.context)
+            .setView(view)
+            .setPositiveButton(R.string.file_rename, this)
+            .setNegativeButton(R.string.common_cancel, this)
+            .setTitle(R.string.public_share_name)
+
+        viewThemeUtils.dialog.colorMaterialAlertDialogBackground(binding.userInput.context, builder)
+
+        return builder.create()
+    }
+
+    override fun onClick(dialog: DialogInterface, which: Int) {
+        when (which) {
+            AlertDialog.BUTTON_POSITIVE -> {
+                var newName = ""
+                if (binding.userInput.text != null) {
+                    newName = binding.userInput.text.toString().trim { it <= ' ' }
+                }
+
+                if (TextUtils.isEmpty(newName)) {
+                    DisplayUtils.showSnackMessage(requireActivity(), R.string.label_empty)
+                    return
+                }
+
+                (requireActivity() as ComponentsGetter).fileOperationsHelper.setLabelToPublicShare(
+                    publicShare,
+                    newName
+                )
+            }
+        }
+    }
+
+    companion object {
+        private const val ARG_PUBLIC_SHARE = "PUBLIC_SHARE"
+
+        fun newInstance(share: OCShare?): RenamePublicShareDialogFragment {
+            val bundle = Bundle().apply {
+                putParcelable(ARG_PUBLIC_SHARE, share)
+            }
+
+            return RenamePublicShareDialogFragment().apply {
+                arguments = bundle
+            }
+        }
+    }
+}

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

@@ -60,6 +60,7 @@ import com.nextcloud.utils.ShortcutUtil;
 import com.nextcloud.utils.extensions.BundleExtensionsKt;
 import com.nextcloud.utils.extensions.FileExtensionsKt;
 import com.nextcloud.utils.extensions.IntentExtensionsKt;
+import com.nextcloud.utils.fileNameValidator.FileNameValidator;
 import com.nextcloud.utils.view.FastScrollUtils;
 import com.owncloud.android.MainApp;
 import com.owncloud.android.R;
@@ -221,7 +222,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
     protected boolean mHideFab = true;
     protected ActionMode mActiveActionMode;
     protected boolean mIsActionModeNew;
-    protected OCFileListFragment.MultiChoiceModeListener mMultiChoiceModeListener;
+    protected MultiChoiceModeListener mMultiChoiceModeListener;
 
     protected SearchType currentSearchType;
     protected boolean searchFragment;
@@ -990,6 +991,14 @@ public class OCFileListFragment extends ExtendedListFragment implements
     }
 
     private void folderOnItemClick(OCFile file, int position) {
+        if (requireActivity() instanceof FolderPickerActivity) {
+            String filenameErrorMessage = FileNameValidator.INSTANCE.checkFileName(file.getFileName(), getCapabilities(), requireContext(), null);
+            if (filenameErrorMessage != null) {
+                DisplayUtils.showSnackMessage(requireActivity(), filenameErrorMessage);
+                return;
+            }
+        }
+
         if (file.isEncrypted()) {
             User user = ((FileActivity) mContainerActivity).getUser().orElseThrow(RuntimeException::new);
 
@@ -1246,6 +1255,19 @@ public class OCFileListFragment extends ExtendedListFragment implements
             mContainerActivity.getFileOperationsHelper().toggleFavoriteFiles(checkedFiles, false);
             return true;
         } else if (itemId == R.id.action_move_or_copy) {
+            String invalidFilename = checkInvalidFilenames(checkedFiles);
+
+            if (invalidFilename != null) {
+                DisplayUtils.showSnackMessage(requireActivity(), getString(R.string.file_name_validator_rename_before_move_or_copy, invalidFilename));
+                return false;
+            }
+
+            if (!FileNameValidator.INSTANCE.checkParentRemotePaths(new ArrayList<>(checkedFiles), getCapabilities(), requireContext())) {
+                browseToRoot();
+                DisplayUtils.showSnackMessage(requireActivity(), R.string.file_name_validator_current_path_is_invalid);
+                return false;
+            }
+
             pickFolderForMoveOrCopy(checkedFiles);
             return true;
         } else if (itemId == R.id.action_select_all_action_menu) {
@@ -1264,6 +1286,27 @@ public class OCFileListFragment extends ExtendedListFragment implements
         return false;
     }
 
+    private void browseToRoot() {
+        OCFile root = mContainerActivity.getStorageManager().getFileByEncryptedRemotePath(ROOT_PATH);
+        browseToFolder(root,0);
+    }
+
+    private OCCapability getCapabilities() {
+        final User currentUser = accountManager.getUser();
+        return mContainerActivity.getStorageManager().getCapability(currentUser.getAccountName());
+    }
+
+    private String checkInvalidFilenames(Set<OCFile> checkedFiles) {
+        for (OCFile file : checkedFiles) {
+            String errorMessage = FileNameValidator.INSTANCE.checkFileName(file.getFileName(), getCapabilities(), requireContext(), null);
+            if (errorMessage != null) {
+                return errorMessage;
+            }
+        }
+
+        return null;
+    }
+
     private void pickFolderForMoveOrCopy(final Set<OCFile> checkedFiles) {
         int requestCode = FileDisplayActivity.REQUEST_CODE__MOVE_OR_COPY_FILES;
         String extraAction = FolderPickerActivity.MOVE_OR_COPY;
@@ -1550,7 +1593,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
             }
         }
 
-        if (SearchType.FILE_SEARCH != currentSearchType && getActivity() != null) {
+        if (FILE_SEARCH != currentSearchType && getActivity() != null) {
             getActivity().invalidateOptionsMenu();
         }
     }
@@ -1559,27 +1602,27 @@ public class OCFileListFragment extends ExtendedListFragment implements
         if (event != null) {
             switch (event.getSearchType()) {
                 case FILE_SEARCH:
-                    setEmptyListMessage(SearchType.FILE_SEARCH);
+                    setEmptyListMessage(FILE_SEARCH);
                     break;
 
                 case FAVORITE_SEARCH:
-                    setEmptyListMessage(SearchType.FAVORITE_SEARCH);
+                    setEmptyListMessage(FAVORITE_SEARCH);
                     break;
 
                 case RECENTLY_MODIFIED_SEARCH:
-                    setEmptyListMessage(SearchType.RECENTLY_MODIFIED_SEARCH);
+                    setEmptyListMessage(RECENTLY_MODIFIED_SEARCH);
                     break;
 
                 case SHARED_FILTER:
-                    setEmptyListMessage(SearchType.SHARED_FILTER);
+                    setEmptyListMessage(SHARED_FILTER);
                     break;
 
                 default:
-                    setEmptyListMessage(SearchType.NO_SEARCH);
+                    setEmptyListMessage(NO_SEARCH);
                     break;
             }
         } else {
-            setEmptyListMessage(SearchType.NO_SEARCH);
+            setEmptyListMessage(NO_SEARCH);
         }
     }
 
@@ -1616,7 +1659,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
     private void resetSearchAttributes() {
         searchFragment = false;
         searchEvent = null;
-        currentSearchType = SearchType.NO_SEARCH;
+        currentSearchType = NO_SEARCH;
     }
 
     @Subscribe(threadMode = ThreadMode.BACKGROUND)
@@ -1635,8 +1678,8 @@ public class OCFileListFragment extends ExtendedListFragment implements
             RemoteOperationResult remoteOperationResult = toggleFavoriteOperation.execute(client);
 
             if (remoteOperationResult.isSuccess()) {
-                boolean removeFromList = currentSearchType == SearchType.FAVORITE_SEARCH && !event.getShouldFavorite();
-                setEmptyListMessage(SearchType.FAVORITE_SEARCH);
+                boolean removeFromList = currentSearchType == FAVORITE_SEARCH && !event.getShouldFavorite();
+                setEmptyListMessage(FAVORITE_SEARCH);
                 mAdapter.setFavoriteAttributeForItemID(event.getRemotePath(), event.getShouldFavorite(), removeFromList);
             }
 
@@ -1674,7 +1717,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
         searchFragment = true;
         setEmptyListLoadingMessage();
         mAdapter.setData(new ArrayList<>(),
-                         SearchType.NO_SEARCH,
+                         NO_SEARCH,
                          mContainerActivity.getStorageManager(),
                          mFile,
                          true);

+ 23 - 3
app/src/main/java/com/owncloud/android/ui/helpers/UriUploader.kt

@@ -15,6 +15,7 @@ import android.net.Uri
 import android.os.Parcelable
 import com.nextcloud.client.account.User
 import com.nextcloud.client.jobs.upload.FileUploadHelper
+import com.nextcloud.utils.fileNameValidator.FileNameValidator
 import com.owncloud.android.R
 import com.owncloud.android.files.services.NameCollisionPolicy
 import com.owncloud.android.lib.common.utils.Log_OC
@@ -57,9 +58,11 @@ class UriUploader(
         ERROR_UNKNOWN,
         ERROR_NO_FILE_TO_UPLOAD,
         ERROR_READ_PERMISSION_NOT_GRANTED,
-        ERROR_SENSITIVE_PATH
+        ERROR_SENSITIVE_PATH,
+        INVALID_FILE_NAME
     }
 
+    @Suppress("NestedBlockDepth")
     fun uploadUris(): UriUploaderResultCode {
         var code = UriUploaderResultCode.OK
         try {
@@ -70,9 +73,22 @@ class UriUploader(
                 Log_OC.e(TAG, "Sensitive URI detected, aborting upload.")
                 code = UriUploaderResultCode.ERROR_SENSITIVE_PATH
             } else {
-                val uris = mUrisToUpload.filterNotNull()
+                var isFilenameValid = true
+
+                val uris = mUrisToUpload
+                    .filterNotNull()
                     .map { it as Uri }
                     .map { Pair(it, getRemotePathForUri(it)) }
+                    .filter { (_, filename) ->
+                        isFilenameValid = FileNameValidator.checkFileName(
+                            filename.removePrefix("/"),
+                            mActivity.capabilities,
+                            mActivity,
+                            null
+                        ) == null
+
+                        isFilenameValid
+                    }
 
                 val fileUris = uris
                     .filter { it.first.scheme == ContentResolver.SCHEME_FILE }
@@ -87,7 +103,11 @@ class UriUploader(
                     val (contentUris, contentRemotePaths) = contentUrisNew.unzip()
                     copyThenUpload(contentUris.toTypedArray(), contentRemotePaths.toTypedArray())
                 } else if (fileUris.isEmpty()) {
-                    code = UriUploaderResultCode.ERROR_NO_FILE_TO_UPLOAD
+                    code = if (!isFilenameValid) {
+                        UriUploaderResultCode.INVALID_FILE_NAME
+                    } else {
+                        UriUploaderResultCode.ERROR_NO_FILE_TO_UPLOAD
+                    }
                 }
             }
         } catch (e: SecurityException) {

+ 2 - 1
app/src/main/java/com/owncloud/android/utils/glide/HttpStreamFetcher.kt

@@ -31,7 +31,8 @@ class HttpStreamFetcher internal constructor(
     @Throws(Exception::class)
     override fun loadData(priority: Priority): InputStream? {
         val client = clientFactory.create(user)
-        if (client != null) {
+
+        if (client != null && url.isNotBlank()) {
             var get: GetMethod? = null
             try {
                 get = GetMethod(url)

+ 11 - 0
app/src/main/res/color/card_border_selector.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Nextcloud - Android Client
+  ~
+  ~ SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
+  ~ SPDX-License-Identifier: AGPL-3.0-or-later
+  -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:color="@color/primary" android:state_checked="true"/>
+    <item android:color="@color/grey_600" android:state_checked="false"/>
+</selector>

+ 12 - 0
app/src/main/res/drawable/rounded_rect_8dp.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Nextcloud - Android Client
+  ~
+  ~ SPDX-FileCopyrightText: 2023 ZetaTom
+  ~ SPDX-FileCopyrightText: 2023 Nextcloud GmbH
+  ~ SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <corners android:radius="8dp" />
+</shape>

+ 14 - 3
app/src/main/res/layout/template_button.xml

@@ -2,26 +2,37 @@
 <!--
   ~ Nextcloud - Android Client
   ~
+  ~ SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
   ~ SPDX-FileCopyrightText: 2021 Andy Scherzinger <info@andy-scherzinger>
   ~ SPDX-FileCopyrightText: 2018 Tobias Kaminsky <tobias@kaminsky.me>
   ~ SPDX-FileCopyrightText: 2018 Nextcloud GmbH
   ~ SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
 -->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<LinearLayout
+    android:id="@+id/template_layout"
+    xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
+    android:clickable="true"
+    android:focusable="true"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
+    android:background="@drawable/rounded_rect_8dp"
+    android:backgroundTint="@color/grey_200"
     android:orientation="vertical"
+    android:layout_marginEnd="@dimen/standard_margin"
+    android:layout_marginBottom="@dimen/standard_margin"
     android:paddingTop="@dimen/standard_padding">
 
     <com.google.android.material.card.MaterialCardView
         android:id="@+id/template_container"
+        android:checkable="true"
+        android:clickable="true"
+        android:focusable="true"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_gravity="center_horizontal"
-        app:cardElevation="0dp"
-        app:strokeColor="@color/grey_200"
+        app:strokeColor="@color/card_border_selector"
         app:strokeWidth="2dp">
 
         <ImageView

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

@@ -371,6 +371,7 @@
     <string name="filename_forbidden_characters">Forbidden characters: / \\ &lt; &gt; : " | ? *</string>
     <string name="filename_forbidden_charaters_from_server">Filename contains at least one invalid character</string>
     <string name="filename_empty">Filename cannot be empty</string>
+    <string name="folder_name_empty">Folder name cannot be empty</string>
     <string name="label_empty">Label cannot be empty</string>
     <string name="wait_a_moment">Wait a moment…</string>
     <string name="wait_checking_credentials">Checking stored credentials</string>
@@ -1214,7 +1215,17 @@
     <string name="secure_share_not_set_up">Secure sharing is not set up for this user</string>
     <string name="share_not_allowed_when_file_drop">Resharing is not allowed during secure file drop</string>
     <string name="file_list_empty_local_search">No file or folder matching your search</string>
+
     <string name="unified_search_fragment_calendar_event_not_found">Event not found, you can always sync to update. Redirecting to web…</string>
     <string name="unified_search_fragment_contact_not_found">Contact not found, you can always sync to update. Redirecting to web…</string>
     <string name="unified_search_fragment_permission_needed">Permissions are required to open search result otherwise it will redirected to web…</string>
+
+    <string name="file_name_validator_current_path_is_invalid">Current folder name is invalid, please rename the folder. Redirecting to root</string>
+    <string name="file_name_validator_rename_before_move_or_copy">%s. Please rename the file before moving or copying</string>
+    <string name="file_name_validator_upload_content_error">Some contents cannot able to uploaded due to contains reserved names or invalid character</string>
+    <string name="file_name_validator_error_contains_reserved_names_or_invalid_characters">Folder path contains reserved names or invalid character</string>
+    <string name="file_name_validator_error_invalid_character">Name contains an invalid character: %s</string>
+    <string name="file_name_validator_error_reserved_names">%s is a forbidden name</string>
+    <string name="file_name_validator_error_forbidden_file_extensions">.%s is a forbidden file extension</string>
+    <string name="file_name_validator_error_ends_with_space_period">Name ends with a space or a period</string>
 </resources>

+ 1 - 1
build.gradle

@@ -10,7 +10,7 @@
  */
 buildscript {
     ext {
-        androidLibraryVersion ="2c62a70fc8dda04965700be481f75fb1e1d3c1f5"
+        androidLibraryVersion ="86b0279d70"
         androidPluginVersion = '8.5.2'
         androidxMediaVersion = '1.4.0'
         androidxTestVersion = "1.6.1"

+ 1 - 1
gradle.properties

@@ -21,4 +21,4 @@ org.gradle.parallel=true
 org.gradle.configureondemand=true
 
 # Needed for local libs
-# org.gradle.dependency.verification=lenient
+# org.gradle.dependency.verification=lenient

+ 8 - 0
gradle/verification-metadata.xml

@@ -6061,6 +6061,14 @@
             <sha256 value="de2e7d2fdde1d6981af8fcd54812dfe510ab7e22bdf8225ed569616b6c1155f4" origin="Generated by Gradle" reason="Artifact is not signed"/>
          </artifact>
       </component>
+      <component group="com.github.nextcloud" name="android-library" version="86b0279d70">
+         <artifact name="android-library-86b0279d70.aar">
+            <sha256 value="b2c1943afb79c13aa876addc1b74a19f56e9ec2e8b5b7ced6b79fa89b0632c44" origin="Generated by Gradle" reason="Artifact is not signed"/>
+         </artifact>
+         <artifact name="android-library-86b0279d70.module">
+            <sha256 value="45d96eff7306b527c04416b5c80b69480064e644c39622d62a04a9701b819baa" origin="Generated by Gradle" reason="Artifact is not signed"/>
+         </artifact>
+      </component>
       <component group="com.github.nextcloud" name="android-library" version="9fdcd0af0ff910086281f32e3b8ef74490671149">
          <artifact name="android-library-9fdcd0af0ff910086281f32e3b8ef74490671149.aar">
             <sha256 value="57ab4fd7c922875a7e0b5feac20aa27ab5df0fd3b4e042f92ed727c0b6316e81" origin="Generated by Gradle" reason="Artifact is not signed"/>

+ 1 - 1
scripts/analysis/detectWrongSettings.sh

@@ -6,7 +6,7 @@
 
 snapshotCount=$(./gradlew dependencies | grep SNAPSHOT -c)
 betaCount=$(grep "<bool name=\"is_beta\">true</bool>" app/src/main/res/values/setup.xml -c)
-libraryHash=$(grep androidLibraryVersion build.gradle | cut -f2 -d'"' | grep "^[0-9a-zA-Z]\{40\}$" -c)
+libraryHash=$(grep androidLibraryVersion build.gradle | cut -f2 -d'"' | grep -vi "snapshot"  | grep "^[0-9a-zA-Z]\{10,40\}$" -c)
 
 
 if [[ $snapshotCount -gt 0 ]] ; then

+ 1 - 1
scripts/analysis/lint-results.txt

@@ -1,2 +1,2 @@
 DO NOT TOUCH; GENERATED BY DRONE
-      <span class="mdl-layout-title">Lint Report: 3 errors and 62 warnings</span>
+      <span class="mdl-layout-title">Lint Report: 3 errors and 60 warnings</span>