Browse Source

Merge master

Signed-off-by: alperozturk <alper_ozturk@proton.me>
alperozturk 1 year ago
parent
commit
c9ecd1b0c6
59 changed files with 1645 additions and 1413 deletions
  1. 2 2
      .github/workflows/codeql.yml
  2. 1 1
      .github/workflows/scorecard.yml
  3. 3 3
      app/build.gradle
  4. BIN
      app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_check.png
  5. BIN
      app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_delete.png
  6. BIN
      app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_request.png
  7. BIN
      app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileAllShareTypes.png
  8. BIN
      app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileNone.png
  9. 1 1
      app/src/androidTest/java/com/nextcloud/client/ActivitiesActivityIT.kt
  10. 3 2
      app/src/huawei/java/com/owncloud/android/ui/activity/HuaweiCommunityActivity.kt
  11. 1 1
      app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt
  12. 8 5
      app/src/main/java/com/nextcloud/client/database/migrations/LegacyMigration.kt
  13. 21 3
      app/src/main/java/com/nextcloud/client/database/migrations/LegacyMigrationHelper.java
  14. 32 0
      app/src/main/java/com/nextcloud/utils/extensions/TextViewExtensions.kt
  15. 0 128
      app/src/main/java/com/owncloud/android/ui/activity/CommunityActivity.java
  16. 146 0
      app/src/main/java/com/owncloud/android/ui/activity/CommunityActivity.kt
  17. 12 7
      app/src/main/java/com/owncloud/android/ui/activity/ContactsPreferenceActivity.java
  18. 12 7
      app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java
  19. 18 29
      app/src/main/java/com/owncloud/android/ui/activity/FilePickerActivity.kt
  20. 27 15
      app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt
  21. 0 373
      app/src/main/java/com/owncloud/android/ui/activity/NotificationsActivity.java
  22. 376 0
      app/src/main/java/com/owncloud/android/ui/activity/NotificationsActivity.kt
  23. 0 478
      app/src/main/java/com/owncloud/android/ui/activity/PassCodeActivity.java
  24. 437 0
      app/src/main/java/com/owncloud/android/ui/activity/PassCodeActivity.kt
  25. 0 254
      app/src/main/java/com/owncloud/android/ui/activity/RichDocumentsEditorWebView.java
  26. 244 0
      app/src/main/java/com/owncloud/android/ui/activity/RichDocumentsEditorWebView.kt
  27. 35 30
      app/src/main/java/com/owncloud/android/ui/activity/UploadFilesActivity.java
  28. 41 6
      app/src/main/java/com/owncloud/android/ui/adapter/UploadListAdapter.java
  29. 92 1
      app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java
  30. 26 0
      app/src/main/res/drawable/ic_contact_book.xml
  31. 36 21
      app/src/main/res/layout/file_details_sharing_fragment.xml
  32. 14 12
      app/src/main/res/layout/passcodelock.xml
  33. 0 1
      app/src/main/res/values-ar/strings.xml
  34. 6 7
      app/src/main/res/values-cs-rCZ/strings.xml
  35. 0 1
      app/src/main/res/values-da/strings.xml
  36. 0 1
      app/src/main/res/values-de/strings.xml
  37. 1 0
      app/src/main/res/values-es-rCL/strings.xml
  38. 1 0
      app/src/main/res/values-es-rSV/strings.xml
  39. 1 0
      app/src/main/res/values-et-rEE/strings.xml
  40. 2 0
      app/src/main/res/values-eu/strings.xml
  41. 2 0
      app/src/main/res/values-fa/strings.xml
  42. 2 0
      app/src/main/res/values-fi-rFI/strings.xml
  43. 0 1
      app/src/main/res/values-fr/strings.xml
  44. 0 1
      app/src/main/res/values-gl/strings.xml
  45. 3 0
      app/src/main/res/values-hu-rHU/strings.xml
  46. 15 0
      app/src/main/res/values-nb-rNO/strings.xml
  47. 3 0
      app/src/main/res/values-pl/strings.xml
  48. 0 1
      app/src/main/res/values-ru/strings.xml
  49. 0 1
      app/src/main/res/values-sr/strings.xml
  50. 0 1
      app/src/main/res/values-sv/strings.xml
  51. 0 1
      app/src/main/res/values-tr/strings.xml
  52. 0 1
      app/src/main/res/values-uk/strings.xml
  53. 0 1
      app/src/main/res/values-zh-rCN/strings.xml
  54. 0 1
      app/src/main/res/values-zh-rTW/strings.xml
  55. 2 1
      app/src/main/res/values/strings.xml
  56. 8 8
      app/src/main/res/values/styles.xml
  57. 3 3
      app/src/test/java/com/nextcloud/client/core/LocalConnectionTest.kt
  58. 7 2
      gradle.properties
  59. 1 1
      scripts/analysis/lint-results.txt

+ 2 - 2
.github/workflows/codeql.yml

@@ -32,7 +32,7 @@ jobs:
         with:
           swap-size-gb: 10
       - name: Initialize CodeQL
-        uses: github/codeql-action/init@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5
+        uses: github/codeql-action/init@66b90a5db151a8042fa97405c6cf843bbe433f7b # v2.22.7
         with:
           languages: ${{ matrix.language }}
       - name: Set up JDK 17
@@ -46,4 +46,4 @@ jobs:
           echo "org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError" > "$HOME/.gradle/gradle.properties"
           ./gradlew assembleDebug
       - name: Perform CodeQL Analysis
-        uses: github/codeql-action/analyze@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5
+        uses: github/codeql-action/analyze@66b90a5db151a8042fa97405c6cf843bbe433f7b # v2.22.7

+ 1 - 1
.github/workflows/scorecard.yml

@@ -37,6 +37,6 @@ jobs:
 
       # Upload the results to GitHub's code scanning dashboard.
       - name: "Upload to code-scanning"
-        uses: github/codeql-action/upload-sarif@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5
+        uses: github/codeql-action/upload-sarif@66b90a5db151a8042fa97405c6cf843bbe433f7b # v2.22.7
         with:
           sarif_file: results.sarif

+ 3 - 3
app/build.gradle

@@ -84,8 +84,8 @@ android {
 
     defaultConfig {
         minSdkVersion 24
-        targetSdkVersion 33
-        compileSdk 33
+        targetSdkVersion 34
+        compileSdk 34
 
         buildConfigField 'boolean', 'CI', ciBuild.toString()
         buildConfigField 'boolean', 'RUNTIME_PERF_ANALYSIS', perfAnalysis.toString()
@@ -246,7 +246,7 @@ dependencies {
     implementation 'org.apache.jackrabbit:jackrabbit-webdav:2.13.5' // remove after entire switch to lib v2
     implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
     implementation 'androidx.legacy:legacy-support-v4:1.0.0'
-    implementation 'com.google.android.material:material:1.9.0'
+    implementation 'com.google.android.material:material:1.10.0'
     implementation 'com.jakewharton:disklrucache:2.0.2'
     implementation "androidx.appcompat:appcompat:$appCompatVersion"
     implementation 'androidx.webkit:webkit:1.7.0'

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


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


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


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


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


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

@@ -174,7 +174,7 @@ class ActivitiesActivityIT : AbstractIT() {
             sut.dismissSnackbar()
         }
 
-        shortSleep()
+        longSleep()
         waitForIdleSync()
 
         screenshot(sut)

+ 3 - 2
app/src/huawei/java/com/owncloud/android/ui/activity/HuaweiCommunityActivity.kt

@@ -20,14 +20,15 @@
  */
 package com.owncloud.android.ui.activity
 
+import android.os.Bundle
 import android.view.View
 
 /**
  * Activity providing information about ways to participate in the app's development.
  */
 class HuaweiCommunityActivity : CommunityActivity() {
-    override fun setupContent() {
-        super.setupContent()
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
         binding.communityReleaseCandidatePlaystore.visibility = View.GONE
     }
 }

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

@@ -92,7 +92,7 @@ abstract class NextcloudDatabase : RoomDatabase() {
                 INSTANCE = Room
                     .databaseBuilder(context, NextcloudDatabase::class.java, ProviderMeta.DB_NAME)
                     .allowMainThreadQueries()
-                    .addLegacyMigrations(clock)
+                    .addLegacyMigrations(clock, context)
                     .addMigrations(RoomMigration())
                     .addMigrations(Migration67to68())
                     .addMigrations(Migration70to71())

+ 8 - 5
app/src/main/java/com/nextcloud/client/database/migrations/LegacyMigration.kt

@@ -22,6 +22,7 @@
 
 package com.nextcloud.client.database.migrations
 
+import android.content.Context
 import androidx.room.RoomDatabase
 import androidx.room.migration.Migration
 import androidx.sqlite.db.SupportSQLiteDatabase
@@ -36,12 +37,13 @@ private const val MIN_SUPPORTED_DB_VERSION = 24
 class LegacyMigration(
     private val from: Int,
     private val to: Int,
-    private val clock: Clock
+    private val clock: Clock,
+    private val context: Context
 ) : Migration(from, to) {
 
     override fun migrate(database: SupportSQLiteDatabase) {
-        LegacyMigrationHelper(clock)
-            .onUpgrade(database, from, to)
+        LegacyMigrationHelper(clock, context)
+            .tryUpgrade(database, from, to)
     }
 }
 
@@ -52,10 +54,11 @@ class LegacyMigration(
  */
 @Suppress("ForEachOnRange")
 fun RoomDatabase.Builder<NextcloudDatabase>.addLegacyMigrations(
-    clock: Clock
+    clock: Clock,
+    context: Context
 ): RoomDatabase.Builder<NextcloudDatabase> {
     (MIN_SUPPORTED_DB_VERSION until NextcloudDatabase.FIRST_ROOM_DB_VERSION - 1)
-        .map { from -> LegacyMigration(from, from + 1, clock) }
+        .map { from -> LegacyMigration(from, from + 1, clock, context) }
         .forEach { migration -> this.addMigrations(migration) }
     return this
 }

+ 21 - 3
app/src/main/java/com/nextcloud/client/database/migrations/LegacyMigrationHelper.java

@@ -22,6 +22,7 @@
 
 package com.nextcloud.client.database.migrations;
 
+import android.app.ActivityManager;
 import android.content.Context;
 import android.database.Cursor;
 import android.database.sqlite.SQLiteException;
@@ -31,11 +32,11 @@ import com.owncloud.android.datamodel.SyncedFolder;
 import com.owncloud.android.db.ProviderMeta;
 import com.owncloud.android.files.services.NameCollisionPolicy;
 import com.owncloud.android.lib.common.utils.Log_OC;
-import com.owncloud.android.providers.FileContentProvider;
 
 import java.util.Locale;
 
 import androidx.sqlite.db.SupportSQLiteDatabase;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 
 public class LegacyMigrationHelper {
 
@@ -52,12 +53,29 @@ public class LegacyMigrationHelper {
     private static final String UPGRADE_VERSION_MSG = "OUT of the ADD in onUpgrade; oldVersion == %d, newVersion == %d";
 
     private final Clock clock;
+    private final Context context;
 
-    public LegacyMigrationHelper(Clock clock) {
+    public LegacyMigrationHelper(Clock clock, Context context) {
         this.clock = clock;
+        this.context = context;
     }
 
-    public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
+    public void tryUpgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
+        try {
+            upgrade(db, oldVersion, newVersion);
+        } catch (Throwable t) {
+            Log_OC.i(TAG, "Migration upgrade failed due to " + t);
+            clearStorage();
+        }
+    }
+
+    @SuppressFBWarnings("RV_RETURN_VALUE_IGNORED_BAD_PRACTICE")
+    private void clearStorage() {
+        context.getCacheDir().delete();
+        ((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE)).clearApplicationUserData();
+    }
+
+    private void upgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
         Log_OC.i(TAG, "Entering in onUpgrade");
         boolean upgraded = false;
 

+ 32 - 0
app/src/main/java/com/nextcloud/utils/extensions/TextViewExtensions.kt

@@ -0,0 +1,32 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Alper Ozturk
+ * Copyright (C) 2023 Alper Ozturk
+ * Copyright (C) 2023 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.utils.extensions
+
+import android.text.method.LinkMovementMethod
+import android.widget.TextView
+import androidx.core.text.HtmlCompat
+
+@Suppress("NewLineAtEndOfFile")
+fun TextView.setHtmlContent(value: String) {
+    movementMethod = LinkMovementMethod.getInstance()
+    text = HtmlCompat.fromHtml(value, HtmlCompat.FROM_HTML_MODE_LEGACY)
+}

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

@@ -1,128 +0,0 @@
-/*
- * Nextcloud Android client application
- *
- * @author Andy Scherzinger
- * @author Tobias Kaminsky
- * Copyright (C) 2016 Andy Scherzinger
- * Copyright (C) 2016 Nextcloud
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or 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 <http://www.gnu.org/licenses/>.
- */
-package com.owncloud.android.ui.activity;
-
-import android.os.Bundle;
-import android.text.Html;
-import android.text.method.LinkMovementMethod;
-import android.view.MenuItem;
-import android.widget.TextView;
-
-import com.google.android.material.button.MaterialButton;
-import com.owncloud.android.R;
-import com.owncloud.android.databinding.CommunityLayoutBinding;
-import com.owncloud.android.utils.DisplayUtils;
-
-/**
- * Activity providing information about ways to participate in the app's development.
- */
-public class CommunityActivity extends DrawerActivity {
-
-    protected CommunityLayoutBinding binding;
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        binding = CommunityLayoutBinding.inflate(getLayoutInflater());
-        setContentView(binding.getRoot());
-
-        // setup toolbar
-        setupToolbar();
-
-        updateActionBarTitleAndHomeButtonByString(getString(R.string.drawer_community));
-
-        // setup drawer
-        setupDrawer(R.id.nav_community);
-
-        setupContent();
-    }
-
-    protected void setupContent() {
-        binding.communityReleaseCandidateText.setMovementMethod(LinkMovementMethod.getInstance());
-
-        TextView contributeForumView = binding.communityContributeForumText;
-        contributeForumView.setMovementMethod(LinkMovementMethod.getInstance());
-        contributeForumView.setText(Html.fromHtml(getString(R.string.community_contribute_forum_text) + " " +
-                                                      getString(R.string.community_contribute_forum_text_link,
-                                                                viewThemeUtils
-                                                                    .files
-                                                                    .primaryColorToHexString(this),
-                                                                getString(R.string.help_link),
-                                                                getString(R.string.community_contribute_forum_forum))));
-
-        TextView contributeTranslationView = binding.communityContributeTranslateText;
-        contributeTranslationView.setMovementMethod(LinkMovementMethod.getInstance());
-        contributeTranslationView.setText(Html.fromHtml(
-            getString(R.string.community_contribute_translate_link,
-                      viewThemeUtils.files.primaryColorToHexString(this),
-                      getString(R.string.translation_link),
-                      getString(R.string.community_contribute_translate_translate)) + " " +
-                getString(R.string.community_contribute_translate_text)));
-
-        TextView contributeGithubView = binding.communityContributeGithubText;
-        contributeGithubView.setMovementMethod(LinkMovementMethod.getInstance());
-        contributeGithubView.setText(Html.fromHtml(
-            getString(R.string.community_contribute_github_text,
-                      getString(R.string.community_contribute_github_text_link,
-                                viewThemeUtils.files.primaryColorToHexString(this),
-                                getString(R.string.contributing_link)))));
-
-        MaterialButton reportButton = binding.communityTestingReport;
-        viewThemeUtils.material.colorMaterialButtonPrimaryFilled(reportButton);
-        reportButton.setOnClickListener(v -> DisplayUtils.startLinkIntent(this, R.string.report_issue_empty_link));
-
-        binding.communityBetaFdroid.setOnClickListener(
-            l -> DisplayUtils.startLinkIntent(this, R.string.fdroid_beta_link));
-
-        binding.communityReleaseCandidateFdroid.setOnClickListener(
-            l -> DisplayUtils.startLinkIntent(this, R.string.fdroid_link));
-
-        binding.communityReleaseCandidatePlaystore.setOnClickListener(
-            l -> DisplayUtils.startLinkIntent(this, R.string.play_store_register_beta));
-
-        binding.communityBetaApk.setOnClickListener(
-            l -> DisplayUtils.startLinkIntent(this, R.string.beta_apk_link));
-    }
-
-    @Override
-    public boolean onOptionsItemSelected(MenuItem item) {
-        boolean retval = true;
-        if (item.getItemId() == android.R.id.home) {
-            if (isDrawerOpen()) {
-                closeDrawer();
-            } else {
-                openDrawer();
-            }
-        } else {
-            retval = super.onOptionsItemSelected(item);
-        }
-        return retval;
-    }
-
-    @Override
-    protected void onResume() {
-        super.onResume();
-
-        setDrawerMenuItemChecked(R.id.nav_community);
-    }
-}

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

@@ -0,0 +1,146 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Andy Scherzinger
+ * @author Tobias Kaminsky
+ * Copyright (C) 2016 Andy Scherzinger
+ * Copyright (C) 2016 Nextcloud
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or 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 <http://www.gnu.org/licenses/>.
+ */
+package com.owncloud.android.ui.activity
+
+import android.os.Bundle
+import android.text.method.LinkMovementMethod
+import android.view.MenuItem
+import com.nextcloud.utils.extensions.setHtmlContent
+import com.owncloud.android.R
+import com.owncloud.android.databinding.CommunityLayoutBinding
+import com.owncloud.android.utils.DisplayUtils
+
+/**
+ * Activity providing information about ways to participate in the app's development.
+ */
+open class CommunityActivity : DrawerActivity() {
+    lateinit var binding: CommunityLayoutBinding
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        binding = CommunityLayoutBinding.inflate(layoutInflater)
+        setContentView(binding.root)
+
+        setupToolbar()
+        updateActionBarTitleAndHomeButtonByString(getString(R.string.drawer_community))
+
+        setupDrawer(R.id.nav_community)
+        binding.communityReleaseCandidateText.movementMethod = LinkMovementMethod.getInstance()
+        setupContributeForumView()
+        setupContributeTranslationView()
+        setupContributeGithubView()
+        setupReportButton()
+        setOnClickListeners()
+    }
+
+    private fun setupContributeForumView() {
+        val htmlContent = getString(R.string.community_contribute_forum_text) + " " +
+            getString(
+                R.string.community_contribute_forum_text_link,
+                viewThemeUtils.files
+                    .primaryColorToHexString(this),
+                getString(R.string.help_link),
+                getString(R.string.community_contribute_forum_forum)
+            )
+        binding.communityContributeForumText.setHtmlContent(htmlContent)
+    }
+
+    private fun setupContributeTranslationView() {
+        val htmlContent = getString(
+            R.string.community_contribute_translate_link,
+            viewThemeUtils.files.primaryColorToHexString(this),
+            getString(R.string.translation_link),
+            getString(R.string.community_contribute_translate_translate)
+        ) + " " +
+            getString(R.string.community_contribute_translate_text)
+        binding.communityContributeTranslateText.setHtmlContent(htmlContent)
+    }
+
+    private fun setupContributeGithubView() {
+        val htmlContent = getString(
+            R.string.community_contribute_github_text,
+            getString(
+                R.string.community_contribute_github_text_link,
+                viewThemeUtils.files.primaryColorToHexString(this),
+                getString(R.string.contributing_link)
+            )
+        )
+        binding.communityContributeGithubText.setHtmlContent(htmlContent)
+    }
+
+    private fun setupReportButton() {
+        val reportButton = binding.communityTestingReport
+        viewThemeUtils.material.colorMaterialButtonPrimaryFilled(reportButton)
+        reportButton.setOnClickListener {
+            DisplayUtils.startLinkIntent(
+                this,
+                R.string.report_issue_empty_link
+            )
+        }
+    }
+
+    private fun setOnClickListeners() {
+        binding.communityBetaFdroid.setOnClickListener {
+            DisplayUtils.startLinkIntent(
+                this,
+                R.string.fdroid_beta_link
+            )
+        }
+        binding.communityReleaseCandidateFdroid.setOnClickListener {
+            DisplayUtils.startLinkIntent(
+                this,
+                R.string.fdroid_link
+            )
+        }
+        binding.communityReleaseCandidatePlaystore.setOnClickListener {
+            DisplayUtils.startLinkIntent(
+                this,
+                R.string.play_store_register_beta
+            )
+        }
+        binding.communityBetaApk.setOnClickListener {
+            DisplayUtils.startLinkIntent(
+                this,
+                R.string.beta_apk_link
+            )
+        }
+    }
+
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        var retval = true
+        if (item.itemId == android.R.id.home) {
+            if (isDrawerOpen) {
+                closeDrawer()
+            } else {
+                openDrawer()
+            }
+        } else {
+            retval = super.onOptionsItemSelected(item)
+        }
+        return retval
+    }
+
+    override fun onResume() {
+        super.onResume()
+        setDrawerMenuItemChecked(R.id.nav_community)
+    }
+}

+ 12 - 7
app/src/main/java/com/owncloud/android/ui/activity/ContactsPreferenceActivity.java

@@ -36,6 +36,7 @@ import com.owncloud.android.ui.fragment.contactsbackup.BackupListFragment;
 
 import javax.inject.Inject;
 
+import androidx.activity.OnBackPressedCallback;
 import androidx.drawerlayout.widget.DrawerLayout;
 import androidx.fragment.app.FragmentManager;
 import androidx.fragment.app.FragmentTransaction;
@@ -115,6 +116,8 @@ public class ContactsPreferenceActivity extends FileActivity implements FileFrag
             }
             transaction.commit();
         }
+
+        getOnBackPressedDispatcher().addCallback(this, onBackPressedCallback);
     }
 
     @Override
@@ -137,12 +140,14 @@ public class ContactsPreferenceActivity extends FileActivity implements FileFrag
         // not needed
     }
 
-    @Override
-    public void onBackPressed() {
-        if (getSupportFragmentManager().findFragmentByTag(BackupListFragment.TAG) != null) {
-            getSupportFragmentManager().popBackStack(BACKUP_TO_LIST, FragmentManager.POP_BACK_STACK_INCLUSIVE);
-        } else {
-            finish();
+    private final OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(true) {
+        @Override
+        public void handleOnBackPressed() {
+            if (getSupportFragmentManager().findFragmentByTag(BackupListFragment.TAG) != null) {
+                getSupportFragmentManager().popBackStack(BACKUP_TO_LIST, FragmentManager.POP_BACK_STACK_INCLUSIVE);
+            } else {
+                finish();
+            }
         }
-    }
+    };
 }

+ 12 - 7
app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java

@@ -2507,8 +2507,7 @@ public class FileDisplayActivity extends FileActivity
         setUser(user);
 
         if (fileId == null) {
-            dismissLoadingDialog();
-            DisplayUtils.showSnackMessage(this, getString(R.string.error_retrieving_file));
+            onFileRequestError(null);
             return;
         }
 
@@ -2529,8 +2528,7 @@ public class FileDisplayActivity extends FileActivity
         setUser(user);
 
         if (filepath == null) {
-            dismissLoadingDialog();
-            DisplayUtils.showSnackMessage(this, getString(R.string.error_retrieving_file));
+            onFileRequestError(null);
             return;
         }
 
@@ -2544,8 +2542,7 @@ public class FileDisplayActivity extends FileActivity
         try {
             client = clientFactory.create(user);
         } catch (ClientFactory.CreationException e) {
-            dismissLoadingDialog();
-            DisplayUtils.showSnackMessage(this, getString(R.string.error_retrieving_file));
+            onFileRequestError(null);
             return;
         }
 
@@ -2554,9 +2551,17 @@ public class FileDisplayActivity extends FileActivity
                                                                     client,
                                                                     storageManager,
                                                                     user);
-        asyncRunner.postQuickTask(getRemoteFileTask, this::onFileRequestResult, null);
+        asyncRunner.postQuickTask(getRemoteFileTask, this::onFileRequestResult, this::onFileRequestError);
+    }
+
+    private Unit onFileRequestError(Throwable throwable) {
+        dismissLoadingDialog();
+        DisplayUtils.showSnackMessage(this, getString(R.string.error_retrieving_file));
+        Log_OC.e(TAG, "Requesting file from remote failed!", throwable);
+        return null;
     }
 
+
     private Unit onFileRequestResult(GetRemoteFileTask.Result result) {
         dismissLoadingDialog();
 

+ 18 - 29
app/src/main/java/com/owncloud/android/ui/activity/FilePickerActivity.java → app/src/main/java/com/owncloud/android/ui/activity/FilePickerActivity.kt

@@ -18,40 +18,29 @@
  * You should have received a copy of the GNU General Public License
  * along with this program. If not, see <https://www.gnu.org/licenses/>.
  */
+package com.owncloud.android.ui.activity
 
-package com.owncloud.android.ui.activity;
-
-import android.os.Bundle;
-import android.view.View;
-
-import com.owncloud.android.R;
-import com.owncloud.android.ui.fragment.OCFileListFragment;
-
-import androidx.fragment.app.FragmentTransaction;
+import android.os.Bundle
+import com.owncloud.android.R
+import com.owncloud.android.ui.fragment.OCFileListFragment
 
 /**
  * File picker of remote files
  */
-public class FilePickerActivity extends FolderPickerActivity {
-
-    @Override
-    public void onClick(View v) {
-        super.onClick(v);
-    }
+class FilePickerActivity : FolderPickerActivity() {
 
-    @Override
-    protected void createFragments() {
-        OCFileListFragment listOfFiles = new OCFileListFragment();
-        Bundle args = new Bundle();
-        args.putBoolean(OCFileListFragment.ARG_ONLY_FOLDERS_CLICKABLE, true);
-        args.putBoolean(OCFileListFragment.ARG_HIDE_FAB, true);
-        args.putBoolean(OCFileListFragment.ARG_HIDE_ITEM_OPTIONS, true);
-        args.putBoolean(OCFileListFragment.ARG_SEARCH_ONLY_FOLDER, false);
-        args.putBoolean(OCFileListFragment.ARG_FILE_SELECTABLE, true);
-        args.putString(OCFileListFragment.ARG_MIMETYPE, getIntent().getStringExtra(OCFileListFragment.ARG_MIMETYPE));
-        listOfFiles.setArguments(args);
-        FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
-        transaction.add(R.id.fragment_container, listOfFiles, TAG_LIST_OF_FOLDERS);
-        transaction.commit();
+    override fun createFragments() {
+        val listOfFiles = OCFileListFragment()
+        val args = Bundle()
+        args.putBoolean(OCFileListFragment.ARG_ONLY_FOLDERS_CLICKABLE, true)
+        args.putBoolean(OCFileListFragment.ARG_HIDE_FAB, true)
+        args.putBoolean(OCFileListFragment.ARG_HIDE_ITEM_OPTIONS, true)
+        args.putBoolean(OCFileListFragment.ARG_SEARCH_ONLY_FOLDER, false)
+        args.putBoolean(OCFileListFragment.ARG_FILE_SELECTABLE, true)
+        args.putString(OCFileListFragment.ARG_MIMETYPE, intent.getStringExtra(OCFileListFragment.ARG_MIMETYPE))
+        listOfFiles.arguments = args
+        val transaction = supportFragmentManager.beginTransaction()
+        transaction.add(R.id.fragment_container, listOfFiles, TAG_LIST_OF_FOLDERS)
+        transaction.commit()
     }
 }

+ 27 - 15
app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt

@@ -32,6 +32,7 @@ import android.view.ActionMode
 import android.view.Menu
 import android.view.MenuItem
 import android.view.View
+import androidx.activity.OnBackPressedCallback
 import androidx.localbroadcastmanager.content.LocalBroadcastManager
 import com.google.android.material.button.MaterialButton
 import com.nextcloud.client.di.Injectable
@@ -125,9 +126,33 @@ open class FolderPickerActivity :
 
         // sets message for empty list of folders
         setBackgroundText()
+
+        handleOnBackPressed()
+
         Log_OC.d(TAG, "onCreate() end")
     }
 
+    private fun handleOnBackPressed() {
+        onBackPressedDispatcher.addCallback(
+            this,
+            object : OnBackPressedCallback(true) {
+                override fun handleOnBackPressed() {
+                    val listOfFiles = listOfFilesFragment
+                    if (listOfFiles != null) {
+                        // should never be null, indeed
+                        val levelsUp = listOfFiles.onBrowseUp()
+                        if (levelsUp == 0) {
+                            finish()
+                            return
+                        }
+                        file = listOfFiles.currentFile
+                        updateUiElements()
+                    }
+                }
+            }
+        )
+    }
+
     override fun onActionModeStarted(mode: ActionMode) {
         super.onActionModeStarted(mode)
         if (account != null) {
@@ -321,20 +346,6 @@ open class FolderPickerActivity :
         }
     }
 
-    override fun onBackPressed() {
-        val listOfFiles = listOfFilesFragment
-        if (listOfFiles != null) {
-            // should never be null, indeed
-            val levelsUp = listOfFiles.onBrowseUp()
-            if (levelsUp == 0) {
-                finish()
-                return
-            }
-            file = listOfFiles.currentFile
-            updateUiElements()
-        }
-    }
-
     private fun updateUiElements() {
         toggleChooseEnabled()
         updateNavigationElementsInActionBar()
@@ -605,6 +616,7 @@ open class FolderPickerActivity :
         const val MOVE_OR_COPY = "MOVE_OR_COPY"
         const val CHOOSE_LOCATION = "CHOOSE_LOCATION"
         private val TAG = FolderPickerActivity::class.java.simpleName
-        protected const val TAG_LIST_OF_FOLDERS = "LIST_OF_FOLDERS"
+
+        const val TAG_LIST_OF_FOLDERS = "LIST_OF_FOLDERS"
     }
 }

+ 0 - 373
app/src/main/java/com/owncloud/android/ui/activity/NotificationsActivity.java

@@ -1,373 +0,0 @@
-/*
- * Nextcloud Android client application
- *
- * @author Andy Scherzinger
- * @author Mario Danic
- * @author Chris Narkiewicz
- * Copyright (C) 2017 Andy Scherzinger
- * Copyright (C) 2017 Mario Danic
- * Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
- *
- * 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 <http://www.gnu.org/licenses/>.
- */
-
-package com.owncloud.android.ui.activity;
-
-import android.os.Bundle;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
-
-import com.google.android.material.snackbar.Snackbar;
-import com.nextcloud.client.account.User;
-import com.nextcloud.client.account.UserAccountManager;
-import com.nextcloud.client.jobs.NotificationWork;
-import com.nextcloud.client.network.ClientFactory;
-import com.nextcloud.java.util.Optional;
-import com.owncloud.android.R;
-import com.owncloud.android.databinding.NotificationsLayoutBinding;
-import com.owncloud.android.datamodel.ArbitraryDataProvider;
-import com.owncloud.android.datamodel.ArbitraryDataProviderImpl;
-import com.owncloud.android.lib.common.OwnCloudClient;
-import com.owncloud.android.lib.common.operations.RemoteOperationResult;
-import com.owncloud.android.lib.common.utils.Log_OC;
-import com.owncloud.android.lib.resources.notifications.GetNotificationsRemoteOperation;
-import com.owncloud.android.lib.resources.notifications.models.Notification;
-import com.owncloud.android.ui.adapter.NotificationListAdapter;
-import com.owncloud.android.ui.asynctasks.DeleteAllNotificationsTask;
-import com.owncloud.android.ui.notifications.NotificationsContract;
-import com.owncloud.android.utils.DisplayUtils;
-import com.owncloud.android.utils.PushUtils;
-
-import java.util.List;
-
-import javax.inject.Inject;
-
-import androidx.annotation.VisibleForTesting;
-import androidx.recyclerview.widget.LinearLayoutManager;
-
-/**
- * Activity displaying all server side stored notification items.
- */
-public class NotificationsActivity extends DrawerActivity implements NotificationsContract.View {
-
-    private static final String TAG = NotificationsActivity.class.getSimpleName();
-
-    private NotificationsLayoutBinding binding;
-    private NotificationListAdapter adapter;
-    private Snackbar snackbar;
-    private OwnCloudClient client;
-    private Optional<User> optionalUser;
-
-    @Inject ClientFactory clientFactory;
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        Log_OC.v(TAG, "onCreate() start");
-        super.onCreate(savedInstanceState);
-
-        binding = NotificationsLayoutBinding.inflate(getLayoutInflater());
-        setContentView(binding.getRoot());
-
-        optionalUser = getUser();
-
-        // use account from intent (opened via android notification can have a different account than current one)
-        if (getIntent() != null && getIntent().getExtras() != null) {
-            String accountName = getIntent().getExtras().getString(NotificationWork.KEY_NOTIFICATION_ACCOUNT);
-            if (accountName != null && optionalUser.isPresent()) {
-                User user = optionalUser.get();
-                if (user.getAccountName().equalsIgnoreCase(accountName)) {
-                    accountManager.setCurrentOwnCloudAccount(accountName);
-                    setUser(getUserAccountManager().getUser());
-                    optionalUser = getUser();
-                }
-            }
-        }
-
-        // setup toolbar
-        setupToolbar();
-
-        updateActionBarTitleAndHomeButtonByString(getString(R.string.drawer_item_notifications));
-
-        viewThemeUtils.androidx.themeSwipeRefreshLayout(binding.swipeContainingList);
-        viewThemeUtils.androidx.themeSwipeRefreshLayout(binding.swipeContainingEmpty);
-
-        // setup drawer
-        setupDrawer(R.id.nav_notifications);
-
-        if (!optionalUser.isPresent()) {
-            // show error
-            runOnUiThread(() -> setEmptyContent(
-                              getString(R.string.notifications_no_results_headline),
-                              getString(R.string.account_not_found))
-                         );
-            return;
-        }
-
-        binding.swipeContainingList.setOnRefreshListener(() -> {
-            setLoadingMessage();
-            binding.swipeContainingList.setRefreshing(true);
-            fetchAndSetData();
-        });
-
-        binding.swipeContainingEmpty.setOnRefreshListener(() -> {
-            setLoadingMessageEmpty();
-            fetchAndSetData();
-        });
-
-        setupPushWarning();
-        setupContent();
-    }
-
-    private void setupPushWarning() {
-        if (!getResources().getBoolean(R.bool.show_push_warning)) {
-            return;
-        }
-        if (snackbar != null) {
-            if (!snackbar.isShown()) {
-                snackbar.show();
-            }
-        } else {
-            String pushUrl = getResources().getString(R.string.push_server_url);
-
-            if (pushUrl.isEmpty()) {
-                snackbar = Snackbar.make(binding.emptyList.emptyListView,
-                                         R.string.push_notifications_not_implemented,
-                                         Snackbar.LENGTH_INDEFINITE);
-            } else {
-                final ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(this);
-                final String accountName = optionalUser.isPresent() ? optionalUser.get().getAccountName() : "";
-                final boolean usesOldLogin = arbitraryDataProvider.getBooleanValue(accountName,
-                                                                                   UserAccountManager.ACCOUNT_USES_STANDARD_PASSWORD);
-
-                if (usesOldLogin) {
-                    snackbar = Snackbar.make(binding.emptyList.emptyListView,
-                                             R.string.push_notifications_old_login,
-                                             Snackbar.LENGTH_INDEFINITE);
-                } else {
-                    String pushValue = arbitraryDataProvider.getValue(accountName, PushUtils.KEY_PUSH);
-
-                    if (pushValue == null || pushValue.isEmpty()) {
-                        snackbar = Snackbar.make(binding.emptyList.emptyListView,
-                                                 R.string.push_notifications_temp_error,
-                                                 Snackbar.LENGTH_INDEFINITE);
-                    }
-                }
-            }
-
-            if (snackbar != null && !snackbar.isShown()) {
-                snackbar.show();
-            }
-        }
-    }
-
-    @Override
-    public void openDrawer() {
-        super.openDrawer();
-
-        if (snackbar != null && snackbar.isShown()) {
-            snackbar.dismiss();
-        }
-    }
-
-    @Override
-    public void closeDrawer() {
-        super.closeDrawer();
-
-        setupPushWarning();
-    }
-
-    /**
-     * sets up the UI elements and loads all notification items.
-     */
-    private void setupContent() {
-        binding.emptyList.emptyListIcon.setImageResource(R.drawable.ic_notification);
-        setLoadingMessageEmpty();
-
-        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
-
-        binding.list.setLayoutManager(layoutManager);
-
-        fetchAndSetData();
-    }
-
-    @VisibleForTesting
-    public void populateList(List<Notification> notifications) {
-        initializeAdapter();
-        adapter.setNotificationItems(notifications);
-        binding.loadingContent.setVisibility(View.GONE);
-
-        if (notifications.size() > 0) {
-            binding.swipeContainingEmpty.setVisibility(View.GONE);
-            binding.swipeContainingList.setVisibility(View.VISIBLE);
-        } else {
-            setEmptyContent(
-                getString(R.string.notifications_no_results_headline),
-                getString(R.string.notifications_no_results_message)
-                           );
-            binding.swipeContainingList.setVisibility(View.GONE);
-            binding.swipeContainingEmpty.setVisibility(View.VISIBLE);
-        }
-    }
-
-    private void fetchAndSetData() {
-        Thread t = new Thread(() -> {
-            initializeAdapter();
-
-            GetNotificationsRemoteOperation getRemoteNotificationOperation = new GetNotificationsRemoteOperation();
-            final RemoteOperationResult<List<Notification>> result = getRemoteNotificationOperation.execute(client);
-
-            if (result.isSuccess() && result.getResultData() != null) {
-                runOnUiThread(() -> populateList(result.getResultData()));
-            } else {
-                Log_OC.d(TAG, result.getLogMessage());
-                // show error
-                runOnUiThread(() -> setEmptyContent(getString(R.string.notifications_no_results_headline), result.getLogMessage()));
-            }
-
-            hideRefreshLayoutLoader();
-        });
-
-        t.start();
-    }
-
-    private void initializeClient() {
-        if (client == null && optionalUser.isPresent()) {
-            try {
-                User user = optionalUser.get();
-                client = clientFactory.create(user);
-            } catch (ClientFactory.CreationException e) {
-                Log_OC.e(TAG, "Error initializing client", e);
-            }
-        }
-    }
-
-    private void initializeAdapter() {
-        initializeClient();
-        if (adapter == null) {
-            adapter = new NotificationListAdapter(client, this, viewThemeUtils);
-            binding.list.setAdapter(adapter);
-        }
-    }
-
-    private void hideRefreshLayoutLoader() {
-        runOnUiThread(() -> {
-            binding.swipeContainingList.setRefreshing(false);
-            binding.swipeContainingEmpty.setRefreshing(false);
-        });
-    }
-
-    public boolean onCreateOptionsMenu(Menu menu) {
-        getMenuInflater().inflate(R.menu.activity_notifications, menu);
-
-        return true;
-    }
-
-    @Override
-    public boolean onOptionsItemSelected(MenuItem item) {
-        boolean retval = true;
-
-        int itemId = item.getItemId();
-        if (itemId == android.R.id.home) {
-            if (isDrawerOpen()) {
-                closeDrawer();
-            } else {
-                openDrawer();
-            }
-        } else if (itemId == R.id.action_empty_notifications) {
-            new DeleteAllNotificationsTask(client, this).execute();
-        } else {
-            retval = super.onOptionsItemSelected(item);
-        }
-
-        return retval;
-    }
-
-    private void setLoadingMessage() {
-        binding.swipeContainingEmpty.setVisibility(View.GONE);
-    }
-
-    @VisibleForTesting
-    public void setLoadingMessageEmpty() {
-        binding.swipeContainingList.setVisibility(View.GONE);
-        binding.emptyList.emptyListView.setVisibility(View.GONE);
-        binding.loadingContent.setVisibility(View.VISIBLE);
-    }
-
-    @VisibleForTesting
-    public void setEmptyContent(String headline, String message) {
-        binding.swipeContainingList.setVisibility(View.GONE);
-        binding.loadingContent.setVisibility(View.GONE);
-        binding.swipeContainingEmpty.setVisibility(View.VISIBLE);
-        binding.emptyList.emptyListView.setVisibility(View.VISIBLE);
-
-        binding.emptyList.emptyListViewHeadline.setText(headline);
-        binding.emptyList.emptyListViewText.setText(message);
-        binding.emptyList.emptyListIcon.setImageResource(R.drawable.ic_notification);
-
-        binding.emptyList.emptyListViewText.setVisibility(View.VISIBLE);
-        binding.emptyList.emptyListIcon.setVisibility(View.VISIBLE);
-    }
-
-    @Override
-    protected void onResume() {
-        super.onResume();
-        setDrawerMenuItemChecked(R.id.nav_notifications);
-    }
-
-    @Override
-    public void onRemovedNotification(boolean isSuccess) {
-        if (!isSuccess) {
-            DisplayUtils.showSnackMessage(this, getString(R.string.remove_notification_failed));
-            fetchAndSetData();
-        }
-    }
-
-    @Override
-    public void removeNotification(NotificationListAdapter.NotificationViewHolder holder) {
-        adapter.removeNotification(holder);
-
-        if (adapter.getItemCount() == 0) {
-            setEmptyContent(getString(R.string.notifications_no_results_headline), getString(R.string.notifications_no_results_message));
-            binding.swipeContainingList.setVisibility(View.GONE);
-            binding.loadingContent.setVisibility(View.GONE);
-            binding.swipeContainingEmpty.setVisibility(View.VISIBLE);
-        }
-    }
-
-    @Override
-    public void onRemovedAllNotifications(boolean isSuccess) {
-        if (isSuccess) {
-            adapter.removeAllNotifications();
-            setEmptyContent(getString(R.string.notifications_no_results_headline), getString(R.string.notifications_no_results_message));
-            binding.loadingContent.setVisibility(View.GONE);
-            binding.swipeContainingList.setVisibility(View.GONE);
-            binding.swipeContainingEmpty.setVisibility(View.VISIBLE);
-        } else {
-            DisplayUtils.showSnackMessage(this, getString(R.string.clear_notifications_failed));
-        }
-    }
-
-    @Override
-    public void onActionCallback(boolean isSuccess,
-                                 Notification notification,
-                                 NotificationListAdapter.NotificationViewHolder holder) {
-        if (isSuccess) {
-            adapter.removeNotification(holder);
-        } else {
-            adapter.setButtons(holder, notification);
-            DisplayUtils.showSnackMessage(this, getString(R.string.notification_action_failed));
-        }
-    }
-}

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

@@ -0,0 +1,376 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Andy Scherzinger
+ * @author Mario Danic
+ * @author Chris Narkiewicz
+ * Copyright (C) 2017 Andy Scherzinger
+ * Copyright (C) 2017 Mario Danic
+ * Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+package com.owncloud.android.ui.activity
+
+import android.os.Bundle
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import androidx.annotation.VisibleForTesting
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.google.android.material.snackbar.Snackbar
+import com.nextcloud.client.account.User
+import com.nextcloud.client.account.UserAccountManager
+import com.nextcloud.client.jobs.NotificationWork
+import com.nextcloud.client.network.ClientFactory.CreationException
+import com.nextcloud.java.util.Optional
+import com.owncloud.android.R
+import com.owncloud.android.databinding.NotificationsLayoutBinding
+import com.owncloud.android.datamodel.ArbitraryDataProvider
+import com.owncloud.android.datamodel.ArbitraryDataProviderImpl
+import com.owncloud.android.lib.common.OwnCloudClient
+import com.owncloud.android.lib.common.utils.Log_OC
+import com.owncloud.android.lib.resources.notifications.GetNotificationsRemoteOperation
+import com.owncloud.android.lib.resources.notifications.models.Notification
+import com.owncloud.android.ui.adapter.NotificationListAdapter
+import com.owncloud.android.ui.adapter.NotificationListAdapter.NotificationViewHolder
+import com.owncloud.android.ui.asynctasks.DeleteAllNotificationsTask
+import com.owncloud.android.ui.notifications.NotificationsContract
+import com.owncloud.android.utils.DisplayUtils
+import com.owncloud.android.utils.PushUtils
+
+/**
+ * Activity displaying all server side stored notification items.
+ */
+class NotificationsActivity : DrawerActivity(), NotificationsContract.View {
+
+    private lateinit var binding: NotificationsLayoutBinding
+
+    private var adapter: NotificationListAdapter? = null
+    private var snackbar: Snackbar? = null
+    private var client: OwnCloudClient? = null
+    private var optionalUser: Optional<User>? = null
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        Log_OC.v(TAG, "onCreate() start")
+
+        super.onCreate(savedInstanceState)
+
+        binding = NotificationsLayoutBinding.inflate(layoutInflater)
+        setContentView(binding.root)
+
+        optionalUser = user
+
+        intent?.let {
+            it.extras?.let { bundle ->
+                setupUser(bundle)
+            }
+        }
+
+        setupToolbar()
+        updateActionBarTitleAndHomeButtonByString(getString(R.string.drawer_item_notifications))
+        setupDrawer(R.id.nav_notifications)
+
+        if (optionalUser?.isPresent == false) {
+            showError()
+        }
+
+        setupContainingList()
+        setupPushWarning()
+        setupContent()
+    }
+
+    private fun setupContainingList() {
+        viewThemeUtils.androidx.themeSwipeRefreshLayout(binding.swipeContainingList)
+        viewThemeUtils.androidx.themeSwipeRefreshLayout(binding.swipeContainingEmpty)
+        binding.swipeContainingList.setOnRefreshListener {
+            setLoadingMessage()
+            binding.swipeContainingList.isRefreshing = true
+            fetchAndSetData()
+        }
+        binding.swipeContainingEmpty.setOnRefreshListener {
+            setLoadingMessageEmpty()
+            fetchAndSetData()
+        }
+    }
+
+    private fun setupUser(bundle: Bundle) {
+        val accountName = bundle.getString(NotificationWork.KEY_NOTIFICATION_ACCOUNT)
+
+        if (accountName != null && optionalUser?.isPresent == true) {
+            val user = optionalUser?.get()
+            if (user?.accountName.equals(accountName, ignoreCase = true)) {
+                accountManager.setCurrentOwnCloudAccount(accountName)
+                setUser(userAccountManager.user)
+                optionalUser = getUser()
+            }
+        }
+    }
+
+    private fun showError() {
+        runOnUiThread {
+            setEmptyContent(
+                getString(R.string.notifications_no_results_headline),
+                getString(R.string.account_not_found)
+            )
+        }
+        return
+    }
+
+    private fun setupPushWarning() {
+        if (!resources.getBoolean(R.bool.show_push_warning)) {
+            return
+        }
+
+        if (snackbar != null) {
+            if (snackbar?.isShown == false) {
+                snackbar?.show()
+            }
+        } else {
+            val pushUrl = resources.getString(R.string.push_server_url)
+            if (pushUrl.isEmpty()) {
+                snackbar = Snackbar.make(
+                    binding.emptyList.emptyListView,
+                    R.string.push_notifications_not_implemented,
+                    Snackbar.LENGTH_INDEFINITE
+                )
+            } else {
+                val arbitraryDataProvider: ArbitraryDataProvider = ArbitraryDataProviderImpl(this)
+                val accountName: String = if (optionalUser?.isPresent == true) {
+                    optionalUser?.get()?.accountName ?: ""
+                } else {
+                    ""
+                }
+                val usesOldLogin = arbitraryDataProvider.getBooleanValue(
+                    accountName,
+                    UserAccountManager.ACCOUNT_USES_STANDARD_PASSWORD
+                )
+
+                if (usesOldLogin) {
+                    snackbar = Snackbar.make(
+                        binding.emptyList.emptyListView,
+                        R.string.push_notifications_old_login,
+                        Snackbar.LENGTH_INDEFINITE
+                    )
+                } else {
+                    val pushValue = arbitraryDataProvider.getValue(accountName, PushUtils.KEY_PUSH)
+                    if (pushValue.isEmpty()) {
+                        snackbar = Snackbar.make(
+                            binding.emptyList.emptyListView,
+                            R.string.push_notifications_temp_error,
+                            Snackbar.LENGTH_INDEFINITE
+                        )
+                    }
+                }
+            }
+
+            if (snackbar != null && snackbar?.isShown == false) {
+                snackbar?.show()
+            }
+        }
+    }
+
+    override fun openDrawer() {
+        super.openDrawer()
+        if (snackbar != null && snackbar?.isShown == true) {
+            snackbar?.dismiss()
+        }
+    }
+
+    override fun closeDrawer() {
+        super.closeDrawer()
+        setupPushWarning()
+    }
+
+    /**
+     * sets up the UI elements and loads all notification items.
+     */
+    private fun setupContent() {
+        binding.emptyList.emptyListIcon.setImageResource(R.drawable.ic_notification)
+        setLoadingMessageEmpty()
+        val layoutManager = LinearLayoutManager(this)
+        binding.list.layoutManager = layoutManager
+        fetchAndSetData()
+    }
+
+    @VisibleForTesting
+    fun populateList(notifications: List<Notification>?) {
+        initializeAdapter()
+        adapter?.setNotificationItems(notifications)
+        binding.loadingContent.visibility = View.GONE
+
+        if (notifications?.isNotEmpty() == true) {
+            binding.swipeContainingEmpty.visibility = View.GONE
+            binding.swipeContainingList.visibility = View.VISIBLE
+        } else {
+            setEmptyContent(
+                getString(R.string.notifications_no_results_headline),
+                getString(R.string.notifications_no_results_message)
+            )
+            binding.swipeContainingList.visibility = View.GONE
+            binding.swipeContainingEmpty.visibility = View.VISIBLE
+        }
+    }
+
+    private fun fetchAndSetData() {
+        val t = Thread {
+            initializeAdapter()
+            val getRemoteNotificationOperation = GetNotificationsRemoteOperation()
+            val result = getRemoteNotificationOperation.execute(client)
+            if (result.isSuccess && result.resultData != null) {
+                runOnUiThread { populateList(result.resultData) }
+            } else {
+                Log_OC.d(TAG, result.logMessage)
+                // show error
+                runOnUiThread {
+                    setEmptyContent(
+                        getString(R.string.notifications_no_results_headline),
+                        result.logMessage
+                    )
+                }
+            }
+            hideRefreshLayoutLoader()
+        }
+        t.start()
+    }
+
+    private fun initializeClient() {
+        if (client == null && optionalUser?.isPresent == true) {
+            try {
+                val user = optionalUser?.get()
+                client = clientFactory.create(user)
+            } catch (e: CreationException) {
+                Log_OC.e(TAG, "Error initializing client", e)
+            }
+        }
+    }
+
+    private fun initializeAdapter() {
+        initializeClient()
+        if (adapter == null) {
+            adapter = NotificationListAdapter(client, this, viewThemeUtils)
+            binding.list.adapter = adapter
+        }
+    }
+
+    private fun hideRefreshLayoutLoader() {
+        runOnUiThread {
+            binding.swipeContainingList.isRefreshing = false
+            binding.swipeContainingEmpty.isRefreshing = false
+        }
+    }
+
+    override fun onCreateOptionsMenu(menu: Menu): Boolean {
+        menuInflater.inflate(R.menu.activity_notifications, menu)
+        return true
+    }
+
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        var retval = true
+        val itemId = item.itemId
+        if (itemId == android.R.id.home) {
+            if (isDrawerOpen) {
+                closeDrawer()
+            } else {
+                openDrawer()
+            }
+        } else if (itemId == R.id.action_empty_notifications) {
+            DeleteAllNotificationsTask(client, this).execute()
+        } else {
+            retval = super.onOptionsItemSelected(item)
+        }
+        return retval
+    }
+
+    private fun setLoadingMessage() {
+        binding.swipeContainingEmpty.visibility = View.GONE
+    }
+
+    @VisibleForTesting
+    fun setLoadingMessageEmpty() {
+        binding.swipeContainingList.visibility = View.GONE
+        binding.emptyList.emptyListView.visibility = View.GONE
+        binding.loadingContent.visibility = View.VISIBLE
+    }
+
+    @VisibleForTesting
+    fun setEmptyContent(headline: String?, message: String?) {
+        binding.swipeContainingList.visibility = View.GONE
+        binding.loadingContent.visibility = View.GONE
+        binding.swipeContainingEmpty.visibility = View.VISIBLE
+        binding.emptyList.emptyListView.visibility = View.VISIBLE
+        binding.emptyList.emptyListViewHeadline.text = headline
+        binding.emptyList.emptyListViewText.text = message
+        binding.emptyList.emptyListIcon.setImageResource(R.drawable.ic_notification)
+        binding.emptyList.emptyListViewText.visibility = View.VISIBLE
+        binding.emptyList.emptyListIcon.visibility = View.VISIBLE
+    }
+
+    override fun onResume() {
+        super.onResume()
+        setDrawerMenuItemChecked(R.id.nav_notifications)
+    }
+
+    override fun onRemovedNotification(isSuccess: Boolean) {
+        if (!isSuccess) {
+            DisplayUtils.showSnackMessage(this, getString(R.string.remove_notification_failed))
+            fetchAndSetData()
+        }
+    }
+
+    override fun removeNotification(holder: NotificationViewHolder) {
+        adapter?.removeNotification(holder)
+        if (adapter?.itemCount == 0) {
+            setEmptyContent(
+                getString(R.string.notifications_no_results_headline),
+                getString(R.string.notifications_no_results_message)
+            )
+            binding.swipeContainingList.visibility = View.GONE
+            binding.loadingContent.visibility = View.GONE
+            binding.swipeContainingEmpty.visibility = View.VISIBLE
+        }
+    }
+
+    override fun onRemovedAllNotifications(isSuccess: Boolean) {
+        if (isSuccess) {
+            adapter?.removeAllNotifications()
+            setEmptyContent(
+                getString(R.string.notifications_no_results_headline),
+                getString(R.string.notifications_no_results_message)
+            )
+            binding.loadingContent.visibility = View.GONE
+            binding.swipeContainingList.visibility = View.GONE
+            binding.swipeContainingEmpty.visibility = View.VISIBLE
+        } else {
+            DisplayUtils.showSnackMessage(this, getString(R.string.clear_notifications_failed))
+        }
+    }
+
+    override fun onActionCallback(
+        isSuccess: Boolean,
+        notification: Notification,
+        holder: NotificationViewHolder
+    ) {
+        if (isSuccess) {
+            adapter?.removeNotification(holder)
+        } else {
+            adapter?.setButtons(holder, notification)
+            DisplayUtils.showSnackMessage(this, getString(R.string.notification_action_failed))
+        }
+    }
+
+    companion object {
+        private val TAG = NotificationsActivity::class.java.simpleName
+    }
+}

+ 0 - 478
app/src/main/java/com/owncloud/android/ui/activity/PassCodeActivity.java

@@ -1,478 +0,0 @@
-/*
- *   ownCloud Android client application
- *
- *   @author Bartek Przybylski
- *   @author masensio
- *   @author David A. Velasco
- *   Copyright (C) 2011 Bartek Przybylski
- *   Copyright (C) 2015 ownCloud Inc.
- *   Copyright (C) 2020 Kwon Yuna <yunaghgh@naver.com>
- *
- *   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/>.
- *
- */
-package com.owncloud.android.ui.activity;
-
-import android.content.Intent;
-import android.os.Bundle;
-import android.text.Editable;
-import android.text.TextUtils;
-import android.text.TextWatcher;
-import android.view.KeyEvent;
-import android.view.View;
-import android.view.Window;
-import android.view.inputmethod.InputMethodManager;
-import android.widget.EditText;
-
-import com.google.android.material.snackbar.Snackbar;
-import com.nextcloud.client.di.Injectable;
-import com.nextcloud.client.preferences.AppPreferences;
-import com.owncloud.android.R;
-import com.owncloud.android.authentication.PassCodeManager;
-import com.owncloud.android.databinding.PasscodelockBinding;
-import com.owncloud.android.lib.common.utils.Log_OC;
-import com.owncloud.android.ui.components.PassCodeEditText;
-import com.owncloud.android.utils.theme.ViewThemeUtils;
-
-import java.util.Arrays;
-
-import javax.inject.Inject;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.VisibleForTesting;
-import androidx.appcompat.app.AppCompatActivity;
-
-public class PassCodeActivity extends AppCompatActivity implements Injectable {
-
-    private static final String TAG = PassCodeActivity.class.getSimpleName();
-    private static final String KEY_PASSCODE_DIGITS = "PASSCODE_DIGITS";
-    private static final String KEY_CONFIRMING_PASSCODE = "CONFIRMING_PASSCODE";
-
-    public final static String ACTION_REQUEST_WITH_RESULT = "ACTION_REQUEST_WITH_RESULT";
-    public final static String ACTION_CHECK_WITH_RESULT = "ACTION_CHECK_WITH_RESULT";
-    public final static String ACTION_CHECK = "ACTION_CHECK";
-    public final static String KEY_PASSCODE = "KEY_PASSCODE";
-    public final static String KEY_CHECK_RESULT = "KEY_CHECK_RESULT";
-
-    public final static String PREFERENCE_PASSCODE_D = "PrefPinCode";
-    public final static String PREFERENCE_PASSCODE_D1 = "PrefPinCode1";
-    public final static String PREFERENCE_PASSCODE_D2 = "PrefPinCode2";
-    public final static String PREFERENCE_PASSCODE_D3 = "PrefPinCode3";
-    public final static String PREFERENCE_PASSCODE_D4 = "PrefPinCode4";
-
-    @Inject AppPreferences preferences;
-    @Inject PassCodeManager passCodeManager;
-    @Inject ViewThemeUtils viewThemeUtils;
-    private PasscodelockBinding binding;
-    private final PassCodeEditText[] passCodeEditTexts = new PassCodeEditText[4];
-    private String[] passCodeDigits = {"", "", "", ""};
-    private boolean confirmingPassCode;
-    private boolean changed = true; // to control that only one blocks jump
-
-    /**
-     * Initializes the activity.
-     * <p>
-     * An intent with a valid ACTION is expected; if none is found, an {@link IllegalArgumentException} will be thrown.
-     *
-     * @param savedInstanceState Previously saved state - irrelevant in this case
-     */
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        binding = PasscodelockBinding.inflate(getLayoutInflater());
-        setContentView(binding.getRoot());
-
-        viewThemeUtils.platform.colorTextButtons(binding.cancel);
-
-        passCodeEditTexts[0] = binding.txt0;
-        passCodeEditTexts[1] = binding.txt1;
-        passCodeEditTexts[2] = binding.txt2;
-        passCodeEditTexts[3] = binding.txt3;
-
-        for (EditText passCodeEditText : passCodeEditTexts) {
-            viewThemeUtils.platform.colorEditText(passCodeEditText);
-        }
-
-        passCodeEditTexts[0].requestFocus();
-
-        Window window = getWindow();
-        if (window != null) {
-            window.setSoftInputMode(android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
-        }
-
-        if (ACTION_CHECK.equals(getIntent().getAction())) {
-            /// this is a pass code request; the user has to input the right value
-            binding.header.setText(R.string.pass_code_enter_pass_code);
-            binding.explanation.setVisibility(View.INVISIBLE);
-            setCancelButtonEnabled(false);      // no option to cancel
-
-            showDelay();
-
-        } else if (ACTION_REQUEST_WITH_RESULT.equals(getIntent().getAction())) {
-            if (savedInstanceState != null) {
-                confirmingPassCode = savedInstanceState.getBoolean(PassCodeActivity.KEY_CONFIRMING_PASSCODE);
-                passCodeDigits = savedInstanceState.getStringArray(PassCodeActivity.KEY_PASSCODE_DIGITS);
-            }
-            if (confirmingPassCode) {
-                // the app was in the passcode confirmation
-                requestPassCodeConfirmation();
-            } else {
-                // pass code preference has just been activated in SettingsActivity;
-                // will receive and confirm pass code value
-                binding.header.setText(R.string.pass_code_configure_your_pass_code);
-
-                binding.explanation.setVisibility(View.VISIBLE);
-            }
-            setCancelButtonEnabled(true);
-
-        } else if (ACTION_CHECK_WITH_RESULT.equals(getIntent().getAction())) {
-            // pass code preference has just been disabled in SettingsActivity;
-            // will confirm user knows pass code, then remove it
-            binding.header.setText(R.string.pass_code_remove_your_pass_code);
-            binding.explanation.setVisibility(View.INVISIBLE);
-            setCancelButtonEnabled(true);
-
-        } else {
-            throw new IllegalArgumentException("A valid ACTION is needed in the Intent passed to " + TAG);
-        }
-
-        setTextListeners();
-    }
-
-    /**
-     * Enables or disables the cancel button to allow the user interrupt the ACTION requested to the activity.
-     *
-     * @param enabled 'True' makes the cancel button available, 'false' hides it.
-     */
-    protected void setCancelButtonEnabled(boolean enabled) {
-        if (enabled) {
-            binding.cancel.setVisibility(View.VISIBLE);
-            binding.cancel.setOnClickListener(v -> finish());
-        } else {
-            binding.cancel.setVisibility(View.INVISIBLE);
-            binding.cancel.setOnClickListener(null);
-        }
-    }
-
-    @VisibleForTesting
-    public PasscodelockBinding getBinding() {
-        return binding;
-    }
-
-    /**
-     * Binds the appropriate listeners to the input boxes receiving each digit of the pass code.
-     */
-    protected void setTextListeners() {
-        for (int i = 0; i < passCodeEditTexts.length; i++) {
-            final PassCodeEditText editText = passCodeEditTexts[i];
-            boolean isLast = (i == 3);
-
-            editText.addTextChangedListener(new PassCodeDigitTextWatcher(i, isLast));
-            if (i > 0) {
-                setOnKeyListener(i);
-            }
-
-            int finalIndex = i;
-            editText.setOnFocusChangeListener((v, hasFocus) -> onPassCodeEditTextFocusChange(finalIndex));
-        }
-    }
-
-    private void onPassCodeEditTextFocusChange(final int passCodeIndex) {
-        for (int i = 0; i < passCodeIndex; i++) {
-            if (TextUtils.isEmpty(passCodeEditTexts[i].getText())) {
-                passCodeEditTexts[i].requestFocus();
-                break;
-            }
-        }
-    }
-
-    private void setOnKeyListener(final int passCodeIndex) {
-        passCodeEditTexts[passCodeIndex].setOnKeyListener((v, keyCode, event) -> {
-            if (keyCode == KeyEvent.KEYCODE_DEL && changed) {
-                passCodeEditTexts[passCodeIndex - 1].requestFocus();
-                if (!confirmingPassCode) {
-                    passCodeDigits[passCodeIndex - 1] = "";
-                }
-                passCodeEditTexts[passCodeIndex - 1].setText("");
-                changed = false;
-
-            } else if (!changed) {
-                changed = true;
-            }
-            return false;
-        });
-    }
-
-    /**
-     * Processes the pass code entered by the user just after the last digit was in.
-     * <p>
-     * Takes into account the action requested to the activity, the currently saved pass code and the previously typed
-     * pass code, if any.
-     */
-    private void processFullPassCode() {
-        if (ACTION_CHECK.equals(getIntent().getAction())) {
-            if (checkPassCode()) {
-                preferences.resetPinWrongAttempts();
-
-                /// pass code accepted in request, user is allowed to access the app
-                passCodeManager.updateLockTimestamp();
-                hideSoftKeyboard();
-                finish();
-
-            } else {
-                preferences.increasePinWrongAttempts();
-
-                showErrorAndRestart(R.string.pass_code_wrong, R.string.pass_code_enter_pass_code, View.INVISIBLE);
-            }
-
-        } else if (ACTION_CHECK_WITH_RESULT.equals(getIntent().getAction())) {
-            if (checkPassCode()) {
-                passCodeManager.updateLockTimestamp();
-                Intent resultIntent = new Intent();
-                resultIntent.putExtra(KEY_CHECK_RESULT, true);
-                setResult(RESULT_OK, resultIntent);
-                hideSoftKeyboard();
-                finish();
-            } else {
-                showErrorAndRestart(R.string.pass_code_wrong, R.string.pass_code_enter_pass_code, View.INVISIBLE);
-            }
-
-        } else if (ACTION_REQUEST_WITH_RESULT.equals(getIntent().getAction())) {
-            /// enabling pass code
-            if (!confirmingPassCode) {
-                requestPassCodeConfirmation();
-
-            } else if (confirmPassCode()) {
-                /// confirmed: user typed the same pass code twice
-                savePassCodeAndExit();
-
-            } else {
-                showErrorAndRestart(R.string.pass_code_mismatch, R.string.pass_code_configure_your_pass_code, View.VISIBLE);
-            }
-        }
-    }
-
-    private void hideSoftKeyboard() {
-        View focusedView = getCurrentFocus();
-        if (focusedView != null) {
-            InputMethodManager inputMethodManager =
-                (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
-            inputMethodManager.hideSoftInputFromWindow(
-                focusedView.getWindowToken(),
-                0);
-        }
-    }
-
-    private void showErrorAndRestart(int errorMessage, int headerMessage, int explanationVisibility) {
-        Arrays.fill(passCodeDigits, null);
-        Snackbar.make(findViewById(android.R.id.content), getString(errorMessage), Snackbar.LENGTH_LONG).show();
-        binding.header.setText(headerMessage);                          // TODO check if really needed
-        binding.explanation.setVisibility(explanationVisibility); // TODO check if really needed
-        clearBoxes();
-
-        showDelay();
-    }
-
-
-    /**
-     * Ask to the user for retyping the pass code just entered before saving it as the current pass code.
-     */
-    protected void requestPassCodeConfirmation() {
-        clearBoxes();
-        binding.header.setText(R.string.pass_code_reenter_your_pass_code);
-        binding.explanation.setVisibility(View.INVISIBLE);
-        confirmingPassCode = true;
-    }
-
-    /**
-     * Compares pass code entered by the user with the value currently saved in the app.
-     *
-     * @return 'True' if entered pass code equals to the saved one.
-     */
-    protected boolean checkPassCode() {
-        String[] savedPassCodeDigits = preferences.getPassCode();
-
-        boolean result = true;
-        for (int i = 0; i < passCodeDigits.length && result; i++) {
-            result = passCodeDigits[i] != null && passCodeDigits[i].equals(savedPassCodeDigits[i]);
-        }
-        return result;
-    }
-
-    /**
-     * Compares pass code retyped by the user in the input fields with the value entered just before.
-     *
-     * @return 'True' if retyped pass code equals to the entered before.
-     */
-    protected boolean confirmPassCode() {
-        confirmingPassCode = false;
-
-        for (int i = 0; i < passCodeEditTexts.length; i++) {
-            Editable passCodeText = passCodeEditTexts[i].getText();
-            if (passCodeText == null || !passCodeText.toString().equals(passCodeDigits[i])) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    /**
-     * Sets the input fields to empty strings and puts the focus on the first one.
-     */
-    protected void clearBoxes() {
-        for (EditText mPassCodeEditText : passCodeEditTexts) {
-            mPassCodeEditText.setText("");
-        }
-        passCodeEditTexts[0].requestFocus();
-    }
-
-    /**
-     * Overrides click on the BACK arrow to correctly cancel ACTION_ENABLE or ACTION_DISABLE, while preventing than
-     * ACTION_CHECK may be worked around.
-     *
-     * @param keyCode Key code of the key that triggered the down event.
-     * @param event   Event triggered.
-     * @return 'True' when the key event was processed by this method.
-     */
-    @Override
-    public boolean onKeyDown(int keyCode, KeyEvent event) {
-        if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) {
-            if (ACTION_CHECK.equals(getIntent().getAction())) {
-                moveTaskToBack(true);
-                finishAndRemoveTask();
-            } else if (ACTION_REQUEST_WITH_RESULT.equals(getIntent().getAction()) ||
-                ACTION_CHECK_WITH_RESULT.equals(getIntent().getAction())) {
-                finish();
-            }// else, do nothing, but report that the key was consumed to stay alive
-            return true;
-        }
-        return super.onKeyDown(keyCode, event);
-    }
-
-    /**
-     * Saves the pass code input by the user as the current pass code.
-     */
-    protected void savePassCodeAndExit() {
-        Intent resultIntent = new Intent();
-        resultIntent.putExtra(KEY_PASSCODE,
-                              passCodeDigits[0] + passCodeDigits[1] + passCodeDigits[2] + passCodeDigits[3]);
-
-        setResult(RESULT_OK, resultIntent);
-
-        passCodeManager.updateLockTimestamp();
-
-        finish();
-    }
-
-    private void showDelay() {
-        int delay = preferences.pinBruteForceDelay();
-
-        if (delay > 0) {
-            binding.explanation.setText(R.string.brute_force_delay);
-            binding.explanation.setVisibility(View.VISIBLE);
-            binding.txt0.setEnabled(false);
-            binding.txt1.setEnabled(false);
-            binding.txt2.setEnabled(false);
-            binding.txt3.setEnabled(false);
-
-            new Thread(new Runnable() {
-                @Override
-                public void run() {
-                    try {
-                        Thread.sleep(delay * 1000L);
-
-                        runOnUiThread(() -> {
-                            binding.explanation.setVisibility(View.INVISIBLE);
-                            binding.txt0.setEnabled(true);
-                            binding.txt1.setEnabled(true);
-                            binding.txt2.setEnabled(true);
-                            binding.txt3.setEnabled(true);
-                        });
-                    } catch (InterruptedException e) {
-                        Log_OC.e(this, "Could not delay password input prompt");
-                    }
-                }
-            }).start();
-        }
-    }
-
-    @Override
-    public void onSaveInstanceState(@NonNull Bundle outState) {
-        super.onSaveInstanceState(outState);
-        outState.putBoolean(PassCodeActivity.KEY_CONFIRMING_PASSCODE, confirmingPassCode);
-        outState.putStringArray(PassCodeActivity.KEY_PASSCODE_DIGITS, passCodeDigits);
-    }
-
-    private class PassCodeDigitTextWatcher implements TextWatcher {
-
-        private int mIndex = -1;
-        private boolean mLastOne;
-
-        /**
-         * Constructor
-         *
-         * @param index   Position in the pass code of the input field that will be bound to this watcher.
-         * @param lastOne 'True' means that watcher corresponds to the last position of the pass code.
-         */
-        PassCodeDigitTextWatcher(int index, boolean lastOne) {
-            mIndex = index;
-            mLastOne = lastOne;
-
-            if (mIndex < 0) {
-                throw new IllegalArgumentException(
-                    "Invalid index in " + PassCodeDigitTextWatcher.class.getSimpleName() +
-                        " constructor"
-                );
-            }
-        }
-
-        private int next() {
-            return mLastOne ? 0 : mIndex + 1;
-        }
-
-        /**
-         * Performs several actions when the user types a digit in an input field: - saves the input digit to the state
-         * of the activity; this will allow retyping the pass code to confirm it. - moves the focus automatically to the
-         * next field - for the last field, triggers the processing of the full pass code
-         *
-         * @param s Changed text
-         */
-        @Override
-        public void afterTextChanged(Editable s) {
-            if (s.length() > 0) {
-                if (!confirmingPassCode) {
-                    Editable passCodeText = passCodeEditTexts[mIndex].getText();
-
-                    if (passCodeText != null) {
-                        passCodeDigits[mIndex] = passCodeText.toString();
-                    }
-                }
-
-                if (mLastOne) {
-                    processFullPassCode();
-                } else {
-                    passCodeEditTexts[next()].requestFocus();
-                }
-
-            } else {
-                Log_OC.d(TAG, "Text box " + mIndex + " was cleaned");
-            }
-        }
-
-        @Override
-        public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
-
-        @Override
-        public void onTextChanged(CharSequence s, int start, int before, int count) {}
-    }
-
-}

+ 437 - 0
app/src/main/java/com/owncloud/android/ui/activity/PassCodeActivity.kt

@@ -0,0 +1,437 @@
+/*
+ *   ownCloud Android client application
+ *
+ *   @author Bartek Przybylski
+ *   @author masensio
+ *   @author David A. Velasco
+ *   Copyright (C) 2011 Bartek Przybylski
+ *   Copyright (C) 2015 ownCloud Inc.
+ *   Copyright (C) 2020 Kwon Yuna <yunaghgh@naver.com>
+ *
+ *   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/>.
+ *
+ */
+package com.owncloud.android.ui.activity
+
+import android.content.Intent
+import android.os.Bundle
+import android.text.Editable
+import android.text.TextUtils
+import android.text.TextWatcher
+import android.view.KeyEvent
+import android.view.View
+import android.view.WindowManager
+import android.view.inputmethod.InputMethodManager
+import androidx.annotation.VisibleForTesting
+import androidx.appcompat.app.AppCompatActivity
+import com.google.android.material.snackbar.Snackbar
+import com.nextcloud.android.common.ui.theme.utils.ColorRole
+import com.nextcloud.client.di.Injectable
+import com.nextcloud.client.preferences.AppPreferences
+import com.owncloud.android.R
+import com.owncloud.android.authentication.PassCodeManager
+import com.owncloud.android.databinding.PasscodelockBinding
+import com.owncloud.android.lib.common.utils.Log_OC
+import com.owncloud.android.ui.components.PassCodeEditText
+import com.owncloud.android.utils.theme.ViewThemeUtils
+import java.util.Arrays
+import javax.inject.Inject
+
+@Suppress("TooManyFunctions", "MagicNumber")
+class PassCodeActivity : AppCompatActivity(), Injectable {
+
+    companion object {
+        private val TAG = PassCodeActivity::class.java.simpleName
+
+        private const val KEY_PASSCODE_DIGITS = "PASSCODE_DIGITS"
+        private const val KEY_CONFIRMING_PASSCODE = "CONFIRMING_PASSCODE"
+        const val ACTION_REQUEST_WITH_RESULT = "ACTION_REQUEST_WITH_RESULT"
+        const val ACTION_CHECK_WITH_RESULT = "ACTION_CHECK_WITH_RESULT"
+        const val ACTION_CHECK = "ACTION_CHECK"
+        const val KEY_PASSCODE = "KEY_PASSCODE"
+        const val KEY_CHECK_RESULT = "KEY_CHECK_RESULT"
+        const val PREFERENCE_PASSCODE_D = "PrefPinCode"
+        const val PREFERENCE_PASSCODE_D1 = "PrefPinCode1"
+        const val PREFERENCE_PASSCODE_D2 = "PrefPinCode2"
+        const val PREFERENCE_PASSCODE_D3 = "PrefPinCode3"
+        const val PREFERENCE_PASSCODE_D4 = "PrefPinCode4"
+    }
+
+    @JvmField
+    @Inject
+    var preferences: AppPreferences? = null
+
+    @JvmField
+    @Inject
+    var passCodeManager: PassCodeManager? = null
+
+    @JvmField
+    @Inject
+    var viewThemeUtils: ViewThemeUtils? = null
+
+    @get:VisibleForTesting
+    lateinit var binding: PasscodelockBinding
+        private set
+
+    private val passCodeEditTexts = arrayOfNulls<PassCodeEditText>(4)
+    private var passCodeDigits: Array<String?>? = arrayOf("", "", "", "")
+    private var confirmingPassCode = false
+    private var changed = true // to control that only one blocks jump
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        binding = PasscodelockBinding.inflate(layoutInflater)
+        setContentView(binding.root)
+
+        applyTint()
+        setupPasscodeEditTexts()
+        setSoftInputMode()
+        setupUI(savedInstanceState)
+        setTextListeners()
+    }
+
+    private fun applyTint() {
+        viewThemeUtils?.platform?.colorViewBackground(binding.cardViewContent, ColorRole.SURFACE_VARIANT)
+        viewThemeUtils?.material?.colorMaterialButtonPrimaryBorderless(binding.cancel)
+    }
+
+    private fun setupPasscodeEditTexts() {
+        passCodeEditTexts[0] = binding.txt0
+        passCodeEditTexts[1] = binding.txt1
+        passCodeEditTexts[2] = binding.txt2
+        passCodeEditTexts[3] = binding.txt3
+
+        passCodeEditTexts.forEach {
+            it?.let { viewThemeUtils?.platform?.colorEditText(it) }
+        }
+
+        passCodeEditTexts[0]?.requestFocus()
+    }
+
+    private fun setSoftInputMode() {
+        window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)
+    }
+
+    private fun setupUI(savedInstanceState: Bundle?) {
+        if (ACTION_CHECK == intent.action) {
+            // / this is a pass code request; the user has to input the right value
+            binding.header.setText(R.string.pass_code_enter_pass_code)
+            binding.explanation.visibility = View.INVISIBLE
+            setCancelButtonEnabled(false) // no option to cancel
+            showDelay()
+        } else if (ACTION_REQUEST_WITH_RESULT == intent.action) {
+            if (savedInstanceState != null) {
+                confirmingPassCode = savedInstanceState.getBoolean(KEY_CONFIRMING_PASSCODE)
+                passCodeDigits = savedInstanceState.getStringArray(KEY_PASSCODE_DIGITS)
+            }
+            if (confirmingPassCode) {
+                // the app was in the passcode confirmation
+                requestPassCodeConfirmation()
+            } else {
+                // pass code preference has just been activated in SettingsActivity;
+                // will receive and confirm pass code value
+                binding.header.setText(R.string.pass_code_configure_your_pass_code)
+                binding.explanation.visibility = View.VISIBLE
+            }
+            setCancelButtonEnabled(true)
+        } else if (ACTION_CHECK_WITH_RESULT == intent.action) {
+            // pass code preference has just been disabled in SettingsActivity;
+            // will confirm user knows pass code, then remove it
+            binding.header.setText(R.string.pass_code_remove_your_pass_code)
+            binding.explanation.visibility = View.INVISIBLE
+            setCancelButtonEnabled(true)
+        } else {
+            throw IllegalArgumentException("A valid ACTION is needed in the Intent passed to $TAG")
+        }
+    }
+
+    private fun setCancelButtonEnabled(enabled: Boolean) {
+        binding.cancel.visibility = if (enabled) {
+            View.VISIBLE
+        } else {
+            View.INVISIBLE
+        }
+        binding.cancel.setOnClickListener {
+            if (enabled) {
+                finish()
+            }
+        }
+    }
+
+    private fun setTextListeners() {
+        for (i in passCodeEditTexts.indices) {
+            val editText = passCodeEditTexts[i]
+            val isLast = (i == 3)
+
+            editText?.addTextChangedListener(PassCodeDigitTextWatcher(i, isLast))
+
+            if (i > 0) {
+                setOnKeyListener(i)
+            }
+
+            editText?.onFocusChangeListener = View.OnFocusChangeListener { _: View?, _: Boolean ->
+                onPassCodeEditTextFocusChange(i)
+            }
+        }
+    }
+
+    private fun onPassCodeEditTextFocusChange(passCodeIndex: Int) {
+        for (i in 0 until passCodeIndex) {
+            if (TextUtils.isEmpty(passCodeEditTexts[i]?.text)) {
+                passCodeEditTexts[i]?.requestFocus()
+                break
+            }
+        }
+    }
+
+    private fun setOnKeyListener(passCodeIndex: Int) {
+        passCodeEditTexts[passCodeIndex]?.setOnKeyListener { _: View?, keyCode: Int, _: KeyEvent? ->
+            if (keyCode == KeyEvent.KEYCODE_DEL && changed) {
+                passCodeEditTexts[passCodeIndex - 1]?.requestFocus()
+
+                if (!confirmingPassCode) {
+                    passCodeDigits?.set(passCodeIndex - 1, "")
+                }
+
+                passCodeEditTexts[passCodeIndex - 1]?.setText("")
+
+                changed = false
+            } else if (!changed) {
+                changed = true
+            }
+            false
+        }
+    }
+
+    /**
+     * Processes the pass code entered by the user just after the last digit was in.
+     *
+     *
+     * Takes into account the action requested to the activity, the currently saved pass code and the previously typed
+     * pass code, if any.
+     */
+    private fun processFullPassCode() {
+        if (ACTION_CHECK == intent.action) {
+            if (checkPassCode()) {
+                preferences?.resetPinWrongAttempts()
+
+                // / pass code accepted in request, user is allowed to access the app
+                passCodeManager?.updateLockTimestamp()
+                hideSoftKeyboard()
+                finish()
+            } else {
+                preferences?.increasePinWrongAttempts()
+                showErrorAndRestart(R.string.pass_code_wrong, R.string.pass_code_enter_pass_code, View.INVISIBLE)
+            }
+        } else if (ACTION_CHECK_WITH_RESULT == intent.action) {
+            if (checkPassCode()) {
+                passCodeManager?.updateLockTimestamp()
+
+                val resultIntent = Intent()
+                resultIntent.putExtra(KEY_CHECK_RESULT, true)
+                setResult(RESULT_OK, resultIntent)
+                hideSoftKeyboard()
+                finish()
+            } else {
+                showErrorAndRestart(R.string.pass_code_wrong, R.string.pass_code_enter_pass_code, View.INVISIBLE)
+            }
+        } else if (ACTION_REQUEST_WITH_RESULT == intent.action) {
+            // / enabling pass code
+            if (!confirmingPassCode) {
+                requestPassCodeConfirmation()
+            } else if (confirmPassCode()) {
+                // / confirmed: user typed the same pass code twice
+                savePassCodeAndExit()
+            } else {
+                showErrorAndRestart(
+                    R.string.pass_code_mismatch,
+                    R.string.pass_code_configure_your_pass_code,
+                    View.VISIBLE
+                )
+            }
+        }
+    }
+
+    private fun hideSoftKeyboard() {
+        currentFocus?.let {
+            val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
+            inputMethodManager.hideSoftInputFromWindow(
+                it.windowToken,
+                0
+            )
+        }
+    }
+
+    private fun showErrorAndRestart(errorMessage: Int, headerMessage: Int, explanationVisibility: Int) {
+        passCodeDigits?.let { Arrays.fill(it, null) }
+
+        Snackbar.make(findViewById(android.R.id.content), getString(errorMessage), Snackbar.LENGTH_LONG).show()
+        binding.header.setText(headerMessage) // TODO check if really needed
+        binding.explanation.visibility = explanationVisibility // TODO check if really needed
+        clearBoxes()
+        showDelay()
+    }
+
+    /**
+     * Ask to the user for retyping the pass code just entered before saving it as the current pass code.
+     */
+    private fun requestPassCodeConfirmation() {
+        clearBoxes()
+        binding.header.setText(R.string.pass_code_reenter_your_pass_code)
+        binding.explanation.visibility = View.INVISIBLE
+        confirmingPassCode = true
+    }
+
+    private fun checkPassCode(): Boolean {
+        val savedPassCodeDigits = preferences?.passCode
+        return passCodeDigits?.zip(savedPassCodeDigits.orEmpty()) { input, saved ->
+            input != null && input == saved
+        }?.all { it } ?: false
+    }
+
+    private fun confirmPassCode(): Boolean {
+        return passCodeEditTexts.indices.all { i ->
+            passCodeEditTexts[i]?.text.toString() == passCodeDigits!![i]
+        }
+    }
+
+    private fun clearBoxes() {
+        passCodeEditTexts.forEach { it?.text?.clear() }
+        passCodeEditTexts.firstOrNull()?.requestFocus()
+    }
+
+    /**
+     * Overrides click on the BACK arrow to correctly cancel ACTION_ENABLE or ACTION_DISABLE, while preventing than
+     * ACTION_CHECK may be worked around.
+     *
+     * @param keyCode Key code of the key that triggered the down event.
+     * @param event   Event triggered.
+     * @return 'True' when the key event was processed by this method.
+     */
+    override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
+        if (keyCode == KeyEvent.KEYCODE_BACK && event.repeatCount == 0) {
+            if (ACTION_CHECK == intent.action) {
+                moveTaskToBack(true)
+                finishAndRemoveTask()
+            } else if (ACTION_REQUEST_WITH_RESULT == intent.action || ACTION_CHECK_WITH_RESULT == intent.action) {
+                finish()
+            } // else, do nothing, but report that the key was consumed to stay alive
+            return true
+        }
+        return super.onKeyDown(keyCode, event)
+    }
+
+    private fun savePassCodeAndExit() {
+        val resultIntent = Intent()
+        resultIntent.putExtra(
+            KEY_PASSCODE,
+            passCodeDigits!![0] + passCodeDigits!![1] + passCodeDigits!![2] + passCodeDigits!![3]
+        )
+        setResult(RESULT_OK, resultIntent)
+        passCodeManager?.updateLockTimestamp()
+        finish()
+    }
+
+    private fun showDelay() {
+        val delay = preferences?.pinBruteForceDelay() ?: 0
+
+        if (delay <= 0) {
+            return
+        }
+
+        binding.explanation.setText(R.string.brute_force_delay)
+        binding.explanation.visibility = View.VISIBLE
+        binding.txt0.isEnabled = false
+        binding.txt1.isEnabled = false
+        binding.txt2.isEnabled = false
+        binding.txt3.isEnabled = false
+
+        Thread(object : Runnable {
+            override fun run() {
+                try {
+                    Thread.sleep(delay * 1000L)
+
+                    runOnUiThread {
+                        binding.explanation.visibility = View.INVISIBLE
+                        binding.txt0.isEnabled = true
+                        binding.txt1.isEnabled = true
+                        binding.txt2.isEnabled = true
+                        binding.txt3.isEnabled = true
+                    }
+                } catch (e: InterruptedException) {
+                    Log_OC.e(this, "Could not delay password input prompt")
+                }
+            }
+        }).start()
+    }
+
+    public override fun onSaveInstanceState(outState: Bundle) {
+        super.onSaveInstanceState(outState)
+        outState.putBoolean(KEY_CONFIRMING_PASSCODE, confirmingPassCode)
+        outState.putStringArray(KEY_PASSCODE_DIGITS, passCodeDigits)
+    }
+
+    private inner class PassCodeDigitTextWatcher(index: Int, lastOne: Boolean) : TextWatcher {
+        private var mIndex = -1
+        private val mLastOne: Boolean
+
+        init {
+            mIndex = index
+            mLastOne = lastOne
+
+            require(mIndex >= 0) {
+                "Invalid index in " + PassCodeDigitTextWatcher::class.java.simpleName +
+                    " constructor"
+            }
+        }
+
+        private operator fun next(): Int {
+            return if (mLastOne) 0 else mIndex + 1
+        }
+
+        /**
+         * Performs several actions when the user types a digit in an input field: - saves the input digit to the state
+         * of the activity; this will allow retyping the pass code to confirm it. - moves the focus automatically to the
+         * next field - for the last field, triggers the processing of the full pass code
+         *
+         * @param s Changed text
+         */
+        override fun afterTextChanged(s: Editable) {
+            if (s.isNotEmpty()) {
+                if (!confirmingPassCode) {
+                    val passCodeText = passCodeEditTexts[mIndex]?.text
+
+                    if (passCodeText != null) {
+                        passCodeDigits!![mIndex] = passCodeText.toString()
+                    }
+                }
+
+                if (mLastOne) {
+                    processFullPassCode()
+                } else {
+                    passCodeEditTexts[next()]?.requestFocus()
+                }
+            } else {
+                Log_OC.d(TAG, "Text box $mIndex was cleaned")
+            }
+        }
+
+        @Suppress("EmptyFunctionBlock")
+        override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
+        }
+
+        @Suppress("EmptyFunctionBlock")
+        override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
+        }
+    }
+}

+ 0 - 254
app/src/main/java/com/owncloud/android/ui/activity/RichDocumentsEditorWebView.java

@@ -1,254 +0,0 @@
-/*
- * Nextcloud Android client application
- *
- * @author Tobias Kaminsky
- * @author Chris Narkiewicz
- *
- * Copyright (C) 2018 Tobias Kaminsky
- * Copyright (C) 2018 Nextcloud GmbH.
- * Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU 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 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 <https://www.gnu.org/licenses/>.
- */
-
-package com.owncloud.android.ui.activity;
-
-import android.content.Intent;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Bundle;
-import android.text.TextUtils;
-import android.view.KeyEvent;
-import android.webkit.JavascriptInterface;
-
-import com.nextcloud.client.account.CurrentAccountProvider;
-import com.nextcloud.client.account.User;
-import com.nextcloud.client.network.ClientFactory;
-import com.owncloud.android.R;
-import com.owncloud.android.datamodel.OCFile;
-import com.owncloud.android.lib.common.OwnCloudAccount;
-import com.owncloud.android.lib.common.operations.RemoteOperationResult;
-import com.owncloud.android.lib.common.utils.Log_OC;
-import com.owncloud.android.operations.RichDocumentsCreateAssetOperation;
-import com.owncloud.android.ui.asynctasks.PrintAsyncTask;
-import com.owncloud.android.ui.asynctasks.RichDocumentsLoadUrlTask;
-import com.owncloud.android.ui.fragment.OCFileListFragment;
-import com.owncloud.android.utils.DisplayUtils;
-import com.owncloud.android.utils.FileStorageUtils;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.io.File;
-import java.lang.ref.WeakReference;
-
-import javax.inject.Inject;
-
-import androidx.annotation.NonNull;
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-
-/**
- * Opens document for editing via Richdocuments app in a web view
- */
-public class RichDocumentsEditorWebView extends EditorWebView {
-    private static final int REQUEST_REMOTE_FILE = 100;
-    private static final String URL = "URL";
-    private static final String HYPERLINK = "Url";
-    private static final String TYPE = "Type";
-    private static final String PRINT = "print";
-    private static final String SLIDESHOW = "slideshow";
-    private static final String NEW_NAME = "NewName";
-
-    @Inject
-    protected CurrentAccountProvider currentAccountProvider;
-
-    @Inject
-    protected ClientFactory clientFactory;
-
-    @SuppressFBWarnings("ANDROID_WEB_VIEW_JAVASCRIPT_INTERFACE")
-    @Override
-    protected void postOnCreate() {
-        super.postOnCreate();
-
-        getWebView().addJavascriptInterface(new RichDocumentsMobileInterface(), "RichDocumentsMobileInterface");
-
-        // load url in background
-        loadUrl(getIntent().getStringExtra(EXTRA_URL));
-    }
-
-    @Override
-    protected void onNewIntent(Intent intent) {
-        super.onNewIntent(intent);
-    }
-
-    private void openFileChooser() {
-        Intent action = new Intent(this, FilePickerActivity.class);
-        action.putExtra(OCFileListFragment.ARG_MIMETYPE, "image/");
-        startActivityForResult(action, REQUEST_REMOTE_FILE);
-    }
-
-    @Override
-    protected void handleActivityResult(int requestCode, int resultCode, Intent data) {
-        switch (requestCode) {
-            case REQUEST_REMOTE_FILE:
-                handleRemoteFile(data);
-                break;
-
-            default:
-                // unexpected, do nothing
-                break;
-        }
-
-        super.handleActivityResult(requestCode, resultCode, data);
-    }
-
-    private void handleRemoteFile(Intent data) {
-        OCFile file = data.getParcelableExtra(FolderPickerActivity.EXTRA_FILES);
-
-        new Thread(() -> {
-            User user = currentAccountProvider.getUser();
-            RichDocumentsCreateAssetOperation operation = new RichDocumentsCreateAssetOperation(file.getRemotePath());
-            RemoteOperationResult result = operation.execute(user, this);
-
-            if (result.isSuccess()) {
-                String asset = (String) result.getSingleData();
-
-                runOnUiThread(() -> getWebView().evaluateJavascript("OCA.RichDocuments.documentsMain.postAsset('" +
-                                                                   file.getFileName() + "', '" + asset + "');", null));
-            } else {
-                runOnUiThread(() -> DisplayUtils.showSnackMessage(this, "Inserting image failed!"));
-            }
-        }).start();
-    }
-
-    @Override
-    protected void onSaveInstanceState(@NonNull Bundle outState) {
-        outState.putString(EXTRA_URL, url);
-        super.onSaveInstanceState(outState);
-    }
-
-    @Override
-    public void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
-        url = savedInstanceState.getString(EXTRA_URL);
-        super.onRestoreInstanceState(savedInstanceState);
-    }
-
-    @Override
-    protected void onResume() {
-        super.onResume();
-
-        getWebView().evaluateJavascript("if (typeof OCA.RichDocuments.documentsMain.postGrabFocus !== 'undefined') " +
-                                            "{ OCA.RichDocuments.documentsMain.postGrabFocus(); }",
-                                        null);
-    }
-
-    private void printFile(Uri url) {
-        OwnCloudAccount account = accountManager.getCurrentOwnCloudAccount();
-
-        if (account == null) {
-            DisplayUtils.showSnackMessage(getWebView(), getString(R.string.failed_to_print));
-            return;
-        }
-
-        File targetFile = new File(FileStorageUtils.getTemporalPath(account.getName()) + "/print.pdf");
-
-        new PrintAsyncTask(targetFile, url.toString(), new WeakReference<>(this)).execute();
-    }
-
-    @Override
-    public void loadUrl(String url) {
-        if (TextUtils.isEmpty(url)) {
-            new RichDocumentsLoadUrlTask(this, getUser().get(), getFile()).execute();
-        } else {
-            super.loadUrl(url);
-        }
-    }
-
-    private void showSlideShow(Uri url) {
-        Intent intent = new Intent(this, ExternalSiteWebView.class);
-        intent.putExtra(ExternalSiteWebView.EXTRA_URL, url.toString());
-        intent.putExtra(ExternalSiteWebView.EXTRA_SHOW_SIDEBAR, false);
-        intent.putExtra(ExternalSiteWebView.EXTRA_SHOW_TOOLBAR, false);
-        startActivity(intent);
-    }
-
-    private class RichDocumentsMobileInterface extends MobileInterface {
-        @JavascriptInterface
-        public void insertGraphic() {
-            openFileChooser();
-        }
-
-        @JavascriptInterface
-        public void documentLoaded() {
-            runOnUiThread(RichDocumentsEditorWebView.this::hideLoading);
-        }
-
-        @JavascriptInterface
-        public void downloadAs(String json) {
-            try {
-                JSONObject downloadJson = new JSONObject(json);
-
-                Uri url = Uri.parse(downloadJson.getString(URL));
-
-                switch (downloadJson.getString(TYPE)) {
-                    case PRINT:
-                        printFile(url);
-                        break;
-
-                    case SLIDESHOW:
-                        showSlideShow(url);
-                        break;
-
-                    default:
-                        downloadFile(url);
-                        break;
-                }
-            } catch (JSONException e) {
-                Log_OC.e(this, "Failed to parse download json message: " + e);
-            }
-        }
-
-        @JavascriptInterface
-        public void fileRename(String renameString) {
-            // when shared file is renamed in another instance, we will get notified about it
-            // need to change filename for sharing
-            try {
-                JSONObject renameJson = new JSONObject(renameString);
-                String newName = renameJson.getString(NEW_NAME);
-                getFile().setFileName(newName);
-            } catch (JSONException e) {
-                Log_OC.e(this, "Failed to parse rename json message: " + e);
-            }
-        }
-
-        @JavascriptInterface
-        public void paste() {
-            // Javascript cannot do this by itself, so help out.
-            getWebView().dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_PASTE));
-            getWebView().dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_PASTE));
-        }
-
-        @JavascriptInterface
-        public void hyperlink(String hyperlink) {
-            try {
-                String url = new JSONObject(hyperlink).getString(HYPERLINK);
-                Intent intent = new Intent(Intent.ACTION_VIEW);
-                intent.setData(Uri.parse(url));
-                startActivity(intent);
-            } catch (JSONException e) {
-                Log_OC.e(this, "Failed to parse download json message: " + e);
-            }
-        }
-    }
-}

+ 244 - 0
app/src/main/java/com/owncloud/android/ui/activity/RichDocumentsEditorWebView.kt

@@ -0,0 +1,244 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * @author Chris Narkiewicz
+ *
+ * Copyright (C) 2018 Tobias Kaminsky
+ * Copyright (C) 2018 Nextcloud GmbH.
+ * Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU 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 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 <https://www.gnu.org/licenses/>.
+ */
+package com.owncloud.android.ui.activity
+
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.text.TextUtils
+import android.view.KeyEvent
+import android.webkit.JavascriptInterface
+import androidx.activity.result.ActivityResult
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
+import com.nextcloud.client.account.CurrentAccountProvider
+import com.nextcloud.client.network.ClientFactory
+import com.owncloud.android.R
+import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.lib.common.utils.Log_OC
+import com.owncloud.android.operations.RichDocumentsCreateAssetOperation
+import com.owncloud.android.ui.asynctasks.PrintAsyncTask
+import com.owncloud.android.ui.asynctasks.RichDocumentsLoadUrlTask
+import com.owncloud.android.ui.fragment.OCFileListFragment
+import com.owncloud.android.utils.DisplayUtils
+import com.owncloud.android.utils.FileStorageUtils
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
+import org.json.JSONException
+import org.json.JSONObject
+import java.io.File
+import java.lang.ref.WeakReference
+import javax.inject.Inject
+
+/**
+ * Opens document for editing via Richdocuments app in a web view
+ */
+class RichDocumentsEditorWebView : EditorWebView() {
+    @JvmField
+    @Inject
+    var currentAccountProvider: CurrentAccountProvider? = null
+
+    @JvmField
+    @Inject
+    var clientFactory: ClientFactory? = null
+
+    private var activityResult: ActivityResultLauncher<Intent>? = null
+
+    @SuppressFBWarnings("ANDROID_WEB_VIEW_JAVASCRIPT_INTERFACE")
+    override fun postOnCreate() {
+        super.postOnCreate()
+
+        webView.addJavascriptInterface(RichDocumentsMobileInterface(), "RichDocumentsMobileInterface")
+
+        intent.getStringExtra(EXTRA_URL)?.let {
+            loadUrl(it)
+        }
+
+        registerActivityResult()
+    }
+
+    override fun onNewIntent(intent: Intent) {
+        super.onNewIntent(intent)
+    }
+
+    private fun openFileChooser() {
+        val action = Intent(this, FilePickerActivity::class.java)
+        action.putExtra(OCFileListFragment.ARG_MIMETYPE, "image/")
+        activityResult?.launch(action)
+    }
+
+    private fun registerActivityResult() {
+        activityResult =
+            registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
+                if (RESULT_OK == result.resultCode) {
+                    result.data?.let {
+                        handleRemoteFile(it)
+                    }
+                }
+            }
+    }
+
+    private fun handleRemoteFile(data: Intent) {
+        val file = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            data.getParcelableExtra(FolderPickerActivity.EXTRA_FILES, OCFile::class.java)
+        } else {
+            @Suppress("DEPRECATION")
+            data.getParcelableExtra(FolderPickerActivity.EXTRA_FILES)
+        }
+
+        Thread {
+            val user = currentAccountProvider?.user
+            val operation = RichDocumentsCreateAssetOperation(file?.remotePath)
+            val result = operation.execute(user, this)
+            if (result.isSuccess) {
+                val asset = result.singleData as String
+                runOnUiThread {
+                    webView.evaluateJavascript(
+                        "OCA.RichDocuments.documentsMain.postAsset('" +
+                            file?.fileName + "', '" + asset + "');",
+                        null
+                    )
+                }
+            } else {
+                runOnUiThread { DisplayUtils.showSnackMessage(this, "Inserting image failed!") }
+            }
+        }.start()
+    }
+
+    override fun onSaveInstanceState(outState: Bundle) {
+        outState.putString(EXTRA_URL, url)
+        super.onSaveInstanceState(outState)
+    }
+
+    override fun onRestoreInstanceState(savedInstanceState: Bundle) {
+        url = savedInstanceState.getString(EXTRA_URL)
+        super.onRestoreInstanceState(savedInstanceState)
+    }
+
+    override fun onResume() {
+        super.onResume()
+        webView.evaluateJavascript(
+            "if (typeof OCA.RichDocuments.documentsMain.postGrabFocus !== 'undefined') " +
+                "{ OCA.RichDocuments.documentsMain.postGrabFocus(); }",
+            null
+        )
+    }
+
+    private fun printFile(url: Uri) {
+        val account = accountManager.currentOwnCloudAccount
+        if (account == null) {
+            DisplayUtils.showSnackMessage(webView, getString(R.string.failed_to_print))
+            return
+        }
+        val targetFile = File(FileStorageUtils.getTemporalPath(account.name) + "/print.pdf")
+        PrintAsyncTask(targetFile, url.toString(), WeakReference(this)).execute()
+    }
+
+    public override fun loadUrl(url: String) {
+        if (TextUtils.isEmpty(url)) {
+            RichDocumentsLoadUrlTask(this, user.get(), file).execute()
+        } else {
+            super.loadUrl(url)
+        }
+    }
+
+    private fun showSlideShow(url: Uri) {
+        val intent = Intent(this, ExternalSiteWebView::class.java)
+        intent.putExtra(EXTRA_URL, url.toString())
+        intent.putExtra(EXTRA_SHOW_SIDEBAR, false)
+        intent.putExtra(EXTRA_SHOW_TOOLBAR, false)
+        startActivity(intent)
+    }
+
+    private inner class RichDocumentsMobileInterface : MobileInterface() {
+        @JavascriptInterface
+        fun insertGraphic() {
+            openFileChooser()
+        }
+
+        @JavascriptInterface
+        fun documentLoaded() {
+            runOnUiThread { hideLoading() }
+        }
+
+        @JavascriptInterface
+        fun downloadAs(json: String?) {
+            try {
+                json ?: return
+                val downloadJson = JSONObject(json)
+                val url = Uri.parse(downloadJson.getString(URL))
+                when (downloadJson.getString(TYPE)) {
+                    PRINT -> printFile(url)
+                    SLIDESHOW -> showSlideShow(url)
+                    else -> downloadFile(url)
+                }
+            } catch (e: JSONException) {
+                Log_OC.e(this, "Failed to parse download json message: $e")
+            }
+        }
+
+        @JavascriptInterface
+        fun fileRename(renameString: String?) {
+            // when shared file is renamed in another instance, we will get notified about it
+            // need to change filename for sharing
+            try {
+                renameString ?: return
+                val renameJson = JSONObject(renameString)
+                val newName = renameJson.getString(NEW_NAME)
+                file.fileName = newName
+            } catch (e: JSONException) {
+                Log_OC.e(this, "Failed to parse rename json message: $e")
+            }
+        }
+
+        @JavascriptInterface
+        fun paste() {
+            // Javascript cannot do this by itself, so help out.
+            webView.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_PASTE))
+            webView.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_PASTE))
+        }
+
+        @JavascriptInterface
+        fun hyperlink(hyperlink: String?) {
+            try {
+                hyperlink ?: return
+                val url = JSONObject(hyperlink).getString(HYPERLINK)
+                val intent = Intent(Intent.ACTION_VIEW)
+                intent.data = Uri.parse(url)
+                startActivity(intent)
+            } catch (e: JSONException) {
+                Log_OC.e(this, "Failed to parse download json message: $e")
+            }
+        }
+    }
+
+    companion object {
+        private const val URL = "URL"
+        private const val HYPERLINK = "Url"
+        private const val TYPE = "Type"
+        private const val PRINT = "print"
+        private const val SLIDESHOW = "slideshow"
+        private const val NEW_NAME = "NewName"
+    }
+}

+ 35 - 30
app/src/main/java/com/owncloud/android/ui/activity/UploadFilesActivity.java

@@ -61,6 +61,7 @@ import java.util.List;
 
 import javax.inject.Inject;
 
+import androidx.activity.OnBackPressedCallback;
 import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 import androidx.appcompat.app.ActionBar;
@@ -258,6 +259,8 @@ public class UploadFilesActivity extends DrawerActivity implements LocalFileList
 
         checkWritableFolder(mCurrentDir);
 
+        getOnBackPressedDispatcher().addCallback(this, onBackPressedCallback);
+
         Log_OC.d(TAG, "onCreate() end");
     }
 
@@ -369,43 +372,45 @@ public class UploadFilesActivity extends DrawerActivity implements LocalFileList
         }
     }
 
-    @Override
-    public void onBackPressed() {
-        if (isSearchOpen() && mSearchView != null) {
-            mSearchView.setQuery("", false);
-            mFileListFragment.onClose();
-            mSearchView.onActionViewCollapsed();
-            setDrawerIndicatorEnabled(isDrawerIndicatorAvailable());
-        } else {
-            if (mDirectories.getCount() <= SINGLE_DIR) {
-                finish();
-                return;
-            }
+    private final OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(true) {
+        @Override
+        public void handleOnBackPressed() {
+            if (isSearchOpen() && mSearchView != null) {
+                mSearchView.setQuery("", false);
+                mFileListFragment.onClose();
+                mSearchView.onActionViewCollapsed();
+                setDrawerIndicatorEnabled(isDrawerIndicatorAvailable());
+            } else {
+                if (mDirectories.getCount() <= SINGLE_DIR) {
+                    finish();
+                    return;
+                }
 
-            File parentFolder = mCurrentDir.getParentFile();
-            if (!parentFolder.canRead()) {
-                checkLocalStoragePathPickerPermission();
-                return;
-            }
+                File parentFolder = mCurrentDir.getParentFile();
+                if (!parentFolder.canRead()) {
+                    checkLocalStoragePathPickerPermission();
+                    return;
+                }
 
-            popDirname();
-            mFileListFragment.onNavigateUp();
-            mCurrentDir = mFileListFragment.getCurrentDirectory();
-            checkWritableFolder(mCurrentDir);
+                popDirname();
+                mFileListFragment.onNavigateUp();
+                mCurrentDir = mFileListFragment.getCurrentDirectory();
+                checkWritableFolder(mCurrentDir);
 
-            if (mCurrentDir.getParentFile() == null) {
-                ActionBar actionBar = getSupportActionBar();
-                if (actionBar != null) {
-                    actionBar.setDisplayHomeAsUpEnabled(false);
+                if (mCurrentDir.getParentFile() == null) {
+                    ActionBar actionBar = getSupportActionBar();
+                    if (actionBar != null) {
+                        actionBar.setDisplayHomeAsUpEnabled(false);
+                    }
                 }
-            }
 
-            // invalidate checked state when navigating directories
-            if (!mLocalFolderPickerMode) {
-                setSelectAllMenuItem(mOptionsMenu.findItem(R.id.action_select_all), false);
+                // invalidate checked state when navigating directories
+                if (!mLocalFolderPickerMode) {
+                    setSelectAllMenuItem(mOptionsMenu.findItem(R.id.action_select_all), false);
+                }
             }
         }
-    }
+    };
 
     @Override
     protected void onSaveInstanceState(@NonNull Bundle outState) {

+ 41 - 6
app/src/main/java/com/owncloud/android/ui/adapter/UploadListAdapter.java

@@ -62,6 +62,8 @@ import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.operations.RefreshFolderOperation;
 import com.owncloud.android.ui.activity.ConflictsResolveActivity;
 import com.owncloud.android.ui.activity.FileActivity;
+import com.owncloud.android.ui.activity.FileDisplayActivity;
+import com.owncloud.android.ui.preview.PreviewImageFragment;
 import com.owncloud.android.utils.DisplayUtils;
 import com.owncloud.android.utils.MimeTypeUtil;
 import com.owncloud.android.utils.theme.ViewThemeUtils;
@@ -346,12 +348,15 @@ public class UploadListAdapter extends SectionedRecyclerViewAdapter<SectionedVie
                 itemViewHolder.binding.uploadRightButton.setOnClickListener(v -> removeUpload(item));
             }
             itemViewHolder.binding.uploadRightButton.setVisibility(View.VISIBLE);
-        } else {    // UploadStatus.UPLOAD_SUCCESS
+        } else {    // UploadStatus.UPLOAD_SUCCEEDED
             itemViewHolder.binding.uploadRightButton.setVisibility(View.INVISIBLE);
         }
 
         itemViewHolder.binding.uploadListItemLayout.setOnClickListener(null);
 
+        // Set icon or thumbnail
+        itemViewHolder.binding.thumbnail.setImageResource(R.drawable.file);
+
         // click on item
         if (item.getUploadStatus() == UploadStatus.UPLOAD_FAILED) {
             final UploadResult uploadResult = item.getLastResult();
@@ -381,12 +386,15 @@ public class UploadListAdapter extends SectionedRecyclerViewAdapter<SectionedVie
                     );
                 }
             });
-        } else {
-            itemViewHolder.binding.uploadListItemLayout.setOnClickListener(v -> onUploadItemClick(item));
+        } else if (item.getUploadStatus() == UploadStatus.UPLOAD_SUCCEEDED){
+            itemViewHolder.binding.uploadListItemLayout.setOnClickListener(v -> onUploadedItemClick(item));
         }
 
-        // Set icon or thumbnail
-        itemViewHolder.binding.thumbnail.setImageResource(R.drawable.file);
+
+        // click on thumbnail to open locally
+        if (item.getUploadStatus() != UploadStatus.UPLOAD_SUCCEEDED){
+            itemViewHolder.binding.thumbnail.setOnClickListener(v -> onUploadingItemClick(item));
+        }
 
         /*
          * Cancellation needs do be checked and done before changing the drawable in fileIcon, or
@@ -738,7 +746,10 @@ public class UploadListAdapter extends SectionedRecyclerViewAdapter<SectionedVie
         notifyDataSetChanged();
     }
 
-    private void onUploadItemClick(OCUpload file) {
+    /**
+     * Open local file.
+     */
+    private void onUploadingItemClick(OCUpload file) {
         File f = new File(file.getLocalPath());
         if (!f.exists()) {
             DisplayUtils.showSnackMessage(parentActivity, R.string.local_file_not_found_message);
@@ -747,6 +758,30 @@ public class UploadListAdapter extends SectionedRecyclerViewAdapter<SectionedVie
         }
     }
 
+    /**
+     * Open remote file.
+     */
+    private void onUploadedItemClick(OCUpload upload) {
+        final OCFile file = parentActivity.getStorageManager().getFileByEncryptedRemotePath(upload.getRemotePath());
+        if (file == null){
+            DisplayUtils.showSnackMessage(parentActivity, R.string.error_retrieving_file);
+            Log_OC.i(TAG, "Could not find uploaded file on remote.");
+            return;
+        }
+
+        if (PreviewImageFragment.canBePreviewed(file)){
+            //show image preview and stay in uploads tab
+            Intent intent = FileDisplayActivity.openFileIntent(parentActivity, parentActivity.getUser().get(), file);
+            parentActivity.startActivity(intent);
+        }else{
+            Intent intent = new Intent(parentActivity, FileDisplayActivity.class);
+            intent.setAction(Intent.ACTION_VIEW);
+            intent.putExtra(FileDisplayActivity.KEY_FILE_PATH, upload.getRemotePath());
+            intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+            parentActivity.startActivity(intent);
+        }
+    }
+
 
     /**
      * Open file with app associates with its MIME type. If MIME type unknown, show list with all apps.

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

@@ -7,7 +7,7 @@
  *
  * Copyright (C) 2018 Andy Scherzinger
  * Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
- * Copyright (C) 2020 TSI-mc
+ * Copyright (C) 2023 TSI-mc
  *
  * This program is free software; you can redistribute it and/or
  * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
@@ -25,11 +25,17 @@
 
 package com.owncloud.android.ui.fragment;
 
+import android.Manifest;
 import android.accounts.AccountManager;
+import android.app.Activity;
 import android.app.SearchManager;
 import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
 import android.graphics.drawable.Drawable;
+import android.net.Uri;
 import android.os.Bundle;
+import android.provider.ContactsContract;
 import android.text.InputType;
 import android.text.TextUtils;
 import android.view.LayoutInflater;
@@ -46,6 +52,7 @@ import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.lib.common.OwnCloudAccount;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
+import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.lib.resources.shares.OCShare;
 import com.owncloud.android.lib.resources.shares.ShareType;
 import com.owncloud.android.lib.resources.status.NextcloudVersion;
@@ -61,6 +68,7 @@ import com.owncloud.android.ui.fragment.util.FileDetailSharingFragmentHelper;
 import com.owncloud.android.ui.helpers.FileOperationsHelper;
 import com.owncloud.android.utils.ClipboardUtil;
 import com.owncloud.android.utils.DisplayUtils;
+import com.owncloud.android.utils.PermissionUtil;
 import com.owncloud.android.utils.theme.ViewThemeUtils;
 
 import java.util.ArrayList;
@@ -68,6 +76,8 @@ import java.util.List;
 
 import javax.inject.Inject;
 
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
@@ -167,6 +177,8 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
                                                             file.isEncrypted()));
         binding.sharesList.setLayoutManager(new LinearLayoutManager(getContext()));
 
+        binding.pickContactEmailBtn.setOnClickListener(v -> checkContactPermission());
+
         setupView();
 
         return view;
@@ -208,6 +220,7 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
             } else {
                 binding.searchView.setQueryHint(getResources().getString(R.string.reshare_not_allowed));
                 binding.searchView.setInputType(InputType.TYPE_NULL);
+                binding.pickContactEmailBtn.setVisibility(View.GONE);
                 disableSearchView(binding.searchView);
             }
         }
@@ -460,6 +473,52 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
         adapter.addShares(publicShares);
     }
 
+    private void checkContactPermission() {
+        if (PermissionUtil.checkSelfPermission(requireActivity(), Manifest.permission.READ_CONTACTS)) {
+            pickContactEmail();
+        } else {
+            requestContactPermissionLauncher.launch(Manifest.permission.READ_CONTACTS);
+        }
+    }
+
+    private void pickContactEmail() {
+        Intent intent = new Intent(Intent.ACTION_PICK);
+        intent.setDataAndType(ContactsContract.Contacts.CONTENT_URI, ContactsContract.CommonDataKinds.Email.CONTENT_TYPE);
+        onContactSelectionResultLauncher.launch(intent);
+    }
+
+    private void handleContactResult(@NonNull Uri contactUri) {
+        // Define the projection to get all email addresses.
+        String[] projection = {ContactsContract.CommonDataKinds.Email.ADDRESS};
+
+        Cursor cursor = fileActivity.getContentResolver().query(contactUri, projection, null, null, null);
+
+        if (cursor != null) {
+            if (cursor.moveToFirst()) {
+                // The contact has only one email address, use it.
+                int columnIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.ADDRESS);
+                if (columnIndex != -1) {
+                    // Use the email address as needed.
+                    // email variable contains the selected contact's email address.
+                    String email = cursor.getString(columnIndex);
+                    binding.searchView.post(() -> {
+                        binding.searchView.setQuery(email, false);
+                        binding.searchView.requestFocus();
+                    });
+                } else {
+                    DisplayUtils.showSnackMessage(binding.getRoot(), R.string.email_pick_failed);
+                    Log_OC.e(FileDetailSharingFragment.class.getSimpleName(), "Failed to pick email address.");
+                }
+            } else {
+                DisplayUtils.showSnackMessage(binding.getRoot(), R.string.email_pick_failed);
+                Log_OC.e(FileDetailSharingFragment.class.getSimpleName(), "Failed to pick email address as no Email found.");
+            }
+            cursor.close();
+        } else {
+            DisplayUtils.showSnackMessage(binding.getRoot(), R.string.email_pick_failed);
+            Log_OC.e(FileDetailSharingFragment.class.getSimpleName(), "Failed to pick email address as Cursor is null.");
+        }
+    }
 
     private boolean containsNoNewPublicShare(List<OCShare> shares) {
         for (OCShare share : shares) {
@@ -546,6 +605,38 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
         fileOperationsHelper.setPermissionsToShare(share, permission);
     }
 
+    //launcher for contact permission
+    private final ActivityResultLauncher<String> requestContactPermissionLauncher =
+        registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {
+            if (isGranted) {
+                pickContactEmail();
+            } else {
+                DisplayUtils.showSnackMessage(binding.getRoot(), R.string.contact_no_permission);
+            }
+        });
+
+    //launcher to handle contact selection
+    private final ActivityResultLauncher<Intent> onContactSelectionResultLauncher =
+        registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
+                                  result -> {
+                                      if (result.getResultCode() == Activity.RESULT_OK) {
+                                          Intent intent = result.getData();
+                                          if (intent == null) {
+                                              DisplayUtils.showSnackMessage(binding.getRoot(), R.string.email_pick_failed);
+                                              return;
+                                          }
+
+                                          Uri contactUri = intent.getData();
+                                          if (contactUri == null) {
+                                              DisplayUtils.showSnackMessage(binding.getRoot(), R.string.email_pick_failed);
+                                              return;
+                                          }
+
+                                          handleContactResult(contactUri);
+
+                                      }
+                                  });
+
     public interface OnEditShareListener {
         void editExistingShare(OCShare share, int screenTypePermission, boolean isReshareShown,
                                boolean isExpiryDateShown);

+ 26 - 0
app/src/main/res/drawable/ic_contact_book.xml

@@ -0,0 +1,26 @@
+<!--
+    @author Google LLC
+    Copyright (C) 2023 Google LLC
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="#666666"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M20,0L4,0v2h16L20,0zM4,24h16v-2L4,22v2zM20,4L4,4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2zM12,6.75c1.24,0 2.25,1.01 2.25,2.25s-1.01,2.25 -2.25,2.25S9.75,10.24 9.75,9 10.76,6.75 12,6.75zM17,17L7,17v-1.5c0,-1.67 3.33,-2.5 5,-2.5s5,0.83 5,2.5L17,17z" />
+</vector>

+ 36 - 21
app/src/main/res/layout/file_details_sharing_fragment.xml

@@ -1,7 +1,10 @@
 <?xml version="1.0" encoding="utf-8"?><!--
   Nextcloud Android client application
 
+  @author TSI-mc
+
   Copyright (C) 2018 Andy Scherzinger
+  Copyright (C) 2023 TSI-mc
 
   This program is free software; you can redistribute it and/or
   modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
@@ -29,83 +32,94 @@
         android:id="@+id/search_container"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
+        android:orientation="horizontal"
         android:paddingStart="@dimen/standard_padding"
         android:paddingEnd="@dimen/zero">
 
         <ImageView
             android:id="@+id/searchViewIcon"
-            android:layout_height="@dimen/user_icon_size"
             android:layout_width="@dimen/user_icon_size"
-            android:padding="@dimen/standard_half_padding"
-            android:contentDescription="@string/avatar"
+            android:layout_height="@dimen/user_icon_size"
             android:layout_gravity="center_vertical"
+            android:contentDescription="@string/avatar"
+            android:padding="@dimen/standard_half_padding"
             android:src="@drawable/ic_search_grey" />
 
         <androidx.appcompat.widget.SearchView
             android:id="@+id/searchView"
             style="@style/ownCloud.SearchView"
-            android:layout_width="match_parent"
+            android:layout_width="0dp"
             android:layout_height="wrap_content"
             android:layout_marginStart="@dimen/zero"
             android:layout_marginEnd="@dimen/standard_quarter_margin"
+            android:layout_weight="1"
             android:hint="@string/share_search"
             app:searchIcon="@null" />
 
+        <androidx.appcompat.widget.AppCompatImageView
+            android:id="@+id/pick_contact_email_btn"
+            android:layout_width="@dimen/minimum_size_for_touchable_area"
+            android:layout_height="@dimen/minimum_size_for_touchable_area"
+            android:layout_gravity="center_vertical"
+            android:padding="12dp"
+            android:layout_marginEnd="@dimen/standard_quarter_margin"
+            android:src="@drawable/ic_contact_book" />
+
     </LinearLayout>
 
     <LinearLayout
         android:id="@+id/shared_with_you_container"
+        android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:layout_marginBottom="@dimen/standard_half_margin"
-        android:layout_width="match_parent"
         android:orientation="horizontal"
         android:paddingLeft="@dimen/standard_padding"
-        android:paddingRight="@dimen/standard_padding"
-        android:paddingTop="@dimen/standard_padding">
+        android:paddingTop="@dimen/standard_padding"
+        android:paddingRight="@dimen/standard_padding">
 
         <ImageView
-            android:contentDescription="@string/avatar"
             android:id="@+id/shared_with_you_avatar"
-            android:layout_height="@dimen/user_icon_size"
             android:layout_width="@dimen/user_icon_size"
+            android:layout_height="@dimen/user_icon_size"
+            android:contentDescription="@string/avatar"
             android:src="@drawable/ic_user" />
 
         <LinearLayout
-            android:layout_height="wrap_content"
             android:layout_width="match_parent"
+            android:layout_height="wrap_content"
             android:orientation="vertical"
             android:paddingLeft="@dimen/standard_padding"
-            android:paddingRight="@dimen/standard_padding"
-            android:paddingTop="@dimen/standard_half_padding">
+            android:paddingTop="@dimen/standard_half_padding"
+            android:paddingRight="@dimen/standard_padding">
 
             <TextView
                 android:id="@+id/shared_with_you_username"
-                android:layout_height="wrap_content"
                 android:layout_width="match_parent"
+                android:layout_height="wrap_content"
                 android:text="@string/shared_with_you_by"
                 android:textSize="@dimen/two_line_primary_text_size" />
 
             <LinearLayout
                 android:id="@+id/shared_with_you_note_container"
-                android:layout_height="match_parent"
                 android:layout_width="match_parent"
+                android:layout_height="match_parent"
                 android:orientation="horizontal"
                 android:paddingTop="@dimen/standard_half_padding"
                 tools:ignore="UseCompoundDrawables">
 
                 <ImageView
-                    android:contentDescription="@string/note_icon_hint"
-                    android:layout_height="16dp"
                     android:layout_width="16dp"
+                    android:layout_height="16dp"
+                    android:contentDescription="@string/note_icon_hint"
                     android:src="@drawable/file_text" />
 
                 <TextView
                     android:id="@+id/shared_with_you_note"
+                    android:layout_width="0dp"
                     android:layout_height="wrap_content"
                     android:layout_weight="1"
-                    android:layout_width="0dp"
-                    android:paddingEnd="@dimen/standard_half_padding"
                     android:paddingStart="@dimen/standard_half_padding"
+                    android:paddingEnd="@dimen/standard_half_padding"
                     android:textSize="16sp" />
             </LinearLayout>
 
@@ -113,10 +127,11 @@
     </LinearLayout>
 
     <androidx.recyclerview.widget.RecyclerView
-        android:divider="@drawable/divider"
-        android:dividerHeight="1dp"
         android:id="@+id/sharesList"
+        android:layout_width="match_parent"
         android:layout_height="match_parent"
-        android:layout_width="match_parent" />
+        android:divider="@drawable/divider"
+        android:dividerHeight="1dp"
+        tools:listitem="@layout/file_details_share_link_share_item" />
 
 </LinearLayout>

+ 14 - 12
app/src/main/res/layout/passcodelock.xml

@@ -17,36 +17,36 @@
   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 -->
-<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+<ScrollView
+    xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:gravity="center_horizontal">
 
-    <androidx.cardview.widget.CardView xmlns:card_view="http://schemas.android.com/apk/res-auto"
-        android:id="@+id/card_view"
+    <com.google.android.material.card.MaterialCardView
+        xmlns:card_view="http://schemas.android.com/apk/res-auto"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:layout_gravity="center"
         android:layout_margin="@dimen/standard_double_margin"
-        card_view:cardCornerRadius="4dp"
+        card_view:strokeWidth="0dp"
+        card_view:cardCornerRadius="16dp"
         card_view:cardElevation="@dimen/dialog_elevation">
 
         <LinearLayout
+            android:id="@+id/card_view_content"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:orientation="vertical"
-            android:paddingStart="@dimen/standard_padding"
-            android:paddingTop="@dimen/standard_padding"
-            android:paddingEnd="@dimen/standard_padding"
-            android:paddingBottom="@dimen/standard_half_padding">
+            android:padding="@dimen/standard_padding">
 
             <LinearLayout
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:orientation="vertical">
 
-                <TextView
+                <com.google.android.material.textview.MaterialTextView
                     android:id="@+id/header"
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content"
@@ -56,7 +56,7 @@
                     android:textSize="@dimen/two_line_primary_text_size"
                     android:textStyle="bold" />
 
-                <TextView
+                <com.google.android.material.textview.MaterialTextView
                     android:id="@+id/explanation"
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content"
@@ -96,12 +96,14 @@
 
             <com.google.android.material.button.MaterialButton
                 android:id="@+id/cancel"
-                style="@style/Button.Borderless"
+                style="@style/Widget.Material3.Button.TextButton"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:layout_gravity="end"
                 android:text="@string/common_cancel" />
+
         </LinearLayout>
 
-    </androidx.cardview.widget.CardView>
+    </com.google.android.material.card.MaterialCardView>
+
 </ScrollView>

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

@@ -325,7 +325,6 @@
     <string name="file_delete">حذف </string>
     <string name="file_detail_activity_error">خطأ في استرداد النشاطات للملف</string>
     <string name="file_details_no_content">حدث خطأ في تحميل التفاصيل</string>
-    <string name="file_downloader_notification_title_prefix">تنزيل \u0020</string>
     <string name="file_icon">الملف</string>
     <string name="file_keep">حفظ</string>
     <string name="file_list_empty">قم برفع بعض المحتوى أو زامن مع أجهزتك</string>

+ 6 - 7
app/src/main/res/values-cs-rCZ/strings.xml

@@ -50,7 +50,7 @@
     <string name="auth_host_url">Adresa serveru https://…</string>
     <string name="auth_incorrect_address_title">Nesprávný formát adresy serveru</string>
     <string name="auth_incorrect_path_title">Server nenalezen</string>
-    <string name="auth_no_net_conn_title">Žádné síťové spojení</string>
+    <string name="auth_no_net_conn_title">Nepřipojeno k síti</string>
     <string name="auth_nossl_plain_ok_title">Zabezpečené spojení není k dispozici.</string>
     <string name="auth_not_configured_title">Nesprávně formulované nastavení pro server</string>
     <string name="auth_oauth_error">Neúspěšné ověření se</string>
@@ -163,11 +163,11 @@
     <string name="contactlist_item_icon">Ikona uživatele v seznamu kontaktů</string>
     <string name="contactlist_no_permission">Nejsou udělena oprávnění, proto nebylo nic naimportováno.</string>
     <string name="contacts">Kontakty</string>
-    <string name="contacts_backup_button">Zálohovat nyní</string>
+    <string name="contacts_backup_button">Zazálohovat nyní</string>
     <string name="contacts_preferences_backup_scheduled">Zálohování naplánováno a brzy začne</string>
     <string name="contacts_preferences_import_scheduled">Import naplánován a brzy začne</string>
     <string name="contacts_preferences_no_file_found">Nenalezen žádný soubor</string>
-    <string name="contacts_preferences_something_strange_happened">Nepodařilo se najít vaši poslední zálohu!</string>
+    <string name="contacts_preferences_something_strange_happened">Nepodařilo se najít vaši nejaktuálnější zálohu!</string>
     <string name="copied_to_clipboard">Zkopírováno do schránky</string>
     <string name="copy_file_error">Při pokusu o zkopírování tohoto souboru či složky došlo k chybě</string>
     <string name="copy_file_invalid_into_descendent">Není možné zkopírovat složku do některé z jejích vlastních podsložek</string>
@@ -325,7 +325,6 @@
     <string name="file_delete">Smazat</string>
     <string name="file_detail_activity_error">Při načítání aktivit u souboru došlo k chybě</string>
     <string name="file_details_no_content">Nepodařilo se načíst podrobnosti.</string>
-    <string name="file_downloader_notification_title_prefix">Stahování \u0020</string>
     <string name="file_icon">Soubor</string>
     <string name="file_keep">Ponechat</string>
     <string name="file_list_empty">Nahrajte nějaký obsah, nebo synchronizujte s vašimi zařízeními.</string>
@@ -363,7 +362,7 @@
     <string name="file_migration_migrating">Přesouvání dat…</string>
     <string name="file_migration_ok_finished">Dokončeno</string>
     <string name="file_migration_override_data_folder">Nahradit</string>
-    <string name="file_migration_preparing">Příprava migrace…</string>
+    <string name="file_migration_preparing">Příprava stěhování…</string>
     <string name="file_migration_restoring_accounts_configuration">Obnovování nastavení účtu…</string>
     <string name="file_migration_saving_accounts_configuration">Ukládání nastavení účtu…</string>
     <string name="file_migration_source_not_readable">Opravdu chcete změnit složku pro ukládání dat na %1$s?\n\nPoznámka: Všechna data bude třeba znovu stáhnout.</string>
@@ -598,7 +597,7 @@
     <string name="prefs_enable_media_scan_notifications_summary">Upozorňovat na nově nalezené složky s médii</string>
     <string name="prefs_gpl_v2">GNU General Public License, verze 2</string>
     <string name="prefs_help">Nápověda</string>
-    <string name="prefs_imprint">Imprint</string>
+    <string name="prefs_imprint">Impresum</string>
     <string name="prefs_instant_behaviour_dialogTitle">Původní soubor bude…</string>
     <string name="prefs_instant_behaviour_title">Původní soubor bude…</string>
     <string name="prefs_instant_upload_path_use_date_subfolders_summary">Ukládat v podsložkách podle data</string>
@@ -935,7 +934,7 @@
     <string name="user_info_address">Adresa</string>
     <string name="user_info_email">E-mail</string>
     <string name="user_info_phone">Telefonní číslo</string>
-    <string name="user_info_twitter">Twitter</string>
+    <string name="user_info_twitter">X (Twitter)</string>
     <string name="user_info_website">Webové stránky</string>
     <string name="user_information_retrieval_error">Chyba při načítání informací o uživateli</string>
     <string name="userinfo_no_info_headline">Nezadány žádné osobní údaje</string>

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

@@ -325,7 +325,6 @@
     <string name="file_delete">Slet</string>
     <string name="file_detail_activity_error">Fejl ved indlæsning af aktiviteter for fil</string>
     <string name="file_details_no_content">Fejl ved indlæsning af detaljer</string>
-    <string name="file_downloader_notification_title_prefix">Downloader \u0020</string>
     <string name="file_icon">Fil</string>
     <string name="file_keep">Behold</string>
     <string name="file_list_empty">Upload indhold eller synkronisér med dine enheder.</string>

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

@@ -325,7 +325,6 @@
     <string name="file_delete">Löschen</string>
     <string name="file_detail_activity_error">Fehler beim Abrufen der Aktivitäten für die Datei</string>
     <string name="file_details_no_content">Fehler beim Laden der Details</string>
-    <string name="file_downloader_notification_title_prefix">Herunterladen \u0020</string>
     <string name="file_icon">Datei</string>
     <string name="file_keep">Behalten</string>
     <string name="file_list_empty">Laden Sie Inhalt hoch oder synchronisieren Sie mit Ihren Geräten.</string>

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

@@ -239,6 +239,7 @@
     <string name="folder_confirm_create">Crear</string>
     <string name="folder_list_empty_headline">No hay carpetas aquí</string>
     <string name="folder_picker_choose_button_text">Seleccionar</string>
+    <string name="folder_picker_move_button_text">Mover</string>
     <string name="forbidden_permissions">No se te permite %s</string>
     <string name="forbidden_permissions_copy">para copiar este archivo</string>
     <string name="forbidden_permissions_create">para crear este archivo</string>

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

@@ -540,6 +540,7 @@
     <string name="wait_a_moment">Aguarda un momento…</string>
     <string name="wait_checking_credentials">Verificando credenciales almacenadas</string>
     <string name="wait_for_tmp_copy_from_private_storage">Copiando el archivo desde almacenamiento privado</string>
+    <string name="webview_version_check_alert_dialog_positive_button_title">Actualizar</string>
     <string name="what_s_new_image">Imagen de qué es nuevo</string>
     <string name="whats_new_skip">Omitir</string>
     <string name="whats_new_title">Nuevo en %1$s</string>

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

@@ -525,6 +525,7 @@
     <string name="version_dev_download">Lae alla</string>
     <string name="wait_a_moment">Oota üks hetk…</string>
     <string name="wait_for_tmp_copy_from_private_storage">Faili kopeerimine privaatsest salvestusalast</string>
+    <string name="webview_version_check_alert_dialog_positive_button_title">Uuenda</string>
     <string name="whats_new_skip">Jäta vahele</string>
     <string name="whats_your_status">Mis on su staatus?</string>
     <string name="write_email">Saada kiri</string>

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

@@ -393,6 +393,7 @@
     <string name="folder_list_empty_headline">Ez dago karpetarik hemen</string>
     <string name="folder_picker_choose_button_text">Aukeratu</string>
     <string name="folder_picker_choose_caption_text">Aukeratu helburuko karpeta</string>
+    <string name="folder_picker_copy_button_text">Kopiatu</string>
     <string name="folder_picker_move_button_text">Mugitu</string>
     <string name="forbidden_permissions">Ez daukazu baimenik %s</string>
     <string name="forbidden_permissions_copy">fitxategi hau kopiatzeko</string>
@@ -943,6 +944,7 @@
     <string name="wait_a_moment">Itxaron momentu bat…</string>
     <string name="wait_checking_credentials">Gordetako nortasun-datuak konprobatzen</string>
     <string name="wait_for_tmp_copy_from_private_storage">Fitxategia biltegiratze pribatutik kopiatzen</string>
+    <string name="webview_version_check_alert_dialog_positive_button_title">Eguneratu</string>
     <string name="what_s_new_image">Zer da irudi berria</string>
     <string name="whats_new_skip">Salto egin</string>
     <string name="whats_new_title">Berria %1$s-n</string>

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

@@ -393,6 +393,7 @@
     <string name="folder_list_empty_headline">هیچ  پوشه ای اینجا وجود ندارد</string>
     <string name="folder_picker_choose_button_text">انتخاب کردن</string>
     <string name="folder_picker_choose_caption_text">پوشهٔ هدف را انتخاب کنید</string>
+    <string name="folder_picker_copy_button_text">رونوشت</string>
     <string name="folder_picker_move_button_text">انتقال</string>
     <string name="forbidden_permissions">شما مجاز نیستید%s</string>
     <string name="forbidden_permissions_copy">کپی این فایل</string>
@@ -943,6 +944,7 @@
     <string name="wait_a_moment">یک لحظه صبر کنید...</string>
     <string name="wait_checking_credentials">بررسی اعتبارنامه‌های ذخیره شده</string>
     <string name="wait_for_tmp_copy_from_private_storage">کپی کردن فایل از حافظه خصوصی</string>
+    <string name="webview_version_check_alert_dialog_positive_button_title">به‌روز رسانی</string>
     <string name="what_s_new_image">چه تصویر جدیدی است</string>
     <string name="whats_new_skip">رد شدن</string>
     <string name="whats_new_title">جدید در %1$s</string>

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

@@ -381,6 +381,7 @@
     <string name="folder_list_empty_headline">Ei kansioita täällä</string>
     <string name="folder_picker_choose_button_text">Valitse</string>
     <string name="folder_picker_choose_caption_text">Valitse kohdekansio</string>
+    <string name="folder_picker_copy_button_text">Kopioi</string>
     <string name="folder_picker_move_button_text">Siirrä</string>
     <string name="forbidden_permissions">Sinulla ei ole oikeutta %s</string>
     <string name="forbidden_permissions_copy">kopioida tämä tiedosto</string>
@@ -906,6 +907,7 @@ GNU yleinen lisenssi, versio 2</string>
     <string name="wait_a_moment">Odota hetki…</string>
     <string name="wait_checking_credentials">Tarkistetaan tallennettuja tilitietoja</string>
     <string name="wait_for_tmp_copy_from_private_storage">Kopioidaan tiedostoa yksityisestä tallennustilasta</string>
+    <string name="webview_version_check_alert_dialog_positive_button_title">Päivitä</string>
     <string name="what_s_new_image">Mitä uutta -kuva</string>
     <string name="whats_new_skip">Ohita</string>
     <string name="whats_new_title">Uutta versiossa %1$s</string>

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

@@ -327,7 +327,6 @@ Attention, la suppression est irréversible.</string>
     <string name="file_delete">Supprimer</string>
     <string name="file_detail_activity_error">Erreur lors de la récupération de l’activité du fichier</string>
     <string name="file_details_no_content">Impossible de charger les détails</string>
-    <string name="file_downloader_notification_title_prefix">Téléchargement de \u0020</string>
     <string name="file_icon">Fichier</string>
     <string name="file_keep">Conserver</string>
     <string name="file_list_empty">Déposez du contenu ou synchronisez vos appareils.</string>

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

@@ -325,7 +325,6 @@
     <string name="file_delete">Eliminar</string>
     <string name="file_detail_activity_error">Produciuse un erro ao recuperar actividades para o ficheiro</string>
     <string name="file_details_no_content">Produciuse un fallo ao cargar os detalles</string>
-    <string name="file_downloader_notification_title_prefix">Descargando \u0020</string>
     <string name="file_icon">Ficheiro</string>
     <string name="file_keep">Conservar</string>
     <string name="file_list_empty">Envíe algún contido ou sincronice cos seus dispositivos.</string>

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

@@ -18,6 +18,7 @@
     <string name="actionbar_copy">Másolás</string>
     <string name="actionbar_mkdir">Új mappa</string>
     <string name="actionbar_move">Áthelyezés</string>
+    <string name="actionbar_move_or_copy">Áthelyezés vagy Másolás</string>
     <string name="actionbar_open_with">Megnyitás ezzel</string>
     <string name="actionbar_search">Keresés</string>
     <string name="actionbar_see_details">Részletek</string>
@@ -962,7 +963,9 @@ A Nextcloud itt érhető el: https://nextcloud.com</string>
     <string name="wait_a_moment">Egy pillanat…</string>
     <string name="wait_checking_credentials">Tárolt hitelesítő adatok ellenőrzése</string>
     <string name="wait_for_tmp_copy_from_private_storage">Fájl másolása a privát tárolóról</string>
+    <string name="webview_version_check_alert_dialog_message">A bejelentkezéshez frissítse az Android System WebView alkalmazást</string>
     <string name="webview_version_check_alert_dialog_positive_button_title">Frissítés</string>
+    <string name="webview_version_check_alert_dialog_title">Frissítse az Android System WebView-t</string>
     <string name="what_s_new_image">Újdonságok kép</string>
     <string name="whats_new_skip">Kihagyás</string>
     <string name="whats_new_title">Új itt: %1$s</string>

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

@@ -18,6 +18,7 @@
     <string name="actionbar_copy">Kopier</string>
     <string name="actionbar_mkdir">Ny mappe</string>
     <string name="actionbar_move">Flytt</string>
+    <string name="actionbar_move_or_copy">Flytt eller kopier</string>
     <string name="actionbar_open_with">Åpne med</string>
     <string name="actionbar_search">Søk</string>
     <string name="actionbar_see_details">Detaljer</string>
@@ -425,6 +426,13 @@
     <string name="image_editor_rotate_ccw">Roter mot klokken</string>
     <string name="image_editor_rotate_cw">Roter med klokken</string>
     <string name="image_editor_unable_to_edit_image">Kan ikke endre bildet.</string>
+    <string name="image_preview_filedetails">Fildetaljer</string>
+    <string name="image_preview_image_taking_conditions">Betingelser for å ta bilder</string>
+    <string name="image_preview_unit_fnumber">ƒ/%s</string>
+    <string name="image_preview_unit_iso">ISO %s</string>
+    <string name="image_preview_unit_megapixel">%s MP</string>
+    <string name="image_preview_unit_millimetres">%s mm</string>
+    <string name="image_preview_unit_seconds">%s s</string>
     <string name="in_folder">i mappen %1$s</string>
     <string name="instant_upload_existing">Last også opp eksisterende filer</string>
     <string name="instant_upload_on_charging">Bare last opp under lading</string>
@@ -505,6 +513,7 @@
     <string name="no_calendar_exists">Ingen kalender finnes</string>
     <string name="no_email_app_available">Ingen program for å behandle e-post adresse</string>
     <string name="no_items">Ingen gjenstander</string>
+    <string name="no_map_app_availble">Ingen app tilgjengelig for å håndtere kart</string>
     <string name="no_mutliple_accounts_allowed">Kun én konto er tillatt</string>
     <string name="no_pdf_app_available">Ingen app tilgjengelig for å håndtere PDF</string>
     <string name="no_send_app">Ingen program for å sende valgt fil</string>
@@ -591,7 +600,9 @@
     <string name="prefs_imprint">Avtrykk</string>
     <string name="prefs_instant_behaviour_dialogTitle">Opprinnelig fil vil bli…</string>
     <string name="prefs_instant_behaviour_title">Opprinnelig fil vil bli…</string>
+    <string name="prefs_instant_upload_path_use_date_subfolders_summary">Lagre i undermapper basert på dato</string>
     <string name="prefs_instant_upload_path_use_subfolders_title">Bruk undermapper</string>
+    <string name="prefs_instant_upload_subfolder_rule_title">Alternativer for undermappe</string>
     <string name="prefs_keys_exist">Legg til ende-til-ende -kryptering på denne klienten</string>
     <string name="prefs_license">Lisens</string>
     <string name="prefs_lock">Appsikkerhet</string>
@@ -781,6 +792,8 @@
     <string name="stream_not_possible_headline">Intern strøming ikke mulig</string>
     <string name="stream_not_possible_message">Vennligst last ned media i stedet, eller bruk ekstern app.</string>
     <string name="strict_mode">Streng modus: ingen HTTP-tilkobling tillatt!</string>
+    <string name="sub_folder_rule_day">År/Måned/Dag</string>
+    <string name="sub_folder_rule_month">År/Måned</string>
     <string name="sub_folder_rule_year">År</string>
     <string name="subject_shared_with_you">\"%1$s\" er blitt delt med deg</string>
     <string name="subject_user_shared_with_you">%1$s delte \"%2$s\" med deg</string>
@@ -932,7 +945,9 @@
     <string name="wait_a_moment">Vent et øyeblikk…</string>
     <string name="wait_checking_credentials">Sjekker lagrede påloggingsdetaljer</string>
     <string name="wait_for_tmp_copy_from_private_storage">Kopierer fil fra privat lager</string>
+    <string name="webview_version_check_alert_dialog_message">Vennligst oppdater Android systemets WebView-app for pålogging</string>
     <string name="webview_version_check_alert_dialog_positive_button_title">Oppdater</string>
+    <string name="webview_version_check_alert_dialog_title">Oppdater Android systemets WebView</string>
     <string name="what_s_new_image">Hva er nytt-bilde</string>
     <string name="whats_new_skip">Hopp over</string>
     <string name="whats_new_title">Nytt i %1$s</string>

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

@@ -18,6 +18,7 @@
     <string name="actionbar_copy">Kopiuj</string>
     <string name="actionbar_mkdir">Nowy katalog</string>
     <string name="actionbar_move">Przenieś</string>
+    <string name="actionbar_move_or_copy">Przenieś lub kopiuj</string>
     <string name="actionbar_open_with">Otwórz za pomocą</string>
     <string name="actionbar_search">Wyszukaj</string>
     <string name="actionbar_see_details">Szczegóły</string>
@@ -944,7 +945,9 @@
     <string name="wait_a_moment">Proszę czekać…</string>
     <string name="wait_checking_credentials">Sprawdzanie danych</string>
     <string name="wait_for_tmp_copy_from_private_storage">Kopiowanie pliku z prywatnego magazynu</string>
+    <string name="webview_version_check_alert_dialog_message">Aby się zalogować, zaktualizuj aplikację WebView systemu Android</string>
     <string name="webview_version_check_alert_dialog_positive_button_title">Aktualizuj</string>
+    <string name="webview_version_check_alert_dialog_title">Zaktualizuj WebView systemu Android</string>
     <string name="what_s_new_image">Jaki jest nowy obraz</string>
     <string name="whats_new_skip">Pomiń</string>
     <string name="whats_new_title">Co nowego w %1$s</string>

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

@@ -325,7 +325,6 @@
     <string name="file_delete">Удалить</string>
     <string name="file_detail_activity_error">Ошибка получения истории событий, связанных с файлом</string>
     <string name="file_details_no_content">Не удалось получить подробные сведения</string>
-    <string name="file_downloader_notification_title_prefix">Скачивание \u0020</string>
     <string name="file_icon">Файл</string>
     <string name="file_keep">Сохранить</string>
     <string name="file_list_empty">Добавьте что-нибудь или синхронизируйте со своими устройствами!</string>

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

@@ -325,7 +325,6 @@
     <string name="file_delete">Обриши</string>
     <string name="file_detail_activity_error">Грешка при добављању активности за фајл</string>
     <string name="file_details_no_content">Грешка при учитавању детаља</string>
-    <string name="file_downloader_notification_title_prefix">Преузима се \u0020</string>
     <string name="file_icon">Фајл</string>
     <string name="file_keep">Задржи</string>
     <string name="file_list_empty">Отпремите неки садржај или синхронизујте са вашим уређајима.</string>

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

@@ -325,7 +325,6 @@
     <string name="file_delete">Ta bort</string>
     <string name="file_detail_activity_error">Fel vid hämtning av aktiviteter för fil</string>
     <string name="file_details_no_content">Kunde inte läsa in detaljer</string>
-    <string name="file_downloader_notification_title_prefix">Laddar ner \u0020</string>
     <string name="file_icon">Fil</string>
     <string name="file_keep">Behåll</string>
     <string name="file_list_empty">Ladda upp något eller synkronisera med dina enheter</string>

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

@@ -325,7 +325,6 @@
     <string name="file_delete">Sil</string>
     <string name="file_detail_activity_error">Dosya işlemleri alınırken sorun çıktı</string>
     <string name="file_details_no_content">Ayrıntılar yüklenemedi</string>
-    <string name="file_downloader_notification_title_prefix">İndiriliyor \u0020</string>
     <string name="file_icon">Dosya</string>
     <string name="file_keep">Tut</string>
     <string name="file_list_empty">Bazı içerikler yükleyin ya da aygıtlarınızla eşitleyin.</string>

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

@@ -323,7 +323,6 @@
     <string name="file_delete">Вилучити</string>
     <string name="file_detail_activity_error">Помилка з отриманням дії для файлу</string>
     <string name="file_details_no_content">Не вдалося завантажити подробиці</string>
-    <string name="file_downloader_notification_title_prefix">Звантаження \u0020</string>
     <string name="file_icon">Файл</string>
     <string name="file_keep">Зберегти</string>
     <string name="file_list_empty">Додати дані або синхронізувати з вашими пристроями.</string>

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

@@ -325,7 +325,6 @@
     <string name="file_delete">删除</string>
     <string name="file_detail_activity_error">获取文件动态时出错</string>
     <string name="file_details_no_content">加载详情失败</string>
-    <string name="file_downloader_notification_title_prefix">下载中 \u0020</string>
     <string name="file_icon">文件</string>
     <string name="file_keep">保留</string>
     <string name="file_list_empty">上传一些内容或与您的设备同步。</string>

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

@@ -325,7 +325,6 @@
     <string name="file_delete">刪除</string>
     <string name="file_detail_activity_error">取得檔案活動時發生錯誤</string>
     <string name="file_details_no_content">載入詳細資訊失敗</string>
-    <string name="file_downloader_notification_title_prefix">正在下載 \u0020</string>
     <string name="file_icon">檔案</string>
     <string name="file_keep">保留</string>
     <string name="file_list_empty">上傳一些內容或與您的裝置同步。</string>

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

@@ -162,7 +162,6 @@
     <string name="uploads_view_upload_status_fetching_server_version">Fetching server version…</string>
     <string name="uploads_view_later_waiting_to_upload">Waiting to upload</string>
     <string name="uploads_view_group_header" translatable="false">%1$s (%2$d)</string>
-    <string name="file_downloader_notification_title_prefix">Downloading \u0020</string>
     <string name="downloader_download_in_progress_ticker">Downloading…</string>
     <string name="downloader_download_in_progress_content">%1$d%% Downloading %2$s</string>
     <string name="downloader_download_succeeded_ticker">Downloaded</string>
@@ -603,6 +602,7 @@
     <string name="actionbar_calendar_contacts_restore">Restore contacts &amp; calendar</string>
     <string name="contacts_backup_button">Back up now</string>
     <string name="contactlist_no_permission">No permission given, nothing imported.</string>
+    <string name="contact_no_permission">Contact permission is required.</string>
     <string name="restore_backup">Restore backup</string>
     <string name="contacts_preferences_no_file_found">No file found</string>
     <string name="contacts_preferences_something_strange_happened">Could not find your last backup!</string>
@@ -940,6 +940,7 @@
     <string name="link_share_file_drop">File drop (upload only)</string>
     <string name="could_not_retrieve_shares">Could not retrieve shares</string>
     <string name="failed_update_ui">Failed to update UI</string>
+    <string name="email_pick_failed">Failed to pick email address.</string>
     <string name="remote">(remote)</string>
     <string name="set_status">Set status</string>
     <string name="online_status">Online status</string>

+ 8 - 8
app/src/main/res/values/styles.xml

@@ -70,7 +70,7 @@
         <item name="colorPrimary">@color/text_color</item>
         <item name="colorOnSurface">@color/text_color</item>
         <item name="colorError">@color/hwSecurityRed</item>
-        <item name="editTextStyle">@style/Widget.MaterialComponents.TextInputEditText.OutlinedBox</item>
+        <item name="editTextStyle">@style/Widget.Material3.TextInputEditText.OutlinedBox</item>
     </style>
 
     <style name="TextInputLayoutInputWarning" parent="Widget.App.TextInputLayout">
@@ -102,7 +102,7 @@
         <item name="android:buttonBarButtonStyle">@style/FallbackTheming.Dialog.ButtonStyle</item>
     </style>
 
-    <style name="FallbackTheming.Dialog.ButtonStyle" parent="Widget.MaterialComponents.Button.TextButton.Dialog">
+    <style name="FallbackTheming.Dialog.ButtonStyle" parent="Widget.Material3.Button.TextButton.Dialog">
         <item name="android:textColor">@color/text_color</item>
     </style>
 
@@ -363,7 +363,7 @@
 
     <style name="Widget.App.Login.TextInputLayout" parent="Widget.Material3.TextInputLayout.OutlinedBox">
         <item name="materialThemeOverlay">@style/ThemeOverlay.App.Login.TextInputLayout</item>
-        <item name="shapeAppearance">@style/ShapeAppearance.MaterialComponents.SmallComponent</item>
+        <item name="shapeAppearance">@style/ShapeAppearance.Material3.SmallComponent</item>
         <item name="hintTextColor">?attr/colorOnSurface</item>
     </style>
 
@@ -371,9 +371,9 @@
         <item name="colorPrimary">@color/white</item>
         <item name="colorOnSurface">@color/white</item>
         <item name="colorError">@color/hwSecurityRed</item>
-        <item name="textAppearanceSubtitle1">@style/TextAppearance.MaterialComponents.Subtitle1</item>
-        <item name="textAppearanceCaption">@style/TextAppearance.MaterialComponents.Caption</item>
-        <item name="editTextStyle">@style/Widget.MaterialComponents.TextInputEditText.OutlinedBox</item>
+        <item name="textAppearanceSubtitle1">@style/TextAppearance.Material3.BodyLarge</item>
+        <item name="textAppearanceCaption">@style/TextAppearance.Material3.BodySmall</item>
+        <item name="editTextStyle">@style/Widget.Material3.TextInputEditText.OutlinedBox</item>
     </style>
 
     <style name="TextInputLayoutLogin" parent="Widget.Material3.TextInputLayout.OutlinedBox">
@@ -476,11 +476,11 @@
     </style>
 
 
-    <style name="ThemeOverlay.App.BottomSheetDialog" parent="@style/ThemeOverlay.MaterialComponents.BottomSheetDialog">
+    <style name="ThemeOverlay.App.BottomSheetDialog" parent="@style/ThemeOverlay.Material3.BottomSheetDialog">
         <item name="bottomSheetStyle">@style/App.BottomSheetDialog</item>
     </style>
 
-    <style name="App.BottomSheetDialog" parent="Widget.MaterialComponents.BottomSheet.Modal">
+    <style name="App.BottomSheetDialog" parent="Widget.Material3.BottomSheet.Modal">
         <item name="backgroundTint">@color/bg_default</item>
     </style>
 

+ 3 - 3
app/src/test/java/com/nextcloud/client/core/LocalConnectionTest.kt

@@ -65,7 +65,7 @@ class LocalConnectionTest {
 
         // THEN
         //      no binding is performed
-        verify(exactly = 0) { context.bindService(any(), any(), any()) }
+        verify(exactly = 0) { context.bindService(any(), any(), Context.BIND_AUTO_CREATE) }
     }
 
     @Test
@@ -76,12 +76,12 @@ class LocalConnectionTest {
 
         // WHEN
         //      bind requested
-        every { context.bindService(mockIntent, any(), any()) } returns true
+        every { context.bindService(mockIntent!!, any(), Context.BIND_AUTO_CREATE) } returns true
         connection.bind()
 
         // THEN
         //      service bound
-        verify { context.bindService(mockIntent, any(), any()) }
+        verify { context.bindService(mockIntent!!, any(), Context.BIND_AUTO_CREATE) }
     }
 
     @Test

+ 7 - 2
gradle.properties

@@ -9,5 +9,10 @@ android.nonTransitiveRClass=false
 android.nonFinalResIds=false
 #android.debug.obsoleteApi=true
 
-# Minimum max heap space to get reliable builds
-org.gradle.jvmargs=-Xmx1g
+
+# JVM arguments to optimize heap usage, enable heap dump on out-of-memory errors, and set the file encoding
+org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
+kotlin.daemon.jvmargs=-Xmx4096m
+org.gradle.caching=true
+org.gradle.parallel=true
+org.gradle.configureondemand=true

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

@@ -1,2 +1,2 @@
 DO NOT TOUCH; GENERATED BY DRONE
-      <span class="mdl-layout-title">Lint Report: 75 warnings</span>
+      <span class="mdl-layout-title">Lint Report: 74 warnings</span>