Explorar o código

Merge pull request #12612 from nextcloud/feature/assistant

Assistant Feature
Tobias Kaminsky hai 1 ano
pai
achega
c1c29891ba
Modificáronse 64 ficheiros con 3205 adicións e 172 borrados
  1. 38 2
      .idea/inspectionProfiles/ktlint.xml
  2. 14 1
      app/build.gradle
  3. 1 1
      app/detekt.yml
  4. 1 1
      app/schemas/com.nextcloud.client.database.NextcloudDatabase/78.json
  5. 1203 0
      app/schemas/com.nextcloud.client.database.NextcloudDatabase/79.json
  6. BIN=BIN
      app/screenshots/gplay/debug/com.nextcloud.client.ActivitiesActivityIT_loading.png
  7. BIN=BIN
      app/screenshots/gplay/debug/com.nextcloud.client.SyncedFoldersActivityIT_open.png
  8. BIN=BIN
      app/screenshots/gplay/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_showNotifications.png
  9. BIN=BIN
      app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivities.png
  10. BIN=BIN
      app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesError.png
  11. BIN=BIN
      app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_empty.png
  12. BIN=BIN
      app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_loading.png
  13. 9 4
      app/src/androidTest/java/com/nextcloud/client/ActivitiesActivityIT.kt
  14. 6 3
      app/src/androidTest/java/com/nextcloud/client/SyncedFoldersActivityIT.java
  15. 109 0
      app/src/androidTest/java/com/nextcloud/client/assistant/AssistantRepositoryTests.kt
  16. 14 1
      app/src/androidTest/java/com/owncloud/android/AbstractIT.java
  17. 7 7
      app/src/androidTest/java/com/owncloud/android/ui/activity/NotificationsActivityIT.kt
  18. 1 1
      app/src/androidTest/java/com/owncloud/android/ui/activity/UploadFilesActivityIT.kt
  19. 11 14
      app/src/androidTest/java/com/owncloud/android/ui/fragment/FileDetailFragmentStaticServerIT.kt
  20. 2 2
      app/src/androidTest/java/com/owncloud/android/ui/trashbin/TrashbinActivityIT.kt
  21. 3 3
      app/src/androidTest/java/com/owncloud/android/util/EncryptionTestIT.java
  22. 1 1
      app/src/androidTest/java/com/owncloud/android/utils/EncryptionUtilsV2IT.kt
  23. 81 98
      app/src/main/AndroidManifest.xml
  24. 179 0
      app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt
  25. 279 0
      app/src/main/java/com/nextcloud/client/assistant/AsssistantScreen.kt
  26. 74 0
      app/src/main/java/com/nextcloud/client/assistant/component/AddTaskAlertDialog.kt
  27. 42 0
      app/src/main/java/com/nextcloud/client/assistant/component/CenterText.kt
  28. 65 0
      app/src/main/java/com/nextcloud/client/assistant/component/TaskTypesRow.kt
  29. 174 0
      app/src/main/java/com/nextcloud/client/assistant/component/TaskView.kt
  30. 93 0
      app/src/main/java/com/nextcloud/client/assistant/repository/AssistantMockRepository.kt
  31. 53 0
      app/src/main/java/com/nextcloud/client/assistant/repository/AssistantRepository.kt
  32. 39 0
      app/src/main/java/com/nextcloud/client/assistant/repository/AssistantRepositoryType.kt
  33. 2 1
      app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt
  34. 2 0
      app/src/main/java/com/nextcloud/client/database/entity/CapabilityEntity.kt
  35. 4 0
      app/src/main/java/com/nextcloud/client/di/ComponentsModule.java
  36. 133 0
      app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt
  37. 28 0
      app/src/main/java/com/nextcloud/ui/composeActivity/ComposeDestination.kt
  38. 93 0
      app/src/main/java/com/nextcloud/ui/composeComponents/alertDialog/SimpleAlertDialog.kt
  39. 100 0
      app/src/main/java/com/nextcloud/ui/composeComponents/bottomSheet/MoreActionsBottomSheet.kt
  40. 2 0
      app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java
  41. 2 1
      app/src/main/java/com/owncloud/android/db/ProviderMeta.java
  42. 1 1
      app/src/main/java/com/owncloud/android/ui/activities/ActivitiesActivity.java
  43. 0 1
      app/src/main/java/com/owncloud/android/ui/activity/CommunityActivity.kt
  44. 35 14
      app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java
  45. 0 1
      app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java
  46. 1 1
      app/src/main/java/com/owncloud/android/ui/activity/NotificationsActivity.kt
  47. 3 3
      app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt
  48. 7 0
      app/src/main/java/com/owncloud/android/ui/adapter/SyncedFolderAdapter.java
  49. 4 0
      app/src/main/java/com/owncloud/android/ui/fragment/ExtendedListFragment.java
  50. 1 1
      app/src/main/java/com/owncloud/android/ui/fragment/FileDetailActivitiesFragment.java
  51. 1 1
      app/src/main/java/com/owncloud/android/ui/trashbin/TrashbinActivity.kt
  52. 7 0
      app/src/main/java/com/owncloud/android/utils/DrawerMenuUtil.java
  53. 1 1
      app/src/main/java/com/owncloud/android/utils/WebViewUtil.kt
  54. 32 0
      app/src/main/res/drawable/ic_assistant.xml
  55. 54 0
      app/src/main/res/layout/activity_compose.xml
  56. 31 0
      app/src/main/res/layout/drawer_header.xml
  57. 31 0
      app/src/main/res/layout/fragment_compose_view.xml
  58. 9 0
      app/src/main/res/menu/partial_drawer_entries.xml
  59. 28 1
      app/src/main/res/values/strings.xml
  60. 2 2
      appscan/build.gradle
  61. 2 2
      build.gradle
  62. 3 0
      gradle.properties
  63. 85 0
      gradle/verification-metadata.xml
  64. 2 2
      gradle/wrapper/gradle-wrapper.properties

+ 38 - 2
.idea/inspectionProfiles/ktlint.xml

@@ -2,9 +2,45 @@
   <profile version="1.0">
     <option name="myName" value="ktlint" />
     <inspection_tool class="AutoCloseableResource" enabled="true" level="WARNING" enabled_by_default="true">
-      <option name="METHOD_MATCHER_CONFIG" value="java.util.Formatter,format,java.io.Writer,append,com.google.common.base.Preconditions,checkNotNull,org.hibernate.Session,close,java.io.PrintWriter,printf,java.io.PrintStream,printf,java.nio.channels.FileChannel,position" />
+      <option name="METHOD_MATCHER_CONFIG" value="java.util.Formatter,format,java.io.Writer,append,com.google.common.base.Preconditions,checkNotNull,org.hibernate.Session,close,java.io.PrintWriter,printf,java.io.PrintStream,printf,java.nio.channels.FileChannel,position,okhttp3.Call,execute" />
     </inspection_tool>
     <inspection_tool class="KotlinUnusedImport" enabled="true" level="ERROR" enabled_by_default="true" />
+    <inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
+      <option name="composableFile" value="true" />
+      <option name="previewFile" value="true" />
+    </inspection_tool>
+    <inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
+      <option name="composableFile" value="true" />
+      <option name="previewFile" value="true" />
+    </inspection_tool>
+    <inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
+      <option name="composableFile" value="true" />
+      <option name="previewFile" value="true" />
+    </inspection_tool>
+    <inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
+      <option name="composableFile" value="true" />
+      <option name="previewFile" value="true" />
+    </inspection_tool>
+    <inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
+      <option name="composableFile" value="true" />
+      <option name="previewFile" value="true" />
+    </inspection_tool>
+    <inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
+      <option name="composableFile" value="true" />
+      <option name="previewFile" value="true" />
+    </inspection_tool>
+    <inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
+      <option name="composableFile" value="true" />
+      <option name="previewFile" value="true" />
+    </inspection_tool>
+    <inspection_tool class="PreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
+      <option name="composableFile" value="true" />
+      <option name="previewFile" value="true" />
+    </inspection_tool>
+    <inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
+      <option name="composableFile" value="true" />
+      <option name="previewFile" value="true" />
+    </inspection_tool>
     <inspection_tool class="RedundantSemicolon" enabled="true" level="ERROR" enabled_by_default="true" />
   </profile>
-</component>
+</component>

+ 14 - 1
app/build.gradle

@@ -19,7 +19,7 @@ buildscript {
 
 plugins {
     id "com.diffplug.spotless" version "6.20.0"
-    id 'com.google.devtools.ksp' version '1.9.23-1.0.19' apply false
+    id 'com.google.devtools.ksp' version '1.9.22-1.0.17' apply false
 }
 
 apply plugin: 'com.android.application'
@@ -223,6 +223,7 @@ android {
         dataBinding true
         viewBinding true
         aidl true
+        compose = true
     }
 
     compileOptions {
@@ -246,6 +247,10 @@ android {
         // Adds exported schema location as test app assets.
         androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
     }
+
+    composeOptions {
+        kotlinCompilerExtensionVersion = "1.5.10"
+    }
 }
 
 dependencies {
@@ -255,6 +260,14 @@ dependencies {
         exclude group: 'org.ogce', module: 'xpp3' // unused in Android and brings wrong Junit version
     }
 
+    // Jetpack Compose
+    implementation(platform("androidx.compose:compose-bom:2024.02.01"))
+    implementation("androidx.compose.ui:ui")
+    implementation("androidx.compose.ui:ui-graphics")
+    implementation("androidx.compose.material3:material3")
+    implementation("androidx.compose.ui:ui-tooling-preview:1.6.2")
+    debugImplementation 'androidx.compose.ui:ui-tooling:1.6.2'
+
     compileOnly 'org.jbundle.util.osgi.wrapped:org.jbundle.util.osgi.wrapped.org.apache.http.client:4.1.2'
     // remove after entire switch to lib v2
     implementation "commons-httpclient:commons-httpclient:3.1@jar" // remove after entire switch to lib v2

+ 1 - 1
app/detekt.yml

@@ -199,7 +199,7 @@ naming:
     minimumFunctionNameLength: 3
   FunctionNaming:
     active: true
-    functionPattern: '^([a-z$][a-zA-Z$0-9]*)|(`.*`)$'
+    functionPattern: '^([a-z$A-Z][a-zA-Z$0-9]*)|(`.*`)$'
     excludeClassPattern: '$^'
     ignoreOverridden: true
     excludes:

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

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

+ 1203 - 0
app/schemas/com.nextcloud.client.database.NextcloudDatabase/79.json

@@ -0,0 +1,1203 @@
+{
+    "formatVersion": 1,
+    "database": {
+        "version": 79,
+        "identityHash": "ec997f271f9045e8483b260f036a168f",
+        "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)",
+                "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
+                    }
+                ],
+                "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` INTEGER, `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": "INTEGER",
+                        "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)",
+                "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
+                    }
+                ],
+                "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, 'ec997f271f9045e8483b260f036a168f')"
+        ]
+    }
+}

BIN=BIN
app/screenshots/gplay/debug/com.nextcloud.client.ActivitiesActivityIT_loading.png


BIN=BIN
app/screenshots/gplay/debug/com.nextcloud.client.SyncedFoldersActivityIT_open.png


BIN=BIN
app/screenshots/gplay/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_showNotifications.png


BIN=BIN
app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivities.png


BIN=BIN
app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesError.png


BIN=BIN
app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_empty.png


BIN=BIN
app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_loading.png


+ 9 - 4
app/src/androidTest/java/com/nextcloud/client/ActivitiesActivityIT.kt

@@ -21,6 +21,7 @@
  */
 package com.nextcloud.client
 
+import android.view.View
 import androidx.test.espresso.Espresso
 import androidx.test.espresso.contrib.DrawerActions
 import androidx.test.espresso.intent.rule.IntentsTestRule
@@ -60,15 +61,19 @@ class ActivitiesActivityIT : AbstractIT() {
     @Test
     @ScreenshotTest
     fun loading() {
-        val sut: ActivitiesActivity = activityRule.launchActivity(null)
-        sut.runOnUiThread {
-            sut.dismissSnackbar()
+        val sut: ActivitiesActivity = activityRule.launchActivity(null).apply {
+            runOnUiThread {
+                dismissSnackbar()
+                binding.emptyList.root.visibility = View.GONE
+                binding.swipeContainingList.visibility = View.GONE
+                binding.loadingContent.visibility = View.VISIBLE
+            }
         }
 
         shortSleep()
         waitForIdleSync()
 
-        Screenshot.snapActivity(sut).record()
+        Screenshot.snap(sut.binding.loadingContent).record()
     }
 
     @Test

+ 6 - 3
app/src/androidTest/java/com/nextcloud/client/SyncedFoldersActivityIT.java

@@ -27,6 +27,7 @@ import android.content.Intent;
 
 import com.nextcloud.client.preferences.SubFolderRule;
 import com.owncloud.android.AbstractIT;
+import com.owncloud.android.databinding.SyncedFoldersLayoutBinding;
 import com.owncloud.android.datamodel.MediaFolderType;
 import com.owncloud.android.datamodel.SyncedFolderDisplayItem;
 import com.owncloud.android.ui.activity.SyncedFoldersActivity;
@@ -51,9 +52,11 @@ public class SyncedFoldersActivityIT extends AbstractIT {
     @Test
     @ScreenshotTest
     public void open() {
-        Activity sut = activityRule.launchActivity(null);
-
-        screenshot(sut);
+        SyncedFoldersActivity activity = activityRule.launchActivity(null);
+        activity.adapter.clear();
+        SyncedFoldersLayoutBinding sut = activity.binding;
+        shortSleep();
+        screenshot(sut.emptyList.emptyListView);
     }
 
     @Test

+ 109 - 0
app/src/androidTest/java/com/nextcloud/client/assistant/AssistantRepositoryTests.kt

@@ -0,0 +1,109 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Alper Ozturk
+ * Copyright (C) 2024 Alper Ozturk
+ * Copyright (C) 2024 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.client.assistant
+
+import com.nextcloud.client.assistant.repository.AssistantRepository
+import com.owncloud.android.AbstractOnServerIT
+import com.owncloud.android.lib.resources.status.NextcloudVersion
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+
+@Suppress("MagicNumber")
+class AssistantRepositoryTests : AbstractOnServerIT() {
+
+    private var sut: AssistantRepository? = null
+
+    @Before
+    fun setup() {
+        sut = AssistantRepository(nextcloudClient)
+    }
+
+    @Test
+    fun testGetTaskTypes() {
+        testOnlyOnServer(NextcloudVersion.nextcloud_28)
+
+        if (capability.assistant.isFalse) {
+            return
+        }
+
+        val result = sut?.getTaskTypes()
+        assertTrue(result?.isSuccess == true)
+
+        val taskTypes = result?.resultData?.types
+        assertTrue(taskTypes?.isNotEmpty() == true)
+    }
+
+    @Test
+    fun testGetTaskList() {
+        testOnlyOnServer(NextcloudVersion.nextcloud_28)
+
+        if (capability.assistant.isFalse) {
+            return
+        }
+
+        val result = sut?.getTaskList("assistant")
+        assertTrue(result?.isSuccess == true)
+
+        val taskList = result?.resultData?.tasks
+        assertTrue(taskList?.isEmpty() == true || (taskList?.size ?: 0) > 0)
+    }
+
+    @Test
+    fun testCreateTask() {
+        testOnlyOnServer(NextcloudVersion.nextcloud_28)
+
+        if (capability.assistant.isFalse) {
+            return
+        }
+
+        val input = "Give me some random output for test purpose"
+        val type = "OCP\\TextProcessing\\FreePromptTaskType"
+        val result = sut?.createTask(input, type)
+        assertTrue(result?.isSuccess == true)
+    }
+
+    @Test
+    fun testDeleteTask() {
+        testOnlyOnServer(NextcloudVersion.nextcloud_28)
+
+        if (capability.assistant.isFalse) {
+            return
+        }
+
+        testCreateTask()
+
+        sleep(120)
+
+        val resultOfTaskList = sut?.getTaskList("assistant")
+        assertTrue(resultOfTaskList?.isSuccess == true)
+
+        sleep(120)
+
+        val taskList = resultOfTaskList?.resultData?.tasks
+
+        assert((taskList?.size ?: 0) > 0)
+
+        val result = sut?.deleteTask(taskList!!.first().id)
+        assertTrue(result?.isSuccess == true)
+    }
+}

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

@@ -195,13 +195,18 @@ public abstract class AbstractIT {
     }
 
     protected void testOnlyOnServer(OwnCloudVersion version) throws AccountUtils.AccountNotFoundException {
+        OCCapability ocCapability = getCapability();
+        assumeTrue(ocCapability.getVersion().isNewerOrEqual(version));
+    }
+
+    protected OCCapability getCapability() throws AccountUtils.AccountNotFoundException {
         NextcloudClient client = OwnCloudClientFactory.createNextcloudClient(user, targetContext);
 
         OCCapability ocCapability = (OCCapability) new GetCapabilitiesRemoteOperation()
             .execute(client)
             .getSingleData();
 
-        assumeTrue(ocCapability.getVersion().isNewerOrEqual(version));
+        return ocCapability;
     }
 
     @Before
@@ -334,6 +339,14 @@ public abstract class AbstractIT {
         }
     }
 
+    protected void sleep(int second) {
+        try {
+            Thread.sleep(1000L * second);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+    }
+
     public OCFile createFolder(String remotePath) {
         RemoteOperationResult check = new ExistenceCheckRemoteOperation(remotePath, false).execute(client);
 

+ 7 - 7
app/src/androidTest/java/com/owncloud/android/ui/activity/NotificationsActivityIT.kt

@@ -53,8 +53,6 @@ class NotificationsActivityIT : AbstractIT() {
     @ScreenshotTest
     @SuppressWarnings("MagicNumber")
     fun showNotifications() {
-        val sut: NotificationsActivity = activityRule.launchActivity(null)
-
         val date = GregorianCalendar()
         date.set(2005, 4, 17, 10, 35, 30) // random date
 
@@ -133,11 +131,13 @@ class NotificationsActivityIT : AbstractIT() {
             )
         )
 
-        sut.runOnUiThread { sut.populateList(notifications) }
-
-        shortSleep()
-
-        screenshot(sut)
+        activityRule.launchActivity(null).apply {
+            runOnUiThread {
+                populateList(notifications)
+            }
+            shortSleep()
+            screenshot(binding.list)
+        }
     }
 
     @Test

+ 1 - 1
app/src/androidTest/java/com/owncloud/android/ui/activity/UploadFilesActivityIT.kt

@@ -72,7 +72,7 @@ class UploadFilesActivityIT : AbstractIT() {
         waitForIdleSync()
         shortSleep()
 
-        screenshot(sut)
+        screenshot(sut.fileListFragment.binding.emptyList.emptyListView)
     }
 
     @Test

+ 11 - 14
app/src/androidTest/java/com/owncloud/android/ui/fragment/FileDetailFragmentStaticServerIT.kt

@@ -94,12 +94,6 @@ class FileDetailFragmentStaticServerIT : AbstractIT() {
     @ScreenshotTest
     @Suppress("MagicNumber")
     fun showDetailsActivities() {
-        val activity = testActivityRule.launchActivity(null)
-        val sut = FileDetailFragment.newInstance(oCFile, user, 0)
-        activity.addFragment(sut)
-
-        waitForIdleSync()
-
         val date = GregorianCalendar()
         date.set(2005, 4, 17, 10, 35, 30) // random date
 
@@ -152,13 +146,16 @@ class FileDetailFragmentStaticServerIT : AbstractIT() {
             )
         )
 
-        activity.runOnUiThread {
-            sut.fileDetailActivitiesFragment.populateList(activities as List<Any>?, true)
+        val sut = FileDetailFragment.newInstance(oCFile, user, 0)
+        testActivityRule.launchActivity(null).apply {
+            addFragment(sut)
+            waitForIdleSync()
+            runOnUiThread {
+                sut.fileDetailActivitiesFragment.populateList(activities as List<Any>?, true)
+            }
+            longSleep()
+            screenshot(sut.fileDetailActivitiesFragment.binding.swipeContainingList)
         }
-
-        shortSleep()
-        shortSleep()
-        screenshot(activity)
     }
 
     // @Test
@@ -176,7 +173,7 @@ class FileDetailFragmentStaticServerIT : AbstractIT() {
 
         shortSleep()
         shortSleep()
-        screenshot(activity)
+        screenshot(sut.fileDetailActivitiesFragment.binding.list)
     }
 
     @Test
@@ -197,7 +194,7 @@ class FileDetailFragmentStaticServerIT : AbstractIT() {
 
         shortSleep()
         shortSleep()
-        screenshot(activity)
+        screenshot(sut.fileDetailActivitiesFragment.binding.emptyList.emptyListView)
     }
 
     @Test

+ 2 - 2
app/src/androidTest/java/com/owncloud/android/ui/trashbin/TrashbinActivityIT.kt

@@ -89,7 +89,7 @@ class TrashbinActivityIT : AbstractIT() {
         shortSleep()
         waitForIdleSync()
 
-        screenshot(sut)
+        screenshot(sut.binding.emptyList.emptyListView)
     }
 
     @Test
@@ -105,7 +105,7 @@ class TrashbinActivityIT : AbstractIT() {
 
         shortSleep()
 
-        screenshot(sut)
+        screenshot(sut.binding.listFragmentLayout)
     }
 
     @Test

+ 3 - 3
app/src/androidTest/java/com/owncloud/android/util/EncryptionTestIT.java

@@ -610,7 +610,7 @@ public class EncryptionTestIT extends AbstractIT {
         EncryptionUtils.encryptFileDropFiles(decryptedFolderMetadata1, encryptedFolderMetadata1, publicKey);
 
         // serialize
-        String encryptedJson = serializeJSON(encryptedFolderMetadata1);
+        String encryptedJson = serializeJSON(encryptedFolderMetadata1, true);
 
         // de-serialize
         EncryptedFolderMetadataFileV1 encryptedFolderMetadata2 = deserializeJSON(encryptedJson,
@@ -626,8 +626,8 @@ public class EncryptionTestIT extends AbstractIT {
             folderID);
 
         // compare
-        assertFalse(compareJsonStrings(serializeJSON(decryptedFolderMetadata1),
-                                       serializeJSON(decryptedFolderMetadata2)));
+        assertFalse(compareJsonStrings(serializeJSON(decryptedFolderMetadata1, true),
+                                       serializeJSON(decryptedFolderMetadata2, true)));
 
         assertEquals(decryptedFolderMetadata1.getFiles().size() + decryptedFolderMetadata1.getFiledrop().size(),
                      decryptedFolderMetadata2.getFiles().size());

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

@@ -831,7 +831,7 @@ class EncryptionUtilsV2IT : AbstractIT() {
         val signature = encryptionUtilsV2.getMessageSignature(enc1Cert, enc1PrivateKey, encryptedFolderMetadata1)
 
         // serialize
-        val encryptedJson = EncryptionUtils.serializeJSON(encryptedFolderMetadata1)
+        val encryptedJson = EncryptionUtils.serializeJSON(encryptedFolderMetadata1, true)
 
         // de-serialize
         val encryptedFolderMetadata2 = EncryptionUtils.deserializeJSON(

+ 81 - 98
app/src/main/AndroidManifest.xml

@@ -1,22 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?><!--
-  Nextcloud Android client application
-
-  Copyright (C) 2012  Bartek Przybylski
-  Copyright (C) 2012-2016 ownCloud Inc.
-  Copyright (C) 2016 Nextcloud
-
-  This program is free software: you can redistribute it and/or modify
-  it under the terms of the GNU General Public License version 2,
-  as published by the Free Software Foundation.
-
-  This program is distributed in the hope that it will be useful,
-  but WITHOUT ANY WARRANTY; without even the implied warranty of
-  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-  GNU General Public License for more details.
-
-  You should have received a copy of the GNU General Public License
-  along with this program.  If not, see <http://www.gnu.org/licenses/>.
--->
+<?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools">
 
@@ -24,16 +6,15 @@
     <uses-permission android:name="android.permission.READ_CONTACTS" />
     <uses-permission android:name="android.permission.WRITE_CONTACTS" />
     <uses-permission android:name="android.permission.READ_CALENDAR" />
-    <uses-permission android:name="android.permission.WRITE_CALENDAR" />
-
-    <!-- Used for document scanning, but lib declares it as required, which it's not -->
+    <uses-permission android:name="android.permission.WRITE_CALENDAR" /> <!-- Used for document scanning, but lib declares it as required, which it's not -->
     <uses-feature
         android:name="android.hardware.camera2"
         android:required="false"
         tools:node="replace" />
-
-    <!-- WRITE_EXTERNAL_STORAGE may be enabled or disabled by the user after installation in
-        API >= 23; the app needs to handle this -->
+    <!--
+ WRITE_EXTERNAL_STORAGE may be enabled or disabled by the user after installation in
+        API >= 23; the app needs to handle this
+    -->
     <uses-permission
         android:name="android.permission.WRITE_EXTERNAL_STORAGE"
         android:maxSdkVersion="29"
@@ -45,9 +26,7 @@
         android:name="android.permission.READ_EXTERNAL_STORAGE"
         android:maxSdkVersion="32" />
     <uses-permission android:name="android.permission.CAMERA" />
-    <uses-permission android:name="android.permission.VIBRATE" />
-
-    <!-- Next permissions are always approved in installation time, the apps needs to do nothing special in runtime -->
+    <uses-permission android:name="android.permission.VIBRATE" /> <!-- Next permissions are always approved in installation time, the apps needs to do nothing special in runtime -->
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.READ_SYNC_STATS" />
     <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
@@ -62,35 +41,72 @@
     <uses-permission
         android:name="com.android.launcher.permission.INSTALL_SHORTCUT"
         android:maxSdkVersion="25" />
-
-    <!-- Apps that target Android 9 (API level 28) or higher and use foreground services
-    must request the FOREGROUND_SERVICE permission -->
-    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
-
-    <!-- Runtime permissions introduced in Android 13 (API level 33) -->
+    <!--
+ Apps that target Android 9 (API level 28) or higher and use foreground services
+    must request the FOREGROUND_SERVICE permission
+    -->
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <!-- Runtime permissions introduced in Android 13 (API level 33) -->
     <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
     <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
-    <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
-
-    <!-- Needed for Android 14 (API level 34) -->
+    <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" /> <!-- Needed for Android 14 (API level 34) -->
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
-
-    <!-- Some Chromebooks don't support touch. Although not essential,
-         it's a good idea to explicitly include this declaration. -->
+    <!--
+ Some Chromebooks don't support touch. Although not essential,
+         it's a good idea to explicitly include this declaration.
+    -->
     <uses-feature
         android:name="android.hardware.touchscreen"
         android:required="false" />
-
     <uses-feature
         android:name="android.hardware.camera"
         android:required="false" />
 
+    <queries>
+        <package android:name="it.niedermann.nextcloud.deck" />
+        <package android:name="it.niedermann.nextcloud.deck.play" />
+        <package android:name="it.niedermann.nextcloud.deck.dev" />
+        <package android:name="at.bitfire.davdroid" />
+
+        <intent>
+            <action android:name="android.intent.action.VIEW" />
+
+            <data android:mimeType="*/*" />
+        </intent>
+        <intent>
+            <action android:name="android.intent.action.SEND" />
+
+            <data android:mimeType="*/*" />
+        </intent>
+        <intent>
+            <action android:name="android.intent.action.SEND_MULTIPLE" />
+
+            <data android:mimeType="*/*" />
+        </intent>
+        <intent>
+            <action android:name="android.intent.action.PICK" />
+
+            <data android:mimeType="*/*" />
+        </intent>
+        <intent>
+            <action android:name="android.media.action.IMAGE_CAPTURE" />
+        </intent>
+        <intent>
+            <action android:name="android.media.action.IMAGE_CAPTURE_SECURE" />
+        </intent>
+        <intent>
+            <action android:name="android.media.action.VIDEO_CAPTURE" />
+        </intent>
+        <intent>
+            <action android:name="android.intent.action.GET_CONTENT" />
+        </intent>
+    </queries>
+
     <application
         android:name=".MainApp"
         android:allowBackup="false"
-        android:fullBackupContent="@xml/backup_config"
         android:dataExtractionRules="@xml/backup_rules"
+        android:fullBackupContent="@xml/backup_config"
         android:icon="@mipmap/ic_launcher"
         android:installLocation="internalOnly"
         android:label="@string/app_name"
@@ -101,8 +117,11 @@
         android:supportsRtl="true"
         android:theme="@style/Theme.ownCloud.Toolbar"
         android:usesCleartextTraffic="true"
-        tools:replace="android:allowBackup"
-        tools:ignore="UnusedAttribute">
+        tools:ignore="UnusedAttribute"
+        tools:replace="android:allowBackup">
+        <activity
+            android:name="com.nextcloud.ui.composeActivity.ComposeActivity"
+            android:exported="false" />
 
         <uses-library
             android:name="org.apache.http.legacy"
@@ -203,8 +222,8 @@
         <activity
             android:name=".ui.activity.SetupEncryptionActivity"
             android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
-            android:theme="@style/Theme.NoBackground"
-            android:exported="false" />
+            android:exported="false"
+            android:theme="@style/Theme.NoBackground" />
         <activity
             android:name=".ui.activity.ContactsPreferenceActivity"
             android:exported="false"
@@ -217,12 +236,16 @@
             android:theme="@style/Theme.ownCloud.NoActionBar">
             <intent-filter>
                 <action android:name="android.intent.action.SEND" />
+
                 <category android:name="android.intent.category.DEFAULT" />
+
                 <data android:mimeType="*/*" />
             </intent-filter>
             <intent-filter>
                 <action android:name="android.intent.action.SEND_MULTIPLE" />
+
                 <category android:name="android.intent.category.DEFAULT" />
+
                 <data android:mimeType="*/*" />
             </intent-filter>
         </activity>
@@ -236,9 +259,10 @@
             android:theme="@style/Theme.ownCloud.Overlay" />
         <activity
             android:name=".ui.preview.PreviewMediaActivity"
-            android:exported="false"
             android:configChanges="orientation|screenLayout|screenSize|keyboardHidden"
+            android:exported="false"
             android:theme="@style/Theme.ownCloud.Media" />
+
         <service
             android:name=".authentication.AccountAuthenticatorService"
             android:exported="false">
@@ -264,8 +288,8 @@
         </service>
         <service
             android:name="com.nextcloud.client.widget.DashboardWidgetService"
-            android:permission="android.permission.BIND_REMOTEVIEWS"
-            android:exported="true" />
+            android:exported="true"
+            android:permission="android.permission.BIND_REMOTEVIEWS" />
 
         <provider
             android:name=".providers.FileContentProvider"
@@ -303,14 +327,12 @@
                 android:readPermission="false"
                 android:writePermission="false" />
         </provider>
-
         <provider
             android:name=".providers.UsersAndGroupsSearchProvider"
             android:authorities="@string/users_and_groups_search_authority"
             android:enabled="true"
             android:exported="false"
             android:label="@string/share_search" />
-
         <provider
             android:name=".providers.DocumentsStorageProvider"
             android:authorities="@string/document_provider_authority"
@@ -321,9 +343,7 @@
             <intent-filter>
                 <action android:name="android.content.action.DOCUMENTS_PROVIDER" />
             </intent-filter>
-        </provider>
-
-        <!-- new provider used to generate URIs without file:// scheme (forbidden from Android 7) -->
+        </provider> <!-- new provider used to generate URIs without file:// scheme (forbidden from Android 7) -->
         <provider
             android:name="androidx.core.content.FileProvider"
             android:authorities="@string/file_provider_authority"
@@ -338,8 +358,7 @@
             android:authorities="@string/image_cache_provider_authority"
             android:exported="true"
             android:grantUriPermissions="true"
-            android:permission="android.permission.MANAGE_DOCUMENTS" />
-        <!-- Disable WorkManager initialization. Whoever designed this, should pay closer attention -->
+            android:permission="android.permission.MANAGE_DOCUMENTS" /> <!-- Disable WorkManager initialization. Whoever designed this, should pay closer attention -->
         <!-- to "best before" dates in his fridge. -->
         <!-- disable default provider -->
         <provider
@@ -395,12 +414,12 @@
             android:exported="false" />
         <service
             android:name="com.nextcloud.client.jobs.transfer.FileTransferService"
-            android:foregroundServiceType="dataSync"
-            android:exported="false" />
+            android:exported="false"
+            android:foregroundServiceType="dataSync" />
         <service
             android:name="com.nextcloud.client.media.PlayerService"
-            android:foregroundServiceType="mediaPlayback"
-            android:exported="false" />
+            android:exported="false"
+            android:foregroundServiceType="mediaPlayback" />
 
         <activity
             android:name=".ui.activity.PassCodeActivity"
@@ -478,6 +497,7 @@
             <intent-filter>
                 <action android:name="android.intent.action.SEARCH" />
             </intent-filter>
+
             <meta-data
                 android:name="android.app.searchable"
                 android:resource="@xml/users_and_groups_searchable" />
@@ -514,7 +534,6 @@
             android:name="com.nextcloud.client.editimage.EditImageActivity"
             android:exported="false"
             android:theme="@style/Theme.ownCloud.Toolbar.NullBackground" />
-
         <activity
             android:name="com.nmc.android.ui.LauncherActivity"
             android:exported="true"
@@ -525,42 +544,6 @@
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
-
     </application>
 
-    <queries>
-        <package android:name="it.niedermann.nextcloud.deck" />
-        <package android:name="it.niedermann.nextcloud.deck.play" />
-        <package android:name="it.niedermann.nextcloud.deck.dev" />
-        <package android:name="at.bitfire.davdroid"/>
-
-        <intent>
-            <action android:name="android.intent.action.VIEW" />
-            <data android:mimeType="*/*" />
-        </intent>
-        <intent>
-            <action android:name="android.intent.action.SEND" />
-            <data android:mimeType="*/*" />
-        </intent>
-        <intent>
-            <action android:name="android.intent.action.SEND_MULTIPLE" />
-            <data android:mimeType="*/*" />
-        </intent>
-        <intent>
-            <action android:name="android.intent.action.PICK" />
-            <data android:mimeType="*/*" />
-        </intent>
-        <intent>
-            <action android:name="android.media.action.IMAGE_CAPTURE" />
-        </intent>
-        <intent>
-            <action android:name="android.media.action.IMAGE_CAPTURE_SECURE" />
-        </intent>
-        <intent>
-            <action android:name="android.media.action.VIDEO_CAPTURE" />
-        </intent>
-        <intent>
-            <action android:name="android.intent.action.GET_CONTENT" />
-        </intent>
-    </queries>
-</manifest>
+</manifest>

+ 179 - 0
app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt

@@ -0,0 +1,179 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Alper Ozturk
+ * Copyright (C) 2024 Alper Ozturk
+ * Copyright (C) 2024 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.client.assistant
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.nextcloud.client.assistant.repository.AssistantRepositoryType
+import com.owncloud.android.MainApp
+import com.owncloud.android.R
+import com.owncloud.android.lib.resources.assistant.model.Task
+import com.owncloud.android.lib.resources.assistant.model.TaskType
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+
+class AssistantViewModel(private val repository: AssistantRepositoryType) : ViewModel() {
+
+    sealed class State {
+        data object Idle : State()
+        data object Loading : State()
+        data class Error(val messageId: Int) : State()
+        data class TaskCreated(val messageId: Int) : State()
+        data class TaskDeleted(val messageId: Int) : State()
+    }
+
+    private val _state = MutableStateFlow<State>(State.Loading)
+    val state: StateFlow<State> = _state
+
+    private val _selectedTaskType = MutableStateFlow<TaskType?>(null)
+    val selectedTaskType: StateFlow<TaskType?> = _selectedTaskType
+
+    private val _taskTypes = MutableStateFlow<List<TaskType>?>(null)
+    val taskTypes: StateFlow<List<TaskType>?> = _taskTypes
+
+    private var _taskList: List<Task>? = null
+
+    private val _filteredTaskList = MutableStateFlow<List<Task>?>(null)
+    val filteredTaskList: StateFlow<List<Task>?> = _filteredTaskList
+
+    init {
+        getTaskTypes()
+        getTaskList()
+    }
+
+    fun createTask(
+        input: String,
+        type: String
+    ) {
+        viewModelScope.launch(Dispatchers.IO) {
+            val result = repository.createTask(input, type)
+
+            val messageId = if (result.isSuccess) {
+                R.string.assistant_screen_task_create_success_message
+            } else {
+                R.string.assistant_screen_task_create_fail_message
+            }
+
+            _state.update {
+                State.TaskCreated(messageId)
+            }
+        }
+    }
+
+    fun selectTaskType(task: TaskType) {
+        _selectedTaskType.update {
+            filterTaskList(task.id)
+            task
+        }
+    }
+
+    private fun getTaskTypes() {
+        viewModelScope.launch(Dispatchers.IO) {
+            val allTaskType = MainApp.getAppContext().getString(R.string.assistant_screen_all_task_type)
+            val result = arrayListOf(TaskType(null, allTaskType, null))
+            val taskTypesResult = repository.getTaskTypes()
+
+            if (taskTypesResult.isSuccess) {
+                result.addAll(taskTypesResult.resultData.types)
+                _taskTypes.update {
+                    result.toList()
+                }
+
+                _selectedTaskType.update {
+                    result.first()
+                }
+            } else {
+                _state.update {
+                    State.Error(R.string.assistant_screen_task_types_error_state_message)
+                }
+            }
+        }
+    }
+
+    fun getTaskList(appId: String = "assistant", onCompleted: () -> Unit = {}) {
+        viewModelScope.launch(Dispatchers.IO) {
+            val result = repository.getTaskList(appId)
+            if (result.isSuccess) {
+                _taskList = result.resultData.tasks
+
+                filterTaskList(_selectedTaskType.value?.id)
+
+                _state.update {
+                    State.Idle
+                }
+
+                onCompleted()
+            } else {
+                _state.update {
+                    State.Error(R.string.assistant_screen_task_list_error_state_message)
+                }
+            }
+        }
+    }
+
+    fun deleteTask(id: Long) {
+        viewModelScope.launch(Dispatchers.IO) {
+            val result = repository.deleteTask(id)
+
+            val messageId = if (result.isSuccess) {
+                R.string.assistant_screen_task_delete_success_message
+            } else {
+                R.string.assistant_screen_task_delete_fail_message
+            }
+
+            _state.update {
+                State.TaskDeleted(messageId)
+            }
+
+            if (result.isSuccess) {
+                removeTaskFromList(id)
+            }
+        }
+    }
+
+    fun resetState() {
+        _state.update {
+            State.Idle
+        }
+    }
+
+    private fun filterTaskList(taskTypeId: String?) {
+        if (taskTypeId == null) {
+            _filteredTaskList.update {
+                _taskList
+            }
+        } else {
+            _filteredTaskList.update {
+                _taskList?.filter { it.type == taskTypeId }
+            }
+        }
+    }
+
+    private fun removeTaskFromList(id: Long) {
+        _filteredTaskList.update { currentList ->
+            currentList?.filter { it.id != id }
+        }
+    }
+}

+ 279 - 0
app/src/main/java/com/nextcloud/client/assistant/AsssistantScreen.kt

@@ -0,0 +1,279 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Alper Ozturk
+ * Copyright (C) 2024 Alper Ozturk
+ * Copyright (C) 2024 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.client.assistant
+
+import android.app.Activity
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.FloatingActionButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.LinearProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.nextcloud.client.assistant.component.AddTaskAlertDialog
+import com.nextcloud.client.assistant.component.CenterText
+import com.nextcloud.client.assistant.component.TaskTypesRow
+import com.nextcloud.client.assistant.component.TaskView
+import com.nextcloud.client.assistant.repository.AssistantMockRepository
+import com.nextcloud.ui.composeActivity.ComposeActivity
+import com.nextcloud.ui.composeComponents.alertDialog.SimpleAlertDialog
+import com.owncloud.android.R
+import com.owncloud.android.lib.resources.assistant.model.Task
+import com.owncloud.android.lib.resources.assistant.model.TaskType
+import com.owncloud.android.utils.DisplayUtils
+import kotlinx.coroutines.delay
+
+@Suppress("LongMethod")
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun AssistantScreen(viewModel: AssistantViewModel, activity: Activity) {
+    val state by viewModel.state.collectAsState()
+    val selectedTaskType by viewModel.selectedTaskType.collectAsState()
+    val filteredTaskList by viewModel.filteredTaskList.collectAsState()
+    val taskTypes by viewModel.taskTypes.collectAsState()
+    var showAddTaskAlertDialog by remember { mutableStateOf(false) }
+    var showDeleteTaskAlertDialog by remember { mutableStateOf(false) }
+    var taskIdToDeleted: Long? by remember {
+        mutableStateOf(null)
+    }
+    val pullRefreshState = rememberPullToRefreshState()
+
+    @Suppress("MagicNumber")
+    if (pullRefreshState.isRefreshing) {
+        LaunchedEffect(true) {
+            delay(1500)
+            viewModel.getTaskList(onCompleted = {
+                pullRefreshState.endRefresh()
+            })
+        }
+    }
+
+    Box(Modifier.nestedScroll(pullRefreshState.nestedScrollConnection)) {
+        if (state == AssistantViewModel.State.Loading || pullRefreshState.isRefreshing) {
+            CenterText(text = stringResource(id = R.string.assistant_screen_loading))
+        } else {
+            if (filteredTaskList.isNullOrEmpty()) {
+                EmptyTaskList(selectedTaskType, taskTypes, viewModel)
+            } else {
+                AssistantContent(
+                    filteredTaskList!!,
+                    taskTypes,
+                    selectedTaskType,
+                    viewModel,
+                    showDeleteTaskAlertDialog = { taskId ->
+                        taskIdToDeleted = taskId
+                        showDeleteTaskAlertDialog = true
+                    }
+                )
+            }
+        }
+
+        if (pullRefreshState.isRefreshing) {
+            LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
+        } else {
+            LinearProgressIndicator(progress = { pullRefreshState.progress }, modifier = Modifier.fillMaxWidth())
+        }
+
+        if (selectedTaskType?.name != stringResource(id = R.string.assistant_screen_all_task_type)) {
+            FloatingActionButton(
+                modifier = Modifier
+                    .align(Alignment.BottomEnd)
+                    .padding(16.dp),
+                onClick = {
+                    showAddTaskAlertDialog = true
+                }
+            ) {
+                Icon(Icons.Filled.Add, "Add Task Icon")
+            }
+        }
+    }
+
+    ScreenState(state, activity, viewModel)
+
+    if (showDeleteTaskAlertDialog) {
+        taskIdToDeleted?.let { id ->
+            SimpleAlertDialog(
+                title = stringResource(id = R.string.assistant_screen_delete_task_alert_dialog_title),
+                description = stringResource(id = R.string.assistant_screen_delete_task_alert_dialog_description),
+                dismiss = { showDeleteTaskAlertDialog = false },
+                onComplete = { viewModel.deleteTask(id) }
+            )
+        }
+    }
+
+    if (showAddTaskAlertDialog) {
+        selectedTaskType?.let { taskType ->
+            AddTaskAlertDialog(
+                title = taskType.name,
+                description = taskType.description,
+                addTask = { input ->
+                    taskType.id?.let {
+                        viewModel.createTask(input = input, type = it)
+                    }
+                },
+                dismiss = {
+                    showAddTaskAlertDialog = false
+                }
+            )
+        }
+    }
+}
+
+@Composable
+private fun ScreenState(
+    state: AssistantViewModel.State,
+    activity: Activity,
+    viewModel: AssistantViewModel
+) {
+    val messageId: Int? = when (state) {
+        is AssistantViewModel.State.Error -> {
+            state.messageId
+        }
+
+        is AssistantViewModel.State.TaskCreated -> {
+            state.messageId
+        }
+
+        is AssistantViewModel.State.TaskDeleted -> {
+            state.messageId
+        }
+        else -> {
+            null
+        }
+    }
+
+    messageId?.let {
+        DisplayUtils.showSnackMessage(
+            activity,
+            stringResource(id = messageId)
+        )
+
+        viewModel.resetState()
+    }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun AssistantContent(
+    taskList: List<Task>,
+    taskTypes: List<TaskType>?,
+    selectedTaskType: TaskType?,
+    viewModel: AssistantViewModel,
+    showDeleteTaskAlertDialog: (Long) -> Unit
+) {
+    LazyColumn(
+        modifier = Modifier
+            .fillMaxSize()
+            .padding(16.dp)
+    ) {
+        stickyHeader {
+            TaskTypesRow(selectedTaskType, data = taskTypes) { task ->
+                viewModel.selectTaskType(task)
+            }
+
+            Spacer(modifier = Modifier.height(8.dp))
+        }
+
+        items(taskList) { task ->
+            TaskView(task, showDeleteTaskAlertDialog = { showDeleteTaskAlertDialog(task.id) })
+            Spacer(modifier = Modifier.height(8.dp))
+        }
+    }
+}
+
+@Composable
+private fun EmptyTaskList(selectedTaskType: TaskType?, taskTypes: List<TaskType>?, viewModel: AssistantViewModel) {
+    val text = if (selectedTaskType?.name == stringResource(id = R.string.assistant_screen_all_task_type)) {
+        stringResource(id = R.string.assistant_screen_no_task_available_for_all_task_filter_text)
+    } else {
+        stringResource(
+            id = R.string.assistant_screen_no_task_available_text,
+            selectedTaskType?.name ?: ""
+        )
+    }
+
+    Column(
+        modifier = Modifier
+            .fillMaxSize()
+            .padding(16.dp)
+    ) {
+        TaskTypesRow(selectedTaskType, data = taskTypes) { task ->
+            viewModel.selectTaskType(task)
+        }
+
+        Spacer(modifier = Modifier.height(8.dp))
+
+        CenterText(text = text)
+    }
+}
+
+@Composable
+@Preview
+private fun AssistantScreenPreview() {
+    val mockRepository = AssistantMockRepository()
+    MaterialTheme(
+        content = {
+            AssistantScreen(
+                viewModel = AssistantViewModel(repository = mockRepository),
+                activity = ComposeActivity()
+            )
+        }
+    )
+}
+
+@Composable
+@Preview
+private fun AssistantEmptyScreenPreview() {
+    val mockRepository = AssistantMockRepository(giveEmptyTasks = true)
+    MaterialTheme(
+        content = {
+            AssistantScreen(
+                viewModel = AssistantViewModel(repository = mockRepository),
+                activity = ComposeActivity()
+            )
+        }
+    )
+}

+ 74 - 0
app/src/main/java/com/nextcloud/client/assistant/component/AddTaskAlertDialog.kt

@@ -0,0 +1,74 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Alper Ozturk
+ * Copyright (C) 2024 Alper Ozturk
+ * Copyright (C) 2024 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.client.assistant.component
+
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.tooling.preview.Preview
+import com.nextcloud.ui.composeComponents.alertDialog.SimpleAlertDialog
+import com.owncloud.android.R
+
+@Composable
+fun AddTaskAlertDialog(title: String?, description: String?, addTask: (String) -> Unit, dismiss: () -> Unit) {
+    var input by remember {
+        mutableStateOf("")
+    }
+
+    SimpleAlertDialog(
+        title = title ?: "",
+        description = description ?: "",
+        dismiss = { dismiss() },
+        onComplete = {
+            addTask(input)
+        },
+        content = {
+            TextField(
+                placeholder = {
+                    Text(
+                        text = stringResource(
+                            id = R.string.assistant_screen_create_task_alert_dialog_input_field_placeholder
+                        )
+                    )
+                },
+                keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
+                value = input,
+                onValueChange = {
+                    input = it
+                }
+            )
+        }
+    )
+}
+
+@Composable
+@Preview
+private fun AddTaskAlertDialogPreview() {
+    AddTaskAlertDialog(title = "Title", description = "Description", addTask = { }, dismiss = {})
+}

+ 42 - 0
app/src/main/java/com/nextcloud/client/assistant/component/CenterText.kt

@@ -0,0 +1,42 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Alper Ozturk
+ * Copyright (C) 2024 Alper Ozturk
+ * Copyright (C) 2024 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.client.assistant.component
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.sp
+
+@Composable
+fun CenterText(text: String) {
+    Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+        Text(
+            text = text,
+            fontSize = 18.sp,
+            textAlign = TextAlign.Center
+        )
+    }
+}

+ 65 - 0
app/src/main/java/com/nextcloud/client/assistant/component/TaskTypesRow.kt

@@ -0,0 +1,65 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Alper Ozturk
+ * Copyright (C) 2024 Alper Ozturk
+ * Copyright (C) 2024 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.client.assistant.component
+
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.FilledTonalButton
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import com.owncloud.android.lib.resources.assistant.model.TaskType
+
+@Composable
+fun TaskTypesRow(selectedTaskType: TaskType?, data: List<TaskType>?, selectTaskType: (TaskType) -> Unit) {
+    Row(
+        modifier = Modifier
+            .fillMaxWidth()
+            .horizontalScroll(rememberScrollState())
+    ) {
+        data?.forEach { taskType ->
+            taskType.name?.let { taskTypeName ->
+                FilledTonalButton(
+                    onClick = { selectTaskType(taskType) },
+                    colors = ButtonDefaults.buttonColors(
+                        containerColor = if (selectedTaskType?.id == taskType.id) {
+                            Color.Unspecified
+                        } else {
+                            Color.Gray
+                        }
+                    )
+                ) {
+                    Text(text = taskTypeName)
+                }
+
+                Spacer(modifier = Modifier.padding(end = 8.dp))
+            }
+        }
+    }
+}

+ 174 - 0
app/src/main/java/com/nextcloud/client/assistant/component/TaskView.kt

@@ -0,0 +1,174 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Alper Ozturk
+ * Copyright (C) 2024 Alper Ozturk
+ * Copyright (C) 2024 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.client.assistant.component
+
+import androidx.compose.animation.animateContentSize
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.spring
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.nextcloud.ui.composeComponents.bottomSheet.MoreActionsBottomSheet
+import com.owncloud.android.R
+import com.owncloud.android.lib.resources.assistant.model.Task
+
+@OptIn(ExperimentalFoundationApi::class)
+@Suppress("LongMethod", "MagicNumber")
+@Composable
+fun TaskView(
+    task: Task,
+    showDeleteTaskAlertDialog: (Long) -> Unit
+) {
+    var expanded by remember { mutableStateOf(false) }
+    var showMoreActionsBottomSheet by remember { mutableStateOf(false) }
+
+    Column(
+        modifier = Modifier
+            .fillMaxWidth()
+            .clip(RoundedCornerShape(16.dp))
+            .background(MaterialTheme.colorScheme.primary)
+            .combinedClickable(onClick = {
+                expanded = !expanded
+            }, onLongClick = {
+                showMoreActionsBottomSheet = true
+            })
+            .padding(start = 8.dp)
+    ) {
+        Spacer(modifier = Modifier.height(8.dp))
+
+        task.input?.let {
+            Text(
+                text = it,
+                color = Color.White,
+                fontSize = 18.sp
+            )
+        }
+
+        Spacer(modifier = Modifier.height(16.dp))
+
+        task.output?.let {
+            HorizontalDivider(modifier = Modifier.padding(horizontal = 4.dp, vertical = 8.dp))
+
+            Text(
+                text = if (expanded) it else it.take(100) + "...",
+                fontSize = 12.sp,
+                color = Color.White,
+                modifier = Modifier
+                    .animateContentSize(
+                        animationSpec = spring(
+                            dampingRatio = Spring.DampingRatioLowBouncy,
+                            stiffness = Spring.StiffnessLow
+                        )
+                    )
+            )
+        }
+
+        if ((task.output?.length ?: 0) >= 100) {
+            Text(
+                text = if (!expanded) {
+                    stringResource(id = R.string.assistant_screen_task_view_show_more)
+                } else {
+                    stringResource(id = R.string.assistant_screen_task_view_show_less)
+                },
+                textAlign = TextAlign.End,
+                color = Color.White,
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .padding(16.dp)
+            )
+        }
+
+        if (showMoreActionsBottomSheet) {
+            val bottomSheetAction = listOf(
+                Triple(
+                    R.drawable.ic_delete,
+                    R.string.assistant_screen_task_more_actions_bottom_sheet_delete_action
+                ) {
+                    showDeleteTaskAlertDialog(task.id)
+                }
+            )
+
+            MoreActionsBottomSheet(
+                title = task.input,
+                actions = bottomSheetAction,
+                dismiss = { showMoreActionsBottomSheet = false }
+            )
+        }
+    }
+}
+
+@Preview
+@Composable
+private fun TaskViewPreview() {
+    val output =
+        "Lorem Ipsum is simply dummy text of the printing and " +
+            "typesetting industry. Lorem Ipsum has been the " +
+            "industry's standard dummy text ever since the 1500s, " +
+            "when an unknown printer took a galley of type and " +
+            "scrambled it to make a type specimen book. " +
+            "It has survived not only five centuries, but also " +
+            "the leap into electronic typesetting, remaining" +
+            " essentially unchanged. It wLorem Ipsum is simply dummy" +
+            " text of the printing and typesetting industry. " +
+            "Lorem Ipsum has been the industry's standard dummy " +
+            "text ever since the 1500s, when an unknown printer took a" +
+            " galley of type and scrambled it to make a type specimen book. " +
+            "It has survived not only five centuries, but also the leap " +
+            "into electronic typesetting, remaining essentially unchanged."
+
+    TaskView(
+        task = Task(
+            1,
+            "Free Prompt",
+            0,
+            "1",
+            "1",
+            "Give me text",
+            output,
+            "",
+            ""
+        )
+    ) {
+    }
+}

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

@@ -0,0 +1,93 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Alper Ozturk
+ * Copyright (C) 2024 Alper Ozturk
+ * Copyright (C) 2024 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.client.assistant.repository
+
+import com.owncloud.android.lib.common.operations.RemoteOperationResult
+import com.owncloud.android.lib.resources.assistant.model.Task
+import com.owncloud.android.lib.resources.assistant.model.TaskList
+import com.owncloud.android.lib.resources.assistant.model.TaskType
+import com.owncloud.android.lib.resources.assistant.model.TaskTypes
+
+class AssistantMockRepository(private val giveEmptyTasks: Boolean = false) : AssistantRepositoryType {
+    override fun getTaskTypes(): RemoteOperationResult<TaskTypes> {
+        return RemoteOperationResult<TaskTypes>(RemoteOperationResult.ResultCode.OK).apply {
+            resultData = TaskTypes(
+                listOf(
+                    TaskType("1", "FreePrompt", "You can create free prompt text"),
+                    TaskType("2", "Generate Headline", "You can create generate headline text")
+                )
+            )
+        }
+    }
+
+    override fun createTask(input: String, type: String): RemoteOperationResult<Void> {
+        return RemoteOperationResult<Void>(RemoteOperationResult.ResultCode.OK)
+    }
+
+    override fun getTaskList(appId: String): RemoteOperationResult<TaskList> {
+        val taskList = if (giveEmptyTasks) {
+            TaskList(listOf())
+        } else {
+            TaskList(
+                listOf(
+                    Task(
+                        1,
+                        "FreePrompt",
+                        null,
+                        "12",
+                        "",
+                        "Give me some text",
+                        "Lorem Ipsum is simply dummy text of the printing and typesetting industry. " +
+                            "Lorem Ipsum has been the industry's standard dummy text ever since the 1500s," +
+                            " when an unknown printer took a galley of type and scrambled it to make a type" +
+                            " specimen book. It has survived not only five centuries, " +
+                            "but also the leap into electronic typesetting, remaining essentially unchanged." +
+                            " It was popularised in the 1960s with the release of Letraset sheets containing " +
+                            "Lorem Ipsum passages, and more recently with desktop publishing software like Aldus" +
+                            " PageMaker including versions of Lorem Ipsum",
+                        "",
+                        ""
+                    ),
+                    Task(
+                        2,
+                        "GenerateHeadline",
+                        null,
+                        "12",
+                        "",
+                        "Give me some text 2",
+                        "Lorem Ipsum is simply dummy text of the printing and typesetting industry.",
+                        "",
+                        ""
+                    )
+                )
+            )
+        }
+
+        return RemoteOperationResult<TaskList>(RemoteOperationResult.ResultCode.OK).apply {
+            resultData = taskList
+        }
+    }
+
+    override fun deleteTask(id: Long): RemoteOperationResult<Void> {
+        return RemoteOperationResult<Void>(RemoteOperationResult.ResultCode.OK)
+    }
+}

+ 53 - 0
app/src/main/java/com/nextcloud/client/assistant/repository/AssistantRepository.kt

@@ -0,0 +1,53 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Alper Ozturk
+ * Copyright (C) 2024 Alper Ozturk
+ * Copyright (C) 2024 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.client.assistant.repository
+
+import com.nextcloud.common.NextcloudClient
+import com.owncloud.android.lib.common.operations.RemoteOperationResult
+import com.owncloud.android.lib.resources.assistant.CreateTaskRemoteOperation
+import com.owncloud.android.lib.resources.assistant.DeleteTaskRemoteOperation
+import com.owncloud.android.lib.resources.assistant.GetTaskListRemoteOperation
+import com.owncloud.android.lib.resources.assistant.GetTaskTypesRemoteOperation
+import com.owncloud.android.lib.resources.assistant.model.TaskList
+import com.owncloud.android.lib.resources.assistant.model.TaskTypes
+
+class AssistantRepository(private val client: NextcloudClient) : AssistantRepositoryType {
+
+    override fun getTaskTypes(): RemoteOperationResult<TaskTypes> {
+        return GetTaskTypesRemoteOperation().execute(client)
+    }
+
+    override fun createTask(
+        input: String,
+        type: String
+    ): RemoteOperationResult<Void> {
+        return CreateTaskRemoteOperation(input, type).execute(client)
+    }
+
+    override fun getTaskList(appId: String): RemoteOperationResult<TaskList> {
+        return GetTaskListRemoteOperation(appId).execute(client)
+    }
+
+    override fun deleteTask(id: Long): RemoteOperationResult<Void> {
+        return DeleteTaskRemoteOperation(id).execute(client)
+    }
+}

+ 39 - 0
app/src/main/java/com/nextcloud/client/assistant/repository/AssistantRepositoryType.kt

@@ -0,0 +1,39 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Alper Ozturk
+ * Copyright (C) 2024 Alper Ozturk
+ * Copyright (C) 2024 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.client.assistant.repository
+
+import com.owncloud.android.lib.common.operations.RemoteOperationResult
+import com.owncloud.android.lib.resources.assistant.model.TaskList
+import com.owncloud.android.lib.resources.assistant.model.TaskTypes
+
+interface AssistantRepositoryType {
+    fun getTaskTypes(): RemoteOperationResult<TaskTypes>
+
+    fun createTask(
+        input: String,
+        type: String
+    ): RemoteOperationResult<Void>
+
+    fun getTaskList(appId: String): RemoteOperationResult<TaskList>
+
+    fun deleteTask(id: Long): RemoteOperationResult<Void>
+}

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

@@ -71,7 +71,8 @@ import com.owncloud.android.db.ProviderMeta
         AutoMigration(from = 74, to = 75),
         AutoMigration(from = 75, to = 76),
         AutoMigration(from = 76, to = 77),
-        AutoMigration(from = 77, to = 78)
+        AutoMigration(from = 77, to = 78),
+        AutoMigration(from = 78, to = 79, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class)
     ],
     exportSchema = true
 )

+ 2 - 0
app/src/main/java/com/nextcloud/client/database/entity/CapabilityEntity.kt

@@ -32,6 +32,8 @@ data class CapabilityEntity(
     @PrimaryKey(autoGenerate = true)
     @ColumnInfo(name = ProviderTableMeta._ID)
     val id: Int?,
+    @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_ASSISTANT)
+    val assistant: Int?,
     @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_ACCOUNT_NAME)
     val accountName: String?,
     @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_VERSION_MAYOR)

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

@@ -41,6 +41,7 @@ import com.nextcloud.client.widget.DashboardWidgetService;
 import com.nextcloud.ui.ChooseAccountDialogFragment;
 import com.nextcloud.ui.ImageDetailFragment;
 import com.nextcloud.ui.SetStatusDialogFragment;
+import com.nextcloud.ui.composeActivity.ComposeActivity;
 import com.nextcloud.ui.fileactions.FileActionsBottomSheet;
 import com.nmc.android.ui.LauncherActivity;
 import com.owncloud.android.MainApp;
@@ -199,6 +200,9 @@ abstract class ComponentsModule {
     @ContributesAndroidInjector
     abstract CommunityActivity participateActivity();
 
+    @ContributesAndroidInjector
+    abstract ComposeActivity composeActivity();
+
     @ContributesAndroidInjector
     abstract PassCodeActivity passCodeActivity();
 

+ 133 - 0
app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt

@@ -0,0 +1,133 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Alper Ozturk
+ * Copyright (C) 2024 Alper Ozturk
+ * Copyright (C) 2024 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.ui.composeActivity
+
+import android.content.Context
+import android.os.Bundle
+import android.view.MenuItem
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import com.nextcloud.client.assistant.AssistantScreen
+import com.nextcloud.client.assistant.AssistantViewModel
+import com.nextcloud.client.assistant.repository.AssistantRepository
+import com.nextcloud.common.NextcloudClient
+import com.nextcloud.common.User
+import com.nextcloud.utils.extensions.getSerializableArgument
+import com.owncloud.android.R
+import com.owncloud.android.databinding.ActivityComposeBinding
+import com.owncloud.android.lib.common.OwnCloudClientFactory
+import com.owncloud.android.lib.common.accounts.AccountUtils
+import com.owncloud.android.lib.common.utils.Log_OC
+import com.owncloud.android.ui.activity.DrawerActivity
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
+class ComposeActivity : DrawerActivity() {
+
+    lateinit var binding: ActivityComposeBinding
+    private var menuItemId: Int? = null
+
+    companion object {
+        const val DESTINATION = "DESTINATION"
+        const val TITLE = "TITLE"
+        const val MENU_ITEM = "MENU_ITEM"
+    }
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        binding = ActivityComposeBinding.inflate(layoutInflater)
+        setContentView(binding.root)
+
+        val destination = intent.getSerializableArgument(DESTINATION, ComposeDestination::class.java)
+        val titleId = intent.getIntExtra(TITLE, R.string.empty)
+        menuItemId = intent.getIntExtra(MENU_ITEM, -1)
+
+        setupToolbar()
+        updateActionBarTitleAndHomeButtonByString(getString(titleId))
+
+        if (menuItemId != -1) {
+            setupDrawer(menuItemId!!)
+        }
+
+        binding.composeView.setContent {
+            MaterialTheme(
+                colorScheme = viewThemeUtils.getColorScheme(this),
+                content = {
+                    Content(destination, storageManager.user, this)
+                }
+            )
+        }
+    }
+
+    override fun onResume() {
+        super.onResume()
+        if (menuItemId != -1) {
+            setDrawerMenuItemChecked(R.id.nav_assistant)
+        }
+    }
+
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        return when (item.itemId) {
+            android.R.id.home -> {
+                if (isDrawerOpen) closeDrawer() else openDrawer()
+                true
+            }
+            else -> super.onOptionsItemSelected(item)
+        }
+    }
+
+    @Composable
+    private fun Content(destination: ComposeDestination?, user: User, context: Context) {
+        var nextcloudClient by remember { mutableStateOf<NextcloudClient?>(null) }
+
+        LaunchedEffect(Unit) {
+            nextcloudClient = getNextcloudClient(user, context)
+        }
+
+        if (destination == ComposeDestination.AssistantScreen) {
+            nextcloudClient?.let { client ->
+                AssistantScreen(
+                    viewModel = AssistantViewModel(
+                        repository = AssistantRepository(client)
+                    ),
+                    activity = this
+                )
+            }
+        }
+    }
+
+    private suspend fun getNextcloudClient(user: User, context: Context): NextcloudClient? {
+        return withContext(Dispatchers.IO) {
+            try {
+                OwnCloudClientFactory.createNextcloudClient(user, context)
+            } catch (e: AccountUtils.AccountNotFoundException) {
+                Log_OC.e(this, "Error caught at init of createNextcloudClient", e)
+                null
+            }
+        }
+    }
+}

+ 28 - 0
app/src/main/java/com/nextcloud/ui/composeActivity/ComposeDestination.kt

@@ -0,0 +1,28 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Alper Ozturk
+ * Copyright (C) 2024 Alper Ozturk
+ * Copyright (C) 2024 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.ui.composeActivity
+
+import java.io.Serializable
+
+enum class ComposeDestination : Serializable {
+    AssistantScreen
+}

+ 93 - 0
app/src/main/java/com/nextcloud/ui/composeComponents/alertDialog/SimpleAlertDialog.kt

@@ -0,0 +1,93 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Alper Ozturk
+ * Copyright (C) 2024 Alper Ozturk
+ * Copyright (C) 2024 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.ui.composeComponents.alertDialog
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.FilledTonalButton
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.owncloud.android.R
+
+@Suppress("LongParameterList")
+@Composable
+fun SimpleAlertDialog(
+    title: String,
+    description: String?,
+    heightFraction: Float? = null,
+    content: @Composable (() -> Unit)? = null,
+    onComplete: () -> Unit,
+    dismiss: () -> Unit
+) {
+    val modifier = if (heightFraction != null) {
+        Modifier
+            .fillMaxWidth()
+            .fillMaxHeight(heightFraction)
+    } else {
+        Modifier.fillMaxWidth()
+    }
+
+    AlertDialog(
+        onDismissRequest = { dismiss() },
+        title = {
+            Text(text = title)
+        },
+        text = {
+            Column(modifier = modifier) {
+                description?.let {
+                    Text(text = description)
+                }
+
+                content?.let {
+                    Spacer(modifier = Modifier.height(16.dp))
+
+                    content()
+                }
+            }
+        },
+        confirmButton = {
+            FilledTonalButton(onClick = {
+                onComplete()
+                dismiss()
+            }) {
+                Text(
+                    stringResource(id = R.string.common_ok)
+                )
+            }
+        },
+        dismissButton = {
+            TextButton(onClick = { dismiss() }) {
+                Text(
+                    stringResource(id = R.string.common_cancel)
+                )
+            }
+        }
+    )
+}

+ 100 - 0
app/src/main/java/com/nextcloud/ui/composeComponents/bottomSheet/MoreActionsBottomSheet.kt

@@ -0,0 +1,100 @@
+package com.nextcloud.ui.composeComponents.bottomSheet
+
+import android.annotation.SuppressLint
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme.colorScheme
+import androidx.compose.material3.ModalBottomSheet
+import androidx.compose.material3.Text
+import androidx.compose.material3.rememberModalBottomSheetState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import kotlinx.coroutines.launch
+
+@SuppressLint("ResourceAsColor")
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun MoreActionsBottomSheet(
+    title: String? = null,
+    actions: List<Triple<Int, Int, () -> Unit>>,
+    dismiss: () -> Unit
+) {
+    val sheetState = rememberModalBottomSheetState()
+    val scope = rememberCoroutineScope()
+
+    ModalBottomSheet(
+        modifier = Modifier.padding(top = 32.dp),
+        onDismissRequest = {
+            dismiss()
+        },
+        sheetState = sheetState
+    ) {
+        Column(
+            horizontalAlignment = Alignment.Start,
+            verticalArrangement = Arrangement.Top,
+            modifier = Modifier
+                .fillMaxWidth()
+                .padding(all = 8.dp)
+        ) {
+            title?.let {
+                Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxWidth()) {
+                    Text(text = title, fontSize = 18.sp)
+                }
+            }
+
+            Spacer(modifier = Modifier.height(16.dp))
+
+            actions.forEach { action ->
+                Row(
+                    modifier = Modifier
+                        .fillMaxWidth()
+                        .clickable {
+                            scope
+                                .launch { sheetState.hide() }
+                                .invokeOnCompletion {
+                                    if (!sheetState.isVisible) {
+                                        action.third()
+                                        dismiss()
+                                    }
+                                }
+                        }
+                        .padding(all = 16.dp),
+                    verticalAlignment = Alignment.CenterVertically
+                ) {
+                    Icon(
+                        painter = painterResource(id = action.first),
+                        contentDescription = "action icon",
+                        tint = colorScheme.primary,
+                        modifier = Modifier.size(20.dp)
+                    )
+
+                    Spacer(modifier = Modifier.width(16.dp))
+
+                    Text(
+                        text = stringResource(action.second),
+                        fontSize = 16.sp
+                    )
+                }
+            }
+
+            Spacer(modifier = Modifier.height(32.dp))
+        }
+    }
+}

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

@@ -2005,6 +2005,7 @@ public class FileDataStorageManager {
                           capability.getUserStatusSupportsEmoji().getValue());
         contentValues.put(ProviderTableMeta.CAPABILITIES_FILES_LOCKING_VERSION,
                           capability.getFilesLockingVersion());
+        contentValues.put(ProviderTableMeta.CAPABILITIES_ASSISTANT, capability.getAssistant().getValue());
         contentValues.put(ProviderTableMeta.CAPABILITIES_GROUPFOLDERS, capability.getGroupfolders().getValue());
         contentValues.put(ProviderTableMeta.CAPABILITIES_DROP_ACCOUNT, capability.getDropAccount().getValue());
         contentValues.put(ProviderTableMeta.CAPABILITIES_SECURITY_GUARD, capability.getSecurityGuard().getValue());
@@ -2173,6 +2174,7 @@ public class FileDataStorageManager {
                 getBoolean(cursor, ProviderTableMeta.CAPABILITIES_USER_STATUS_SUPPORTS_EMOJI));
             capability.setFilesLockingVersion(
                 getString(cursor, ProviderTableMeta.CAPABILITIES_FILES_LOCKING_VERSION));
+            capability.setAssistant(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_ASSISTANT));
             capability.setGroupfolders(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_GROUPFOLDERS));
             capability.setDropAccount(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_DROP_ACCOUNT));
             capability.setSecurityGuard(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_SECURITY_GUARD));

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

@@ -35,7 +35,7 @@ import java.util.List;
  */
 public class ProviderMeta {
     public static final String DB_NAME = "filelist";
-    public static final int DB_VERSION = 78;
+    public static final int DB_VERSION = 79;
 
     private ProviderMeta() {
         // No instance
@@ -265,6 +265,7 @@ public class ProviderMeta {
         public static final String CAPABILITIES_ETAG = "etag";
         public static final String CAPABILITIES_USER_STATUS = "user_status";
         public static final String CAPABILITIES_USER_STATUS_SUPPORTS_EMOJI = "user_status_supports_emoji";
+        public static final String CAPABILITIES_ASSISTANT = "assistant";
         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";

+ 1 - 1
app/src/main/java/com/owncloud/android/ui/activities/ActivitiesActivity.java

@@ -61,7 +61,7 @@ import static com.owncloud.android.ui.activity.FileActivity.EXTRA_USER;
 public class ActivitiesActivity extends DrawerActivity implements ActivityListInterface, ActivitiesContract.View {
     private static final String TAG = ActivitiesActivity.class.getSimpleName();
 
-    private ActivityListLayoutBinding binding;
+    ActivityListLayoutBinding binding;
     private ActivityListAdapter adapter;
     private int lastGiven;
     private boolean isLoadingActivities;

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

@@ -42,7 +42,6 @@ open class CommunityActivity : DrawerActivity() {
 
         setupToolbar()
         updateActionBarTitleAndHomeButtonByString(getString(R.string.drawer_community))
-
         setupDrawer(R.id.nav_community)
         binding.communityReleaseCandidateText.movementMethod = LinkMovementMethod.getInstance()
         setupContributeForumView()

+ 35 - 14
app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java

@@ -74,6 +74,8 @@ import com.nextcloud.client.preferences.AppPreferences;
 import com.nextcloud.common.NextcloudClient;
 import com.nextcloud.java.util.Optional;
 import com.nextcloud.ui.ChooseAccountDialogFragment;
+import com.nextcloud.ui.composeActivity.ComposeActivity;
+import com.nextcloud.ui.composeActivity.ComposeDestination;
 import com.owncloud.android.MainApp;
 import com.owncloud.android.R;
 import com.owncloud.android.authentication.PassCodeManager;
@@ -123,6 +125,7 @@ import org.greenrobot.eventbus.ThreadMode;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 
 import javax.inject.Inject;
@@ -357,15 +360,22 @@ public abstract class DrawerActivity extends ToolbarActivity
         if (getResources().getBoolean(R.bool.is_branded_client) || !preferences.isShowEcosystemApps()) {
             ecosystemApps.setVisibility(View.GONE);
         } else {
-            LinearLayout[] views = {
-                ecosystemApps.findViewById(R.id.drawer_ecosystem_notes),
-                ecosystemApps.findViewById(R.id.drawer_ecosystem_talk),
-                ecosystemApps.findViewById(R.id.drawer_ecosystem_more)
-            };
+            LinearLayout notesView = ecosystemApps.findViewById(R.id.drawer_ecosystem_notes);
+            LinearLayout talkView = ecosystemApps.findViewById(R.id.drawer_ecosystem_talk);
+            LinearLayout moreView = ecosystemApps.findViewById(R.id.drawer_ecosystem_more);
+            LinearLayout assistantView = ecosystemApps.findViewById(R.id.drawer_ecosystem_assistant);
+
+            notesView.setOnClickListener(v -> openAppOrStore("it.niedermann.owncloud.notes"));
+            talkView.setOnClickListener(v -> openAppOrStore("com.nextcloud.talk2"));
+            moreView.setOnClickListener(v -> openAppStore("Nextcloud", true));
+            assistantView.setOnClickListener(v -> startComposeActivity(ComposeDestination.AssistantScreen, R.string.assistant_screen_top_bar_title, -1));
+            if (getCapabilities() != null && getCapabilities().getAssistant().isTrue()) {
+                assistantView.setVisibility(View.VISIBLE);
+            } else {
+                assistantView.setVisibility(View.GONE);
+            }
 
-            views[0].setOnClickListener(v -> openAppOrStore("it.niedermann.owncloud.notes"));
-            views[1].setOnClickListener(v -> openAppOrStore("com.nextcloud.talk2"));
-            views[2].setOnClickListener(v -> openAppStore("Nextcloud", true));
+            List<LinearLayout> views = Arrays.asList(notesView, talkView, moreView, assistantView);
 
             int iconColor;
             if (Hct.fromInt(primaryColor).getTone() < 80.0) {
@@ -373,6 +383,7 @@ public abstract class DrawerActivity extends ToolbarActivity
             } else {
                 iconColor = getColor(R.color.grey_800_transparent);
             }
+
             for (LinearLayout view : views) {
                 ImageView imageView = (ImageView) view.getChildAt(0);
                 imageView.setImageTintList(ColorStateList.valueOf(iconColor));
@@ -404,8 +415,8 @@ public abstract class DrawerActivity extends ToolbarActivity
     }
 
     /**
-     * Open app store page of specified app or search for specified string.
-     * Will attempt to open browser when no app store is available.
+     * Open app store page of specified app or search for specified string. Will attempt to open browser when no app
+     * store is available.
      *
      * @param string packageName or url-encoded search string
      * @param search false -> show app corresponding to packageName; true -> open search for string
@@ -467,7 +478,7 @@ public abstract class DrawerActivity extends ToolbarActivity
         DrawerMenuUtil.filterTrashbinMenuItem(menu, capability);
         DrawerMenuUtil.filterActivityMenuItem(menu, capability);
         DrawerMenuUtil.filterGroupfoldersMenuItem(menu, capability);
-
+        DrawerMenuUtil.filterAssistantMenuItem(menu, capability, getResources());
         DrawerMenuUtil.setupHomeMenuItem(menu, getResources());
 
         DrawerMenuUtil.removeMenuItem(menu, R.id.nav_community,
@@ -535,6 +546,8 @@ public abstract class DrawerActivity extends ToolbarActivity
             startSharedSearch(menuItem);
         } else if (itemId == R.id.nav_recently_modified) {
             startRecentlyModifiedSearch(menuItem);
+        } else if (itemId == R.id.nav_assistant) {
+            startComposeActivity(ComposeDestination.AssistantScreen, R.string.assistant_screen_top_bar_title, itemId);
         } else if (itemId == R.id.nav_groupfolders) {
             MainApp.showOnlyFilesOnDevice(false);
             Intent intent = new Intent(getApplicationContext(), FileDisplayActivity.class);
@@ -553,6 +566,14 @@ public abstract class DrawerActivity extends ToolbarActivity
         }
     }
 
+    private void startComposeActivity(ComposeDestination destination, int titleId, int menuItemId) {
+        Intent composeActivity = new Intent(getApplicationContext(), ComposeActivity.class);
+        composeActivity.putExtra(ComposeActivity.DESTINATION, destination);
+        composeActivity.putExtra(ComposeActivity.TITLE, titleId);
+        composeActivity.putExtra(ComposeActivity.MENU_ITEM, menuItemId);
+        startActivity(composeActivity);
+    }
+
     private void startActivity(Class<? extends Activity> activity) {
         startActivity(new Intent(getApplicationContext(), activity));
     }
@@ -692,8 +713,8 @@ public abstract class DrawerActivity extends ToolbarActivity
     /**
      * Enable or disable interaction with all drawers.
      *
-     * @param lockMode The new lock mode for the given drawer. One of {@link DrawerLayout#LOCK_MODE_UNLOCKED}, {@link
-     *                 DrawerLayout#LOCK_MODE_LOCKED_CLOSED} or {@link DrawerLayout#LOCK_MODE_LOCKED_OPEN}.
+     * @param lockMode The new lock mode for the given drawer. One of {@link DrawerLayout#LOCK_MODE_UNLOCKED},
+     *                 {@link DrawerLayout#LOCK_MODE_LOCKED_CLOSED} or {@link DrawerLayout#LOCK_MODE_LOCKED_OPEN}.
      */
     public void setDrawerLockMode(int lockMode) {
         if (mDrawerLayout != null) {
@@ -1155,7 +1176,7 @@ public abstract class DrawerActivity extends ToolbarActivity
         return true;
     }
 
-    public AppPreferences getAppPreferences(){
+    public AppPreferences getAppPreferences() {
         return preferences;
     }
 

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

@@ -150,7 +150,6 @@ import java.io.File;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
-import java.util.Stack;
 
 import javax.inject.Inject;
 

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

@@ -55,7 +55,7 @@ import com.owncloud.android.utils.PushUtils
  */
 class NotificationsActivity : DrawerActivity(), NotificationsContract.View {
 
-    private lateinit var binding: NotificationsLayoutBinding
+    lateinit var binding: NotificationsLayoutBinding
 
     private var adapter: NotificationListAdapter? = null
     private var snackbar: Snackbar? = null

+ 3 - 3
app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt

@@ -80,7 +80,7 @@ import javax.inject.Inject
 /**
  * Activity displaying all auto-synced folders and/or instant upload media folders.
  */
-@Suppress("TooManyFunctions")
+@Suppress("TooManyFunctions", "LargeClass")
 class SyncedFoldersActivity :
     FileActivity(),
     SyncedFolderAdapter.ClickListener,
@@ -165,8 +165,8 @@ class SyncedFoldersActivity :
     @Inject
     lateinit var syncedFolderProvider: SyncedFolderProvider
 
-    private lateinit var binding: SyncedFoldersLayoutBinding
-    private lateinit var adapter: SyncedFolderAdapter
+    lateinit var binding: SyncedFoldersLayoutBinding
+    lateinit var adapter: SyncedFolderAdapter
 
     private var syncedFolderPreferencesDialogFragment: SyncedFolderPreferencesDialogFragment? = null
     private var path: String? = null

+ 7 - 0
app/src/main/java/com/owncloud/android/ui/adapter/SyncedFolderAdapter.java

@@ -50,6 +50,7 @@ import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
 
 /**
  * Adapter to display all auto-synced folders and/or instant upload media folders.
@@ -179,6 +180,12 @@ public class SyncedFolderAdapter extends SectionedRecyclerViewAdapter<SectionedV
         }
     }
 
+    @VisibleForTesting
+    public void clear() {
+        filteredSyncFolderItems.clear();
+        syncFolderItems.clear();
+    }
+
     public int getUnfilteredSectionCount() {
         if (syncFolderItems.size() > 0) {
             return syncFolderItems.size() + 1;

+ 4 - 0
app/src/main/java/com/owncloud/android/ui/fragment/ExtendedListFragment.java

@@ -142,6 +142,10 @@ public class ExtendedListFragment extends Fragment implements
 
     private ListFragmentBinding binding;
 
+    public ListFragmentBinding getBinding() {
+        return binding;
+    }
+
     protected void setRecyclerViewAdapter(RecyclerView.Adapter recyclerViewAdapter) {
         mRecyclerView.setAdapter(recyclerViewAdapter);
     }

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

@@ -106,7 +106,7 @@ public class FileDetailActivitiesFragment extends Fragment implements
     private FileOperationsHelper operationsHelper;
     private VersionListInterface.CommentCallback callback;
 
-    private FileDetailsActivitiesFragmentBinding binding;
+    FileDetailsActivitiesFragmentBinding binding;
 
     @Inject UserAccountManager accountManager;
     @Inject ClientFactory clientFactory;

+ 1 - 1
app/src/main/java/com/owncloud/android/ui/trashbin/TrashbinActivity.kt

@@ -84,7 +84,7 @@ class TrashbinActivity :
     var trashbinPresenter: TrashbinPresenter? = null
 
     private var active = false
-    private lateinit var binding: TrashbinActivityBinding
+    lateinit var binding: TrashbinActivityBinding
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)

+ 7 - 0
app/src/main/java/com/owncloud/android/utils/DrawerMenuUtil.java

@@ -64,6 +64,13 @@ public final class DrawerMenuUtil {
         }
     }
 
+    public static void filterAssistantMenuItem(Menu menu, @Nullable OCCapability capability, Resources resources) {
+        boolean showCondition = capability != null && capability.getAssistant().isTrue() && !resources.getBoolean(R.bool.is_branded_client);
+        if (!showCondition) {
+            filterMenuItems(menu, R.id.nav_assistant);
+        }
+    }
+
     public static void filterGroupfoldersMenuItem(Menu menu, @Nullable OCCapability capability) {
         if (capability != null && !capability.getGroupfolders().isTrue()) {
             filterMenuItems(menu, R.id.nav_groupfolders);

+ 1 - 1
app/src/main/java/com/owncloud/android/utils/WebViewUtil.kt

@@ -123,7 +123,7 @@ class WebViewUtil(private val context: Context) {
      * @return
      */
     @SuppressLint("PrivateApi", "DiscouragedPrivateApi")
-    @Suppress("TooGenericExceptionCaught")
+    @Suppress("TooGenericExceptionCaught", "NestedBlockDepth")
     fun setProxyKKPlus(webView: WebView) {
         val proxyHost = OwnCloudClientManagerFactory.getProxyHost()
         val proxyPort = OwnCloudClientManagerFactory.getProxyPort()

+ 32 - 0
app/src/main/res/drawable/ic_assistant.xml

@@ -0,0 +1,32 @@
+<!--
+  ~ Nextcloud Android client application
+  ~
+  ~ @author Alper Ozturk
+  ~ Copyright (C) 2024 Alper Ozturk
+  ~ Copyright (C) 2024 Nextcloud GmbH
+  ~
+  ~ This program is free software: you can redistribute it and/or modify
+  ~ it under the terms of the GNU Affero General Public License as published by
+  ~ the Free Software Foundation, either version 3 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  ~ GNU Affero General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU Affero General Public License
+  ~ along with this program. If not, see <https://www.gnu.org/licenses/>.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="1dp"
+    android:height="1dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+  <path
+      android:pathData="M9,4.5a0.75,0.75 0,0 1,0.721 0.544l0.813,2.846a3.75,3.75 0,0 0,2.576 2.576l2.846,0.813a0.75,0.75 0,0 1,0 1.442l-2.846,0.813a3.75,3.75 0,0 0,-2.576 2.576l-0.813,2.846a0.75,0.75 0,0 1,-1.442 0l-0.813,-2.846a3.75,3.75 0,0 0,-2.576 -2.576l-2.846,-0.813a0.75,0.75 0,0 1,0 -1.442l2.846,-0.813A3.75,3.75 0,0 0,7.466 7.89l0.813,-2.846A0.75,0.75 0,0 1,9 4.5ZM18,1.5a0.75,0.75 0,0 1,0.728 0.568l0.258,1.036c0.236,0.94 0.97,1.674 1.91,1.91l1.036,0.258a0.75,0.75 0,0 1,0 1.456l-1.036,0.258c-0.94,0.236 -1.674,0.97 -1.91,1.91l-0.258,1.036a0.75,0.75 0,0 1,-1.456 0l-0.258,-1.036a2.625,2.625 0,0 0,-1.91 -1.91l-1.036,-0.258a0.75,0.75 0,0 1,0 -1.456l1.036,-0.258a2.625,2.625 0,0 0,1.91 -1.91l0.258,-1.036A0.75,0.75 0,0 1,18 1.5ZM16.5,15a0.75,0.75 0,0 1,0.712 0.513l0.394,1.183c0.15,0.447 0.5,0.799 0.948,0.948l1.183,0.395a0.75,0.75 0,0 1,0 1.422l-1.183,0.395c-0.447,0.15 -0.799,0.5 -0.948,0.948l-0.395,1.183a0.75,0.75 0,0 1,-1.422 0l-0.395,-1.183a1.5,1.5 0,0 0,-0.948 -0.948l-1.183,-0.395a0.75,0.75 0,0 1,0 -1.422l1.183,-0.395c0.447,-0.15 0.799,-0.5 0.948,-0.948l0.395,-1.183A0.75,0.75 0,0 1,16.5 15Z"
+      android:strokeWidth="0"
+      android:fillColor="#FF000000"
+      android:fillType="evenOdd"/>
+</vector>

+ 54 - 0
app/src/main/res/layout/activity_compose.xml

@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Nextcloud Android client application
+  ~
+  ~ @author Alper Ozturk
+  ~ Copyright (C) 2024 Alper Ozturk
+  ~ Copyright (C) 2024 Nextcloud GmbH
+  ~
+  ~ This program is free software: you can redistribute it and/or modify
+  ~ it under the terms of the GNU Affero General Public License as published by
+  ~ the Free Software Foundation, either version 3 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  ~ GNU Affero General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU Affero General Public License
+  ~ along with this program. If not, see <https://www.gnu.org/licenses/>.
+  -->
+
+
+<androidx.drawerlayout.widget.DrawerLayout
+    android:id="@+id/drawer_layout"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:clickable="true"
+    android:fitsSystemWindows="true"
+    android:focusable="true">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical">
+
+        <include layout="@layout/toolbar_standard" />
+
+        <androidx.compose.ui.platform.ComposeView
+            android:id="@+id/compose_view"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"/>
+
+    </LinearLayout>
+
+    <include
+        layout="@layout/drawer"
+        android:layout_width="@dimen/drawer_width"
+        android:layout_height="match_parent"
+        android:layout_gravity="start"/>
+
+</androidx.drawerlayout.widget.DrawerLayout>

+ 31 - 0
app/src/main/res/layout/drawer_header.xml

@@ -71,6 +71,37 @@
         android:layout_marginBottom="@dimen/standard_half_margin"
         android:orientation="horizontal">
 
+        <LinearLayout
+            android:id="@+id/drawer_ecosystem_assistant"
+            android:layout_width="wrap_content"
+            android:layout_marginEnd="30dp"
+            android:layout_height="wrap_content"
+            android:gravity="center"
+            android:background="?android:selectableItemBackgroundBorderless"
+            android:clickable="true"
+            android:focusable="true"
+            android:orientation="vertical">
+
+            <ImageView
+                android:layout_width="40dp"
+                android:layout_height="40dp"
+                android:layout_margin="4dp"
+                android:background="@drawable/white_outline"
+                android:contentDescription="@string/ecosystem_apps_talk"
+                android:padding="8dp"
+                android:src="@drawable/ic_assistant"
+                app:tint="@color/white" />
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center"
+                android:text="@string/ecosystem_apps_display_assistant"
+                android:textColor="@color/white"
+                android:textStyle="bold" />
+
+        </LinearLayout>
+
         <LinearLayout
             android:id="@+id/drawer_ecosystem_talk"
             android:layout_width="wrap_content"

+ 31 - 0
app/src/main/res/layout/fragment_compose_view.xml

@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Nextcloud Android client application
+  ~
+  ~ @author Alper Ozturk
+  ~ Copyright (C) 2024 Alper Ozturk
+  ~ Copyright (C) 2024 Nextcloud GmbH
+  ~
+  ~ This program is free software: you can redistribute it and/or modify
+  ~ it under the terms of the GNU Affero General Public License as published by
+  ~ the Free Software Foundation, either version 3 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  ~ GNU Affero General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU Affero General Public License
+  ~ along with this program. If not, see <https://www.gnu.org/licenses/>.
+  -->
+
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <androidx.compose.ui.platform.ComposeView
+        android:id="@+id/compose_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 9 - 0
app/src/main/res/menu/partial_drawer_entries.xml

@@ -25,11 +25,13 @@
     <group
         android:id="@+id/drawer_menu_standard"
         android:checkableBehavior="single">
+
         <item
             android:id="@+id/nav_all_files"
             android:icon="@drawable/all_files"
             android:orderInCategory="0"
             android:title="@string/drawer_item_all_files" />
+
         <item
             android:id="@+id/nav_personal_files"
             android:icon="@drawable/ic_user"
@@ -74,6 +76,13 @@
             android:id="@+id/nav_notifications"
             android:icon="@drawable/nav_notifications"
             android:title="@string/drawer_item_notifications"/>
+
+        <item
+            android:id="@+id/nav_assistant"
+            android:icon="@drawable/ic_assistant"
+            android:orderInCategory="0"
+            android:title="@string/drawer_item_assistant" />
+
         <item
             android:id="@+id/nav_uploads"
             android:icon="@drawable/uploads"

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

@@ -18,6 +18,33 @@
     <string name="menu_item_sort_by_size_biggest_first">Biggest first</string>
     <string name="menu_item_sort_by_size_smallest_first">Smallest first</string>
 
+    <string name="ecosystem_apps_display_assistant">Assistant</string>
+
+    <string name="assistant_screen_task_types_error_state_message">Cannot able to fetch task types, please check your internet connection.</string>
+    <string name="assistant_screen_task_list_error_state_message">Cannot able to fetch task list, please check your internet connection.</string>
+
+    <string name="assistant_screen_top_bar_title">Assistant</string>
+    <string name="assistant_screen_loading">Task List are loading, please wait</string>
+    <string name="assistant_screen_no_task_available_for_all_task_filter_text">No task available. Select a task type to create a new task.</string>
+    <string name="assistant_screen_no_task_available_text">No task available for %s task type, you can create a new task from bottom right.</string>
+    <string name="assistant_screen_delete_task_alert_dialog_title">Delete Task</string>
+    <string name="assistant_screen_delete_task_alert_dialog_description">Are you sure you want to delete this task?</string>
+
+    <string name="assistant_screen_task_more_actions_bottom_sheet_delete_action">Delete Task</string>
+
+    <string name="assistant_screen_task_create_success_message">Task successfully created</string>
+    <string name="assistant_screen_task_create_fail_message">An error occurred while creating the task</string>
+
+    <string name="assistant_screen_task_delete_success_message">An error occurred while deleting the task</string>
+    <string name="assistant_screen_task_delete_fail_message">Task successfully deleted</string>
+
+    <string name="assistant_screen_create_task_alert_dialog_input_field_placeholder">Type some text</string>
+
+    <string name="assistant_screen_all_task_type">All</string>
+    <string name="assistant_screen_task_view_show_more">Show more</string>
+    <string name="assistant_screen_task_view_show_less">Show less</string>
+
+    <string name="drawer_item_assistant">Assistant</string>
     <string name="drawer_item_all_files">All files</string>
     <string name="drawer_item_personal_files">Personal files</string>
     <string name="drawer_item_home">Home</string>
@@ -1135,7 +1162,7 @@
     <string name="image_preview_unit_megapixel">%s MP</string>
     <string name="image_preview_filedetails">File details</string>
     <string name="image_preview_image_taking_conditions">Image taking conditions</string>
-    <string translatable="false" name="make_model">%1$s %2$s</string>
+    <string name="make_model" translatable="false">%1$s %2$s</string>
     <string name="sub_folder_rule_year">Year</string>
     <string name="sub_folder_rule_month">Year/Month</string>
     <string name="sub_folder_rule_day">Year/Month/Day</string>

+ 2 - 2
appscan/build.gradle

@@ -10,11 +10,11 @@ apply plugin: 'kotlin-android'
 
 android {
     namespace 'com.nextcloud.appscan'
-    compileSdk 33
+    compileSdk 34
 
     defaultConfig {
         minSdk 21
-        targetSdk 33
+        targetSdk 34
 
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
     }

+ 2 - 2
build.gradle

@@ -1,6 +1,6 @@
 buildscript {
     ext {
-        androidLibraryVersion ="5f92d92c490d2d9039bfd392cf16c61a37738646"
+        androidLibraryVersion ="0c886d61f6"
         androidPluginVersion = '8.3.0'
         androidxMediaVersion = '1.3.0'
         androidxTestVersion = "1.5.0"
@@ -11,7 +11,7 @@ buildscript {
         espressoVersion = "3.5.1"
         fidoVersion = "4.1.0-patch2"
         jacoco_version = '0.8.11'
-        kotlin_version = '1.9.23'
+        kotlin_version = '1.9.22'
         markwonVersion = "4.6.2"
         mockitoVersion = "4.11.0"
         mockitoKotlinVersion = "4.1.0"

+ 3 - 0
gradle.properties

@@ -16,3 +16,6 @@ kotlin.daemon.jvmargs=-Xmx4096m
 org.gradle.caching=true
 org.gradle.parallel=true
 org.gradle.configureondemand=true
+
+# Needed for local libs
+# org.gradle.dependency.verification=lenient

+ 85 - 0
gradle/verification-metadata.xml

@@ -187,6 +187,7 @@
             <trusting group="androidx.room"/>
             <trusting group="androidx.sqlite"/>
             <trusting group="androidx.webkit" name="webkit" version="1.10.0"/>
+            <trusting group="androidx.webkit" name="webkit" version="1.9.0"/>
             <trusting group="androidx.work"/>
          </trusted-key>
          <trusted-key id="84789D24DF77A32433CE1F079EB80E92EB2135B1">
@@ -439,6 +440,9 @@
          </artifact>
       </component>
       <component group="androidx.annotation" name="annotation" version="1.0.0">
+         <artifact name="annotation-1.0.0.jar">
+            <sha256 value="0baae9755f7caf52aa80cd04324b91ba93af55d4d1d17dcc9a7b53d99ef7c016" origin="Generated by Gradle" reason="Artifact is not signed"/>
+         </artifact>
          <artifact name="annotation-1.0.0.pom">
             <sha256 value="a179c12db43d9c0300c9db63f4811db496504be5401b951d422b78490ad1e5b4" origin="Generated by Gradle" reason="Artifact is not signed"/>
          </artifact>
@@ -534,6 +538,9 @@
          </artifact>
       </component>
       <component group="androidx.annotation" name="annotation-jvm" version="1.6.0">
+         <artifact name="annotation-jvm-1.6.0.jar">
+            <sha256 value="60b10b5ef5769b79570172e015b8159405c92f034ba88b9391a977589c9deb4e" origin="Generated by Gradle" reason="Artifact is not signed"/>
+         </artifact>
          <artifact name="annotation-jvm-1.6.0.module">
             <sha256 value="3f5a8faa19de667e63dca9730ff8ef0e478e4bafb5feeb8258e5c086246dc90c" origin="Generated by Gradle" reason="Artifact is not signed"/>
          </artifact>
@@ -1229,6 +1236,14 @@
             <sha256 value="64cad9fa6a92d5cbde2dfc0994619aec3704994923b8eb0aa8c0a0581bee18a8" origin="Generated by Gradle" reason="Artifact is not signed"/>
          </artifact>
       </component>
+      <component group="androidx.databinding" name="viewbinding" version="8.2.0">
+         <artifact name="viewbinding-8.2.0.aar">
+            <sha256 value="7d20274fccaf968d1b9c7b30a61363e8f9bcab89d9a3c3a0472c670e334f3260" origin="Generated by Gradle" reason="Artifact is not signed"/>
+         </artifact>
+         <artifact name="viewbinding-8.2.0.module">
+            <sha256 value="45b4e919739627f4f98e3c693ea583125e6fdd39ac1913b4fc8a30111754e0eb" origin="Generated by Gradle" reason="Artifact is not signed"/>
+         </artifact>
+      </component>
       <component group="androidx.databinding" name="viewbinding" version="8.2.2">
          <artifact name="viewbinding-8.2.2.aar">
             <sha256 value="b808b84a1f998b6758540550e8e12bb163f293b13fa99c147a318b72e4cf67d0" origin="Generated by Gradle" reason="Artifact is not signed"/>
@@ -1396,6 +1411,14 @@
             <sha256 value="b3955b619e8a16c38af39c19126867c72d1954db05551709e58c082b946078c4" origin="Generated by Gradle" reason="Artifact is not signed"/>
          </artifact>
       </component>
+      <component group="androidx.fragment" name="fragment-ktx" version="1.3.6">
+         <artifact name="fragment-ktx-1.3.6.aar">
+            <sha256 value="3f84a013fdeb8bac92d4ab607aebf39a4ff945f4585a635960ed769cd0255df1" origin="Generated by Gradle" reason="Artifact is not signed"/>
+         </artifact>
+         <artifact name="fragment-ktx-1.3.6.module">
+            <sha256 value="a775bab4e5ef78605c2b0f8bc9dcd9e2c2dd6b5d5d9320012a69a5d01375059a" origin="Generated by Gradle" reason="Artifact is not signed"/>
+         </artifact>
+      </component>
       <component group="androidx.fragment" name="fragment-ktx" version="1.6.0">
          <artifact name="fragment-ktx-1.6.0.aar">
             <sha256 value="48c57ecebc6c1b07baf7dd1b77095560bb1f158aec83ba155645b77a1ce53307" origin="Generated by Gradle" reason="Artifact is not signed"/>
@@ -1513,6 +1536,11 @@
             <sha256 value="5fb7c8514d8c56cada5e29ef89dc0289e71942ab4cb0b2e6dca137b9dcb8fdd4" origin="Generated by Gradle" reason="Artifact is not signed"/>
          </artifact>
       </component>
+      <component group="androidx.lifecycle" name="lifecycle-common" version="2.4.0">
+         <artifact name="lifecycle-common-2.4.0.module">
+            <sha256 value="5ad5eafc22e8b04e58fa81d18d2570562971977e18f009500b5bd449bc6337bc" origin="Generated by Gradle" reason="Artifact is not signed"/>
+         </artifact>
+      </component>
       <component group="androidx.lifecycle" name="lifecycle-common" version="2.5.1">
          <artifact name="lifecycle-common-2.5.1.jar">
             <sha256 value="20ad1520f625cf455e6afd7290988306d3a9886efa993e0860fbabf4bb3f7bda" origin="Generated by Gradle" reason="Artifact is not signed"/>
@@ -1529,6 +1557,14 @@
             <sha256 value="93747a9145cb36bc71005f598ede32e2b1149ade5a16e62b0e4969345bc62d85" origin="Generated by Gradle" reason="Artifact is not signed"/>
          </artifact>
       </component>
+      <component group="androidx.lifecycle" name="lifecycle-common-java8" version="2.6.1">
+         <artifact name="lifecycle-common-java8-2.6.1.jar">
+            <sha256 value="c6deada2fac53b8ea6523dbda77597b128006674616f140f04df23264c6d1aa3" origin="Generated by Gradle" reason="Artifact is not signed"/>
+         </artifact>
+         <artifact name="lifecycle-common-java8-2.6.1.module">
+            <sha256 value="1beb0b9fffb630a005deca1d3583d2acbec8685d6de809a3a6e0e433f418b6c3" origin="Generated by Gradle" reason="Artifact is not signed"/>
+         </artifact>
+      </component>
       <component group="androidx.lifecycle" name="lifecycle-livedata" version="2.0.0">
          <artifact name="lifecycle-livedata-2.0.0.aar">
             <sha256 value="c82609ced8c498f0a701a30fb6771bb7480860daee84d82e0a81ee86edf7ba39" origin="Generated by Gradle" reason="Artifact is not signed"/>
@@ -2715,6 +2751,11 @@
             <sha256 value="f4063ab5094f35aa586ebfd26834f0f914fbcae33e7dea1b0238dab380e40089" origin="Generated by Gradle" reason="Artifact is not signed"/>
          </artifact>
       </component>
+      <component group="com.android.application" name="com.android.application.gradle.plugin" version="8.3.0">
+         <artifact name="com.android.application.gradle.plugin-8.3.0.pom">
+            <sha256 value="f3d60bb5f262d10d4ed77fe987f69192a9595d28b991fd0c4a1d8524db65b3d5" origin="Generated by Gradle" reason="Artifact is not signed"/>
+         </artifact>
+      </component>
       <component group="com.android.databinding" name="baseLibrary" version="8.2.2">
          <artifact name="baseLibrary-8.2.2.jar">
             <sha256 value="794113709dab21b06c262b3795e73cb708fbacae61715f34361e1af6237a1870" origin="Generated by Gradle" reason="Artifact is not signed"/>
@@ -2731,6 +2772,11 @@
             <sha256 value="15c10e317f27ab7e563c2ddd219b2cf5c9b95c85ec7e912d4610e8c1c30d998d" origin="Generated by Gradle" reason="Artifact is not signed"/>
          </artifact>
       </component>
+      <component group="com.android.library" name="com.android.library.gradle.plugin" version="8.3.0">
+         <artifact name="com.android.library.gradle.plugin-8.3.0.pom">
+            <sha256 value="b309d7723d3a21151538cc8843393da70d0dd1b4066a54f620f79464121aada4" origin="Generated by Gradle" reason="Artifact is not signed"/>
+         </artifact>
+      </component>
       <component group="com.android.tools" name="annotations" version="31.2.2">
          <artifact name="annotations-31.2.2.jar">
             <sha256 value="ee3bfd9cdb5012bdb61520f8654a785577e9bb337e5939c5c6149a446684ee16" origin="Generated by Gradle" reason="Artifact is not signed"/>
@@ -3957,6 +4003,14 @@
             <sha256 value="6a91a2139a3cae8126c509cf65d136d49c35cb032b581ac1a56cb6a649cc0245" origin="Generated by Gradle"/>
          </artifact>
       </component>
+      <component group="com.github.nextcloud" name="android-library" version="0c886d61f6">
+         <artifact name="android-library-0c886d61f6.aar">
+            <sha256 value="9c3a87487717acd878305a00f0d6f2337b9f5512d541f7ac46b9bdb5a53020b9" origin="Generated by Gradle" reason="Artifact is not signed"/>
+         </artifact>
+         <artifact name="android-library-0c886d61f6.module">
+            <sha256 value="c29795ee883fc3364b2f16be5b9246b927271b961214f1a661b2caa2f42459a8" origin="Generated by Gradle" reason="Artifact is not signed"/>
+         </artifact>
+      </component>
       <component group="com.github.nextcloud" name="android-library" version="2b1da4cb14e2cd4b79e231b0be54e0bae699f143">
          <artifact name="android-library-2b1da4cb14e2cd4b79e231b0be54e0bae699f143.aar">
             <sha256 value="bdc44e874f1e14338213ae5723e71710940a31416ff1c52c9eb2f282e5d3f29a" origin="Generated by Gradle" reason="Artifact is not signed"/>
@@ -3973,6 +4027,14 @@
             <sha256 value="9f5dc2343cdf500b2cc17756418fce0506d13ef480ab8276cda415950e795991" origin="Generated by Gradle" reason="Artifact is not signed"/>
          </artifact>
       </component>
+      <component group="com.github.nextcloud" name="android-library" version="6dcffdb0ba">
+         <artifact name="android-library-6dcffdb0ba.aar">
+            <sha256 value="00e121d803e9258b36cb2d6e20904552c9e88dbff6b193b3b9adc0fcd224959a" origin="Generated by Gradle" reason="Artifact is not signed"/>
+         </artifact>
+         <artifact name="android-library-6dcffdb0ba.module">
+            <sha256 value="0e72841878595f83c6d8f93aa51f78a67e821f53446fcb7a5d94cfaad8ebbbac" origin="Generated by Gradle" reason="Artifact is not signed"/>
+         </artifact>
+      </component>
       <component group="com.github.nextcloud" name="android-library" version="acc7df66e4a43ed7f450136c13753f2743fb245e">
          <artifact name="android-library-acc7df66e4a43ed7f450136c13753f2743fb245e.aar">
             <sha256 value="f30c2d22c923c6ff8c605eb4cf713cfaf49eb967611f70dc3d139725b0891483" origin="Generated by Gradle" reason="Artifact is not signed"/>
@@ -3989,6 +4051,14 @@
             <sha256 value="ddd0f25a7d363aba6de79e8160ab02be999706afa51d9f9e8a30e40399421697" origin="Generated by Gradle" reason="Artifact is not signed"/>
          </artifact>
       </component>
+      <component group="com.github.nextcloud" name="android-library" version="fcb36db7ba">
+         <artifact name="android-library-fcb36db7ba.aar">
+            <sha256 value="2bae1a11ea687d92fcf6a76a52f4c20ee338e710d2281ce4cd8cae5d1e108151" origin="Generated by Gradle" reason="Artifact is not signed"/>
+         </artifact>
+         <artifact name="android-library-fcb36db7ba.module">
+            <sha256 value="9396eb66e3150b0d412d35e2dc9b29b078fbfc16e3c6e1c4f157318de797abfb" origin="Generated by Gradle" reason="Artifact is not signed"/>
+         </artifact>
+      </component>
       <component group="com.github.nextcloud-deps" name="qrcodescanner" version="0.1.2.4">
          <artifact name="qrcodescanner-0.1.2.4.aar">
             <sha256 value="b286128792cc04f59b0defa2c937c86d9e2fc824a8011b9af9eea7fd0ea84303" origin="Generated by Gradle" reason="Artifact is not signed"/>
@@ -6050,6 +6120,11 @@
             <sha256 value="5be0fd6355d0e9539899cbb0ff733906de36a64897255b86fde6880e8ad1d871" origin="Generated by Gradle"/>
          </artifact>
       </component>
+      <component group="io.gitlab.arturbosch.detekt" name="io.gitlab.arturbosch.detekt.gradle.plugin" version="1.23.5">
+         <artifact name="io.gitlab.arturbosch.detekt.gradle.plugin-1.23.5.pom">
+            <sha256 value="a326d380421a273859c741cb186a2ddba8cebf032ffa5feef94ff6d577e01185" origin="Generated by Gradle" reason="Artifact is not signed"/>
+         </artifact>
+      </component>
       <component group="io.grpc" name="grpc-api" version="1.45.1">
          <artifact name="grpc-api-1.45.1.jar">
             <sha256 value="dc381fe018fb10bba8cc66f98db1050a70cee49a8270017c22ec6f77b10f13e5" origin="Generated by Gradle"/>
@@ -7996,6 +8071,16 @@
             <sha256 value="5cb2601f0b639e17ed323637faaaf78585772a2383a43957c23655019a06fc00" origin="Generated by Gradle"/>
          </artifact>
       </component>
+      <component group="org.jetbrains.kotlin.android" name="org.jetbrains.kotlin.android.gradle.plugin" version="1.9.22">
+         <artifact name="org.jetbrains.kotlin.android.gradle.plugin-1.9.22.pom">
+            <sha256 value="c1b02fe072557f4c45b68c9772dfcbadb27d87b80a39e1a2796408ebbe4d0211" origin="Generated by Gradle" reason="Artifact is not signed"/>
+         </artifact>
+      </component>
+      <component group="org.jetbrains.kotlin.kapt" name="org.jetbrains.kotlin.kapt.gradle.plugin" version="1.9.22">
+         <artifact name="org.jetbrains.kotlin.kapt.gradle.plugin-1.9.22.pom">
+            <sha256 value="1daf64ddd8e90a6aa8a831f3e649b4b094e72fe91df0dfd91b5b1ba1dcd54d54" origin="Generated by Gradle" reason="Artifact is not signed"/>
+         </artifact>
+      </component>
       <component group="org.jetbrains.kotlinx" name="atomicfu" version="0.16.1">
          <artifact name="atomicfu-0.16.1.module">
             <sha256 value="fdcf04fc25f6a43f557f341ee0053caa25e759f591169c86566f1dad37fc77a6" origin="Generated by Gradle"/>

+ 2 - 2
gradle/wrapper/gradle-wrapper.properties

@@ -1,7 +1,7 @@
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip
 networkTimeout=10000
 validateDistributionUrl=true
 zipStoreBase=GRADLE_USER_HOME
-zipStorePath=wrapper/dists
+zipStorePath=wrapper/dists