瀏覽代碼

Merge master

Signed-off-by: alperozturk <alper_ozturk@proton.me>
alperozturk 1 年之前
父節點
當前提交
6c71a29c26
共有 44 個文件被更改,包括 1828 次插入1884 次删除
  1. 4 2
      .drone.yml
  2. 二進制
      app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_check.png
  3. 二進制
      app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_delete.png
  4. 二進制
      app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_request.png
  5. 10 5
      app/src/androidTest/java/com/nextcloud/client/FileDisplayActivityIT.kt
  6. 9 5
      app/src/androidTest/java/com/owncloud/android/AbstractOnServerIT.java
  7. 1 1
      app/src/androidTest/java/com/owncloud/android/ui/trashbin/TrashbinLocalRepository.kt
  8. 9 2
      app/src/main/java/com/nextcloud/client/jobs/MediaFoldersDetectionWork.kt
  9. 12 8
      app/src/main/java/com/nextcloud/client/onboarding/FirstRunActivity.kt
  10. 0 159
      app/src/main/java/com/nextcloud/client/onboarding/WhatsNewActivity.java
  11. 169 0
      app/src/main/java/com/nextcloud/client/onboarding/WhatsNewActivity.kt
  12. 39 31
      app/src/main/java/com/owncloud/android/MainApp.java
  13. 0 40
      app/src/main/java/com/owncloud/android/authentication/DeepLinkLoginActivity.java
  14. 37 0
      app/src/main/java/com/owncloud/android/authentication/DeepLinkLoginActivity.kt
  15. 19 17
      app/src/main/java/com/owncloud/android/datamodel/MediaFolder.kt
  16. 18 27
      app/src/main/java/com/owncloud/android/datamodel/MediaFolderType.kt
  17. 0 51
      app/src/main/java/com/owncloud/android/datamodel/MediaFoldersModel.java
  18. 24 0
      app/src/main/java/com/owncloud/android/datamodel/MediaFoldersModel.kt
  19. 1 1
      app/src/main/java/com/owncloud/android/datamodel/SyncedFolderProvider.java
  20. 7 0
      app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java
  21. 0 489
      app/src/main/java/com/owncloud/android/ui/activity/PassCodeActivity.java
  22. 437 0
      app/src/main/java/com/owncloud/android/ui/activity/PassCodeActivity.kt
  23. 0 254
      app/src/main/java/com/owncloud/android/ui/activity/RichDocumentsEditorWebView.java
  24. 244 0
      app/src/main/java/com/owncloud/android/ui/activity/RichDocumentsEditorWebView.kt
  25. 19 19
      app/src/main/java/com/owncloud/android/ui/adapter/ShareeListAdapter.java
  26. 1 1
      app/src/main/java/com/owncloud/android/ui/adapter/SyncedFolderAdapter.java
  27. 52 0
      app/src/main/java/com/owncloud/android/ui/components/PassCodeEditText.kt
  28. 1 1
      app/src/main/java/com/owncloud/android/ui/dialog/parcel/SyncedFolderParcelable.java
  29. 2 2
      app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java
  30. 0 227
      app/src/main/java/com/owncloud/android/ui/trashbin/RemoteTrashbinRepository.java
  31. 182 0
      app/src/main/java/com/owncloud/android/ui/trashbin/RemoteTrashbinRepository.kt
  32. 0 334
      app/src/main/java/com/owncloud/android/ui/trashbin/TrashbinActivity.java
  33. 340 0
      app/src/main/java/com/owncloud/android/ui/trashbin/TrashbinActivity.kt
  34. 17 33
      app/src/main/java/com/owncloud/android/ui/trashbin/TrashbinContract.kt
  35. 0 116
      app/src/main/java/com/owncloud/android/ui/trashbin/TrashbinPresenter.java
  36. 117 0
      app/src/main/java/com/owncloud/android/ui/trashbin/TrashbinPresenter.kt
  37. 10 16
      app/src/main/java/com/owncloud/android/ui/trashbin/TrashbinRepository.kt
  38. 19 32
      app/src/main/res/layout/passcodelock.xml
  39. 6 6
      app/src/main/res/values-cs-rCZ/strings.xml
  40. 4 0
      app/src/main/res/values-hu-rHU/strings.xml
  41. 10 4
      app/src/main/res/values-ja-rJP/strings.xml
  42. 5 0
      app/src/main/res/values-uk/strings.xml
  43. 2 0
      app/src/main/res/values/styles.xml
  44. 1 1
      scripts/analysis/lint-results.txt

+ 4 - 2
.drone.yml

@@ -32,7 +32,7 @@ services:
     image: ghcr.io/nextcloud/continuous-integration-shallow-server:latest # also change in updateScreenshots.sh
     environment:
       EVAL: true
-      SERVER_VERSION: 'stable25'
+      SERVER_VERSION: 'stable27'
     commands:
       - BRANCH="$SERVER_VERSION" /usr/local/bin/initnc.sh
       - echo 127.0.0.1 server >> /etc/hosts
@@ -171,4 +171,6 @@ name: GIT_TOKEN
 data: XIoa9IYq+xQ+N5iln8dlpWv0jV6ROr7HuE24ioUr4uQ8m8SjyH0yognWYLYLqnbTKrFWlFZiEMQTH/sZiWjRFvV1iL0=
 ---
 kind: signature
-hmac: aec1cabcb7b4f98f1be1d5b7a052a629ce3193e19a3692dd52ada48b6e72a1c9
+hmac: b78dcc477ff74ccbd7877df011090783847f8b5215a994be6597408bd735b524
+
+...

二進制
app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_check.png


二進制
app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_delete.png


二進制
app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_request.png


+ 10 - 5
app/src/androidTest/java/com/nextcloud/client/FileDisplayActivityIT.kt

@@ -25,6 +25,8 @@ import android.app.Activity
 import androidx.test.espresso.Espresso
 import androidx.test.espresso.Espresso.onView
 import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
+import androidx.test.espresso.action.ViewActions.scrollTo
 import androidx.test.espresso.assertion.ViewAssertions.matches
 import androidx.test.espresso.contrib.DrawerActions
 import androidx.test.espresso.contrib.NavigationViewActions
@@ -238,12 +240,15 @@ class FileDisplayActivityIT : AbstractOnServerIT() {
         onView(withId(R.id.sort_button)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
 
         // browse into folder
-        onView(withId(R.id.list_root)).perform(
-            RecyclerViewActions.actionOnItemAtPosition<OCFileListItemViewHolder>(
-                0,
-                click()
+        onView(withId(R.id.list_root))
+            .perform(scrollTo())
+            .perform(closeSoftKeyboard())
+            .perform(
+                RecyclerViewActions.actionOnItemAtPosition<OCFileListItemViewHolder>(
+                    0,
+                    click()
+                )
             )
-        )
         shortSleep()
         checkToolbarTitle(topFolder)
         // sort button should now be visible

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

@@ -128,11 +128,15 @@ public abstract class AbstractOnServerIT extends AbstractIT {
 
             if (!remoteFile.getRemotePath().equals("/")) {
                 if (remoteFile.isEncrypted()) {
-                    assertTrue(new ToggleEncryptionRemoteOperation(remoteFile.getLocalId(),
-                                                                   remoteFile.getRemotePath(),
-                                                                   false)
-                                   .execute(client)
-                                   .isSuccess());
+                    ToggleEncryptionRemoteOperation operation = new ToggleEncryptionRemoteOperation(remoteFile.getLocalId(),
+                                                                                                    remoteFile.getRemotePath(),
+                                                                                                    false);
+
+                    boolean operationResult = operation
+                        .execute(client)
+                        .isSuccess();
+
+                    assertTrue(operationResult);
                 }
 
                 boolean removeResult = false;

+ 1 - 1
app/src/androidTest/java/com/owncloud/android/ui/trashbin/TrashbinLocalRepository.kt

@@ -26,7 +26,7 @@ import com.owncloud.android.R
 import com.owncloud.android.lib.resources.trashbin.model.TrashbinFile
 import com.owncloud.android.ui.trashbin.TrashbinRepository.LoadFolderCallback
 
-class TrashbinLocalRepository(val testCase: TrashbinActivityIT.TestCase) : TrashbinRepository {
+class TrashbinLocalRepository(private val testCase: TrashbinActivityIT.TestCase) : TrashbinRepository {
     override fun emptyTrashbin(callback: TrashbinRepository.OperationCallback?) {
         TODO("Not yet implemented")
     }

+ 9 - 2
app/src/main/java/com/nextcloud/client/jobs/MediaFoldersDetectionWork.kt

@@ -105,12 +105,19 @@ class MediaFoldersDetectionWork constructor(
         )
         val imageMediaFolderPaths: MutableList<String> = ArrayList()
         val videoMediaFolderPaths: MutableList<String> = ArrayList()
+
         for (imageMediaFolder in imageMediaFolders) {
-            imageMediaFolderPaths.add(imageMediaFolder.absolutePath)
+            imageMediaFolder.absolutePath?.let {
+                imageMediaFolderPaths.add(it)
+            }
         }
+
         for (videoMediaFolder in videoMediaFolders) {
-            imageMediaFolderPaths.add(videoMediaFolder.absolutePath)
+            videoMediaFolder.absolutePath?.let {
+                imageMediaFolderPaths.add(it)
+            }
         }
+
         val arbitraryDataString = arbitraryDataProvider.getValue(ACCOUNT_NAME_GLOBAL, KEY_MEDIA_FOLDERS)
         if (!TextUtils.isEmpty(arbitraryDataString)) {
             mediaFoldersModel = gson.fromJson(arbitraryDataString, MediaFoldersModel::class.java)

+ 12 - 8
app/src/main/java/com/nextcloud/client/onboarding/FirstRunActivity.kt

@@ -198,17 +198,21 @@ class FirstRunActivity : BaseActivity(), ViewPager.OnPageChangeListener, Injecta
             this,
             object : OnBackPressedCallback(true) {
                 override fun handleOnBackPressed() {
-                    onFinish()
+                    val isFromAddAccount = intent.getBooleanExtra(EXTRA_ALLOW_CLOSE, false)
 
-                    if (intent.getBooleanExtra(EXTRA_ALLOW_CLOSE, false)) {
-                        onBackPressedDispatcher.onBackPressed()
+                    val destination: Intent = if (isFromAddAccount) {
+                        Intent(applicationContext, FileDisplayActivity::class.java)
                     } else {
-                        val intent = Intent(applicationContext, AuthenticatorActivity::class.java)
-                        intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
-                        intent.putExtra(EXTRA_EXIT, true)
-                        startActivity(intent)
-                        finish()
+                        Intent(applicationContext, AuthenticatorActivity::class.java)
                     }
+
+                    if (!isFromAddAccount) {
+                        destination.putExtra(EXTRA_EXIT, true)
+                    }
+
+                    destination.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
+                    startActivity(destination)
+                    finish()
                 }
             }
         )

+ 0 - 159
app/src/main/java/com/nextcloud/client/onboarding/WhatsNewActivity.java

@@ -1,159 +0,0 @@
-/*
- * Nextcloud Android client application
- *
- * @author Bartosz Przybylski
- * @author Chris Narkiewicz
- * Copyright (C) 2015 Bartosz Przybylski
- * Copyright (C) 2015 ownCloud Inc.
- * Copyright (C) 2016 Nextcloud.
- * 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 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.nextcloud.client.onboarding;
-
-import android.os.Bundle;
-import android.view.View;
-import android.widget.Button;
-import android.widget.ImageButton;
-import android.widget.TextView;
-
-import com.google.android.material.button.MaterialButton;
-import com.nextcloud.android.common.ui.theme.utils.ColorRole;
-import com.nextcloud.client.appinfo.AppInfo;
-import com.nextcloud.client.di.Injectable;
-import com.nextcloud.client.preferences.AppPreferences;
-import com.owncloud.android.BuildConfig;
-import com.owncloud.android.R;
-import com.owncloud.android.databinding.WhatsNewActivityBinding;
-import com.owncloud.android.ui.adapter.FeaturesViewAdapter;
-import com.owncloud.android.ui.adapter.FeaturesWebViewAdapter;
-import com.owncloud.android.ui.whatsnew.ProgressIndicator;
-import com.owncloud.android.utils.theme.ViewThemeUtils;
-
-import javax.inject.Inject;
-
-import androidx.fragment.app.FragmentActivity;
-import androidx.viewpager.widget.ViewPager;
-
-/**
- * Activity displaying new features after an update.
- */
-public class WhatsNewActivity extends FragmentActivity implements ViewPager.OnPageChangeListener, Injectable {
-
-    @Inject AppPreferences preferences;
-    @Inject AppInfo appInfo;
-    @Inject OnboardingService onboarding;
-    @Inject ViewThemeUtils.Factory viewThemeUtilsFactory;
-    private ViewThemeUtils viewThemeUtils;
-    
-    private WhatsNewActivityBinding binding;
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        binding = WhatsNewActivityBinding.inflate(getLayoutInflater());
-        setContentView(binding.getRoot());
-
-        viewThemeUtils = viewThemeUtilsFactory.withPrimaryAsBackground();
-        viewThemeUtils.platform.themeStatusBar(this, ColorRole.PRIMARY);
-
-        
-        String[] urls = getResources().getStringArray(R.array.whatsnew_urls);
-
-        boolean showWebView = urls.length > 0;
-
-        if (showWebView) {
-            FeaturesWebViewAdapter featuresWebViewAdapter = new FeaturesWebViewAdapter(getSupportFragmentManager(),
-                                                                                       urls);
-            binding.progressIndicator.setNumberOfSteps(featuresWebViewAdapter.getCount());
-            binding.contentPanel.setAdapter(featuresWebViewAdapter);
-        } else {
-            FeaturesViewAdapter featuresViewAdapter = new FeaturesViewAdapter(getSupportFragmentManager(),
-                                                                              onboarding.getWhatsNew());
-            binding.progressIndicator.setNumberOfSteps(featuresViewAdapter.getCount());
-            binding.contentPanel.setAdapter(featuresViewAdapter);
-        }
-
-        binding.contentPanel.addOnPageChangeListener(this);
-
-        viewThemeUtils.platform.colorImageView(binding.forward, ColorRole.ON_PRIMARY);
-
-        binding.forward.setOnClickListener(view -> {
-            if (binding.progressIndicator.hasNextStep()) {
-                binding.contentPanel.setCurrentItem(binding.contentPanel.getCurrentItem() + 1, true);
-                binding.progressIndicator.animateToStep(binding.contentPanel.getCurrentItem() + 1);
-            } else {
-                onFinish();
-                finish();
-            }
-            updateNextButtonIfNeeded();
-        });
-
-        binding.forward.setBackground(null);
-
-        viewThemeUtils.platform.colorTextView(binding.skip, ColorRole.ON_PRIMARY);
-        binding.skip.setOnClickListener(view -> {
-            onFinish();
-            finish();
-        });
-
-        viewThemeUtils.platform.colorTextView(binding.welcomeText, ColorRole.ON_PRIMARY);
-
-        if (showWebView) {
-            binding.welcomeText.setText(R.string.app_name);
-        } else {
-            binding.welcomeText.setText(String.format(getString(R.string.whats_new_title), appInfo.getVersionName()));
-        }
-
-        updateNextButtonIfNeeded();
-    }
-
-    @Override
-    public void onBackPressed() {
-        onFinish();
-        super.onBackPressed();
-    }
-
-    private void updateNextButtonIfNeeded() {
-        if (!binding.progressIndicator.hasNextStep()) {
-            binding.forward.setImageResource(R.drawable.ic_ok);
-            binding.skip.setVisibility(View.INVISIBLE);
-        } else {
-            binding.forward.setImageResource(R.drawable.arrow_right);
-            binding.skip.setVisibility(View.VISIBLE);
-        }
-    }
-
-    private void onFinish() {
-        preferences.setLastSeenVersionCode(BuildConfig.VERSION_CODE);
-    }
-
-    @Override
-    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
-        // unused but to be implemented due to abstract parent
-    }
-
-    @Override
-    public void onPageSelected(int position) {
-        binding.progressIndicator.animateToStep(position + 1);
-        updateNextButtonIfNeeded();
-    }
-
-    @Override
-    public void onPageScrollStateChanged(int state) {
-        // unused but to be implemented due to abstract parent
-    }
-}
-

+ 169 - 0
app/src/main/java/com/nextcloud/client/onboarding/WhatsNewActivity.kt

@@ -0,0 +1,169 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Bartosz Przybylski
+ * @author Chris Narkiewicz
+ * Copyright (C) 2015 Bartosz Przybylski
+ * Copyright (C) 2015 ownCloud Inc.
+ * Copyright (C) 2016 Nextcloud.
+ * 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 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.nextcloud.client.onboarding
+
+import android.os.Bundle
+import android.view.View
+import androidx.activity.OnBackPressedCallback
+import androidx.fragment.app.FragmentActivity
+import androidx.viewpager.widget.ViewPager
+import com.nextcloud.android.common.ui.theme.utils.ColorRole
+import com.nextcloud.client.appinfo.AppInfo
+import com.nextcloud.client.di.Injectable
+import com.nextcloud.client.preferences.AppPreferences
+import com.owncloud.android.BuildConfig
+import com.owncloud.android.R
+import com.owncloud.android.databinding.WhatsNewActivityBinding
+import com.owncloud.android.ui.adapter.FeaturesViewAdapter
+import com.owncloud.android.ui.adapter.FeaturesWebViewAdapter
+import com.owncloud.android.utils.theme.ViewThemeUtils
+import javax.inject.Inject
+
+/**
+ * Activity displaying new features after an update.
+ */
+class WhatsNewActivity : FragmentActivity(), ViewPager.OnPageChangeListener, Injectable {
+
+    @JvmField
+    @Inject
+    var preferences: AppPreferences? = null
+
+    @JvmField
+    @Inject
+    var appInfo: AppInfo? = null
+
+    @JvmField
+    @Inject
+    var onboarding: OnboardingService? = null
+
+    @JvmField
+    @Inject
+    var viewThemeUtilsFactory: ViewThemeUtils.Factory? = null
+
+    private var viewThemeUtils: ViewThemeUtils? = null
+
+    private lateinit var binding: WhatsNewActivityBinding
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        binding = WhatsNewActivityBinding.inflate(layoutInflater)
+        setContentView(binding.root)
+
+        viewThemeUtils = viewThemeUtilsFactory?.withPrimaryAsBackground()
+        viewThemeUtils?.platform?.themeStatusBar(this, ColorRole.PRIMARY)
+
+        val urls = resources.getStringArray(R.array.whatsnew_urls)
+        val showWebView = urls.isNotEmpty()
+
+        setupFeatureViewAdapter(showWebView, urls)
+        binding.contentPanel.addOnPageChangeListener(this)
+        setupForwardImageButton()
+        setupSkipImageButton()
+        setupWelcomeText(showWebView)
+        updateNextButtonIfNeeded()
+        handleOnBackPressed()
+    }
+
+    @Suppress("SpreadOperator")
+    private fun setupFeatureViewAdapter(showWebView: Boolean, urls: Array<String>) {
+        val adapter = if (showWebView) {
+            FeaturesWebViewAdapter(supportFragmentManager, *urls)
+        } else {
+            onboarding?.let {
+                FeaturesViewAdapter(supportFragmentManager, *it.whatsNew)
+            }
+        }
+
+        adapter?.let {
+            binding.progressIndicator.setNumberOfSteps(it.count)
+            binding.contentPanel.adapter = it
+        }
+    }
+
+    private fun setupForwardImageButton() {
+        viewThemeUtils?.platform?.colorImageView(binding.forward, ColorRole.ON_PRIMARY)
+        binding.forward.setOnClickListener {
+            if (binding.progressIndicator.hasNextStep()) {
+                binding.contentPanel.setCurrentItem(binding.contentPanel.currentItem + 1, true)
+                binding.progressIndicator.animateToStep(binding.contentPanel.currentItem + 1)
+            } else {
+                onFinish()
+                finish()
+            }
+            updateNextButtonIfNeeded()
+        }
+        binding.forward.background = null
+    }
+
+    private fun setupSkipImageButton() {
+        viewThemeUtils?.platform?.colorTextView(binding.skip, ColorRole.ON_PRIMARY)
+        binding.skip.setOnClickListener {
+            onFinish()
+            finish()
+        }
+    }
+
+    private fun setupWelcomeText(showWebView: Boolean) {
+        viewThemeUtils?.platform?.colorTextView(binding.welcomeText, ColorRole.ON_PRIMARY)
+        binding.welcomeText.text = if (showWebView) {
+            getString(R.string.app_name)
+        } else {
+            String.format(getString(R.string.whats_new_title), appInfo?.versionName)
+        }
+    }
+
+    private fun handleOnBackPressed() {
+        onBackPressedDispatcher.addCallback(
+            this,
+            object : OnBackPressedCallback(true) {
+                override fun handleOnBackPressed() {
+                    onFinish()
+                    onBackPressedDispatcher.onBackPressed()
+                }
+            }
+        )
+    }
+
+    private fun updateNextButtonIfNeeded() {
+        val hasNextStep = binding.progressIndicator.hasNextStep()
+        binding.forward.setImageResource(if (hasNextStep) R.drawable.arrow_right else R.drawable.ic_ok)
+        binding.skip.visibility = if (hasNextStep) View.VISIBLE else View.INVISIBLE
+    }
+
+    private fun onFinish() {
+        preferences?.lastSeenVersionCode = BuildConfig.VERSION_CODE
+    }
+
+    override fun onPageSelected(position: Int) {
+        binding.progressIndicator.animateToStep(position + 1)
+        updateNextButtonIfNeeded()
+    }
+
+    @Suppress("EmptyFunctionBlock")
+    override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {}
+
+    @Suppress("EmptyFunctionBlock")
+    override fun onPageScrollStateChanged(state: Int) {}
+}

+ 39 - 31
app/src/main/java/com/owncloud/android/MainApp.java

@@ -254,9 +254,12 @@ public class MainApp extends MultiDexApplication implements HasAndroidInjector {
 
         if (!isCrashReportingProcess && !appInfo.isDebugBuild()) {
             Thread.UncaughtExceptionHandler defaultPlatformHandler = Thread.getDefaultUncaughtExceptionHandler();
-            final ExceptionHandler crashReporter = new ExceptionHandler(this,
-                                                                        defaultPlatformHandler);
-            Thread.setDefaultUncaughtExceptionHandler(crashReporter);
+
+            if (defaultPlatformHandler != null) {
+                final ExceptionHandler crashReporter = new ExceptionHandler(this,
+                                                                            defaultPlatformHandler);
+                Thread.setDefaultUncaughtExceptionHandler(crashReporter);
+            }
         }
     }
 
@@ -791,25 +794,34 @@ public class MainApp extends MultiDexApplication implements HasAndroidInjector {
                     + syncedFolder.getId() + " - " + syncedFolder.getLocalPath());
 
                 for (MediaFolder imageMediaFolder : imageMediaFolders) {
-                    if (imageMediaFolder.absolutePath.equals(syncedFolder.getLocalPath())) {
-                        newSyncedFolder = (SyncedFolder) syncedFolder.clone();
-                        newSyncedFolder.setType(MediaFolderType.IMAGE);
-                        primaryKey = syncedFolderProvider.storeSyncedFolder(newSyncedFolder);
-                        Log_OC.i(TAG, "Migrated image synced_folders record: "
-                            + primaryKey + " - " + newSyncedFolder.getLocalPath());
-                        break;
+                    String absolutePathOfImageFolder = imageMediaFolder.absolutePath;
+
+                    if (absolutePathOfImageFolder != null) {
+                        if (absolutePathOfImageFolder.equals(syncedFolder.getLocalPath())) {
+                            newSyncedFolder = (SyncedFolder) syncedFolder.clone();
+                            newSyncedFolder.setType(MediaFolderType.IMAGE);
+                            primaryKey = syncedFolderProvider.storeSyncedFolder(newSyncedFolder);
+                            Log_OC.i(TAG, "Migrated image synced_folders record: "
+                                + primaryKey + " - " + newSyncedFolder.getLocalPath());
+                            break;
+                        }
                     }
                 }
 
                 for (MediaFolder videoMediaFolder : videoMediaFolders) {
-                    if (videoMediaFolder.absolutePath.equals(syncedFolder.getLocalPath())) {
-                        newSyncedFolder = (SyncedFolder) syncedFolder.clone();
-                        newSyncedFolder.setType(MediaFolderType.VIDEO);
-                        primaryKey = syncedFolderProvider.storeSyncedFolder(newSyncedFolder);
-                        Log_OC.i(TAG, "Migrated video synced_folders record: "
-                            + primaryKey + " - " + newSyncedFolder.getLocalPath());
-                        break;
+                    String absolutePathOfVideoFolder = videoMediaFolder.absolutePath;
+
+                    if (absolutePathOfVideoFolder != null) {
+                        if (absolutePathOfVideoFolder.equals(syncedFolder.getLocalPath())) {
+                            newSyncedFolder = (SyncedFolder) syncedFolder.clone();
+                            newSyncedFolder.setType(MediaFolderType.VIDEO);
+                            primaryKey = syncedFolderProvider.storeSyncedFolder(newSyncedFolder);
+                            Log_OC.i(TAG, "Migrated video synced_folders record: "
+                                + primaryKey + " - " + newSyncedFolder.getLocalPath());
+                            break;
+                        }
                     }
+
                 }
             }
 
@@ -835,19 +847,22 @@ public class MainApp extends MultiDexApplication implements HasAndroidInjector {
 
             List<SyncedFolder> syncedFolderList = syncedFolderProvider.getSyncedFolders();
             Map<Pair<String, String>, Long> syncedFolders = new HashMap<>();
-            ArrayList<Long> ids = new ArrayList<>();
             for (SyncedFolder syncedFolder : syncedFolderList) {
                 Pair<String, String> checkPair = new Pair<>(syncedFolder.getAccount(), syncedFolder.getLocalPath());
                 if (syncedFolders.containsKey(checkPair)) {
-                    if (syncedFolder.getId() > syncedFolders.get(checkPair)) {
-                        syncedFolders.put(checkPair, syncedFolder.getId());
+                    Long folderId = syncedFolders.get(checkPair);
+
+                    if (folderId != null) {
+                        if (syncedFolder.getId() > folderId) {
+                            syncedFolders.put(checkPair, syncedFolder.getId());
+                        }
                     }
                 } else {
                     syncedFolders.put(checkPair, syncedFolder.getId());
                 }
             }
 
-            ids.addAll(syncedFolders.values());
+            ArrayList<Long> ids = new ArrayList<>(syncedFolders.values());
 
             if (ids.size() > 0) {
                 int deletedCount = syncedFolderProvider.deleteSyncedFoldersNotInList(ids);
@@ -865,18 +880,11 @@ public class MainApp extends MultiDexApplication implements HasAndroidInjector {
         return dispatchingAndroidInjector;
     }
 
-
     public static void setAppTheme(DarkMode mode) {
         switch (mode) {
-            case LIGHT:
-                AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
-                break;
-            case DARK:
-                AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
-                break;
-            case SYSTEM:
-                AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
-                break;
+            case LIGHT -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
+            case DARK -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
+            case SYSTEM -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
         }
     }
 }

+ 0 - 40
app/src/main/java/com/owncloud/android/authentication/DeepLinkLoginActivity.java

@@ -1,40 +0,0 @@
-package com.owncloud.android.authentication;
-
-import android.net.Uri;
-import android.os.Bundle;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import com.nextcloud.client.di.Injectable;
-import com.owncloud.android.R;
-
-public class DeepLinkLoginActivity extends AuthenticatorActivity implements Injectable {
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        if (!getResources().getBoolean(R.bool.multiaccount_support) &&
-            accountManager.getAccounts().length == 1) {
-            Toast.makeText(this, R.string.no_mutliple_accounts_allowed, Toast.LENGTH_LONG).show();
-            return;
-        }
-
-        setContentView(R.layout.deep_link_login);
-
-        Uri data = getIntent().getData();
-
-        if (data != null) {
-            try {
-                String prefix = getString(R.string.login_data_own_scheme) + PROTOCOL_SUFFIX + "login/";
-                LoginUrlInfo loginUrlInfo = parseLoginDataUrl(prefix, data.toString());
-
-                TextView loginText = findViewById(R.id.loginInfo);
-                loginText.setText(String.format(getString(R.string.direct_login_text), loginUrlInfo.username,
-                                                loginUrlInfo.serverAddress));
-            } catch (IllegalArgumentException e) {
-                Toast.makeText(this, R.string.direct_login_failed, Toast.LENGTH_LONG).show();
-            }
-        }
-    }
-}

+ 37 - 0
app/src/main/java/com/owncloud/android/authentication/DeepLinkLoginActivity.kt

@@ -0,0 +1,37 @@
+package com.owncloud.android.authentication
+
+import android.os.Bundle
+import android.widget.TextView
+import android.widget.Toast
+import com.nextcloud.client.di.Injectable
+import com.owncloud.android.R
+
+class DeepLinkLoginActivity : AuthenticatorActivity(), Injectable {
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        if (!resources.getBoolean(R.bool.multiaccount_support) &&
+            accountManager.accounts.size == 1
+        ) {
+            Toast.makeText(this, R.string.no_mutliple_accounts_allowed, Toast.LENGTH_LONG).show()
+            return
+        }
+
+        setContentView(R.layout.deep_link_login)
+
+        intent.data?.let {
+            try {
+                val prefix = getString(R.string.login_data_own_scheme) + PROTOCOL_SUFFIX + "login/"
+                val loginUrlInfo = parseLoginDataUrl(prefix, it.toString())
+                val loginText = findViewById<TextView>(R.id.loginInfo)
+                loginText.text = String.format(
+                    getString(R.string.direct_login_text), loginUrlInfo.username,
+                    loginUrlInfo.serverAddress
+                )
+            } catch (e: IllegalArgumentException) {
+                Toast.makeText(this, R.string.direct_login_failed, Toast.LENGTH_LONG).show()
+            }
+        }
+    }
+}

+ 19 - 17
app/src/main/java/com/owncloud/android/datamodel/MediaFolder.java → app/src/main/java/com/owncloud/android/datamodel/MediaFolder.kt

@@ -16,29 +16,31 @@
  * 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/>.
+ * License along with this program. If not, see <http:></http:>//www.gnu.org/licenses/>.
  */
-package com.owncloud.android.datamodel;
-
-import java.util.ArrayList;
-import java.util.List;
+package com.owncloud.android.datamodel
 
 /**
  * Business object representing a media folder with all information that are gathered via media queries.
  */
-public class MediaFolder {
-    /** name of the folder. */
-    public String folderName;
+class MediaFolder {
+    /** name of the folder.  */
+    @JvmField
+    var folderName: String? = null
+
+    /** absolute path of the folder.  */
+    @JvmField
+    var absolutePath: String? = null
 
-    /** absolute path of the folder. */
-    public String absolutePath;
-    
-    /** list of file paths of the folder's content */
-    public List<String> filePaths = new ArrayList<>();
+    /** list of file paths of the folder's content  */
+    @JvmField
+    var filePaths: List<String> = ArrayList()
 
-    /** total number of files in the media folder. */
-    public long numberOfFiles;
+    /** total number of files in the media folder.  */
+    @JvmField
+    var numberOfFiles: Long = 0
 
-    /** type of media folder. */
-    public MediaFolderType type;
+    /** type of media folder.  */
+    @JvmField
+    var type: MediaFolderType? = null
 }

+ 18 - 27
app/src/main/java/com/owncloud/android/datamodel/MediaFolderType.java → app/src/main/java/com/owncloud/android/datamodel/MediaFolderType.kt

@@ -17,37 +17,28 @@
  * 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.datamodel;
+package com.owncloud.android.datamodel
 
-import android.util.SparseArray;
+import android.util.SparseArray
 
 /**
  * Types of media folder.
  */
-public enum MediaFolderType {
-    CUSTOM(0),
-    IMAGE(1),
-    VIDEO(2);
-
-    private Integer id;
-
-    private static SparseArray<MediaFolderType> reverseMap = new SparseArray<>(3);
-
-    static {
-        reverseMap.put(CUSTOM.getId(), CUSTOM);
-        reverseMap.put(IMAGE.getId(), IMAGE);
-        reverseMap.put(VIDEO.getId(), VIDEO);
-    }
-
-    MediaFolderType(Integer id) {
-        this.id = id;
-    }
-
-    public static MediaFolderType getById(Integer id) {
-        return reverseMap.get(id);
-    }
-
-    public Integer getId() {
-        return this.id;
+enum class MediaFolderType(@JvmField val id: Int) {
+    CUSTOM(0), IMAGE(1), VIDEO(2);
+
+    companion object {
+        private val reverseMap = SparseArray<MediaFolderType>(3)
+
+        init {
+            reverseMap.put(CUSTOM.id, CUSTOM)
+            reverseMap.put(IMAGE.id, IMAGE)
+            reverseMap.put(VIDEO.id, VIDEO)
+        }
+
+        @JvmStatic
+        fun getById(id: Int?): MediaFolderType {
+            return reverseMap[id!!]
+        }
     }
 }

+ 0 - 51
app/src/main/java/com/owncloud/android/datamodel/MediaFoldersModel.java

@@ -1,51 +0,0 @@
-/*
- * Nextcloud Android client application
- *
- * @author Mario Danic
- * @author Andy Scherzinger
- * Copyright (C) 2018 Mario Danic
- * Copyright (C) 2018 Andy Scherzinger
- *
- * 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.datamodel;
-
-import java.util.List;
-
-public class MediaFoldersModel {
-    private List<String> imageMediaFolders;
-    private List<String> videoMediaFolders;
-
-    public MediaFoldersModel(List<String> imageMediaFolders, List<String> videoMediaFolders) {
-        this.imageMediaFolders = imageMediaFolders;
-        this.videoMediaFolders = videoMediaFolders;
-    }
-
-    public List<String> getImageMediaFolders() {
-        return this.imageMediaFolders;
-    }
-
-    public List<String> getVideoMediaFolders() {
-        return this.videoMediaFolders;
-    }
-
-    public void setImageMediaFolders(List<String> imageMediaFolders) {
-        this.imageMediaFolders = imageMediaFolders;
-    }
-
-    public void setVideoMediaFolders(List<String> videoMediaFolders) {
-        this.videoMediaFolders = videoMediaFolders;
-    }
-}

+ 24 - 0
app/src/main/java/com/owncloud/android/datamodel/MediaFoldersModel.kt

@@ -0,0 +1,24 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Mario Danic
+ * @author Andy Scherzinger
+ * Copyright (C) 2018 Mario Danic
+ * Copyright (C) 2018 Andy Scherzinger
+ *
+ * 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.datamodel
+
+class MediaFoldersModel(var imageMediaFolders: List<String>, var videoMediaFolders: List<String>)

+ 1 - 1
app/src/main/java/com/owncloud/android/datamodel/SyncedFolderProvider.java

@@ -415,7 +415,7 @@ public class SyncedFolderProvider extends Observable {
         cv.put(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_UPLOAD_ACTION, syncedFolder.getUploadAction());
         cv.put(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_NAME_COLLISION_POLICY,
                syncedFolder.getNameCollisionPolicyInt());
-        cv.put(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_TYPE, syncedFolder.getType().getId());
+        cv.put(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_TYPE, syncedFolder.getType().id);
         cv.put(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_HIDDEN, syncedFolder.isHidden());
         cv.put(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_SUBFOLDER_RULE, syncedFolder.getSubfolderRule().ordinal());
 

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

@@ -780,6 +780,13 @@ public abstract class FileActivity extends DrawerActivity
         }
     }
 
+    public void refreshList() {
+        final Fragment fileListFragment = getSupportFragmentManager().findFragmentByTag(FileDisplayActivity.TAG_LIST_OF_FILES);
+        if (fileListFragment != null)  {
+            ((OCFileListFragment) fileListFragment).onRefresh();
+        }
+    }
+
     private void onCreateShareViaLinkOperationFinish(CreateShareViaLinkOperation operation,
                                                      RemoteOperationResult result) {
         FileDetailSharingFragment sharingFragment = getShareFileFragment();

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

@@ -1,489 +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.View.OnClickListener;
-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.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 EditText[] passCodeEditTexts = new EditText[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 passcodeconfirmation
-                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(new OnClickListener() {
-                @Override
-                public void onClick(View 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() {
-        passCodeEditTexts[0].addTextChangedListener(new PassCodeDigitTextWatcher(0, false));
-        passCodeEditTexts[1].addTextChangedListener(new PassCodeDigitTextWatcher(1, false));
-        passCodeEditTexts[2].addTextChangedListener(new PassCodeDigitTextWatcher(2, false));
-        passCodeEditTexts[3].addTextChangedListener(new PassCodeDigitTextWatcher(3, true));
-
-        setOnKeyListener(1);
-        setOnKeyListener(2);
-        setOnKeyListener(3);
-
-        passCodeEditTexts[1].setOnFocusChangeListener((v, hasFocus) -> onPassCodeEditTextFocusChange(1));
-
-        passCodeEditTexts[2].setOnFocusChangeListener((v, hasFocus) -> onPassCodeEditTextFocusChange(2));
-
-        passCodeEditTexts[3].setOnFocusChangeListener((v, hasFocus) -> onPassCodeEditTextFocusChange(3));
-    }
-
-    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;
-
-        boolean result = true;
-        for (int i = 0; i < passCodeEditTexts.length && result; i++) {
-            result = passCodeEditTexts[i].getText().toString().equals(passCodeDigits[i]);
-        }
-        return result;
-    }
-
-    /**
-     * 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 * 1000);
-
-                        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) {
-                    passCodeDigits[mIndex] = passCodeEditTexts[mIndex].getText().toString();
-                }
-                passCodeEditTexts[next()].requestFocus();
-
-                if (mLastOne) {
-                    processFullPassCode();
-                }
-
-            } else {
-                Log_OC.d(TAG, "Text box " + mIndex + " was cleaned");
-            }
-        }
-
-        @Override
-        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
-            // nothing to do
-        }
-
-        @Override
-        public void onTextChanged(CharSequence s, int start, int before, int count) {
-            // nothing to do
-        }
-    }
-}

+ 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"
+    }
+}

+ 19 - 19
app/src/main/java/com/owncloud/android/ui/adapter/ShareeListAdapter.java

@@ -25,6 +25,7 @@
 
 package com.owncloud.android.ui.adapter;
 
+import android.annotation.SuppressLint;
 import android.graphics.drawable.Drawable;
 import android.view.LayoutInflater;
 import android.view.ViewGroup;
@@ -94,15 +95,15 @@ public class ShareeListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
     @Override
     public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
         switch (ShareType.fromValue(viewType)) {
-            case PUBLIC_LINK:
-            case EMAIL:
+            case PUBLIC_LINK, EMAIL -> {
                 return new LinkShareViewHolder(
                     FileDetailsShareLinkShareItemBinding.inflate(LayoutInflater.from(fileActivity),
                                                                  parent,
                                                                  false),
                     fileActivity,
                     viewThemeUtils);
-            case NEW_PUBLIC_LINK:
+            }
+            case NEW_PUBLIC_LINK -> {
                 if (encrypted) {
                     return new NewSecureFileDropViewHolder(
                         FileDetailsShareSecureFileDropAddNewItemBinding.inflate(LayoutInflater.from(fileActivity),
@@ -116,17 +117,20 @@ public class ShareeListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
                                                                             false)
                     );
                 }
-            case INTERNAL:
+            }
+            case INTERNAL -> {
                 return new InternalShareViewHolder(
                     FileDetailsShareInternalShareLinkBinding.inflate(LayoutInflater.from(fileActivity), parent, false),
                     fileActivity);
-            default:
+            }
+            default -> {
                 return new ShareViewHolder(FileDetailsShareShareItemBinding.inflate(LayoutInflater.from(fileActivity),
                                                                                     parent,
                                                                                     false),
                                            user,
                                            fileActivity,
                                            viewThemeUtils);
+            }
         }
     }
 
@@ -138,17 +142,13 @@ public class ShareeListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
 
         final OCShare share = shares.get(position);
 
-        if (holder instanceof LinkShareViewHolder) {
-            LinkShareViewHolder publicShareViewHolder = (LinkShareViewHolder) holder;
+        if (holder instanceof LinkShareViewHolder publicShareViewHolder) {
             publicShareViewHolder.bind(share, listener);
-        } else if (holder instanceof InternalShareViewHolder) {
-            InternalShareViewHolder internalShareViewHolder = (InternalShareViewHolder) holder;
+        } else if (holder instanceof InternalShareViewHolder internalShareViewHolder) {
             internalShareViewHolder.bind(share, listener);
-        } else if (holder instanceof NewLinkShareViewHolder) {
-            NewLinkShareViewHolder newLinkShareViewHolder = (NewLinkShareViewHolder) holder;
+        } else if (holder instanceof NewLinkShareViewHolder newLinkShareViewHolder) {
             newLinkShareViewHolder.bind(listener);
-        } else if (holder instanceof NewSecureFileDropViewHolder) {
-            NewSecureFileDropViewHolder newSecureFileDropViewHolder = (NewSecureFileDropViewHolder) holder;
+        } else if (holder instanceof NewSecureFileDropViewHolder newSecureFileDropViewHolder) {
             newSecureFileDropViewHolder.bind(listener);
         } else {
             ShareViewHolder userViewHolder = (ShareViewHolder) holder;
@@ -166,6 +166,7 @@ public class ShareeListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
         return shares.size();
     }
 
+    @SuppressLint("NotifyDataSetChanged")
     public void addShares(List<OCShare> sharesToAdd) {
         shares.addAll(sharesToAdd);
         sortShares();
@@ -174,22 +175,21 @@ public class ShareeListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
 
     @Override
     public void avatarGenerated(Drawable avatarDrawable, Object callContext) {
-        if (callContext instanceof ImageView) {
-            ImageView iv = (ImageView) callContext;
+        if (callContext instanceof ImageView iv) {
             iv.setImageDrawable(avatarDrawable);
         }
     }
 
     @Override
     public boolean shouldCallGeneratedCallback(String tag, Object callContext) {
-        if (callContext instanceof ImageView) {
-            ImageView iv = (ImageView) callContext;
+        if (callContext instanceof ImageView iv) {
             // needs to be changed once federated users have avatars
             return String.valueOf(iv.getTag()).equals(tag.split("@")[0]);
         }
         return false;
     }
 
+    @SuppressLint("NotifyDataSetChanged")
     public void remove(OCShare share) {
         shares.remove(share);
         notifyDataSetChanged();
@@ -210,8 +210,8 @@ public class ShareeListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
             }
         }
 
-        Collections.sort(links, (o1, o2) -> Long.compare(o2.getSharedDate(), o1.getSharedDate()));
-        Collections.sort(users, (o1, o2) -> Long.compare(o2.getSharedDate(), o1.getSharedDate()));
+        links.sort((o1, o2) -> Long.compare(o2.getSharedDate(), o1.getSharedDate()));
+        users.sort((o1, o2) -> Long.compare(o2.getSharedDate(), o1.getSharedDate()));
 
         shares = links;
         shares.addAll(users);

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

@@ -248,7 +248,7 @@ public class SyncedFolderAdapter extends SectionedRecyclerViewAdapter<SectionedV
     public int getSectionByLocalPathAndType(String localPath, int type) {
         for (int i = 0; i < filteredSyncFolderItems.size(); i++) {
             if (filteredSyncFolderItems.get(i).getLocalPath().equalsIgnoreCase(localPath) &&
-                filteredSyncFolderItems.get(i).getType().getId().equals(type)) {
+                filteredSyncFolderItems.get(i).getType().id == type) {
                 return i;
             }
         }

+ 52 - 0
app/src/main/java/com/owncloud/android/ui/components/PassCodeEditText.kt

@@ -0,0 +1,52 @@
+/*
+ * 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.owncloud.android.ui.components
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.util.AttributeSet
+import android.view.KeyEvent
+import android.view.MotionEvent
+import android.view.View
+import androidx.appcompat.widget.AppCompatEditText
+
+@SuppressLint("ClickableViewAccessibility")
+class PassCodeEditText(context: Context, attrs: AttributeSet?) : AppCompatEditText(context, attrs) {
+
+    init {
+        disableFocusChangeViaTap()
+    }
+
+    private fun disableFocusChangeViaTap() {
+        setSelectAllOnFocus(false)
+        setTextIsSelectable(false)
+        setOnTouchListener { _: View?, _: MotionEvent? -> true }
+    }
+
+    override fun onKeyPreIme(keyCode: Int, event: KeyEvent): Boolean {
+        val isBackButtonPressed = (event.keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP)
+        if (isBackButtonPressed) {
+            // Override default behaviour and prevent dismissing the keyboard
+            return true
+        }
+        return super.dispatchKeyEvent(event)
+    }
+}

+ 1 - 1
app/src/main/java/com/owncloud/android/ui/dialog/parcel/SyncedFolderParcelable.java

@@ -105,7 +105,7 @@ public class SyncedFolderParcelable implements Parcelable {
         dest.writeInt(existing ? 1 : 0);
         dest.writeInt(enabled ? 1 : 0);
         dest.writeInt(subfolderByDate ? 1 : 0);
-        dest.writeInt(type.getId());
+        dest.writeInt(type.id);
         dest.writeString(account);
         dest.writeInt(uploadAction);
         dest.writeInt(nameCollisionPolicy.serialize());

+ 2 - 2
app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java

@@ -932,15 +932,15 @@ public class FileOperationsHelper {
     }
 
     public void renameFile(OCFile file, String newFilename) {
-        // RenameFile
         Intent service = new Intent(fileActivity, OperationsService.class);
+
         service.setAction(OperationsService.ACTION_RENAME);
         service.putExtra(OperationsService.EXTRA_ACCOUNT, fileActivity.getAccount());
         service.putExtra(OperationsService.EXTRA_REMOTE_PATH, file.getRemotePath());
         service.putExtra(OperationsService.EXTRA_NEWNAME, newFilename);
         mWaitingForOpId = fileActivity.getOperationsServiceBinder().queueNewOperation(service);
 
-        fileActivity.showLoadingDialog(fileActivity.getString(R.string.wait_a_moment));
+        fileActivity.refreshList();
     }
 
 

+ 0 - 227
app/src/main/java/com/owncloud/android/ui/trashbin/RemoteTrashbinRepository.java

@@ -1,227 +0,0 @@
-/*
- * Nextcloud Android client application
- *
- * @author Tobias Kaminsky
- * @author TSI-mc
- * @author Chris Narkiewicz
- *
- * Copyright (C) 2018 Tobias Kaminsky
- * Copyright (C) 2018 Nextcloud GmbH.
- * Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
- * Copyright (C) 2023 TSI-mc
- *
- * 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.trashbin;
-
-import android.os.AsyncTask;
-
-import com.nextcloud.client.account.User;
-import com.nextcloud.client.network.ClientFactory;
-import com.nextcloud.common.NextcloudClient;
-import com.owncloud.android.R;
-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.trashbin.EmptyTrashbinRemoteOperation;
-import com.owncloud.android.lib.resources.trashbin.ReadTrashbinFolderRemoteOperation;
-import com.owncloud.android.lib.resources.trashbin.RemoveTrashbinFileRemoteOperation;
-import com.owncloud.android.lib.resources.trashbin.RestoreTrashbinFileRemoteOperation;
-import com.owncloud.android.lib.resources.trashbin.model.TrashbinFile;
-
-import java.util.List;
-
-import androidx.annotation.NonNull;
-
-public class RemoteTrashbinRepository implements TrashbinRepository {
-
-    private final User user;
-    private final ClientFactory clientFactory;
-
-    RemoteTrashbinRepository(User user, ClientFactory clientFactory) {
-        this.user = user;
-        this.clientFactory = clientFactory;
-    }
-
-    public void removeTrashbinFile(TrashbinFile file, OperationCallback callback) {
-        new RemoveTrashbinFileTask(user, clientFactory, file, callback).execute();
-    }
-
-    private static class RemoveTrashbinFileTask extends AsyncTask<Void, Void, Boolean> {
-
-        private User user;
-        private ClientFactory clientFactory;
-        private TrashbinFile file;
-        private OperationCallback callback;
-
-        private RemoveTrashbinFileTask(User user,
-                                       ClientFactory clientFactory,
-                                       TrashbinFile file,
-                                       OperationCallback callback) {
-            this.user = user;
-            this.clientFactory = clientFactory;
-            this.file = file;
-            this.callback = callback;
-        }
-
-        @Override
-        protected Boolean doInBackground(Void... voids) {
-            try {
-                OwnCloudClient client = clientFactory.create(user);
-                RemoteOperationResult result = new RemoveTrashbinFileRemoteOperation(file.getFullRemotePath())
-                    .execute(client);
-                return result.isSuccess();
-            } catch (ClientFactory.CreationException e) {
-                Log_OC.e(this, "Cannot create client", e);
-                return Boolean.FALSE;
-            }
-        }
-
-        @Override
-        protected void onPostExecute(Boolean success) {
-            super.onPostExecute(success);
-
-            callback.onResult(success);
-        }
-    }
-
-    public void emptyTrashbin(OperationCallback callback) {
-        new EmptyTrashbinTask(user, clientFactory, callback).execute();
-    }
-
-    private static class EmptyTrashbinTask extends AsyncTask<Void, Void, Boolean> {
-
-        private User user;
-        private ClientFactory clientFactory;
-        private OperationCallback callback;
-
-        private EmptyTrashbinTask(User user, ClientFactory clientFactory, OperationCallback callback) {
-            this.user = user;
-            this.clientFactory = clientFactory;
-            this.callback = callback;
-        }
-
-        @Override
-        protected Boolean doInBackground(Void... voids) {
-            try {
-                NextcloudClient client = clientFactory.createNextcloudClient(user);
-                EmptyTrashbinRemoteOperation emptyTrashbinFileOperation = new EmptyTrashbinRemoteOperation();
-                RemoteOperationResult<Boolean> result = emptyTrashbinFileOperation.execute(client);
-                return result.isSuccess();
-            } catch (ClientFactory.CreationException e) {
-                Log_OC.e(this, "Cannot create client", e);
-                return Boolean.FALSE;
-            }
-        }
-
-        @Override
-        protected void onPostExecute(Boolean success) {
-            super.onPostExecute(success);
-
-            callback.onResult(success);
-        }
-    }
-
-    @Override
-    public void restoreFile(TrashbinFile file, OperationCallback callback) {
-        new RestoreTrashbinFileTask(file, user, clientFactory, callback).execute();
-    }
-
-    private static class RestoreTrashbinFileTask extends AsyncTask<Void, Void, Boolean> {
-
-        private TrashbinFile file;
-        private User user;
-        private ClientFactory clientFactory;
-        private TrashbinRepository.OperationCallback callback;
-
-        private RestoreTrashbinFileTask(TrashbinFile file, User user, ClientFactory clientFactory,
-                                        TrashbinRepository.OperationCallback callback) {
-            this.file = file;
-            this.user = user;
-            this.clientFactory = clientFactory;
-            this.callback = callback;
-        }
-
-        @Override
-        protected Boolean doInBackground(Void... voids) {
-            try {
-                OwnCloudClient client = clientFactory.create(user);
-                RemoteOperationResult result = new RestoreTrashbinFileRemoteOperation(file.getFullRemotePath(),
-                                                                                      file.getFileName()).execute(client);
-
-                return result.isSuccess();
-            } catch (ClientFactory.CreationException e) {
-                Log_OC.e(this, "Cannot create client", e);
-                return Boolean.FALSE;
-            }
-        }
-
-        @Override
-        protected void onPostExecute(Boolean success) {
-            super.onPostExecute(success);
-
-            callback.onResult(success);
-        }
-    }
-
-    @Override
-    public void getFolder(String remotePath, @NonNull LoadFolderCallback callback) {
-        new ReadRemoteTrashbinFolderTask(remotePath, user, clientFactory, callback).execute();
-    }
-
-    private static class ReadRemoteTrashbinFolderTask extends AsyncTask<Void, Void, Boolean> {
-
-        private String remotePath;
-        private User user;
-        private ClientFactory clientFactory;
-        private List<TrashbinFile> trashbinFiles;
-        private LoadFolderCallback callback;
-
-        private ReadRemoteTrashbinFolderTask(String remotePath, User user, ClientFactory clientFactory,
-                                             LoadFolderCallback callback) {
-            this.remotePath = remotePath;
-            this.user = user;
-            this.clientFactory = clientFactory;
-            this.callback = callback;
-        }
-
-        @Override
-        protected Boolean doInBackground(Void... voids) {
-            try {
-                OwnCloudClient client = clientFactory.create(user);
-                RemoteOperationResult<List<TrashbinFile>> result =
-                    new ReadTrashbinFolderRemoteOperation(remotePath).execute(client);
-                if (result.isSuccess()) {
-                    trashbinFiles = result.getResultData();
-                    return Boolean.TRUE;
-                } else {
-                    return Boolean.FALSE;
-                }
-            } catch (ClientFactory.CreationException e) {
-                return Boolean.FALSE;
-            }
-        }
-
-        @Override
-        protected void onPostExecute(Boolean success) {
-            super.onPostExecute(success);
-
-            if (success) {
-                callback.onSuccess(trashbinFiles);
-            } else {
-                callback.onError(R.string.trashbin_loading_failed);
-            }
-        }
-    }
-}

+ 182 - 0
app/src/main/java/com/owncloud/android/ui/trashbin/RemoteTrashbinRepository.kt

@@ -0,0 +1,182 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * @author TSI-mc
+ * @author Chris Narkiewicz
+ *
+ * Copyright (C) 2018 Tobias Kaminsky
+ * Copyright (C) 2018 Nextcloud GmbH.
+ * Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
+ * Copyright (C) 2023 TSI-mc
+ *
+ * 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/>.
+ */
+@file:Suppress("DEPRECATION")
+
+package com.owncloud.android.ui.trashbin
+
+import android.os.AsyncTask
+import com.nextcloud.client.account.User
+import com.nextcloud.client.network.ClientFactory
+import com.nextcloud.client.network.ClientFactory.CreationException
+import com.owncloud.android.R
+import com.owncloud.android.lib.common.utils.Log_OC
+import com.owncloud.android.lib.resources.trashbin.EmptyTrashbinRemoteOperation
+import com.owncloud.android.lib.resources.trashbin.ReadTrashbinFolderRemoteOperation
+import com.owncloud.android.lib.resources.trashbin.RemoveTrashbinFileRemoteOperation
+import com.owncloud.android.lib.resources.trashbin.RestoreTrashbinFileRemoteOperation
+import com.owncloud.android.lib.resources.trashbin.model.TrashbinFile
+import com.owncloud.android.ui.trashbin.TrashbinRepository.LoadFolderCallback
+import com.owncloud.android.ui.trashbin.TrashbinRepository.OperationCallback
+
+class RemoteTrashbinRepository internal constructor(private val user: User, private val clientFactory: ClientFactory) :
+    TrashbinRepository {
+
+    override fun removeTrashbinFile(file: TrashbinFile?, callback: OperationCallback?) {
+        RemoveTrashbinFileTask(user, clientFactory, file, callback).execute()
+    }
+
+    private class RemoveTrashbinFileTask(
+        private val user: User,
+        private val clientFactory: ClientFactory,
+        private val file: TrashbinFile?,
+        private val callback: OperationCallback?
+    ) : AsyncTask<Void?, Void?, Boolean>() {
+
+        @Deprecated("Deprecated in Java")
+        override fun doInBackground(vararg voids: Void?): Boolean {
+            return try {
+                val client = clientFactory.create(user)
+                val result = RemoveTrashbinFileRemoteOperation(file!!.fullRemotePath)
+                    .execute(client)
+                result.isSuccess
+            } catch (e: CreationException) {
+                Log_OC.e(this, "Cannot create client", e)
+                false
+            }
+        }
+
+        @Deprecated("Deprecated in Java")
+        override fun onPostExecute(success: Boolean) {
+            super.onPostExecute(success)
+            callback?.onResult(success)
+        }
+    }
+
+    override fun emptyTrashbin(callback: OperationCallback?) {
+        EmptyTrashbinTask(user, clientFactory, callback).execute()
+    }
+
+    private class EmptyTrashbinTask(
+        private val user: User,
+        private val clientFactory: ClientFactory,
+        private val callback: OperationCallback?
+    ) : AsyncTask<Void?, Void?, Boolean>() {
+
+        @Deprecated("Deprecated in Java")
+        override fun doInBackground(vararg voids: Void?): Boolean {
+            return try {
+                val client = clientFactory.createNextcloudClient(user)
+                val emptyTrashbinFileOperation = EmptyTrashbinRemoteOperation()
+                val result = emptyTrashbinFileOperation.execute(client)
+                result.isSuccess
+            } catch (e: CreationException) {
+                Log_OC.e(this, "Cannot create client", e)
+                false
+            }
+        }
+
+        @Deprecated("Deprecated in Java")
+        override fun onPostExecute(success: Boolean) {
+            super.onPostExecute(success)
+            callback?.onResult(success)
+        }
+    }
+
+    override fun restoreFile(file: TrashbinFile?, callback: OperationCallback?) {
+        RestoreTrashbinFileTask(file, user, clientFactory, callback).execute()
+    }
+
+    private class RestoreTrashbinFileTask(
+        private val file: TrashbinFile?,
+        private val user: User,
+        private val clientFactory: ClientFactory,
+        private val callback: OperationCallback?
+    ) : AsyncTask<Void?, Void?, Boolean>() {
+
+        @Deprecated("Deprecated in Java")
+        override fun doInBackground(vararg voids: Void?): Boolean {
+            return try {
+                val client = clientFactory.create(user)
+                val result = RestoreTrashbinFileRemoteOperation(
+                    file!!.fullRemotePath,
+                    file.fileName
+                ).execute(client)
+                result.isSuccess
+            } catch (e: CreationException) {
+                Log_OC.e(this, "Cannot create client", e)
+                false
+            }
+        }
+
+        @Deprecated("Deprecated in Java")
+        override fun onPostExecute(success: Boolean) {
+            super.onPostExecute(success)
+            callback?.onResult(success)
+        }
+    }
+
+    override fun getFolder(remotePath: String?, callback: LoadFolderCallback?) {
+        callback?.let {
+            ReadRemoteTrashbinFolderTask(remotePath, user, clientFactory, it).execute()
+        }
+    }
+
+    private class ReadRemoteTrashbinFolderTask(
+        private val remotePath: String?,
+        private val user: User,
+        private val clientFactory: ClientFactory,
+        private val callback: LoadFolderCallback
+    ) : AsyncTask<Void?, Void?, Boolean>() {
+        private var trashbinFiles: List<TrashbinFile?>? = null
+
+        @Deprecated("Deprecated in Java")
+        override fun doInBackground(vararg voids: Void?): Boolean {
+            return try {
+                val client = clientFactory.create(user)
+                val result = ReadTrashbinFolderRemoteOperation(remotePath).execute(client)
+                if (result.isSuccess) {
+                    trashbinFiles = result.resultData
+                    true
+                } else {
+                    false
+                }
+            } catch (e: CreationException) {
+                false
+            }
+        }
+
+        @Deprecated("Deprecated in Java")
+        override fun onPostExecute(success: Boolean) {
+            super.onPostExecute(success)
+
+            if (success) {
+                callback.onSuccess(trashbinFiles)
+            } else {
+                callback.onError(R.string.trashbin_loading_failed)
+            }
+        }
+    }
+}

+ 0 - 334
app/src/main/java/com/owncloud/android/ui/trashbin/TrashbinActivity.java

@@ -1,334 +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.trashbin;
-
-import android.content.Intent;
-import android.os.Bundle;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
-import android.widget.PopupMenu;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import com.google.android.material.snackbar.Snackbar;
-import com.nextcloud.client.account.CurrentAccountProvider;
-import com.nextcloud.client.account.User;
-import com.nextcloud.client.di.Injectable;
-import com.nextcloud.client.network.ClientFactory;
-import com.nextcloud.client.preferences.AppPreferences;
-import com.nextcloud.java.util.Optional;
-import com.owncloud.android.R;
-import com.owncloud.android.databinding.TrashbinActivityBinding;
-import com.owncloud.android.lib.resources.trashbin.model.TrashbinFile;
-import com.owncloud.android.ui.EmptyRecyclerView;
-import com.owncloud.android.ui.activity.DrawerActivity;
-import com.owncloud.android.ui.adapter.TrashbinListAdapter;
-import com.owncloud.android.ui.dialog.SortingOrderDialogFragment;
-import com.owncloud.android.ui.interfaces.TrashbinActivityInterface;
-import com.owncloud.android.utils.DisplayUtils;
-import com.owncloud.android.utils.FileSortOrder;
-import com.owncloud.android.utils.theme.ViewThemeUtils;
-
-import java.util.List;
-
-import javax.inject.Inject;
-
-import androidx.annotation.VisibleForTesting;
-import androidx.core.content.res.ResourcesCompat;
-import androidx.recyclerview.widget.LinearLayoutManager;
-
-import static com.owncloud.android.utils.DisplayUtils.openSortingOrderDialogFragment;
-
-/**
- * Presenting trashbin data, received from presenter
- */
-public class TrashbinActivity extends DrawerActivity implements
-    TrashbinActivityInterface,
-    SortingOrderDialogFragment.OnSortingOrderListener,
-    TrashbinContract.View,
-    Injectable {
-
-    public static final int EMPTY_LIST_COUNT = 1;
-    @Inject AppPreferences preferences;
-    @Inject CurrentAccountProvider accountProvider;
-    @Inject ClientFactory clientFactory;
-    @Inject ViewThemeUtils viewThemeUtils;
-
-    private TrashbinListAdapter trashbinListAdapter;
-
-    @VisibleForTesting
-    TrashbinPresenter trashbinPresenter;
-
-    private boolean active;
-    private TrashbinActivityBinding binding;
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        final User currentUser = getUser().orElse(accountProvider.getUser());
-        final String targetAccount = getIntent().getStringExtra(Intent.EXTRA_USER);
-        if (targetAccount != null && !currentUser.nameEquals(targetAccount)) {
-            final Optional<User> targetUser = getUserAccountManager().getUser(targetAccount);
-            if (targetUser.isPresent()) {
-                setUser(targetUser.get());
-            } else {
-                Toast.makeText(this, R.string.associated_account_not_found, Toast.LENGTH_LONG).show();
-                finish();
-                return;
-            }
-        }
-
-        final RemoteTrashbinRepository trashRepository =
-            new RemoteTrashbinRepository(getUser().orElse(accountProvider.getUser()), clientFactory);
-        trashbinPresenter = new TrashbinPresenter(trashRepository, this);
-
-        binding = TrashbinActivityBinding.inflate(getLayoutInflater());
-        setContentView(binding.getRoot());
-
-        setupToolbar();
-        findViewById(R.id.sort_list_button_group).setVisibility(View.VISIBLE);
-        findViewById(R.id.switch_grid_view_button).setVisibility(View.GONE);
-        updateActionBarTitleAndHomeButtonByString(getString(R.string.trashbin_activity_title));
-        setupDrawer(R.id.nav_trashbin);
-    }
-
-    @Override
-    protected void onStart() {
-        super.onStart();
-        active = true;
-        setupContent();
-    }
-
-    @Override
-    protected void onResume() {
-        super.onResume();
-
-        setDrawerMenuItemChecked(R.id.nav_trashbin);
-    }
-
-    private void setupContent() {
-        EmptyRecyclerView recyclerView = binding.list;
-        recyclerView.setEmptyView(binding.emptyList.emptyListView);
-        binding.emptyList.emptyListView.setVisibility(View.GONE);
-        binding.emptyList.emptyListIcon.setImageResource(R.drawable.ic_delete);
-        binding.emptyList.emptyListIcon.setVisibility(View.VISIBLE);
-        binding.emptyList.emptyListViewHeadline.setText(getString(R.string.trashbin_empty_headline));
-        binding.emptyList.emptyListViewText.setText(getString(R.string.trashbin_empty_message));
-        binding.emptyList.emptyListViewText.setVisibility(View.VISIBLE);
-
-        trashbinListAdapter = new TrashbinListAdapter(
-            this,
-            getStorageManager(),
-            preferences,
-            this,
-            getUser().orElse(accountProvider.getUser()),
-            viewThemeUtils
-        );
-        recyclerView.setAdapter(trashbinListAdapter);
-        recyclerView.setHasFixedSize(true);
-        recyclerView.setHasFooter(true);
-        recyclerView.setLayoutManager(new LinearLayoutManager(this));
-
-        viewThemeUtils.androidx.themeSwipeRefreshLayout(binding.swipeContainingList);
-        binding.swipeContainingList.setOnRefreshListener(this::loadFolder);
-
-        viewThemeUtils.material.colorMaterialTextButton(findViewById(R.id.sort_button));
-
-        findViewById(R.id.sort_button).setOnClickListener(l ->
-                                                              openSortingOrderDialogFragment(getSupportFragmentManager(),
-                                                                                             preferences.getSortOrderByType(
-                                                                                                 FileSortOrder.Type.trashBinView,
-                                                                                                 FileSortOrder.sort_new_to_old))
-                                                         );
-
-        loadFolder();
-    }
-
-    protected void loadFolder() {
-        if (trashbinListAdapter.getItemCount() > EMPTY_LIST_COUNT) {
-            binding.swipeContainingList.setRefreshing(true);
-        } else {
-            showInitialLoading();
-        }
-        trashbinPresenter.loadFolder();
-    }
-
-    @Override
-    public boolean onOptionsItemSelected(MenuItem item) {
-        boolean retval = true;
-        int itemId = item.getItemId();
-        if (itemId == android.R.id.home) {
-            if (isDrawerOpen()) {
-                closeDrawer();
-            } else if (trashbinPresenter.isRoot()) {
-                onBackPressed();
-            } else {
-                openDrawer();
-            }
-        } else if (itemId == R.id.action_empty_trashbin) {
-            trashbinPresenter.emptyTrashbin();
-        } else {
-            retval = super.onOptionsItemSelected(item);
-        }
-
-        return retval;
-    }
-
-    @Override
-    public void onOverflowIconClicked(TrashbinFile file, View view) {
-        PopupMenu popup = new PopupMenu(this, view);
-        popup.inflate(R.menu.item_trashbin);
-
-        popup.setOnMenuItemClickListener(item -> {
-            trashbinPresenter.removeTrashbinFile(file);
-
-            return true;
-        });
-        popup.show();
-    }
-
-    @Override
-    public void onItemClicked(TrashbinFile file) {
-        if (file.isFolder()) {
-            trashbinPresenter.enterFolder(file.getRemotePath());
-
-            mDrawerToggle.setDrawerIndicatorEnabled(false);
-        }
-    }
-
-    @Override
-    public void onRestoreIconClicked(TrashbinFile file, View view) {
-        trashbinPresenter.restoreTrashbinFile(file);
-    }
-
-    @Override
-    public boolean onCreateOptionsMenu(Menu menu) {
-        getMenuInflater().inflate(R.menu.activity_trashbin, menu);
-
-        return true;
-    }
-
-    @Override
-    protected void onPause() {
-        super.onPause();
-        active = false;
-
-        trashbinListAdapter.cancelAllPendingTasks();
-    }
-
-    @Override
-    public void onBackPressed() {
-        trashbinPresenter.navigateUp();
-    }
-
-    public void close() {
-        super.onBackPressed();
-    }
-
-    public void setDrawerIndicatorEnabled(boolean bool) {
-        mDrawerToggle.setDrawerIndicatorEnabled(bool);
-    }
-
-
-    @Override
-    public void onSortingOrderChosen(FileSortOrder sortOrder) {
-        TextView sortButton = findViewById(R.id.sort_button);
-        sortButton.setText(DisplayUtils.getSortOrderStringId(sortOrder));
-        trashbinListAdapter.setSortOrder(sortOrder);
-    }
-
-    @Override
-    public void showTrashbinFolder(List<TrashbinFile> trashbinFiles) {
-        if (active) {
-            trashbinListAdapter.setTrashbinFiles(trashbinFiles, true);
-            binding.swipeContainingList.setRefreshing(false);
-            binding.loadingContent.setVisibility(View.GONE);
-            binding.emptyList.emptyListIcon.setImageResource(R.drawable.ic_delete);
-            binding.emptyList.emptyListViewHeadline.setText(getString(R.string.trashbin_empty_headline));
-            binding.emptyList.emptyListViewText.setText(getString(R.string.trashbin_empty_message));
-            binding.list.setVisibility(View.VISIBLE);
-        }
-    }
-
-    @Override
-    public void removeFile(TrashbinFile file) {
-        if (active) {
-            trashbinListAdapter.removeFile(file);
-        }
-    }
-
-    @Override
-    public void removeAllFiles() {
-        trashbinListAdapter.removeAllFiles();
-    }
-
-    @Override
-    public void showSnackbarError(int message, TrashbinFile file) {
-        if (active) {
-            binding.swipeContainingList.setRefreshing(false);
-            Snackbar.make(binding.list,
-                          String.format(getString(message), file.getFileName()), Snackbar.LENGTH_LONG)
-                .show();
-        }
-    }
-
-    @VisibleForTesting
-    public void showInitialLoading() {
-        binding.emptyList.emptyListView.setVisibility(View.GONE);
-        binding.list.setVisibility(View.GONE);
-        binding.loadingContent.setVisibility(View.VISIBLE);
-    }
-
-    @VisibleForTesting
-    public void showUser() {
-        binding.loadingContent.setVisibility(View.GONE);
-        binding.list.setVisibility(View.VISIBLE);
-        binding.swipeContainingList.setRefreshing(false);
-
-        binding.emptyList.emptyListViewText.setText(getUser().get().getAccountName());
-        binding.emptyList.emptyListViewText.setVisibility(View.VISIBLE);
-        binding.emptyList.emptyListView.setVisibility(View.VISIBLE);
-    }
-
-    @Override
-    public void showError(int message) {
-        if (active) {
-            trashbinListAdapter.removeAllFiles();
-            
-            binding.loadingContent.setVisibility(View.GONE);
-            binding.list.setVisibility(View.VISIBLE);
-            binding.swipeContainingList.setRefreshing(false);
-
-            binding.emptyList.emptyListViewHeadline.setText(R.string.common_error);
-            binding.emptyList.emptyListIcon.setImageDrawable(ResourcesCompat.getDrawable(getResources(),
-                                                                                         R.drawable.ic_list_empty_error,
-                                                                                         null));
-            binding.emptyList.emptyListViewText.setText(message);
-            binding.emptyList.emptyListViewText.setVisibility(View.VISIBLE);
-            binding.emptyList.emptyListIcon.setVisibility(View.VISIBLE);
-            binding.emptyList.emptyListView.setVisibility(View.VISIBLE);
-        }
-    }
-}

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

@@ -0,0 +1,340 @@
+/*
+ * 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.trashbin
+
+import android.content.Intent
+import android.os.Bundle
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import android.widget.PopupMenu
+import android.widget.TextView
+import android.widget.Toast
+import androidx.activity.OnBackPressedCallback
+import androidx.annotation.VisibleForTesting
+import androidx.core.content.res.ResourcesCompat
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.google.android.material.snackbar.Snackbar
+import com.nextcloud.client.account.CurrentAccountProvider
+import com.nextcloud.client.di.Injectable
+import com.nextcloud.client.network.ClientFactory
+import com.nextcloud.client.preferences.AppPreferences
+import com.owncloud.android.R
+import com.owncloud.android.databinding.TrashbinActivityBinding
+import com.owncloud.android.lib.resources.trashbin.model.TrashbinFile
+import com.owncloud.android.ui.activity.DrawerActivity
+import com.owncloud.android.ui.adapter.TrashbinListAdapter
+import com.owncloud.android.ui.dialog.SortingOrderDialogFragment.OnSortingOrderListener
+import com.owncloud.android.ui.interfaces.TrashbinActivityInterface
+import com.owncloud.android.utils.DisplayUtils
+import com.owncloud.android.utils.FileSortOrder
+import com.owncloud.android.utils.theme.ViewThemeUtils
+import javax.inject.Inject
+
+/**
+ * Presenting trashbin data, received from presenter
+ */
+class TrashbinActivity :
+    DrawerActivity(),
+    TrashbinActivityInterface,
+    OnSortingOrderListener,
+    TrashbinContract.View,
+    Injectable {
+
+    @JvmField
+    @Inject
+    var preferences: AppPreferences? = null
+
+    @JvmField
+    @Inject
+    var accountProvider: CurrentAccountProvider? = null
+
+    @JvmField
+    @Inject
+    var clientFactory: ClientFactory? = null
+
+    @JvmField
+    @Inject
+    var viewThemeUtils: ViewThemeUtils? = null
+
+    private var trashbinListAdapter: TrashbinListAdapter? = null
+
+    @VisibleForTesting
+    var trashbinPresenter: TrashbinPresenter? = null
+
+    private var active = false
+    private lateinit var binding: TrashbinActivityBinding
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        val currentUser = user.orElse(accountProvider!!.user)
+        val targetAccount = intent.getStringExtra(Intent.EXTRA_USER)
+
+        if (targetAccount != null && !currentUser.nameEquals(targetAccount)) {
+            val targetUser = userAccountManager.getUser(targetAccount)
+            if (targetUser.isPresent) {
+                setUser(targetUser.get())
+            } else {
+                Toast.makeText(this, R.string.associated_account_not_found, Toast.LENGTH_LONG).show()
+                finish()
+                return
+            }
+        }
+
+        clientFactory?.let {
+            val trashRepository = RemoteTrashbinRepository(user.orElse(accountProvider!!.user), it)
+            trashbinPresenter = TrashbinPresenter(trashRepository, this)
+        }
+
+        binding = TrashbinActivityBinding.inflate(layoutInflater)
+
+        setContentView(binding.root)
+        setupToolbar()
+
+        findViewById<View>(R.id.sort_list_button_group).visibility = View.VISIBLE
+        findViewById<View>(R.id.switch_grid_view_button).visibility =
+            View.GONE
+
+        updateActionBarTitleAndHomeButtonByString(getString(R.string.trashbin_activity_title))
+        setupDrawer(R.id.nav_trashbin)
+    }
+
+    override fun onStart() {
+        super.onStart()
+
+        active = true
+        setupContent()
+    }
+
+    override fun onResume() {
+        super.onResume()
+
+        setDrawerMenuItemChecked(R.id.nav_trashbin)
+    }
+
+    private fun setupContent() {
+        val recyclerView = binding.list
+        recyclerView.setEmptyView(binding.emptyList.emptyListView)
+
+        binding.emptyList.emptyListView.visibility = View.GONE
+        binding.emptyList.emptyListIcon.setImageResource(R.drawable.ic_delete)
+        binding.emptyList.emptyListIcon.visibility = View.VISIBLE
+        binding.emptyList.emptyListViewHeadline.text = getString(R.string.trashbin_empty_headline)
+        binding.emptyList.emptyListViewText.text = getString(R.string.trashbin_empty_message)
+        binding.emptyList.emptyListViewText.visibility = View.VISIBLE
+
+        trashbinListAdapter = TrashbinListAdapter(
+            this,
+            storageManager,
+            preferences,
+            this,
+            user.orElse(accountProvider!!.user),
+            viewThemeUtils
+        )
+
+        recyclerView.adapter = trashbinListAdapter
+        recyclerView.setHasFixedSize(true)
+        recyclerView.setHasFooter(true)
+        recyclerView.layoutManager = LinearLayoutManager(this)
+
+        viewThemeUtils.androidx.themeSwipeRefreshLayout(binding.swipeContainingList)
+        binding.swipeContainingList.setOnRefreshListener { loadFolder() }
+        viewThemeUtils.material.colorMaterialTextButton(findViewById(R.id.sort_button))
+
+        findViewById<View>(R.id.sort_button).setOnClickListener {
+            DisplayUtils.openSortingOrderDialogFragment(
+                supportFragmentManager,
+                preferences?.getSortOrderByType(
+                    FileSortOrder.Type.trashBinView,
+                    FileSortOrder.sort_new_to_old
+                )
+            )
+        }
+
+        loadFolder()
+
+        handleOnBackPressed()
+    }
+
+    private fun handleOnBackPressed() {
+        onBackPressedDispatcher.addCallback(
+            this,
+            object : OnBackPressedCallback(true) {
+                override fun handleOnBackPressed() {
+                    trashbinPresenter?.navigateUp()
+                }
+            }
+        )
+    }
+
+    fun loadFolder() {
+        trashbinListAdapter?.let {
+            if (it.itemCount > EMPTY_LIST_COUNT) {
+                binding.swipeContainingList.isRefreshing = true
+            } else {
+                showInitialLoading()
+            }
+
+            trashbinPresenter?.loadFolder()
+        }
+    }
+
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        var retval = true
+        val itemId = item.itemId
+        if (itemId == android.R.id.home) {
+            if (isDrawerOpen) {
+                closeDrawer()
+            } else if (trashbinPresenter?.isRoot == true) {
+                trashbinPresenter?.navigateUp()
+            } else {
+                openDrawer()
+            }
+        } else if (itemId == R.id.action_empty_trashbin) {
+            trashbinPresenter?.emptyTrashbin()
+        } else {
+            retval = super.onOptionsItemSelected(item)
+        }
+        return retval
+    }
+
+    override fun onOverflowIconClicked(file: TrashbinFile, view: View) {
+        val popup = PopupMenu(this, view)
+        popup.inflate(R.menu.item_trashbin)
+        popup.setOnMenuItemClickListener {
+            trashbinPresenter?.removeTrashbinFile(file)
+            true
+        }
+        popup.show()
+    }
+
+    override fun onItemClicked(file: TrashbinFile) {
+        if (file.isFolder) {
+            trashbinPresenter?.enterFolder(file.remotePath)
+            mDrawerToggle.isDrawerIndicatorEnabled = false
+        }
+    }
+
+    override fun onRestoreIconClicked(file: TrashbinFile, view: View) {
+        trashbinPresenter?.restoreTrashbinFile(file)
+    }
+
+    override fun onCreateOptionsMenu(menu: Menu): Boolean {
+        menuInflater.inflate(R.menu.activity_trashbin, menu)
+        return true
+    }
+
+    override fun onPause() {
+        super.onPause()
+        active = false
+        trashbinListAdapter?.cancelAllPendingTasks()
+    }
+
+    override fun close() {
+        trashbinPresenter?.navigateUp()
+    }
+
+    override fun setDrawerIndicatorEnabled(bool: Boolean) {
+        mDrawerToggle.isDrawerIndicatorEnabled = bool
+    }
+
+    override fun onSortingOrderChosen(sortOrder: FileSortOrder?) {
+        val sortButton = findViewById<TextView>(R.id.sort_button)
+        sortButton.setText(DisplayUtils.getSortOrderStringId(sortOrder))
+        trashbinListAdapter?.setSortOrder(sortOrder)
+    }
+
+    override fun showTrashbinFolder(trashbinFiles: List<TrashbinFile?>?) {
+        if (active) {
+            trashbinListAdapter?.setTrashbinFiles(trashbinFiles, true)
+            binding.swipeContainingList.isRefreshing = false
+            binding.loadingContent.visibility = View.GONE
+            binding.emptyList.emptyListIcon.setImageResource(R.drawable.ic_delete)
+            binding.emptyList.emptyListViewHeadline.text = getString(R.string.trashbin_empty_headline)
+            binding.emptyList.emptyListViewText.text = getString(R.string.trashbin_empty_message)
+            binding.list.visibility = View.VISIBLE
+        }
+    }
+
+    override fun removeFile(file: TrashbinFile?) {
+        if (active) {
+            trashbinListAdapter?.removeFile(file)
+        }
+    }
+
+    override fun removeAllFiles() {
+        trashbinListAdapter?.removeAllFiles()
+    }
+
+    override fun showSnackbarError(message: Int, file: TrashbinFile?) {
+        if (active) {
+            binding.swipeContainingList.isRefreshing = false
+            Snackbar.make(binding.list, String.format(getString(message), file?.fileName), Snackbar.LENGTH_LONG)
+                .show()
+        }
+    }
+
+    @VisibleForTesting
+    fun showInitialLoading() {
+        binding.emptyList.emptyListView.visibility = View.GONE
+        binding.list.visibility = View.GONE
+        binding.loadingContent.visibility = View.VISIBLE
+    }
+
+    @VisibleForTesting
+    fun showUser() {
+        binding.loadingContent.visibility = View.GONE
+        binding.list.visibility = View.VISIBLE
+        binding.swipeContainingList.isRefreshing = false
+        binding.emptyList.emptyListViewText.text = user.get().accountName
+        binding.emptyList.emptyListViewText.visibility = View.VISIBLE
+        binding.emptyList.emptyListView.visibility = View.VISIBLE
+    }
+
+    override fun showError(message: Int) {
+        if (active) {
+            trashbinListAdapter?.removeAllFiles()
+            binding.loadingContent.visibility = View.GONE
+            binding.list.visibility = View.VISIBLE
+            binding.swipeContainingList.isRefreshing = false
+            binding.emptyList.emptyListViewHeadline.setText(R.string.common_error)
+            binding.emptyList.emptyListIcon.setImageDrawable(
+                ResourcesCompat.getDrawable(
+                    resources,
+                    R.drawable.ic_list_empty_error,
+                    null
+                )
+            )
+            binding.emptyList.emptyListViewText.setText(message)
+            binding.emptyList.emptyListViewText.visibility = View.VISIBLE
+            binding.emptyList.emptyListIcon.visibility = View.VISIBLE
+            binding.emptyList.emptyListView.visibility = View.VISIBLE
+        }
+    }
+
+    companion object {
+        const val EMPTY_LIST_COUNT = 1
+    }
+}

+ 17 - 33
app/src/main/java/com/owncloud/android/ui/trashbin/TrashbinContract.java → app/src/main/java/com/owncloud/android/ui/trashbin/TrashbinContract.kt

@@ -18,47 +18,31 @@
  * 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.trashbin;
+package com.owncloud.android.ui.trashbin
 
-import com.owncloud.android.lib.resources.trashbin.model.TrashbinFile;
-
-import java.util.List;
+import com.owncloud.android.lib.resources.trashbin.model.TrashbinFile
 
 /**
  * Contract between view (TrashbinActivity) and presenter (TrashbinPresenter)
  */
-public interface TrashbinContract {
-
+interface TrashbinContract {
     interface View {
-        void showTrashbinFolder(List<TrashbinFile> trashbinFiles);
-
-        void showSnackbarError(int message, TrashbinFile file);
-
-        void showError(int message);
-
-        void removeFile(TrashbinFile file);
-
-        void removeAllFiles();
-
-        void close();
-
-        void setDrawerIndicatorEnabled(boolean bool);
+        fun showTrashbinFolder(trashbinFiles: List<TrashbinFile?>?)
+        fun showSnackbarError(message: Int, file: TrashbinFile?)
+        fun showError(message: Int)
+        fun removeFile(file: TrashbinFile?)
+        fun removeAllFiles()
+        fun close()
+        fun setDrawerIndicatorEnabled(bool: Boolean)
     }
 
     interface Presenter {
-
-        boolean isRoot();
-
-        void loadFolder();
-
-        void navigateUp();
-
-        void enterFolder(String folder);
-
-        void restoreTrashbinFile(TrashbinFile file);
-
-        void removeTrashbinFile(TrashbinFile file);
-
-        void emptyTrashbin();
+        val isRoot: Boolean
+        fun loadFolder()
+        fun navigateUp()
+        fun enterFolder(folder: String?)
+        fun restoreTrashbinFile(file: TrashbinFile?)
+        fun removeTrashbinFile(file: TrashbinFile?)
+        fun emptyTrashbin()
     }
 }

+ 0 - 116
app/src/main/java/com/owncloud/android/ui/trashbin/TrashbinPresenter.java

@@ -1,116 +0,0 @@
-/*
- * Nextcloud Android client application
- *
- * @author Tobias Kaminsky
- * Copyright (C) 2018 Tobias Kaminsky
- * Copyright (C) 2018 Nextcloud GmbH.
- *
- * 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.trashbin;
-
-import com.owncloud.android.R;
-import com.owncloud.android.lib.resources.trashbin.model.TrashbinFile;
-
-import java.io.File;
-import java.util.List;
-
-import static com.owncloud.android.datamodel.OCFile.ROOT_PATH;
-
-/**
- * Coordinates between model and view: querying model, updating view, react to UI input
- */
-public class TrashbinPresenter implements TrashbinContract.Presenter {
-
-    private TrashbinContract.View trashbinView;
-    private TrashbinRepository trashbinRepository;
-    private String currentPath = ROOT_PATH;
-
-    public TrashbinPresenter(TrashbinRepository trashbinRepository, TrashbinContract.View trashbinView) {
-        this.trashbinRepository = trashbinRepository;
-        this.trashbinView = trashbinView;
-    }
-
-    @Override
-    public void enterFolder(String folder) {
-        currentPath = folder;
-        loadFolder();
-    }
-
-    @Override
-    public boolean isRoot() {
-        return !ROOT_PATH.equals(currentPath);
-    }
-
-    @Override
-    public void navigateUp() {
-        if (ROOT_PATH.equals(currentPath)) {
-            trashbinView.close();
-        } else {
-            currentPath = new File(currentPath).getParent();
-
-            loadFolder();
-        }
-
-        trashbinView.setDrawerIndicatorEnabled(ROOT_PATH.equals(currentPath));
-    }
-
-    @Override
-    public void loadFolder() {
-        trashbinRepository.getFolder(currentPath, new TrashbinRepository.LoadFolderCallback() {
-            @Override
-            public void onSuccess(List<TrashbinFile> files) {
-                trashbinView.showTrashbinFolder(files);
-            }
-
-            @Override
-            public void onError(int error) {
-                trashbinView.showError(error);
-            }
-        });
-    }
-
-    @Override
-    public void restoreTrashbinFile(TrashbinFile file) {
-        trashbinRepository.restoreFile(file, success -> {
-            if (success) {
-                trashbinView.removeFile(file);
-            } else {
-                trashbinView.showSnackbarError(R.string.trashbin_file_not_restored, file);
-            }
-        });
-    }
-
-    @Override
-    public void removeTrashbinFile(TrashbinFile file) {
-        trashbinRepository.removeTrashbinFile(file, success -> {
-            if (success) {
-                trashbinView.removeFile(file);
-            } else {
-                trashbinView.showSnackbarError(R.string.trashbin_file_not_deleted, file);
-            }
-        });
-    }
-
-    @Override
-    public void emptyTrashbin() {
-        trashbinRepository.emptyTrashbin(success -> {
-            if (success) {
-                trashbinView.removeAllFiles();
-            } else {
-                trashbinView.showError(R.string.trashbin_not_emptied);
-            }
-        });
-    }
-}

+ 117 - 0
app/src/main/java/com/owncloud/android/ui/trashbin/TrashbinPresenter.kt

@@ -0,0 +1,117 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2018 Tobias Kaminsky
+ * Copyright (C) 2018 Nextcloud GmbH.
+ *
+ * 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.trashbin
+
+import com.owncloud.android.R
+import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.lib.resources.trashbin.model.TrashbinFile
+import com.owncloud.android.ui.trashbin.TrashbinContract.Presenter
+import com.owncloud.android.ui.trashbin.TrashbinRepository.LoadFolderCallback
+import java.io.File
+
+/**
+ * Coordinates between model and view: querying model, updating view, react to UI input
+ */
+class TrashbinPresenter(
+    private val trashbinRepository: TrashbinRepository,
+    private val trashbinView: TrashbinContract.View
+) : Presenter {
+
+    private var currentPath: String? = OCFile.ROOT_PATH
+
+    override fun enterFolder(folder: String?) {
+        currentPath = folder
+        loadFolder()
+    }
+
+    override val isRoot: Boolean
+        get() = OCFile.ROOT_PATH != currentPath
+
+    override fun navigateUp() {
+        if (OCFile.ROOT_PATH == currentPath) {
+            trashbinView.close()
+        } else {
+            currentPath?.let {
+                currentPath = File(it).parent
+                loadFolder()
+            }
+        }
+
+        trashbinView.setDrawerIndicatorEnabled(OCFile.ROOT_PATH == currentPath)
+    }
+
+    override fun loadFolder() {
+        trashbinRepository.getFolder(
+            currentPath,
+            object : LoadFolderCallback {
+                override fun onSuccess(files: List<TrashbinFile?>?) {
+                    trashbinView.showTrashbinFolder(files)
+                }
+
+                override fun onError(error: Int) {
+                    trashbinView.showError(error)
+                }
+            }
+        )
+    }
+
+    override fun restoreTrashbinFile(file: TrashbinFile?) {
+        trashbinRepository.restoreFile(
+            file,
+            object : TrashbinRepository.OperationCallback {
+                override fun onResult(success: Boolean) {
+                    if (success) {
+                        trashbinView.removeFile(file)
+                    } else {
+                        trashbinView.showSnackbarError(R.string.trashbin_file_not_restored, file)
+                    }
+                }
+            }
+        )
+    }
+
+    override fun removeTrashbinFile(file: TrashbinFile?) {
+        trashbinRepository.removeTrashbinFile(
+            file,
+            object : TrashbinRepository.OperationCallback {
+                override fun onResult(success: Boolean) {
+                    if (success) {
+                        trashbinView.removeFile(file)
+                    } else {
+                        trashbinView.showSnackbarError(R.string.trashbin_file_not_deleted, file)
+                    }
+                }
+            }
+        )
+    }
+
+    override fun emptyTrashbin() {
+        trashbinRepository.emptyTrashbin(object : TrashbinRepository.OperationCallback {
+            override fun onResult(success: Boolean) {
+                if (success) {
+                    trashbinView.removeAllFiles()
+                } else {
+                    trashbinView.showError(R.string.trashbin_not_emptied)
+                }
+            }
+        })
+    }
+}

+ 10 - 16
app/src/main/java/com/owncloud/android/ui/trashbin/TrashbinRepository.java → app/src/main/java/com/owncloud/android/ui/trashbin/TrashbinRepository.kt

@@ -18,31 +18,25 @@
  * 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.trashbin;
+package com.owncloud.android.ui.trashbin
 
-import com.owncloud.android.lib.resources.trashbin.model.TrashbinFile;
-
-import java.util.List;
+import com.owncloud.android.lib.resources.trashbin.model.TrashbinFile
 
 /**
  * Contract between presenter and model
  */
-public interface TrashbinRepository {
+interface TrashbinRepository {
     interface LoadFolderCallback {
-        void onSuccess(List<TrashbinFile> files);
-
-        void onError(int error);
+        fun onSuccess(files: List<TrashbinFile?>?)
+        fun onError(error: Int)
     }
 
     interface OperationCallback {
-        void onResult(boolean success);
+        fun onResult(success: Boolean)
     }
 
-    void getFolder(String remotePath, LoadFolderCallback callback);
-
-    void restoreFile(TrashbinFile file, OperationCallback callback);
-
-    void emptyTrashbin(OperationCallback callback);
-
-    void removeTrashbinFile(TrashbinFile file, OperationCallback callback);
+    fun getFolder(remotePath: String?, callback: LoadFolderCallback?)
+    fun restoreFile(file: TrashbinFile?, callback: OperationCallback?)
+    fun emptyTrashbin(callback: OperationCallback?)
+    fun removeTrashbinFile(file: TrashbinFile?, callback: OperationCallback?)
 }

+ 19 - 32
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"
@@ -69,41 +69,26 @@
                     android:layout_height="wrap_content"
                     android:gravity="center_horizontal">
 
-                    <com.google.android.material.textfield.TextInputEditText
+                    <com.owncloud.android.ui.components.PassCodeEditText
                         android:id="@+id/txt0"
                         style="@style/PassCodeStyle"
-                        android:cursorVisible="false"
-                        android:focusable="true"
-                        android:hint="@string/hidden_character"
-                        android:imeOptions="flagNoExtractUi"
                         android:importantForAutofill="no"
                         tools:text="123">
+                    </com.owncloud.android.ui.components.PassCodeEditText>
 
-                        <requestFocus />
-                    </com.google.android.material.textfield.TextInputEditText>
-
-                    <com.google.android.material.textfield.TextInputEditText
+                    <com.owncloud.android.ui.components.PassCodeEditText
                         android:id="@+id/txt1"
                         style="@style/PassCodeStyle"
-                        android:cursorVisible="false"
-                        android:hint="@string/hidden_character"
-                        android:imeOptions="flagNoExtractUi"
                         android:importantForAutofill="no" />
 
-                    <com.google.android.material.textfield.TextInputEditText
+                    <com.owncloud.android.ui.components.PassCodeEditText
                         android:id="@+id/txt2"
                         style="@style/PassCodeStyle"
-                        android:cursorVisible="false"
-                        android:hint="@string/hidden_character"
-                        android:imeOptions="flagNoExtractUi"
                         android:importantForAutofill="no" />
 
-                    <com.google.android.material.textfield.TextInputEditText
+                    <com.owncloud.android.ui.components.PassCodeEditText
                         android:id="@+id/txt3"
                         style="@style/PassCodeStyle"
-                        android:cursorVisible="false"
-                        android:hint="@string/hidden_character"
-                        android:imeOptions="flagNoExtractUi"
                         android:importantForAutofill="no" />
                 </LinearLayout>
 
@@ -111,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>

+ 6 - 6
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>
@@ -363,7 +363,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 +598,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 +935,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>

+ 4 - 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>
@@ -324,6 +325,7 @@
     <string name="file_delete">Törlés</string>
     <string name="file_detail_activity_error">Hiba a fájl tevékenységeinek lekérésekor</string>
     <string name="file_details_no_content">A részletek betöltése sikertelen</string>
+    <string name="file_downloader_notification_title_prefix">Letöltés \u0020</string>
     <string name="file_icon">Fájl</string>
     <string name="file_keep">Megtartás</string>
     <string name="file_list_empty">Töltsön fel új tartalmat vagy szinkronizáljon az eszközeivel</string>
@@ -962,7 +964,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>

+ 10 - 4
app/src/main/res/values-ja-rJP/strings.xml

@@ -14,10 +14,11 @@
     <string name="action_send_share">送信/共有</string>
     <string name="action_switch_grid_view">グリッド表示</string>
     <string name="action_switch_list_view">リスト表示</string>
-    <string name="actionbar_calendar_contacts_restore">連絡先とカレンダーを復元する</string>
+    <string name="actionbar_calendar_contacts_restore">連絡先とカレンダーを復元</string>
     <string name="actionbar_copy">コピー</string>
     <string name="actionbar_mkdir">新しいフォルダー</string>
     <string name="actionbar_move">移動</string>
+    <string name="actionbar_move_or_copy">移動またはコピー</string>
     <string name="actionbar_open_with">次で開く</string>
     <string name="actionbar_search">検索</string>
     <string name="actionbar_see_details">詳細</string>
@@ -30,12 +31,12 @@
     <string name="activity_chooser_send_file_title">送信</string>
     <string name="activity_chooser_title">リンク送信…</string>
     <string name="activity_icon">アクティビティ</string>
-    <string name="add_another_public_share_link">別のリンクを作成</string>
+    <string name="add_another_public_share_link">別のリンクを追加</string>
     <string name="add_new_public_share">新規公開共有リンクを追加</string>
     <string name="add_new_secure_file_drop">新しいセキュアなファイルドロップを追加</string>
     <string name="add_to_cloud">%1$s に追加</string>
     <string name="advanced_settings">高度な設定</string>
-    <string name="allow_resharing">再共有を許可</string>
+    <string name="allow_resharing">再共有を許可する</string>
     <string name="app_widget_description">ダッシュボードから一つのウィジェットを表示</string>
     <string name="appbar_search_in">%s の中を検索</string>
     <string name="associated_account_not_found">関連付けられたアカウントが見つかりません!</string>
@@ -45,7 +46,7 @@
     <string name="auth_account_not_the_same">入力されたユーザーはこのアカウントのユーザーと一致しません</string>
     <string name="auth_bad_oc_version_title">認識できないサーバーのバージョンです</string>
     <string name="auth_connection_established">接続が確立しました</string>
-    <string name="auth_fail_get_user_name">サーバーが正しいユーザーIDを返していない場合、管理者に連絡してください。</string>
+    <string name="auth_fail_get_user_name">サーバーが正しいユーザーIDを返していません。管理者に連絡してください。</string>
     <string name="auth_host_url">サーバーアドレス https://…</string>
     <string name="auth_incorrect_address_title">サーバーアドレスの書式が違います</string>
     <string name="auth_incorrect_path_title">サーバーが見つかりません</string>
@@ -550,6 +551,7 @@
     <string name="permission_storage_access">ファイルをダウンロードとアップロードする追加の権限が必要です。</string>
     <string name="picture_set_as_no_app">画像を設定するアプリが見つかりませんでした</string>
     <string name="pin_home">ホームスクリーンにピン留めする</string>
+    <string name="pin_shortcut_label">%1$sを開く</string>
     <string name="placeholder_fileSize">389 KB</string>
     <string name="placeholder_filename">placeholder.txt</string>
     <string name="placeholder_media_time">12:23:45</string>
@@ -591,6 +593,7 @@
     <string name="prefs_instant_behaviour_title">元のファイルになります…</string>
     <string name="prefs_instant_upload_path_use_date_subfolders_summary">日付を基にしたサブフォルダーに保存</string>
     <string name="prefs_instant_upload_path_use_subfolders_title">サブフォルダーを利用</string>
+    <string name="prefs_instant_upload_subfolder_rule_title">サブフォルダーのオプション</string>
     <string name="prefs_keys_exist">このクライアントに End-to-End 暗号化を追加</string>
     <string name="prefs_license">ライセンス</string>
     <string name="prefs_lock">アプリパスコード</string>
@@ -603,6 +606,7 @@
     <string name="prefs_manage_accounts">アカウント管理</string>
     <string name="prefs_recommend">友達にすすめる</string>
     <string name="prefs_setup_e2e">end-to-end 暗号化を設定</string>
+    <string name="prefs_show_ecosystem_apps">アップスイッチャーを表示</string>
     <string name="prefs_show_hidden_files">隠しファイルを表示</string>
     <string name="prefs_sourcecode">ソースコードを入手</string>
     <string name="prefs_storage_path">データ保存フォルダー</string>
@@ -925,7 +929,9 @@
     <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_message">ログインするにはAndroidシステムのWebViewを更新してください</string>
     <string name="webview_version_check_alert_dialog_positive_button_title">更新</string>
+    <string name="webview_version_check_alert_dialog_title">AndroidシステムのWebViewをアップデート</string>
     <string name="what_s_new_image">新しいイメージとは</string>
     <string name="whats_new_skip">スキップ</string>
     <string name="whats_new_title">%1$sの新機能</string>

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

@@ -18,6 +18,7 @@
     <string name="actionbar_copy">Копіювати</string>
     <string name="actionbar_mkdir">Новий каталог</string>
     <string name="actionbar_move">Перемістити</string>
+    <string name="actionbar_move_or_copy">Перемістити або копіювати</string>
     <string name="actionbar_open_with">Відкрити за допомогою</string>
     <string name="actionbar_search">Пошук</string>
     <string name="actionbar_see_details">Деталі</string>
@@ -322,6 +323,7 @@
     <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>
@@ -574,6 +576,7 @@
     <string name="prefs_calendar_contacts_sync_setup_successful">Налаштування синхронізації календаря та контактів</string>
     <string name="prefs_category_about">Про програму</string>
     <string name="prefs_category_details">Деталі</string>
+    <string name="prefs_category_dev">Розробка</string>
     <string name="prefs_category_general">Основне</string>
     <string name="prefs_category_more">Більше</string>
     <string name="prefs_daily_backup_summary">Щоденне створення резервних копій календарів та контактів</string>
@@ -934,7 +937,9 @@
     <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_message">Оновіть застосунок Android System WebView для отримання доступу до входу</string>
     <string name="webview_version_check_alert_dialog_positive_button_title">Оновлення</string>
+    <string name="webview_version_check_alert_dialog_title">Оновити Android System WebView</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/styles.xml

@@ -324,9 +324,11 @@
     </style>
 
     <style name="PassCodeStyle">
+        <item name="android:hint">@string/hidden_character</item>
         <item name="android:layout_width">50dp</item>
         <item name="android:layout_height">50dp</item>
         <item name="android:gravity">center</item>
+        <item name="android:cursorVisible">false</item>
         <item name="android:layout_margin">10dp</item>
         <item name="android:inputType">numberDecimal</item>
         <item name="android:numeric">decimal</item>

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

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