瀏覽代碼

Merge master

Signed-off-by: alperozturk <alper_ozturk@proton.me>
alperozturk 1 年之前
父節點
當前提交
95931f229d
共有 100 個文件被更改,包括 3143 次插入3440 次删除
  1. 2 6
      .github/workflows/codeql.yml
  2. 1 1
      .github/workflows/scorecard.yml
  3. 二進制
      app/screenshots/gplay/debug/com.nextcloud.client.SyncedFoldersActivityIT_testSyncedFolderDialog.png
  4. 二進制
      app/screenshots/gplay/debug/com.owncloud.android.ui.activity.FolderPickerActivityIT_open.png
  5. 二進制
      app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testEnforcedPasswordDialog.png
  6. 二進制
      app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testFileActionsBottomSheet.png
  7. 二進制
      app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testOptionalPasswordDialog.png
  8. 二進制
      app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFileDialog.png
  9. 二進制
      app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFilesDialog.png
  10. 二進制
      app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFolderDialog.png
  11. 二進制
      app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFoldersDialog.png
  12. 18 0
      app/src/androidTest/java/com/owncloud/android/AbstractIT.java
  13. 1 1
      app/src/androidTest/java/com/owncloud/android/ui/activity/FolderPickerActivityIT.java
  14. 36 0
      app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.java
  15. 1 0
      app/src/androidTest/java/com/owncloud/android/ui/dialog/SyncFileNotEnoughSpaceDialogFragmentTest.java
  16. 15 19
      app/src/main/java/com/nextcloud/client/di/FragmentInjector.kt
  17. 13 17
      app/src/main/java/com/nextcloud/client/integrations/deck/DeckApi.kt
  18. 0 96
      app/src/main/java/com/nextcloud/client/integrations/deck/DeckApiImpl.java
  19. 85 0
      app/src/main/java/com/nextcloud/client/integrations/deck/DeckApiImpl.kt
  20. 1 1
      app/src/main/java/com/nextcloud/client/jobs/NotificationWork.kt
  21. 10 21
      app/src/main/java/com/nextcloud/ui/SquareLoaderImageView.kt
  22. 2 4
      app/src/main/java/com/nextcloud/ui/fileactions/FileAction.kt
  23. 4 0
      app/src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java
  24. 7 6
      app/src/main/java/com/owncloud/android/datamodel/OCFile.java
  25. 3 11
      app/src/main/java/com/owncloud/android/files/FileMenuFilter.java
  26. 0 741
      app/src/main/java/com/owncloud/android/files/services/FileDownloader.java
  27. 750 0
      app/src/main/java/com/owncloud/android/files/services/FileDownloader.kt
  28. 4 52
      app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java
  29. 54 45
      app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt
  30. 10 2
      app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt
  31. 1 3
      app/src/main/java/com/owncloud/android/ui/activity/UploadFilesActivity.java
  32. 0 89
      app/src/main/java/com/owncloud/android/ui/adapter/StoragePathAdapter.java
  33. 80 0
      app/src/main/java/com/owncloud/android/ui/adapter/StoragePathAdapter.kt
  34. 0 173
      app/src/main/java/com/owncloud/android/ui/dialog/ConfirmationDialogFragment.java
  35. 156 0
      app/src/main/java/com/owncloud/android/ui/dialog/ConfirmationDialogFragment.kt
  36. 0 105
      app/src/main/java/com/owncloud/android/ui/dialog/IndeterminateProgressDialog.java
  37. 92 0
      app/src/main/java/com/owncloud/android/ui/dialog/IndeterminateProgressDialog.kt
  38. 0 88
      app/src/main/java/com/owncloud/android/ui/dialog/LoadingDialog.java
  39. 79 0
      app/src/main/java/com/owncloud/android/ui/dialog/LoadingDialog.kt
  40. 0 169
      app/src/main/java/com/owncloud/android/ui/dialog/LocalStoragePathPickerDialogFragment.java
  41. 148 0
      app/src/main/java/com/owncloud/android/ui/dialog/LocalStoragePathPickerDialogFragment.kt
  42. 0 131
      app/src/main/java/com/owncloud/android/ui/dialog/MultipleAccountsDialog.java
  43. 108 0
      app/src/main/java/com/owncloud/android/ui/dialog/MultipleAccountsDialog.kt
  44. 0 227
      app/src/main/java/com/owncloud/android/ui/dialog/SharePasswordDialogFragment.java
  45. 250 0
      app/src/main/java/com/owncloud/android/ui/dialog/SharePasswordDialogFragment.kt
  46. 0 189
      app/src/main/java/com/owncloud/android/ui/dialog/SortingOrderDialogFragment.java
  47. 134 0
      app/src/main/java/com/owncloud/android/ui/dialog/SortingOrderDialogFragment.kt
  48. 0 140
      app/src/main/java/com/owncloud/android/ui/dialog/SyncFileNotEnoughSpaceDialogFragment.java
  49. 119 0
      app/src/main/java/com/owncloud/android/ui/dialog/SyncFileNotEnoughSpaceDialogFragment.kt
  50. 0 655
      app/src/main/java/com/owncloud/android/ui/dialog/SyncedFolderPreferencesDialogFragment.java
  51. 566 0
      app/src/main/java/com/owncloud/android/ui/dialog/SyncedFolderPreferencesDialogFragment.kt
  52. 3 4
      app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java
  53. 34 89
      app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java
  54. 12 5
      app/src/main/java/com/owncloud/android/ui/fragment/UnifiedSearchFragment.kt
  55. 1 22
      app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java
  56. 2 2
      app/src/main/java/com/owncloud/android/ui/preview/FileDownloadFragment.java
  57. 1 2
      app/src/main/java/com/owncloud/android/ui/preview/PreviewImageFragment.java
  58. 1 2
      app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaFragment.java
  59. 1 2
      app/src/main/java/com/owncloud/android/ui/preview/PreviewTextFileFragment.java
  60. 110 0
      app/src/main/java/com/owncloud/android/utils/WebViewUtil.kt
  61. 0 10
      app/src/main/res/drawable/ic_move.xml
  62. 22 10
      app/src/main/res/layout/files_folder_picker.xml
  63. 4 3
      app/src/main/res/layout/loading_dialog.xml
  64. 70 238
      app/src/main/res/layout/sorting_order_fragment.xml
  65. 10 34
      app/src/main/res/layout/storage_path_item.xml
  66. 26 24
      app/src/main/res/layout/synced_folders_settings_layout.xml
  67. 1 0
      app/src/main/res/values-es-rCR/strings.xml
  68. 1 0
      app/src/main/res/values-es-rDO/strings.xml
  69. 2 0
      app/src/main/res/values-es-rEC/strings.xml
  70. 1 0
      app/src/main/res/values-es-rGT/strings.xml
  71. 2 0
      app/src/main/res/values-es-rMX/strings.xml
  72. 1 0
      app/src/main/res/values-es-rSV/strings.xml
  73. 1 1
      app/src/main/res/values-es/strings.xml
  74. 3 0
      app/src/main/res/values-et-rEE/strings.xml
  75. 2 0
      app/src/main/res/values-eu/strings.xml
  76. 2 0
      app/src/main/res/values-fa/strings.xml
  77. 2 0
      app/src/main/res/values-fi-rFI/strings.xml
  78. 3 0
      app/src/main/res/values-fr/strings.xml
  79. 2 0
      app/src/main/res/values-gd/strings.xml
  80. 3 0
      app/src/main/res/values-gl/strings.xml
  81. 4 0
      app/src/main/res/values-hr/strings.xml
  82. 4 0
      app/src/main/res/values-hu-rHU/strings.xml
  83. 3 0
      app/src/main/res/values-in/strings.xml
  84. 3 0
      app/src/main/res/values-is/strings.xml
  85. 4 0
      app/src/main/res/values-it/strings.xml
  86. 4 0
      app/src/main/res/values-iw/strings.xml
  87. 4 0
      app/src/main/res/values-ja-rJP/strings.xml
  88. 2 0
      app/src/main/res/values-ka-rGE/strings.xml
  89. 3 0
      app/src/main/res/values-ko/strings.xml
  90. 2 0
      app/src/main/res/values-lo/strings.xml
  91. 4 0
      app/src/main/res/values-lt-rLT/strings.xml
  92. 2 0
      app/src/main/res/values-lv/strings.xml
  93. 3 0
      app/src/main/res/values-mk/strings.xml
  94. 4 0
      app/src/main/res/values-nb-rNO/strings.xml
  95. 4 0
      app/src/main/res/values-nl/strings.xml
  96. 4 0
      app/src/main/res/values-pl/strings.xml
  97. 4 0
      app/src/main/res/values-pt-rBR/strings.xml
  98. 4 0
      app/src/main/res/values-pt-rPT/strings.xml
  99. 4 0
      app/src/main/res/values-ro/strings.xml
  100. 4 0
      app/src/main/res/values-ru/strings.xml

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

@@ -12,10 +12,6 @@ on:
 permissions:
   contents: read
 
-concurrency:
-    group: code-ql-${{ github.head_ref || github.run_id }}
-    cancel-in-progress: true
-
 jobs:
   analyze:
     name: Analyze
@@ -36,7 +32,7 @@ jobs:
         with:
           swap-size-gb: 10
       - name: Initialize CodeQL
-        uses: github/codeql-action/init@49abf0ba24d0b7953cb586944e918a0b92074c80 # v2.22.4
+        uses: github/codeql-action/init@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5
         with:
           languages: ${{ matrix.language }}
       - name: Set up JDK 17
@@ -50,4 +46,4 @@ jobs:
           echo "org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError" > "$HOME/.gradle/gradle.properties"
           ./gradlew assembleDebug
       - name: Perform CodeQL Analysis
-        uses: github/codeql-action/analyze@49abf0ba24d0b7953cb586944e918a0b92074c80 # v2.22.4
+        uses: github/codeql-action/analyze@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5

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

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

二進制
app/screenshots/gplay/debug/com.nextcloud.client.SyncedFoldersActivityIT_testSyncedFolderDialog.png


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


二進制
app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testEnforcedPasswordDialog.png


二進制
app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testFileActionsBottomSheet.png


二進制
app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testOptionalPasswordDialog.png


二進制
app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFileDialog.png


二進制
app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFilesDialog.png


二進制
app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFolderDialog.png


二進制
app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFoldersDialog.png


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

@@ -7,6 +7,8 @@ import android.accounts.OperationCanceledException;
 import android.app.Activity;
 import android.content.ActivityNotFoundException;
 import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
 import android.os.Build;
 import android.os.Bundle;
 import android.text.TextUtils;
@@ -58,6 +60,7 @@ import java.io.FileWriter;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.Collection;
+import java.util.Locale;
 import java.util.Objects;
 
 import androidx.annotation.NonNull;
@@ -403,6 +406,21 @@ public abstract class AbstractIT {
         assertTrue(result.getLogMessage(), result.isSuccess());
     }
 
+    protected void enableRTL() {
+        Locale locale = new Locale("ar");
+        Resources resources = InstrumentationRegistry.getInstrumentation().getTargetContext().getResources();
+        Configuration config = resources.getConfiguration();
+        config.setLocale(locale);
+        resources.updateConfiguration(config, null);
+    }
+
+    protected void resetLocale() {
+        Resources resources = InstrumentationRegistry.getInstrumentation().getTargetContext().getResources();
+        Configuration defaultConfig = resources.getConfiguration();
+        defaultConfig.setLocale(Locale.getDefault());
+        resources.updateConfiguration(defaultConfig, null);
+    }
+
     protected void screenshot(View view) {
         screenshot(view, "");
     }

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

@@ -130,7 +130,7 @@ public class FolderPickerActivityIT extends AbstractIT {
         sut.setFile(origin);
 
         sut.runOnUiThread(() -> {
-            sut.findViewById(R.id.folder_picker_btn_choose).requestFocus();
+            sut.findViewById(R.id.folder_picker_btn_copy).requestFocus();
         });
         waitForIdleSync();
         screenshot(sut);

+ 36 - 0
app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.java

@@ -92,6 +92,7 @@ import androidx.activity.result.contract.ActivityResultContract;
 import androidx.annotation.NonNull;
 import androidx.fragment.app.DialogFragment;
 import androidx.test.espresso.intent.rule.IntentsTestRule;
+import androidx.test.rule.GrantPermissionRule;
 import kotlin.Unit;
 
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
@@ -108,6 +109,9 @@ public class DialogFragmentIT extends AbstractIT {
         return activityRule.launchActivity(intent);
     }
 
+    @Rule
+    public GrantPermissionRule permissionRule = GrantPermissionRule.grant(
+        android.Manifest.permission.POST_NOTIFICATIONS);
 
     @After
     public void quitLooperIfNeeded() {
@@ -134,6 +138,38 @@ public class DialogFragmentIT extends AbstractIT {
         showDialog(dialog);
     }
 
+    @Test
+    @ScreenshotTest
+    public void testConfirmationDialogWithOneAction() {
+        ConfirmationDialogFragment dialog = ConfirmationDialogFragment.newInstance(R.string.upload_list_empty_text_auto_upload, new String[]{}, R.string.filedetails_sync_file, R.string.common_ok, -1, -1);
+        showDialog(dialog);
+    }
+
+    @Test
+    @ScreenshotTest
+    public void testConfirmationDialogWithTwoAction() {
+        ConfirmationDialogFragment dialog = ConfirmationDialogFragment.newInstance(R.string.upload_list_empty_text_auto_upload, new String[]{}, R.string.filedetails_sync_file, R.string.common_ok, R.string.common_cancel, -1);
+        showDialog(dialog);
+    }
+
+    @Test
+    @ScreenshotTest
+    public void testConfirmationDialogWithThreeAction() {
+        ConfirmationDialogFragment dialog = ConfirmationDialogFragment.newInstance(R.string.upload_list_empty_text_auto_upload, new String[]{}, R.string.filedetails_sync_file, R.string.common_ok, R.string.common_cancel, R.string.common_confirm);
+        showDialog(dialog);
+    }
+
+    @Test
+    @ScreenshotTest
+    public void testConfirmationDialogWithThreeActionRTL() {
+        enableRTL();
+
+        ConfirmationDialogFragment dialog = ConfirmationDialogFragment.newInstance(R.string.upload_list_empty_text_auto_upload, new String[] { }, -1, R.string.common_ok, R.string.common_cancel, R.string.common_confirm);
+        showDialog(dialog);
+
+        resetLocale();
+    }
+
     @Test
     @ScreenshotTest
     public void testRemoveFileDialog() {

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

@@ -45,6 +45,7 @@ public class SyncFileNotEnoughSpaceDialogFragmentTest extends AbstractIT {
         FileDisplayActivity test = activityRule.launchActivity(null);
         OCFile ocFile = new OCFile("/Document/");
         ocFile.setFileLength(5000000);
+        ocFile.setFolder();
 
         SyncFileNotEnoughSpaceDialogFragment dialog = SyncFileNotEnoughSpaceDialogFragment.newInstance(ocFile, 1000);
         dialog.show(test.getListOfFilesFragment().getFragmentManager(), "1");

+ 15 - 19
app/src/main/java/com/nextcloud/client/di/FragmentInjector.java → app/src/main/java/com/nextcloud/client/di/FragmentInjector.kt

@@ -17,30 +17,26 @@
  * 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.di
 
-package com.nextcloud.client.di;
+import android.content.Context
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentManager
+import dagger.android.support.AndroidSupportInjection
 
-import android.content.Context;
-
-import androidx.annotation.NonNull;
-import androidx.fragment.app.Fragment;
-import androidx.fragment.app.FragmentManager;
-import dagger.android.support.AndroidSupportInjection;
-
-class FragmentInjector extends FragmentManager.FragmentLifecycleCallbacks {
-    @Override
-    public void onFragmentPreAttached(
-        @NonNull FragmentManager fragmentManager,
-        @NonNull Fragment fragment,
-        @NonNull Context context
+internal class FragmentInjector : FragmentManager.FragmentLifecycleCallbacks() {
+    override fun onFragmentPreAttached(
+        fragmentManager: FragmentManager,
+        fragment: Fragment,
+        context: Context
     ) {
-        super.onFragmentPreAttached(fragmentManager, fragment, context);
-        if (fragment instanceof Injectable) {
+        super.onFragmentPreAttached(fragmentManager, fragment, context)
+        if (fragment is Injectable) {
             try {
-                AndroidSupportInjection.inject(fragment);
-            } catch (IllegalArgumentException directCause) {
+                AndroidSupportInjection.inject(fragment)
+            } catch (directCause: IllegalArgumentException) {
                 // this provides a cause description that is a bit more friendly for developers
-                throw new InjectorNotFoundException(fragment, directCause);
+                throw InjectorNotFoundException(fragment, directCause)
             }
         }
     }

+ 13 - 17
app/src/main/java/com/nextcloud/client/integrations/deck/DeckApi.java → app/src/main/java/com/nextcloud/client/integrations/deck/DeckApi.kt

@@ -17,23 +17,18 @@
  * 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.nextcloud.client.integrations.deck
 
-package com.nextcloud.client.integrations.deck;
-
-import android.app.PendingIntent;
-
-import com.nextcloud.client.account.User;
-import com.nextcloud.java.util.Optional;
-import com.owncloud.android.lib.resources.notifications.models.Notification;
-
-import androidx.annotation.NonNull;
+import android.app.PendingIntent
+import com.nextcloud.client.account.User
+import com.nextcloud.java.util.Optional
+import com.owncloud.android.lib.resources.notifications.models.Notification
 
 /**
- * This API is for an integration with the <a href="https://github.com/stefan-niedermann/nextcloud-deck">Nextcloud
- * Deck</a> app for android.
+ * This API is for an integration with the [Nextcloud
+ * Deck](https://github.com/stefan-niedermann/nextcloud-deck) app for android.
  */
-public interface DeckApi {
-
+interface DeckApi {
     /**
      * Creates a PendingIntent that can be used in a NotificationBuilder to open the notification link in Deck app
      *
@@ -41,9 +36,10 @@ public interface DeckApi {
      * @param user         The user that is affected by the notification
      * @return If notification can be consumed by Deck, a PendingIntent opening notification link in Deck app; empty
      * value otherwise
-     * @see <a href="https://apps.nextcloud.com/apps/deck">Deck Server App</a>
+     * @see [Deck Server App](https://apps.nextcloud.com/apps/deck)
      */
-    @NonNull
-    Optional<PendingIntent> createForwardToDeckActionIntent(@NonNull final Notification notification,
-                                                            @NonNull final User user);
+    fun createForwardToDeckActionIntent(
+        notification: Notification,
+        user: User
+    ): Optional<PendingIntent?>
 }

+ 0 - 96
app/src/main/java/com/nextcloud/client/integrations/deck/DeckApiImpl.java

@@ -1,96 +0,0 @@
-/*
- * Nextcloud application
- *
- * @author Stefan Niedermann
- * Copyright (C) 2020 Stefan Niedermann <info@niedermann.it>
- *
- * 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 <http://www.gnu.org/licenses/>.
- */
-
-package com.nextcloud.client.integrations.deck;
-
-import android.app.PendingIntent;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-
-import com.nextcloud.client.account.User;
-import com.nextcloud.java.util.Optional;
-import com.owncloud.android.lib.resources.notifications.models.Notification;
-
-import androidx.annotation.NonNull;
-
-public class DeckApiImpl implements DeckApi {
-
-    static final String APP_NAME = "deck";
-    static final String[] DECK_APP_PACKAGES = new String[] {
-        "it.niedermann.nextcloud.deck",
-        "it.niedermann.nextcloud.deck.play",
-        "it.niedermann.nextcloud.deck.dev"
-    };
-    static final String DECK_ACTIVITY_TO_START = "it.niedermann.nextcloud.deck.ui.PushNotificationActivity";
-
-    private static final String EXTRA_ACCOUNT = "account";
-    private static final String EXTRA_LINK = "link";
-    private static final String EXTRA_OBJECT_ID = "objectId";
-    private static final String EXTRA_SUBJECT = "subject";
-    private static final String EXTRA_SUBJECT_RICH = "subjectRich";
-    private static final String EXTRA_MESSAGE = "message";
-    private static final String EXTRA_MESSAGE_RICH = "messageRich";
-    private static final String EXTRA_USER = "user";
-    private static final String EXTRA_NID = "nid";
-
-    private final Context context;
-    private final PackageManager packageManager;
-
-    public DeckApiImpl(@NonNull Context context, @NonNull PackageManager packageManager) {
-        this.context = context;
-        this.packageManager = packageManager;
-    }
-
-    @NonNull
-    @Override
-    public Optional<PendingIntent> createForwardToDeckActionIntent(@NonNull Notification notification, @NonNull User user) {
-        if (APP_NAME.equalsIgnoreCase(notification.app)) {
-            final Intent intent = new Intent();
-            for (String appPackage : DECK_APP_PACKAGES) {
-                intent.setClassName(appPackage, DECK_ACTIVITY_TO_START);
-                if (packageManager.resolveActivity(intent, 0) != null) {
-                    return Optional.of(createPendingIntent(intent, notification, user));
-                }
-            }
-        }
-        return Optional.empty();
-    }
-
-    private PendingIntent createPendingIntent(@NonNull Intent intent, @NonNull Notification notification, @NonNull User user) {
-        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        return PendingIntent.getActivity(context, notification.getNotificationId(),
-                                         putExtrasToIntent(intent, notification, user),
-                                         PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE);
-    }
-
-    private Intent putExtrasToIntent(@NonNull Intent intent, @NonNull Notification notification, @NonNull User user) {
-        return intent
-            .putExtra(EXTRA_ACCOUNT, user.getAccountName())
-            .putExtra(EXTRA_LINK, notification.getLink())
-            .putExtra(EXTRA_OBJECT_ID, notification.getObjectId())
-            .putExtra(EXTRA_SUBJECT, notification.getSubject())
-            .putExtra(EXTRA_SUBJECT_RICH, notification.getSubjectRich())
-            .putExtra(EXTRA_MESSAGE, notification.getMessage())
-            .putExtra(EXTRA_MESSAGE_RICH, notification.getMessageRich())
-            .putExtra(EXTRA_USER, notification.getUser())
-            .putExtra(EXTRA_NID, notification.getNotificationId());
-    }
-}

+ 85 - 0
app/src/main/java/com/nextcloud/client/integrations/deck/DeckApiImpl.kt

@@ -0,0 +1,85 @@
+/*
+ * Nextcloud application
+ *
+ * @author Stefan Niedermann
+ * Copyright (C) 2020 Stefan Niedermann <info@niedermann.it>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.client.integrations.deck
+
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import com.nextcloud.client.account.User
+import com.nextcloud.java.util.Optional
+import com.owncloud.android.lib.resources.notifications.models.Notification
+
+class DeckApiImpl(private val context: Context, private val packageManager: PackageManager) : DeckApi {
+    override fun createForwardToDeckActionIntent(notification: Notification, user: User): Optional<PendingIntent?> {
+        if (APP_NAME.equals(notification.app, ignoreCase = true)) {
+            val intent = Intent()
+            for (appPackage in DECK_APP_PACKAGES) {
+                intent.setClassName(appPackage, DECK_ACTIVITY_TO_START)
+                if (packageManager.resolveActivity(intent, 0) != null) {
+                    return Optional.of(createPendingIntent(intent, notification, user))
+                }
+            }
+        }
+        return Optional.empty()
+    }
+
+    private fun createPendingIntent(intent: Intent, notification: Notification, user: User): PendingIntent {
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+        return PendingIntent.getActivity(
+            context,
+            notification.getNotificationId(),
+            putExtrasToIntent(intent, notification, user),
+            PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
+        )
+    }
+
+    private fun putExtrasToIntent(intent: Intent, notification: Notification, user: User): Intent {
+        return intent
+            .putExtra(EXTRA_ACCOUNT, user.accountName)
+            .putExtra(EXTRA_LINK, notification.getLink())
+            .putExtra(EXTRA_OBJECT_ID, notification.getObjectId())
+            .putExtra(EXTRA_SUBJECT, notification.getSubject())
+            .putExtra(EXTRA_SUBJECT_RICH, notification.getSubjectRich())
+            .putExtra(EXTRA_MESSAGE, notification.getMessage())
+            .putExtra(EXTRA_MESSAGE_RICH, notification.getMessageRich())
+            .putExtra(EXTRA_USER, notification.getUser())
+            .putExtra(EXTRA_NID, notification.getNotificationId())
+    }
+
+    companion object {
+        const val APP_NAME = "deck"
+        val DECK_APP_PACKAGES = arrayOf(
+            "it.niedermann.nextcloud.deck",
+            "it.niedermann.nextcloud.deck.play",
+            "it.niedermann.nextcloud.deck.dev"
+        )
+        const val DECK_ACTIVITY_TO_START = "it.niedermann.nextcloud.deck.ui.PushNotificationActivity"
+        private const val EXTRA_ACCOUNT = "account"
+        private const val EXTRA_LINK = "link"
+        private const val EXTRA_OBJECT_ID = "objectId"
+        private const val EXTRA_SUBJECT = "subject"
+        private const val EXTRA_SUBJECT_RICH = "subjectRich"
+        private const val EXTRA_MESSAGE = "message"
+        private const val EXTRA_MESSAGE_RICH = "messageRich"
+        private const val EXTRA_USER = "user"
+        private const val EXTRA_NID = "nid"
+    }
+}

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

@@ -142,7 +142,7 @@ class NotificationWork constructor(
 
         val deckActionOverrideIntent = deckApi.createForwardToDeckActionIntent(notification, user)
 
-        val pendingIntent: PendingIntent
+        val pendingIntent: PendingIntent?
         if (deckActionOverrideIntent.isPresent) {
             pendingIntent = deckActionOverrideIntent.get()
         } else {

+ 10 - 21
app/src/main/java/com/nextcloud/ui/SquareLoaderImageView.java → app/src/main/java/com/nextcloud/ui/SquareLoaderImageView.kt

@@ -17,32 +17,21 @@
  * 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.ui
 
-package com.nextcloud.ui;
-
-import android.content.Context;
-import android.util.AttributeSet;
-
-import com.elyeproj.loaderviewlibrary.LoaderImageView;
+import android.content.Context
+import android.util.AttributeSet
+import com.elyeproj.loaderviewlibrary.LoaderImageView
 
 /**
  * Square version of loader image.
  */
-class SquareLoaderImageView extends LoaderImageView {
-    public SquareLoaderImageView(Context context) {
-        super(context);
-    }
-
-    public SquareLoaderImageView(Context context, AttributeSet attrs) {
-        super(context, attrs);
-    }
-
-    public SquareLoaderImageView(Context context, AttributeSet attrs, int defStyle) {
-        super(context, attrs, defStyle);
-    }
+internal class SquareLoaderImageView : LoaderImageView {
+    constructor(context: Context?) : super(context)
+    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
+    constructor(context: Context?, attrs: AttributeSet?, defStyle: Int) : super(context, attrs, defStyle)
 
-    @Override
-    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-        super.onMeasure(widthMeasureSpec, widthMeasureSpec);
+    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+        super.onMeasure(widthMeasureSpec, widthMeasureSpec)
     }
 }

+ 2 - 4
app/src/main/java/com/nextcloud/ui/fileactions/FileAction.kt

@@ -39,8 +39,7 @@ enum class FileAction(@IdRes val id: Int, @StringRes val title: Int, @DrawableRe
 
     // File moving
     RENAME_FILE(R.id.action_rename_file, R.string.common_rename, R.drawable.ic_rename),
-    MOVE(R.id.action_move, R.string.actionbar_move, R.drawable.ic_move),
-    COPY(R.id.action_copy, R.string.actionbar_copy, R.drawable.ic_content_copy),
+    MOVE_OR_COPY(R.id.action_move_or_copy, R.string.actionbar_move_or_copy, R.drawable.ic_external),
 
     // favorites
     FAVORITE(R.id.action_favorite, R.string.favorite, R.drawable.ic_star),
@@ -83,8 +82,7 @@ enum class FileAction(@IdRes val id: Int, @StringRes val title: Int, @DrawableRe
             SEE_DETAILS,
             LOCK_FILE,
             RENAME_FILE,
-            MOVE,
-            COPY,
+            MOVE_OR_COPY,
             DOWNLOAD_FILE,
             EXPORT_FILE,
             STREAM_MEDIA,

+ 4 - 0
app/src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java

@@ -125,6 +125,7 @@ import com.owncloud.android.ui.dialog.SslUntrustedCertDialog.OnSslUntrustedCertL
 import com.owncloud.android.utils.DisplayUtils;
 import com.owncloud.android.utils.ErrorMessageAdapter;
 import com.owncloud.android.utils.PermissionUtil;
+import com.owncloud.android.utils.WebViewUtil;
 import com.owncloud.android.utils.theme.CapabilityUtils;
 import com.owncloud.android.utils.theme.ViewThemeUtils;
 
@@ -268,6 +269,7 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
         viewThemeUtils = viewThemeUtilsFactory.withPrimaryAsBackground();
         viewThemeUtils.platform.themeStatusBar(this, ColorRole.PRIMARY);
 
+        WebViewUtil webViewUtil = new WebViewUtil(this);
 
         Uri data = getIntent().getData();
         boolean directLogin = data != null && data.toString().startsWith(getString(R.string.login_data_own_scheme));
@@ -337,6 +339,8 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity
         }
 
         initServerPreFragment(savedInstanceState);
+
+        webViewUtil.checkWebViewVersion();
     }
 
     private void deleteCookies() {

+ 7 - 6
app/src/main/java/com/owncloud/android/datamodel/OCFile.java

@@ -24,14 +24,11 @@ package com.owncloud.android.datamodel;
 
 import android.content.ContentResolver;
 import android.content.Context;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.LayerDrawable;
 import android.net.Uri;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.text.TextUtils;
 
-import com.nextcloud.android.common.ui.theme.utils.ColorRole;
 import com.owncloud.android.R;
 import com.owncloud.android.lib.common.network.WebdavEntry;
 import com.owncloud.android.lib.common.network.WebdavUtils;
@@ -41,9 +38,7 @@ import com.owncloud.android.lib.resources.files.model.GeoLocation;
 import com.owncloud.android.lib.resources.files.model.ImageDimension;
 import com.owncloud.android.lib.resources.files.model.ServerFileInterface;
 import com.owncloud.android.lib.resources.shares.ShareeUser;
-import com.owncloud.android.utils.DrawableUtil;
 import com.owncloud.android.utils.MimeType;
-import com.owncloud.android.utils.theme.ViewThemeUtils;
 
 import java.io.File;
 import java.util.ArrayList;
@@ -52,7 +47,6 @@ import java.util.List;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
-import androidx.core.content.ContextCompat;
 import androidx.core.content.FileProvider;
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 import third_parties.daveKoeller.AlphanumComparator;
@@ -350,6 +344,13 @@ public class OCFile implements Parcelable, Comparable<OCFile>, ServerFileInterfa
         return false;
     }
 
+    public String getFileNameWithExtension(int fileNameLength) {
+        String fileName = getFileName();
+        String shortFileName = fileName.substring(0, Math.min(fileName.length(), fileNameLength));
+        String extension = "." + fileName.substring(fileName.lastIndexOf('.') + 1);
+        return shortFileName + extension;
+    }
+
     /**
      * The path, where the file is stored locally
      *

+ 3 - 11
app/src/main/java/com/owncloud/android/files/FileMenuFilter.java

@@ -161,8 +161,7 @@ public class FileMenuFilter {
         filterDownload(toHide, synchronizing);
         filterExport(toHide);
         filterRename(toHide, synchronizing);
-        filterCopy(toHide, synchronizing);
-        filterMove(toHide, synchronizing);
+        filterMoveOrCopy(toHide, synchronizing);
         filterRemove(toHide, synchronizing);
         filterSelectAll(toHide, inSingleFileFragment);
         filterDeselectAll(toHide, inSingleFileFragment);
@@ -346,19 +345,12 @@ public class FileMenuFilter {
         }
     }
 
-    private void filterMove(List<Integer> toHide, boolean synchronizing) {
+    private void filterMoveOrCopy(List<Integer> toHide, boolean synchronizing) {
         if (files.isEmpty() || synchronizing || containsEncryptedFile() || containsEncryptedFolder() || containsLockedFile()) {
-            toHide.add(R.id.action_move);
+            toHide.add(R.id.action_move_or_copy);
         }
     }
 
-    private void filterCopy(List<Integer> toHide, boolean synchronizing) {
-        if (files.isEmpty() || synchronizing || containsEncryptedFile() || containsEncryptedFolder()) {
-            toHide.add(R.id.action_copy);
-        }
-    }
-
-
     private void filterRename(Collection<Integer> toHide, boolean synchronizing) {
         if (!isSingleSelection() || synchronizing || containsEncryptedFile() || containsEncryptedFolder() || containsLockedFile()) {
             toHide.add(R.id.action_rename_file);

+ 0 - 741
app/src/main/java/com/owncloud/android/files/services/FileDownloader.java

@@ -1,741 +0,0 @@
-/*
- *   ownCloud Android client application
- *
- *   Copyright (C) 2012 Bartek Przybylski
- *   Copyright (C) 2012-2016 ownCloud Inc.
- *
- *   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.files.services;
-
-import android.accounts.Account;
-import android.accounts.AccountManager;
-import android.accounts.OnAccountsUpdateListener;
-import android.app.Notification;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.app.Service;
-import android.content.Intent;
-import android.graphics.BitmapFactory;
-import android.os.Binder;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.IBinder;
-import android.os.Looper;
-import android.os.Message;
-import android.os.Process;
-import android.util.Pair;
-
-import com.nextcloud.client.account.User;
-import com.nextcloud.client.account.UserAccountManager;
-import com.nextcloud.client.files.downloader.DownloadTask;
-import com.nextcloud.java.util.Optional;
-import com.owncloud.android.R;
-import com.owncloud.android.authentication.AuthenticatorActivity;
-import com.owncloud.android.datamodel.FileDataStorageManager;
-import com.owncloud.android.datamodel.OCFile;
-import com.owncloud.android.datamodel.UploadsStorageManager;
-import com.owncloud.android.lib.common.OwnCloudAccount;
-import com.owncloud.android.lib.common.OwnCloudClient;
-import com.owncloud.android.lib.common.OwnCloudClientManagerFactory;
-import com.owncloud.android.lib.common.network.OnDatatransferProgressListener;
-import com.owncloud.android.lib.common.operations.RemoteOperationResult;
-import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode;
-import com.owncloud.android.lib.common.utils.Log_OC;
-import com.owncloud.android.lib.resources.files.FileUtils;
-import com.owncloud.android.operations.DownloadFileOperation;
-import com.owncloud.android.operations.DownloadType;
-import com.owncloud.android.providers.DocumentsStorageProvider;
-import com.owncloud.android.ui.activity.ConflictsResolveActivity;
-import com.owncloud.android.ui.activity.FileActivity;
-import com.owncloud.android.ui.activity.FileDisplayActivity;
-import com.owncloud.android.ui.dialog.SendShareDialog;
-import com.owncloud.android.ui.fragment.OCFileListFragment;
-import com.owncloud.android.ui.notifications.NotificationUtils;
-import com.owncloud.android.ui.preview.PreviewImageActivity;
-import com.owncloud.android.ui.preview.PreviewImageFragment;
-import com.owncloud.android.utils.ErrorMessageAdapter;
-import com.owncloud.android.utils.MimeTypeUtil;
-import com.owncloud.android.utils.theme.ViewThemeUtils;
-
-import java.io.File;
-import java.security.SecureRandom;
-import java.util.AbstractList;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.Vector;
-
-import javax.inject.Inject;
-
-import androidx.core.app.NotificationCompat;
-import androidx.localbroadcastmanager.content.LocalBroadcastManager;
-import dagger.android.AndroidInjection;
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-
-public class FileDownloader extends Service
-        implements OnDatatransferProgressListener, OnAccountsUpdateListener {
-
-    public static final String EXTRA_USER = "USER";
-    public static final String EXTRA_FILE = "FILE";
-
-    private static final String DOWNLOAD_ADDED_MESSAGE = "DOWNLOAD_ADDED";
-    private static final String DOWNLOAD_FINISH_MESSAGE = "DOWNLOAD_FINISH";
-    public static final String EXTRA_DOWNLOAD_RESULT = "RESULT";
-    public static final String EXTRA_REMOTE_PATH = "REMOTE_PATH";
-    public static final String EXTRA_LINKED_TO_PATH = "LINKED_TO";
-    public static final String ACCOUNT_NAME = "ACCOUNT_NAME";
-    public static final String DOWNLOAD_TYPE = "DOWNLOAD_TYPE";
-
-    private static final int FOREGROUND_SERVICE_ID = 412;
-
-    private static final String TAG = FileDownloader.class.getSimpleName();
-
-    private Looper mServiceLooper;
-    private ServiceHandler mServiceHandler;
-    private IBinder mBinder;
-    private OwnCloudClient mDownloadClient;
-    private Optional<User> currentUser = Optional.empty();
-    private FileDataStorageManager mStorageManager;
-
-    private IndexedForest<DownloadFileOperation> mPendingDownloads = new IndexedForest<>();
-
-    private DownloadFileOperation mCurrentDownload;
-
-    private NotificationManager mNotificationManager;
-    private NotificationCompat.Builder mNotificationBuilder;
-    private int mLastPercent;
-
-    private Notification mNotification;
-
-    private long conflictUploadId;
-
-    public boolean mStartedDownload = false;
-
-    @Inject UserAccountManager accountManager;
-    @Inject UploadsStorageManager uploadsStorageManager;
-    @Inject LocalBroadcastManager localBroadcastManager;
-    @Inject ViewThemeUtils viewThemeUtils;
-
-    public static String getDownloadAddedMessage() {
-        return FileDownloader.class.getName() + DOWNLOAD_ADDED_MESSAGE;
-    }
-
-    public static String getDownloadFinishMessage() {
-        return FileDownloader.class.getName() + DOWNLOAD_FINISH_MESSAGE;
-    }
-
-    /**
-     * Service initialization
-     */
-    @Override
-    public void onCreate() {
-        super.onCreate();
-        AndroidInjection.inject(this);
-        Log_OC.d(TAG, "Creating service");
-        mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
-        HandlerThread thread = new HandlerThread("FileDownloaderThread", Process.THREAD_PRIORITY_BACKGROUND);
-        thread.start();
-        mServiceLooper = thread.getLooper();
-        mServiceHandler = new ServiceHandler(mServiceLooper, this);
-        mBinder = new FileDownloaderBinder();
-
-        NotificationCompat.Builder builder = NotificationUtils.newNotificationBuilder(this, viewThemeUtils).setContentTitle(
-            getApplicationContext().getResources().getString(R.string.app_name))
-            .setContentText(getApplicationContext().getResources().getString(R.string.foreground_service_download))
-            .setSmallIcon(R.drawable.notification_icon)
-            .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.notification_icon));
-
-
-        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
-            builder.setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_DOWNLOAD);
-        }
-
-        mNotification = builder.build();
-
-        // add AccountsUpdatedListener
-        AccountManager am = AccountManager.get(getApplicationContext());
-        am.addOnAccountsUpdatedListener(this, null, false);
-    }
-
-
-    /**
-     * Service clean up
-     */
-    @Override
-    public void onDestroy() {
-        Log_OC.v(TAG, "Destroying service");
-        mBinder = null;
-        mServiceHandler = null;
-        mServiceLooper.quit();
-        mServiceLooper = null;
-        mNotificationManager = null;
-
-        // remove AccountsUpdatedListener
-        AccountManager am = AccountManager.get(getApplicationContext());
-        am.removeOnAccountsUpdatedListener(this);
-        super.onDestroy();
-    }
-
-
-    /**
-     * Entry point to add one or several files to the queue of downloads.
-     *
-     * New downloads are added calling to startService(), resulting in a call to this method.
-     * This ensures the service will keep on working although the caller activity goes away.
-     */
-    @Override
-    public int onStartCommand(Intent intent, int flags, int startId) {
-        Log_OC.d(TAG, "Starting command with id " + startId);
-
-        startForeground(FOREGROUND_SERVICE_ID, mNotification);
-
-        if (intent == null || !intent.hasExtra(EXTRA_USER) || !intent.hasExtra(EXTRA_FILE)) {
-            Log_OC.e(TAG, "Not enough information provided in intent");
-            return START_NOT_STICKY;
-        } else {
-            final User user = intent.getParcelableExtra(EXTRA_USER);
-            final OCFile file = intent.getParcelableExtra(EXTRA_FILE);
-            final String behaviour = intent.getStringExtra(OCFileListFragment.DOWNLOAD_BEHAVIOUR);
-
-            DownloadType downloadType = DownloadType.DOWNLOAD;
-            if (intent.hasExtra(DOWNLOAD_TYPE)) {
-                downloadType = (DownloadType) intent.getSerializableExtra(DOWNLOAD_TYPE);
-            }
-            String activityName = intent.getStringExtra(SendShareDialog.ACTIVITY_NAME);
-            String packageName = intent.getStringExtra(SendShareDialog.PACKAGE_NAME);
-            conflictUploadId = intent.getLongExtra(ConflictsResolveActivity.EXTRA_CONFLICT_UPLOAD_ID, -1);
-            AbstractList<String> requestedDownloads = new Vector<String>();
-            try {
-                DownloadFileOperation newDownload = new DownloadFileOperation(user,
-                                                                              file,
-                                                                              behaviour,
-                                                                              activityName,
-                                                                              packageName,
-                                                                              getBaseContext(),
-                                                                              downloadType);
-                newDownload.addDatatransferProgressListener(this);
-                newDownload.addDatatransferProgressListener((FileDownloaderBinder) mBinder);
-                Pair<String, String> putResult = mPendingDownloads.putIfAbsent(user.getAccountName(),
-                                                                               file.getRemotePath(),
-                                                                               newDownload);
-                if (putResult != null) {
-                    String downloadKey = putResult.first;
-                    requestedDownloads.add(downloadKey);
-                    sendBroadcastNewDownload(newDownload, putResult.second);
-                }   // else, file already in the queue of downloads; don't repeat the request
-
-            } catch (IllegalArgumentException e) {
-                Log_OC.e(TAG, "Not enough information provided in intent: " + e.getMessage());
-                return START_NOT_STICKY;
-            }
-
-            if (requestedDownloads.size() > 0) {
-                Message msg = mServiceHandler.obtainMessage();
-                msg.arg1 = startId;
-                msg.obj = requestedDownloads;
-                mServiceHandler.sendMessage(msg);
-            }
-        }
-
-        return START_NOT_STICKY;
-    }
-
-    /**
-     * Provides a binder object that clients can use to perform operations on the queue of downloads,
-     * excepting the addition of new files.
-     *
-     * Implemented to perform cancellation, pause and resume of existing downloads.
-     */
-    @Override
-    public IBinder onBind(Intent intent) {
-        return mBinder;
-    }
-
-
-    /**
-     * Called when ALL the bound clients were onbound.
-     */
-    @Override
-    public boolean onUnbind(Intent intent) {
-        ((FileDownloaderBinder) mBinder).clearListeners();
-        return false;   // not accepting rebinding (default behaviour)
-    }
-
-    @Override
-    public void onAccountsUpdated(Account[] accounts) {
-         //review the current download and cancel it if its account doesn't exist
-        if (mCurrentDownload != null && !accountManager.exists(mCurrentDownload.getUser().toPlatformAccount())) {
-            mCurrentDownload.cancel();
-        }
-        // The rest of downloads are cancelled when they try to start
-    }
-
-
-    /**
-     * Binder to let client components to perform operations on the queue of downloads.
-     * <p/>
-     * It provides by itself the available operations.
-     */
-    public class FileDownloaderBinder extends Binder implements OnDatatransferProgressListener {
-
-        /**
-         * Map of listeners that will be reported about progress of downloads from a
-         * {@link FileDownloaderBinder}
-         * instance.
-         */
-        private Map<Long, OnDatatransferProgressListener> mBoundListeners =
-                new HashMap<Long, OnDatatransferProgressListener>();
-
-
-        /**
-         * Cancels a pending or current download of a remote file.
-         *
-         * @param account ownCloud account where the remote file is stored.
-         * @param file    A file in the queue of pending downloads
-         */
-        public void cancel(Account account, OCFile file) {
-            Pair<DownloadFileOperation, String> removeResult =
-                mPendingDownloads.remove(account.name, file.getRemotePath());
-            DownloadFileOperation download = removeResult.first;
-            if (download != null) {
-                download.cancel();
-            } else {
-                if (mCurrentDownload != null && currentUser.isPresent() &&
-                    mCurrentDownload.getRemotePath().startsWith(file.getRemotePath()) &&
-                        account.name.equals(currentUser.get().getAccountName())) {
-                    mCurrentDownload.cancel();
-                }
-            }
-        }
-
-        /**
-         * Cancels all the downloads for an account
-         */
-        public void cancel(String accountName) {
-            if (mCurrentDownload != null && mCurrentDownload.getUser().nameEquals(accountName)) {
-                mCurrentDownload.cancel();
-            }
-            // Cancel pending downloads
-            cancelPendingDownloads(accountName);
-        }
-
-        public void clearListeners() {
-            mBoundListeners.clear();
-        }
-
-
-        /**
-         * Returns True when the file described by 'file' in the ownCloud account 'account'
-         * is downloading or waiting to download.
-         *
-         * If 'file' is a directory, returns 'true' if any of its descendant files is downloading or
-         * waiting to download.
-         *
-         * @param user    user where the remote file is stored.
-         * @param file    A file that could be in the queue of downloads.
-         */
-        public boolean isDownloading(User user, OCFile file) {
-            return user != null && file != null && mPendingDownloads.contains(user.getAccountName(), file.getRemotePath());
-        }
-
-
-        /**
-         * Adds a listener interested in the progress of the download for a concrete file.
-         *
-         * @param listener Object to notify about progress of transfer.
-         * @param file     {@link OCFile} of interest for listener.
-         */
-        public void addDatatransferProgressListener(OnDatatransferProgressListener listener, OCFile file) {
-            if (file == null || listener == null) {
-                return;
-            }
-            mBoundListeners.put(file.getFileId(), listener);
-        }
-
-
-        /**
-         * Removes a listener interested in the progress of the download for a concrete file.
-         *
-         * @param listener      Object to notify about progress of transfer.
-         * @param file          {@link OCFile} of interest for listener.
-         */
-        public void removeDatatransferProgressListener(OnDatatransferProgressListener listener, OCFile file) {
-            if (file == null || listener == null) {
-                return;
-            }
-            Long fileId = file.getFileId();
-            if (mBoundListeners.get(fileId) == listener) {
-                mBoundListeners.remove(fileId);
-            }
-        }
-
-        @Override
-        public void onTransferProgress(long progressRate, long totalTransferredSoFar,
-                                       long totalToTransfer, String fileName) {
-            OnDatatransferProgressListener boundListener =
-                    mBoundListeners.get(mCurrentDownload.getFile().getFileId());
-            if (boundListener != null) {
-                boundListener.onTransferProgress(progressRate, totalTransferredSoFar,
-                                                 totalToTransfer, fileName);
-            }
-        }
-
-    }
-
-    /**
-     * Download worker. Performs the pending downloads in the order they were requested.
-
-     * Created with the Looper of a new thread, started in {@link FileUploader#onCreate()}.
-     */
-    private static class ServiceHandler extends Handler {
-        // don't make it a final class, and don't remove the static ; lint will warn about a
-        // possible memory leak
-        FileDownloader mService;
-
-        public ServiceHandler(Looper looper, FileDownloader service) {
-            super(looper);
-            if (service == null) {
-                throw new IllegalArgumentException("Received invalid NULL in parameter 'service'");
-            }
-            mService = service;
-        }
-
-        @Override
-        public void handleMessage(Message msg) {
-            @SuppressWarnings("unchecked")
-            AbstractList<String> requestedDownloads = (AbstractList<String>) msg.obj;
-            if (msg.obj != null) {
-                Iterator<String> it = requestedDownloads.iterator();
-                while (it.hasNext()) {
-                    String next = it.next();
-                    mService.downloadFile(next);
-                }
-            }
-            mService.mStartedDownload=false;
-
-            (new Handler()).postDelayed(() -> {
-                if(!mService.mStartedDownload){
-                    mService.mNotificationManager.cancel(R.string.downloader_download_in_progress_ticker);
-                }
-                Log_OC.d(TAG, "Stopping after command with id " + msg.arg1);
-                mService.mNotificationManager.cancel(FOREGROUND_SERVICE_ID);
-                mService.stopForeground(true);
-                mService.stopSelf(msg.arg1);
-            }, 2000);
-        }
-    }
-
-
-    /**
-     * Core download method: requests a file to download and stores it.
-     *
-     * @param downloadKey Key to access the download to perform, contained in mPendingDownloads
-     */
-    private void downloadFile(String downloadKey) {
-
-        mStartedDownload = true;
-        mCurrentDownload = mPendingDownloads.get(downloadKey);
-
-        if (mCurrentDownload != null) {
-            // Detect if the account exists
-            if (accountManager.exists(mCurrentDownload.getUser().toPlatformAccount())) {
-                notifyDownloadStart(mCurrentDownload);
-                RemoteOperationResult downloadResult = null;
-                try {
-                    /// prepare client object to send the request to the ownCloud server
-                    Account currentDownloadAccount = mCurrentDownload.getUser().toPlatformAccount();
-                    Optional<User> currentDownloadUser = accountManager.getUser(currentDownloadAccount.name);
-                    if (!currentUser.equals(currentDownloadUser)) {
-                        currentUser = currentDownloadUser;
-                        mStorageManager = new FileDataStorageManager(currentUser.get(), getContentResolver());
-                    }   // else, reuse storage manager from previous operation
-
-                    // always get client from client manager, to get fresh credentials in case
-                    // of update
-                    OwnCloudAccount ocAccount = currentDownloadUser.get().toOwnCloudAccount();
-                    mDownloadClient = OwnCloudClientManagerFactory.getDefaultSingleton().
-                            getClientFor(ocAccount, this);
-
-
-                    /// perform the download
-                    downloadResult = mCurrentDownload.execute(mDownloadClient);
-                    if (downloadResult.isSuccess() && mCurrentDownload.getDownloadType() == DownloadType.DOWNLOAD) {
-                        saveDownloadedFile();
-                    }
-
-                } catch (Exception e) {
-                    Log_OC.e(TAG, "Error downloading", e);
-                    downloadResult = new RemoteOperationResult(e);
-
-                } finally {
-                    Pair<DownloadFileOperation, String> removeResult = mPendingDownloads.removePayload(
-                        mCurrentDownload.getUser().getAccountName(), mCurrentDownload.getRemotePath());
-
-                    if (downloadResult == null) {
-                        downloadResult = new RemoteOperationResult(new RuntimeException("Error downloading…"));
-                    }
-
-                    /// notify result
-                    notifyDownloadResult(mCurrentDownload, downloadResult);
-                    sendBroadcastDownloadFinished(mCurrentDownload, downloadResult, removeResult.second);
-                }
-            } else {
-                cancelPendingDownloads(mCurrentDownload.getUser().getAccountName());
-            }
-        }
-    }
-
-
-    /**
-     * Updates the OC File after a successful download.
-     *
-     * TODO move to DownloadFileOperation
-     *  unify with code from {@link DocumentsStorageProvider} and {@link DownloadTask}.
-     */
-    private void saveDownloadedFile() {
-        OCFile file = mStorageManager.getFileById(mCurrentDownload.getFile().getFileId());
-
-        if (file == null) {
-            // try to get file via path, needed for overwriting existing files on conflict dialog
-            file = mStorageManager.getFileByDecryptedRemotePath(mCurrentDownload.getFile().getRemotePath());
-        }
-
-        if (file == null) {
-            Log_OC.e(this, "Could not save " + mCurrentDownload.getFile().getRemotePath());
-            return;
-        }
-
-        long syncDate = System.currentTimeMillis();
-        file.setLastSyncDateForProperties(syncDate);
-        file.setLastSyncDateForData(syncDate);
-        file.setUpdateThumbnailNeeded(true);
-        file.setModificationTimestamp(mCurrentDownload.getModificationTimestamp());
-        file.setModificationTimestampAtLastSyncForData(mCurrentDownload.getModificationTimestamp());
-        file.setEtag(mCurrentDownload.getEtag());
-        file.setMimeType(mCurrentDownload.getMimeType());
-        file.setStoragePath(mCurrentDownload.getSavePath());
-        file.setFileLength(new File(mCurrentDownload.getSavePath()).length());
-        file.setRemoteId(mCurrentDownload.getFile().getRemoteId());
-        mStorageManager.saveFile(file);
-        if (MimeTypeUtil.isMedia(mCurrentDownload.getMimeType())) {
-            FileDataStorageManager.triggerMediaScan(file.getStoragePath(), file);
-        }
-        mStorageManager.saveConflict(file, null);
-    }
-
-    /**
-     * Creates a status notification to show the download progress
-     *
-     * @param download Download operation starting.
-     */
-    private void notifyDownloadStart(DownloadFileOperation download) {
-        /// create status notification with a progress bar
-        mLastPercent = 0;
-        mNotificationBuilder = NotificationUtils.newNotificationBuilder(this, viewThemeUtils);
-        mNotificationBuilder
-            .setSmallIcon(R.drawable.notification_icon)
-            .setTicker(getString(R.string.downloader_download_in_progress_ticker))
-            .setContentTitle(getString(R.string.downloader_download_in_progress_ticker))
-            .setOngoing(true)
-            .setProgress(100, 0, download.getSize() < 0)
-            .setContentText(
-                String.format(getString(R.string.downloader_download_in_progress_content), 0,
-                              new File(download.getSavePath()).getName())
-                           );
-
-        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
-            mNotificationBuilder.setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_DOWNLOAD);
-        }
-
-        /// includes a pending intent in the notification showing the details view of the file
-        Intent showDetailsIntent = null;
-        if (PreviewImageFragment.canBePreviewed(download.getFile())) {
-            showDetailsIntent = new Intent(this, PreviewImageActivity.class);
-        } else {
-            showDetailsIntent = new Intent(this, FileDisplayActivity.class);
-        }
-        showDetailsIntent.putExtra(FileActivity.EXTRA_FILE, download.getFile());
-        showDetailsIntent.putExtra(FileActivity.EXTRA_USER, download.getUser());
-        showDetailsIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
-
-        mNotificationBuilder.setContentIntent(PendingIntent.getActivity(this, (int) System.currentTimeMillis(),
-                                                                        showDetailsIntent, PendingIntent.FLAG_IMMUTABLE));
-
-
-        if (mNotificationManager == null) {
-            mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
-        }
-        if (mNotificationManager != null) {
-            mNotificationManager.notify(R.string.downloader_download_in_progress_ticker, mNotificationBuilder.build());
-        }
-    }
-
-
-    /**
-     * Callback method to update the progress bar in the status notification.
-     */
-    @Override
-    public void onTransferProgress(long progressRate, long totalTransferredSoFar,
-                                   long totalToTransfer, String filePath) {
-        int percent = (int) (100.0 * ((double) totalTransferredSoFar) / ((double) totalToTransfer));
-        if (percent != mLastPercent) {
-            mNotificationBuilder.setProgress(100, percent, totalToTransfer < 0);
-            String fileName = filePath.substring(filePath.lastIndexOf(FileUtils.PATH_SEPARATOR) + 1);
-            String text = String.format(getString(R.string.downloader_download_in_progress_content), percent, fileName);
-            mNotificationBuilder.setContentText(text);
-
-            if (mNotificationManager == null) {
-                mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
-            }
-
-            if (mNotificationManager != null) {
-                mNotificationManager.notify(R.string.downloader_download_in_progress_ticker,
-                        mNotificationBuilder.build());
-            }
-        }
-        mLastPercent = percent;
-    }
-
-
-    /**
-     * Updates the status notification with the result of a download operation.
-     *
-     * @param downloadResult Result of the download operation.
-     * @param download       Finished download operation
-     */
-    @SuppressFBWarnings("DMI")
-    private void notifyDownloadResult(DownloadFileOperation download,
-                                      RemoteOperationResult downloadResult) {
-        if (mNotificationManager == null) {
-            mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
-        }
-
-        if (!downloadResult.isCancelled()) {
-            if (downloadResult.isSuccess()) {
-                if (conflictUploadId > 0) {
-                    uploadsStorageManager.removeUpload(conflictUploadId);
-                }
-                // Dont show notification except an error has occured.
-                return;
-            }
-            int tickerId = downloadResult.isSuccess() ?
-                    R.string.downloader_download_succeeded_ticker : R.string.downloader_download_failed_ticker;
-
-            boolean needsToUpdateCredentials = ResultCode.UNAUTHORIZED == downloadResult.getCode();
-            tickerId = needsToUpdateCredentials ?
-                    R.string.downloader_download_failed_credentials_error : tickerId;
-
-            mNotificationBuilder
-                    .setTicker(getString(tickerId))
-                    .setContentTitle(getString(tickerId))
-                    .setAutoCancel(true)
-                    .setOngoing(false)
-                    .setProgress(0, 0, false);
-
-            if (needsToUpdateCredentials) {
-                configureUpdateCredentialsNotification(download.getUser());
-
-            } else {
-                // TODO put something smart in showDetailsIntent
-                Intent showDetailsIntent = new Intent();
-                mNotificationBuilder.setContentIntent(PendingIntent.getActivity(this, (int) System.currentTimeMillis(),
-                                                                                showDetailsIntent, PendingIntent.FLAG_IMMUTABLE));
-            }
-
-            mNotificationBuilder.setContentText(ErrorMessageAdapter.getErrorCauseMessage(downloadResult,
-                    download, getResources()));
-
-            if (mNotificationManager != null) {
-                mNotificationManager.notify((new SecureRandom()).nextInt(), mNotificationBuilder.build());
-
-                // Remove success notification
-                if (downloadResult.isSuccess()) {
-                    // Sleep 2 seconds, so show the notification before remove it
-                    NotificationUtils.cancelWithDelay(mNotificationManager,
-                                                      R.string.downloader_download_succeeded_ticker, 2000);
-                }
-            }
-        }
-    }
-
-    private void configureUpdateCredentialsNotification(User user) {
-        // let the user update credentials with one click
-        Intent updateAccountCredentials = new Intent(this, AuthenticatorActivity.class);
-        updateAccountCredentials.putExtra(AuthenticatorActivity.EXTRA_ACCOUNT, user.toPlatformAccount());
-        updateAccountCredentials.putExtra(
-                AuthenticatorActivity.EXTRA_ACTION,
-                AuthenticatorActivity.ACTION_UPDATE_EXPIRED_TOKEN
-        );
-        updateAccountCredentials.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        updateAccountCredentials.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
-        updateAccountCredentials.addFlags(Intent.FLAG_FROM_BACKGROUND);
-        mNotificationBuilder.setContentIntent(
-            PendingIntent.getActivity(this,
-                                      (int) System.currentTimeMillis(),
-                                      updateAccountCredentials,
-                                      PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE)
-                                             );
-    }
-
-
-    /**
-     * Sends a broadcast when a download finishes in order to the interested activities can
-     * update their view
-     *
-     * @param download               Finished download operation
-     * @param downloadResult         Result of the download operation
-     * @param unlinkedFromRemotePath Path in the downloads tree where the download was unlinked from
-     */
-    private void sendBroadcastDownloadFinished(
-            DownloadFileOperation download,
-            RemoteOperationResult downloadResult,
-            String unlinkedFromRemotePath) {
-
-        Intent end = new Intent(getDownloadFinishMessage());
-        end.putExtra(EXTRA_DOWNLOAD_RESULT, downloadResult.isSuccess());
-        end.putExtra(ACCOUNT_NAME, download.getUser().getAccountName());
-        end.putExtra(EXTRA_REMOTE_PATH, download.getRemotePath());
-        end.putExtra(OCFileListFragment.DOWNLOAD_BEHAVIOUR, download.getBehaviour());
-        end.putExtra(SendShareDialog.ACTIVITY_NAME, download.getActivityName());
-        end.putExtra(SendShareDialog.PACKAGE_NAME, download.getPackageName());
-        if (unlinkedFromRemotePath != null) {
-            end.putExtra(EXTRA_LINKED_TO_PATH, unlinkedFromRemotePath);
-        }
-        end.setPackage(getPackageName());
-        localBroadcastManager.sendBroadcast(end);
-    }
-
-
-    /**
-     * Sends a broadcast when a new download is added to the queue.
-     *
-     * @param download           Added download operation
-     * @param linkedToRemotePath Path in the downloads tree where the download was linked to
-     */
-    private void sendBroadcastNewDownload(DownloadFileOperation download,
-                                          String linkedToRemotePath) {
-        Intent added = new Intent(getDownloadAddedMessage());
-        added.putExtra(ACCOUNT_NAME, download.getUser().getAccountName());
-        added.putExtra(EXTRA_REMOTE_PATH, download.getRemotePath());
-        added.putExtra(EXTRA_LINKED_TO_PATH, linkedToRemotePath);
-        added.setPackage(getPackageName());
-        localBroadcastManager.sendBroadcast(added);
-    }
-
-    private void cancelPendingDownloads(String accountName) {
-        mPendingDownloads.remove(accountName);
-    }
-}

+ 750 - 0
app/src/main/java/com/owncloud/android/files/services/FileDownloader.kt

@@ -0,0 +1,750 @@
+/*
+ *   ownCloud Android client application
+ *
+ *   Copyright (C) 2012 Bartek Przybylski
+ *   Copyright (C) 2012-2016 ownCloud Inc.
+ *
+ *   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.files.services
+
+import android.accounts.Account
+import android.accounts.AccountManager
+import android.accounts.OnAccountsUpdateListener
+import android.app.Notification
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.app.Service
+import android.content.Intent
+import android.os.Binder
+import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.IBinder
+import android.os.Looper
+import android.os.Message
+import android.os.Process
+import androidx.core.app.NotificationCompat
+import androidx.localbroadcastmanager.content.LocalBroadcastManager
+import com.nextcloud.client.account.User
+import com.nextcloud.client.account.UserAccountManager
+import com.nextcloud.java.util.Optional
+import com.owncloud.android.R
+import com.owncloud.android.authentication.AuthenticatorActivity
+import com.owncloud.android.datamodel.FileDataStorageManager
+import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.datamodel.UploadsStorageManager
+import com.owncloud.android.lib.common.OwnCloudClient
+import com.owncloud.android.lib.common.OwnCloudClientManagerFactory
+import com.owncloud.android.lib.common.network.OnDatatransferProgressListener
+import com.owncloud.android.lib.common.operations.RemoteOperationResult
+import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode
+import com.owncloud.android.lib.common.utils.Log_OC
+import com.owncloud.android.operations.DownloadFileOperation
+import com.owncloud.android.operations.DownloadType
+import com.owncloud.android.ui.activity.ConflictsResolveActivity
+import com.owncloud.android.ui.activity.FileActivity
+import com.owncloud.android.ui.activity.FileDisplayActivity
+import com.owncloud.android.ui.dialog.SendShareDialog
+import com.owncloud.android.ui.fragment.OCFileListFragment
+import com.owncloud.android.ui.notifications.NotificationUtils
+import com.owncloud.android.ui.preview.PreviewImageActivity
+import com.owncloud.android.ui.preview.PreviewImageFragment
+import com.owncloud.android.utils.ErrorMessageAdapter
+import com.owncloud.android.utils.MimeTypeUtil
+import com.owncloud.android.utils.theme.ViewThemeUtils
+import dagger.android.AndroidInjection
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
+import java.io.File
+import java.security.SecureRandom
+import java.util.AbstractList
+import javax.inject.Inject
+
+class FileDownloader : Service(), OnDatatransferProgressListener, OnAccountsUpdateListener {
+    private var mServiceLooper: Looper? = null
+    private var mServiceHandler: ServiceHandler? = null
+    private var mBinder: IBinder? = null
+    private var mDownloadClient: OwnCloudClient? = null
+    private var currentUser = Optional.empty<User>()
+    private var mStorageManager: FileDataStorageManager? = null
+    private val mPendingDownloads = IndexedForest<DownloadFileOperation>()
+    private var mCurrentDownload: DownloadFileOperation? = null
+    private var notificationManager: NotificationManager? = null
+    private var notification: Notification? = null
+    private var notificationBuilder: NotificationCompat.Builder? = null
+    private var mLastPercent = 0
+    private var conflictUploadId: Long = 0
+    var mStartedDownload = false
+
+    @JvmField
+    @Inject
+    var accountManager: UserAccountManager? = null
+
+    @JvmField
+    @Inject
+    var uploadsStorageManager: UploadsStorageManager? = null
+
+    @JvmField
+    @Inject
+    var localBroadcastManager: LocalBroadcastManager? = null
+
+    @JvmField
+    @Inject
+    var viewThemeUtils: ViewThemeUtils? = null
+
+    /**
+     * Service initialization
+     */
+    override fun onCreate() {
+        super.onCreate()
+
+        AndroidInjection.inject(this)
+        Log_OC.d(TAG, "Creating service")
+        initNotificationManager()
+        val thread = HandlerThread("FileDownloaderThread", Process.THREAD_PRIORITY_BACKGROUND)
+        thread.start()
+        mServiceLooper = thread.looper
+        mServiceHandler = ServiceHandler(mServiceLooper, this)
+        mBinder = FileDownloaderBinder()
+        initNotificationBuilder()
+
+        // add AccountsUpdatedListener
+        val am = AccountManager.get(applicationContext)
+        am.addOnAccountsUpdatedListener(this, null, false)
+    }
+
+    private fun initNotificationManager() {
+        if (notificationManager == null) {
+            notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
+        }
+    }
+
+    private fun initNotificationBuilder() {
+        val resources = applicationContext.resources
+        val title = resources.getString(R.string.foreground_service_download)
+
+        notificationBuilder = NotificationUtils.newNotificationBuilder(this, viewThemeUtils)
+            .setSmallIcon(R.drawable.notification_icon)
+            .setOngoing(true)
+            .setContentTitle(title)
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            notificationBuilder?.setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_DOWNLOAD)
+        }
+        notification = notificationBuilder?.build()
+    }
+
+    private fun notifyNotificationManager() {
+        notificationManager?.notify(R.string.downloader_download_in_progress_ticker, notificationBuilder?.build())
+    }
+
+    /**
+     * Service clean up
+     */
+    override fun onDestroy() {
+        Log_OC.v(TAG, "Destroying service")
+        mBinder = null
+        mServiceHandler = null
+        mServiceLooper!!.quit()
+        mServiceLooper = null
+        notificationManager = null
+        notification = null
+        notificationBuilder = null
+
+        // remove AccountsUpdatedListener
+        val am = AccountManager.get(applicationContext)
+        am.removeOnAccountsUpdatedListener(this)
+        super.onDestroy()
+    }
+
+    /**
+     * Entry point to add one or several files to the queue of downloads.
+     *
+     * New downloads are added calling to startService(), resulting in a call to this method.
+     * This ensures the service will keep on working although the caller activity goes away.
+     */
+    @Suppress("LongParameterList")
+    override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
+        Log_OC.d(TAG, "Starting command with id $startId")
+
+        startForeground(FOREGROUND_SERVICE_ID, notification)
+
+        if (!intent.hasExtra(EXTRA_USER) || !intent.hasExtra(EXTRA_FILE)) {
+            Log_OC.e(TAG, "Not enough information provided in intent")
+            return START_NOT_STICKY
+        }
+
+        val user = intent.getParcelableExtra<User>(EXTRA_USER)
+        val file = intent.getParcelableExtra<OCFile>(EXTRA_FILE)
+        val behaviour = intent.getStringExtra(OCFileListFragment.DOWNLOAD_BEHAVIOUR)
+        var downloadType: DownloadType? = DownloadType.DOWNLOAD
+        if (intent.hasExtra(DOWNLOAD_TYPE)) {
+            downloadType = intent.getSerializableExtra(DOWNLOAD_TYPE) as DownloadType?
+        }
+        val activityName = intent.getStringExtra(SendShareDialog.ACTIVITY_NAME)
+        val packageName = intent.getStringExtra(SendShareDialog.PACKAGE_NAME)
+        conflictUploadId = intent.getLongExtra(ConflictsResolveActivity.EXTRA_CONFLICT_UPLOAD_ID, -1)
+
+        val requestedDownloads = handleDownloadRequest(user, file, behaviour, downloadType, activityName, packageName)
+
+        if (requestedDownloads.isNotEmpty()) {
+            val msg = mServiceHandler?.obtainMessage()
+            msg?.arg1 = startId
+            msg?.obj = requestedDownloads
+            msg?.let {
+                mServiceHandler?.sendMessage(it)
+            }
+        }
+
+        return START_NOT_STICKY
+    }
+
+    @Suppress("LongParameterList")
+    private fun handleDownloadRequest(
+        user: User?,
+        file: OCFile?,
+        behaviour: String?,
+        downloadType: DownloadType?,
+        activityName: String?,
+        packageName: String?
+    ): List<String> {
+        val requestedDownloads: MutableList<String> = ArrayList()
+
+        if (user == null || file == null) {
+            return requestedDownloads
+        }
+
+        try {
+            val newDownload = DownloadFileOperation(
+                user,
+                file,
+                behaviour,
+                activityName,
+                packageName,
+                baseContext,
+                downloadType
+            )
+            newDownload.addDatatransferProgressListener(this)
+            newDownload.addDatatransferProgressListener(mBinder as FileDownloaderBinder?)
+
+            val putResult = mPendingDownloads.putIfAbsent(user.accountName, file.remotePath, newDownload)
+
+            if (putResult != null) {
+                val downloadKey = putResult.first
+                requestedDownloads.add(downloadKey)
+                sendBroadcastNewDownload(newDownload, putResult.second)
+            }
+        } catch (e: IllegalArgumentException) {
+            Log_OC.e(TAG, "Not enough information provided in intent: " + e.message)
+        }
+
+        return requestedDownloads
+    }
+
+    /**
+     * Provides a binder object that clients can use to perform operations on the queue of downloads,
+     * excepting the addition of new files.
+     *
+     * Implemented to perform cancellation, pause and resume of existing downloads.
+     */
+    override fun onBind(intent: Intent): IBinder? {
+        return mBinder
+    }
+
+    /**
+     * Called when ALL the bound clients were onbound.
+     */
+    override fun onUnbind(intent: Intent): Boolean {
+        (mBinder as FileDownloaderBinder?)!!.clearListeners()
+        return false // not accepting rebinding (default behaviour)
+    }
+
+    override fun onAccountsUpdated(accounts: Array<Account>) {
+        // review the current download and cancel it if its account doesn't exist
+        if (mCurrentDownload != null && !accountManager!!.exists(mCurrentDownload!!.user.toPlatformAccount())) {
+            mCurrentDownload!!.cancel()
+        }
+        // The rest of downloads are cancelled when they try to start
+    }
+
+    /**
+     * Binder to let client components to perform operations on the queue of downloads.
+     *
+     *
+     * It provides by itself the available operations.
+     */
+    inner class FileDownloaderBinder : Binder(), OnDatatransferProgressListener {
+        /**
+         * Map of listeners that will be reported about progress of downloads from a
+         * [FileDownloaderBinder]
+         * instance.
+         */
+        private val mBoundListeners: MutableMap<Long, OnDatatransferProgressListener> = HashMap()
+
+        /**
+         * Cancels a pending or current download of a remote file.
+         *
+         * @param account ownCloud account where the remote file is stored.
+         * @param file    A file in the queue of pending downloads
+         */
+        @Suppress("ComplexMethod")
+        fun cancel(account: Account, file: OCFile) {
+            val removeResult = mPendingDownloads.remove(account.name, file.remotePath)
+            val download = removeResult.first
+
+            if (download != null) {
+                download.cancel()
+            } else {
+                mCurrentDownload?.takeIf {
+                    it.remotePath.startsWith(file.remotePath) && account.name == currentUser?.get()?.accountName
+                }?.cancel()
+            }
+        }
+
+        /**
+         * Cancels all the downloads for an account
+         */
+        fun cancel(accountName: String?) {
+            if (mCurrentDownload != null && mCurrentDownload!!.user.nameEquals(accountName)) {
+                mCurrentDownload!!.cancel()
+            }
+            // Cancel pending downloads
+            cancelPendingDownloads(accountName)
+        }
+
+        fun clearListeners() {
+            mBoundListeners.clear()
+        }
+
+        /**
+         * Returns True when the file described by 'file' in the ownCloud account 'account'
+         * is downloading or waiting to download.
+         *
+         * If 'file' is a directory, returns 'true' if any of its descendant files is downloading or
+         * waiting to download.
+         *
+         * @param user    user where the remote file is stored.
+         * @param file    A file that could be in the queue of downloads.
+         */
+        fun isDownloading(user: User?, file: OCFile?): Boolean {
+            return user != null && file != null && mPendingDownloads.contains(user.accountName, file.remotePath)
+        }
+
+        /**
+         * Adds a listener interested in the progress of the download for a concrete file.
+         *
+         * @param listener Object to notify about progress of transfer.
+         * @param file     [OCFile] of interest for listener.
+         */
+        fun addDataTransferProgressListener(listener: OnDatatransferProgressListener?, file: OCFile?) {
+            if (file == null || listener == null) {
+                return
+            }
+            mBoundListeners[file.fileId] = listener
+        }
+
+        /**
+         * Removes a listener interested in the progress of the download for a concrete file.
+         *
+         * @param listener      Object to notify about progress of transfer.
+         * @param file          [OCFile] of interest for listener.
+         */
+        fun removeDataTransferProgressListener(listener: OnDatatransferProgressListener?, file: OCFile?) {
+            if (file == null || listener == null) {
+                return
+            }
+            val fileId = file.fileId
+            if (mBoundListeners[fileId] === listener) {
+                mBoundListeners.remove(fileId)
+            }
+        }
+
+        override fun onTransferProgress(
+            progressRate: Long,
+            totalTransferredSoFar: Long,
+            totalToTransfer: Long,
+            fileName: String
+        ) {
+            val boundListener = mBoundListeners[mCurrentDownload!!.file.fileId]
+            boundListener?.onTransferProgress(
+                progressRate,
+                totalTransferredSoFar,
+                totalToTransfer,
+                fileName
+            )
+        }
+    }
+
+    /**
+     * Download worker. Performs the pending downloads in the order they were requested.
+     *
+     * Created with the Looper of a new thread, started in [FileUploader.onCreate].
+     */
+    private class ServiceHandler(looper: Looper?, service: FileDownloader?) : Handler(looper!!) {
+        // don't make it a final class, and don't remove the static ; lint will warn about a
+        // possible memory leak
+        var mService: FileDownloader
+
+        init {
+            requireNotNull(service) { "Received invalid NULL in parameter 'service'" }
+            mService = service
+        }
+
+        @Suppress("MagicNumber")
+        override fun handleMessage(msg: Message) {
+            val requestedDownloads = msg.obj as AbstractList<String>
+            if (msg.obj != null) {
+                val it: Iterator<String> = requestedDownloads.iterator()
+                while (it.hasNext()) {
+                    val next = it.next()
+                    mService.downloadFile(next)
+                }
+            }
+            mService.mStartedDownload = false
+
+            Handler(Looper.getMainLooper()).postDelayed({
+                if (!mService.mStartedDownload) {
+                    mService.notificationManager!!.cancel(R.string.downloader_download_in_progress_ticker)
+                }
+                Log_OC.d(TAG, "Stopping after command with id " + msg.arg1)
+                mService.notificationManager!!.cancel(FOREGROUND_SERVICE_ID)
+                mService.stopForeground(true)
+                mService.stopSelf(msg.arg1)
+            }, 2000)
+        }
+    }
+
+    /**
+     * Core download method: requests a file to download and stores it.
+     *
+     * @param downloadKey Key to access the download to perform, contained in mPendingDownloads
+     */
+    @Suppress("NestedBlockDepth", "TooGenericExceptionCaught")
+    private fun downloadFile(downloadKey: String) {
+        mStartedDownload = true
+        mCurrentDownload = mPendingDownloads[downloadKey]
+
+        if (mCurrentDownload != null) {
+            val isAccountExist = accountManager?.exists(mCurrentDownload!!.user.toPlatformAccount())
+
+            if (isAccountExist == true) {
+                notifyDownloadStart(mCurrentDownload!!)
+                var downloadResult: RemoteOperationResult<*>? = null
+                try {
+                    // / prepare client object to send the request to the ownCloud server
+                    val currentDownloadAccount = mCurrentDownload!!.user.toPlatformAccount()
+                    val currentDownloadUser = accountManager!!.getUser(currentDownloadAccount.name)
+                    if (currentUser != currentDownloadUser) {
+                        currentUser = currentDownloadUser
+                        mStorageManager = FileDataStorageManager(currentUser.get(), contentResolver)
+                    } // else, reuse storage manager from previous operation
+
+                    // always get client from client manager, to get fresh credentials in case
+                    // of update
+                    val ocAccount = currentDownloadUser.get().toOwnCloudAccount()
+                    mDownloadClient = OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(ocAccount, this)
+
+                    // / perform the download
+                    downloadResult = mCurrentDownload!!.execute(mDownloadClient)
+                    if (downloadResult.isSuccess && mCurrentDownload!!.downloadType === DownloadType.DOWNLOAD) {
+                        saveDownloadedFile()
+                    }
+                } catch (e: Exception) {
+                    Log_OC.e(TAG, "Error downloading", e)
+                    downloadResult = RemoteOperationResult<Any?>(e)
+                } finally {
+                    val removeResult = mPendingDownloads.removePayload(
+                        mCurrentDownload!!.user.accountName,
+                        mCurrentDownload!!.remotePath
+                    )
+                    if (downloadResult == null) {
+                        downloadResult = RemoteOperationResult<Any?>(RuntimeException("Error downloading…"))
+                    }
+
+                    // / notify result
+                    notifyDownloadResult(mCurrentDownload!!, downloadResult)
+                    sendBroadcastDownloadFinished(mCurrentDownload!!, downloadResult, removeResult.second)
+                }
+            } else {
+                cancelPendingDownloads(mCurrentDownload!!.user.accountName)
+            }
+        }
+    }
+
+    /**
+     * Updates the OC File after a successful download.
+     *
+     * TODO move to DownloadFileOperation
+     * unify with code from [DocumentsStorageProvider] and [DownloadTask].
+     */
+    private fun saveDownloadedFile() {
+        var file = mStorageManager?.getFileById(mCurrentDownload!!.file.fileId)
+        if (file == null) {
+            // try to get file via path, needed for overwriting existing files on conflict dialog
+            file = mStorageManager?.getFileByDecryptedRemotePath(mCurrentDownload!!.file.remotePath)
+        }
+        if (file == null) {
+            Log_OC.e(this, "Could not save " + mCurrentDownload!!.file.remotePath)
+            return
+        }
+
+        val syncDate = System.currentTimeMillis()
+        file.lastSyncDateForProperties = syncDate
+        file.lastSyncDateForData = syncDate
+        file.isUpdateThumbnailNeeded = true
+        file.modificationTimestamp = mCurrentDownload!!.modificationTimestamp
+        file.modificationTimestampAtLastSyncForData = mCurrentDownload!!.modificationTimestamp
+        file.etag = mCurrentDownload!!.etag
+        file.mimeType = mCurrentDownload!!.mimeType
+        file.storagePath = mCurrentDownload!!.savePath
+        file.fileLength = File(mCurrentDownload!!.savePath).length()
+        file.remoteId = mCurrentDownload!!.file.remoteId
+        mStorageManager!!.saveFile(file)
+        if (MimeTypeUtil.isMedia(mCurrentDownload!!.mimeType)) {
+            FileDataStorageManager.triggerMediaScan(file.storagePath, file)
+        }
+        mStorageManager!!.saveConflict(file, null)
+    }
+
+    /**
+     * Creates a status notification to show the download progress
+     *
+     * @param download Download operation starting.
+     */
+    @Suppress("MagicNumber")
+    private fun notifyDownloadStart(download: DownloadFileOperation) {
+        val fileName = download.file.getFileNameWithExtension(10)
+        val titlePrefix = getString(R.string.file_downloader_notification_title_prefix)
+        val title = titlePrefix + fileName
+
+        // / update status notification with a progress bar
+        mLastPercent = 0
+        notificationBuilder
+            ?.setContentTitle(title)
+            ?.setTicker(title)
+            ?.setProgress(100, 0, download.size < 0)
+
+        // / includes a pending intent in the notification showing the details view of the file
+        val showDetailsIntent: Intent = if (PreviewImageFragment.canBePreviewed(download.file)) {
+            Intent(this, PreviewImageActivity::class.java)
+        } else {
+            Intent(this, FileDisplayActivity::class.java)
+        }
+
+        showDetailsIntent.putExtra(FileActivity.EXTRA_FILE, download.file)
+        showDetailsIntent.putExtra(FileActivity.EXTRA_USER, download.user)
+        showDetailsIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
+        notificationBuilder?.setContentIntent(
+            PendingIntent.getActivity(
+                this,
+                System.currentTimeMillis().toInt(),
+                showDetailsIntent,
+                PendingIntent.FLAG_IMMUTABLE
+            )
+        )
+        initNotificationManager()
+        notifyNotificationManager()
+    }
+
+    /**
+     * Callback method to update the progress bar in the status notification.
+     */
+    @Suppress("MagicNumber")
+    override fun onTransferProgress(
+        progressRate: Long,
+        totalTransferredSoFar: Long,
+        totalToTransfer: Long,
+        filePath: String
+    ) {
+        val percent = (100.0 * totalTransferredSoFar.toDouble() / totalToTransfer.toDouble()).toInt()
+        if (percent != mLastPercent) {
+            notificationBuilder?.setProgress(100, percent, totalToTransfer < 0)
+            initNotificationManager()
+            notifyNotificationManager()
+        }
+        mLastPercent = percent
+    }
+
+    /**
+     * Updates the status notification with the result of a download operation.
+     *
+     * @param downloadResult Result of the download operation.
+     * @param download       Finished download operation
+     */
+    @SuppressFBWarnings("DMI")
+    @Suppress("MagicNumber")
+    private fun notifyDownloadResult(
+        download: DownloadFileOperation,
+        downloadResult: RemoteOperationResult<*>
+    ) {
+        initNotificationManager()
+        if (!downloadResult.isCancelled) {
+            if (downloadResult.isSuccess) {
+                if (conflictUploadId > 0) {
+                    uploadsStorageManager!!.removeUpload(conflictUploadId)
+                }
+                // Don't show notification except an error has occurred.
+                return
+            }
+
+            var tickerId = if (downloadResult.isSuccess) {
+                R.string.downloader_download_succeeded_ticker
+            } else {
+                R.string.downloader_download_failed_ticker
+            }
+
+            val needsToUpdateCredentials = ResultCode.UNAUTHORIZED == downloadResult.code
+
+            tickerId = if (needsToUpdateCredentials) {
+                R.string.downloader_download_failed_credentials_error
+            } else {
+                tickerId
+            }
+
+            notificationBuilder
+                ?.setSmallIcon(R.drawable.notification_icon)
+                ?.setTicker(getString(tickerId))
+                ?.setAutoCancel(true)
+                ?.setOngoing(false)
+                ?.setProgress(0, 0, false)
+
+            if (needsToUpdateCredentials) {
+                configureUpdateCredentialsNotification(download.user)
+            } else {
+                // TODO put something smart in showDetailsIntent
+                val showDetailsIntent = Intent()
+                notificationBuilder?.setContentIntent(
+                    PendingIntent.getActivity(
+                        this,
+                        System.currentTimeMillis().toInt(),
+                        showDetailsIntent,
+                        PendingIntent.FLAG_IMMUTABLE
+                    )
+                )
+            }
+            notificationBuilder?.setContentText(
+                ErrorMessageAdapter.getErrorCauseMessage(
+                    downloadResult,
+                    download,
+                    resources
+                )
+            )
+            if (notificationManager != null) {
+                notificationManager?.notify(SecureRandom().nextInt(), notificationBuilder?.build())
+
+                // Remove success notification
+                if (downloadResult.isSuccess) {
+                    // Sleep 2 seconds, so show the notification before remove it
+                    NotificationUtils.cancelWithDelay(
+                        notificationManager,
+                        R.string.downloader_download_succeeded_ticker,
+                        2000
+                    )
+                }
+            }
+        }
+    }
+
+    private fun configureUpdateCredentialsNotification(user: User) {
+        // let the user update credentials with one click
+        val updateAccountCredentials = Intent(this, AuthenticatorActivity::class.java)
+        updateAccountCredentials.putExtra(AuthenticatorActivity.EXTRA_ACCOUNT, user.toPlatformAccount())
+        updateAccountCredentials.putExtra(
+            AuthenticatorActivity.EXTRA_ACTION,
+            AuthenticatorActivity.ACTION_UPDATE_EXPIRED_TOKEN
+        )
+        updateAccountCredentials.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+        updateAccountCredentials.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
+        updateAccountCredentials.addFlags(Intent.FLAG_FROM_BACKGROUND)
+        notificationBuilder!!.setContentIntent(
+            PendingIntent.getActivity(
+                this,
+                System.currentTimeMillis().toInt(),
+                updateAccountCredentials,
+                PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
+            )
+        )
+    }
+
+    /**
+     * Sends a broadcast when a download finishes in order to the interested activities can
+     * update their view
+     *
+     * @param download               Finished download operation
+     * @param downloadResult         Result of the download operation
+     * @param unlinkedFromRemotePath Path in the downloads tree where the download was unlinked from
+     */
+    private fun sendBroadcastDownloadFinished(
+        download: DownloadFileOperation,
+        downloadResult: RemoteOperationResult<*>,
+        unlinkedFromRemotePath: String?
+    ) {
+        val end = Intent(downloadFinishMessage)
+        end.putExtra(EXTRA_DOWNLOAD_RESULT, downloadResult.isSuccess)
+        end.putExtra(ACCOUNT_NAME, download.user.accountName)
+        end.putExtra(EXTRA_REMOTE_PATH, download.remotePath)
+        end.putExtra(OCFileListFragment.DOWNLOAD_BEHAVIOUR, download.behaviour)
+        end.putExtra(SendShareDialog.ACTIVITY_NAME, download.activityName)
+        end.putExtra(SendShareDialog.PACKAGE_NAME, download.packageName)
+        if (unlinkedFromRemotePath != null) {
+            end.putExtra(EXTRA_LINKED_TO_PATH, unlinkedFromRemotePath)
+        }
+        end.setPackage(packageName)
+        localBroadcastManager!!.sendBroadcast(end)
+    }
+
+    /**
+     * Sends a broadcast when a new download is added to the queue.
+     *
+     * @param download           Added download operation
+     * @param linkedToRemotePath Path in the downloads tree where the download was linked to
+     */
+    private fun sendBroadcastNewDownload(
+        download: DownloadFileOperation,
+        linkedToRemotePath: String
+    ) {
+        val added = Intent(downloadAddedMessage)
+        added.putExtra(ACCOUNT_NAME, download.user.accountName)
+        added.putExtra(EXTRA_REMOTE_PATH, download.remotePath)
+        added.putExtra(EXTRA_LINKED_TO_PATH, linkedToRemotePath)
+        added.setPackage(packageName)
+        localBroadcastManager!!.sendBroadcast(added)
+    }
+
+    private fun cancelPendingDownloads(accountName: String?) {
+        mPendingDownloads.remove(accountName)
+    }
+
+    companion object {
+        const val EXTRA_USER = "USER"
+        const val EXTRA_FILE = "FILE"
+        private const val DOWNLOAD_ADDED_MESSAGE = "DOWNLOAD_ADDED"
+        private const val DOWNLOAD_FINISH_MESSAGE = "DOWNLOAD_FINISH"
+        const val EXTRA_DOWNLOAD_RESULT = "RESULT"
+        const val EXTRA_REMOTE_PATH = "REMOTE_PATH"
+        const val EXTRA_LINKED_TO_PATH = "LINKED_TO"
+        const val ACCOUNT_NAME = "ACCOUNT_NAME"
+        const val DOWNLOAD_TYPE = "DOWNLOAD_TYPE"
+        private const val FOREGROUND_SERVICE_ID = 412
+        private val TAG = FileDownloader::class.java.simpleName
+
+        @JvmStatic
+        val downloadAddedMessage: String
+            get() = FileDownloader::class.java.name + DOWNLOAD_ADDED_MESSAGE
+
+        @JvmStatic
+        val downloadFinishMessage: String
+            get() = FileDownloader::class.java.name + DOWNLOAD_FINISH_MESSAGE
+    }
+}

+ 4 - 52
app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java

@@ -204,8 +204,7 @@ public class FileDisplayActivity extends FileActivity
 
     public static final int REQUEST_CODE__SELECT_CONTENT_FROM_APPS = REQUEST_CODE__LAST_SHARED + 1;
     public static final int REQUEST_CODE__SELECT_FILES_FROM_FILE_SYSTEM = REQUEST_CODE__LAST_SHARED + 2;
-    public static final int REQUEST_CODE__MOVE_FILES = REQUEST_CODE__LAST_SHARED + 3;
-    public static final int REQUEST_CODE__COPY_FILES = REQUEST_CODE__LAST_SHARED + 4;
+    public static final int REQUEST_CODE__MOVE_OR_COPY_FILES = REQUEST_CODE__LAST_SHARED + 3;
     public static final int REQUEST_CODE__UPLOAD_FROM_CAMERA = REQUEST_CODE__LAST_SHARED + 5;
     public static final int REQUEST_CODE__UPLOAD_SCAN_DOC_FROM_CAMERA = REQUEST_CODE__LAST_SHARED + 6;
 
@@ -437,12 +436,10 @@ public class FileDisplayActivity extends FileActivity
 
     private void checkOutdatedServer() {
         Optional<User> user = getUser();
-        OwnCloudVersion serverVersion = user.get().getServer().getVersion();
-
         // show outdated warning
         if (user.isPresent() &&
             CapabilityUtils.checkOutdatedWarning(getResources(),
-                                                 serverVersion,
+                                                 user.get().getServer().getVersion(),
                                                  getCapabilities().getExtendedSupport().isTrue())) {
             DisplayUtils.showServerOutdatedSnackbar(this, Snackbar.LENGTH_LONG);
         }
@@ -886,32 +883,9 @@ public class FileDisplayActivity extends FileActivity
                                                            FileUploader.LOCAL_BEHAVIOUR_DELETE);
                     }
                 }
-            }, new String[]{FileOperationsHelper.createImageFile(getActivity()).getAbsolutePath()}).execute();
-        } else if (requestCode == REQUEST_CODE__MOVE_FILES && resultCode == RESULT_OK) {
-            exitSelectionMode();
-            final Intent fData = data;
-            getHandler().postDelayed(
-                new Runnable() {
-                    @Override
-                    public void run() {
-                        requestMoveOperation(fData);
-                    }
-                },
-                DELAY_TO_REQUEST_OPERATIONS_LATER
-                                    );
-
-        } else if (requestCode == REQUEST_CODE__COPY_FILES && resultCode == RESULT_OK) {
+            }, new String[] { FileOperationsHelper.createImageFile(getActivity()).getAbsolutePath() }).execute();
+        } else if (requestCode == REQUEST_CODE__MOVE_OR_COPY_FILES && resultCode == RESULT_OK) {
             exitSelectionMode();
-            final Intent fData = data;
-            getHandler().postDelayed(
-                new Runnable() {
-                    @Override
-                    public void run() {
-                        requestCopyOperation(fData);
-                    }
-                },
-                DELAY_TO_REQUEST_OPERATIONS_LATER
-                                    );
         } else if (requestCode == PermissionUtil.REQUEST_CODE_MANAGE_ALL_FILES) {
             syncAndUpdateFolder(true);
         } else {
@@ -1018,28 +992,6 @@ public class FileDisplayActivity extends FileActivity
 
     }
 
-    /**
-     * Request the operation for moving the file/folder from one path to another
-     *
-     * @param data Intent received
-     */
-    private void requestMoveOperation(Intent data) {
-        final OCFile folderToMoveAt = data.getParcelableExtra(FolderPickerActivity.EXTRA_FOLDER);
-        final List<String> filePaths = data.getStringArrayListExtra(FolderPickerActivity.EXTRA_FILE_PATHS);
-        getFileOperationsHelper().moveFiles(filePaths, folderToMoveAt);
-    }
-
-    /**
-     * Request the operation for copying the file/folder from one path to another
-     *
-     * @param data Intent received
-     */
-    private void requestCopyOperation(Intent data) {
-        final OCFile targetFolder = data.getParcelableExtra(FolderPickerActivity.EXTRA_FOLDER);
-        final List<String> filePaths = data.getStringArrayListExtra(FolderPickerActivity.EXTRA_FILE_PATHS);
-        getFileOperationsHelper().copyFiles(filePaths, targetFolder);
-    }
-
     private boolean isSearchOpen() {
         if (searchView == null) {
             return false;

+ 54 - 45
app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt

@@ -44,6 +44,7 @@ import com.owncloud.android.lib.common.utils.Log_OC
 import com.owncloud.android.lib.resources.files.SearchRemoteOperation
 import com.owncloud.android.operations.CreateFolderOperation
 import com.owncloud.android.operations.RefreshFolderOperation
+import com.owncloud.android.services.OperationsService
 import com.owncloud.android.syncadapter.FileSyncAdapter
 import com.owncloud.android.ui.dialog.CreateFolderDialogFragment
 import com.owncloud.android.ui.dialog.SortingOrderDialogFragment.OnSortingOrderListener
@@ -73,7 +74,9 @@ open class FolderPickerActivity :
     var isDoNotEnterEncryptedFolder = false
         private set
     private var mCancelBtn: MaterialButton? = null
-    private var mChooseBtn: MaterialButton? = null
+    private var mCopyBtn: MaterialButton? = null
+    private var mMoveBtn: MaterialButton? = null
+
     private var caption: String? = null
 
     private var mAction: String? = null
@@ -85,6 +88,7 @@ open class FolderPickerActivity :
     override fun onCreate(savedInstanceState: Bundle?) {
         Log_OC.d(TAG, "onCreate() start")
         super.onCreate(savedInstanceState)
+
         if (this is FilePickerActivity) {
             setContentView(R.layout.files_picker)
         } else {
@@ -101,29 +105,15 @@ open class FolderPickerActivity :
         findViewById<View>(R.id.switch_grid_view_button).visibility =
             View.GONE
         mAction = intent.getStringExtra(EXTRA_ACTION)
+
         if (mAction != null) {
-            when (mAction) {
-                MOVE -> {
-                    caption = resources.getText(R.string.move_to).toString()
-                    mSearchOnlyFolders = true
-                    isDoNotEnterEncryptedFolder = true
-                }
-                COPY -> {
-                    caption = resources.getText(R.string.copy_to).toString()
-                    mSearchOnlyFolders = true
-                    isDoNotEnterEncryptedFolder = true
-                }
-                CHOOSE_LOCATION -> {
-                    caption = resources.getText(R.string.choose_location).toString()
-                    mSearchOnlyFolders = true
-                    isDoNotEnterEncryptedFolder = true
-                    mChooseBtn!!.text = resources.getString(R.string.common_select)
-                }
-                else -> caption = themeUtils.getDefaultDisplayNameForRootFolder(this)
-            }
+            caption = resources.getText(R.string.folder_picker_choose_caption_text).toString()
+            mSearchOnlyFolders = true
+            isDoNotEnterEncryptedFolder = true
         } else {
             caption = themeUtils.getDefaultDisplayNameForRootFolder(this)
         }
+
         mTargetFilePaths = intent.getStringArrayListExtra(EXTRA_FILE_PATHS)
 
         if (savedInstanceState == null) {
@@ -351,13 +341,14 @@ open class FolderPickerActivity :
     }
 
     private fun toggleChooseEnabled() {
-        mChooseBtn?.isEnabled = checkFolderSelectable()
+        mCopyBtn?.isEnabled = checkFolderSelectable()
+        mMoveBtn?.isEnabled = checkFolderSelectable()
     }
 
     // for copy and move, disable selecting parent folder of target files
     private fun checkFolderSelectable(): Boolean {
         return when {
-            mAction != COPY && mAction != MOVE -> true
+            mAction != MOVE_OR_COPY -> true
             mTargetFilePaths.isNullOrEmpty() -> true
             file?.isFolder != true -> true
             // all of the target files are already in the selected directory
@@ -385,38 +376,57 @@ open class FolderPickerActivity :
      */
     private fun initControls() {
         mCancelBtn = findViewById(R.id.folder_picker_btn_cancel)
-        mChooseBtn = findViewById(R.id.folder_picker_btn_choose)
-        if (mChooseBtn != null) {
-            viewThemeUtils.material.colorMaterialButtonPrimaryFilled(mChooseBtn!!)
-            mChooseBtn!!.setOnClickListener(this)
+        mCopyBtn = findViewById(R.id.folder_picker_btn_copy)
+        mMoveBtn = findViewById(R.id.folder_picker_btn_move)
+
+        if (mCopyBtn != null) {
+            viewThemeUtils.material.colorMaterialButtonPrimaryFilled(mCopyBtn!!)
+            mCopyBtn!!.setOnClickListener(this)
+        }
+        if (mMoveBtn != null) {
+            viewThemeUtils.material.colorMaterialButtonPrimaryTonal(mMoveBtn!!)
+            mMoveBtn!!.setOnClickListener(this)
         }
+
         if (mCancelBtn != null) {
             if (this is FilePickerActivity) {
                 viewThemeUtils.material.colorMaterialButtonPrimaryFilled(mCancelBtn!!)
             } else {
-                viewThemeUtils.material.colorMaterialButtonPrimaryOutlined(mCancelBtn!!)
+                viewThemeUtils.material.colorMaterialButtonText(mCancelBtn!!)
             }
             mCancelBtn!!.setOnClickListener(this)
         }
     }
 
     override fun onClick(v: View) {
-        if (v == mCancelBtn) {
-            finish()
-        } else if (v == mChooseBtn) {
-            val i = intent
-            val resultData = Intent()
-            resultData.putExtra(EXTRA_FOLDER, listOfFilesFragment!!.currentFile)
-            val targetFiles = i.getParcelableArrayListExtra<Parcelable>(EXTRA_FILES)
-            if (targetFiles != null) {
-                resultData.putParcelableArrayListExtra(EXTRA_FILES, targetFiles)
-            }
-            mTargetFilePaths.let {
-                resultData.putStringArrayListExtra(EXTRA_FILE_PATHS, it)
+        when (v) {
+            mCancelBtn -> finish()
+            mCopyBtn, mMoveBtn -> copyOrMove(v)
+        }
+    }
+
+    private fun copyOrMove(v: View) {
+        val i = intent
+        val resultData = Intent()
+        resultData.putExtra(EXTRA_FOLDER, listOfFilesFragment?.currentFile)
+
+        i.getParcelableArrayListExtra<Parcelable>(EXTRA_FILES)?.let { targetFiles ->
+            resultData.putParcelableArrayListExtra(EXTRA_FILES, targetFiles)
+        }
+
+        mTargetFilePaths?.let {
+            val action = when (v) {
+                mCopyBtn -> OperationsService.ACTION_COPY_FILE
+                mMoveBtn -> OperationsService.ACTION_MOVE_FILE
+                else -> throw IllegalArgumentException("Unknown operation")
             }
-            setResult(RESULT_OK, resultData)
-            finish()
+
+            fileOperationsHelper.moveOrCopyFiles(action, it, file)
+            resultData.putStringArrayListExtra(EXTRA_FILE_PATHS, it)
         }
+
+        setResult(RESULT_OK, resultData)
+        finish()
     }
 
     override fun onRemoteOperationFinish(operation: RemoteOperation<*>?, result: RemoteOperationResult<*>) {
@@ -571,8 +581,8 @@ open class FolderPickerActivity :
         }
     }
 
-    override fun onSortingOrderChosen(selection: FileSortOrder) {
-        listOfFilesFragment!!.sortFiles(selection)
+    override fun onSortingOrderChosen(selection: FileSortOrder?) {
+        listOfFilesFragment?.sortFiles(selection)
     }
 
     companion object {
@@ -592,8 +602,7 @@ open class FolderPickerActivity :
         @JvmField
         val EXTRA_ACTION = FolderPickerActivity::class.java.canonicalName?.plus(".EXTRA_ACTION")
 
-        const val MOVE = "MOVE"
-        const val COPY = "COPY"
+        const val MOVE_OR_COPY = "MOVE_OR_COPY"
         const val CHOOSE_LOCATION = "CHOOSE_LOCATION"
         private val TAG = FolderPickerActivity::class.java.simpleName
         protected const val TAG_LIST_OF_FOLDERS = "LIST_OF_FOLDERS"

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

@@ -643,7 +643,11 @@ class SyncedFoldersActivity :
         }
     }
 
-    override fun onSaveSyncedFolderPreference(syncedFolder: SyncedFolderParcelable) {
+    override fun onSaveSyncedFolderPreference(syncedFolder: SyncedFolderParcelable?) {
+        if (syncedFolder == null) {
+            return
+        }
+
         // custom folders newly created aren't in the list already,
         // so triggering a refresh
         if (MediaFolderType.CUSTOM == syncedFolder.type && syncedFolder.id == SyncedFolder.UNPERSISTED_ID) {
@@ -730,7 +734,11 @@ class SyncedFoldersActivity :
         syncedFolderPreferencesDialogFragment = null
     }
 
-    override fun onDeleteSyncedFolderPreference(syncedFolder: SyncedFolderParcelable) {
+    override fun onDeleteSyncedFolderPreference(syncedFolder: SyncedFolderParcelable?) {
+        if (syncedFolder == null) {
+            return
+        }
+
         syncedFolderProvider.deleteSyncedFolder(syncedFolder.id)
         adapter.removeItem(syncedFolder.section)
     }

+ 1 - 3
app/src/main/java/com/owncloud/android/ui/activity/UploadFilesActivity.java

@@ -528,9 +528,7 @@ public class UploadFilesActivity extends DrawerActivity implements LocalFileList
             // to the ownCloud folder instead of copying
             String[] args = {getString(R.string.app_name)};
             ConfirmationDialogFragment dialog = ConfirmationDialogFragment.newInstance(
-                R.string.upload_query_move_foreign_files, args, 0, R.string.common_yes, -1,
-                R.string.common_no
-                                                                                      );
+                R.string.upload_query_move_foreign_files, args, 0, R.string.common_yes,  R.string.common_no, -1);
             dialog.setOnConfirmationListener(this);
             dialog.show(getSupportFragmentManager(), QUERY_TO_MOVE_DIALOG_TAG);
         }

+ 0 - 89
app/src/main/java/com/owncloud/android/ui/adapter/StoragePathAdapter.java

@@ -1,89 +0,0 @@
-/*
- * Nextcloud Android client application
- *
- * @author Andy Scherzinger
- * Copyright (C) 2019 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.ui.adapter;
-
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import com.owncloud.android.databinding.StoragePathItemBinding;
-
-import java.util.List;
-
-import androidx.annotation.NonNull;
-import androidx.recyclerview.widget.RecyclerView;
-
-public class StoragePathAdapter extends RecyclerView.Adapter<StoragePathAdapter.StoragePathViewHolder> {
-    private List<StoragePathItem> pathList;
-    private StoragePathAdapterListener storagePathAdapterListener;
-
-    public StoragePathAdapter(List<StoragePathItem> pathList, StoragePathAdapterListener storagePathAdapterListener) {
-        this.pathList = pathList;
-        this.storagePathAdapterListener = storagePathAdapterListener;
-    }
-
-    @NonNull
-    @Override
-    public StoragePathViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
-        return new StoragePathAdapter.StoragePathViewHolder(
-            StoragePathItemBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)
-        );
-    }
-
-    @Override
-    public void onBindViewHolder(@NonNull StoragePathViewHolder holder, int position) {
-        if (pathList != null && pathList.size() > position) {
-            StoragePathItem storagePathItem = pathList.get(position);
-
-            holder.binding.icon.setImageResource(storagePathItem.getIcon());
-            holder.binding.name.setText(storagePathItem.getName());
-        }
-    }
-
-    @Override
-    public int getItemCount() {
-        return pathList.size();
-    }
-
-    public interface StoragePathAdapterListener {
-        /**
-         * sets the chosen path.
-         *
-         * @param path chosen path
-         */
-        void chosenPath(String path);
-    }
-
-    class StoragePathViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
-        StoragePathItemBinding binding;
-
-        public StoragePathViewHolder(StoragePathItemBinding binding) {
-            super(binding.getRoot());
-            this.binding = binding;
-            this.binding.getRoot().setOnClickListener(this);
-        }
-
-        @Override
-        public void onClick(View view) {
-            storagePathAdapterListener.chosenPath(pathList.get(getAdapterPosition()).getPath());
-        }
-    }
-}

+ 80 - 0
app/src/main/java/com/owncloud/android/ui/adapter/StoragePathAdapter.kt

@@ -0,0 +1,80 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Andy Scherzinger
+ * Copyright (C) 2019 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.ui.adapter
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import com.owncloud.android.databinding.StoragePathItemBinding
+import com.owncloud.android.ui.adapter.StoragePathAdapter.StoragePathViewHolder
+import com.owncloud.android.utils.theme.ViewThemeUtils
+
+class StoragePathAdapter(
+    private val pathList: List<StoragePathItem>?,
+    private val storagePathAdapterListener: StoragePathAdapterListener,
+    private val viewThemeUtils: ViewThemeUtils
+) : RecyclerView.Adapter<StoragePathViewHolder>() {
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StoragePathViewHolder {
+        return StoragePathViewHolder(
+            StoragePathItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+        )
+    }
+
+    override fun onBindViewHolder(holder: StoragePathViewHolder, position: Int) {
+        if (pathList != null && pathList.size > position) {
+            val storagePathItem = pathList[position]
+            holder.binding.btnStoragePath.setIconResource(storagePathItem.icon)
+            holder.binding.btnStoragePath.text = storagePathItem.name
+            viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(holder.binding.btnStoragePath)
+        }
+    }
+
+    override fun getItemCount(): Int {
+        return pathList?.size ?: 0
+    }
+
+    interface StoragePathAdapterListener {
+        /**
+         * sets the chosen path.
+         *
+         * @param path chosen path
+         */
+        fun chosenPath(path: String)
+    }
+
+    inner class StoragePathViewHolder(var binding: StoragePathItemBinding) :
+        RecyclerView.ViewHolder(
+            binding.root
+        ),
+        View.OnClickListener {
+        init {
+            binding.root.setOnClickListener(this)
+        }
+
+        override fun onClick(view: View) {
+            val path = pathList?.get(absoluteAdapterPosition)?.path
+            path?.let {
+                storagePathAdapterListener.chosenPath(it)
+            }
+        }
+    }
+}

+ 0 - 173
app/src/main/java/com/owncloud/android/ui/dialog/ConfirmationDialogFragment.java

@@ -1,173 +0,0 @@
-/*
- * ownCloud Android client application
- *
- * Copyright (C) 2012 Bartek Przybylski Copyright (C) 2015 ownCloud Inc.
- *
- * 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.dialog;
-
-import android.app.Activity;
-import android.app.Dialog;
-import android.os.Bundle;
-
-import com.google.android.material.dialog.MaterialAlertDialogBuilder;
-import com.nextcloud.client.di.Injectable;
-import com.owncloud.android.R;
-import com.owncloud.android.utils.theme.ViewThemeUtils;
-
-import javax.inject.Inject;
-
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AlertDialog;
-import androidx.fragment.app.DialogFragment;
-
-
-public class ConfirmationDialogFragment extends DialogFragment implements Injectable {
-
-    final static String ARG_MESSAGE_RESOURCE_ID = "resource_id";
-    final static String ARG_MESSAGE_ARGUMENTS = "string_array";
-    final static String ARG_TITLE_ID = "title_id";
-
-    final static String ARG_POSITIVE_BTN_RES = "positive_btn_res";
-    final static String ARG_NEUTRAL_BTN_RES = "neutral_btn_res";
-    final static String ARG_NEGATIVE_BTN_RES = "negative_btn_res";
-
-    public static final String FTAG_CONFIRMATION = "CONFIRMATION_FRAGMENT";
-
-    @Inject ViewThemeUtils viewThemeUtils;
-
-
-    private ConfirmationDialogFragmentListener mListener;
-
-    /**
-     * Public factory method to create new ConfirmationDialogFragment instances.
-     *
-     * @param messageResId     Resource id for a message to show in the dialog.
-     * @param messageArguments Arguments to complete the message, if it's a format string. May be null.
-     * @param titleResId       Resource id for a text to show in the title. 0 for default alert title, -1 for no title.
-     * @param posBtn           Resource id for the text of the positive button. -1 for no positive button.
-     * @param neuBtn           Resource id for the text of the neutral button. -1 for no neutral button.
-     * @param negBtn           Resource id for the text of the negative button. -1 for no negative button.
-     * @return Dialog ready to show.
-     */
-    public static ConfirmationDialogFragment newInstance(int messageResId, String[] messageArguments, int titleResId,
-                                                         int posBtn, int neuBtn, int negBtn) {
-        if (messageResId == -1) {
-            throw new IllegalStateException("Calling confirmation dialog without message resource");
-        }
-
-        ConfirmationDialogFragment frag = new ConfirmationDialogFragment();
-        Bundle args = new Bundle();
-        args.putInt(ARG_MESSAGE_RESOURCE_ID, messageResId);
-        args.putStringArray(ARG_MESSAGE_ARGUMENTS, messageArguments);
-        args.putInt(ARG_TITLE_ID, titleResId);
-        args.putInt(ARG_POSITIVE_BTN_RES, posBtn);
-        args.putInt(ARG_NEUTRAL_BTN_RES, neuBtn);
-        args.putInt(ARG_NEGATIVE_BTN_RES, negBtn);
-        frag.setArguments(args);
-        return frag;
-    }
-
-    @Override
-    public void onStart() {
-        super.onStart();
-
-        AlertDialog alertDialog = (AlertDialog) getDialog();
-
-        if(alertDialog != null) {
-            viewThemeUtils.platform.colorTextButtons(alertDialog.getButton(AlertDialog.BUTTON_POSITIVE),
-                                                     alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE),
-                                                     alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL));
-        }
-    }
-
-    public void setOnConfirmationListener(ConfirmationDialogFragmentListener listener) {
-        mListener = listener;
-    }
-
-    @NonNull
-    @Override
-    public Dialog onCreateDialog(Bundle savedInstanceState) {
-        Bundle arguments = getArguments();
-
-        if (arguments == null) {
-            throw new IllegalArgumentException("Arguments may not be null");
-        }
-
-        Activity activity = getActivity();
-
-        if (activity == null) {
-            throw new IllegalArgumentException("Activity may not be null");
-        }
-
-        Object[] messageArguments = arguments.getStringArray(ARG_MESSAGE_ARGUMENTS);
-        int messageId = arguments.getInt(ARG_MESSAGE_RESOURCE_ID, -1);
-        int titleId = arguments.getInt(ARG_TITLE_ID, -1);
-        int posBtn = arguments.getInt(ARG_POSITIVE_BTN_RES, -1);
-        int neuBtn = arguments.getInt(ARG_NEUTRAL_BTN_RES, -1);
-        int negBtn = arguments.getInt(ARG_NEGATIVE_BTN_RES, -1);
-
-        if (messageArguments == null) {
-            messageArguments = new String[]{};
-        }
-
-        MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(activity)
-            .setIcon(R.drawable.ic_warning)
-            .setIconAttribute(android.R.attr.alertDialogIcon)
-            .setMessage(String.format(getString(messageId), messageArguments));
-
-        if (titleId == 0) {
-            builder.setTitle(android.R.string.dialog_alert_title);
-        } else if (titleId != -1) {
-            builder.setTitle(titleId);
-        }
-
-        if (posBtn != -1) {
-            builder.setPositiveButton(posBtn, (dialog, whichButton) -> {
-                if (mListener != null) {
-                    mListener.onConfirmation(getTag());
-                }
-                dialog.dismiss();
-            });
-        }
-        if (neuBtn != -1) {
-            builder.setNeutralButton(neuBtn, (dialog, whichButton) -> {
-                if (mListener != null) {
-                    mListener.onNeutral(getTag());
-                }
-                dialog.dismiss();
-            });
-        }
-        if (negBtn != -1) {
-            builder.setNegativeButton(negBtn, (dialog, which) -> {
-                if (mListener != null) {
-                    mListener.onCancel(getTag());
-                }
-                dialog.dismiss();
-            });
-        }
-
-        viewThemeUtils.dialog.colorMaterialAlertDialogBackground(activity, builder);
-
-        return builder.create();
-    }
-
-    public interface ConfirmationDialogFragmentListener {
-        void onConfirmation(String callerTag);
-
-        void onNeutral(String callerTag);
-
-        void onCancel(String callerTag);
-    }
-}
-

+ 156 - 0
app/src/main/java/com/owncloud/android/ui/dialog/ConfirmationDialogFragment.kt

@@ -0,0 +1,156 @@
+/*
+ * ownCloud Android client application
+ *
+ * Copyright (C) 2012 Bartek Przybylski Copyright (C) 2015 ownCloud Inc.
+ *
+ * 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.dialog
+
+//noinspection SuspiciousImport
+import android.R
+import android.app.Dialog
+import android.content.DialogInterface
+import android.os.Bundle
+import androidx.appcompat.app.AlertDialog
+import androidx.fragment.app.DialogFragment
+import com.google.android.material.button.MaterialButton
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.nextcloud.client.di.Injectable
+import com.owncloud.android.utils.theme.ViewThemeUtils
+import javax.inject.Inject
+
+open class ConfirmationDialogFragment : DialogFragment(), Injectable {
+
+    @JvmField
+    @Inject
+    var viewThemeUtils: ViewThemeUtils? = null
+
+    private var mListener: ConfirmationDialogFragmentListener? = null
+
+    override fun onStart() {
+        super.onStart()
+
+        val alertDialog = dialog as AlertDialog?
+
+        if (alertDialog != null) {
+            val positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE) as MaterialButton?
+            if (positiveButton != null) {
+                viewThemeUtils?.material?.colorMaterialButtonPrimaryTonal(positiveButton)
+            }
+
+            val negativeButton = alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE) as MaterialButton?
+            if (negativeButton != null) {
+                viewThemeUtils?.material?.colorMaterialButtonPrimaryBorderless(negativeButton)
+            }
+
+            val neutralButton = alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL) as MaterialButton?
+            if (neutralButton != null) {
+                viewThemeUtils?.material?.colorMaterialButtonPrimaryBorderless(neutralButton)
+            }
+        }
+    }
+
+    fun setOnConfirmationListener(listener: ConfirmationDialogFragmentListener?) {
+        mListener = listener
+    }
+
+    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+        val messageArguments = requireArguments().getStringArray(ARG_MESSAGE_ARGUMENTS) ?: arrayOf<String>()
+        val titleId = requireArguments().getInt(ARG_TITLE_ID, -1)
+        val messageId = requireArguments().getInt(ARG_MESSAGE_RESOURCE_ID, -1)
+        val positiveButtonTextId = requireArguments().getInt(ARG_POSITIVE_BTN_RES, -1)
+        val negativeButtonTextId = requireArguments().getInt(ARG_NEGATIVE_BTN_RES, -1)
+        val neutralButtonTextId = requireArguments().getInt(ARG_NEUTRAL_BTN_RES, -1)
+
+        @Suppress("SpreadOperator")
+        val message = getString(messageId, *messageArguments)
+
+        val builder = MaterialAlertDialogBuilder(requireActivity())
+            .setTitle(if (titleId == 0) { R.string.dialog_alert_title } else { titleId })
+            .setIcon(com.owncloud.android.R.drawable.ic_warning)
+            .setIconAttribute(R.attr.alertDialogIcon)
+            .setMessage(message)
+
+        if (positiveButtonTextId != -1) {
+            builder.setPositiveButton(positiveButtonTextId) { dialog: DialogInterface, _: Int ->
+                mListener?.onConfirmation(tag)
+                dialog.dismiss()
+            }
+        }
+        if (negativeButtonTextId != -1) {
+            builder.setNegativeButton(negativeButtonTextId) { dialog: DialogInterface, _: Int ->
+                mListener?.onCancel(tag)
+                dialog.dismiss()
+            }
+        }
+        if (neutralButtonTextId != -1) {
+            builder.setNeutralButton(neutralButtonTextId) { dialog: DialogInterface, _: Int ->
+                mListener?.onNeutral(tag)
+                dialog.dismiss()
+            }
+        }
+
+        viewThemeUtils?.dialog?.colorMaterialAlertDialogBackground(requireActivity(), builder)
+
+        return builder.create()
+    }
+
+    interface ConfirmationDialogFragmentListener {
+        fun onConfirmation(callerTag: String?)
+        fun onNeutral(callerTag: String?)
+        fun onCancel(callerTag: String?)
+    }
+
+    companion object {
+        const val ARG_MESSAGE_RESOURCE_ID = "resource_id"
+        const val ARG_MESSAGE_ARGUMENTS = "string_array"
+        const val ARG_TITLE_ID = "title_id"
+        const val ARG_POSITIVE_BTN_RES = "positive_btn_res"
+        const val ARG_NEUTRAL_BTN_RES = "neutral_btn_res"
+        const val ARG_NEGATIVE_BTN_RES = "negative_btn_res"
+        const val FTAG_CONFIRMATION = "CONFIRMATION_FRAGMENT"
+
+        /**
+         * Public factory method to create new ConfirmationDialogFragment instances.
+         *
+         * @param messageResId         Resource id for a message to show in the dialog.
+         * @param messageArguments     Arguments to complete the message, if it's a format string. May be null.
+         * @param titleResId           Resource id for a text to show in the title. 0 for default alert title, -1 for no
+         * title.
+         * @param positiveButtonTextId Resource id for the text of the positive button. -1 for no positive button.
+         * @param neutralButtonTextId  Resource id for the text of the neutral button. -1 for no neutral button.
+         * @param negativeButtonTextId Resource id for the text of the negative button. -1 for no negative button.
+         * @return Dialog ready to show.
+         */
+        @JvmStatic
+        fun newInstance(
+            messageResId: Int,
+            messageArguments: Array<String?>?,
+            titleResId: Int,
+            positiveButtonTextId: Int,
+            negativeButtonTextId: Int,
+            neutralButtonTextId: Int
+        ): ConfirmationDialogFragment {
+            check(messageResId != -1) { "Calling confirmation dialog without message resource" }
+            val frag = ConfirmationDialogFragment()
+            val args = Bundle()
+            args.putInt(ARG_MESSAGE_RESOURCE_ID, messageResId)
+            args.putStringArray(ARG_MESSAGE_ARGUMENTS, messageArguments)
+            args.putInt(ARG_TITLE_ID, titleResId)
+            args.putInt(ARG_POSITIVE_BTN_RES, positiveButtonTextId)
+            args.putInt(ARG_NEGATIVE_BTN_RES, negativeButtonTextId)
+            args.putInt(ARG_NEUTRAL_BTN_RES, neutralButtonTextId)
+            frag.arguments = args
+            return frag
+        }
+    }
+}

+ 0 - 105
app/src/main/java/com/owncloud/android/ui/dialog/IndeterminateProgressDialog.java

@@ -1,105 +0,0 @@
-/**
- *   ownCloud Android client application
- *
- *   Copyright (C) 2015 ownCloud Inc.
- *
- *   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.dialog;
-
-import android.app.Dialog;
-import android.app.ProgressDialog;
-import android.content.DialogInterface;
-import android.content.DialogInterface.OnKeyListener;
-import android.os.Bundle;
-import android.view.KeyEvent;
-import android.widget.ProgressBar;
-
-import com.nextcloud.client.di.Injectable;
-import com.owncloud.android.R;
-import com.owncloud.android.utils.theme.ViewThemeUtils;
-
-import javax.inject.Inject;
-
-import androidx.annotation.NonNull;
-import androidx.fragment.app.DialogFragment;
-
-
-public class IndeterminateProgressDialog extends DialogFragment implements Injectable {
-
-    private static final String ARG_MESSAGE_ID = IndeterminateProgressDialog.class.getCanonicalName() + ".ARG_MESSAGE_ID";
-    private static final String ARG_CANCELABLE = IndeterminateProgressDialog.class.getCanonicalName() + ".ARG_CANCELABLE";
-
-    @Inject ViewThemeUtils viewThemeUtils;
-
-    /**
-     * Public factory method to get dialog instances.
-     *
-     * @param messageId     Resource id for a message to show in the dialog.
-     * @param cancelable    If 'true', the dialog can be cancelled by the user input (BACK button, touch outside...)
-     * @return              New dialog instance, ready to show.
-     */
-    public static IndeterminateProgressDialog newInstance(int messageId, boolean cancelable) {
-        IndeterminateProgressDialog fragment = new IndeterminateProgressDialog();
-        fragment.setStyle(DialogFragment.STYLE_NO_FRAME, R.style.ownCloud_AlertDialog);
-        Bundle args = new Bundle();
-        args.putInt(ARG_MESSAGE_ID, messageId);
-        args.putBoolean(ARG_CANCELABLE, cancelable);
-        fragment.setArguments(args);
-        return fragment;
-    }
-
-
-    /**
-     * {@inheritDoc}
-     */
-    @NonNull
-    @Override
-    public Dialog onCreateDialog(Bundle savedInstanceState) {
-        /// create indeterminate progress dialog
-        final ProgressDialog progressDialog = new ProgressDialog(getActivity(), R.style.ProgressDialogTheme);
-        progressDialog.setIndeterminate(true);
-        progressDialog.setOnShowListener(dialog -> {
-            ProgressBar v = progressDialog.findViewById(android.R.id.progress);
-            viewThemeUtils.platform.tintDrawable(requireContext(), v.getIndeterminateDrawable());
-        });
-
-        /// set message
-        int messageId = getArguments().getInt(ARG_MESSAGE_ID, R.string.placeholder_sentence);
-        progressDialog.setMessage(getString(messageId));
-
-        /// set cancellation behavior
-        boolean cancelable = getArguments().getBoolean(ARG_CANCELABLE, false);
-        if (!cancelable) {
-            progressDialog.setCancelable(false);
-            // disable the back button
-            OnKeyListener keyListener = new OnKeyListener() {
-                @Override
-                public boolean onKey(DialogInterface dialog, int keyCode,
-                        KeyEvent event) {
-
-                    return keyCode == KeyEvent.KEYCODE_BACK;
-                }
-
-            };
-            progressDialog.setOnKeyListener(keyListener);
-        }
-
-        return progressDialog;
-    }
-
-}
-
-

+ 92 - 0
app/src/main/java/com/owncloud/android/ui/dialog/IndeterminateProgressDialog.kt

@@ -0,0 +1,92 @@
+/**
+ * ownCloud Android client application
+ *
+ * Copyright (C) 2015 ownCloud Inc.
+ *
+ * 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:></http:>//www.gnu.org/licenses/>.
+ *
+ */
+
+@file:Suppress("DEPRECATION")
+
+package com.owncloud.android.ui.dialog
+
+import android.app.Dialog
+import android.app.ProgressDialog
+import android.content.DialogInterface
+import android.os.Bundle
+import android.view.KeyEvent
+import android.widget.ProgressBar
+import androidx.fragment.app.DialogFragment
+import com.nextcloud.client.di.Injectable
+import com.owncloud.android.R
+import com.owncloud.android.utils.theme.ViewThemeUtils
+import javax.inject.Inject
+
+class IndeterminateProgressDialog : DialogFragment(), Injectable {
+
+    @JvmField
+    @Inject
+    var viewThemeUtils: ViewThemeUtils? = null
+
+    /**
+     * {@inheritDoc}
+     */
+    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+        // / create indeterminate progress dialog
+        val progressDialog = ProgressDialog(requireActivity(), R.style.ProgressDialogTheme)
+        progressDialog.isIndeterminate = true
+        progressDialog.setOnShowListener {
+            val v = progressDialog.findViewById<ProgressBar>(android.R.id.progress)
+            viewThemeUtils?.platform?.tintDrawable(requireContext(), v.indeterminateDrawable)
+        }
+
+        // / set message
+        val messageId = requireArguments().getInt(ARG_MESSAGE_ID, R.string.placeholder_sentence)
+        progressDialog.setMessage(getString(messageId))
+
+        // / set cancellation behavior
+        val cancelable = requireArguments().getBoolean(ARG_CANCELABLE, false)
+        if (!cancelable) {
+            progressDialog.setCancelable(false)
+            // disable the back button
+            val keyListener =
+                DialogInterface.OnKeyListener { _, keyCode, _ -> keyCode == KeyEvent.KEYCODE_BACK }
+            progressDialog.setOnKeyListener(keyListener)
+        }
+        return progressDialog
+    }
+
+    companion object {
+        private val ARG_MESSAGE_ID = IndeterminateProgressDialog::class.java.canonicalName?.plus(".ARG_MESSAGE_ID")
+        private val ARG_CANCELABLE = IndeterminateProgressDialog::class.java.canonicalName?.plus(".ARG_CANCELABLE")
+
+        /**
+         * Public factory method to get dialog instances.
+         *
+         * @param messageId     Resource id for a message to show in the dialog.
+         * @param cancelable    If 'true', the dialog can be cancelled by the user input (BACK button, touch outside...)
+         * @return New dialog instance, ready to show.
+         */
+        @JvmStatic
+        fun newInstance(messageId: Int, cancelable: Boolean): IndeterminateProgressDialog {
+            val fragment = IndeterminateProgressDialog()
+            fragment.setStyle(STYLE_NO_FRAME, R.style.ownCloud_AlertDialog)
+            val args = Bundle()
+            args.putInt(ARG_MESSAGE_ID, messageId)
+            args.putBoolean(ARG_CANCELABLE, cancelable)
+            fragment.arguments = args
+            return fragment
+        }
+    }
+}

+ 0 - 88
app/src/main/java/com/owncloud/android/ui/dialog/LoadingDialog.java

@@ -1,88 +0,0 @@
-/*
- *   ownCloud Android client application
- *
- *   Copyright (C) 2015 ownCloud Inc.
- *
- *   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.dialog;
-
-import android.app.Dialog;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.Window;
-import android.widget.ProgressBar;
-import android.widget.TextView;
-
-import com.nextcloud.client.di.Injectable;
-import com.owncloud.android.R;
-import com.owncloud.android.utils.theme.ViewThemeUtils;
-
-import javax.inject.Inject;
-
-import androidx.annotation.NonNull;
-import androidx.fragment.app.DialogFragment;
-
-public class LoadingDialog extends DialogFragment implements Injectable {
-
-    @Inject ViewThemeUtils viewThemeUtils;
-    private String mMessage;
-
-    @Override
-    public void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        setRetainInstance(true);
-        setCancelable(false);
-    }
-
-    public static LoadingDialog newInstance(String message) {
-        LoadingDialog loadingDialog = new LoadingDialog();
-        loadingDialog.mMessage = message;
-        return loadingDialog;
-    }
-
-    @Override
-    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
-        // Create a view by inflating desired layout
-        View v = inflater.inflate(R.layout.loading_dialog, container,  false);
-
-        // set value
-        TextView tv = v.findViewById(R.id.loadingText);
-        tv.setText(mMessage);
-
-        // set progress wheel color
-        ProgressBar progressBar = v.findViewById(R.id.loadingBar);
-        viewThemeUtils.platform.tintDrawable(requireContext(), progressBar.getIndeterminateDrawable());
-
-        return v;
-    }
-
-    @NonNull
-    @Override
-    public Dialog onCreateDialog(Bundle savedInstanceState) {
-        Dialog dialog = super.onCreateDialog(savedInstanceState);
-        dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
-        return dialog;
-    }
-
-    @Override
-    public void onDestroyView() {
-        if (getDialog() != null && getRetainInstance()) {
-            getDialog().setDismissMessage(null);
-        }
-        super.onDestroyView();
-    }
-}

+ 79 - 0
app/src/main/java/com/owncloud/android/ui/dialog/LoadingDialog.kt

@@ -0,0 +1,79 @@
+/*
+ *   ownCloud Android client application
+ *
+ *   Copyright (C) 2015 ownCloud Inc.
+ *
+ *   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.dialog
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.DialogFragment
+import com.nextcloud.android.common.ui.theme.utils.ColorRole
+import com.nextcloud.client.di.Injectable
+import com.owncloud.android.databinding.LoadingDialogBinding
+import com.owncloud.android.utils.theme.ViewThemeUtils
+import javax.inject.Inject
+
+class LoadingDialog : DialogFragment(), Injectable {
+
+    @JvmField
+    @Inject
+    var viewThemeUtils: ViewThemeUtils? = null
+
+    private var mMessage: String? = null
+    private lateinit var binding: LoadingDialogBinding
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        retainInstance = true
+        isCancelable = false
+    }
+
+    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+        binding = LoadingDialogBinding.inflate(inflater, container, false)
+        binding.loadingText.text = mMessage
+
+        val loadingDrawable = binding.loadingBar.indeterminateDrawable
+        if (loadingDrawable != null) {
+            viewThemeUtils?.platform?.tintDrawable(requireContext(), loadingDrawable)
+        }
+
+        viewThemeUtils?.platform?.colorViewBackground(binding.loadingLayout, ColorRole.SURFACE_VARIANT)
+
+        return binding.root
+    }
+
+    override fun onDestroyView() {
+        if (dialog != null && retainInstance) {
+            dialog?.setDismissMessage(null)
+        }
+
+        super.onDestroyView()
+    }
+
+    companion object {
+
+        @JvmStatic
+        fun newInstance(message: String?): LoadingDialog {
+            val loadingDialog = LoadingDialog()
+            loadingDialog.mMessage = message
+            return loadingDialog
+        }
+    }
+}

+ 0 - 169
app/src/main/java/com/owncloud/android/ui/dialog/LocalStoragePathPickerDialogFragment.java

@@ -1,169 +0,0 @@
-/*
- * Nextcloud Android client application
- *
- * @author Andy Scherzinger
- * Copyright (C) 2019 Andy Scherzinger
- *
- * 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.dialog;
-
-import android.app.Dialog;
-import android.content.DialogInterface;
-import android.os.Bundle;
-import android.os.Environment;
-import android.view.LayoutInflater;
-import android.view.View;
-
-import com.google.android.material.dialog.MaterialAlertDialogBuilder;
-import com.nextcloud.client.di.Injectable;
-import com.owncloud.android.R;
-import com.owncloud.android.databinding.StoragePathDialogBinding;
-import com.owncloud.android.ui.adapter.StoragePathAdapter;
-import com.owncloud.android.ui.adapter.StoragePathItem;
-import com.owncloud.android.utils.FileStorageUtils;
-import com.owncloud.android.utils.theme.ViewThemeUtils;
-
-import java.io.File;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-import javax.inject.Inject;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.AlertDialog;
-import androidx.fragment.app.DialogFragment;
-import androidx.recyclerview.widget.LinearLayoutManager;
-
-/**
- * Picker dialog for choosing a (storage) path.
- */
-public class LocalStoragePathPickerDialogFragment extends DialogFragment
-    implements DialogInterface.OnClickListener, StoragePathAdapter.StoragePathAdapterListener, Injectable {
-
-    public static final String LOCAL_STORAGE_PATH_PICKER_FRAGMENT = "LOCAL_STORAGE_PATH_PICKER_FRAGMENT";
-
-    private static Set<String> internalStoragePaths = new HashSet<>();
-
-    @Inject ViewThemeUtils viewThemeUtils;
-
-    static {
-        internalStoragePaths.add("/storage/emulated/legacy");
-        internalStoragePaths.add("/storage/emulated/0");
-        internalStoragePaths.add("/mnt/sdcard");
-    }
-
-    private StoragePathDialogBinding binding;
-
-    public static LocalStoragePathPickerDialogFragment newInstance() {
-        return new LocalStoragePathPickerDialogFragment();
-    }
-
-    @Override
-    public void onStart() {
-        super.onStart();
-
-        AlertDialog alertDialog = (AlertDialog) getDialog();
-
-        if (alertDialog != null) {
-            viewThemeUtils.platform.colorTextButtons(alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE));
-        }
-    }
-
-    @Override
-    public void onCreate(@Nullable Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-    }
-
-    @NonNull
-    @Override
-    public Dialog onCreateDialog(Bundle savedInstanceState) {
-        if (!(getActivity() instanceof StoragePathAdapter.StoragePathAdapterListener)) {
-            throw new IllegalArgumentException("Calling activity must implement " +
-                                                   "StoragePathAdapter.StoragePathAdapterListener");
-        }
-
-        // Inflate the layout for the dialog
-        LayoutInflater inflater = requireActivity().getLayoutInflater();
-        binding = StoragePathDialogBinding.inflate(inflater, null, false);
-        View view = binding.getRoot();
-
-        StoragePathAdapter adapter = new StoragePathAdapter(getPathList(), this);
-
-        binding.storagePathRecyclerView.setAdapter(adapter);
-        binding.storagePathRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
-
-        // Build the dialog
-        MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(binding.getRoot().getContext());
-        builder.setView(view)
-            .setNegativeButton(R.string.common_cancel, this)
-            .setTitle(R.string.storage_choose_location);
-
-        viewThemeUtils.dialog.colorMaterialAlertDialogBackground(binding.getRoot().getContext(), builder);
-
-        return builder.create();
-    }
-
-    @Override
-    public void onDestroyView() {
-        binding = null;
-        super.onDestroyView();
-    }
-
-    @Override
-    public void onClick(DialogInterface dialog, int which) {
-        if (which == AlertDialog.BUTTON_NEGATIVE) {
-            dismissAllowingStateLoss();
-        }
-    }
-
-    private List<StoragePathItem> getPathList() {
-        List<StoragePathItem> storagePathItems = new ArrayList<>();
-
-        for (FileStorageUtils.StandardDirectory standardDirectory : FileStorageUtils.StandardDirectory.getStandardDirectories()) {
-            addIfExists(storagePathItems, standardDirectory.getIcon(), getString(standardDirectory.getDisplayName()),
-                        Environment.getExternalStoragePublicDirectory(standardDirectory.getName()).getAbsolutePath());
-        }
-
-        String sdCard = getString(R.string.storage_internal_storage);
-        for (String dir : FileStorageUtils.getStorageDirectories(requireActivity())) {
-            if (internalStoragePaths.contains(dir)) {
-                addIfExists(storagePathItems, R.drawable.ic_sd_grey600, sdCard, dir);
-            } else {
-                addIfExists(storagePathItems, R.drawable.ic_sd_grey600, new File(dir).getName(), dir);
-            }
-        }
-
-        return storagePathItems;
-    }
-
-    private void addIfExists(List<StoragePathItem> storagePathItems, int icon, String name, String path) {
-        File file = new File(path);
-        if (file.exists() && file.canRead()) {
-            storagePathItems.add(new StoragePathItem(icon, name, path));
-        }
-    }
-
-    @Override
-    public void chosenPath(String path) {
-        if (getActivity() != null) {
-            ((StoragePathAdapter.StoragePathAdapterListener) getActivity()).chosenPath(path);
-        }
-        dismissAllowingStateLoss();
-    }
-}

+ 148 - 0
app/src/main/java/com/owncloud/android/ui/dialog/LocalStoragePathPickerDialogFragment.kt

@@ -0,0 +1,148 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Andy Scherzinger
+ * Copyright (C) 2019 Andy Scherzinger
+ *
+ * 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.dialog
+
+import android.app.Dialog
+import android.content.DialogInterface
+import android.os.Bundle
+import android.os.Environment
+import androidx.appcompat.app.AlertDialog
+import androidx.fragment.app.DialogFragment
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.google.android.material.button.MaterialButton
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.nextcloud.client.di.Injectable
+import com.owncloud.android.R
+import com.owncloud.android.databinding.StoragePathDialogBinding
+import com.owncloud.android.ui.adapter.StoragePathAdapter
+import com.owncloud.android.ui.adapter.StoragePathAdapter.StoragePathAdapterListener
+import com.owncloud.android.ui.adapter.StoragePathItem
+import com.owncloud.android.utils.FileStorageUtils
+import com.owncloud.android.utils.FileStorageUtils.StandardDirectory
+import com.owncloud.android.utils.theme.ViewThemeUtils
+import java.io.File
+import javax.inject.Inject
+
+class LocalStoragePathPickerDialogFragment :
+    DialogFragment(),
+    DialogInterface.OnClickListener,
+    StoragePathAdapterListener,
+    Injectable {
+
+    @Inject
+    lateinit var viewThemeUtils: ViewThemeUtils
+
+    private lateinit var binding: StoragePathDialogBinding
+
+    override fun onStart() {
+        super.onStart()
+
+        val alertDialog = dialog as AlertDialog?
+
+        val positiveButton = alertDialog?.getButton(AlertDialog.BUTTON_POSITIVE) as MaterialButton?
+        positiveButton?.let {
+            viewThemeUtils.material.colorMaterialButtonPrimaryTonal(positiveButton)
+        }
+    }
+
+    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+        require(activity is StoragePathAdapterListener) {
+            "Calling activity must implement " +
+                "StoragePathAdapter.StoragePathAdapterListener"
+        }
+
+        // Inflate the layout for the dialog
+        val inflater = requireActivity().layoutInflater
+        binding = StoragePathDialogBinding.inflate(inflater, null, false)
+
+        val adapter = StoragePathAdapter(pathList, this, viewThemeUtils)
+        binding.storagePathRecyclerView.adapter = adapter
+        binding.storagePathRecyclerView.layoutManager = LinearLayoutManager(requireActivity())
+
+        // Build the dialog
+        val builder = MaterialAlertDialogBuilder(requireContext())
+        builder
+            .setView(binding.root)
+            .setPositiveButton(R.string.common_cancel, this)
+            .setTitle(R.string.storage_choose_location)
+
+        viewThemeUtils.dialog.colorMaterialAlertDialogBackground(requireContext(), builder)
+
+        return builder.create()
+    }
+
+    override fun onClick(dialog: DialogInterface, which: Int) {
+        if (which == AlertDialog.BUTTON_POSITIVE) {
+            dismissAllowingStateLoss()
+        }
+    }
+
+    private val pathList: List<StoragePathItem>
+        get() {
+            val storagePathItems: MutableList<StoragePathItem> = ArrayList()
+            for (standardDirectory in StandardDirectory.getStandardDirectories()) {
+                addIfExists(
+                    storagePathItems,
+                    standardDirectory.icon,
+                    getString(standardDirectory.displayName),
+                    Environment.getExternalStoragePublicDirectory(standardDirectory.name).absolutePath
+                )
+            }
+            val sdCard = getString(R.string.storage_internal_storage)
+            for (dir in FileStorageUtils.getStorageDirectories(requireActivity())) {
+                if (internalStoragePaths.contains(dir)) {
+                    addIfExists(storagePathItems, R.drawable.ic_sd_grey600, sdCard, dir)
+                } else {
+                    addIfExists(storagePathItems, R.drawable.ic_sd_grey600, File(dir).name, dir)
+                }
+            }
+            return storagePathItems
+        }
+
+    private fun addIfExists(storagePathItems: MutableList<StoragePathItem>, icon: Int, name: String, path: String) {
+        val file = File(path)
+        if (file.exists() && file.canRead()) {
+            storagePathItems.add(StoragePathItem(icon, name, path))
+        }
+    }
+
+    override fun chosenPath(path: String) {
+        if (activity != null) {
+            (activity as StoragePathAdapterListener?)!!.chosenPath(path)
+        }
+        dismissAllowingStateLoss()
+    }
+
+    companion object {
+        const val LOCAL_STORAGE_PATH_PICKER_FRAGMENT = "LOCAL_STORAGE_PATH_PICKER_FRAGMENT"
+        private val internalStoragePaths: MutableSet<String> = HashSet()
+
+        init {
+            internalStoragePaths.add("/storage/emulated/legacy")
+            internalStoragePaths.add("/storage/emulated/0")
+            internalStoragePaths.add("/mnt/sdcard")
+        }
+
+        @JvmStatic
+        fun newInstance(): LocalStoragePathPickerDialogFragment {
+            return LocalStoragePathPickerDialogFragment()
+        }
+    }
+}

+ 0 - 131
app/src/main/java/com/owncloud/android/ui/dialog/MultipleAccountsDialog.java

@@ -1,131 +0,0 @@
-/*
- *
- *  Nextcloud Android client application
- *
- *  @author Tobias Kaminsky
- *  @author Chris Narkiewicz <hello@ezaquarii.com>
- *
- *  Copyright (C) 2019 Tobias Kaminsky
- *  Copyright (C) 2019 Nextcloud GmbH
- *  Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
- *
- *  This program is free software: you can redistribute it and/or modify
- *  it under the terms of the GNU Affero General Public License as published by
- *  the Free Software Foundation, either version 3 of the License, or
- *  (at your option) any later version.
- *
- *  This program is distributed in the hope that it will be useful,
- *  but WITHOUT ANY WARRANTY; without even the implied warranty of
- *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- *  GNU Affero General Public License for more details.
- *
- *  You should have received a copy of the GNU Affero General Public License
- *  along with this program. If not, see <https://www.gnu.org/licenses/>.
- *
- */
-
-package com.owncloud.android.ui.dialog;
-
-import android.app.Activity;
-import android.app.Dialog;
-import android.content.Context;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-
-import com.google.android.material.dialog.MaterialAlertDialogBuilder;
-import com.nextcloud.client.account.User;
-import com.nextcloud.client.account.UserAccountManager;
-import com.nextcloud.client.di.Injectable;
-import com.owncloud.android.R;
-import com.owncloud.android.databinding.MultipleAccountsBinding;
-import com.owncloud.android.ui.adapter.UserListAdapter;
-import com.owncloud.android.ui.adapter.UserListItem;
-import com.owncloud.android.utils.theme.ViewThemeUtils;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import javax.inject.Inject;
-
-import androidx.annotation.NonNull;
-import androidx.fragment.app.DialogFragment;
-import androidx.recyclerview.widget.LinearLayoutManager;
-
-public class MultipleAccountsDialog extends DialogFragment implements Injectable, UserListAdapter.ClickListener {
-
-    @Inject UserAccountManager accountManager;
-    @Inject ViewThemeUtils viewThemeUtils;
-    public boolean highlightCurrentlyActiveAccount = true;
-
-    @NonNull
-    @Override
-    public Dialog onCreateDialog(Bundle savedInstanceState) {
-        Activity activity = getActivity();
-        if (activity == null) {
-            throw new IllegalArgumentException("Activity may not be null");
-        }
-
-        // Inflate the layout for the dialog
-        LayoutInflater inflater = activity.getLayoutInflater();
-        MultipleAccountsBinding binding = MultipleAccountsBinding.inflate(inflater, null, false);
-
-        final Context parent = getActivity();
-        MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(binding.getRoot().getContext());
-
-        UserListAdapter adapter = new UserListAdapter(parent,
-                                                      accountManager,
-                                                      getAccountListItems(),
-                                                      this,
-                                                      false,
-                                                      highlightCurrentlyActiveAccount,
-                                                      false,
-                                                      viewThemeUtils);
-
-        binding.list.setHasFixedSize(true);
-        binding.list.setLayoutManager(new LinearLayoutManager(activity));
-        binding.list.setAdapter(adapter);
-
-        builder.setView(binding.getRoot()).setTitle(R.string.common_choose_account);
-
-        viewThemeUtils.dialog.colorMaterialAlertDialogBackground(binding.getRoot().getContext(), builder);
-
-        return builder.create();
-    }
-
-    /**
-     * creates the account list items list including the add-account action in case
-     * multiaccount_support is enabled.
-     *
-     * @return list of account list items
-     */
-    private List<UserListItem> getAccountListItems() {
-        List<User> users = accountManager.getAllUsers();
-        List<UserListItem> adapterUserList = new ArrayList<>(users.size());
-        for (User user : users) {
-            adapterUserList.add(new UserListItem(user));
-        }
-
-        return adapterUserList;
-    }
-
-    @Override
-    public void onOptionItemClicked(User user, View view) {
-        // By default, access account if option is clicked
-        onAccountClicked(user);
-    }
-
-    @Override
-    public void onAccountClicked(User user) {
-        final AccountChooserInterface parentActivity = (AccountChooserInterface) getActivity();
-        if (parentActivity != null) {
-            parentActivity.onAccountChosen(user);
-        }
-        dismiss();
-    }
-
-    @Override
-    public void onDestroyView() {
-        super.onDestroyView();
-    }
-}

+ 108 - 0
app/src/main/java/com/owncloud/android/ui/dialog/MultipleAccountsDialog.kt

@@ -0,0 +1,108 @@
+/*
+ *
+ *  Nextcloud Android client application
+ *
+ *  @author Tobias Kaminsky
+ *  @author Chris Narkiewicz <hello@ezaquarii.com>
+ *
+ *  Copyright (C) 2019 Tobias Kaminsky
+ *  Copyright (C) 2019 Nextcloud GmbH
+ *  Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
+ *
+ *  This program is free software: you can redistribute it and/or modify
+ *  it under the terms of the GNU Affero General Public License as published by
+ *  the Free Software Foundation, either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ *  GNU Affero General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Affero General Public License
+ *  along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+package com.owncloud.android.ui.dialog
+
+import android.app.Dialog
+import android.os.Bundle
+import android.view.View
+import androidx.fragment.app.DialogFragment
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.nextcloud.client.account.User
+import com.nextcloud.client.account.UserAccountManager
+import com.nextcloud.client.di.Injectable
+import com.owncloud.android.R
+import com.owncloud.android.databinding.MultipleAccountsBinding
+import com.owncloud.android.ui.adapter.UserListAdapter
+import com.owncloud.android.ui.adapter.UserListItem
+import com.owncloud.android.utils.theme.ViewThemeUtils
+import javax.inject.Inject
+
+class MultipleAccountsDialog : DialogFragment(), Injectable, UserListAdapter.ClickListener {
+    @JvmField
+    @Inject
+    var accountManager: UserAccountManager? = null
+
+    @JvmField
+    @Inject
+    var viewThemeUtils: ViewThemeUtils? = null
+    var highlightCurrentlyActiveAccount = true
+
+    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+        val inflater = requireActivity().layoutInflater
+        val binding = MultipleAccountsBinding.inflate(inflater, null, false)
+
+        val builder = MaterialAlertDialogBuilder(binding.root.context)
+        val adapter = UserListAdapter(
+            requireActivity(),
+            accountManager,
+            accountListItems,
+            this,
+            false,
+            highlightCurrentlyActiveAccount,
+            false,
+            viewThemeUtils
+        )
+
+        binding.list.setHasFixedSize(true)
+        binding.list.layoutManager = LinearLayoutManager(requireActivity())
+        binding.list.adapter = adapter
+
+        builder.setView(binding.root).setTitle(R.string.common_choose_account)
+
+        viewThemeUtils?.dialog?.colorMaterialAlertDialogBackground(requireContext(), builder)
+
+        return builder.create()
+    }
+
+    private val accountListItems: List<UserListItem>
+        /**
+         * creates the account list items list including the add-account action in case
+         * multiaccount_support is enabled.
+         *
+         * @return list of account list items
+         */
+        get() {
+            val users = accountManager?.allUsers ?: listOf()
+
+            val adapterUserList: MutableList<UserListItem> = ArrayList(users.size)
+            for (user in users) {
+                adapterUserList.add(UserListItem(user))
+            }
+            return adapterUserList
+        }
+
+    override fun onOptionItemClicked(user: User, view: View) {
+        // By default, access account if option is clicked
+        onAccountClicked(user)
+    }
+
+    override fun onAccountClicked(user: User) {
+        val parentActivity = activity as AccountChooserInterface?
+        parentActivity?.onAccountChosen(user)
+        dismiss()
+    }
+}

+ 0 - 227
app/src/main/java/com/owncloud/android/ui/dialog/SharePasswordDialogFragment.java

@@ -1,227 +0,0 @@
-/*
- *   ownCloud Android client application
- *
- *   @author masensio
- *   @author Andy Scherzinger
- *   Copyright (C) 2015 ownCloud GmbH.
- *   Copyright (C) 2018 Andy Scherzinger
- *
- *   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.dialog;
-
-import android.app.Dialog;
-import android.content.DialogInterface;
-import android.os.Bundle;
-import android.text.TextUtils;
-import android.view.LayoutInflater;
-import android.view.View;
-
-import com.google.android.material.dialog.MaterialAlertDialogBuilder;
-import com.nextcloud.client.di.Injectable;
-import com.owncloud.android.R;
-import com.owncloud.android.databinding.PasswordDialogBinding;
-import com.owncloud.android.datamodel.OCFile;
-import com.owncloud.android.lib.resources.shares.OCShare;
-import com.owncloud.android.ui.activity.FileActivity;
-import com.owncloud.android.utils.DisplayUtils;
-import com.owncloud.android.utils.KeyboardUtils;
-import com.owncloud.android.utils.theme.ViewThemeUtils;
-
-import javax.inject.Inject;
-
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AlertDialog;
-import androidx.fragment.app.DialogFragment;
-
-/**
- * Dialog to input the password for sharing a file/folder.
- * <p>
- * Triggers the share when the password is introduced.
- */
-public class SharePasswordDialogFragment extends DialogFragment implements DialogInterface.OnClickListener, Injectable {
-
-    private static final String ARG_FILE = "FILE";
-    private static final String ARG_SHARE = "SHARE";
-    private static final String ARG_CREATE_SHARE = "CREATE_SHARE";
-    private static final String ARG_ASK_FOR_PASSWORD = "ASK_FOR_PASSWORD";
-    public static final String PASSWORD_FRAGMENT = "PASSWORD_FRAGMENT";
-
-    @Inject ViewThemeUtils viewThemeUtils;
-    @Inject KeyboardUtils keyboardUtils;
-
-    private PasswordDialogBinding binding;
-    private OCFile file;
-    private OCShare share;
-    private boolean createShare;
-    private boolean askForPassword;
-
-    @Override
-    public void onStart() {
-        super.onStart();
-
-        AlertDialog alertDialog = (AlertDialog) getDialog();
-        if (alertDialog != null) {
-            viewThemeUtils.platform.colorTextButtons(alertDialog.getButton(AlertDialog.BUTTON_POSITIVE),
-                                                     alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE));
-            viewThemeUtils.platform.colorTextButtons(getResources().getColor(R.color.highlight_textColor_Warning),
-                                                     alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL));
-
-            alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> {
-                String password = binding.sharePassword.getText().toString();
-
-                if (!askForPassword && TextUtils.isEmpty(password)) {
-                    DisplayUtils.showSnackMessage(binding.getRoot(), R.string.share_link_empty_password);
-                    return;
-                }
-
-                if (share == null) {
-                    setPassword(createShare, file, password);
-                } else {
-                    setPassword(share, password);
-                }
-
-                alertDialog.dismiss();
-            });
-        }
-    }
-
-    @Override
-    public void onResume() {
-        super.onResume();
-        keyboardUtils.showKeyboardForEditText(requireDialog().getWindow(), binding.sharePassword);
-    }
-
-    /**
-     * Public factory method to create new SharePasswordDialogFragment instances.
-     *
-     * @param file        OCFile bound to the public share that which password will be set or updated
-     * @param createShare When 'true', the request for password will be followed by the creation of a new public link;
-     *                    when 'false', a public share is assumed to exist, and the password is bound to it.
-     * @return Dialog ready to show.
-     */
-    public static SharePasswordDialogFragment newInstance(OCFile file, boolean createShare, boolean askForPassword) {
-        SharePasswordDialogFragment frag = new SharePasswordDialogFragment();
-        Bundle args = new Bundle();
-        args.putParcelable(ARG_FILE, file);
-        args.putBoolean(ARG_CREATE_SHARE, createShare);
-        args.putBoolean(ARG_ASK_FOR_PASSWORD, askForPassword);
-        frag.setArguments(args);
-        return frag;
-    }
-
-    /**
-     * Public factory method to create new SharePasswordDialogFragment instances.
-     *
-     * @param share OCFile bound to the public share that which password will be set or updated
-     * @return Dialog ready to show.
-     */
-    public static SharePasswordDialogFragment newInstance(OCShare share, boolean askForPassword) {
-        SharePasswordDialogFragment frag = new SharePasswordDialogFragment();
-        Bundle args = new Bundle();
-        args.putParcelable(ARG_SHARE, share);
-        args.putBoolean(ARG_ASK_FOR_PASSWORD, askForPassword);
-        frag.setArguments(args);
-        return frag;
-    }
-
-    /**
-     * Public factory method to create new SharePasswordDialogFragment instances.
-     *
-     * @param share OCFile bound to the public share that which password will be set or updated
-     * @return Dialog ready to show.
-     */
-    public static SharePasswordDialogFragment newInstance(OCShare share) {
-        SharePasswordDialogFragment frag = new SharePasswordDialogFragment();
-        Bundle args = new Bundle();
-        args.putParcelable(ARG_SHARE, share);
-        frag.setArguments(args);
-        return frag;
-    }
-
-    @NonNull
-    @Override
-    public Dialog onCreateDialog(Bundle savedInstanceState) {
-        file = getArguments().getParcelable(ARG_FILE);
-        share = getArguments().getParcelable(ARG_SHARE);
-        createShare = getArguments().getBoolean(ARG_CREATE_SHARE, false);
-        askForPassword = getArguments().getBoolean(ARG_ASK_FOR_PASSWORD, false);
-
-        // Inflate the layout for the dialog
-        LayoutInflater inflater = requireActivity().getLayoutInflater();
-        binding = PasswordDialogBinding.inflate(inflater, null, false);
-        View view = binding.getRoot();
-
-        // Setup layout
-        binding.sharePassword.setText("");
-        viewThemeUtils.material.colorTextInputLayout(binding.sharePasswordContainer);
-
-        int negativeButtonCaption;
-        int title;
-        if (askForPassword) {
-            title = R.string.share_link_optional_password_title;
-            negativeButtonCaption = R.string.common_skip;
-        } else {
-            title = R.string.share_link_password_title;
-            negativeButtonCaption = R.string.common_cancel;
-        }
-
-        // Build the dialog
-        MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(view.getContext());
-
-        builder.setView(view)
-            .setPositiveButton(R.string.common_ok, null)
-            .setNegativeButton(negativeButtonCaption, this)
-            .setNeutralButton(R.string.common_delete, this)
-            .setTitle(title);
-
-        viewThemeUtils.dialog.colorMaterialAlertDialogBackground(view.getContext(), builder);
-
-        return builder.create();
-    }
-
-    @Override
-    public void onClick(DialogInterface dialog, int which) {
-        if (which == AlertDialog.BUTTON_NEUTRAL) {
-            if (share == null) {
-                setPassword(createShare, file, null);
-            } else {
-                setPassword(share, null);
-            }
-        } else if (which == AlertDialog.BUTTON_NEGATIVE && askForPassword) {
-            if (share == null) {
-                setPassword(createShare, file, null);
-            } else {
-                setPassword(share, null);
-            }
-        }
-    }
-
-    private void setPassword(boolean createShare, OCFile file, String password) {
-        if (createShare) {
-            ((FileActivity) getActivity()).getFileOperationsHelper().shareFileViaPublicShare(file, password);
-        } else {
-            ((FileActivity) getActivity()).getFileOperationsHelper().setPasswordToShare(share, password);
-        }
-    }
-
-    private void setPassword(OCShare share, String password) {
-        ((FileActivity) getActivity()).getFileOperationsHelper().setPasswordToShare(share, password);
-    }
-
-    @Override
-    public void onDestroyView() {
-        super.onDestroyView();
-        binding = null;
-    }
-}

+ 250 - 0
app/src/main/java/com/owncloud/android/ui/dialog/SharePasswordDialogFragment.kt

@@ -0,0 +1,250 @@
+/*
+ *   ownCloud Android client application
+ *
+ *   @author masensio
+ *   @author Andy Scherzinger
+ *   Copyright (C) 2015 ownCloud GmbH.
+ *   Copyright (C) 2018 Andy Scherzinger
+ *
+ *   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.dialog
+
+import android.app.Dialog
+import android.content.DialogInterface
+import android.os.Build
+import android.os.Bundle
+import android.text.TextUtils
+import androidx.appcompat.app.AlertDialog
+import androidx.core.content.ContextCompat
+import androidx.fragment.app.DialogFragment
+import com.google.android.material.button.MaterialButton
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.nextcloud.client.di.Injectable
+import com.owncloud.android.R
+import com.owncloud.android.databinding.PasswordDialogBinding
+import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.lib.resources.shares.OCShare
+import com.owncloud.android.ui.activity.FileActivity
+import com.owncloud.android.utils.DisplayUtils
+import com.owncloud.android.utils.KeyboardUtils
+import com.owncloud.android.utils.theme.ViewThemeUtils
+import javax.inject.Inject
+
+/**
+ * Dialog to input the password for sharing a file/folder.
+ *
+ *
+ * Triggers the share when the password is introduced.
+ */
+class SharePasswordDialogFragment : DialogFragment(), Injectable {
+    @JvmField
+    @Inject
+    var viewThemeUtils: ViewThemeUtils? = null
+
+    @JvmField
+    @Inject
+    var keyboardUtils: KeyboardUtils? = null
+
+    private var binding: PasswordDialogBinding? = null
+    private var file: OCFile? = null
+    private var share: OCShare? = null
+    private var createShare = false
+    private var askForPassword = false
+
+    override fun onStart() {
+        super.onStart()
+
+        val alertDialog = dialog as AlertDialog?
+
+        if (alertDialog != null) {
+            val positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE) as MaterialButton?
+            if (positiveButton != null) {
+                viewThemeUtils?.material?.colorMaterialButtonPrimaryTonal(positiveButton)
+                positiveButton.setOnClickListener {
+                    val sharePassword = binding?.sharePassword?.text
+
+                    if (sharePassword != null) {
+                        val password = sharePassword.toString()
+                        if (!askForPassword && TextUtils.isEmpty(password)) {
+                            DisplayUtils.showSnackMessage(binding?.root, R.string.share_link_empty_password)
+                            return@setOnClickListener
+                        }
+                        if (share == null) {
+                            setPassword(createShare, file, password)
+                        } else {
+                            setPassword(share!!, password)
+                        }
+                    }
+
+                    alertDialog.dismiss()
+                }
+            }
+
+            val negativeButton = alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE) as MaterialButton?
+            if (negativeButton != null) {
+                viewThemeUtils?.material?.colorMaterialButtonPrimaryBorderless(negativeButton)
+            }
+
+            val neutralButton = alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL) as MaterialButton?
+            if (neutralButton != null) {
+                val warningColorId = ContextCompat.getColor(requireContext(), R.color.highlight_textColor_Warning)
+                viewThemeUtils?.platform?.colorTextButtons(warningColorId, neutralButton)
+            }
+        }
+    }
+
+    override fun onResume() {
+        super.onResume()
+        keyboardUtils?.showKeyboardForEditText(requireDialog().window, binding!!.sharePassword)
+    }
+
+    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+        file = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            requireArguments().getParcelable(ARG_FILE, OCFile::class.java)
+        } else {
+            @Suppress("DEPRECATION")
+            requireArguments().getParcelable(ARG_FILE)
+        }
+
+        share = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            requireArguments().getParcelable(ARG_SHARE, OCShare::class.java)
+        } else {
+            @Suppress("DEPRECATION")
+            requireArguments().getParcelable(ARG_SHARE)
+        }
+
+        createShare = requireArguments().getBoolean(ARG_CREATE_SHARE, false)
+        askForPassword = requireArguments().getBoolean(ARG_ASK_FOR_PASSWORD, false)
+
+        // Inflate the layout for the dialog
+        val inflater = requireActivity().layoutInflater
+        binding = PasswordDialogBinding.inflate(inflater, null, false)
+
+        // Setup layout
+        binding?.sharePassword?.setText("")
+        viewThemeUtils?.material?.colorTextInputLayout(binding!!.sharePasswordContainer)
+
+        val neutralButtonTextId: Int
+        val title: Int
+        if (askForPassword) {
+            title = R.string.share_link_optional_password_title
+            neutralButtonTextId = R.string.common_skip
+        } else {
+            title = R.string.share_link_password_title
+            neutralButtonTextId = R.string.common_cancel
+        }
+
+        // Build the dialog
+        val builder = MaterialAlertDialogBuilder(requireContext())
+        builder.setView(binding!!.root)
+            .setPositiveButton(R.string.common_ok, null)
+            .setNegativeButton(R.string.common_delete) { _: DialogInterface?, _: Int -> callSetPassword() }
+            .setNeutralButton(neutralButtonTextId) { _: DialogInterface?, _: Int ->
+                if (askForPassword) {
+                    callSetPassword()
+                }
+            }
+            .setTitle(title)
+
+        viewThemeUtils?.dialog?.colorMaterialAlertDialogBackground(requireContext(), builder)
+
+        return builder.create()
+    }
+
+    private fun callSetPassword() {
+        if (share == null) {
+            setPassword(createShare, file, null)
+        } else {
+            setPassword(share!!, null)
+        }
+    }
+
+    private fun setPassword(createShare: Boolean, file: OCFile?, password: String?) {
+        val fileOperationsHelper = (requireActivity() as FileActivity).fileOperationsHelper ?: return
+        if (createShare) {
+            fileOperationsHelper.shareFileViaPublicShare(file, password)
+        } else {
+            fileOperationsHelper.setPasswordToShare(share, password)
+        }
+    }
+
+    private fun setPassword(share: OCShare, password: String?) {
+        val fileOperationsHelper = (requireActivity() as FileActivity).fileOperationsHelper ?: return
+        fileOperationsHelper.setPasswordToShare(share, password)
+    }
+
+    override fun onDestroyView() {
+        super.onDestroyView()
+        binding = null
+    }
+
+    companion object {
+        private const val ARG_FILE = "FILE"
+        private const val ARG_SHARE = "SHARE"
+        private const val ARG_CREATE_SHARE = "CREATE_SHARE"
+        private const val ARG_ASK_FOR_PASSWORD = "ASK_FOR_PASSWORD"
+        const val PASSWORD_FRAGMENT = "PASSWORD_FRAGMENT"
+
+        /**
+         * Public factory method to create new SharePasswordDialogFragment instances.
+         *
+         * @param file        OCFile bound to the public share that which
+         * password will be set or updated
+         * @param createShare When 'true', the request for password will be
+         * followed by the creation of a new public link
+         * when 'false', a public share is assumed to exist, and the password is bound to it.
+         * @return Dialog ready to show.
+         */
+        @JvmStatic
+        fun newInstance(file: OCFile?, createShare: Boolean, askForPassword: Boolean): SharePasswordDialogFragment {
+            val frag = SharePasswordDialogFragment()
+            val args = Bundle()
+            args.putParcelable(ARG_FILE, file)
+            args.putBoolean(ARG_CREATE_SHARE, createShare)
+            args.putBoolean(ARG_ASK_FOR_PASSWORD, askForPassword)
+            frag.arguments = args
+            return frag
+        }
+
+        /**
+         * Public factory method to create new SharePasswordDialogFragment instances.
+         *
+         * @param share OCFile bound to the public share that which password will be set or updated
+         * @return Dialog ready to show.
+         */
+        @JvmStatic
+        fun newInstance(share: OCShare?, askForPassword: Boolean): SharePasswordDialogFragment {
+            val frag = SharePasswordDialogFragment()
+            val args = Bundle()
+            args.putParcelable(ARG_SHARE, share)
+            args.putBoolean(ARG_ASK_FOR_PASSWORD, askForPassword)
+            frag.arguments = args
+            return frag
+        }
+
+        /**
+         * Public factory method to create new SharePasswordDialogFragment instances.
+         *
+         * @param share OCFile bound to the public share that which password will be set or updated
+         * @return Dialog ready to show.
+         */
+        fun newInstance(share: OCShare?): SharePasswordDialogFragment {
+            val frag = SharePasswordDialogFragment()
+            val args = Bundle()
+            args.putParcelable(ARG_SHARE, share)
+            frag.arguments = args
+            return frag
+        }
+    }
+}

+ 0 - 189
app/src/main/java/com/owncloud/android/ui/dialog/SortingOrderDialogFragment.java

@@ -1,189 +0,0 @@
-/*
- * Nextcloud Android client application
- *
- * @author Andy Scherzinger
- * Copyright (C) 2017 Andy Scherzinger
- * Copyright (C) 2017 Nextcloud
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
- *
- * You should have received a copy of the GNU Affero General Public
- * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package com.owncloud.android.ui.dialog;
-
-import android.app.Dialog;
-import android.graphics.Typeface;
-import android.os.Bundle;
-import android.view.View;
-import android.widget.ImageButton;
-import android.widget.TextView;
-
-import com.google.android.material.dialog.MaterialAlertDialogBuilder;
-import com.nextcloud.client.di.Injectable;
-import com.owncloud.android.R;
-import com.owncloud.android.databinding.SortingOrderFragmentBinding;
-import com.owncloud.android.lib.common.utils.Log_OC;
-import com.owncloud.android.utils.FileSortOrder;
-import com.owncloud.android.utils.theme.ViewThemeUtils;
-
-import javax.inject.Inject;
-
-import androidx.annotation.NonNull;
-import androidx.fragment.app.DialogFragment;
-
-/**
- * Dialog to show and choose the sorting order for the file listing.
- */
-public class SortingOrderDialogFragment extends DialogFragment implements Injectable {
-
-    private final static String TAG = SortingOrderDialogFragment.class.getSimpleName();
-
-    public static final String SORTING_ORDER_FRAGMENT = "SORTING_ORDER_FRAGMENT";
-    private static final String KEY_SORT_ORDER = "SORT_ORDER";
-
-    private SortingOrderFragmentBinding binding;
-    private View[] mTaggedViews;
-    private String mCurrentSortOrderName;
-
-
-    @Inject ViewThemeUtils viewThemeUtils;
-
-    public static SortingOrderDialogFragment newInstance(FileSortOrder sortOrder) {
-        SortingOrderDialogFragment dialogFragment = new SortingOrderDialogFragment();
-
-        Bundle args = new Bundle();
-        args.putString(KEY_SORT_ORDER, sortOrder.name);
-        dialogFragment.setArguments(args);
-
-        dialogFragment.setStyle(STYLE_NORMAL, R.style.Theme_ownCloud_Dialog);
-
-        return dialogFragment;
-    }
-
-    @Override
-    public void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        // keep the state of the fragment on configuration changes
-        setRetainInstance(true);
-
-        binding = null;
-        mCurrentSortOrderName = getArguments().getString(KEY_SORT_ORDER, FileSortOrder.sort_a_to_z.name);
-    }
-
-    /**
-     * find all relevant UI elements and set their values.
-     *
-     * @param binding the parent binding
-     */
-    private void setupDialogElements(SortingOrderFragmentBinding binding) {
-        viewThemeUtils.platform.colorTextButtons(binding.cancel);
-
-        mTaggedViews = new View[12];
-        mTaggedViews[0] = binding.sortByNameAscending;
-        mTaggedViews[0].setTag(FileSortOrder.sort_a_to_z);
-        mTaggedViews[1] = binding.sortByNameAZText;
-        mTaggedViews[1].setTag(FileSortOrder.sort_a_to_z);
-        mTaggedViews[2] = binding.sortByNameDescending;
-        mTaggedViews[2].setTag(FileSortOrder.sort_z_to_a);
-        mTaggedViews[3] = binding.sortByNameZAText;
-        mTaggedViews[3].setTag(FileSortOrder.sort_z_to_a);
-        mTaggedViews[4] = binding.sortByModificationDateAscending;
-        mTaggedViews[4].setTag(FileSortOrder.sort_old_to_new);
-        mTaggedViews[5] = binding.sortByModificationDateOldestFirstText;
-        mTaggedViews[5].setTag(FileSortOrder.sort_old_to_new);
-        mTaggedViews[6] = binding.sortByModificationDateDescending;
-        mTaggedViews[6].setTag(FileSortOrder.sort_new_to_old);
-        mTaggedViews[7] = binding.sortByModificationDateNewestFirstText;
-        mTaggedViews[7].setTag(FileSortOrder.sort_new_to_old);
-        mTaggedViews[8] = binding.sortBySizeAscending;
-        mTaggedViews[8].setTag(FileSortOrder.sort_small_to_big);
-        mTaggedViews[9] = binding.sortBySizeSmallestFirstText;
-        mTaggedViews[9].setTag(FileSortOrder.sort_small_to_big);
-        mTaggedViews[10] = binding.sortBySizeDescending;
-        mTaggedViews[10].setTag(FileSortOrder.sort_big_to_small);
-        mTaggedViews[11] = binding.sortBySizeBiggestFirstText;
-        mTaggedViews[11].setTag(FileSortOrder.sort_big_to_small);
-
-        setupActiveOrderSelection();
-    }
-
-    /**
-     * tints the icon reflecting the actual sorting choice in the apps primary color.
-     */
-    private void setupActiveOrderSelection() {
-        for (View view : mTaggedViews) {
-            if (!((FileSortOrder) view.getTag()).name.equals(mCurrentSortOrderName)) {
-                continue;
-            }
-            if (view instanceof ImageButton) {
-                viewThemeUtils.platform.themeImageButton((ImageButton) view);
-                ((ImageButton) view).setSelected(true);
-            }
-            if (view instanceof TextView) {
-                viewThemeUtils.platform.colorPrimaryTextViewElement((TextView) view);
-                ((TextView) view).setTypeface(Typeface.DEFAULT_BOLD);
-            }
-        }
-    }
-
-    /**
-     * setup all listeners.
-     */
-    private void setupListeners() {
-        binding.cancel.setOnClickListener(view -> dismiss());
-
-        OnSortOrderClickListener sortOrderClickListener = new OnSortOrderClickListener();
-
-        for (View view : mTaggedViews) {
-            view.setOnClickListener(sortOrderClickListener);
-        }
-    }
-
-    @Override
-    @NonNull
-    public Dialog onCreateDialog(Bundle savedInstanceState) {
-        binding = SortingOrderFragmentBinding.inflate(requireActivity().getLayoutInflater(), null, false);
-
-        setupDialogElements(binding);
-        setupListeners();
-
-        MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(binding.getRoot().getContext());
-        builder.setView(binding.getRoot());
-
-        viewThemeUtils.dialog.colorMaterialAlertDialogBackground(binding.getRoot().getContext(), builder);
-
-        return builder.create();
-    }
-
-    @Override
-    public void onDestroyView() {
-        Log_OC.d(TAG, "destroy SortingOrderDialogFragment view");
-        if (getDialog() != null && getRetainInstance()) {
-            getDialog().setDismissMessage(null);
-        }
-        super.onDestroyView();
-    }
-
-    private class OnSortOrderClickListener implements View.OnClickListener {
-        @Override
-        public void onClick(View v) {
-            dismissAllowingStateLoss();
-            ((SortingOrderDialogFragment.OnSortingOrderListener) getActivity())
-                .onSortingOrderChosen((FileSortOrder) v.getTag());
-        }
-    }
-
-    public interface OnSortingOrderListener {
-        void onSortingOrderChosen(FileSortOrder selection);
-    }
-}

+ 134 - 0
app/src/main/java/com/owncloud/android/ui/dialog/SortingOrderDialogFragment.kt

@@ -0,0 +1,134 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Andy Scherzinger
+ * Copyright (C) 2017 Andy Scherzinger
+ * Copyright (C) 2017 Nextcloud
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.owncloud.android.ui.dialog
+
+import android.app.Dialog
+import android.os.Bundle
+import android.view.View
+import androidx.fragment.app.DialogFragment
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.nextcloud.client.di.Injectable
+import com.owncloud.android.R
+import com.owncloud.android.databinding.SortingOrderFragmentBinding
+import com.owncloud.android.lib.common.utils.Log_OC
+import com.owncloud.android.utils.FileSortOrder
+import com.owncloud.android.utils.theme.ViewThemeUtils
+import javax.inject.Inject
+
+/**
+ * Dialog to show and choose the sorting order for the file listing.
+ */
+class SortingOrderDialogFragment : DialogFragment(), Injectable {
+
+    private var binding: SortingOrderFragmentBinding? = null
+
+    private var currentSortOrderName: String? = null
+
+    @JvmField
+    @Inject
+    var viewThemeUtils: ViewThemeUtils? = null
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        // keep the state of the fragment on configuration changes
+        retainInstance = true
+
+        binding = null
+        currentSortOrderName = requireArguments().getString(KEY_SORT_ORDER, FileSortOrder.sort_a_to_z.name)
+    }
+
+    /**
+     * find all relevant UI elements and set their values.
+     *
+     * @param binding the parent binding
+     */
+    private fun setupDialogElements(binding: SortingOrderFragmentBinding) {
+        val bindings = listOf(
+            binding.sortByNameAscending to FileSortOrder.sort_a_to_z,
+            binding.sortByNameDescending to FileSortOrder.sort_z_to_a,
+            binding.sortByModificationDateAscending to FileSortOrder.sort_old_to_new,
+            binding.sortByModificationDateDescending to FileSortOrder.sort_new_to_old,
+            binding.sortBySizeAscending to FileSortOrder.sort_small_to_big,
+            binding.sortBySizeDescending to FileSortOrder.sort_big_to_small
+        )
+
+        bindings.forEach { (view, sortOrder) ->
+            view.tag = sortOrder
+            view.let {
+                it.setOnClickListener(OnSortOrderClickListener())
+                viewThemeUtils?.material?.colorMaterialButtonPrimaryBorderless(it)
+            }
+        }
+
+        viewThemeUtils?.material?.colorMaterialButtonPrimaryTonal(binding.cancel)
+        binding.cancel.setOnClickListener { dismiss() }
+    }
+
+    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+        binding = SortingOrderFragmentBinding.inflate(requireActivity().layoutInflater, null, false)
+        setupDialogElements(binding!!)
+
+        val builder = MaterialAlertDialogBuilder(requireContext())
+        builder.setView(binding?.root)
+
+        viewThemeUtils?.dialog?.colorMaterialAlertDialogBackground(requireContext(), builder)
+
+        return builder.create()
+    }
+
+    override fun onDestroyView() {
+        Log_OC.d(TAG, "destroy SortingOrderDialogFragment view")
+
+        if (dialog != null && retainInstance) {
+            dialog?.setDismissMessage(null)
+        }
+
+        super.onDestroyView()
+    }
+
+    private inner class OnSortOrderClickListener : View.OnClickListener {
+        override fun onClick(v: View) {
+            dismissAllowingStateLoss()
+            (activity as OnSortingOrderListener?)?.onSortingOrderChosen(v.tag as FileSortOrder)
+        }
+    }
+
+    interface OnSortingOrderListener {
+        fun onSortingOrderChosen(selection: FileSortOrder?)
+    }
+
+    companion object {
+
+        private val TAG = SortingOrderDialogFragment::class.java.simpleName
+        const val SORTING_ORDER_FRAGMENT = "SORTING_ORDER_FRAGMENT"
+        private const val KEY_SORT_ORDER = "SORT_ORDER"
+
+        @JvmStatic
+        fun newInstance(sortOrder: FileSortOrder): SortingOrderDialogFragment {
+            val dialogFragment = SortingOrderDialogFragment()
+            val args = Bundle()
+            args.putString(KEY_SORT_ORDER, sortOrder.name)
+            dialogFragment.arguments = args
+            dialogFragment.setStyle(STYLE_NORMAL, R.style.Theme_ownCloud_Dialog)
+            return dialogFragment
+        }
+    }
+}

+ 0 - 140
app/src/main/java/com/owncloud/android/ui/dialog/SyncFileNotEnoughSpaceDialogFragment.java

@@ -1,140 +0,0 @@
-/*
- *   Nextcloud Android client application
- *
- *   @author Kilian Périsset
- *   Copyright (C) 2020 Infomaniak Network SA
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU Affero General Public License (GPLv3),
- *   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.dialog;
-
-import android.app.Dialog;
-import android.content.Intent;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.storage.StorageManager;
-
-import com.nextcloud.client.di.Injectable;
-import com.owncloud.android.R;
-import com.owncloud.android.datamodel.OCFile;
-import com.owncloud.android.ui.dialog.ConfirmationDialogFragment.ConfirmationDialogFragmentListener;
-import com.owncloud.android.ui.fragment.OCFileListFragment;
-import com.owncloud.android.utils.DisplayUtils;
-import com.owncloud.android.utils.theme.ViewThemeUtils;
-
-import javax.inject.Inject;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-import androidx.appcompat.app.AlertDialog;
-
-/**
- * Dialog requiring confirmation when a file/folder is too "big" to be synchronized/downloaded on device.
- */
-public class SyncFileNotEnoughSpaceDialogFragment extends ConfirmationDialogFragment implements
-    ConfirmationDialogFragmentListener, Injectable {
-
-    private static final String ARG_PASSED_FILE = "fragment_parent_caller";
-    private static final int REQUEST_CODE_STORAGE = 20;
-
-    private OCFile targetFile;
-
-    @Inject ViewThemeUtils viewThemeUtils;
-
-    public static SyncFileNotEnoughSpaceDialogFragment newInstance(OCFile file, long availableDeviceSpace) {
-        Bundle args = new Bundle();
-        SyncFileNotEnoughSpaceDialogFragment frag = new SyncFileNotEnoughSpaceDialogFragment();
-        String properFileSize = DisplayUtils.bytesToHumanReadable(file.getFileLength());
-        String properDiskAvailableSpace = DisplayUtils.bytesToHumanReadable(availableDeviceSpace);
-
-        // Defining title, message and resources
-        args.putInt(ARG_TITLE_ID, R.string.sync_not_enough_space_dialog_title);
-        args.putInt(ARG_MESSAGE_RESOURCE_ID, R.string.sync_not_enough_space_dialog_placeholder);
-        args.putStringArray(ARG_MESSAGE_ARGUMENTS,
-                            new String[]{
-                                file.getFileName(),
-                                properFileSize,
-                                properDiskAvailableSpace});
-        args.putParcelable(ARG_PASSED_FILE, file);
-
-        // Defining buttons
-        if (file.isFolder()) {
-            args.putInt(ARG_POSITIVE_BTN_RES, R.string.sync_not_enough_space_dialog_action_choose);
-        }
-        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N_MR1) {
-            args.putInt(ARG_NEGATIVE_BTN_RES, R.string.sync_not_enough_space_dialog_action_free_space);
-        }
-        args.putInt(ARG_NEUTRAL_BTN_RES, R.string.common_cancel);
-
-        frag.setArguments(args);
-        return frag;
-    }
-
-    @Override
-    public void onStart() {
-        super.onStart();
-
-        AlertDialog alertDialog = (AlertDialog) getDialog();
-
-        if (alertDialog != null) {
-            viewThemeUtils.platform.colorTextButtons(alertDialog.getButton(AlertDialog.BUTTON_POSITIVE),
-                                                     alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL),
-                                                     alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE));
-        }
-    }
-
-    @NonNull
-    @Override
-    public Dialog onCreateDialog(Bundle savedInstanceState) {
-        Bundle arguments = getArguments();
-
-        if (arguments == null) {
-            throw new IllegalArgumentException("Arguments may not be null");
-        }
-
-        targetFile = arguments.getParcelable(ARG_PASSED_FILE);
-        setOnConfirmationListener(this);
-
-        return super.onCreateDialog(savedInstanceState);
-    }
-
-    /**
-     * (Only if file is a folder), will access the destination folder to allow user to choose what to synchronize
-     */
-    @Override
-    public void onConfirmation(String callerTag) {
-        OCFileListFragment frag = (OCFileListFragment) getTargetFragment();
-        if (frag != null && targetFile != null) {
-            frag.onItemClicked(targetFile);
-        }
-    }
-
-    /**
-     * Will abort/cancel the process (is neutral to "hack" android button position ._.)
-     */
-    @Override
-    public void onNeutral(String callerTag) {
-        // Nothing
-    }
-
-    /**
-     * Will access to storage manager in order to empty useless files
-     */
-    @RequiresApi(api = Build.VERSION_CODES.N_MR1)
-    @Override
-    public void onCancel(String callerTag) {
-        Intent storageIntent = new Intent(StorageManager.ACTION_MANAGE_STORAGE);
-        startActivityForResult(storageIntent, REQUEST_CODE_STORAGE);
-    }
-}

+ 119 - 0
app/src/main/java/com/owncloud/android/ui/dialog/SyncFileNotEnoughSpaceDialogFragment.kt

@@ -0,0 +1,119 @@
+/*
+ *   Nextcloud Android client application
+ *
+ *   @author Kilian Périsset
+ *   Copyright (C) 2020 Infomaniak Network SA
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU Affero General Public License (GPLv3),
+ *   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.dialog
+
+import android.app.Dialog
+import android.content.Intent
+import android.os.Build
+import android.os.Bundle
+import android.os.storage.StorageManager
+import androidx.annotation.RequiresApi
+import com.owncloud.android.R
+import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.ui.dialog.ConfirmationDialogFragment.ConfirmationDialogFragmentListener
+import com.owncloud.android.ui.fragment.OCFileListFragment
+import com.owncloud.android.utils.DisplayUtils
+
+/**
+ * Dialog requiring confirmation when a file/folder is too "big" to be synchronized/downloaded on device.
+ */
+class SyncFileNotEnoughSpaceDialogFragment :
+    ConfirmationDialogFragment(),
+    ConfirmationDialogFragmentListener {
+
+    private var targetFile: OCFile? = null
+
+    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+        targetFile = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            requireArguments().getParcelable(ARG_PASSED_FILE, OCFile::class.java)
+        } else {
+            @Suppress("DEPRECATION")
+            requireArguments().getParcelable(ARG_PASSED_FILE)
+        }
+
+        setOnConfirmationListener(this)
+
+        return super.onCreateDialog(savedInstanceState)
+    }
+
+    /**
+     * (Only if file is a folder), will access the destination folder to allow user to choose what to synchronize
+     */
+    override fun onConfirmation(callerTag: String?) {
+        val frag = targetFragment as OCFileListFragment?
+
+        if (frag != null && targetFile != null) {
+            frag.onItemClicked(targetFile)
+        }
+    }
+
+    /**
+     * Will abort/cancel the process (is neutral to "hack" android button position ._.)
+     */
+    override fun onNeutral(callerTag: String?) {
+        // Nothing
+    }
+
+    /**
+     * Will access to storage manager in order to empty useless files
+     */
+    @RequiresApi(api = Build.VERSION_CODES.N_MR1)
+    override fun onCancel(callerTag: String?) {
+        val storageIntent = Intent(StorageManager.ACTION_MANAGE_STORAGE)
+        startActivityForResult(storageIntent, REQUEST_CODE_STORAGE)
+    }
+
+    companion object {
+        private const val ARG_PASSED_FILE = "fragment_parent_caller"
+        private const val REQUEST_CODE_STORAGE = 20
+
+        @JvmStatic
+        fun newInstance(file: OCFile, availableDeviceSpace: Long): SyncFileNotEnoughSpaceDialogFragment {
+            val args = Bundle()
+            val frag = SyncFileNotEnoughSpaceDialogFragment()
+            val properFileSize = DisplayUtils.bytesToHumanReadable(file.fileLength)
+            val properDiskAvailableSpace = DisplayUtils.bytesToHumanReadable(availableDeviceSpace)
+
+            // Defining title, message and resources
+            args.putInt(ARG_TITLE_ID, R.string.sync_not_enough_space_dialog_title)
+            args.putInt(ARG_MESSAGE_RESOURCE_ID, R.string.sync_not_enough_space_dialog_placeholder)
+            args.putStringArray(
+                ARG_MESSAGE_ARGUMENTS,
+                arrayOf(
+                    file.fileName,
+                    properFileSize,
+                    properDiskAvailableSpace
+                )
+            )
+            args.putParcelable(ARG_PASSED_FILE, file)
+
+            // Defining buttons
+            if (file.isFolder) {
+                args.putInt(ARG_POSITIVE_BTN_RES, R.string.sync_not_enough_space_dialog_action_choose)
+            }
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
+                args.putInt(ARG_NEGATIVE_BTN_RES, R.string.sync_not_enough_space_dialog_action_free_space)
+            }
+            args.putInt(ARG_NEUTRAL_BTN_RES, R.string.common_cancel)
+
+            frag.arguments = args
+            return frag
+        }
+    }
+}

+ 0 - 655
app/src/main/java/com/owncloud/android/ui/dialog/SyncedFolderPreferencesDialogFragment.java

@@ -1,655 +0,0 @@
-/*
- * Nextcloud Android client application
- *
- * @author Andy Scherzinger
- * Copyright (C) 2016 Andy Scherzinger
- * Copyright (C) 2016 Nextcloud
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
- *
- * You should have received a copy of the GNU Affero General Public
- * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-package com.owncloud.android.ui.dialog;
-
-import android.app.Activity;
-import android.app.Dialog;
-import android.content.DialogInterface;
-import android.content.Intent;
-import android.graphics.Typeface;
-import android.os.Bundle;
-import android.text.TextUtils;
-import android.text.style.StyleSpan;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.widget.AdapterView;
-import android.widget.LinearLayout;
-import android.widget.Spinner;
-import android.widget.TextView;
-
-import com.google.android.material.button.MaterialButton;
-import com.google.android.material.dialog.MaterialAlertDialogBuilder;
-import com.nextcloud.client.di.Injectable;
-import com.nextcloud.client.preferences.SubFolderRule;
-import com.owncloud.android.R;
-import com.owncloud.android.databinding.SyncedFoldersSettingsLayoutBinding;
-import com.owncloud.android.datamodel.MediaFolderType;
-import com.owncloud.android.datamodel.SyncedFolder;
-import com.owncloud.android.datamodel.SyncedFolderDisplayItem;
-import com.owncloud.android.files.services.NameCollisionPolicy;
-import com.owncloud.android.lib.common.utils.Log_OC;
-import com.owncloud.android.ui.activity.FolderPickerActivity;
-import com.owncloud.android.ui.activity.UploadFilesActivity;
-import com.owncloud.android.ui.dialog.parcel.SyncedFolderParcelable;
-import com.owncloud.android.utils.DisplayUtils;
-import com.owncloud.android.utils.FileStorageUtils;
-import com.owncloud.android.utils.theme.ViewThemeUtils;
-
-import java.io.File;
-
-import javax.inject.Inject;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.AlertDialog;
-import androidx.appcompat.widget.AppCompatCheckBox;
-import androidx.appcompat.widget.SwitchCompat;
-import androidx.fragment.app.DialogFragment;
-
-import static com.owncloud.android.datamodel.SyncedFolderDisplayItem.UNPERSISTED_ID;
-import static com.owncloud.android.ui.activity.UploadFilesActivity.REQUEST_CODE_KEY;
-
-/**
- * Dialog to show the preferences/configuration of a synced folder allowing the user to change the different
- * parameters.
- */
-public class SyncedFolderPreferencesDialogFragment extends DialogFragment implements Injectable {
-
-    public static final String SYNCED_FOLDER_PARCELABLE = "SyncedFolderParcelable";
-    public static final int REQUEST_CODE__SELECT_REMOTE_FOLDER = 0;
-    public static final int REQUEST_CODE__SELECT_LOCAL_FOLDER = 1;
-
-    private final static String TAG = SyncedFolderPreferencesDialogFragment.class.getSimpleName();
-    private static final String BEHAVIOUR_DIALOG_STATE = "BEHAVIOUR_DIALOG_STATE";
-    private static final String NAME_COLLISION_POLICY_DIALOG_STATE = "NAME_COLLISION_POLICY_DIALOG_STATE";
-    private final static float alphaEnabled = 1.0f;
-    private final static float alphaDisabled = 0.7f;
-
-    @Inject ViewThemeUtils viewThemeUtils;
-
-    private CharSequence[] mUploadBehaviorItemStrings;
-    private CharSequence[] mNameCollisionPolicyItemStrings;
-    private SwitchCompat mEnabledSwitch;
-    private AppCompatCheckBox mUploadOnWifiCheckbox;
-    private AppCompatCheckBox mUploadOnChargingCheckbox;
-    private AppCompatCheckBox mUploadExistingCheckbox;
-    private AppCompatCheckBox mUploadUseSubfoldersCheckbox;
-    private Spinner mUploadSubfolderRuleSpinner;
-    private TextView mUploadBehaviorSummary;
-    private TextView mNameCollisionPolicySummary;
-    private TextView mLocalFolderPath;
-    private TextView mLocalFolderSummary;
-    private TextView mRemoteFolderSummary;
-    private LinearLayout mUploadSubfolderRulesContainer;
-
-    private SyncedFolderParcelable mSyncedFolder;
-    private MaterialButton mCancel;
-    private MaterialButton mSave;
-    private boolean behaviourDialogShown;
-    private boolean nameCollisionPolicyDialogShown;
-    private AlertDialog behaviourDialog;
-    private SyncedFoldersSettingsLayoutBinding binding;
-
-    public static SyncedFolderPreferencesDialogFragment newInstance(SyncedFolderDisplayItem syncedFolder, int section) {
-        if (syncedFolder == null) {
-            throw new IllegalArgumentException("SyncedFolder is mandatory but NULL!");
-        }
-
-        Bundle args = new Bundle();
-        args.putParcelable(SYNCED_FOLDER_PARCELABLE, new SyncedFolderParcelable(syncedFolder, section));
-
-        SyncedFolderPreferencesDialogFragment dialogFragment = new SyncedFolderPreferencesDialogFragment();
-        dialogFragment.setArguments(args);
-        dialogFragment.setStyle(STYLE_NORMAL, R.style.Theme_ownCloud_Dialog);
-
-        return dialogFragment;
-    }
-
-    @Override
-    public void onAttach(@NonNull Activity activity) {
-        super.onAttach(activity);
-        if (!(activity instanceof OnSyncedFolderPreferenceListener)) {
-            throw new IllegalArgumentException("The host activity must implement "
-                                                   + OnSyncedFolderPreferenceListener.class.getCanonicalName());
-        }
-    }
-
-    @Override
-    public void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        // keep the state of the fragment on configuration changes
-        setRetainInstance(true);
-
-        binding = null;
-
-        mSyncedFolder = getArguments().getParcelable(SYNCED_FOLDER_PARCELABLE);
-        mUploadBehaviorItemStrings = getResources().getTextArray(R.array.pref_behaviour_entries);
-        mNameCollisionPolicyItemStrings = getResources().getTextArray(R.array.pref_name_collision_policy_entries);
-    }
-
-    /**
-     * find all relevant UI elements and set their values.
-     *
-     * @param binding the parent binding
-     */
-    private void setupDialogElements(SyncedFoldersSettingsLayoutBinding binding) {
-        if (mSyncedFolder.getType().getId() > MediaFolderType.CUSTOM.getId()) {
-            // hide local folder chooser and delete for non-custom folders
-            binding.localFolderContainer.setVisibility(View.GONE);
-            binding.delete.setVisibility(View.GONE);
-        } else if (mSyncedFolder.getId() <= UNPERSISTED_ID) {
-            // Hide delete/enabled for unpersisted custom folders
-            binding.delete.setVisibility(View.GONE);
-            binding.syncEnabled.setVisibility(View.GONE);
-
-            // auto set custom folder to enabled
-            mSyncedFolder.setEnabled(true);
-
-            // switch text to create headline
-            binding.syncedFoldersSettingsTitle.setText(R.string.autoupload_create_new_custom_folder);
-
-            // disable save button
-            binding.save.setEnabled(false);
-        } else {
-            binding.localFolderContainer.setVisibility(View.GONE);
-        }
-
-        // find/saves UI elements
-        mEnabledSwitch = binding.syncEnabled;
-        viewThemeUtils.androidx.colorSwitchCompat(mEnabledSwitch);
-
-        mLocalFolderPath = binding.syncedFoldersSettingsLocalFolderPath;
-
-        mLocalFolderSummary = binding.localFolderSummary;
-        mRemoteFolderSummary = binding.remoteFolderSummary;
-
-        mUploadOnWifiCheckbox = binding.settingInstantUploadOnWifiCheckbox;
-
-        mUploadOnChargingCheckbox = binding.settingInstantUploadOnChargingCheckbox;
-
-        mUploadExistingCheckbox = binding.settingInstantUploadExistingCheckbox;
-
-        mUploadUseSubfoldersCheckbox = binding.settingInstantUploadPathUseSubfoldersCheckbox;
-
-        mUploadSubfolderRuleSpinner = binding.settingInstantUploadSubfolderRuleSpinner;
-        mUploadSubfolderRulesContainer = binding.settingInstantUploadSubfolderRuleContainer;
-
-
-
-        viewThemeUtils.platform.themeCheckbox(mUploadOnWifiCheckbox,
-                                              mUploadOnChargingCheckbox,
-                                              mUploadExistingCheckbox,
-                                              mUploadUseSubfoldersCheckbox);
-
-        mUploadBehaviorSummary = binding.settingInstantBehaviourSummary;
-
-        mNameCollisionPolicySummary = binding.settingInstantNameCollisionPolicySummary;
-
-        mCancel = binding.cancel;
-        mSave = binding.save;
-
-        viewThemeUtils.platform.colorTextButtons(mCancel, mSave);
-
-        // Set values
-        setEnabled(mSyncedFolder.isEnabled());
-
-        if (!TextUtils.isEmpty(mSyncedFolder.getLocalPath())) {
-            mLocalFolderPath.setText(
-                DisplayUtils.createTextWithSpan(
-                    String.format(
-                        getString(R.string.synced_folders_preferences_folder_path),
-                        mSyncedFolder.getLocalPath()),
-                    mSyncedFolder.getFolderName(),
-                    new StyleSpan(Typeface.BOLD)));
-            mLocalFolderSummary.setText(FileStorageUtils.pathToUserFriendlyDisplay(
-                mSyncedFolder.getLocalPath(),
-                getActivity(),
-                getResources()));
-        } else {
-            mLocalFolderSummary.setText(R.string.choose_local_folder);
-        }
-
-        if (!TextUtils.isEmpty(mSyncedFolder.getLocalPath())) {
-            mRemoteFolderSummary.setText(mSyncedFolder.getRemotePath());
-        } else {
-            mRemoteFolderSummary.setText(R.string.choose_remote_folder);
-        }
-
-        mUploadOnWifiCheckbox.setChecked(mSyncedFolder.isWifiOnly());
-        mUploadOnChargingCheckbox.setChecked(mSyncedFolder.isChargingOnly());
-
-        mUploadExistingCheckbox.setChecked(mSyncedFolder.isExisting());
-        mUploadUseSubfoldersCheckbox.setChecked(mSyncedFolder.isSubfolderByDate());
-
-        mUploadSubfolderRuleSpinner.setSelection(mSyncedFolder.getSubFolderRule().ordinal());
-        if (mUploadUseSubfoldersCheckbox.isChecked()) {
-            mUploadSubfolderRulesContainer.setVisibility(View.VISIBLE);
-        } else {
-            mUploadSubfolderRulesContainer.setVisibility(View.GONE);
-        }
-
-        mUploadBehaviorSummary.setText(mUploadBehaviorItemStrings[mSyncedFolder.getUploadActionInteger()]);
-
-        final int nameCollisionPolicyIndex =
-            getSelectionIndexForNameCollisionPolicy(mSyncedFolder.getNameCollisionPolicy());
-        mNameCollisionPolicySummary.setText(mNameCollisionPolicyItemStrings[nameCollisionPolicyIndex]);
-    }
-
-    /**
-     * set correct icon/flag.
-     *
-     * @param enabled if enabled or disabled
-     */
-    private void setEnabled(boolean enabled) {
-        mSyncedFolder.setEnabled(enabled);
-        mEnabledSwitch.setChecked(enabled);
-
-        setupViews(binding, enabled);
-    }
-
-    /**
-     * set (new) remote path on activity result of the folder picker activity. The result gets originally propagated to
-     * the underlying activity since the picker is an activity and the result can't get passed to the dialog fragment
-     * directly.
-     *
-     * @param path the remote path to be set
-     */
-    public void setRemoteFolderSummary(String path) {
-        mSyncedFolder.setRemotePath(path);
-        mRemoteFolderSummary.setText(path);
-        checkAndUpdateSaveButtonState();
-    }
-
-    /**
-     * set (new) local path on activity result of the folder picker activity. The result gets originally propagated to
-     * the underlying activity since the picker is an activity and the result can't get passed to the dialog fragment
-     * directly.
-     *
-     * @param path the local path to be set
-     */
-    public void setLocalFolderSummary(String path) {
-        mSyncedFolder.setLocalPath(path);
-        mLocalFolderSummary.setText(FileStorageUtils.pathToUserFriendlyDisplay(path, getActivity(), getResources()));
-        mLocalFolderPath.setText(
-            DisplayUtils.createTextWithSpan(
-                String.format(
-                    getString(R.string.synced_folders_preferences_folder_path),
-                    mSyncedFolder.getLocalPath()),
-                new File(mSyncedFolder.getLocalPath()).getName(),
-                new StyleSpan(Typeface.BOLD)));
-        checkAndUpdateSaveButtonState();
-    }
-
-    private void checkAndUpdateSaveButtonState() {
-        if (mSyncedFolder.getLocalPath() != null && mSyncedFolder.getRemotePath() != null) {
-            binding.save.setEnabled(true);
-        } else {
-            binding.save.setEnabled(false);
-        }
-
-        checkWritableFolder();
-    }
-
-    private void checkWritableFolder() {
-        if (!mSyncedFolder.isEnabled()) {
-            binding.settingInstantBehaviourContainer.setEnabled(false);
-            binding.settingInstantBehaviourContainer.setAlpha(alphaDisabled);
-            return;
-        }
-
-        if (mSyncedFolder.getLocalPath() != null && new File(mSyncedFolder.getLocalPath()).canWrite()) {
-            binding.settingInstantBehaviourContainer.setEnabled(true);
-            binding.settingInstantBehaviourContainer.setAlpha(alphaEnabled);
-            mUploadBehaviorSummary.setText(mUploadBehaviorItemStrings[mSyncedFolder.getUploadActionInteger()]);
-        } else {
-            binding.settingInstantBehaviourContainer.setEnabled(false);
-            binding.settingInstantBehaviourContainer.setAlpha(alphaDisabled);
-
-            mSyncedFolder.setUploadAction(
-                getResources().getTextArray(R.array.pref_behaviour_entryValues)[0].toString());
-
-            mUploadBehaviorSummary.setText(R.string.auto_upload_file_behaviour_kept_in_folder);
-        }
-    }
-
-    private void setupViews(SyncedFoldersSettingsLayoutBinding binding, boolean enable) {
-        float alpha;
-        if (enable) {
-            alpha = alphaEnabled;
-        } else {
-            alpha = alphaDisabled;
-        }
-        binding.settingInstantUploadOnWifiContainer.setEnabled(enable);
-        binding.settingInstantUploadOnWifiContainer.setAlpha(alpha);
-
-        binding.settingInstantUploadOnChargingContainer.setEnabled(enable);
-        binding.settingInstantUploadOnChargingContainer.setAlpha(alpha);
-
-        binding.settingInstantUploadExistingContainer.setEnabled(enable);
-        binding.settingInstantUploadExistingContainer.setAlpha(alpha);
-
-        binding.settingInstantUploadPathUseSubfoldersContainer.setEnabled(enable);
-        binding.settingInstantUploadPathUseSubfoldersContainer.setAlpha(alpha);
-
-        binding.remoteFolderContainer.setEnabled(enable);
-        binding.remoteFolderContainer.setAlpha(alpha);
-
-        binding.localFolderContainer.setEnabled(enable);
-        binding.localFolderContainer.setAlpha(alpha);
-
-        binding.settingInstantNameCollisionPolicyContainer.setEnabled(enable);
-        binding.settingInstantNameCollisionPolicyContainer.setAlpha(alpha);
-
-        mUploadOnWifiCheckbox.setEnabled(enable);
-        mUploadOnChargingCheckbox.setEnabled(enable);
-        mUploadExistingCheckbox.setEnabled(enable);
-        mUploadUseSubfoldersCheckbox.setEnabled(enable);
-
-        checkWritableFolder();
-    }
-
-    /**
-     * setup all listeners.
-     *
-     * @param binding the parent binding
-     */
-    private void setupListeners(SyncedFoldersSettingsLayoutBinding binding) {
-        mSave.setOnClickListener(new OnSyncedFolderSaveClickListener());
-        mCancel.setOnClickListener(new OnSyncedFolderCancelClickListener());
-        binding.delete.setOnClickListener(new OnSyncedFolderDeleteClickListener());
-
-        binding.settingInstantUploadOnWifiContainer.setOnClickListener(
-            new OnClickListener() {
-                @Override
-                public void onClick(View v) {
-                    mSyncedFolder.setWifiOnly(!mSyncedFolder.isWifiOnly());
-                    mUploadOnWifiCheckbox.toggle();
-                }
-            });
-
-        binding.settingInstantUploadOnChargingContainer.setOnClickListener(
-            new OnClickListener() {
-                @Override
-                public void onClick(View v) {
-                    mSyncedFolder.setChargingOnly(!mSyncedFolder.isChargingOnly());
-                    mUploadOnChargingCheckbox.toggle();
-                }
-            });
-
-        binding.settingInstantUploadExistingContainer.setOnClickListener(
-            new OnClickListener() {
-                @Override
-                public void onClick(View v) {
-                    mSyncedFolder.setExisting(!mSyncedFolder.isExisting());
-                    mUploadExistingCheckbox.toggle();
-                }
-            });
-
-        binding.settingInstantUploadPathUseSubfoldersContainer.setOnClickListener(
-            new OnClickListener() {
-                @Override
-                public void onClick(View v) {
-                    mSyncedFolder.setSubfolderByDate(!mSyncedFolder.isSubfolderByDate());
-                    mUploadUseSubfoldersCheckbox.toggle();
-                    // Only allow setting subfolder rule if subfolder is allowed
-                    if (mUploadUseSubfoldersCheckbox.isChecked()) {
-                        mUploadSubfolderRulesContainer.setVisibility(View.VISIBLE);
-                    } else {
-                        mUploadSubfolderRulesContainer.setVisibility(View.GONE);
-                    }
-                }
-            });
-
-        binding.settingInstantUploadSubfolderRuleSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
-            @Override
-            public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
-                mSyncedFolder.setSubFolderRule(SubFolderRule.values()[i]);
-            }
-
-            @Override
-            public void onNothingSelected(AdapterView<?> adapterView) {
-                mSyncedFolder.setSubFolderRule(SubFolderRule.YEAR_MONTH);
-            }
-        });
-
-        binding.remoteFolderContainer.setOnClickListener(v -> {
-            Intent action = new Intent(getActivity(), FolderPickerActivity.class);
-            getActivity().startActivityForResult(action, REQUEST_CODE__SELECT_REMOTE_FOLDER);
-        });
-
-        binding.localFolderContainer.setOnClickListener(v -> {
-            Intent action = new Intent(getActivity(), UploadFilesActivity.class);
-            action.putExtra(UploadFilesActivity.KEY_LOCAL_FOLDER_PICKER_MODE, true);
-            action.putExtra(REQUEST_CODE_KEY, REQUEST_CODE__SELECT_LOCAL_FOLDER);
-            getActivity().startActivityForResult(action, REQUEST_CODE__SELECT_LOCAL_FOLDER);
-        });
-
-        binding.syncEnabled.setOnClickListener(new OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                setEnabled(!mSyncedFolder.isEnabled());
-            }
-        });
-
-        binding.settingInstantBehaviourContainer.setOnClickListener(
-            new OnClickListener() {
-                @Override
-                public void onClick(View v) {
-                    showBehaviourDialog();
-                }
-            });
-
-        binding.settingInstantNameCollisionPolicyContainer.setOnClickListener(
-            new OnClickListener() {
-                @Override
-                public void onClick(View v) {
-                    showNameCollisionPolicyDialog();
-                }
-            });
-    }
-
-    private void showBehaviourDialog() {
-        MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getActivity());
-        builder.setTitle(R.string.prefs_instant_behaviour_dialogTitle)
-            .setSingleChoiceItems(getResources().getTextArray(R.array.pref_behaviour_entries),
-                                  mSyncedFolder.getUploadActionInteger(),
-                                  new
-                                      DialogInterface.OnClickListener() {
-                                          public void onClick(DialogInterface dialog, int which) {
-                                              mSyncedFolder.setUploadAction(
-                                                  getResources().getTextArray(
-                                                      R.array.pref_behaviour_entryValues)[which].toString());
-                                              mUploadBehaviorSummary.setText(SyncedFolderPreferencesDialogFragment
-                                                                                 .this.mUploadBehaviorItemStrings[which]);
-                                              behaviourDialogShown = false;
-                                              dialog.dismiss();
-                                          }
-                                      })
-            .setOnCancelListener(new DialogInterface.OnCancelListener() {
-                @Override
-                public void onCancel(DialogInterface dialog) {
-                    behaviourDialogShown = false;
-                }
-            });
-        behaviourDialogShown = true;
-
-        viewThemeUtils.dialog.colorMaterialAlertDialogBackground(getActivity(), builder);
-
-        behaviourDialog = builder.create();
-        behaviourDialog.show();
-    }
-
-    private void showNameCollisionPolicyDialog() {
-        MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getActivity());
-
-        builder.setTitle(R.string.pref_instant_name_collision_policy_dialogTitle)
-            .setSingleChoiceItems(getResources().getTextArray(R.array.pref_name_collision_policy_entries),
-                                  getSelectionIndexForNameCollisionPolicy(mSyncedFolder.getNameCollisionPolicy()),
-                                  new OnNameCollisionDialogClickListener())
-            .setOnCancelListener(dialog -> nameCollisionPolicyDialogShown = false);
-
-        nameCollisionPolicyDialogShown = true;
-
-        viewThemeUtils.dialog.colorMaterialAlertDialogBackground(getActivity(), builder);
-
-        behaviourDialog = builder.create();
-        behaviourDialog.show();
-    }
-
-    @Override
-    @NonNull
-    public Dialog onCreateDialog(Bundle savedInstanceState) {
-        Log_OC.d(TAG, "onCreateView, savedInstanceState is " + savedInstanceState);
-
-        binding = SyncedFoldersSettingsLayoutBinding.inflate(requireActivity().getLayoutInflater(), null, false);
-
-        setupDialogElements(binding);
-        setupListeners(binding);
-
-        MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(binding.getRoot().getContext());
-        builder.setView(binding.getRoot());
-
-        viewThemeUtils.dialog.colorMaterialAlertDialogBackground(binding.getRoot().getContext(), builder);
-
-        return builder.create();
-    }
-
-    @Override
-    public void onDestroyView() {
-        Log_OC.d(TAG, "destroy SyncedFolderPreferencesDialogFragment view");
-        if (getDialog() != null && getRetainInstance()) {
-            getDialog().setDismissMessage(null);
-        }
-
-        if (behaviourDialog != null && behaviourDialog.isShowing()) {
-            behaviourDialog.dismiss();
-        }
-
-        super.onDestroyView();
-    }
-
-    @Override
-    public void onSaveInstanceState(@NonNull Bundle outState) {
-        outState.putBoolean(BEHAVIOUR_DIALOG_STATE, behaviourDialogShown);
-        outState.putBoolean(NAME_COLLISION_POLICY_DIALOG_STATE, nameCollisionPolicyDialogShown);
-
-        super.onSaveInstanceState(outState);
-    }
-
-    @Override
-    public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
-        behaviourDialogShown = savedInstanceState != null &&
-            savedInstanceState.getBoolean(BEHAVIOUR_DIALOG_STATE, false);
-        nameCollisionPolicyDialogShown = savedInstanceState != null &&
-            savedInstanceState.getBoolean(NAME_COLLISION_POLICY_DIALOG_STATE, false);
-
-        if (behaviourDialogShown) {
-            showBehaviourDialog();
-        }
-        if (nameCollisionPolicyDialogShown) {
-            showNameCollisionPolicyDialog();
-        }
-
-        super.onViewStateRestored(savedInstanceState);
-    }
-
-    public interface OnSyncedFolderPreferenceListener {
-        void onSaveSyncedFolderPreference(SyncedFolderParcelable syncedFolder);
-
-        void onCancelSyncedFolderPreference();
-
-        void onDeleteSyncedFolderPreference(SyncedFolderParcelable syncedFolder);
-    }
-
-    private class OnSyncedFolderSaveClickListener implements OnClickListener {
-        @Override
-        public void onClick(View v) {
-            dismiss();
-            ((OnSyncedFolderPreferenceListener) getActivity()).onSaveSyncedFolderPreference(mSyncedFolder);
-        }
-    }
-
-    private class OnSyncedFolderCancelClickListener implements OnClickListener {
-        @Override
-        public void onClick(View v) {
-            dismiss();
-            ((OnSyncedFolderPreferenceListener) getActivity()).onCancelSyncedFolderPreference();
-        }
-    }
-
-    private class OnSyncedFolderDeleteClickListener implements OnClickListener {
-        @Override
-        public void onClick(View v) {
-            dismiss();
-            ((OnSyncedFolderPreferenceListener) getActivity()).onDeleteSyncedFolderPreference(mSyncedFolder);
-        }
-    }
-
-    private class OnNameCollisionDialogClickListener implements DialogInterface.OnClickListener {
-        @Override
-        public void onClick(DialogInterface dialog, int which) {
-            mSyncedFolder.setNameCollisionPolicy(getNameCollisionPolicyForSelectionIndex(which));
-
-            mNameCollisionPolicySummary.setText(
-                SyncedFolderPreferencesDialogFragment.this.mNameCollisionPolicyItemStrings[which]);
-            nameCollisionPolicyDialogShown = false;
-            dialog.dismiss();
-        }
-    }
-
-    /**
-     * Get index for name collision selection dialog.
-     *
-     * @return 0 if ASK_USER, 1 if OVERWRITE, 2 if RENAME, 3 if SKIP, Otherwise: 0
-     */
-    static private Integer getSelectionIndexForNameCollisionPolicy(NameCollisionPolicy nameCollisionPolicy) {
-        switch (nameCollisionPolicy) {
-            case OVERWRITE:
-                return 1;
-            case RENAME:
-                return 2;
-            case CANCEL:
-                return 3;
-            case ASK_USER:
-            default:
-                return 0;
-        }
-    }
-
-    /**
-     * Get index for name collision selection dialog. Inverse of getSelectionIndexForNameCollisionPolicy.
-     *
-     * @return ASK_USER if 0, OVERWRITE if 1, RENAME if 2, SKIP if 3. Otherwise: ASK_USER
-     */
-    static private NameCollisionPolicy getNameCollisionPolicyForSelectionIndex(int index) {
-        switch (index) {
-            case 1:
-                return NameCollisionPolicy.OVERWRITE;
-            case 2:
-                return NameCollisionPolicy.RENAME;
-            case 3:
-                return NameCollisionPolicy.CANCEL;
-            case 0:
-            default:
-                return NameCollisionPolicy.ASK_USER;
-        }
-    }
-}

+ 566 - 0
app/src/main/java/com/owncloud/android/ui/dialog/SyncedFolderPreferencesDialogFragment.kt

@@ -0,0 +1,566 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Andy Scherzinger
+ * Copyright (C) 2016 Andy Scherzinger
+ * Copyright (C) 2016 Nextcloud
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.owncloud.android.ui.dialog
+
+import android.app.Activity
+import android.app.Dialog
+import android.content.DialogInterface
+import android.content.Intent
+import android.graphics.Typeface
+import android.os.Build
+import android.os.Bundle
+import android.text.TextUtils
+import android.text.style.StyleSpan
+import android.view.View
+import android.widget.AdapterView
+import androidx.appcompat.app.AlertDialog
+import androidx.fragment.app.DialogFragment
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.nextcloud.client.di.Injectable
+import com.nextcloud.client.preferences.SubFolderRule
+import com.owncloud.android.R
+import com.owncloud.android.databinding.SyncedFoldersSettingsLayoutBinding
+import com.owncloud.android.datamodel.MediaFolderType
+import com.owncloud.android.datamodel.SyncedFolder
+import com.owncloud.android.datamodel.SyncedFolderDisplayItem
+import com.owncloud.android.files.services.NameCollisionPolicy
+import com.owncloud.android.lib.common.utils.Log_OC
+import com.owncloud.android.ui.activity.FolderPickerActivity
+import com.owncloud.android.ui.activity.UploadFilesActivity
+import com.owncloud.android.ui.dialog.parcel.SyncedFolderParcelable
+import com.owncloud.android.utils.DisplayUtils
+import com.owncloud.android.utils.FileStorageUtils
+import com.owncloud.android.utils.theme.ViewThemeUtils
+import java.io.File
+import javax.inject.Inject
+
+/**
+ * Dialog to show the preferences/configuration of a synced folder allowing the user to change the different
+ * parameters.
+ */
+class SyncedFolderPreferencesDialogFragment : DialogFragment(), Injectable {
+
+    @JvmField
+    @Inject
+    var viewThemeUtils: ViewThemeUtils? = null
+
+    private lateinit var uploadBehaviorItemStrings: Array<CharSequence>
+    private lateinit var nameCollisionPolicyItemStrings: Array<CharSequence>
+
+    private var syncedFolder: SyncedFolderParcelable? = null
+    private var behaviourDialogShown = false
+    private var nameCollisionPolicyDialogShown = false
+    private var behaviourDialog: AlertDialog? = null
+    private var binding: SyncedFoldersSettingsLayoutBinding? = null
+    private var isNeutralButtonActive = true
+
+    @Deprecated("Deprecated in Java")
+    override fun onAttach(activity: Activity) {
+        super.onAttach(activity)
+        require(activity is OnSyncedFolderPreferenceListener) {
+            (
+                "The host activity must implement " +
+                    OnSyncedFolderPreferenceListener::class.java.canonicalName
+                )
+        }
+    }
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        // keep the state of the fragment on configuration changes
+        retainInstance = true
+        binding = null
+
+        val arguments = arguments
+        if (arguments != null) {
+            syncedFolder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+                arguments.getParcelable(SYNCED_FOLDER_PARCELABLE, SyncedFolderParcelable::class.java)
+            } else {
+                @Suppress("DEPRECATION")
+                arguments.getParcelable(SYNCED_FOLDER_PARCELABLE)
+            }
+        }
+
+        uploadBehaviorItemStrings = resources.getTextArray(R.array.pref_behaviour_entries)
+        nameCollisionPolicyItemStrings = resources.getTextArray(R.array.pref_name_collision_policy_entries)
+    }
+
+    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+        Log_OC.d(TAG, "onCreateView, savedInstanceState is $savedInstanceState")
+        binding = SyncedFoldersSettingsLayoutBinding.inflate(requireActivity().layoutInflater, null, false)
+
+        setupDialogElements(binding!!)
+        setupListeners(binding!!)
+
+        val builder = MaterialAlertDialogBuilder(requireContext())
+        builder.setView(binding!!.getRoot())
+
+        viewThemeUtils?.dialog?.colorMaterialAlertDialogBackground(requireContext(), builder)
+
+        return builder.create()
+    }
+
+    /**
+     * find all relevant UI elements and set their values.
+     *
+     * @param binding the parent binding
+     */
+    private fun setupDialogElements(binding: SyncedFoldersSettingsLayoutBinding) {
+        setupLayout(binding)
+        applyUserColor(binding)
+        setButtonOrder(binding)
+        setValuesViaSyncedFolder(binding)
+    }
+
+    private fun setupLayout(binding: SyncedFoldersSettingsLayoutBinding) {
+        if (syncedFolder!!.type.id > MediaFolderType.CUSTOM.id) {
+            // hide local folder chooser and delete for non-custom folders
+            binding.localFolderContainer.visibility = View.GONE
+            isNeutralButtonActive = false
+        } else if (syncedFolder!!.id <= SyncedFolder.UNPERSISTED_ID) {
+            isNeutralButtonActive = false
+
+            // Hide delete/enabled for unpersisted custom folders
+            binding.syncEnabled.visibility = View.GONE
+
+            // auto set custom folder to enabled
+            syncedFolder?.isEnabled = true
+
+            // switch text to create headline
+            binding.syncedFoldersSettingsTitle.setText(R.string.autoupload_create_new_custom_folder)
+
+            // disable save button
+            binding.btnPositive.isEnabled = false
+        } else {
+            binding.localFolderContainer.visibility = View.GONE
+        }
+    }
+
+    private fun applyUserColor(binding: SyncedFoldersSettingsLayoutBinding) {
+        viewThemeUtils?.androidx?.colorSwitchCompat(binding.syncEnabled)
+
+        viewThemeUtils?.platform?.themeCheckbox(
+            binding.settingInstantUploadOnWifiCheckbox,
+            binding.settingInstantUploadOnChargingCheckbox,
+            binding.settingInstantUploadExistingCheckbox,
+            binding.settingInstantUploadPathUseSubfoldersCheckbox
+        )
+
+        viewThemeUtils?.material?.colorMaterialButtonPrimaryTonal(binding.btnPositive)
+        viewThemeUtils?.material?.colorMaterialButtonPrimaryBorderless(binding.btnNegative)
+        viewThemeUtils?.material?.colorMaterialButtonPrimaryBorderless(binding.btnNeutral)
+    }
+
+    private fun setButtonOrder(binding: SyncedFoldersSettingsLayoutBinding) {
+        // btnNeutral  btnNegative btnPositive
+        if (isNeutralButtonActive) {
+            // Cancel   Delete Save
+            binding.btnNeutral.setText(R.string.common_cancel)
+            binding.btnNegative.setText(R.string.common_delete)
+        } else {
+            //          Cancel Save
+            binding.btnNeutral.visibility = View.GONE
+            binding.btnNegative.setText(R.string.common_cancel)
+        }
+    }
+
+    private fun setValuesViaSyncedFolder(binding: SyncedFoldersSettingsLayoutBinding) {
+        syncedFolder?.let {
+            setEnabled(it.isEnabled)
+
+            if (!TextUtils.isEmpty(it.localPath)) {
+                binding.syncedFoldersSettingsLocalFolderPath.text = DisplayUtils.createTextWithSpan(
+                    String.format(
+                        getString(R.string.synced_folders_preferences_folder_path),
+                        it.localPath
+                    ),
+                    it.folderName,
+                    StyleSpan(Typeface.BOLD)
+                )
+                binding.localFolderSummary.text = FileStorageUtils.pathToUserFriendlyDisplay(
+                    it.localPath,
+                    activity,
+                    resources
+                )
+            } else {
+                binding.localFolderSummary.setText(R.string.choose_local_folder)
+            }
+
+            if (!TextUtils.isEmpty(it.localPath)) {
+                binding.remoteFolderSummary.text = it.remotePath
+            } else {
+                binding.remoteFolderSummary.setText(R.string.choose_remote_folder)
+            }
+
+            binding.settingInstantUploadOnWifiCheckbox.isChecked = it.isWifiOnly
+            binding.settingInstantUploadOnChargingCheckbox.isChecked = it.isChargingOnly
+            binding.settingInstantUploadExistingCheckbox.isChecked = it.isExisting
+            binding.settingInstantUploadPathUseSubfoldersCheckbox.isChecked = it.isSubfolderByDate
+
+            binding.settingInstantUploadSubfolderRuleSpinner.setSelection(it.subFolderRule.ordinal)
+
+            binding.settingInstantUploadSubfolderRuleContainer.visibility =
+                if (binding.settingInstantUploadPathUseSubfoldersCheckbox.isChecked) View.VISIBLE else View.GONE
+
+            binding.settingInstantBehaviourSummary.text = uploadBehaviorItemStrings[it.uploadActionInteger]
+            val nameCollisionPolicyIndex = getSelectionIndexForNameCollisionPolicy(
+                it.nameCollisionPolicy
+            )
+            binding.settingInstantNameCollisionPolicySummary.text =
+                nameCollisionPolicyItemStrings[nameCollisionPolicyIndex]
+        }
+    }
+
+    /**
+     * set correct icon/flag.
+     *
+     * @param enabled if enabled or disabled
+     */
+    private fun setEnabled(enabled: Boolean) {
+        syncedFolder?.isEnabled = enabled
+        binding?.syncEnabled?.isChecked = enabled
+        setupViews(binding, enabled)
+    }
+
+    /**
+     * set (new) remote path on activity result of the folder picker activity. The result gets originally propagated to
+     * the underlying activity since the picker is an activity and the result can't get passed to the dialog fragment
+     * directly.
+     *
+     * @param path the remote path to be set
+     */
+    fun setRemoteFolderSummary(path: String?) {
+        syncedFolder?.remotePath = path
+        binding?.remoteFolderSummary?.text = path
+        checkAndUpdateSaveButtonState()
+    }
+
+    /**
+     * set (new) local path on activity result of the folder picker activity. The result gets originally propagated to
+     * the underlying activity since the picker is an activity and the result can't get passed to the dialog fragment
+     * directly.
+     *
+     * @param path the local path to be set
+     */
+    fun setLocalFolderSummary(path: String?) {
+        syncedFolder?.localPath = path
+        binding?.localFolderSummary?.text = FileStorageUtils.pathToUserFriendlyDisplay(path, activity, resources)
+        binding?.syncedFoldersSettingsLocalFolderPath?.text = DisplayUtils.createTextWithSpan(
+            String.format(
+                getString(R.string.synced_folders_preferences_folder_path),
+                syncedFolder!!.localPath
+            ),
+            File(syncedFolder!!.localPath).name,
+            StyleSpan(Typeface.BOLD)
+        )
+        checkAndUpdateSaveButtonState()
+    }
+
+    private fun checkAndUpdateSaveButtonState() {
+        binding?.btnPositive?.isEnabled = syncedFolder!!.localPath != null && syncedFolder!!.remotePath != null
+        checkWritableFolder()
+    }
+
+    private fun checkWritableFolder() {
+        if (!syncedFolder!!.isEnabled) {
+            binding?.settingInstantBehaviourContainer?.isEnabled = false
+            binding?.settingInstantBehaviourContainer?.alpha = alphaDisabled
+            return
+        }
+        if (syncedFolder!!.localPath != null && File(syncedFolder!!.localPath).canWrite()) {
+            binding?.settingInstantBehaviourContainer?.isEnabled = true
+            binding?.settingInstantBehaviourContainer?.alpha = alphaEnabled
+            binding?.settingInstantBehaviourSummary?.text =
+                uploadBehaviorItemStrings[syncedFolder!!.uploadActionInteger]
+        } else {
+            binding?.settingInstantBehaviourContainer?.isEnabled = false
+            binding?.settingInstantBehaviourContainer?.alpha = alphaDisabled
+            syncedFolder?.setUploadAction(
+                resources.getTextArray(R.array.pref_behaviour_entryValues)[0].toString()
+            )
+            binding?.settingInstantBehaviourSummary?.setText(R.string.auto_upload_file_behaviour_kept_in_folder)
+        }
+    }
+
+    private fun setupViews(optionalBinding: SyncedFoldersSettingsLayoutBinding?, enable: Boolean) {
+        val alpha: Float = if (enable) {
+            alphaEnabled
+        } else {
+            alphaDisabled
+        }
+
+        optionalBinding?.let { binding ->
+            binding.settingInstantUploadOnWifiContainer.isEnabled = enable
+            binding.settingInstantUploadOnWifiContainer.alpha = alpha
+            binding.settingInstantUploadOnChargingContainer.isEnabled = enable
+            binding.settingInstantUploadOnChargingContainer.alpha = alpha
+            binding.settingInstantUploadExistingContainer.isEnabled = enable
+            binding.settingInstantUploadExistingContainer.alpha = alpha
+            binding.settingInstantUploadPathUseSubfoldersContainer.isEnabled = enable
+            binding.settingInstantUploadPathUseSubfoldersContainer.alpha = alpha
+            binding.remoteFolderContainer.isEnabled = enable
+            binding.remoteFolderContainer.alpha = alpha
+            binding.localFolderContainer.isEnabled = enable
+            binding.localFolderContainer.alpha = alpha
+            binding.settingInstantNameCollisionPolicyContainer.isEnabled = enable
+            binding.settingInstantNameCollisionPolicyContainer.alpha = alpha
+            binding.settingInstantUploadOnWifiCheckbox.isEnabled = enable
+            binding.settingInstantUploadOnChargingCheckbox.isEnabled = enable
+            binding.settingInstantUploadExistingCheckbox.isEnabled = enable
+            binding.settingInstantUploadPathUseSubfoldersCheckbox.isEnabled = enable
+        }
+
+        checkWritableFolder()
+    }
+
+    /**
+     * setup all listeners.
+     *
+     * @param binding the parent binding
+     */
+    private fun setupListeners(binding: SyncedFoldersSettingsLayoutBinding) {
+        binding.btnPositive.setOnClickListener(OnSyncedFolderSaveClickListener())
+        if (isNeutralButtonActive) {
+            binding.btnNeutral.setOnClickListener(OnSyncedFolderCancelClickListener())
+            binding.btnNegative.setOnClickListener(OnSyncedFolderDeleteClickListener())
+        } else {
+            binding.btnNegative.setOnClickListener(OnSyncedFolderCancelClickListener())
+        }
+
+        syncedFolder?.let { syncedFolder ->
+            binding.settingInstantUploadOnWifiContainer.setOnClickListener {
+                syncedFolder.isWifiOnly = !syncedFolder.isWifiOnly
+                binding.settingInstantUploadOnWifiCheckbox.toggle()
+            }
+            binding.settingInstantUploadOnChargingContainer.setOnClickListener {
+                syncedFolder.isChargingOnly = !syncedFolder.isChargingOnly
+                binding.settingInstantUploadOnChargingCheckbox.toggle()
+            }
+            binding.settingInstantUploadExistingContainer.setOnClickListener {
+                syncedFolder.isExisting = !syncedFolder.isExisting
+                binding.settingInstantUploadExistingCheckbox.toggle()
+            }
+            binding.settingInstantUploadPathUseSubfoldersContainer.setOnClickListener {
+                syncedFolder.isSubfolderByDate = !syncedFolder.isSubfolderByDate
+                binding.settingInstantUploadPathUseSubfoldersCheckbox.toggle()
+
+                // Only allow setting subfolder rule if subfolder is allowed
+                if (binding.settingInstantUploadPathUseSubfoldersCheckbox.isChecked) {
+                    binding.settingInstantUploadSubfolderRuleContainer.visibility = View.VISIBLE
+                } else {
+                    binding.settingInstantUploadSubfolderRuleContainer.visibility = View.GONE
+                }
+            }
+            binding.settingInstantUploadSubfolderRuleSpinner.onItemSelectedListener =
+                object : AdapterView.OnItemSelectedListener {
+                    override fun onItemSelected(adapterView: AdapterView<*>?, view: View, i: Int, l: Long) {
+                        syncedFolder.subFolderRule = SubFolderRule.values()[i]
+                    }
+
+                    override fun onNothingSelected(adapterView: AdapterView<*>?) {
+                        syncedFolder.subFolderRule = SubFolderRule.YEAR_MONTH
+                    }
+                }
+
+            binding.syncEnabled.setOnClickListener { setEnabled(!syncedFolder.isEnabled) }
+        }
+
+        binding.remoteFolderContainer.setOnClickListener {
+            val action = Intent(activity, FolderPickerActivity::class.java)
+            requireActivity().startActivityForResult(action, REQUEST_CODE__SELECT_REMOTE_FOLDER)
+        }
+        binding.localFolderContainer.setOnClickListener {
+            val action = Intent(activity, UploadFilesActivity::class.java)
+            action.putExtra(UploadFilesActivity.KEY_LOCAL_FOLDER_PICKER_MODE, true)
+            action.putExtra(UploadFilesActivity.REQUEST_CODE_KEY, REQUEST_CODE__SELECT_LOCAL_FOLDER)
+            requireActivity().startActivityForResult(action, REQUEST_CODE__SELECT_LOCAL_FOLDER)
+        }
+
+        binding.settingInstantBehaviourContainer.setOnClickListener { showBehaviourDialog() }
+        binding.settingInstantNameCollisionPolicyContainer.setOnClickListener { showNameCollisionPolicyDialog() }
+    }
+
+    private fun showBehaviourDialog() {
+        val builder = MaterialAlertDialogBuilder(requireActivity())
+
+        syncedFolder?.let {
+            val behaviourEntries = resources.getTextArray(R.array.pref_behaviour_entries)
+            val behaviourEntryValues = resources.getTextArray(R.array.pref_behaviour_entryValues)
+            builder.setTitle(R.string.prefs_instant_behaviour_dialogTitle)
+                .setSingleChoiceItems(behaviourEntries, it.uploadActionInteger) { dialog: DialogInterface, which: Int ->
+                    it.setUploadAction(behaviourEntryValues[which].toString())
+                    binding?.settingInstantBehaviourSummary?.text = uploadBehaviorItemStrings[which]
+                    behaviourDialogShown = false
+                    dialog.dismiss()
+                }
+                .setOnCancelListener { behaviourDialogShown = false }
+        }
+
+        behaviourDialogShown = true
+        viewThemeUtils?.dialog?.colorMaterialAlertDialogBackground(requireActivity(), builder)
+
+        behaviourDialog = builder.create()
+        behaviourDialog?.show()
+    }
+
+    private fun showNameCollisionPolicyDialog() {
+        syncedFolder?.let {
+            val builder = MaterialAlertDialogBuilder(requireActivity())
+            builder.setTitle(R.string.pref_instant_name_collision_policy_dialogTitle)
+                .setSingleChoiceItems(
+                    resources.getTextArray(R.array.pref_name_collision_policy_entries),
+                    getSelectionIndexForNameCollisionPolicy(it.nameCollisionPolicy),
+                    OnNameCollisionDialogClickListener()
+                )
+                .setOnCancelListener { nameCollisionPolicyDialogShown = false }
+
+            nameCollisionPolicyDialogShown = true
+
+            viewThemeUtils?.dialog?.colorMaterialAlertDialogBackground(requireActivity(), builder)
+            behaviourDialog = builder.create()
+            behaviourDialog?.show()
+        }
+    }
+
+    override fun onDestroyView() {
+        Log_OC.d(TAG, "destroy SyncedFolderPreferencesDialogFragment view")
+        if (dialog != null && retainInstance) {
+            dialog?.setDismissMessage(null)
+        }
+        if (behaviourDialog != null && behaviourDialog!!.isShowing) {
+            behaviourDialog?.dismiss()
+        }
+
+        super.onDestroyView()
+    }
+
+    override fun onSaveInstanceState(outState: Bundle) {
+        outState.putBoolean(BEHAVIOUR_DIALOG_STATE, behaviourDialogShown)
+        outState.putBoolean(NAME_COLLISION_POLICY_DIALOG_STATE, nameCollisionPolicyDialogShown)
+        super.onSaveInstanceState(outState)
+    }
+
+    override fun onViewStateRestored(savedInstanceState: Bundle?) {
+        behaviourDialogShown = savedInstanceState != null &&
+            savedInstanceState.getBoolean(BEHAVIOUR_DIALOG_STATE, false)
+        nameCollisionPolicyDialogShown = savedInstanceState != null &&
+            savedInstanceState.getBoolean(NAME_COLLISION_POLICY_DIALOG_STATE, false)
+        if (behaviourDialogShown) {
+            showBehaviourDialog()
+        }
+        if (nameCollisionPolicyDialogShown) {
+            showNameCollisionPolicyDialog()
+        }
+
+        super.onViewStateRestored(savedInstanceState)
+    }
+
+    interface OnSyncedFolderPreferenceListener {
+        fun onSaveSyncedFolderPreference(syncedFolder: SyncedFolderParcelable?)
+        fun onCancelSyncedFolderPreference()
+        fun onDeleteSyncedFolderPreference(syncedFolder: SyncedFolderParcelable?)
+    }
+
+    private inner class OnSyncedFolderSaveClickListener : View.OnClickListener {
+        override fun onClick(v: View) {
+            dismiss()
+            (activity as OnSyncedFolderPreferenceListener?)?.onSaveSyncedFolderPreference(syncedFolder)
+        }
+    }
+
+    private inner class OnSyncedFolderCancelClickListener : View.OnClickListener {
+        override fun onClick(v: View) {
+            dismiss()
+            (activity as OnSyncedFolderPreferenceListener?)?.onCancelSyncedFolderPreference()
+        }
+    }
+
+    private inner class OnSyncedFolderDeleteClickListener : View.OnClickListener {
+        override fun onClick(v: View) {
+            dismiss()
+            (activity as OnSyncedFolderPreferenceListener?)?.onDeleteSyncedFolderPreference(syncedFolder)
+        }
+    }
+
+    private inner class OnNameCollisionDialogClickListener : DialogInterface.OnClickListener {
+        override fun onClick(dialog: DialogInterface, which: Int) {
+            syncedFolder!!.nameCollisionPolicy =
+                getNameCollisionPolicyForSelectionIndex(which)
+            binding?.settingInstantNameCollisionPolicySummary?.text =
+                nameCollisionPolicyItemStrings[which]
+            nameCollisionPolicyDialogShown = false
+            dialog.dismiss()
+        }
+    }
+
+    companion object {
+        const val SYNCED_FOLDER_PARCELABLE = "SyncedFolderParcelable"
+        const val REQUEST_CODE__SELECT_REMOTE_FOLDER = 0
+        const val REQUEST_CODE__SELECT_LOCAL_FOLDER = 1
+        private val TAG = SyncedFolderPreferencesDialogFragment::class.java.simpleName
+        private const val BEHAVIOUR_DIALOG_STATE = "BEHAVIOUR_DIALOG_STATE"
+        private const val NAME_COLLISION_POLICY_DIALOG_STATE = "NAME_COLLISION_POLICY_DIALOG_STATE"
+        private const val alphaEnabled = 1.0f
+        private const val alphaDisabled = 0.7f
+
+        @JvmStatic
+        fun newInstance(syncedFolder: SyncedFolderDisplayItem?, section: Int): SyncedFolderPreferencesDialogFragment {
+            requireNotNull(syncedFolder) { "SyncedFolder is mandatory but NULL!" }
+            val args = Bundle()
+            args.putParcelable(SYNCED_FOLDER_PARCELABLE, SyncedFolderParcelable(syncedFolder, section))
+            val dialogFragment = SyncedFolderPreferencesDialogFragment()
+            dialogFragment.arguments = args
+            dialogFragment.setStyle(STYLE_NORMAL, R.style.Theme_ownCloud_Dialog)
+            return dialogFragment
+        }
+
+        /**
+         * Get index for name collision selection dialog.
+         *
+         * @return 0 if ASK_USER, 1 if OVERWRITE, 2 if RENAME, 3 if SKIP, Otherwise: 0
+         */
+        @Suppress("MagicNumber")
+        private fun getSelectionIndexForNameCollisionPolicy(nameCollisionPolicy: NameCollisionPolicy): Int {
+            return when (nameCollisionPolicy) {
+                NameCollisionPolicy.OVERWRITE -> 1
+                NameCollisionPolicy.RENAME -> 2
+                NameCollisionPolicy.CANCEL -> 3
+                NameCollisionPolicy.ASK_USER -> 0
+            }
+        }
+
+        /**
+         * Get index for name collision selection dialog. Inverse of getSelectionIndexForNameCollisionPolicy.
+         *
+         * @return ASK_USER if 0, OVERWRITE if 1, RENAME if 2, SKIP if 3. Otherwise: ASK_USER
+         */
+        @Suppress("MagicNumber")
+        private fun getNameCollisionPolicyForSelectionIndex(index: Int): NameCollisionPolicy {
+            return when (index) {
+                1 -> NameCollisionPolicy.OVERWRITE
+                2 -> NameCollisionPolicy.RENAME
+                3 -> NameCollisionPolicy.CANCEL
+                0 -> NameCollisionPolicy.ASK_USER
+                else -> NameCollisionPolicy.ASK_USER
+            }
+        }
+    }
+}

+ 3 - 4
app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java

@@ -278,8 +278,7 @@ public class FileDetailFragment extends FileFragment implements OnClickListener,
                 R.id.action_favorite,
                 R.id.action_unset_favorite,
                 R.id.action_see_details,
-                R.id.action_move,
-                R.id.action_copy,
+                R.id.action_move_or_copy,
                 R.id.action_stream_media,
                 R.id.action_send_share_file,
                 R.id.action_pin_to_homescreen
@@ -694,7 +693,7 @@ public class FileDetailFragment extends FileFragment implements OnClickListener,
         if (progressListener != null) {
             if (containerActivity.getFileDownloaderBinder() != null) {
                 containerActivity.getFileDownloaderBinder().
-                        addDatatransferProgressListener(progressListener, getFile());
+                        addDataTransferProgressListener(progressListener, getFile());
             }
             if (containerActivity.getFileUploaderBinder() != null) {
                 containerActivity.getFileUploaderBinder().
@@ -709,7 +708,7 @@ public class FileDetailFragment extends FileFragment implements OnClickListener,
         if (progressListener != null) {
             if (containerActivity.getFileDownloaderBinder() != null) {
                 containerActivity.getFileDownloaderBinder().
-                        removeDatatransferProgressListener(progressListener, getFile());
+                    removeDataTransferProgressListener(progressListener, getFile());
             }
             if (containerActivity.getFileUploaderBinder() != null) {
                 containerActivity.getFileUploaderBinder().

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

@@ -26,6 +26,7 @@
  */
 package com.owncloud.android.ui.fragment;
 
+import android.annotation.SuppressLint;
 import android.app.Activity;
 import android.content.Context;
 import android.content.Intent;
@@ -452,25 +453,13 @@ public class OCFileListFragment extends ExtendedListFragment implements
         if (isSearchEventSet(event)) {
 
             switch (event.getSearchType()) {
-                case FILE_SEARCH:
-                    currentSearchType = FILE_SEARCH;
-                    break;
-
-                case FAVORITE_SEARCH:
-                    currentSearchType = FAVORITE_SEARCH;
-                    break;
-
-                case RECENTLY_MODIFIED_SEARCH:
-                    currentSearchType = RECENTLY_MODIFIED_SEARCH;
-                    break;
-
-                case SHARED_FILTER:
-                    currentSearchType = SHARED_FILTER;
-                    break;
-
-                default:
-                    // do nothing
-                    break;
+                case FILE_SEARCH -> currentSearchType = FILE_SEARCH;
+                case FAVORITE_SEARCH -> currentSearchType = FAVORITE_SEARCH;
+                case RECENTLY_MODIFIED_SEARCH -> currentSearchType = RECENTLY_MODIFIED_SEARCH;
+                case SHARED_FILTER -> currentSearchType = SHARED_FILTER;
+                default -> {
+                }
+                // do nothing
             }
 
             prepareActionBarItems(event);
@@ -1048,18 +1037,11 @@ public class OCFileListFragment extends ExtendedListFragment implements
                     if (PreviewImageFragment.canBePreviewed(file)) {
                         // preview image - it handles the download, if needed
                         if (searchFragment) {
-                            VirtualFolderType type;
-                            switch (currentSearchType) {
-                                case FAVORITE_SEARCH:
-                                    type = VirtualFolderType.FAVORITE;
-                                    break;
-                                case GALLERY_SEARCH:
-                                    type = VirtualFolderType.GALLERY;
-                                    break;
-                                default:
-                                    type = VirtualFolderType.NONE;
-                                    break;
-                            }
+                            VirtualFolderType type = switch (currentSearchType) {
+                                case FAVORITE_SEARCH -> VirtualFolderType.FAVORITE;
+                                case GALLERY_SEARCH -> VirtualFolderType.GALLERY;
+                                default -> VirtualFolderType.NONE;
+                            };
                             ((FileDisplayActivity) mContainerActivity).startImagePreview(file, type, !file.isDown());
                         } else {
                             ((FileDisplayActivity) mContainerActivity).startImagePreview(file, !file.isDown());
@@ -1243,11 +1225,8 @@ public class OCFileListFragment extends ExtendedListFragment implements
         } else if (itemId == R.id.action_unset_favorite) {
             mContainerActivity.getFileOperationsHelper().toggleFavoriteFiles(checkedFiles, false);
             return true;
-        } else if (itemId == R.id.action_move) {
-            pickFolderForMoveOrCopy(FolderPickerActivity.MOVE, checkedFiles);
-            return true;
-        } else if (itemId == R.id.action_copy) {
-            pickFolderForMoveOrCopy(FolderPickerActivity.COPY, checkedFiles);
+        } else if (itemId == R.id.action_move_or_copy) {
+            pickFolderForMoveOrCopy(checkedFiles);
             return true;
         } else if (itemId == R.id.action_select_all_action_menu) {
             selectAllFiles(true);
@@ -1265,12 +1244,9 @@ public class OCFileListFragment extends ExtendedListFragment implements
         return false;
     }
 
-    private void pickFolderForMoveOrCopy(final String extraAction, final Set<OCFile> checkedFiles) {
-        int requestCode = switch (extraAction) {
-            case FolderPickerActivity.MOVE -> FileDisplayActivity.REQUEST_CODE__MOVE_FILES;
-            case FolderPickerActivity.COPY -> FileDisplayActivity.REQUEST_CODE__COPY_FILES;
-            default -> throw new IllegalArgumentException("Unknown extra action: " + extraAction);
-        };
+    private void pickFolderForMoveOrCopy(final Set<OCFile> checkedFiles) {
+        int requestCode = FileDisplayActivity.REQUEST_CODE__MOVE_OR_COPY_FILES;
+        String extraAction = FolderPickerActivity.MOVE_OR_COPY;
 
         final Intent action = new Intent(requireActivity(), FolderPickerActivity.class);
         final ArrayList<String> paths = new ArrayList<>(checkedFiles.size());
@@ -1392,12 +1368,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
             setGridSwitchButton();
         }
 
-        if (mHideFab) {
-            setFabVisible(false);
-        } else {
-            setFabVisible(true);
-            // registerFabListener();
-        }
+        setFabVisible(!mHideFab);
 
         // FAB
         setFabEnabled(mFile != null && mFile.canWrite());
@@ -1449,6 +1420,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
         }
     }
 
+    @SuppressLint("NotifyDataSetChanged")
     public void switchLayoutManager(boolean grid) {
         int position = 0;
 
@@ -1496,21 +1468,11 @@ public class OCFileListFragment extends ExtendedListFragment implements
 
         if (requireActivity() instanceof FileDisplayActivity && currentSearchType != null) {
             switch (currentSearchType) {
-                case FAVORITE_SEARCH:
-                    setTitle(R.string.drawer_item_favorites);
-                    break;
-                case GALLERY_SEARCH:
-                    setTitle(R.string.drawer_item_gallery);
-                    break;
-                case RECENTLY_MODIFIED_SEARCH:
-                    setTitle(R.string.drawer_item_recently_modified);
-                    break;
-                case SHARED_FILTER:
-                    setTitle(R.string.drawer_item_shared);
-                    break;
-                default:
-                    setTitle(themeUtils.getDefaultDisplayNameForRootFolder(getContext()), false);
-                    break;
+                case FAVORITE_SEARCH -> setTitle(R.string.drawer_item_favorites);
+                case GALLERY_SEARCH -> setTitle(R.string.drawer_item_gallery);
+                case RECENTLY_MODIFIED_SEARCH -> setTitle(R.string.drawer_item_recently_modified);
+                case SHARED_FILTER -> setTitle(R.string.drawer_item_shared);
+                default -> setTitle(themeUtils.getDefaultDisplayNameForRootFolder(getContext()), false);
             }
         }
 
@@ -1519,14 +1481,11 @@ public class OCFileListFragment extends ExtendedListFragment implements
     protected void prepareActionBarItems(SearchEvent event) {
         if (event != null) {
             switch (event.getSearchType()) {
-                case FAVORITE_SEARCH:
-                case RECENTLY_MODIFIED_SEARCH:
+                case FAVORITE_SEARCH, RECENTLY_MODIFIED_SEARCH ->
                     menuItemAddRemoveValue = MenuItemAddRemove.REMOVE_SORT;
-                    break;
-
-                default:
-                    // do nothing
-                    break;
+                default -> {
+                }
+                // do nothing
             }
         }
 
@@ -1538,25 +1497,11 @@ public class OCFileListFragment extends ExtendedListFragment implements
     protected void setEmptyView(SearchEvent event) {
         if (event != null) {
             switch (event.getSearchType()) {
-                case FILE_SEARCH:
-                    setEmptyListMessage(SearchType.FILE_SEARCH);
-                    break;
-
-                case FAVORITE_SEARCH:
-                    setEmptyListMessage(SearchType.FAVORITE_SEARCH);
-                    break;
-
-                case RECENTLY_MODIFIED_SEARCH:
-                    setEmptyListMessage(SearchType.RECENTLY_MODIFIED_SEARCH);
-                    break;
-
-                case SHARED_FILTER:
-                    setEmptyListMessage(SearchType.SHARED_FILTER);
-                    break;
-
-                default:
-                    setEmptyListMessage(SearchType.NO_SEARCH);
-                    break;
+                case FILE_SEARCH -> setEmptyListMessage(SearchType.FILE_SEARCH);
+                case FAVORITE_SEARCH -> setEmptyListMessage(SearchType.FAVORITE_SEARCH);
+                case RECENTLY_MODIFIED_SEARCH -> setEmptyListMessage(SearchType.RECENTLY_MODIFIED_SEARCH);
+                case SHARED_FILTER -> setEmptyListMessage(SearchType.SHARED_FILTER);
+                default -> setEmptyListMessage(SearchType.NO_SEARCH);
             }
         } else {
             setEmptyListMessage(SearchType.NO_SEARCH);

+ 12 - 5
app/src/main/java/com/owncloud/android/ui/fragment/UnifiedSearchFragment.kt

@@ -30,7 +30,6 @@ import android.view.ViewGroup
 import android.widget.ImageView
 import androidx.annotation.VisibleForTesting
 import androidx.appcompat.widget.SearchView
-import androidx.core.view.MenuItemCompat
 import androidx.core.view.updatePadding
 import androidx.fragment.app.Fragment
 import androidx.lifecycle.ViewModelProvider
@@ -89,7 +88,7 @@ class UnifiedSearchFragment : Fragment(), Injectable, UnifiedSearchListInterface
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
-        vm = ViewModelProvider(this, vmFactory).get(UnifiedSearchViewModel::class.java)
+        vm = ViewModelProvider(this, vmFactory)[UnifiedSearchViewModel::class.java]
         setUpViewModel()
 
         val query = savedInstanceState?.getString(ARG_QUERY) ?: arguments?.getString(ARG_QUERY)
@@ -125,7 +124,7 @@ class UnifiedSearchFragment : Fragment(), Injectable, UnifiedSearchListInterface
                     binding.emptyList.emptyListViewText.text =
                         requireContext().getString(R.string.file_list_empty_unified_search_no_results)
                     binding.emptyList.emptyListIcon.setImageDrawable(
-                        viewThemeUtils.platform.tintPrimaryDrawable(requireContext(), R.drawable.ic_search_grey)
+                        viewThemeUtils.platform.tintDrawable(requireContext(), R.drawable.ic_search_grey)
                     )
                 }
             }
@@ -151,10 +150,12 @@ class UnifiedSearchFragment : Fragment(), Injectable, UnifiedSearchListInterface
         }
     }
 
-    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+    @Suppress("DEPRECATION")
+    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
         _binding = ListFragmentBinding.inflate(inflater, container, false)
         binding.listRoot.updatePadding(top = resources.getDimension(R.dimen.standard_half_padding).toInt())
         setUpBinding()
+
         setHasOptionsMenu(true)
         return binding.root
     }
@@ -223,8 +224,14 @@ class UnifiedSearchFragment : Fragment(), Injectable, UnifiedSearchListInterface
     @Deprecated("Deprecated in Java")
     override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
         val item = menu.findItem(R.id.action_search)
-        searchView = MenuItemCompat.getActionView(item) as SearchView
+        searchView = item.actionView as SearchView?
+
+        // Required to align with TextView width.
+        // Because this fragment is opened with TextView onClick on the previous screen
+        searchView?.maxWidth = Integer.MAX_VALUE
+
         viewThemeUtils.androidx.themeToolbarSearchView(searchView!!)
+
         searchView?.setQuery(vm.query.value, false)
         searchView?.setOnQueryTextListener(this)
         searchView?.isIconified = false

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

@@ -1007,27 +1007,7 @@ public class FileOperationsHelper {
         }
     }
 
-    /**
-     * Start operations to move one or several files
-     *
-     * @param filePaths    Remote paths of files to move
-     * @param targetFolder Folder where the files while be moved into
-     */
-    public void moveFiles(final List<String> filePaths, final OCFile targetFolder) {
-        copyOrMoveFiles(OperationsService.ACTION_MOVE_FILE, filePaths, targetFolder);
-    }
-
-    /**
-     * Start operations to copy one or several files
-     *
-     * @param filePaths    Remote paths of files to move
-     * @param targetFolder Folder where the files while be copied into
-     */
-    public void copyFiles(final List<String> filePaths, final OCFile targetFolder) {
-        copyOrMoveFiles(OperationsService.ACTION_COPY_FILE, filePaths, targetFolder);
-    }
-
-    private void copyOrMoveFiles(final String action, final List<String> filePaths, final OCFile targetFolder) {
+    public void moveOrCopyFiles(String action, final List<String> filePaths, final OCFile targetFolder) {
         for (String path : filePaths) {
             Intent service = new Intent(fileActivity, OperationsService.class);
             service.setAction(action);
@@ -1039,7 +1019,6 @@ public class FileOperationsHelper {
         fileActivity.showLoadingDialog(fileActivity.getString(R.string.wait_a_moment));
     }
 
-
     public void exportFiles(Collection<OCFile> files,
                             Context context,
                             View view,

+ 2 - 2
app/src/main/java/com/owncloud/android/ui/preview/FileDownloadFragment.java

@@ -261,7 +261,7 @@ public class FileDownloadFragment extends FileFragment implements OnClickListene
 
     public void listenForTransferProgress() {
         if (mProgressListener != null && !mListening && containerActivity.getFileDownloaderBinder() != null) {
-            containerActivity.getFileDownloaderBinder().addDatatransferProgressListener(mProgressListener, getFile());
+            containerActivity.getFileDownloaderBinder().addDataTransferProgressListener(mProgressListener, getFile());
             mListening = true;
             setButtonsForTransferring();
         }
@@ -271,7 +271,7 @@ public class FileDownloadFragment extends FileFragment implements OnClickListene
     public void leaveTransferProgress() {
         if (mProgressListener != null && containerActivity.getFileDownloaderBinder() != null) {
             containerActivity.getFileDownloaderBinder()
-                .removeDatatransferProgressListener(mProgressListener, getFile());
+                .removeDataTransferProgressListener(mProgressListener, getFile());
             mListening = false;
         }
     }

+ 1 - 2
app/src/main/java/com/owncloud/android/ui/preview/PreviewImageFragment.java

@@ -375,8 +375,7 @@ public class PreviewImageFragment extends FileFragment implements Injectable {
             Arrays.asList(
                 R.id.action_rename_file,
                 R.id.action_sync_file,
-                R.id.action_move,
-                R.id.action_copy,
+                R.id.action_move_or_copy,
                 R.id.action_favorite,
                 R.id.action_unset_favorite,
                 R.id.action_pin_to_homescreen

+ 1 - 2
app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaFragment.java

@@ -424,8 +424,7 @@ public class PreviewMediaFragment extends FileFragment implements OnTouchListene
             Arrays.asList(
                 R.id.action_rename_file,
                 R.id.action_sync_file,
-                R.id.action_move,
-                R.id.action_copy,
+                R.id.action_move_or_copy,
                 R.id.action_favorite,
                 R.id.action_unset_favorite,
                 R.id.action_pin_to_homescreen

+ 1 - 2
app/src/main/java/com/owncloud/android/ui/preview/PreviewTextFileFragment.java

@@ -300,8 +300,7 @@ public class PreviewTextFileFragment extends PreviewTextFragment {
             Arrays.asList(
                 R.id.action_rename_file,
                 R.id.action_sync_file,
-                R.id.action_move,
-                R.id.action_copy,
+                R.id.action_move_or_copy,
                 R.id.action_favorite,
                 R.id.action_unset_favorite,
                 R.id.action_pin_to_homescreen

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

@@ -0,0 +1,110 @@
+/*
+ * 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.utils
+
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.net.Uri
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.owncloud.android.R
+
+class WebViewUtil(private val context: Context) {
+
+    private val packageName = "com.google.android.webview"
+
+    fun checkWebViewVersion() {
+        if (!isWebViewVersionValid()) {
+            showUpdateDialog()
+        }
+    }
+
+    private fun isWebViewVersionValid(): Boolean {
+        val currentWebViewVersion = getCurrentWebViewMajorVersion() ?: return true
+        val minSupportedWebViewVersion: String = getMinimumSupportedMajorWebViewVersion()
+        return currentWebViewVersion.toInt() >= minSupportedWebViewVersion.toInt()
+    }
+
+    private fun showUpdateDialog() {
+        val builder = MaterialAlertDialogBuilder(context)
+            .setTitle(context.getString(R.string.webview_version_check_alert_dialog_title))
+            .setMessage(context.getString(R.string.webview_version_check_alert_dialog_message))
+            .setCancelable(false)
+            .setPositiveButton(
+                context.getString(R.string.webview_version_check_alert_dialog_positive_button_title)
+            ) { _, _ ->
+                redirectToAndroidSystemWebViewStorePage()
+            }
+
+        val dialog = builder.create()
+        dialog.show()
+    }
+
+    private fun redirectToAndroidSystemWebViewStorePage() {
+        val uri = Uri.parse("market://details?id=$packageName")
+        val intent = Intent(Intent.ACTION_VIEW, uri)
+        intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
+
+        try {
+            context.startActivity(intent)
+        } catch (e: android.content.ActivityNotFoundException) {
+            redirectToPlayStoreWebsiteForAndroidSystemWebView()
+        }
+    }
+
+    private fun redirectToPlayStoreWebsiteForAndroidSystemWebView() {
+        val playStoreWebUri = Uri.parse("https://play.google.com/store/apps/details?id=$packageName")
+        val webIntent = Intent(Intent.ACTION_VIEW, playStoreWebUri)
+        context.startActivity(webIntent)
+    }
+
+    private fun getCurrentWebViewMajorVersion(): String? {
+        val pm: PackageManager = context.packageManager
+
+        return try {
+            val pi = pm.getPackageInfo("com.google.android.webview", 0)
+            val fullVersion = pi.versionName
+
+            // Split the version string by "." and get the first part
+            val versionParts = fullVersion.split("\\.".toRegex()).dropLastWhile { it.isEmpty() }
+                .toTypedArray()
+
+            if (versionParts.isNotEmpty()) {
+                versionParts[0]
+            } else {
+                null
+            }
+        } catch (e: PackageManager.NameNotFoundException) {
+            null
+        }
+    }
+
+    /**
+     * Ideally we should fetch from database, reading actual value
+     * from PlayStore not feasible due to frequently api changes made by
+     * Google
+     *
+     */
+    private fun getMinimumSupportedMajorWebViewVersion(): String {
+        return "118"
+    }
+}

+ 0 - 10
app/src/main/res/drawable/ic_move.xml

@@ -1,10 +0,0 @@
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="24dp"
-    android:height="24dp"
-    android:viewportWidth="24"
-    android:viewportHeight="24"
-    android:tint="?attr/colorControlNormal">
-    <path
-        android:fillColor="@android:color/white"
-        android:pathData="M14 2H6C4.9 2 4 2.9 4 4V20C4 20.41 4.12 20.8 4.34 21.12C4.41 21.23 4.5 21.33 4.59 21.41C4.95 21.78 5.45 22 6 22H13.53C13 21.42 12.61 20.75 12.35 20H6V4H13V9H18V12C18.7 12 19.37 12.12 20 12.34V8L14 2M18 23L23 18.5L20 15.8L18 14V17H14V20H18V23Z" />
-</vector>

+ 22 - 10
app/src/main/res/layout/files_folder_picker.xml

@@ -46,27 +46,39 @@
 	<LinearLayout
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:gravity="center"
         android:orientation="horizontal"
 		android:padding="@dimen/standard_padding">
 
         <com.google.android.material.button.MaterialButton
             android:id="@+id/folder_picker_btn_cancel"
-            style="@style/OutlinedButton"
+            style="@style/Widget.Material3.Button.TextButton"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:layout_weight="1"
             android:text="@string/common_cancel"
-            android:layout_marginEnd="@dimen/standard_half_margin"
+            android:layout_weight="1"
+            app:cornerRadius="@dimen/button_corner_radius" />
+
+        <View
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:layout_weight="1"/>
+
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/folder_picker_btn_copy"
+            style="@style/Widget.Material3.Button.IconButton.Filled"
+            android:layout_width="wrap_content"
+            android:layout_weight="1"
+            android:layout_height="wrap_content"
+            android:text="@string/folder_picker_copy_button_text"
             app:cornerRadius="@dimen/button_corner_radius" />
 
         <com.google.android.material.button.MaterialButton
-		    android:id="@+id/folder_picker_btn_choose"
-            android:theme="@style/Button.Primary"
-		    android:layout_width="wrap_content"
-		    android:layout_height="wrap_content"
-		    android:layout_weight="1"
-		    android:text="@string/folder_picker_choose_button_text"
+            android:id="@+id/folder_picker_btn_move"
+            style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
+            android:layout_width="wrap_content"
+            android:layout_weight="1"
+            android:layout_height="wrap_content"
+            android:text="@string/folder_picker_move_button_text"
             app:cornerRadius="@dimen/button_corner_radius" />
 
 	</LinearLayout>

+ 4 - 3
app/src/main/res/layout/loading_dialog.xml

@@ -16,7 +16,8 @@
   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 -->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/loadingLayout"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
@@ -25,14 +26,14 @@
 
     <ProgressBar
         android:id="@+id/loadingBar"
-        style="?android:attr/progressBarStyle"
+        style="@style/Widget.Material3.CircularProgressIndicator"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_gravity="center_vertical"
         android:indeterminate="true"
         android:indeterminateOnly="false"/>
 
-    <TextView
+    <com.google.android.material.textview.MaterialTextView
         android:id="@+id/loadingText"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"

+ 70 - 238
app/src/main/res/layout/sorting_order_fragment.xml

@@ -18,263 +18,95 @@
   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/>.
 -->
-<LinearLayout
-    android:id="@+id/root"
+<ScrollView
     xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:padding="@dimen/standard_padding"
     android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:minWidth="300dp"
-    android:padding="@dimen/dialog_padding"
-    android:orientation="vertical">
+    android:layout_height="wrap_content">
 
-    <TextView
-        android:id="@+id/header"
-        style="@style/Base.DialogWindowTitle.AppCompat"
+    <LinearLayout
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:text="@string/sort_by"
-        android:paddingBottom="@dimen/standard_padding"/>
-
-    <ScrollView
-        android:id="@+id/scrollableSortings"
-        android:layout_width="match_parent"
-        android:layout_height="0dp"
-        android:layout_weight="1">
+        android:orientation="vertical">
 
-        <TableLayout
+        <com.google.android.material.textview.MaterialTextView
+            android:id="@+id/header"
+            style="@style/Base.DialogWindowTitle.AppCompat"
             android:layout_width="match_parent"
-            android:layout_height="wrap_content">
-
-            <TableRow
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content">
-
-                <ImageButton
-                    android:id="@+id/sortByNameAscending"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:layout_gravity="center_vertical"
-                    android:background="@color/transparent"
-                    android:contentDescription="@string/sort_by_name_ascending"
-                    android:paddingStart="@dimen/standard_padding"
-                    android:paddingTop="@dimen/standard_half_padding"
-                    android:paddingEnd="@dimen/standard_half_padding"
-                    android:paddingBottom="@dimen/standard_half_padding"
-                    android:src="@drawable/ic_alphabetical_asc" />
-
-                <TextView
-                    android:id="@+id/sortByNameAZText"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:layout_gravity="center_vertical"
-                    android:layout_weight="1"
-                    android:ellipsize="middle"
-                    android:singleLine="true"
-                    android:paddingBottom="@dimen/standard_half_padding"
-                    android:paddingEnd="@dimen/standard_double_padding"
-                    android:paddingStart="@dimen/zero"
-                    android:paddingTop="@dimen/standard_half_padding"
-                    android:text="@string/menu_item_sort_by_name_a_z"
-                    android:textColor="@color/standard_grey"
-                    android:textSize="@dimen/two_line_primary_text_size"/>
-
-            </TableRow>
-
-            <TableRow
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content">
-
-                <ImageButton
-                    android:id="@+id/sortByNameDescending"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:layout_gravity="center_vertical"
-                    android:background="@color/transparent"
-                    android:paddingBottom="@dimen/standard_half_padding"
-                    android:paddingStart="@dimen/standard_padding"
-                    android:paddingEnd="@dimen/standard_half_padding"
-                    android:paddingTop="@dimen/standard_half_padding"
-                    android:src="@drawable/ic_alphabetical_desc"
-                    android:contentDescription="@string/sort_by_name_descending"/>
-
-                <TextView
-                    android:id="@+id/sortByNameZAText"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:layout_gravity="center_vertical"
-                    android:layout_weight="1"
-                    android:ellipsize="middle"
-                    android:singleLine="true"
-                    android:paddingBottom="@dimen/standard_half_padding"
-                    android:paddingEnd="@dimen/standard_double_padding"
-                    android:paddingStart="@dimen/zero"
-                    android:paddingTop="@dimen/standard_half_padding"
-                    android:text="@string/menu_item_sort_by_name_z_a"
-                    android:textColor="@color/standard_grey"
-                    android:textSize="@dimen/two_line_primary_text_size"/>
-
-            </TableRow>
-
-            <TableRow
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_marginTop="@dimen/standard_half_margin">
-
-                <ImageButton
-                    android:id="@+id/sortByModificationDateDescending"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:layout_gravity="center_vertical"
-                    android:background="@color/transparent"
-                    android:paddingBottom="@dimen/standard_half_padding"
-                    android:paddingStart="@dimen/standard_padding"
-                    android:paddingEnd="@dimen/standard_half_padding"
-                    android:paddingTop="@dimen/standard_half_padding"
-                    android:src="@drawable/ic_modification_desc"
-                    android:contentDescription="@string/sort_by_modification_date_descending"/>
-
-                <TextView
-                    android:id="@+id/sortByModificationDateNewestFirstText"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:layout_gravity="center_vertical"
-                    android:layout_weight="1"
-                    android:ellipsize="middle"
-                    android:singleLine="true"
-                    android:paddingBottom="@dimen/standard_half_padding"
-                    android:paddingEnd="@dimen/standard_double_padding"
-                    android:paddingStart="@dimen/zero"
-                    android:paddingTop="@dimen/standard_half_padding"
-                    android:text="@string/menu_item_sort_by_date_newest_first"
-                    android:textColor="@color/standard_grey"
-                    android:textSize="@dimen/two_line_primary_text_size"/>
-
-            </TableRow>
-
-            <TableRow
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content">
-
-                <ImageButton
-                    android:id="@+id/sortByModificationDateAscending"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:layout_gravity="center_vertical"
-                    android:background="@color/transparent"
-                    android:paddingBottom="@dimen/standard_half_padding"
-                    android:paddingStart="@dimen/standard_padding"
-                    android:paddingEnd="@dimen/standard_half_padding"
-                    android:paddingTop="@dimen/standard_half_padding"
-                    android:src="@drawable/ic_modification_asc"
-                    android:contentDescription="@string/sort_by_modification_date_ascending"/>
-
-                <TextView
-                    android:id="@+id/sortByModificationDateOldestFirstText"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:layout_gravity="center_vertical"
-                    android:layout_weight="1"
-                    android:ellipsize="middle"
-                    android:singleLine="true"
-                    android:paddingBottom="@dimen/standard_half_padding"
-                    android:paddingEnd="@dimen/standard_double_padding"
-                    android:paddingStart="@dimen/zero"
-                    android:paddingTop="@dimen/standard_half_padding"
-                    android:text="@string/menu_item_sort_by_date_oldest_first"
-                    android:textColor="@color/standard_grey"
-                    android:textSize="@dimen/two_line_primary_text_size"/>
-
-            </TableRow>
-
-            <TableRow
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-
-                android:layout_marginTop="@dimen/standard_half_margin">
-
-                <ImageButton
-                    android:id="@+id/sortBySizeDescending"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:layout_gravity="center_vertical"
-                    android:background="@color/transparent"
-                    android:paddingBottom="@dimen/standard_half_padding"
-                    android:paddingStart="@dimen/standard_padding"
-                    android:paddingEnd="@dimen/standard_half_padding"
-                    android:paddingTop="@dimen/standard_half_padding"
-                    android:src="@drawable/ic_size_desc"
-                    android:contentDescription="@string/sort_by_size_descending"/>
-
-                <TextView
-                    android:id="@+id/sortBySizeBiggestFirstText"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:layout_gravity="center_vertical"
-                    android:layout_weight="1"
-                    android:ellipsize="middle"
-                    android:singleLine="true"
-                    android:paddingBottom="@dimen/standard_half_padding"
-                    android:paddingEnd="@dimen/standard_double_padding"
-                    android:paddingStart="@dimen/zero"
-                    android:paddingTop="@dimen/standard_half_padding"
-                    android:text="@string/menu_item_sort_by_size_biggest_first"
-                    android:textColor="@color/standard_grey"
-                    android:textSize="@dimen/two_line_primary_text_size"/>
-
-            </TableRow>
-
-            <TableRow
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content">
+            android:layout_height="wrap_content"
+            android:gravity="center|start"
+            android:layout_marginBottom="@dimen/standard_margin"
+            android:text="@string/sort_by"/>
 
-                <ImageButton
-                    android:id="@+id/sortBySizeAscending"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:layout_gravity="center_vertical"
-                    android:background="@color/transparent"
-                    android:paddingBottom="@dimen/standard_half_padding"
-                    android:paddingStart="@dimen/standard_padding"
-                    android:paddingEnd="@dimen/standard_half_padding"
-                    android:paddingTop="@dimen/standard_half_padding"
-                    android:src="@drawable/ic_size_asc"
-                    android:contentDescription="@string/sort_by_size_ascending"/>
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/sortByNameAscending"
+            android:gravity="center|start"
+            android:text="@string/menu_item_sort_by_name_a_z"
+            style="@style/Widget.Material3.Button.TextButton.Icon"
+            app:icon="@drawable/ic_alphabetical_asc"
+            app:iconPadding="@dimen/standard_padding"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"/>
 
-                <TextView
-                    android:id="@+id/sortBySizeSmallestFirstText"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:layout_gravity="center_vertical"
-                    android:layout_weight="1"
-                    android:ellipsize="middle"
-                    android:singleLine="true"
-                    android:paddingBottom="@dimen/standard_half_padding"
-                    android:paddingEnd="@dimen/standard_double_padding"
-                    android:paddingStart="@dimen/zero"
-                    android:paddingTop="@dimen/standard_half_padding"
-                    android:text="@string/menu_item_sort_by_size_smallest_first"
-                    android:textColor="@color/standard_grey"
-                    android:textSize="@dimen/two_line_primary_text_size"/>
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/sortByNameDescending"
+            android:gravity="center|start"
+            android:text="@string/menu_item_sort_by_name_z_a"
+            style="@style/Widget.Material3.Button.TextButton.Icon"
+            app:icon="@drawable/ic_alphabetical_desc"
+            app:iconPadding="@dimen/standard_padding"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"/>
 
-            </TableRow>
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/sortByModificationDateDescending"
+            android:gravity="center|start"
+            android:text="@string/menu_item_sort_by_date_newest_first"
+            style="@style/Widget.Material3.Button.TextButton.Icon"
+            app:icon="@drawable/ic_modification_desc"
+            app:iconPadding="@dimen/standard_padding"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"/>
 
-        </TableLayout>
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/sortByModificationDateAscending"
+            android:gravity="center|start"
+            android:text="@string/menu_item_sort_by_date_oldest_first"
+            style="@style/Widget.Material3.Button.TextButton.Icon"
+            app:icon="@drawable/ic_modification_asc"
+            app:iconPadding="@dimen/standard_padding"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"/>
 
-    </ScrollView>
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/sortBySizeDescending"
+            android:gravity="center|start"
+            android:text="@string/menu_item_sort_by_size_biggest_first"
+            style="@style/Widget.Material3.Button.TextButton.Icon"
+            app:icon="@drawable/ic_size_desc"
+            app:iconPadding="@dimen/standard_padding"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"/>
 
-    <LinearLayout
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="@dimen/dialog_padding"
-        android:gravity="end">
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/sortBySizeAscending"
+            android:gravity="center|start"
+            android:text="@string/menu_item_sort_by_size_smallest_first"
+            style="@style/Widget.Material3.Button.TextButton.Icon"
+            app:icon="@drawable/ic_size_asc"
+            app:iconPadding="@dimen/standard_padding"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"/>
 
         <com.google.android.material.button.MaterialButton
             android:id="@+id/cancel"
-            style="@style/Button.Borderless"
+            android:layout_gravity="end"
+            android:gravity="center"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:text="@string/common_cancel"/>
 
     </LinearLayout>
 
-</LinearLayout>
+</ScrollView>

+ 10 - 34
app/src/main/res/layout/storage_path_item.xml

@@ -1,5 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
+<?xml version="1.0" encoding="utf-8"?><!--
   Nextcloud Android client application
 
   Copyright (C) 2019 Andy Scherzinger
@@ -17,39 +16,16 @@
   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/>.
 -->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<com.google.android.material.button.MaterialButton xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/btn_storage_path"
+    style="@style/Widget.Material3.Button.TextButton.Icon"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    android:orientation="horizontal"
-    android:paddingTop="@dimen/standard_half_padding"
+    android:gravity="center|start"
     android:paddingBottom="@dimen/standard_half_padding"
-    android:weightSum="1"
-    tools:ignore="UseCompoundDrawables">
-
-    <ImageView
-        android:id="@+id/icon"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_gravity="center_vertical"
-        android:background="@color/bg_default"
-        android:contentDescription="@string/user_icon"
-        android:paddingStart="@dimen/standard_padding"
-        android:paddingLeft="@dimen/standard_padding"
-        android:paddingEnd="@dimen/standard_padding"
-        android:paddingRight="@dimen/standard_padding"
-        android:src="@drawable/ic_user" />
-
-    <TextView
-        android:id="@+id/name"
-        android:layout_width="0dp"
-        android:layout_height="match_parent"
-        android:layout_marginEnd="@dimen/standard_margin"
-        android:layout_weight="1"
-        android:ellipsize="end"
-        android:gravity="center_vertical"
-        android:singleLine="true"
-        android:textColor="@color/text_color"
-        android:textSize="@dimen/two_line_primary_text_size"
-        tools:text="DCIM" />
-</LinearLayout>
+    android:text="@string/menu_item_sort_by_name_z_a"
+    app:icon="@drawable/ic_user"
+    app:iconPadding="@dimen/standard_padding"
+    tools:text="DCIM" />

+ 26 - 24
app/src/main/res/layout/synced_folders_settings_layout.xml

@@ -61,9 +61,9 @@
             android:layout_width="@dimen/synced_folders_control_width"
             android:layout_height="match_parent"
             android:gravity="center"
-            android:padding="@dimen/standard_padding">
+            android:padding="@dimen/standard_half_padding">
 
-            <androidx.appcompat.widget.SwitchCompat
+            <com.google.android.material.materialswitch.MaterialSwitch
                 android:id="@+id/sync_enabled"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
@@ -395,37 +395,39 @@
         </LinearLayout>
     </ScrollView>
 
-    <RelativeLayout
+    <LinearLayout
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
+        android:orientation="horizontal"
         android:padding="@dimen/standard_padding">
 
         <com.google.android.material.button.MaterialButton
-            android:id="@+id/delete"
-            style="@style/Button.Borderless.Destructive"
+            android:id="@+id/btnNeutral"
+            style="@style/Widget.Material3.Button.TextButton"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:layout_alignParentStart="true"
-            android:text="@string/common_delete" />
+            android:text="@string/common_cancel"
+            android:layout_weight="1"/>
 
-        <LinearLayout
+        <View
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:layout_weight="1"/>
+
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/btnNegative"
             android:layout_width="wrap_content"
+            style="@style/Widget.Material3.Button.TextButton"
             android:layout_height="wrap_content"
-            android:layout_alignParentEnd="true">
-
-            <com.google.android.material.button.MaterialButton
-                android:id="@+id/cancel"
-                style="@style/Button.Borderless"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:text="@string/common_cancel" />
+            android:text="@string/common_delete"
+            android:layout_weight="1"/>
 
-            <com.google.android.material.button.MaterialButton
-                android:id="@+id/save"
-                style="@style/Button.Borderless"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:text="@string/common_save" />
-        </LinearLayout>
-    </RelativeLayout>
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/btnPositive"
+            android:layout_width="wrap_content"
+            android:text="@string/common_save"
+            style="@style/Widget.Material3.Button.TonalButton"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"/>
+    </LinearLayout>
 </LinearLayout>

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

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

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

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

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

@@ -387,6 +387,8 @@
     <string name="folder_confirm_create">Crear</string>
     <string name="folder_list_empty_headline">No hay carpetas aquí</string>
     <string name="folder_picker_choose_button_text">Seleccionar</string>
+    <string name="folder_picker_choose_caption_text">Selecciona la carpeta de destino.</string>
+    <string name="folder_picker_move_button_text">Mover</string>
     <string name="forbidden_permissions">No se te permite %s</string>
     <string name="forbidden_permissions_copy">para copiar este archivo</string>
     <string name="forbidden_permissions_create">para crear este archivo</string>

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

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

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

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

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

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

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

@@ -253,7 +253,7 @@
     <string name="e2e_offline">No es posible sin conexión a internet</string>
     <string name="ecosystem_apps_display_more">Más</string>
     <string name="ecosystem_apps_display_notes">Notas</string>
-    <string name="ecosystem_apps_display_talk">Coloquio</string>
+    <string name="ecosystem_apps_display_talk">Talk</string>
     <string name="ecosystem_apps_more">Más apps de Nextcloud</string>
     <string name="ecosystem_apps_notes">Nextcloud Notas</string>
     <string name="ecosystem_apps_talk">Nextcloud Talk</string>

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

@@ -263,6 +263,8 @@
     <string name="folder_confirm_create">Loo</string>
     <string name="folder_list_empty_headline">Siin ei ole kaustu</string>
     <string name="folder_picker_choose_button_text">Vali</string>
+    <string name="folder_picker_choose_caption_text">Vali sihtkaust</string>
+    <string name="folder_picker_move_button_text">Liiguta</string>
     <string name="forbidden_permissions">Sul ei ole %s õigusi</string>
     <string name="forbidden_permissions_copy">et kopeerida seda faili</string>
     <string name="forbidden_permissions_create">selle faili loomiseks</string>
@@ -436,6 +438,7 @@
     <string name="status_message">Staatuse teade</string>
     <string name="storage_description_default">Vaikeväärtus</string>
     <string name="storage_downloads">Allalaadimised</string>
+    <string name="sub_folder_rule_year">Aasta</string>
     <string name="subject_shared_with_you">\"%1$s\" on sinuga jagatud</string>
     <string name="subject_user_shared_with_you">%1$s jagas sinuga \"%2$s\"</string>
     <string name="sync_conflicts_in_favourites_ticker">Leite konflikte</string>

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

@@ -392,6 +392,8 @@
     <string name="folder_confirm_create">Sortu</string>
     <string name="folder_list_empty_headline">Ez dago karpetarik hemen</string>
     <string name="folder_picker_choose_button_text">Aukeratu</string>
+    <string name="folder_picker_choose_caption_text">Aukeratu helburuko karpeta</string>
+    <string name="folder_picker_move_button_text">Mugitu</string>
     <string name="forbidden_permissions">Ez daukazu baimenik %s</string>
     <string name="forbidden_permissions_copy">fitxategi hau kopiatzeko</string>
     <string name="forbidden_permissions_create">fitxategi hau sortzeko</string>

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

@@ -392,6 +392,8 @@
     <string name="folder_confirm_create">ایجاد کردن</string>
     <string name="folder_list_empty_headline">هیچ  پوشه ای اینجا وجود ندارد</string>
     <string name="folder_picker_choose_button_text">انتخاب کردن</string>
+    <string name="folder_picker_choose_caption_text">پوشهٔ هدف را انتخاب کنید</string>
+    <string name="folder_picker_move_button_text">انتقال</string>
     <string name="forbidden_permissions">شما مجاز نیستید%s</string>
     <string name="forbidden_permissions_copy">کپی این فایل</string>
     <string name="forbidden_permissions_create">برای ساختن این پرونده</string>

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

@@ -380,6 +380,8 @@
     <string name="folder_confirm_create">Luo</string>
     <string name="folder_list_empty_headline">Ei kansioita täällä</string>
     <string name="folder_picker_choose_button_text">Valitse</string>
+    <string name="folder_picker_choose_caption_text">Valitse kohdekansio</string>
+    <string name="folder_picker_move_button_text">Siirrä</string>
     <string name="forbidden_permissions">Sinulla ei ole oikeutta %s</string>
     <string name="forbidden_permissions_copy">kopioida tämä tiedosto</string>
     <string name="forbidden_permissions_create">luoda tätä tiedostoa</string>

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

@@ -394,6 +394,9 @@ Attention, la suppression est irréversible.</string>
     <string name="folder_confirm_create">Créer</string>
     <string name="folder_list_empty_headline">Aucun dossier</string>
     <string name="folder_picker_choose_button_text">Valider</string>
+    <string name="folder_picker_choose_caption_text">Sélectionner le dossier cible</string>
+    <string name="folder_picker_copy_button_text">Copier</string>
+    <string name="folder_picker_move_button_text">Déplacer</string>
     <string name="forbidden_permissions">Vous n\'avez pas la permission %s</string>
     <string name="forbidden_permissions_copy">de copier ce fichier</string>
     <string name="forbidden_permissions_create">de créer ce fichier</string>

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

@@ -336,6 +336,8 @@
     <string name="folder_confirm_create">Cruthaich</string>
     <string name="folder_list_empty_headline">Chan eil pasgan an-seo</string>
     <string name="folder_picker_choose_button_text">Tagh</string>
+    <string name="folder_picker_copy_button_text">Dèan lethbhreac</string>
+    <string name="folder_picker_move_button_text">Gluais</string>
     <string name="forbidden_permissions">Chan fhad thu %s</string>
     <string name="forbidden_permissions_copy">lethbhreac dhen fhaidhle seo a dhèanamh</string>
     <string name="forbidden_permissions_create">am faidhle seo a chruthachadh</string>

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

@@ -392,6 +392,9 @@
     <string name="folder_confirm_create">Crear</string>
     <string name="folder_list_empty_headline">Aquí non hai cartafoles</string>
     <string name="folder_picker_choose_button_text">Escoller</string>
+    <string name="folder_picker_choose_caption_text">Escoller o cartafol de destino</string>
+    <string name="folder_picker_copy_button_text">Copiar</string>
+    <string name="folder_picker_move_button_text">Mover</string>
     <string name="forbidden_permissions">Non ten permiso %s</string>
     <string name="forbidden_permissions_copy">copiar este ficheiro</string>
     <string name="forbidden_permissions_create">para crear este ficheiro</string>

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

@@ -369,6 +369,9 @@
     <string name="folder_confirm_create">Stvori</string>
     <string name="folder_list_empty_headline">Ovdje nema mapa</string>
     <string name="folder_picker_choose_button_text">Odaberite</string>
+    <string name="folder_picker_choose_caption_text">Odaberi ciljnu mapu</string>
+    <string name="folder_picker_copy_button_text">Kopirajte</string>
+    <string name="folder_picker_move_button_text">Premjesti</string>
     <string name="forbidden_permissions">Nemate dopuštenje %s</string>
     <string name="forbidden_permissions_copy">za kopiranje ove datoteke</string>
     <string name="forbidden_permissions_create">za izradu ove datoteke</string>
@@ -877,6 +880,7 @@
     <string name="wait_a_moment">Pričekajte…</string>
     <string name="wait_checking_credentials">U tijeku je provjera spremljenih vjerodajnica</string>
     <string name="wait_for_tmp_copy_from_private_storage">Kopiranje datoteke iz osobnog podatkovnog prostora</string>
+    <string name="webview_version_check_alert_dialog_positive_button_title">Ažuriraj</string>
     <string name="what_s_new_image">Što je nova slika</string>
     <string name="whats_new_skip">Preskoči</string>
     <string name="whats_new_title">Novo u %1$s</string>

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

@@ -392,6 +392,9 @@
     <string name="folder_confirm_create">Létrehozás</string>
     <string name="folder_list_empty_headline">Itt nincsenek mappák</string>
     <string name="folder_picker_choose_button_text">Válasszon</string>
+    <string name="folder_picker_choose_caption_text">Válasszon célmappát</string>
+    <string name="folder_picker_copy_button_text">Másolás</string>
+    <string name="folder_picker_move_button_text">Áthelyezés</string>
     <string name="forbidden_permissions">Nem engedélyezett %s</string>
     <string name="forbidden_permissions_copy">a fájl másolása</string>
     <string name="forbidden_permissions_create">fájl létrehozása</string>
@@ -959,6 +962,7 @@ 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_positive_button_title">Frissítés</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>

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

@@ -329,6 +329,8 @@ Otomatis unggah hanya bekerja dengan baik apabila Anda mengeluarkan aplikasi ini
     <string name="folder_confirm_create">Buat</string>
     <string name="folder_list_empty_headline">Tidak ada folder</string>
     <string name="folder_picker_choose_button_text">Pilih</string>
+    <string name="folder_picker_choose_caption_text">Pilih folder target</string>
+    <string name="folder_picker_move_button_text">Pindah</string>
     <string name="forbidden_permissions">Anda tidak memiliki izin %s</string>
     <string name="forbidden_permissions_copy">untuk menyalin berkas ini</string>
     <string name="forbidden_permissions_create">untuk membuat berkas ini</string>
@@ -668,6 +670,7 @@ Otomatis unggah hanya bekerja dengan baik apabila Anda mengeluarkan aplikasi ini
     <string name="wait_a_moment">Tunggu sebentar…</string>
     <string name="wait_checking_credentials">Mengecek kredensial yang tersimpan</string>
     <string name="wait_for_tmp_copy_from_private_storage">Menyalin berkas dari penyimpanan pribadi</string>
+    <string name="webview_version_check_alert_dialog_positive_button_title">Perbarui</string>
     <string name="whats_new_skip">Lewat</string>
     <string name="whats_new_title">Apa yang baru di %1$s</string>
     <string name="write_email">Kirim surel</string>

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

@@ -340,6 +340,8 @@
     <string name="folder_confirm_create">Búa til</string>
     <string name="folder_list_empty_headline">Engar möppur hér</string>
     <string name="folder_picker_choose_button_text">Velja</string>
+    <string name="folder_picker_choose_caption_text">Veldu úttaksmöppu</string>
+    <string name="folder_picker_move_button_text">Færa</string>
     <string name="forbidden_permissions">Þú hefur ekki heimild %s</string>
     <string name="forbidden_permissions_copy">til að afrita þessa skrá</string>
     <string name="forbidden_permissions_create">til að búa til þessa skrá</string>
@@ -793,6 +795,7 @@
     <string name="wait_a_moment">Bíddu augnablik…</string>
     <string name="wait_checking_credentials">Athuga geymd auðkenni</string>
     <string name="wait_for_tmp_copy_from_private_storage">Afrita skrá úr einkageymslu</string>
+    <string name="webview_version_check_alert_dialog_positive_button_title">Uppfæra</string>
     <string name="what_s_new_image">Mynd fyrir \'Hvað er nýtt\'</string>
     <string name="whats_new_skip">Sleppa</string>
     <string name="whats_new_title">(Nýtt í %s)</string>

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

@@ -392,6 +392,9 @@
     <string name="folder_confirm_create">Crea</string>
     <string name="folder_list_empty_headline">Qui non c\'è alcuna cartella</string>
     <string name="folder_picker_choose_button_text">Scegli</string>
+    <string name="folder_picker_choose_caption_text">Scegli la cartella di destinazione</string>
+    <string name="folder_picker_copy_button_text">Copia</string>
+    <string name="folder_picker_move_button_text">Sposta</string>
     <string name="forbidden_permissions">Non ti è consentito %s</string>
     <string name="forbidden_permissions_copy">per copiare questo file</string>
     <string name="forbidden_permissions_create">per creare questo file</string>
@@ -941,6 +944,7 @@
     <string name="wait_a_moment">Attendi…</string>
     <string name="wait_checking_credentials">Controllo delle credenziali memorizzate</string>
     <string name="wait_for_tmp_copy_from_private_storage">Copia file dall\'archiviazione privata</string>
+    <string name="webview_version_check_alert_dialog_positive_button_title">Aggiorna</string>
     <string name="what_s_new_image">Immagine Cosa c\'è di nuovo</string>
     <string name="whats_new_skip">Salta</string>
     <string name="whats_new_title">Prima volta su %1$s</string>

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

@@ -348,6 +348,9 @@
     <string name="folder_confirm_create">יצירה</string>
     <string name="folder_list_empty_headline">אין כאן תיקיות</string>
     <string name="folder_picker_choose_button_text">בחירה</string>
+    <string name="folder_picker_choose_caption_text">נא לבחור תיקיית יעד</string>
+    <string name="folder_picker_copy_button_text">העתקה</string>
+    <string name="folder_picker_move_button_text">העברה</string>
     <string name="forbidden_permissions">אין לך הרשאה %s</string>
     <string name="forbidden_permissions_copy">להעתיק את הקובץ הזה</string>
     <string name="forbidden_permissions_create">ליצור את הקובץ הזה</string>
@@ -807,6 +810,7 @@
     <string name="wait_a_moment">נא להמתין רגע…</string>
     <string name="wait_checking_credentials">בודק אישורים שמורים</string>
     <string name="wait_for_tmp_copy_from_private_storage">מעתיק קובץ מאחסון פרטי</string>
+    <string name="webview_version_check_alert_dialog_positive_button_title">עדכון</string>
     <string name="what_s_new_image">תמונת מה חדש</string>
     <string name="whats_new_skip">דלג</string>
     <string name="whats_new_title">חדש ב־%1$s</string>

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

@@ -388,6 +388,9 @@
     <string name="folder_confirm_create">作成</string>
     <string name="folder_list_empty_headline">フォルダーがありません</string>
     <string name="folder_picker_choose_button_text">選択</string>
+    <string name="folder_picker_choose_caption_text">ターゲットフォルダーを選択</string>
+    <string name="folder_picker_copy_button_text">コピー</string>
+    <string name="folder_picker_move_button_text">移動</string>
     <string name="forbidden_permissions">%sが許可されていません。</string>
     <string name="forbidden_permissions_copy">このファイルをコピー</string>
     <string name="forbidden_permissions_create">このファイルを作成する</string>
@@ -922,6 +925,7 @@
     <string name="wait_a_moment">少々お待ちください…</string>
     <string name="wait_checking_credentials">保存された資格情報をチェック</string>
     <string name="wait_for_tmp_copy_from_private_storage">プライベートストレージからファイルをコピー中</string>
+    <string name="webview_version_check_alert_dialog_positive_button_title">更新</string>
     <string name="what_s_new_image">新しいイメージとは</string>
     <string name="whats_new_skip">スキップ</string>
     <string name="whats_new_title">%1$sの新機能</string>

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

@@ -219,6 +219,7 @@
     <string name="folder_confirm_create">შექმნა</string>
     <string name="folder_list_empty_headline">აქ დირექტორიები არაა</string>
     <string name="folder_picker_choose_button_text">არჩევა</string>
+    <string name="folder_picker_move_button_text">გადატანა</string>
     <string name="forbidden_permissions">თქვენ არ გაქვთ უფლება, %s</string>
     <string name="forbidden_permissions_copy">რომ დააკოპიროთ ეს ფაილი</string>
     <string name="forbidden_permissions_create">შექმნათ ეს ფაილი</string>
@@ -505,6 +506,7 @@
     <string name="version_dev_download">ჩამოტვირთვა</string>
     <string name="wait_checking_credentials">მოწმდება შენახული უფლებამოსილებანი</string>
     <string name="wait_for_tmp_copy_from_private_storage">ფაილის კოპირება პირადი საცავიდან</string>
+    <string name="webview_version_check_alert_dialog_positive_button_title">განახლება</string>
     <string name="what_s_new_image">რა არის ახალი სურათი</string>
     <string name="whats_new_skip">გამოტოვება</string>
     <string name="whats_new_title">ახალი %1$s-ში</string>

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

@@ -389,6 +389,8 @@
     <string name="folder_confirm_create">생성</string>
     <string name="folder_list_empty_headline">폴더가 없습니다</string>
     <string name="folder_picker_choose_button_text">선택</string>
+    <string name="folder_picker_choose_caption_text">폴더 선택</string>
+    <string name="folder_picker_move_button_text">이동</string>
     <string name="forbidden_permissions">%s 권한이 없음</string>
     <string name="forbidden_permissions_copy">이 파일을 복사할</string>
     <string name="forbidden_permissions_create">파일을 생성할</string>
@@ -941,6 +943,7 @@ Nextcloud를 여기서 확인하십시오: https://nextcloud.com</string>
     <string name="wait_a_moment">잠시 기다려 주십시오…</string>
     <string name="wait_checking_credentials">저장된 인증 정보 확인 중</string>
     <string name="wait_for_tmp_copy_from_private_storage">개인 저장소에서 파일 복사</string>
+    <string name="webview_version_check_alert_dialog_positive_button_title">업데이트</string>
     <string name="what_s_new_image">새로운 기능 사진</string>
     <string name="whats_new_skip">건너뛰기</string>
     <string name="whats_new_title">%1$s의 새로운 것</string>

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

@@ -342,6 +342,8 @@
     <string name="folder_confirm_create">ສ້າງ</string>
     <string name="folder_list_empty_headline">ບໍ່ມີໂຟນເດີ</string>
     <string name="folder_picker_choose_button_text">ເລືອກ</string>
+    <string name="folder_picker_copy_button_text">ສຳເນົາ</string>
+    <string name="folder_picker_move_button_text">ຍ້າຍ</string>
     <string name="forbidden_permissions">ບໍ່ໄດ້ຮັບອານຸຍາດ%s</string>
     <string name="forbidden_permissions_copy">ສໍາເນົາຟາຍນີ້</string>
     <string name="forbidden_permissions_create">ສ້າງຟາຍນີ້</string>

+ 4 - 0
app/src/main/res/values-lt-rLT/strings.xml

@@ -363,6 +363,9 @@
     <string name="folder_confirm_create">Sukurti</string>
     <string name="folder_list_empty_headline">Čia aplankų nėra</string>
     <string name="folder_picker_choose_button_text">Pasirinkti</string>
+    <string name="folder_picker_choose_caption_text">Pasirinkite paskirties aplanką</string>
+    <string name="folder_picker_copy_button_text">Kopija</string>
+    <string name="folder_picker_move_button_text">Perkelti</string>
     <string name="forbidden_permissions">Neturite leidimo %s</string>
     <string name="forbidden_permissions_copy">kopijuoti failo</string>
     <string name="forbidden_permissions_create">Sukurti failą</string>
@@ -851,6 +854,7 @@
     <string name="wait_a_moment">Palaukite…</string>
     <string name="wait_checking_credentials">Dekriptuojami prisijungimo duomenys</string>
     <string name="wait_for_tmp_copy_from_private_storage">Kopijuojamas failas iš privačios saugyklos</string>
+    <string name="webview_version_check_alert_dialog_positive_button_title">Atnaujinti</string>
     <string name="what_s_new_image">Koks naujas vaizdas</string>
     <string name="whats_new_skip">Praleisti</string>
     <string name="whats_new_title">Naujas %1$s</string>

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

@@ -253,6 +253,7 @@
     <string name="folder_confirm_create">Izveidot</string>
     <string name="folder_list_empty_headline">Šeit nav mapju</string>
     <string name="folder_picker_choose_button_text">Izvēlieties</string>
+    <string name="folder_picker_move_button_text">Pārvietot</string>
     <string name="forbidden_permissions">Jums nav atļaujas %s</string>
     <string name="forbidden_permissions_copy">lai kopētu šo datni</string>
     <string name="forbidden_permissions_create">lai izveidotu šo datni</string>
@@ -543,6 +544,7 @@
     <string name="wait_a_moment">Uzgaidiet brītiņu…</string>
     <string name="wait_checking_credentials">Pārbauda saglabātos akredācijas datus</string>
     <string name="wait_for_tmp_copy_from_private_storage">Kopē datni no privātās krātuves</string>
+    <string name="webview_version_check_alert_dialog_positive_button_title">Atjaunināt</string>
     <string name="what_s_new_image">Kas jauns attēls</string>
     <string name="whats_new_skip">Izlaist</string>
     <string name="whats_new_title">Jaunums %1$s</string>

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

@@ -344,6 +344,8 @@
     <string name="folder_confirm_create">Креирај</string>
     <string name="folder_list_empty_headline">Тука нема папки</string>
     <string name="folder_picker_choose_button_text">Избери</string>
+    <string name="folder_picker_choose_caption_text">Избери папка</string>
+    <string name="folder_picker_move_button_text">Премести</string>
     <string name="forbidden_permissions">Вие не сте овластени %s</string>
     <string name="forbidden_permissions_copy">за да ја копирате оваа датотека</string>
     <string name="forbidden_permissions_create">да креирате датотека</string>
@@ -793,6 +795,7 @@
     <string name="wait_a_moment">Почекајте малку…</string>
     <string name="wait_checking_credentials">Проверка на зачуваните акредитиви</string>
     <string name="wait_for_tmp_copy_from_private_storage">Копирам датотека од приватното складиште</string>
+    <string name="webview_version_check_alert_dialog_positive_button_title">Ажурирај</string>
     <string name="what_s_new_image">Слика од тоа што е ново</string>
     <string name="whats_new_skip">Прескокни</string>
     <string name="whats_new_title">Ново во %1$s</string>

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

@@ -392,6 +392,9 @@
     <string name="folder_confirm_create">Opprett</string>
     <string name="folder_list_empty_headline">Ingen mapper her</string>
     <string name="folder_picker_choose_button_text">Velg</string>
+    <string name="folder_picker_choose_caption_text">Velg målmappe</string>
+    <string name="folder_picker_copy_button_text">Kopi</string>
+    <string name="folder_picker_move_button_text">Flytt</string>
     <string name="forbidden_permissions">Du har ikke tillatelse til %s</string>
     <string name="forbidden_permissions_copy">å kopiere denne filen</string>
     <string name="forbidden_permissions_create">å opprette filen</string>
@@ -929,6 +932,7 @@
     <string name="wait_a_moment">Vent et øyeblikk…</string>
     <string name="wait_checking_credentials">Sjekker lagrede påloggingsdetaljer</string>
     <string name="wait_for_tmp_copy_from_private_storage">Kopierer fil fra privat lager</string>
+    <string name="webview_version_check_alert_dialog_positive_button_title">Oppdater</string>
     <string name="what_s_new_image">Hva er nytt-bilde</string>
     <string name="whats_new_skip">Hopp over</string>
     <string name="whats_new_title">Nytt i %1$s</string>

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

@@ -372,6 +372,9 @@
     <string name="folder_confirm_create">Aanmaken</string>
     <string name="folder_list_empty_headline">Hier zijn geen bestanden</string>
     <string name="folder_picker_choose_button_text">Kiezen</string>
+    <string name="folder_picker_choose_caption_text">Kies doelmap…</string>
+    <string name="folder_picker_copy_button_text">Kopiëren</string>
+    <string name="folder_picker_move_button_text">Verplaatsen</string>
     <string name="forbidden_permissions">Je mist autorisatie %s</string>
     <string name="forbidden_permissions_copy">om dit bestand te kopiëren</string>
     <string name="forbidden_permissions_create">om dit bestand te creëren</string>
@@ -886,6 +889,7 @@
     <string name="wait_a_moment">Wacht even…</string>
     <string name="wait_checking_credentials">Opgeslagen inloggegevens nakijken</string>
     <string name="wait_for_tmp_copy_from_private_storage">Bestand vanaf privéopslag kopiëren</string>
+    <string name="webview_version_check_alert_dialog_positive_button_title">Update</string>
     <string name="what_s_new_image">Wat is nieuw afbeelding</string>
     <string name="whats_new_skip">Overslaan</string>
     <string name="whats_new_title">Nieuw in %1$s</string>

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

@@ -392,6 +392,9 @@
     <string name="folder_confirm_create">Utwórz</string>
     <string name="folder_list_empty_headline">Brak katalogów</string>
     <string name="folder_picker_choose_button_text">Wybierz</string>
+    <string name="folder_picker_choose_caption_text">Wybierz katalog docelowy</string>
+    <string name="folder_picker_copy_button_text">Skopiuj</string>
+    <string name="folder_picker_move_button_text">Przenieś</string>
     <string name="forbidden_permissions">Nie masz uprawnień %s</string>
     <string name="forbidden_permissions_copy">do kopiowania tego pliku</string>
     <string name="forbidden_permissions_create">do utworzenia tego pliku</string>
@@ -941,6 +944,7 @@
     <string name="wait_a_moment">Proszę czekać…</string>
     <string name="wait_checking_credentials">Sprawdzanie danych</string>
     <string name="wait_for_tmp_copy_from_private_storage">Kopiowanie pliku z prywatnego magazynu</string>
+    <string name="webview_version_check_alert_dialog_positive_button_title">Aktualizuj</string>
     <string name="what_s_new_image">Jaki jest nowy obraz</string>
     <string name="whats_new_skip">Pomiń</string>
     <string name="whats_new_title">Co nowego w %1$s</string>

+ 4 - 0
app/src/main/res/values-pt-rBR/strings.xml

@@ -392,6 +392,9 @@
     <string name="folder_confirm_create">Criar</string>
     <string name="folder_list_empty_headline">Sem pastas aqui</string>
     <string name="folder_picker_choose_button_text">Escolher</string>
+    <string name="folder_picker_choose_caption_text">Escolher pasta destino</string>
+    <string name="folder_picker_copy_button_text">Copiar</string>
+    <string name="folder_picker_move_button_text">Mover</string>
     <string name="forbidden_permissions">Você não tem permissão %s</string>
     <string name="forbidden_permissions_copy">para copiar este arquivo</string>
     <string name="forbidden_permissions_create">para criar este arquivo</string>
@@ -941,6 +944,7 @@
     <string name="wait_a_moment">Aguarde um momento…</string>
     <string name="wait_checking_credentials">Verificando credenciais salvas</string>
     <string name="wait_for_tmp_copy_from_private_storage">Copiando o arquivo da armazenagem privada</string>
+    <string name="webview_version_check_alert_dialog_positive_button_title">Atualizar</string>
     <string name="what_s_new_image">Imagem nova</string>
     <string name="whats_new_skip">Saltar</string>
     <string name="whats_new_title">Novo em %1$s</string>

+ 4 - 0
app/src/main/res/values-pt-rPT/strings.xml

@@ -381,6 +381,9 @@
     <string name="folder_confirm_create">Criar</string>
     <string name="folder_list_empty_headline">Sem pastas aqui</string>
     <string name="folder_picker_choose_button_text">Escolher</string>
+    <string name="folder_picker_choose_caption_text">Escolher pasta de destino</string>
+    <string name="folder_picker_copy_button_text">copiar</string>
+    <string name="folder_picker_move_button_text">Mover</string>
     <string name="forbidden_permissions">Não tem permissão %s</string>
     <string name="forbidden_permissions_copy">para copiar este ficheiro</string>
     <string name="forbidden_permissions_create">para criar este ficheiro</string>
@@ -921,6 +924,7 @@ Aproveite o novo e melhorado envio automático.</string>
     <string name="wait_a_moment">Aguarde um momento…</string>
     <string name="wait_checking_credentials">A verificar as credenciais guardadas</string>
     <string name="wait_for_tmp_copy_from_private_storage">A copiar o ficheiro do armazenamento privado</string>
+    <string name="webview_version_check_alert_dialog_positive_button_title">Atualizar</string>
     <string name="what_s_new_image">Imagem de novidades</string>
     <string name="whats_new_skip">Passar à frente</string>
     <string name="whats_new_title">Novo em %1$s</string>

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

@@ -386,6 +386,9 @@
     <string name="folder_confirm_create">Creați</string>
     <string name="folder_list_empty_headline">Niciun dosar aici</string>
     <string name="folder_picker_choose_button_text">Alege</string>
+    <string name="folder_picker_choose_caption_text">Alege directorul destinație</string>
+    <string name="folder_picker_copy_button_text">Copiază</string>
+    <string name="folder_picker_move_button_text">Mută</string>
     <string name="forbidden_permissions">Nu ai permisiunea %s</string>
     <string name="forbidden_permissions_copy">să copiați acest fișier</string>
     <string name="forbidden_permissions_create">pentru a crea fisierul</string>
@@ -910,6 +913,7 @@
     <string name="wait_a_moment">Așteaptă un moment…</string>
     <string name="wait_checking_credentials">Se verifică datele de autentificare stocate</string>
     <string name="wait_for_tmp_copy_from_private_storage">Copiere fișier din stocare privată</string>
+    <string name="webview_version_check_alert_dialog_positive_button_title">Actualizare</string>
     <string name="what_s_new_image">Ce imagine este nouă</string>
     <string name="whats_new_skip">Sari peste</string>
     <string name="whats_new_title">Nou în %1$s</string>

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

@@ -393,6 +393,9 @@
     <string name="folder_confirm_create">Создать</string>
     <string name="folder_list_empty_headline">Здесь нет каталогов</string>
     <string name="folder_picker_choose_button_text">Выбрать</string>
+    <string name="folder_picker_choose_caption_text">Выбор папки назначения</string>
+    <string name="folder_picker_copy_button_text">Копировать</string>
+    <string name="folder_picker_move_button_text">Переместить</string>
     <string name="forbidden_permissions">У вас нет разрешений %s</string>
     <string name="forbidden_permissions_copy">для копирования этого файла</string>
     <string name="forbidden_permissions_create">для создания файла</string>
@@ -942,6 +945,7 @@
     <string name="wait_a_moment">Подождите немного…</string>
     <string name="wait_checking_credentials">Проверка сохранённых реквизитов учётных данных</string>
     <string name="wait_for_tmp_copy_from_private_storage">Копирование файла из частного хранилища</string>
+    <string name="webview_version_check_alert_dialog_positive_button_title">Изменение</string>
     <string name="what_s_new_image">Изображение «что нового»</string>
     <string name="whats_new_skip">Пропустить</string>
     <string name="whats_new_title">Новое в %1$s</string>

Some files were not shown because too many files changed in this diff