Эх сурвалжийг харах

Merge master

Signed-off-by: alperozturk <alper_ozturk@proton.me>
alperozturk 1 жил өмнө
parent
commit
32d1e25081
78 өөрчлөгдсөн 3079 нэмэгдсэн , 2953 устгасан
  1. 1 1
      .devcontainer/devcontainer.json
  2. 1 1
      CONTRIBUTING.md
  3. 1 1
      SETUP.md
  4. BIN
      app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithOneAction.png
  5. BIN
      app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithThreeAction.png
  6. BIN
      app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithThreeActionRTL.png
  7. BIN
      app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithTwoAction.png
  8. BIN
      app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testFileActionsBottomSheet.png
  9. BIN
      app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testLoadingDialog.png
  10. BIN
      app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialogDifferentTypes_Screenshot.png
  11. BIN
      app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialog_Screenshot.png
  12. BIN
      app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendShareDialogTest_showDialog.png
  13. BIN
      app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_error.png
  14. BIN
      app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_showMnemonic.png
  15. 4 3
      app/src/androidTest/java/com/owncloud/android/AbstractIT.java
  16. 1 4
      app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.java
  17. 0 73
      app/src/main/java/com/nextcloud/client/di/ActivityInjector.java
  18. 62 0
      app/src/main/java/com/nextcloud/client/di/ActivityInjector.kt
  19. 4 0
      app/src/main/java/com/nextcloud/client/di/ComponentsModule.java
  20. 0 232
      app/src/main/java/com/nextcloud/client/onboarding/FirstRunActivity.java
  21. 275 0
      app/src/main/java/com/nextcloud/client/onboarding/FirstRunActivity.kt
  22. 8 4
      app/src/main/java/com/nextcloud/ui/fileactions/FileActionsBottomSheet.kt
  23. 44 0
      app/src/main/java/com/nextcloud/utils/extensions/BundleExtensions.kt
  24. 741 0
      app/src/main/java/com/owncloud/android/files/services/FileDownloader.java
  25. 0 750
      app/src/main/java/com/owncloud/android/files/services/FileDownloader.kt
  26. 5 5
      app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java
  27. 0 105
      app/src/main/java/com/owncloud/android/ui/dialog/AccountRemovalConfirmationDialog.java
  28. 105 0
      app/src/main/java/com/owncloud/android/ui/dialog/AccountRemovalConfirmationDialog.kt
  29. 6 1
      app/src/main/java/com/owncloud/android/ui/dialog/ConfirmationDialogFragment.kt
  30. 0 230
      app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.java
  31. 203 0
      app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.kt
  32. 0 196
      app/src/main/java/com/owncloud/android/ui/dialog/ExpirationDatePickerDialogFragment.java
  33. 183 0
      app/src/main/java/com/owncloud/android/ui/dialog/ExpirationDatePickerDialogFragment.kt
  34. 1 1
      app/src/main/java/com/owncloud/android/ui/dialog/LoadingDialog.kt
  35. 0 135
      app/src/main/java/com/owncloud/android/ui/dialog/SendFilesDialog.java
  36. 138 0
      app/src/main/java/com/owncloud/android/ui/dialog/SendFilesDialog.kt
  37. 0 286
      app/src/main/java/com/owncloud/android/ui/dialog/SendShareDialog.java
  38. 274 0
      app/src/main/java/com/owncloud/android/ui/dialog/SendShareDialog.kt
  39. 0 551
      app/src/main/java/com/owncloud/android/ui/dialog/SetupEncryptionDialogFragment.java
  40. 563 0
      app/src/main/java/com/owncloud/android/ui/dialog/SetupEncryptionDialogFragment.kt
  41. 2 2
      app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java
  42. 69 61
      app/src/main/java/com/owncloud/android/ui/fragment/FileDetailsSharingProcessFragment.kt
  43. 22 21
      app/src/main/java/com/owncloud/android/ui/fragment/GalleryFragmentBottomSheetDialog.kt
  44. 1 1
      app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java
  45. 2 2
      app/src/main/java/com/owncloud/android/ui/preview/FileDownloadFragment.java
  46. 3 4
      app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaFragment.java
  47. 54 52
      app/src/main/res/layout/file_actions_bottom_sheet.xml
  48. 17 20
      app/src/main/res/layout/file_details_sharing_process_fragment.xml
  49. 1 1
      app/src/main/res/layout/first_run_activity.xml
  50. 119 118
      app/src/main/res/layout/fragment_gallery_bottom_sheet.xml
  51. 26 7
      app/src/main/res/layout/send_files_fragment.xml
  52. 43 77
      app/src/main/res/layout/send_share_fragment.xml
  53. 2 2
      app/src/main/res/layout/setup_encryption_dialog.xml
  54. 9 1
      app/src/main/res/values-ar/strings.xml
  55. 4 0
      app/src/main/res/values-b+en+001/strings.xml
  56. 4 0
      app/src/main/res/values-bg-rBG/strings.xml
  57. 3 0
      app/src/main/res/values-br/strings.xml
  58. 4 0
      app/src/main/res/values-ca/strings.xml
  59. 8 0
      app/src/main/res/values-cs-rCZ/strings.xml
  60. 4 0
      app/src/main/res/values-da/strings.xml
  61. 8 0
      app/src/main/res/values-de/strings.xml
  62. 4 0
      app/src/main/res/values-el/strings.xml
  63. 3 0
      app/src/main/res/values-eo/strings.xml
  64. 3 0
      app/src/main/res/values-es-rAR/strings.xml
  65. 2 0
      app/src/main/res/values-es-rCO/strings.xml
  66. 1 0
      app/src/main/res/values-es-rCR/strings.xml
  67. 1 0
      app/src/main/res/values-es-rDO/strings.xml
  68. 2 0
      app/src/main/res/values-es-rEC/strings.xml
  69. 1 0
      app/src/main/res/values-es-rGT/strings.xml
  70. 4 0
      app/src/main/res/values-es/strings.xml
  71. 6 1
      app/src/main/res/values-fr/strings.xml
  72. 4 0
      app/src/main/res/values-ru/strings.xml
  73. 4 0
      app/src/main/res/values-sr/strings.xml
  74. 5 0
      app/src/main/res/values-sv/strings.xml
  75. 4 0
      app/src/main/res/values-tr/strings.xml
  76. 4 0
      app/src/main/res/values-zh-rCN/strings.xml
  77. 4 0
      app/src/main/res/values-zh-rTW/strings.xml
  78. 2 4
      app/src/main/res/values/dims.xml

+ 1 - 1
.devcontainer/devcontainer.json

@@ -1,4 +1,4 @@
 {
 	"name": "NextcloudAndroid",
-	"dockerFile": "Dockerfile",
+	"dockerFile": "Dockerfile"
 }

+ 1 - 1
CONTRIBUTING.md

@@ -297,7 +297,7 @@ We use [shot](https://github.com/Karumi/Shot) for taking screenshots and compare
      Screenshot.snapActivity(activity).record();
     ```
 
-    - best practise is to first create test with emulator too see behaviour and then create screenshots
+    - best practice is to first create test with emulator too see behaviour and then create screenshots
 
 ## File naming
 

+ 1 - 1
SETUP.md

@@ -51,7 +51,7 @@ To set up the project in Android Studio follow the next steps:
 
 * Open Android Studio and select 'Import Project (Eclipse ADT, Gradle, etc)'. Browse through your file system to the folder 'android' where the project is located. Android Studio will then create the '.iml' files it needs. If you ever close the project but the files are still there, you just select 'Open Project…'. The file chooser will show an Android face as the folder icon, which you can select to reopen the project.
 * Android Studio will try to build the project directly after importing it. To build it manually, follow the menu path 'Build'/'Make Project', or just click the 'Play' button in the toolbar to build and run it in a mobile device or an emulator. The resulting APK file will be saved in the 'build/outputs/apk/' subdirectory in the project folder.
-* Setup Android Studio editor configurtation for the project: ```Settings``` → ```Editor``` → ```Code Style``` → ```Scheme: Project``` and ```Enable EditorConfig support```
+* Setup Android Studio editor configuration for the project: ```Settings``` → ```Editor``` → ```Code Style``` → ```Scheme: Project``` and ```Enable EditorConfig support```
 
 
 ### 3. Working in a terminal with Gradle:

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


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


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


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


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


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


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


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


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


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


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


+ 4 - 3
app/src/androidTest/java/com/owncloud/android/AbstractIT.java

@@ -415,10 +415,11 @@ public abstract class AbstractIT {
     }
 
     protected void resetLocale() {
+        Locale locale = new Locale("en");
         Resources resources = InstrumentationRegistry.getInstrumentation().getTargetContext().getResources();
-        Configuration defaultConfig = resources.getConfiguration();
-        defaultConfig.setLocale(Locale.getDefault());
-        resources.updateConfiguration(defaultConfig, null);
+        Configuration config = resources.getConfiguration();
+        config.setLocale(locale);
+        resources.updateConfiguration(config, null);
     }
 
     protected void screenshot(View view) {

+ 1 - 4
app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.java

@@ -108,10 +108,7 @@ public class DialogFragmentIT extends AbstractIT {
         Intent intent = new Intent(targetContext, FileDisplayActivity.class);
         return activityRule.launchActivity(intent);
     }
-
-    @Rule
-    public GrantPermissionRule permissionRule = GrantPermissionRule.grant(
-        android.Manifest.permission.POST_NOTIFICATIONS);
+    
 
     @After
     public void quitLooperIfNeeded() {

+ 0 - 73
app/src/main/java/com/nextcloud/client/di/ActivityInjector.java

@@ -1,73 +0,0 @@
-/*
- * Nextcloud Android client application
- *
- * @author Chris Narkiewicz
- * Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-
-package com.nextcloud.client.di;
-
-import android.app.Activity;
-import android.app.Application;
-import android.os.Bundle;
-import androidx.fragment.app.FragmentActivity;
-import androidx.fragment.app.FragmentManager;
-import dagger.android.AndroidInjection;
-
-public class ActivityInjector implements Application.ActivityLifecycleCallbacks {
-
-    @Override
-    public final void onActivityCreated(Activity activity, Bundle savedInstanceState) {
-        if (activity instanceof Injectable) {
-            AndroidInjection.inject(activity);
-        }
-
-        if (activity instanceof FragmentActivity) {
-            final FragmentManager fm = ((FragmentActivity) activity).getSupportFragmentManager();
-            fm.registerFragmentLifecycleCallbacks(new FragmentInjector(), true);
-        }
-    }
-
-    @Override
-    public final void onActivityStarted(Activity activity) {
-        // not needed
-    }
-
-    @Override
-    public final void onActivityResumed(Activity activity) {
-        // not needed
-    }
-
-    @Override
-    public final void onActivityPaused(Activity activity) {
-        // not needed
-    }
-
-    @Override
-    public final void onActivityStopped(Activity activity) {
-        // not needed
-    }
-
-    @Override
-    public final void onActivitySaveInstanceState(Activity activity, Bundle outState) {
-        // not needed
-    }
-
-    @Override
-    public final void onActivityDestroyed(Activity activity) {
-        // not needed
-    }
-}

+ 62 - 0
app/src/main/java/com/nextcloud/client/di/ActivityInjector.kt

@@ -0,0 +1,62 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Chris Narkiewicz
+ * Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.client.di
+
+import android.app.Activity
+import android.app.Application.ActivityLifecycleCallbacks
+import android.os.Bundle
+import androidx.fragment.app.FragmentActivity
+import dagger.android.AndroidInjection
+
+class ActivityInjector : ActivityLifecycleCallbacks {
+    override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
+        if (activity is Injectable) {
+            AndroidInjection.inject(activity)
+        }
+        if (activity is FragmentActivity) {
+            val fm = activity.supportFragmentManager
+            fm.registerFragmentLifecycleCallbacks(FragmentInjector(), true)
+        }
+    }
+
+    override fun onActivityStarted(activity: Activity) {
+        // unused atm
+    }
+
+    override fun onActivityResumed(activity: Activity) {
+        // unused atm
+    }
+
+    override fun onActivityPaused(activity: Activity) {
+        // unused atm
+    }
+
+    override fun onActivityStopped(activity: Activity) {
+        // unused atm
+    }
+
+    override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
+        // unused atm
+    }
+
+    override fun onActivityDestroyed(activity: Activity) {
+        // unused atm
+    }
+}

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

@@ -95,6 +95,7 @@ import com.owncloud.android.ui.dialog.MultipleAccountsDialog;
 import com.owncloud.android.ui.dialog.RemoveFilesDialogFragment;
 import com.owncloud.android.ui.dialog.RenameFileDialogFragment;
 import com.owncloud.android.ui.dialog.RenamePublicShareDialogFragment;
+import com.owncloud.android.ui.dialog.SendFilesDialog;
 import com.owncloud.android.ui.dialog.SendShareDialog;
 import com.owncloud.android.ui.dialog.SetupEncryptionDialogFragment;
 import com.owncloud.android.ui.dialog.SharePasswordDialogFragment;
@@ -460,6 +461,9 @@ abstract class ComponentsModule {
     @ContributesAndroidInjector
     abstract FileActionsBottomSheet fileActionsBottomSheet();
 
+    @ContributesAndroidInjector
+    abstract SendFilesDialog sendFilesDialog();
+
     @ContributesAndroidInjector
     abstract DocumentScanActivity documentScanActivity();
 

+ 0 - 232
app/src/main/java/com/nextcloud/client/onboarding/FirstRunActivity.java

@@ -1,232 +0,0 @@
-/*
- * Nextcloud Android client application
- *
- * @author Bartosz Przybylski
- * @author Chris Narkiewicz
- * Copyright (C) 2015 Bartosz Przybylski
- * Copyright (C) 2015 ownCloud Inc.
- * Copyright (C) 2016 Nextcloud.
- * Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
- *
- * You should have received a copy of the GNU Affero General Public
- * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package com.nextcloud.client.onboarding;
-
-import android.accounts.Account;
-import android.accounts.AccountManager;
-import android.content.Intent;
-import android.content.res.Configuration;
-import android.os.Bundle;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.LinearLayout;
-
-import com.nextcloud.android.common.ui.theme.utils.ColorRole;
-import com.nextcloud.client.account.UserAccountManager;
-import com.nextcloud.client.appinfo.AppInfo;
-import com.nextcloud.client.di.Injectable;
-import com.nextcloud.client.preferences.AppPreferences;
-import com.owncloud.android.BuildConfig;
-import com.owncloud.android.R;
-import com.owncloud.android.authentication.AuthenticatorActivity;
-import com.owncloud.android.databinding.FirstRunActivityBinding;
-import com.owncloud.android.features.FeatureItem;
-import com.owncloud.android.ui.activity.BaseActivity;
-import com.owncloud.android.ui.activity.FileDisplayActivity;
-import com.owncloud.android.ui.adapter.FeaturesViewAdapter;
-import com.owncloud.android.utils.DisplayUtils;
-import com.owncloud.android.utils.theme.ViewThemeUtils;
-
-import javax.inject.Inject;
-
-import androidx.viewpager.widget.ViewPager;
-
-/**
- * Activity displaying general feature after a fresh install.
- */
-public class FirstRunActivity extends BaseActivity implements ViewPager.OnPageChangeListener, Injectable {
-
-    public static final String EXTRA_ALLOW_CLOSE = "ALLOW_CLOSE";
-    public static final String EXTRA_EXIT = "EXIT";
-    public static final int FIRST_RUN_RESULT_CODE = 199;
-
-    @Inject UserAccountManager userAccountManager;
-    @Inject AppPreferences preferences;
-    @Inject AppInfo appInfo;
-    @Inject OnboardingService onboarding;
-
-    @Inject ViewThemeUtils.Factory viewThemeUtilsFactory;
-
-    private FirstRunActivityBinding binding;
-    private ViewThemeUtils defaultViewThemeUtils;
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        enableAccountHandling = false;
-
-        super.onCreate(savedInstanceState);
-        defaultViewThemeUtils = viewThemeUtilsFactory.withPrimaryAsBackground();
-        defaultViewThemeUtils.platform.themeStatusBar(this, ColorRole.PRIMARY);
-        this.binding = FirstRunActivityBinding.inflate(getLayoutInflater());
-        setContentView(binding.getRoot());
-
-        boolean isProviderOrOwnInstallationVisible = getResources().getBoolean(R.bool.show_provider_or_own_installation);
-
-        setSlideshowSize(getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE);
-
-
-        defaultViewThemeUtils.material.colorMaterialButtonFilledOnPrimary(binding.login);
-        binding.login.setOnClickListener(v -> {
-            if (getIntent().getBooleanExtra(EXTRA_ALLOW_CLOSE, false)) {
-                Intent authenticatorActivityIntent = new Intent(this, AuthenticatorActivity.class);
-                authenticatorActivityIntent.putExtra(AuthenticatorActivity.EXTRA_USE_PROVIDER_AS_WEBLOGIN, false);
-                startActivityForResult(authenticatorActivityIntent, FIRST_RUN_RESULT_CODE);
-            } else {
-                finish();
-            }
-        });
-
-
-        defaultViewThemeUtils.material.colorMaterialButtonOutlinedOnPrimary(binding.signup);
-        binding.signup.setVisibility(isProviderOrOwnInstallationVisible ? View.VISIBLE : View.GONE);
-        binding.signup.setOnClickListener(v -> {
-            Intent authenticatorActivityIntent = new Intent(this, AuthenticatorActivity.class);
-            authenticatorActivityIntent.putExtra(AuthenticatorActivity.EXTRA_USE_PROVIDER_AS_WEBLOGIN, true);
-
-            if (getIntent().getBooleanExtra(EXTRA_ALLOW_CLOSE, false)) {
-                startActivityForResult(authenticatorActivityIntent, FIRST_RUN_RESULT_CODE);
-            } else {
-                authenticatorActivityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-                startActivity(authenticatorActivityIntent);
-            }
-        });
-
-        defaultViewThemeUtils.platform.colorTextView(binding.hostOwnServer, ColorRole.ON_PRIMARY);
-        binding.hostOwnServer.setVisibility(isProviderOrOwnInstallationVisible ? View.VISIBLE : View.GONE);
-
-        if (isProviderOrOwnInstallationVisible) {
-            binding.hostOwnServer.setOnClickListener(v -> DisplayUtils.startLinkIntent(this, R.string.url_server_install));
-        }
-
-
-        // Sometimes, accounts are not deleted when you uninstall the application so we'll do it now
-        if (onboarding.isFirstRun()) {
-            userAccountManager.removeAllAccounts();
-        }
-
-        FeaturesViewAdapter featuresViewAdapter = new FeaturesViewAdapter(getSupportFragmentManager(), getFirstRun());
-        binding.progressIndicator.setNumberOfSteps(featuresViewAdapter.getCount());
-        binding.contentPanel.setAdapter(featuresViewAdapter);
-
-        binding.contentPanel.addOnPageChangeListener(this);
-    }
-
-    private void setSlideshowSize(boolean isLandscape) {
-        boolean isProviderOrOwnInstallationVisible = getResources().getBoolean(R.bool.show_provider_or_own_installation);
-
-        LinearLayout.LayoutParams layoutParams;
-
-        binding.buttonLayout.setOrientation(isLandscape ? LinearLayout.HORIZONTAL : LinearLayout.VERTICAL);
-
-        if (isProviderOrOwnInstallationVisible) {
-            layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
-        } else {
-            layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, DisplayUtils.convertDpToPixel(isLandscape ? 100f : 150f, this));
-        }
-
-        binding.bottomLayout.setLayoutParams(layoutParams);
-    }
-
-    @Override
-    public void onConfigurationChanged(Configuration newConfig) {
-        super.onConfigurationChanged(newConfig);
-        setSlideshowSize(newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE);
-    }
-
-    @Override
-    public void onBackPressed() {
-        onFinish();
-
-        if (getIntent().getBooleanExtra(EXTRA_ALLOW_CLOSE, false)) {
-            super.onBackPressed();
-        } else {
-            Intent intent = new Intent(getApplicationContext(), AuthenticatorActivity.class);
-            intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
-            intent.putExtra(EXTRA_EXIT, true);
-            startActivity(intent);
-            finish();
-        }
-    }
-
-    private void onFinish() {
-        preferences.setLastSeenVersionCode(BuildConfig.VERSION_CODE);
-    }
-
-    @Override
-    protected void onStop() {
-        onFinish();
-
-        super.onStop();
-    }
-
-    @Override
-    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
-        // unused but to be implemented due to abstract parent
-    }
-
-    @Override
-    public void onPageSelected(int position) {
-        binding.progressIndicator.animateToStep(position + 1);
-    }
-
-    @Override
-    public void onPageScrollStateChanged(int state) {
-        // unused but to be implemented due to abstract parent
-    }
-
-    @Override
-    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
-        super.onActivityResult(requestCode, resultCode, data);
-        if (FIRST_RUN_RESULT_CODE == requestCode && RESULT_OK == resultCode) {
-
-            String accountName = data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME);
-            Account account = userAccountManager.getAccountByName(accountName);
-
-
-            if (account == null) {
-                DisplayUtils.showSnackMessage(this, R.string.account_creation_failed);
-                return;
-            }
-
-            userAccountManager.setCurrentOwnCloudAccount(account.name);
-
-            Intent i = new Intent(this, FileDisplayActivity.class);
-            i.setAction(FileDisplayActivity.RESTART);
-            i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
-            startActivity(i);
-
-            finish();
-        }
-    }
-
-
-    public static FeatureItem[] getFirstRun() {
-        return new FeatureItem[]{
-            new FeatureItem(R.drawable.logo, R.string.first_run_1_text, R.string.empty, true, false),
-            new FeatureItem(R.drawable.first_run_files, R.string.first_run_2_text, R.string.empty, true, false),
-            new FeatureItem(R.drawable.first_run_groupware, R.string.first_run_3_text, R.string.empty, true, false),
-            new FeatureItem(R.drawable.first_run_talk, R.string.first_run_4_text, R.string.empty, true, false)};
-    }
-}

+ 275 - 0
app/src/main/java/com/nextcloud/client/onboarding/FirstRunActivity.kt

@@ -0,0 +1,275 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Bartosz Przybylski
+ * @author Chris Narkiewicz
+ * Copyright (C) 2015 Bartosz Przybylski
+ * Copyright (C) 2015 ownCloud Inc.
+ * Copyright (C) 2016 Nextcloud.
+ * Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.client.onboarding
+
+import android.accounts.AccountManager
+import android.content.Intent
+import android.content.res.Configuration
+import android.os.Bundle
+import android.view.View
+import android.view.ViewGroup
+import android.widget.LinearLayout
+import androidx.activity.OnBackPressedCallback
+import androidx.activity.result.ActivityResult
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.viewpager.widget.ViewPager
+import com.nextcloud.android.common.ui.theme.utils.ColorRole
+import com.nextcloud.client.account.UserAccountManager
+import com.nextcloud.client.appinfo.AppInfo
+import com.nextcloud.client.di.Injectable
+import com.nextcloud.client.preferences.AppPreferences
+import com.owncloud.android.BuildConfig
+import com.owncloud.android.R
+import com.owncloud.android.authentication.AuthenticatorActivity
+import com.owncloud.android.databinding.FirstRunActivityBinding
+import com.owncloud.android.features.FeatureItem
+import com.owncloud.android.ui.activity.BaseActivity
+import com.owncloud.android.ui.activity.FileDisplayActivity
+import com.owncloud.android.ui.adapter.FeaturesViewAdapter
+import com.owncloud.android.utils.DisplayUtils
+import com.owncloud.android.utils.theme.ViewThemeUtils
+import javax.inject.Inject
+
+/**
+ * Activity displaying general feature after a fresh install.
+ */
+class FirstRunActivity : BaseActivity(), ViewPager.OnPageChangeListener, Injectable {
+
+    @JvmField
+    @Inject
+    var userAccountManager: UserAccountManager? = null
+
+    @JvmField
+    @Inject
+    var preferences: AppPreferences? = null
+
+    @JvmField
+    @Inject
+    var appInfo: AppInfo? = null
+
+    @JvmField
+    @Inject
+    var onboarding: OnboardingService? = null
+
+    @JvmField
+    @Inject
+    var viewThemeUtilsFactory: ViewThemeUtils.Factory? = null
+
+    private var activityResult: ActivityResultLauncher<Intent>? = null
+
+    private lateinit var binding: FirstRunActivityBinding
+    private var defaultViewThemeUtils: ViewThemeUtils? = null
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        enableAccountHandling = false
+
+        super.onCreate(savedInstanceState)
+
+        applyDefaultTheme()
+
+        binding = FirstRunActivityBinding.inflate(layoutInflater)
+        setContentView(binding.root)
+
+        val isProviderOrOwnInstallationVisible = resources.getBoolean(R.bool.show_provider_or_own_installation)
+        setSlideshowSize(resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE)
+
+        registerActivityResult()
+        setupLoginButton()
+        setupSignupButton(isProviderOrOwnInstallationVisible)
+        setupHostOwnServerTextView(isProviderOrOwnInstallationVisible)
+        deleteAccountAtFirstLaunch()
+        setupFeaturesViewAdapter()
+        handleOnBackPressed()
+    }
+
+    private fun applyDefaultTheme() {
+        defaultViewThemeUtils = viewThemeUtilsFactory?.withPrimaryAsBackground()
+        defaultViewThemeUtils?.platform?.themeStatusBar(this, ColorRole.PRIMARY)
+    }
+
+    private fun registerActivityResult() {
+        activityResult =
+            registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
+                if (RESULT_OK == result.resultCode) {
+                    val data = result.data
+                    val accountName = data?.getStringExtra(AccountManager.KEY_ACCOUNT_NAME)
+                    val account = userAccountManager?.getAccountByName(accountName)
+                    if (account == null) {
+                        DisplayUtils.showSnackMessage(this, R.string.account_creation_failed)
+                        return@registerForActivityResult
+                    }
+
+                    userAccountManager?.setCurrentOwnCloudAccount(account.name)
+
+                    val i = Intent(this, FileDisplayActivity::class.java)
+                    i.action = FileDisplayActivity.RESTART
+                    i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+                    startActivity(i)
+                    finish()
+                }
+            }
+    }
+
+    private fun setupLoginButton() {
+        defaultViewThemeUtils?.material?.colorMaterialButtonFilledOnPrimary(binding.login)
+        binding.login.setOnClickListener {
+            if (intent.getBooleanExtra(EXTRA_ALLOW_CLOSE, false)) {
+                val authenticatorActivityIntent = getAuthenticatorActivityIntent(false)
+                activityResult?.launch(authenticatorActivityIntent)
+            } else {
+                finish()
+            }
+        }
+    }
+
+    private fun setupSignupButton(isProviderOrOwnInstallationVisible: Boolean) {
+        defaultViewThemeUtils?.material?.colorMaterialButtonOutlinedOnPrimary(binding.signup)
+        binding.signup.visibility = if (isProviderOrOwnInstallationVisible) View.VISIBLE else View.GONE
+        binding.signup.setOnClickListener {
+            val authenticatorActivityIntent = getAuthenticatorActivityIntent(true)
+
+            if (intent.getBooleanExtra(EXTRA_ALLOW_CLOSE, false)) {
+                activityResult?.launch(authenticatorActivityIntent)
+            } else {
+                authenticatorActivityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                startActivity(authenticatorActivityIntent)
+            }
+        }
+    }
+
+    private fun getAuthenticatorActivityIntent(extraUseProviderAsWebLogin: Boolean): Intent {
+        val intent = Intent(this, AuthenticatorActivity::class.java)
+        intent.putExtra(AuthenticatorActivity.EXTRA_USE_PROVIDER_AS_WEBLOGIN, extraUseProviderAsWebLogin)
+        return intent
+    }
+
+    private fun setupHostOwnServerTextView(isProviderOrOwnInstallationVisible: Boolean) {
+        defaultViewThemeUtils?.platform?.colorTextView(binding.hostOwnServer, ColorRole.ON_PRIMARY)
+        binding.hostOwnServer.visibility = if (isProviderOrOwnInstallationVisible) View.VISIBLE else View.GONE
+        if (isProviderOrOwnInstallationVisible) {
+            binding.hostOwnServer.setOnClickListener {
+                DisplayUtils.startLinkIntent(
+                    this,
+                    R.string.url_server_install
+                )
+            }
+        }
+    }
+
+    // Sometimes, accounts are not deleted when you uninstall the application so we'll do it now
+    private fun deleteAccountAtFirstLaunch() {
+        if (onboarding?.isFirstRun == true) {
+            userAccountManager?.removeAllAccounts()
+        }
+    }
+
+    @Suppress("SpreadOperator")
+    private fun setupFeaturesViewAdapter() {
+        val featuresViewAdapter = FeaturesViewAdapter(supportFragmentManager, *firstRun)
+        binding.progressIndicator.setNumberOfSteps(featuresViewAdapter.count)
+        binding.contentPanel.adapter = featuresViewAdapter
+        binding.contentPanel.addOnPageChangeListener(this)
+    }
+
+    private fun handleOnBackPressed() {
+        onBackPressedDispatcher.addCallback(
+            this,
+            object : OnBackPressedCallback(true) {
+                override fun handleOnBackPressed() {
+                    onFinish()
+
+                    if (intent.getBooleanExtra(EXTRA_ALLOW_CLOSE, false)) {
+                        onBackPressedDispatcher.onBackPressed()
+                    } else {
+                        val intent = Intent(applicationContext, AuthenticatorActivity::class.java)
+                        intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
+                        intent.putExtra(EXTRA_EXIT, true)
+                        startActivity(intent)
+                        finish()
+                    }
+                }
+            }
+        )
+    }
+
+    private fun setSlideshowSize(isLandscape: Boolean) {
+        val isProviderOrOwnInstallationVisible = resources.getBoolean(R.bool.show_provider_or_own_installation)
+        binding.buttonLayout.orientation = if (isLandscape) LinearLayout.HORIZONTAL else LinearLayout.VERTICAL
+
+        val layoutParams: LinearLayout.LayoutParams = if (isProviderOrOwnInstallationVisible) {
+            LinearLayout.LayoutParams(
+                ViewGroup.LayoutParams.MATCH_PARENT,
+                ViewGroup.LayoutParams.WRAP_CONTENT
+            )
+        } else {
+            @Suppress("MagicNumber")
+            LinearLayout.LayoutParams(
+                ViewGroup.LayoutParams.MATCH_PARENT,
+                DisplayUtils.convertDpToPixel(if (isLandscape) 100f else 150f, this)
+            )
+        }
+
+        binding.bottomLayout.layoutParams = layoutParams
+    }
+
+    override fun onConfigurationChanged(newConfig: Configuration) {
+        super.onConfigurationChanged(newConfig)
+        setSlideshowSize(newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE)
+    }
+
+    private fun onFinish() {
+        preferences?.lastSeenVersionCode = BuildConfig.VERSION_CODE
+    }
+
+    override fun onStop() {
+        onFinish()
+        super.onStop()
+    }
+
+    override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
+        // unused but to be implemented due to abstract parent
+    }
+
+    override fun onPageSelected(position: Int) {
+        binding.progressIndicator.animateToStep(position + 1)
+    }
+
+    override fun onPageScrollStateChanged(state: Int) {
+        // unused but to be implemented due to abstract parent
+    }
+
+    companion object {
+        const val EXTRA_ALLOW_CLOSE = "ALLOW_CLOSE"
+        const val EXTRA_EXIT = "EXIT"
+
+        val firstRun: Array<FeatureItem>
+            get() = arrayOf(
+                FeatureItem(R.drawable.logo, R.string.first_run_1_text, R.string.empty, true, false),
+                FeatureItem(R.drawable.first_run_files, R.string.first_run_2_text, R.string.empty, true, false),
+                FeatureItem(R.drawable.first_run_groupware, R.string.first_run_3_text, R.string.empty, true, false),
+                FeatureItem(R.drawable.first_run_talk, R.string.first_run_4_text, R.string.empty, true, false)
+            )
+    }
+}

+ 8 - 4
app/src/main/java/com/nextcloud/ui/fileactions/FileActionsBottomSheet.kt

@@ -79,13 +79,13 @@ class FileActionsBottomSheet : BottomSheetDialogFragment(), Injectable {
     @Inject
     lateinit var syncedFolderProvider: SyncedFolderProvider
 
-    lateinit var viewModel: FileActionsViewModel
+    private lateinit var viewModel: FileActionsViewModel
 
     private var _binding: FileActionsBottomSheetBinding? = null
     private val binding
         get() = _binding!!
 
-    lateinit var componentsGetter: ComponentsGetter
+    private lateinit var componentsGetter: ComponentsGetter
 
     private val thumbnailAsyncTasks = mutableListOf<ThumbnailsCacheManager.ThumbnailGenerationTask>()
 
@@ -109,6 +109,8 @@ class FileActionsBottomSheet : BottomSheetDialogFragment(), Injectable {
         bottomSheetDialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED
         bottomSheetDialog.behavior.skipCollapsed = true
 
+        viewThemeUtils.platform.colorViewBackground(binding.bottomSheet, ColorRole.SURFACE)
+
         return binding.root
     }
 
@@ -125,11 +127,13 @@ class FileActionsBottomSheet : BottomSheetDialogFragment(), Injectable {
                 displayActions(state.actions)
                 displayTitle(state.titleFile)
             }
+
             is FileActionsViewModel.UiState.LoadedForMultipleFiles -> {
                 setMultipleFilesThumbnail()
                 displayActions(state.actions)
                 displayTitle(state.fileCount)
             }
+
             FileActionsViewModel.UiState.Loading -> {}
             FileActionsViewModel.UiState.Error -> {
                 context?.let {
@@ -195,11 +199,11 @@ class FileActionsBottomSheet : BottomSheetDialogFragment(), Injectable {
     private fun toggleLoadingOrContent(state: FileActionsViewModel.UiState) {
         if (state is FileActionsViewModel.UiState.Loading) {
             binding.bottomSheetLoading.isVisible = true
-            binding.bottomSheetContent.isVisible = false
+            binding.bottomSheetHeader.isVisible = false
             viewThemeUtils.platform.colorCircularProgressBar(binding.bottomSheetLoading, ColorRole.PRIMARY)
         } else {
             binding.bottomSheetLoading.isVisible = false
-            binding.bottomSheetContent.isVisible = true
+            binding.bottomSheetHeader.isVisible = true
         }
     }
 

+ 44 - 0
app/src/main/java/com/nextcloud/utils/extensions/BundleExtensions.kt

@@ -0,0 +1,44 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Alper Ozturk
+ * Copyright (C) 2023 Alper Ozturk
+ * Copyright (C) 2023 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.utils.extensions
+
+import android.os.Build
+import android.os.Bundle
+import android.os.Parcelable
+import java.io.Serializable
+
+fun <T : Serializable?> Bundle.getSerializableArgument(key: String, type: Class<T>): T? {
+    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+        this.getSerializable(key, type)
+    } else {
+        @Suppress("UNCHECKED_CAST")
+        this.getSerializable(key) as T
+    }
+}
+
+fun <T : Parcelable?> Bundle.getParcelableArgument(key: String, type: Class<T>): T? {
+    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+        this.getParcelable(key, type)
+    } else {
+        this.getParcelable(key)
+    }
+}

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

@@ -0,0 +1,741 @@
+/*
+ *   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);
+    }
+}

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

@@ -1,750 +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.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
-    }
-}

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

@@ -90,7 +90,6 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCo
 import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.lib.resources.files.RestoreFileVersionRemoteOperation;
 import com.owncloud.android.lib.resources.files.SearchRemoteOperation;
-import com.owncloud.android.lib.resources.status.OwnCloudVersion;
 import com.owncloud.android.operations.CopyFileOperation;
 import com.owncloud.android.operations.CreateFolderOperation;
 import com.owncloud.android.operations.MoveFileOperation;
@@ -1733,8 +1732,9 @@ public class FileDisplayActivity extends FileActivity
             boolean fileAvailable = getStorageManager().fileExists(removedFile.getFileId());
 
             if (leftFragment instanceof FileFragment && !fileAvailable && removedFile.equals(((FileFragment) leftFragment).getFile())) {
-                if (leftFragment instanceof PreviewMediaFragment) {
-                    ((PreviewMediaFragment) leftFragment).stopPreview(true);
+                if (leftFragment instanceof PreviewMediaFragment previewMediaFragment) {
+                    previewMediaFragment.stopPreview(true);
+                    onBackPressed();
                 }
                 setFile(getStorageManager().getFileById(removedFile.getParentId()));
                 resetTitleBarAndScrolling();
@@ -1742,8 +1742,8 @@ public class FileDisplayActivity extends FileActivity
             OCFile parentFile = getStorageManager().getFileById(removedFile.getParentId());
             if (parentFile != null && parentFile.equals(getCurrentDir())) {
                 updateListOfFilesFragment(false);
-            } else if (getLeftFragment() instanceof GalleryFragment) {
-                ((GalleryFragment) getLeftFragment()).onRefresh();
+            } else if (getLeftFragment() instanceof GalleryFragment galleryFragment) {
+                galleryFragment.onRefresh();
             }
             supportInvalidateOptionsMenu();
         } else {

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

@@ -1,105 +0,0 @@
-/*
- *
- * Nextcloud Android client application
- *
- * @author Tobias Kaminsky
- * Copyright (C) 2020 Tobias Kaminsky
- * Copyright (C) 2020 Nextcloud GmbH
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package com.owncloud.android.ui.dialog;
-
-import android.app.Dialog;
-import android.os.Bundle;
-
-import com.google.android.material.button.MaterialButton;
-import com.google.android.material.dialog.MaterialAlertDialogBuilder;
-import com.nextcloud.client.account.User;
-import com.nextcloud.client.di.Injectable;
-import com.nextcloud.client.jobs.BackgroundJobManager;
-import com.owncloud.android.R;
-import com.owncloud.android.utils.theme.ViewThemeUtils;
-
-import javax.inject.Inject;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.AlertDialog;
-import androidx.fragment.app.DialogFragment;
-
-public class AccountRemovalConfirmationDialog extends DialogFragment implements Injectable {
-    private static final String KEY_USER = "USER";
-
-    @Inject BackgroundJobManager backgroundJobManager;
-    @Inject ViewThemeUtils viewThemeUtils;
-    private User user;
-
-    public static AccountRemovalConfirmationDialog newInstance(User user) {
-        Bundle bundle = new Bundle();
-        bundle.putParcelable(KEY_USER, user);
-
-        AccountRemovalConfirmationDialog dialog = new AccountRemovalConfirmationDialog();
-        dialog.setArguments(bundle);
-
-        return dialog;
-    }
-
-    @Override
-    public void onCreate(@Nullable Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        Bundle arguments = getArguments();
-        if (arguments != null) {
-            user = arguments.getParcelable(KEY_USER);
-        }
-    }
-
-    @Override
-    public void onStart() {
-        super.onStart();
-
-        AlertDialog alertDialog = (AlertDialog) getDialog();
-        if (alertDialog != null) {
-
-            MaterialButton positiveButton = (MaterialButton) alertDialog.getButton(AlertDialog.BUTTON_POSITIVE);
-            if (positiveButton != null) {
-                viewThemeUtils.material.colorMaterialButtonPrimaryTonal(positiveButton);
-            }
-
-            MaterialButton negativeButton = (MaterialButton) alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE);
-            if (negativeButton != null) {
-                viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(negativeButton);
-            }
-        }
-    }
-
-    @NonNull
-    @Override
-    public Dialog onCreateDialog(Bundle savedInstanceState) {
-        MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity())
-            .setTitle(R.string.delete_account)
-            .setMessage(getResources().getString(R.string.delete_account_warning, user.getAccountName()))
-            .setIcon(R.drawable.ic_warning)
-            .setPositiveButton(R.string.common_ok,
-                               (dialogInterface, i) -> backgroundJobManager.startAccountRemovalJob(user.getAccountName(),
-                                                                                                   false))
-            .setNegativeButton(R.string.common_cancel, null);
-
-        viewThemeUtils.dialog.colorMaterialAlertDialogBackground(requireActivity(), builder);
-
-        return  builder.create();
-    }
-}

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

@@ -0,0 +1,105 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2020 Tobias Kaminsky
+ * Copyright (C) 2020 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package com.owncloud.android.ui.dialog
+
+import android.app.Dialog
+import android.content.DialogInterface
+import android.os.Build
+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.account.User
+import com.nextcloud.client.di.Injectable
+import com.nextcloud.client.jobs.BackgroundJobManager
+import com.owncloud.android.R
+import com.owncloud.android.utils.theme.ViewThemeUtils
+import javax.inject.Inject
+
+class AccountRemovalConfirmationDialog : DialogFragment(), Injectable {
+    @JvmField
+    @Inject
+    var backgroundJobManager: BackgroundJobManager? = null
+
+    @JvmField
+    @Inject
+    var viewThemeUtils: ViewThemeUtils? = null
+
+    private var user: User? = null
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        user = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            requireArguments().getParcelable(KEY_USER, User::class.java)
+        } else {
+            @Suppress("DEPRECATION")
+            requireArguments().getParcelable(KEY_USER)
+        }
+    }
+
+    override fun onStart() {
+        super.onStart()
+
+        val alertDialog = dialog as AlertDialog?
+
+        if (alertDialog != null) {
+            val positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE) as MaterialButton
+            viewThemeUtils?.material?.colorMaterialButtonPrimaryTonal(positiveButton)
+
+            val negativeButton = alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE) as MaterialButton
+            viewThemeUtils?.material?.colorMaterialButtonPrimaryBorderless(negativeButton)
+        }
+    }
+
+    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+        val builder = MaterialAlertDialogBuilder(requireActivity())
+            .setTitle(R.string.delete_account)
+            .setMessage(resources.getString(R.string.delete_account_warning, user!!.accountName))
+            .setIcon(R.drawable.ic_warning)
+            .setPositiveButton(R.string.common_ok) { _: DialogInterface?, _: Int ->
+                backgroundJobManager?.startAccountRemovalJob(
+                    user!!.accountName,
+                    false
+                )
+            }
+            .setNegativeButton(R.string.common_cancel, null)
+
+        viewThemeUtils?.dialog?.colorMaterialAlertDialogBackground(requireActivity(), builder)
+
+        return builder.create()
+    }
+
+    companion object {
+
+        private const val KEY_USER = "USER"
+
+        @JvmStatic
+        fun newInstance(user: User?): AccountRemovalConfirmationDialog {
+            val bundle = Bundle()
+            bundle.putParcelable(KEY_USER, user)
+            val dialog = AccountRemovalConfirmationDialog()
+            dialog.arguments = bundle
+            return dialog
+        }
+    }
+}

+ 6 - 1
app/src/main/java/com/owncloud/android/ui/dialog/ConfirmationDialogFragment.kt

@@ -75,11 +75,16 @@ open class ConfirmationDialogFragment : DialogFragment(), Injectable {
         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 (titleId == 0) {
+            builder.setTitle(R.string.dialog_alert_title)
+        } else if (titleId != -1) {
+            builder.setTitle(titleId)
+        }
+
         if (positiveButtonTextId != -1) {
             builder.setPositiveButton(positiveButtonTextId) { dialog: DialogInterface, _: Int ->
                 mListener?.onConfirmation(tag)

+ 0 - 230
app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.java

@@ -1,230 +0,0 @@
-/*
- *   ownCloud Android client application
- *
- *   @author David A. Velasco
- *   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.content.DialogInterface;
-import android.os.Bundle;
-import android.text.Editable;
-import android.text.TextUtils;
-import android.text.TextWatcher;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.Button;
-import android.widget.TextView;
-
-import com.google.android.material.button.MaterialButton;
-import com.google.android.material.dialog.MaterialAlertDialogBuilder;
-import com.google.common.collect.Sets;
-import com.nextcloud.client.di.Injectable;
-import com.owncloud.android.R;
-import com.owncloud.android.databinding.EditBoxDialogBinding;
-import com.owncloud.android.datamodel.FileDataStorageManager;
-import com.owncloud.android.datamodel.OCFile;
-import com.owncloud.android.lib.resources.files.FileUtils;
-import com.owncloud.android.ui.activity.ComponentsGetter;
-import com.owncloud.android.utils.DisplayUtils;
-import com.owncloud.android.utils.KeyboardUtils;
-import com.owncloud.android.utils.theme.ViewThemeUtils;
-
-import java.util.List;
-import java.util.Set;
-
-import javax.inject.Inject;
-
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AlertDialog;
-import androidx.fragment.app.DialogFragment;
-
-/**
- * Dialog to input the name for a new folder to create.
- * <p>
- * Triggers the folder creation when name is confirmed.
- */
-public class CreateFolderDialogFragment
-    extends DialogFragment implements DialogInterface.OnClickListener, Injectable {
-
-    private static final String ARG_PARENT_FOLDER = "PARENT_FOLDER";
-
-    public static final String CREATE_FOLDER_FRAGMENT = "CREATE_FOLDER_FRAGMENT";
-
-    @Inject FileDataStorageManager fileDataStorageManager;
-    @Inject ViewThemeUtils viewThemeUtils;
-    @Inject KeyboardUtils keyboardUtils;
-
-
-    private OCFile mParentFolder;
-    private MaterialButton positiveButton;
-
-
-    private EditBoxDialogBinding binding;
-
-    /**
-     * Public factory method to create new CreateFolderDialogFragment instances.
-     *
-     * @param parentFolder Folder to create
-     * @return Dialog ready to show.
-     */
-    public static CreateFolderDialogFragment newInstance(OCFile parentFolder) {
-        CreateFolderDialogFragment frag = new CreateFolderDialogFragment();
-        Bundle args = new Bundle();
-        args.putParcelable(ARG_PARENT_FOLDER, parentFolder);
-        frag.setArguments(args);
-        return frag;
-
-    }
-
-    @Override
-    public void onStart() {
-        super.onStart();
-
-        bindButton();
-    }
-
-    private void bindButton() {
-        Dialog dialog = getDialog();
-
-        if (dialog instanceof AlertDialog alertDialog) {
-            positiveButton = (MaterialButton) alertDialog.getButton(AlertDialog.BUTTON_POSITIVE);
-            MaterialButton negativeButton = (MaterialButton) alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE);
-            viewThemeUtils.material.colorMaterialButtonPrimaryTonal(positiveButton);
-            viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(negativeButton);
-        }
-    }
-
-    @Override
-    public void onResume() {
-        super.onResume();
-
-        bindButton();
-        keyboardUtils.showKeyboardForEditText(requireDialog().getWindow(), binding.userInput);
-    }
-
-    @NonNull
-    @Override
-    public Dialog onCreateDialog(Bundle savedInstanceState) {
-        mParentFolder = getArguments().getParcelable(ARG_PARENT_FOLDER);
-
-        // Inflate the layout for the dialog
-        LayoutInflater inflater = requireActivity().getLayoutInflater();
-        binding = EditBoxDialogBinding.inflate(inflater, null, false);
-        View view = binding.getRoot();
-
-        // Setup layout
-        binding.userInput.setText("");
-        viewThemeUtils.material.colorTextInputLayout(binding.userInputContainer);
-
-        OCFile parentFolder = requireArguments().getParcelable(ARG_PARENT_FOLDER);
-        List<OCFile> folderContent = fileDataStorageManager.getFolderContent(parentFolder, false);
-        Set<String> fileNames = Sets.newHashSetWithExpectedSize(folderContent.size());
-
-        for (OCFile file : folderContent) {
-            fileNames.add(file.getFileName());
-        }
-
-        // Add TextChangedListener to handle showing/hiding the input warning message
-        binding.userInput.addTextChangedListener(new TextWatcher() {
-            @Override
-            public void afterTextChanged(Editable s) {
-            }
-
-            @Override
-            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
-            }
-
-            /**
-             * When user enters a hidden file name, the 'hidden file' message is shown. Otherwise,
-             * the message is ensured to be hidden.
-             */
-            @Override
-            public void onTextChanged(CharSequence s, int start, int before, int count) {
-                String newFileName = "";
-                if (binding.userInput.getText() != null) {
-                    newFileName = binding.userInput.getText().toString().trim();
-                }
-
-                if (!TextUtils.isEmpty(newFileName) && newFileName.charAt(0) == '.') {
-                    binding.userInputContainer.setError(getText(R.string.hidden_file_name_warning));
-                } else if (TextUtils.isEmpty(newFileName)) {
-                    binding.userInputContainer.setError(getString(R.string.filename_empty));
-                    if (positiveButton == null) {
-                        bindButton();
-                    }
-                    positiveButton.setEnabled(false);
-                } else if (!FileUtils.isValidName(newFileName)) {
-                    binding.userInputContainer.setError(getString(R.string.filename_forbidden_charaters_from_server));
-                    positiveButton.setEnabled(false);
-                } else if (fileNames.contains(newFileName)) {
-                    binding.userInputContainer.setError(getText(R.string.file_already_exists));
-                    positiveButton.setEnabled(false);
-                } else if (binding.userInputContainer.getError() != null) {
-                    binding.userInputContainer.setError(null);
-                    // Called to remove extra padding
-                    binding.userInputContainer.setErrorEnabled(false);
-                    positiveButton.setEnabled(true);
-                }
-            }
-        });
-
-        // Build the dialog
-        MaterialAlertDialogBuilder builder = buildMaterialAlertDialog(view);
-
-        viewThemeUtils.dialog.colorMaterialAlertDialogBackground(binding.userInputContainer.getContext(), builder);
-
-        return builder.create();
-    }
-
-    private MaterialAlertDialogBuilder buildMaterialAlertDialog(View view) {
-        MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity());
-
-        builder
-            .setView(view)
-            .setPositiveButton(R.string.folder_confirm_create, this)
-            .setNegativeButton(R.string.common_cancel, this)
-            .setTitle(R.string.uploader_info_dirname);
-
-        return builder;
-    }
-
-    @Override
-    public void onClick(DialogInterface dialog, int which) {
-        if (which == AlertDialog.BUTTON_POSITIVE) {
-            String newFolderName =
-                ((TextView) (getDialog().findViewById(R.id.user_input)))
-                    .getText().toString().trim();
-
-            if (TextUtils.isEmpty(newFolderName)) {
-                DisplayUtils.showSnackMessage(requireActivity(), R.string.filename_empty);
-                return;
-            }
-
-            if (!FileUtils.isValidName(newFolderName)) {
-                DisplayUtils.showSnackMessage(requireActivity(), R.string.filename_forbidden_charaters_from_server);
-
-                return;
-            }
-
-            String path = mParentFolder.getDecryptedRemotePath() + newFolderName + OCFile.PATH_SEPARATOR;
-
-            ((ComponentsGetter) requireActivity()).getFileOperationsHelper().createFolder(path);
-        }
-    }
-}

+ 203 - 0
app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.kt

@@ -0,0 +1,203 @@
+/*
+ *   ownCloud Android client application
+ *
+ *   @author David A. Velasco
+ *   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.content.DialogInterface
+import android.os.Bundle
+import android.text.Editable
+import android.text.TextUtils
+import android.text.TextWatcher
+import android.view.View
+import android.widget.TextView
+import androidx.appcompat.app.AlertDialog
+import androidx.fragment.app.DialogFragment
+import com.google.android.material.button.MaterialButton
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.google.common.collect.Sets
+import com.nextcloud.client.di.Injectable
+import com.owncloud.android.R
+import com.owncloud.android.databinding.EditBoxDialogBinding
+import com.owncloud.android.datamodel.FileDataStorageManager
+import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.lib.resources.files.FileUtils
+import com.owncloud.android.ui.activity.ComponentsGetter
+import com.owncloud.android.utils.DisplayUtils
+import com.owncloud.android.utils.KeyboardUtils
+import com.owncloud.android.utils.theme.ViewThemeUtils
+import javax.inject.Inject
+
+/**
+ * Dialog to input the name for a new folder to create.
+ *
+ *
+ * Triggers the folder creation when name is confirmed.
+ */
+class CreateFolderDialogFragment : DialogFragment(), DialogInterface.OnClickListener, Injectable {
+    @JvmField
+    @Inject
+    var fileDataStorageManager: FileDataStorageManager? = null
+
+    @JvmField
+    @Inject
+    var viewThemeUtils: ViewThemeUtils? = null
+
+    @JvmField
+    @Inject
+    var keyboardUtils: KeyboardUtils? = null
+    private var mParentFolder: OCFile? = null
+    private var positiveButton: MaterialButton? = null
+
+    private lateinit var binding: EditBoxDialogBinding
+
+    override fun onStart() {
+        super.onStart()
+        bindButton()
+    }
+
+    private fun bindButton() {
+        val dialog = dialog
+
+        if (dialog is AlertDialog) {
+            positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE) as MaterialButton
+            val negativeButton = dialog.getButton(AlertDialog.BUTTON_NEGATIVE) as MaterialButton
+
+            viewThemeUtils?.material?.colorMaterialButtonPrimaryTonal(positiveButton!!)
+            viewThemeUtils?.material?.colorMaterialButtonPrimaryBorderless(negativeButton)
+        }
+    }
+
+    override fun onResume() {
+        super.onResume()
+        bindButton()
+        keyboardUtils!!.showKeyboardForEditText(requireDialog().window, binding.userInput)
+    }
+
+    @Suppress("EmptyFunctionBlock")
+    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+        mParentFolder = arguments?.getParcelable(ARG_PARENT_FOLDER)
+
+        // Inflate the layout for the dialog
+        val inflater = requireActivity().layoutInflater
+        binding = EditBoxDialogBinding.inflate(inflater, null, false)
+        val view: View = binding.root
+
+        // Setup layout
+        binding.userInput.setText("")
+        viewThemeUtils?.material?.colorTextInputLayout(binding.userInputContainer)
+
+        val parentFolder = requireArguments().getParcelable<OCFile>(ARG_PARENT_FOLDER)
+
+        val folderContent = fileDataStorageManager!!.getFolderContent(parentFolder, false)
+        val fileNames: MutableSet<String> = Sets.newHashSetWithExpectedSize(folderContent.size)
+        for (file in folderContent) {
+            fileNames.add(file.fileName)
+        }
+
+        // Add TextChangedListener to handle showing/hiding the input warning message
+        binding.userInput.addTextChangedListener(object : TextWatcher {
+            override fun afterTextChanged(s: Editable) {}
+            override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
+
+            /**
+             * When user enters a hidden file name, the 'hidden file' message is shown. Otherwise,
+             * the message is ensured to be hidden.
+             */
+            override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
+                var newFileName = ""
+                if (binding.userInput.text != null) {
+                    newFileName = binding.userInput.text.toString().trim { it <= ' ' }
+                }
+                if (!TextUtils.isEmpty(newFileName) && newFileName[0] == '.') {
+                    binding.userInputContainer.error = getText(R.string.hidden_file_name_warning)
+                } else if (TextUtils.isEmpty(newFileName)) {
+                    binding.userInputContainer.error = getString(R.string.filename_empty)
+                    if (positiveButton == null) {
+                        bindButton()
+                    }
+                    positiveButton!!.isEnabled = false
+                } else if (!FileUtils.isValidName(newFileName)) {
+                    binding.userInputContainer.error = getString(R.string.filename_forbidden_charaters_from_server)
+                    positiveButton!!.isEnabled = false
+                } else if (fileNames.contains(newFileName)) {
+                    binding.userInputContainer.error = getText(R.string.file_already_exists)
+                    positiveButton!!.isEnabled = false
+                } else if (binding.userInputContainer.error != null) {
+                    binding.userInputContainer.error = null
+                    // Called to remove extra padding
+                    binding.userInputContainer.isErrorEnabled = false
+                    positiveButton!!.isEnabled = true
+                }
+            }
+        })
+
+        // Build the dialog
+        val builder = buildMaterialAlertDialog(view)
+        viewThemeUtils?.dialog?.colorMaterialAlertDialogBackground(binding.userInputContainer.context, builder)
+        return builder.create()
+    }
+
+    private fun buildMaterialAlertDialog(view: View): MaterialAlertDialogBuilder {
+        val builder = MaterialAlertDialogBuilder(requireActivity())
+        builder
+            .setView(view)
+            .setPositiveButton(R.string.folder_confirm_create, this)
+            .setNegativeButton(R.string.common_cancel, this)
+            .setTitle(R.string.uploader_info_dirname)
+        return builder
+    }
+
+    override fun onClick(dialog: DialogInterface, which: Int) {
+        if (which == AlertDialog.BUTTON_POSITIVE) {
+            val newFolderName = (getDialog()!!.findViewById<View>(R.id.user_input) as TextView)
+                .text.toString().trim { it <= ' ' }
+            if (TextUtils.isEmpty(newFolderName)) {
+                DisplayUtils.showSnackMessage(requireActivity(), R.string.filename_empty)
+                return
+            }
+            if (!FileUtils.isValidName(newFolderName)) {
+                DisplayUtils.showSnackMessage(requireActivity(), R.string.filename_forbidden_charaters_from_server)
+                return
+            }
+            val path = mParentFolder!!.decryptedRemotePath + newFolderName + OCFile.PATH_SEPARATOR
+            (requireActivity() as ComponentsGetter).fileOperationsHelper.createFolder(path)
+        }
+    }
+
+    companion object {
+        private const val ARG_PARENT_FOLDER = "PARENT_FOLDER"
+        const val CREATE_FOLDER_FRAGMENT = "CREATE_FOLDER_FRAGMENT"
+
+        /**
+         * Public factory method to create new CreateFolderDialogFragment instances.
+         *
+         * @param parentFolder Folder to create
+         * @return Dialog ready to show.
+         */
+        @JvmStatic
+        fun newInstance(parentFolder: OCFile?): CreateFolderDialogFragment {
+            val frag = CreateFolderDialogFragment()
+            val args = Bundle()
+            args.putParcelable(ARG_PARENT_FOLDER, parentFolder)
+            frag.arguments = args
+            return frag
+        }
+    }
+}

+ 0 - 196
app/src/main/java/com/owncloud/android/ui/dialog/ExpirationDatePickerDialogFragment.java

@@ -1,196 +0,0 @@
-/*
- *   ownCloud Android client application
- *
- *   @author David A. Velasco
- *   @author Andy Scherzinger
- *   @author TSI-mc
- *   Copyright (C) 2015 ownCloud Inc.
- *   Copyright (C) 2018 Andy Scherzinger
- *   Copyright (C) 2018 TSI-mc
- *
- *   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.DatePickerDialog;
-import android.app.Dialog;
-import android.os.Bundle;
-import android.text.format.DateUtils;
-import android.widget.DatePicker;
-
-import com.google.android.material.button.MaterialButton;
-import com.nextcloud.client.di.Injectable;
-import com.owncloud.android.R;
-import com.owncloud.android.utils.theme.ViewThemeUtils;
-
-import java.util.Calendar;
-
-import javax.inject.Inject;
-
-import androidx.annotation.NonNull;
-import androidx.fragment.app.DialogFragment;
-
-/**
- * Dialog requesting a date after today.
- */
-public class ExpirationDatePickerDialogFragment
-    extends DialogFragment
-    implements DatePickerDialog.OnDateSetListener, Injectable {
-
-    /** Tag for FragmentsManager */
-    public static final String DATE_PICKER_DIALOG = "DATE_PICKER_DIALOG";
-
-    /** Parameter constant for date chosen initially */
-    private static final String ARG_CHOSEN_DATE_IN_MILLIS = "CHOSEN_DATE_IN_MILLIS";
-
-    @Inject ViewThemeUtils viewThemeUtils;
-    private OnExpiryDateListener onExpiryDateListener;
-
-    /**
-     * Factory method to create new instances
-     *
-     * @param chosenDateInMillis Date chosen when the dialog appears
-     * @return New dialog instance
-     */
-    public static ExpirationDatePickerDialogFragment newInstance(long chosenDateInMillis) {
-        Bundle arguments = new Bundle();
-        arguments.putLong(ARG_CHOSEN_DATE_IN_MILLIS, chosenDateInMillis);
-
-        ExpirationDatePickerDialogFragment dialog = new ExpirationDatePickerDialogFragment();
-        dialog.setArguments(arguments);
-        return dialog;
-    }
-
-    public void setOnExpiryDateListener(OnExpiryDateListener onExpiryDateListener) {
-        this.onExpiryDateListener = onExpiryDateListener;
-    }
-
-
-    @Override
-    public void onStart() {
-        super.onStart();
-        final Dialog currentDialog = getDialog();
-
-        if (currentDialog != null) {
-            final DatePickerDialog dialog = (DatePickerDialog) currentDialog;
-
-            MaterialButton positiveButton = (MaterialButton) dialog.getButton(DatePickerDialog.BUTTON_POSITIVE);
-            if (positiveButton != null) {
-                viewThemeUtils.material.colorMaterialButtonPrimaryTonal(positiveButton);
-            }
-
-            MaterialButton negativeButton = (MaterialButton) dialog.getButton(DatePickerDialog.BUTTON_NEGATIVE);
-            if (negativeButton != null) {
-                viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(negativeButton);
-            }
-
-            MaterialButton neutralButton = (MaterialButton) dialog.getButton(DatePickerDialog.BUTTON_NEUTRAL);
-            if (neutralButton != null) {
-                viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(neutralButton);
-            }
-        }
-    }
-
-    /**
-     * {@inheritDoc}
-     *
-     * @return A new dialog to let the user choose an expiration date that will be bound to a share link.
-     */
-    @Override
-    @NonNull
-    public Dialog onCreateDialog(Bundle savedInstanceState) {
-
-        // Chosen date received as an argument must be later than tomorrow ; default to tomorrow in other case
-        final Calendar chosenDate = Calendar.getInstance();
-        long tomorrowInMillis = chosenDate.getTimeInMillis() + DateUtils.DAY_IN_MILLIS;
-        long chosenDateInMillis = requireArguments().getLong(ARG_CHOSEN_DATE_IN_MILLIS);
-        chosenDate.setTimeInMillis(Math.max(chosenDateInMillis, tomorrowInMillis));
-
-        // Create a new instance of DatePickerDialog
-        DatePickerDialog dialog = new DatePickerDialog(
-            requireActivity(),
-            R.style.FallbackDatePickerDialogTheme,
-            this,
-            chosenDate.get(Calendar.YEAR),
-            chosenDate.get(Calendar.MONTH),
-            chosenDate.get(Calendar.DAY_OF_MONTH)
-        );
-
-        //show unset button only when date is already selected
-        if (chosenDateInMillis > 0) {
-            dialog.setButton(
-                Dialog.BUTTON_NEGATIVE,
-                getText(R.string.share_via_link_unset_password),
-                (dialog1, which) -> {
-                    if (onExpiryDateListener != null) {
-                        onExpiryDateListener.onDateUnSet();
-                    }
-                });
-        }
-
-        // Prevent days in the past may be chosen
-        DatePicker picker = dialog.getDatePicker();
-        picker.setMinDate(tomorrowInMillis - 1000);
-
-        // Enforce spinners view; ignored by MD-based theme in Android >=5, but calendar is REALLY buggy
-        // in Android < 5, so let's be sure it never appears (in tablets both spinners and calendar are
-        // shown by default)
-        picker.setCalendarViewShown(false);
-
-        return dialog;
-    }
-
-    public long getCurrentSelectionMillis() {
-        final Dialog dialog = getDialog();
-        if (dialog != null) {
-            final DatePickerDialog datePickerDialog = (DatePickerDialog) dialog;
-            final DatePicker picker = datePickerDialog.getDatePicker();
-            return yearMonthDayToMillis(picker.getYear(), picker.getMonth(), picker.getDayOfMonth());
-        }
-        return 0;
-    }
-
-    /**
-     * Called when the user chooses an expiration date.
-     *
-     * @param view        View instance where the date was chosen
-     * @param year        Year of the date chosen.
-     * @param monthOfYear Month of the date chosen [0, 11]
-     * @param dayOfMonth  Day of the date chosen
-     */
-    @Override
-    public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth) {
-
-        long chosenDateInMillis = yearMonthDayToMillis(year, monthOfYear, dayOfMonth);
-
-        if (onExpiryDateListener != null) {
-            onExpiryDateListener.onDateSet(year, monthOfYear, dayOfMonth, chosenDateInMillis);
-        }
-    }
-
-    private long yearMonthDayToMillis(int year, int monthOfYear, int dayOfMonth) {
-        Calendar date = Calendar.getInstance();
-        date.set(Calendar.YEAR, year);
-        date.set(Calendar.MONTH, monthOfYear);
-        date.set(Calendar.DAY_OF_MONTH, dayOfMonth);
-        return date.getTimeInMillis();
-    }
-
-    public interface OnExpiryDateListener {
-        void onDateSet(int year, int monthOfYear, int dayOfMonth, long chosenDateInMillis);
-
-        void onDateUnSet();
-    }
-}

+ 183 - 0
app/src/main/java/com/owncloud/android/ui/dialog/ExpirationDatePickerDialogFragment.kt

@@ -0,0 +1,183 @@
+/*
+ *   ownCloud Android client application
+ *
+ *   @author David A. Velasco
+ *   @author Andy Scherzinger
+ *   @author TSI-mc
+ *   Copyright (C) 2015 ownCloud Inc.
+ *   Copyright (C) 2018 Andy Scherzinger
+ *   Copyright (C) 2018 TSI-mc
+ *
+ *   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.DatePickerDialog
+import android.app.DatePickerDialog.OnDateSetListener
+import android.app.Dialog
+import android.content.DialogInterface
+import android.os.Bundle
+import android.text.format.DateUtils
+import android.widget.DatePicker
+import androidx.fragment.app.DialogFragment
+import com.google.android.material.button.MaterialButton
+import com.nextcloud.client.di.Injectable
+import com.owncloud.android.R
+import com.owncloud.android.utils.theme.ViewThemeUtils
+import java.util.Calendar
+import javax.inject.Inject
+
+/**
+ * Dialog requesting a date after today.
+ */
+class ExpirationDatePickerDialogFragment : DialogFragment(), OnDateSetListener, Injectable {
+
+    @JvmField
+    @Inject
+    var viewThemeUtils: ViewThemeUtils? = null
+
+    private var onExpiryDateListener: OnExpiryDateListener? = null
+
+    fun setOnExpiryDateListener(onExpiryDateListener: OnExpiryDateListener?) {
+        this.onExpiryDateListener = onExpiryDateListener
+    }
+
+    override fun onStart() {
+        super.onStart()
+
+        val currentDialog = dialog
+
+        if (currentDialog != null) {
+            val dialog = currentDialog as DatePickerDialog
+
+            val positiveButton = dialog.getButton(DatePickerDialog.BUTTON_POSITIVE) as MaterialButton?
+            if (positiveButton != null) {
+                viewThemeUtils?.material?.colorMaterialButtonPrimaryTonal(positiveButton)
+            }
+            val negativeButton = dialog.getButton(DatePickerDialog.BUTTON_NEGATIVE) as MaterialButton?
+            if (negativeButton != null) {
+                viewThemeUtils?.material?.colorMaterialButtonPrimaryBorderless(negativeButton)
+            }
+            val neutralButton = dialog.getButton(DatePickerDialog.BUTTON_NEUTRAL) as MaterialButton?
+            if (neutralButton != null) {
+                viewThemeUtils?.material?.colorMaterialButtonPrimaryBorderless(neutralButton)
+            }
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @return A new dialog to let the user choose an expiration date that will be bound to a share link.
+     */
+    @Suppress("DEPRECATION", "MagicNumber")
+    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+        // Chosen date received as an argument must be later than tomorrow ; default to tomorrow in other case
+        val chosenDate = Calendar.getInstance()
+        val tomorrowInMillis = chosenDate.timeInMillis + DateUtils.DAY_IN_MILLIS
+        val chosenDateInMillis = requireArguments().getLong(ARG_CHOSEN_DATE_IN_MILLIS)
+        chosenDate.timeInMillis = chosenDateInMillis.coerceAtLeast(tomorrowInMillis)
+
+        // Create a new instance of DatePickerDialog
+        val dialog = DatePickerDialog(
+            requireActivity(),
+            R.style.FallbackDatePickerDialogTheme,
+            this,
+            chosenDate[Calendar.YEAR],
+            chosenDate[Calendar.MONTH],
+            chosenDate[Calendar.DAY_OF_MONTH]
+        )
+
+        // show unset button only when date is already selected
+        if (chosenDateInMillis > 0) {
+            dialog.setButton(
+                Dialog.BUTTON_NEGATIVE,
+                getText(R.string.share_via_link_unset_password)
+            ) { _: DialogInterface?, _: Int ->
+                onExpiryDateListener?.onDateUnSet()
+            }
+        }
+
+        // Prevent days in the past may be chosen
+        val picker = dialog.datePicker
+        picker.minDate = tomorrowInMillis - 1000
+
+        // Enforce spinners view; ignored by MD-based theme in Android >=5, but calendar is REALLY buggy
+        // in Android < 5, so let's be sure it never appears (in tablets both spinners and calendar are
+        // shown by default)
+        @Suppress("DEPRECATION")
+        picker.calendarViewShown = false
+        return dialog
+    }
+
+    val currentSelectionMillis: Long
+        get() {
+            val dialog = dialog
+            if (dialog != null) {
+                val datePickerDialog = dialog as DatePickerDialog
+                val picker = datePickerDialog.datePicker
+                return yearMonthDayToMillis(picker.year, picker.month, picker.dayOfMonth)
+            }
+            return 0
+        }
+
+    /**
+     * Called when the user chooses an expiration date.
+     *
+     * @param view        View instance where the date was chosen
+     * @param year        Year of the date chosen.
+     * @param monthOfYear Month of the date chosen [0, 11]
+     * @param dayOfMonth  Day of the date chosen
+     */
+    override fun onDateSet(view: DatePicker, year: Int, monthOfYear: Int, dayOfMonth: Int) {
+        val chosenDateInMillis = yearMonthDayToMillis(year, monthOfYear, dayOfMonth)
+        if (onExpiryDateListener != null) {
+            onExpiryDateListener?.onDateSet(year, monthOfYear, dayOfMonth, chosenDateInMillis)
+        }
+    }
+
+    private fun yearMonthDayToMillis(year: Int, monthOfYear: Int, dayOfMonth: Int): Long {
+        val date = Calendar.getInstance()
+        date[Calendar.YEAR] = year
+        date[Calendar.MONTH] = monthOfYear
+        date[Calendar.DAY_OF_MONTH] = dayOfMonth
+        return date.timeInMillis
+    }
+
+    interface OnExpiryDateListener {
+        fun onDateSet(year: Int, monthOfYear: Int, dayOfMonth: Int, chosenDateInMillis: Long)
+        fun onDateUnSet()
+    }
+
+    companion object {
+        /** Tag for FragmentsManager  */
+        const val DATE_PICKER_DIALOG = "DATE_PICKER_DIALOG"
+
+        /** Parameter constant for date chosen initially  */
+        private const val ARG_CHOSEN_DATE_IN_MILLIS = "CHOSEN_DATE_IN_MILLIS"
+
+        /**
+         * Factory method to create new instances
+         *
+         * @param chosenDateInMillis Date chosen when the dialog appears
+         * @return New dialog instance
+         */
+        fun newInstance(chosenDateInMillis: Long): ExpirationDatePickerDialogFragment {
+            val arguments = Bundle()
+            arguments.putLong(ARG_CHOSEN_DATE_IN_MILLIS, chosenDateInMillis)
+            val dialog = ExpirationDatePickerDialogFragment()
+            dialog.arguments = arguments
+            return dialog
+        }
+    }
+}

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

@@ -54,7 +54,7 @@ class LoadingDialog : DialogFragment(), Injectable {
             viewThemeUtils?.platform?.tintDrawable(requireContext(), loadingDrawable)
         }
 
-        viewThemeUtils?.platform?.colorViewBackground(binding.loadingLayout, ColorRole.SURFACE_VARIANT)
+        viewThemeUtils?.platform?.colorViewBackground(binding.loadingLayout, ColorRole.SURFACE)
 
         return binding.root
     }

+ 0 - 135
app/src/main/java/com/owncloud/android/ui/dialog/SendFilesDialog.java

@@ -1,135 +0,0 @@
-package com.owncloud.android.ui.dialog;
-
-import android.content.ComponentName;
-import android.content.Intent;
-import android.content.pm.ResolveInfo;
-import android.graphics.drawable.Drawable;
-import android.net.Uri;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.Toast;
-
-import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
-import com.nextcloud.client.utils.IntentUtil;
-import com.owncloud.android.R;
-import com.owncloud.android.datamodel.OCFile;
-import com.owncloud.android.ui.adapter.SendButtonAdapter;
-import com.owncloud.android.ui.components.SendButtonData;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Set;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.recyclerview.widget.GridLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-
-/*
- * Nextcloud Android client application
- *
- * @author Tobias Kaminsky
- * Copyright (C) 2020 Tobias Kaminsky
- * Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
- */
-public class SendFilesDialog extends BottomSheetDialogFragment {
-
-    private static final String KEY_OCFILES = "KEY_OCFILES";
-
-    private OCFile[] files;
-
-    public static SendFilesDialog newInstance(Set<OCFile> files) {
-
-        SendFilesDialog dialogFragment = new SendFilesDialog();
-
-        Bundle args = new Bundle();
-        args.putParcelableArray(KEY_OCFILES, files.toArray(new OCFile[0]));
-        dialogFragment.setArguments(args);
-
-        return dialogFragment;
-    }
-
-    @Override
-    public void onCreate(@Nullable Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        // keep the state of the fragment on configuration changes
-        setRetainInstance(true);
-
-        files = (OCFile[]) requireArguments().getParcelableArray(KEY_OCFILES);
-    }
-
-    @Nullable
-    @Override
-    public View onCreateView(@NonNull LayoutInflater inflater,
-                             @Nullable ViewGroup container,
-                             @Nullable Bundle savedInstanceState) {
-
-        View view = inflater.inflate(R.layout.send_files_fragment, container, false);
-
-        // populate send apps
-        Intent sendIntent = IntentUtil.createSendIntent(requireContext(), files);
-        List<ResolveInfo> matches = requireActivity().getPackageManager().queryIntentActivities(sendIntent, 0);
-        if (matches.isEmpty()) {
-            Toast.makeText(getContext(), R.string.no_send_app, Toast.LENGTH_SHORT).show();
-            dismiss();
-            return null;
-        }
-
-        List<SendButtonData> sendButtonDataList = setupSendButtonData(matches);
-
-        SendButtonAdapter.ClickListener clickListener = setupSendButtonClickListener(sendIntent);
-
-        RecyclerView sendButtonsView = view.findViewById(R.id.send_button_recycler_view);
-        sendButtonsView.setLayoutManager(new GridLayoutManager(getActivity(), 4));
-        sendButtonsView.setAdapter(new SendButtonAdapter(sendButtonDataList, clickListener));
-
-        return view;
-    }
-
-    @NonNull
-    private SendButtonAdapter.ClickListener setupSendButtonClickListener(Intent sendIntent) {
-        return sendButtonDataData -> {
-            String packageName = sendButtonDataData.getPackageName();
-            String activityName = sendButtonDataData.getActivityName();
-
-            sendIntent.setComponent(new ComponentName(packageName, activityName));
-            requireActivity().startActivity(Intent.createChooser(sendIntent, getString(R.string.send)));
-
-            dismiss();
-        };
-    }
-
-    @NonNull
-    private List<SendButtonData> setupSendButtonData(List<ResolveInfo> matches) {
-        Drawable icon;
-        SendButtonData sendButtonData;
-        CharSequence label;
-
-        List<SendButtonData> sendButtonDataList = new ArrayList<>(matches.size());
-        for (ResolveInfo match : matches) {
-            icon = match.loadIcon(requireActivity().getPackageManager());
-            label = match.loadLabel(requireActivity().getPackageManager());
-            sendButtonData = new SendButtonData(icon, label,
-                                                match.activityInfo.packageName,
-                                                match.activityInfo.name);
-
-            sendButtonDataList.add(sendButtonData);
-        }
-        return sendButtonDataList;
-    }
-}

+ 138 - 0
app/src/main/java/com/owncloud/android/ui/dialog/SendFilesDialog.kt

@@ -0,0 +1,138 @@
+package com.owncloud.android.ui.dialog
+
+import android.content.ComponentName
+import android.content.Intent
+import android.content.pm.ResolveInfo
+import android.graphics.drawable.Drawable
+import android.os.Build
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Toast
+import androidx.recyclerview.widget.GridLayoutManager
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+import com.nextcloud.android.common.ui.theme.utils.ColorRole
+import com.nextcloud.client.di.Injectable
+import com.nextcloud.client.utils.IntentUtil.createSendIntent
+import com.owncloud.android.R
+import com.owncloud.android.databinding.SendFilesFragmentBinding
+import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.ui.adapter.SendButtonAdapter
+import com.owncloud.android.ui.components.SendButtonData
+import com.owncloud.android.utils.theme.ViewThemeUtils
+import javax.inject.Inject
+
+/*
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2020 Tobias Kaminsky
+ * Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
+ */
+class SendFilesDialog : BottomSheetDialogFragment(R.layout.send_files_fragment), Injectable {
+
+    private var files: Array<OCFile>? = null
+    private lateinit var binding: SendFilesFragmentBinding
+
+    @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
+
+        files = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            requireArguments().getParcelableArray(KEY_OCFILES, OCFile::class.java)
+        } else {
+            @Suppress("DEPRECATION")
+            requireArguments().getParcelableArray(KEY_OCFILES) as Array<OCFile>?
+        }
+    }
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View {
+        binding = SendFilesFragmentBinding.inflate(inflater, container, false)
+
+        setupSendButtonRecyclerView()
+        viewThemeUtils?.platform?.colorViewBackground(binding.bottomSheet, ColorRole.SURFACE)
+
+        return binding.root
+    }
+
+    private fun setupSendButtonRecyclerView() {
+        val sendIntent = createSendIntent(requireContext(), files!!)
+        val matches = requireActivity().packageManager.queryIntentActivities(sendIntent, 0)
+
+        if (matches.isEmpty()) {
+            Toast.makeText(context, R.string.no_send_app, Toast.LENGTH_SHORT).show()
+            dismiss()
+            return
+        }
+
+        val sendButtonDataList = setupSendButtonData(matches)
+        val clickListener = setupSendButtonClickListener(sendIntent)
+
+        @Suppress("MagicNumber")
+        binding.sendButtonRecyclerView.layoutManager = GridLayoutManager(requireActivity(), 4)
+        binding.sendButtonRecyclerView.adapter = SendButtonAdapter(sendButtonDataList, clickListener)
+    }
+
+    private fun setupSendButtonClickListener(sendIntent: Intent): SendButtonAdapter.ClickListener {
+        return SendButtonAdapter.ClickListener { sendButtonDataData: SendButtonData ->
+            val packageName = sendButtonDataData.packageName
+            val activityName = sendButtonDataData.activityName
+            sendIntent.component = ComponentName(packageName, activityName)
+            requireActivity().startActivity(Intent.createChooser(sendIntent, getString(R.string.send)))
+            dismiss()
+        }
+    }
+
+    private fun setupSendButtonData(matches: List<ResolveInfo>): List<SendButtonData> {
+        var icon: Drawable
+        var sendButtonData: SendButtonData
+        var label: CharSequence
+        val sendButtonDataList: MutableList<SendButtonData> = ArrayList(matches.size)
+        for (match in matches) {
+            icon = match.loadIcon(requireActivity().packageManager)
+            label = match.loadLabel(requireActivity().packageManager)
+            sendButtonData = SendButtonData(
+                icon, label,
+                match.activityInfo.packageName,
+                match.activityInfo.name
+            )
+            sendButtonDataList.add(sendButtonData)
+        }
+        return sendButtonDataList
+    }
+
+    companion object {
+        private const val KEY_OCFILES = "KEY_OCFILES"
+
+        fun newInstance(files: Set<OCFile>): SendFilesDialog {
+            val dialogFragment = SendFilesDialog()
+            val args = Bundle()
+            args.putParcelableArray(KEY_OCFILES, files.toTypedArray())
+            dialogFragment.arguments = args
+            return dialogFragment
+        }
+    }
+}

+ 0 - 286
app/src/main/java/com/owncloud/android/ui/dialog/SendShareDialog.java

@@ -1,286 +0,0 @@
-package com.owncloud.android.ui.dialog;
-
-import android.content.ComponentName;
-import android.content.Intent;
-import android.content.pm.ResolveInfo;
-import android.graphics.drawable.Drawable;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import com.google.android.material.bottomsheet.BottomSheetBehavior;
-import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
-import com.google.android.material.snackbar.Snackbar;
-import com.nextcloud.client.di.Injectable;
-import com.nextcloud.client.utils.IntentUtil;
-import com.owncloud.android.R;
-import com.owncloud.android.datamodel.OCFile;
-import com.owncloud.android.lib.common.utils.Log_OC;
-import com.owncloud.android.lib.resources.status.OCCapability;
-import com.owncloud.android.ui.activity.FileActivity;
-import com.owncloud.android.ui.activity.FileDisplayActivity;
-import com.owncloud.android.ui.adapter.SendButtonAdapter;
-import com.owncloud.android.ui.components.SendButtonData;
-import com.owncloud.android.ui.helpers.FileOperationsHelper;
-import com.owncloud.android.utils.MimeTypeUtil;
-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.annotation.Nullable;
-import androidx.recyclerview.widget.GridLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-
-/*
- * Nextcloud Android client application
- *
- * @author Tobias Kaminsky
- * @author Andy Scherzinger
- * Copyright (C) 2017 Tobias Kaminsky
- * Copyright (C) 2017 Nextcloud 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 Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-public class SendShareDialog extends BottomSheetDialogFragment implements Injectable {
-
-    private static final String KEY_OCFILE = "KEY_OCFILE";
-    private static final String KEY_SHARING_PUBLIC_PASSWORD_ENFORCED = "KEY_SHARING_PUBLIC_PASSWORD_ENFORCED";
-    private static final String KEY_SHARING_PUBLIC_ASK_FOR_PASSWORD = "KEY_SHARING_PUBLIC_ASK_FOR_PASSWORD";
-    private static final String KEY_HIDE_NCSHARING_OPTIONS = "KEY_HIDE_NCSHARING_OPTIONS";
-    private static final String TAG = SendShareDialog.class.getSimpleName();
-    public static final String PACKAGE_NAME = "PACKAGE_NAME";
-    public static final String ACTIVITY_NAME = "ACTIVITY_NAME";
-
-    private View view;
-    private OCFile file;
-    private boolean hideNcSharingOptions;
-    private boolean sharingPublicPasswordEnforced;
-    private boolean sharingPublicAskForPassword;
-    private FileOperationsHelper fileOperationsHelper;
-    @Inject ViewThemeUtils viewThemeUtils;
-
-    public static SendShareDialog newInstance(OCFile file, boolean hideNcSharingOptions, OCCapability capability) {
-
-        SendShareDialog dialogFragment = new SendShareDialog();
-
-        Bundle args = new Bundle();
-        args.putParcelable(KEY_OCFILE, file);
-        args.putBoolean(KEY_HIDE_NCSHARING_OPTIONS, hideNcSharingOptions);
-        args.putBoolean(KEY_SHARING_PUBLIC_PASSWORD_ENFORCED,
-                        capability.getFilesSharingPublicPasswordEnforced().isTrue());
-        args.putBoolean(KEY_SHARING_PUBLIC_ASK_FOR_PASSWORD,
-                        capability.getFilesSharingPublicAskForOptionalPassword().isTrue());
-        dialogFragment.setArguments(args);
-
-        return dialogFragment;
-    }
-
-    @Override
-    public void onCreate(@Nullable Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        // keep the state of the fragment on configuration changes
-        setRetainInstance(true);
-
-        file = getArguments().getParcelable(KEY_OCFILE);
-        hideNcSharingOptions = getArguments().getBoolean(KEY_HIDE_NCSHARING_OPTIONS, false);
-        sharingPublicPasswordEnforced = getArguments().getBoolean(KEY_SHARING_PUBLIC_PASSWORD_ENFORCED, false);
-        sharingPublicAskForPassword = getArguments().getBoolean(KEY_SHARING_PUBLIC_ASK_FOR_PASSWORD);
-    }
-
-    @Nullable
-    @Override
-    public View onCreateView(@NonNull LayoutInflater inflater,
-                             @Nullable ViewGroup container,
-                             @Nullable Bundle savedInstanceState) {
-
-        view = inflater.inflate(R.layout.send_share_fragment, container, false);
-
-        LinearLayout sendShareButtons = view.findViewById(R.id.send_share_buttons);
-        View divider = view.findViewById(R.id.divider);
-
-        // Share with people
-        TextView sharePeopleText = view.findViewById(R.id.share_people_button);
-        sharePeopleText.setOnClickListener(v -> shareFile(file));
-
-        ImageView sharePeopleImageView = view.findViewById(R.id.share_people_icon);
-        themeShareButtonImage(sharePeopleImageView);
-        sharePeopleImageView.setOnClickListener(v -> shareFile(file));
-
-        // Share via link button
-        TextView shareLinkText = view.findViewById(R.id.share_link_button);
-        shareLinkText.setOnClickListener(v -> shareByLink());
-
-        ImageView shareLinkImageView = view.findViewById(R.id.share_link_icon);
-        themeShareButtonImage(shareLinkImageView);
-        shareLinkImageView.setOnClickListener(v -> shareByLink());
-
-        if (hideNcSharingOptions) {
-            sendShareButtons.setVisibility(View.GONE);
-            divider.setVisibility(View.GONE);
-        } else if (file.isSharedWithMe() && !file.canReshare()) {
-            showResharingNotAllowedSnackbar();
-
-            if (file.isFolder()) {
-                shareLinkText.setVisibility(View.GONE);
-                shareLinkImageView.setVisibility(View.GONE);
-                sharePeopleText.setVisibility(View.GONE);
-                sharePeopleImageView.setVisibility(View.GONE);
-                getDialog().hide();
-            } else {
-                shareLinkText.setEnabled(false);
-                shareLinkText.setAlpha(0.3f);
-                shareLinkImageView.setEnabled(false);
-                shareLinkImageView.setAlpha(0.3f);
-                sharePeopleText.setEnabled(false);
-                sharePeopleText.setAlpha(0.3f);
-                sharePeopleImageView.setEnabled(false);
-                sharePeopleImageView.setAlpha(0.3f);
-            }
-        }
-
-        // populate send apps
-        Intent sendIntent = IntentUtil.createSendIntent(requireContext(), file);
-
-        List<SendButtonData> sendButtonDataList = setupSendButtonData(sendIntent);
-
-        if ("off".equalsIgnoreCase(requireContext().getString(R.string.send_files_to_other_apps))) {
-            sharePeopleText.setVisibility(View.GONE);
-        }
-
-        SendButtonAdapter.ClickListener clickListener = setupSendButtonClickListener(sendIntent);
-
-        RecyclerView sendButtonsView = view.findViewById(R.id.send_button_recycler_view);
-        sendButtonsView.setLayoutManager(new GridLayoutManager(getActivity(), 4));
-        sendButtonsView.setAdapter(new SendButtonAdapter(sendButtonDataList, clickListener));
-
-        return view;
-    }
-
-    @Override
-    public void onStart() {
-        super.onStart();
-        BottomSheetBehavior.from((View) requireView().getParent()).setState(BottomSheetBehavior.STATE_EXPANDED);
-    }
-
-    private void shareByLink() {
-        if (file.isSharedViaLink()) {
-            ((FileActivity) requireActivity()).getFileOperationsHelper().getFileWithLink(file, viewThemeUtils);
-        } else if (sharingPublicPasswordEnforced || sharingPublicAskForPassword) {
-            // password enforced by server, request to the user before trying to create
-            requestPasswordForShareViaLink();
-        } else {
-            // create without password if not enforced by server or we don't know if enforced;
-            ((FileActivity) requireActivity()).getFileOperationsHelper().shareFileViaPublicShare(file, null);
-        }
-
-        this.dismiss();
-    }
-
-    private void requestPasswordForShareViaLink() {
-        SharePasswordDialogFragment dialog = SharePasswordDialogFragment.newInstance(file,
-                                                                                     true,
-                                                                                     sharingPublicAskForPassword);
-        dialog.show(getFragmentManager(), SharePasswordDialogFragment.PASSWORD_FRAGMENT);
-    }
-
-    private void themeShareButtonImage(ImageView shareImageView) {
-        viewThemeUtils.files.themeAvatarButton(shareImageView);
-    }
-
-    private void showResharingNotAllowedSnackbar() {
-        Snackbar snackbar = Snackbar.make(view, R.string.resharing_is_not_allowed, Snackbar.LENGTH_LONG);
-        snackbar.addCallback(new Snackbar.Callback() {
-            @Override
-            public void onDismissed(Snackbar transientBottomBar, int event) {
-                super.onDismissed(transientBottomBar, event);
-
-                if (file.isFolder()) {
-                    dismiss();
-                }
-            }
-        });
-
-        snackbar.show();
-    }
-
-    @NonNull
-    private SendButtonAdapter.ClickListener setupSendButtonClickListener(Intent sendIntent) {
-        return sendButtonDataData -> {
-            String packageName = sendButtonDataData.getPackageName();
-            String activityName = sendButtonDataData.getActivityName();
-
-            if (MimeTypeUtil.isImage(file) && !file.isDown()) {
-                fileOperationsHelper.sendCachedImage(file, packageName, activityName);
-            } else {
-                // Obtain the file
-                if (file.isDown()) {
-                    sendIntent.setComponent(new ComponentName(packageName, activityName));
-                    requireActivity().startActivity(Intent.createChooser(sendIntent, getString(R.string.send)));
-                } else {  // Download the file
-                    Log_OC.d(TAG, file.getRemotePath() + ": File must be downloaded");
-                    ((SendShareDialog.SendShareDialogDownloader) requireActivity())
-                        .downloadFile(file, packageName, activityName);
-                }
-            }
-
-            dismiss();
-        };
-    }
-
-    @NonNull
-    private List<SendButtonData> setupSendButtonData(Intent sendIntent) {
-        Drawable icon;
-        SendButtonData sendButtonData;
-        CharSequence label;
-        List<ResolveInfo> matches = requireActivity().getPackageManager().queryIntentActivities(sendIntent, 0);
-        List<SendButtonData> sendButtonDataList = new ArrayList<>(matches.size());
-        for (ResolveInfo match : matches) {
-            icon = match.loadIcon(requireActivity().getPackageManager());
-            label = match.loadLabel(requireActivity().getPackageManager());
-            sendButtonData = new SendButtonData(icon, label,
-                    match.activityInfo.packageName,
-                    match.activityInfo.name);
-
-            sendButtonDataList.add(sendButtonData);
-        }
-        return sendButtonDataList;
-    }
-
-    private void shareFile(OCFile file) {
-        dismiss();
-
-        if (getActivity() instanceof FileDisplayActivity) {
-            ((FileDisplayActivity) getActivity()).showDetails(file, 1);
-        } else {
-            fileOperationsHelper.showShareFile(file);
-        }
-    }
-
-    public void setFileOperationsHelper(FileOperationsHelper fileOperationsHelper) {
-        this.fileOperationsHelper = fileOperationsHelper;
-    }
-
-    public interface SendShareDialogDownloader {
-        void downloadFile(OCFile file, String packageName, String activityName);
-    }
-}

+ 274 - 0
app/src/main/java/com/owncloud/android/ui/dialog/SendShareDialog.kt

@@ -0,0 +1,274 @@
+package com.owncloud.android.ui.dialog
+
+import android.content.ComponentName
+import android.content.Intent
+import android.graphics.drawable.Drawable
+import android.os.Build
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.GridLayoutManager
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.bottomsheet.BottomSheetDialog
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+import com.google.android.material.snackbar.Snackbar
+import com.nextcloud.android.common.ui.theme.utils.ColorRole
+import com.nextcloud.client.di.Injectable
+import com.nextcloud.client.utils.IntentUtil.createSendIntent
+import com.owncloud.android.R
+import com.owncloud.android.databinding.SendShareFragmentBinding
+import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.lib.common.utils.Log_OC
+import com.owncloud.android.lib.resources.status.OCCapability
+import com.owncloud.android.ui.activity.FileActivity
+import com.owncloud.android.ui.activity.FileDisplayActivity
+import com.owncloud.android.ui.adapter.SendButtonAdapter
+import com.owncloud.android.ui.components.SendButtonData
+import com.owncloud.android.ui.helpers.FileOperationsHelper
+import com.owncloud.android.utils.MimeTypeUtil
+import com.owncloud.android.utils.theme.ViewThemeUtils
+import javax.inject.Inject
+
+/*
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * @author Andy Scherzinger
+ * Copyright (C) 2017 Tobias Kaminsky
+ * Copyright (C) 2017 Nextcloud 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 Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+class SendShareDialog : BottomSheetDialogFragment(R.layout.send_share_fragment), Injectable {
+
+    private lateinit var binding: SendShareFragmentBinding
+
+    private var file: OCFile? = null
+    private var hideNcSharingOptions = false
+    private var sharingPublicPasswordEnforced = false
+    private var sharingPublicAskForPassword = false
+    private var fileOperationsHelper: FileOperationsHelper? = 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
+        val arguments = requireArguments()
+
+        file = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            arguments.getParcelable(KEY_OCFILE, OCFile::class.java)
+        } else {
+            @Suppress("DEPRECATION")
+            arguments.getParcelable(KEY_OCFILE)
+        }
+
+        hideNcSharingOptions = arguments.getBoolean(KEY_HIDE_NCSHARING_OPTIONS, false)
+        sharingPublicPasswordEnforced = arguments.getBoolean(KEY_SHARING_PUBLIC_PASSWORD_ENFORCED, false)
+        sharingPublicAskForPassword = arguments.getBoolean(KEY_SHARING_PUBLIC_ASK_FOR_PASSWORD)
+    }
+
+    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+        binding = SendShareFragmentBinding.inflate(inflater, container, false)
+
+        binding.btnShare.setOnClickListener { shareFile(file) }
+        binding.btnLink.setOnClickListener { shareByLink() }
+
+        applyTintColor()
+        setupBottomSheetBehaviour()
+        checkButtonVisibilities()
+        setupSendButtonRecyclerView()
+
+        return binding.root
+    }
+
+    @Suppress("MagicNumber")
+    private fun setupSendButtonRecyclerView() {
+        val sendIntent = createSendIntent(requireContext(), file!!)
+        val sendButtonDataList = setupSendButtonData(sendIntent)
+        val clickListener = setupSendButtonClickListener(sendIntent)
+
+        binding.sendButtonRecyclerView.layoutManager = GridLayoutManager(activity, 4)
+        binding.sendButtonRecyclerView.adapter = SendButtonAdapter(sendButtonDataList, clickListener)
+    }
+
+    private fun setupBottomSheetBehaviour() {
+        val bottomSheetDialog = dialog as BottomSheetDialog
+        bottomSheetDialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED
+        bottomSheetDialog.behavior.skipCollapsed = true
+    }
+
+    private fun applyTintColor() {
+        viewThemeUtils?.material?.colorMaterialButtonPrimaryFilled(binding.btnLink)
+        viewThemeUtils?.material?.colorMaterialButtonPrimaryFilled(binding.btnShare)
+        viewThemeUtils?.platform?.colorViewBackground(binding.bottomSheet, ColorRole.SURFACE)
+    }
+
+    @Suppress("MagicNumber")
+    private fun checkButtonVisibilities() {
+        if (hideNcSharingOptions) {
+            binding.sendShareButtons.visibility = View.GONE
+            binding.divider.visibility = View.GONE
+        } else if (file?.isSharedWithMe == true && file?.canReshare() == false) {
+            showSharingNotAllowedMessage()
+
+            if (file?.isFolder == true) {
+                binding.btnShare.visibility = View.GONE
+                binding.btnLink.visibility = View.GONE
+                dialog!!.hide()
+            } else {
+                binding.btnLink.isEnabled = false
+                binding.btnLink.alpha = 0.3f
+
+                binding.btnShare.isEnabled = false
+                binding.btnShare.alpha = 0.3f
+            }
+        }
+    }
+
+    private fun shareByLink() {
+        val fileOperationsHelper = (requireActivity() as FileActivity).fileOperationsHelper
+
+        if (file?.isSharedViaLink == true) {
+            fileOperationsHelper.getFileWithLink(file!!, viewThemeUtils)
+        } else if (sharingPublicPasswordEnforced || sharingPublicAskForPassword) {
+            // password enforced by server, request to the user before trying to create
+            requestPasswordForShareViaLink()
+        } else {
+            // create without password if not enforced by server or we don't know if enforced;
+            fileOperationsHelper.shareFileViaPublicShare(file, null)
+        }
+
+        dismiss()
+    }
+
+    private fun requestPasswordForShareViaLink() {
+        val dialog = SharePasswordDialogFragment.newInstance(
+            file,
+            true,
+            sharingPublicAskForPassword
+        )
+
+        dialog.show(parentFragmentManager, SharePasswordDialogFragment.PASSWORD_FRAGMENT)
+    }
+
+    private fun showSharingNotAllowedMessage() {
+        val message = Snackbar.make(binding.root, R.string.resharing_is_not_allowed, Snackbar.LENGTH_LONG)
+
+        message.addCallback(object : Snackbar.Callback() {
+            override fun onDismissed(transientBottomBar: Snackbar, event: Int) {
+                super.onDismissed(transientBottomBar, event)
+                if (file!!.isFolder) {
+                    dismiss()
+                }
+            }
+        })
+
+        message.show()
+    }
+
+    private fun setupSendButtonClickListener(sendIntent: Intent): SendButtonAdapter.ClickListener {
+        return SendButtonAdapter.ClickListener { sendButtonDataData: SendButtonData ->
+            val packageName = sendButtonDataData.packageName
+            val activityName = sendButtonDataData.activityName
+
+            if (MimeTypeUtil.isImage(file) && !file!!.isDown) {
+                fileOperationsHelper?.sendCachedImage(file, packageName, activityName)
+            } else {
+                // Obtain the file
+                if (file!!.isDown) {
+                    sendIntent.component = ComponentName(packageName, activityName)
+                    requireActivity().startActivity(Intent.createChooser(sendIntent, getString(R.string.send)))
+                } else { // Download the file
+                    Log_OC.d(TAG, file!!.remotePath + ": File must be downloaded")
+                    (requireActivity() as SendShareDialogDownloader)
+                        .downloadFile(file, packageName, activityName)
+                }
+            }
+
+            dismiss()
+        }
+    }
+
+    private fun setupSendButtonData(sendIntent: Intent): List<SendButtonData> {
+        var icon: Drawable
+        var sendButtonData: SendButtonData
+        var label: CharSequence
+        val matches = requireActivity().packageManager.queryIntentActivities(sendIntent, 0)
+        val sendButtonDataList: MutableList<SendButtonData> = ArrayList(matches.size)
+        for (match in matches) {
+            icon = match.loadIcon(requireActivity().packageManager)
+            label = match.loadLabel(requireActivity().packageManager)
+            sendButtonData = SendButtonData(
+                icon, label,
+                match.activityInfo.packageName,
+                match.activityInfo.name
+            )
+            sendButtonDataList.add(sendButtonData)
+        }
+        return sendButtonDataList
+    }
+
+    private fun shareFile(file: OCFile?) {
+        dismiss()
+
+        if (activity is FileDisplayActivity) {
+            (activity as FileDisplayActivity?)?.showDetails(file, 1)
+        } else {
+            fileOperationsHelper?.showShareFile(file)
+        }
+    }
+
+    fun setFileOperationsHelper(fileOperationsHelper: FileOperationsHelper?) {
+        this.fileOperationsHelper = fileOperationsHelper
+    }
+
+    interface SendShareDialogDownloader {
+        fun downloadFile(file: OCFile?, packageName: String?, activityName: String?)
+    }
+
+    companion object {
+        private const val KEY_OCFILE = "KEY_OCFILE"
+        private const val KEY_SHARING_PUBLIC_PASSWORD_ENFORCED = "KEY_SHARING_PUBLIC_PASSWORD_ENFORCED"
+        private const val KEY_SHARING_PUBLIC_ASK_FOR_PASSWORD = "KEY_SHARING_PUBLIC_ASK_FOR_PASSWORD"
+        private const val KEY_HIDE_NCSHARING_OPTIONS = "KEY_HIDE_NCSHARING_OPTIONS"
+        private val TAG = SendShareDialog::class.java.simpleName
+        const val PACKAGE_NAME = "PACKAGE_NAME"
+        const val ACTIVITY_NAME = "ACTIVITY_NAME"
+
+        @JvmStatic
+        fun newInstance(file: OCFile?, hideNcSharingOptions: Boolean, capability: OCCapability): SendShareDialog {
+            val dialogFragment = SendShareDialog()
+            val args = Bundle()
+            args.putParcelable(KEY_OCFILE, file)
+            args.putBoolean(KEY_HIDE_NCSHARING_OPTIONS, hideNcSharingOptions)
+            args.putBoolean(
+                KEY_SHARING_PUBLIC_PASSWORD_ENFORCED,
+                capability.filesSharingPublicPasswordEnforced.isTrue
+            )
+            args.putBoolean(
+                KEY_SHARING_PUBLIC_ASK_FOR_PASSWORD,
+                capability.filesSharingPublicAskForOptionalPassword.isTrue
+            )
+            dialogFragment.arguments = args
+            return dialogFragment
+        }
+    }
+}

+ 0 - 551
app/src/main/java/com/owncloud/android/ui/dialog/SetupEncryptionDialogFragment.java

@@ -1,551 +0,0 @@
-/*
- * Nextcloud Android client application
- *
- * @author Tobias Kaminsky
- * @author TSI-mc
- * Copyright (C) 2017 Tobias Kaminsky
- * Copyright (C) 2017 Nextcloud GmbH.
- * Copyright (C) 2023 TSI-mc
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-package com.owncloud.android.ui.dialog;
-
-import android.accounts.AccountManager;
-import android.app.Dialog;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.Intent;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.Button;
-
-import com.google.android.material.dialog.MaterialAlertDialogBuilder;
-import com.nextcloud.client.account.User;
-import com.nextcloud.client.di.Injectable;
-import com.owncloud.android.R;
-import com.owncloud.android.databinding.SetupEncryptionDialogBinding;
-import com.owncloud.android.datamodel.ArbitraryDataProvider;
-import com.owncloud.android.datamodel.ArbitraryDataProviderImpl;
-import com.owncloud.android.lib.common.accounts.AccountUtils;
-import com.owncloud.android.lib.common.operations.RemoteOperationResult;
-import com.owncloud.android.lib.common.utils.Log_OC;
-import com.owncloud.android.lib.resources.users.DeletePublicKeyOperation;
-import com.owncloud.android.lib.resources.users.GetPrivateKeyOperation;
-import com.owncloud.android.lib.resources.users.GetPublicKeyOperation;
-import com.owncloud.android.lib.resources.users.SendCSROperation;
-import com.owncloud.android.lib.resources.users.StorePrivateKeyOperation;
-import com.owncloud.android.utils.CsrHelper;
-import com.owncloud.android.utils.EncryptionUtils;
-import com.owncloud.android.utils.theme.ViewThemeUtils;
-
-import java.io.IOException;
-import java.lang.ref.WeakReference;
-import java.security.KeyPair;
-import java.security.PrivateKey;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Locale;
-
-import javax.inject.Inject;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.VisibleForTesting;
-import androidx.appcompat.app.AlertDialog;
-import androidx.fragment.app.DialogFragment;
-import androidx.fragment.app.Fragment;
-
-import static com.owncloud.android.utils.EncryptionUtils.MNEMONIC;
-import static com.owncloud.android.utils.EncryptionUtils.decodeStringToBase64Bytes;
-import static com.owncloud.android.utils.EncryptionUtils.decryptStringAsymmetric;
-import static com.owncloud.android.utils.EncryptionUtils.encodeBytesToBase64String;
-import static com.owncloud.android.utils.EncryptionUtils.generateKey;
-
-/*
- *  Dialog to setup encryption
- */
-public class SetupEncryptionDialogFragment extends DialogFragment implements Injectable {
-
-    public static final String SUCCESS = "SUCCESS";
-    public static final int SETUP_ENCRYPTION_RESULT_CODE = 101;
-    public static final int SETUP_ENCRYPTION_REQUEST_CODE = 100;
-    public static final String SETUP_ENCRYPTION_DIALOG_TAG = "SETUP_ENCRYPTION_DIALOG_TAG";
-    public static final String ARG_POSITION = "ARG_POSITION";
-
-    public static final String RESULT_REQUEST_KEY = "RESULT_REQUEST";
-    public static final String RESULT_KEY_CANCELLED = "IS_CANCELLED";
-
-    private static final String ARG_USER = "ARG_USER";
-    private static final String TAG = SetupEncryptionDialogFragment.class.getSimpleName();
-
-    private static final String KEY_CREATED = "KEY_CREATED";
-    private static final String KEY_EXISTING_USED = "KEY_EXISTING_USED";
-    private static final String KEY_FAILED = "KEY_FAILED";
-    private static final String KEY_GENERATE = "KEY_GENERATE";
-
-    @Inject ViewThemeUtils viewThemeUtils;
-
-    private User user;
-    private ArbitraryDataProvider arbitraryDataProvider;
-    private Button positiveButton;
-    private Button neutralButton;
-    private DownloadKeysAsyncTask task;
-    private String keyResult;
-    private ArrayList<String> keyWords;
-    private SetupEncryptionDialogBinding binding;
-
-    /**
-     * Public factory method to create new SetupEncryptionDialogFragment instance
-     *
-     * @return Dialog ready to show.
-     */
-    public static SetupEncryptionDialogFragment newInstance(User user, int position) {
-        SetupEncryptionDialogFragment fragment = new SetupEncryptionDialogFragment();
-        Bundle args = new Bundle();
-        args.putParcelable(ARG_USER, user);
-        args.putInt(ARG_POSITION, position);
-        fragment.setArguments(args);
-        return fragment;
-    }
-
-    @Override
-    public void onStart() {
-        super.onStart();
-
-        AlertDialog alertDialog = (AlertDialog) getDialog();
-
-        if (alertDialog != null) {
-            positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE);
-            neutralButton = alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL);
-            viewThemeUtils.platform.colorTextButtons(positiveButton, neutralButton);
-        }
-
-        task = new DownloadKeysAsyncTask(requireContext());
-        task.execute();
-    }
-
-    @NonNull
-    @Override
-    public Dialog onCreateDialog(Bundle savedInstanceState) {
-        if (getArguments() == null) {
-            throw new IllegalStateException("Arguments may not be null");
-        }
-        user = getArguments().getParcelable(ARG_USER);
-
-        if (savedInstanceState != null) {
-            keyWords = savedInstanceState.getStringArrayList(MNEMONIC);
-        }
-
-        arbitraryDataProvider = new ArbitraryDataProviderImpl(getContext());
-
-        // Inflate the layout for the dialog
-        LayoutInflater inflater = requireActivity().getLayoutInflater();
-        binding = SetupEncryptionDialogBinding.inflate(inflater, null, false);
-
-        // Setup layout
-        viewThemeUtils.material.colorTextInputLayout(binding.encryptionPasswordInputContainer);
-
-        return createDialog(binding.getRoot());
-    }
-
-    @NonNull
-    private Dialog createDialog(View v) {
-        MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(v.getContext());
-        builder.setView(v).setPositiveButton(R.string.common_ok, null)
-            .setNeutralButton(R.string.common_cancel, (dialog, which) -> {
-                dialog.cancel();
-            })
-            .setTitle(R.string.end_to_end_encryption_title);
-
-        viewThemeUtils.dialog.colorMaterialAlertDialogBackground(v.getContext(), builder);
-
-        Dialog dialog = builder.create();
-        dialog.setCanceledOnTouchOutside(false);
-
-        dialog.setOnShowListener(dialog1 -> {
-
-            Button button = ((AlertDialog) dialog1).getButton(AlertDialog.BUTTON_POSITIVE);
-            button.setOnClickListener(view -> {
-                switch (keyResult) {
-                    case KEY_CREATED:
-                        Log_OC.d(TAG, "New keys generated and stored.");
-
-                        dialog1.dismiss();
-
-                        notifyResult();
-                        break;
-
-                    case KEY_EXISTING_USED:
-                        Log_OC.d(TAG, "Decrypt private key");
-
-                        binding.encryptionStatus.setText(R.string.end_to_end_encryption_decrypting);
-
-                        try {
-                            String privateKey = task.get();
-                            String mnemonicUnchanged = binding.encryptionPasswordInput.getText().toString();
-                            String mnemonic = binding.encryptionPasswordInput.getText().toString().replaceAll("\\s", "")
-                                .toLowerCase(Locale.ROOT);
-                            String decryptedPrivateKey = EncryptionUtils.decryptPrivateKey(privateKey,
-                                                                                           mnemonic);
-
-                            arbitraryDataProvider.storeOrUpdateKeyValue(user.getAccountName(),
-                                                                        EncryptionUtils.PRIVATE_KEY, decryptedPrivateKey);
-
-                            dialog1.dismiss();
-                            Log_OC.d(TAG, "Private key successfully decrypted and stored");
-
-                            arbitraryDataProvider.storeOrUpdateKeyValue(user.getAccountName(),
-                                                                        EncryptionUtils.MNEMONIC,
-                                                                        mnemonicUnchanged);
-
-                            // check if private key and public key match
-                            String publicKey = arbitraryDataProvider.getValue(user.getAccountName(),
-                                                                              EncryptionUtils.PUBLIC_KEY);
-
-                            byte[] key1 = generateKey();
-                            String base64encodedKey = encodeBytesToBase64String(key1);
-
-                            String encryptedString = EncryptionUtils.encryptStringAsymmetric(base64encodedKey,
-                                                                                             publicKey);
-                            String decryptedString = decryptStringAsymmetric(encryptedString,
-                                                                             decryptedPrivateKey);
-
-                            byte[] key2 = decodeStringToBase64Bytes(decryptedString);
-
-                            if (!Arrays.equals(key1, key2)) {
-                                throw new Exception("Keys do not match");
-                            }
-
-                            notifyResult();
-
-                        } catch (Exception e) {
-                            binding.encryptionStatus.setText(R.string.end_to_end_encryption_wrong_password);
-                            Log_OC.d(TAG, "Error while decrypting private key: " + e.getMessage());
-                        }
-                        break;
-
-                    case KEY_GENERATE:
-                        binding.encryptionPassphrase.setVisibility(View.GONE);
-                        positiveButton.setVisibility(View.GONE);
-                        neutralButton.setVisibility(View.GONE);
-                        getDialog().setTitle(R.string.end_to_end_encryption_storing_keys);
-
-                        GenerateNewKeysAsyncTask newKeysTask = new GenerateNewKeysAsyncTask(requireContext());
-                        newKeysTask.execute();
-                        break;
-
-                    default:
-                        dialog1.dismiss();
-                        break;
-                }
-            });
-        });
-        return dialog;
-    }
-
-    private void notifyResult() {
-        final Fragment targetFragment = getTargetFragment();
-        if (targetFragment != null) {
-            targetFragment.onActivityResult(getTargetRequestCode(),
-                                            SETUP_ENCRYPTION_RESULT_CODE, getResultIntent());
-        }
-        getParentFragmentManager().setFragmentResult(RESULT_REQUEST_KEY, getResultBundle());
-    }
-
-    @NonNull
-    private Intent getResultIntent() {
-        Intent intentCreated = new Intent();
-        intentCreated.putExtra(SUCCESS, true);
-        intentCreated.putExtra(ARG_POSITION, getArguments().getInt(ARG_POSITION));
-        return intentCreated;
-    }
-
-    @NonNull
-    private Bundle getResultBundle() {
-        final Bundle bundle = new Bundle();
-        bundle.putBoolean(SUCCESS, true);
-        bundle.putInt(ARG_POSITION, getArguments().getInt(ARG_POSITION));
-        return bundle;
-    }
-
-
-    @Override
-    public void onCancel(@NonNull DialogInterface dialog) {
-        super.onCancel(dialog);
-        final Bundle bundle = new Bundle();
-        bundle.putBoolean(RESULT_KEY_CANCELLED, true);
-        getParentFragmentManager().setFragmentResult(RESULT_REQUEST_KEY, bundle);
-    }
-
-    @Override
-    public void onSaveInstanceState(@NonNull Bundle outState) {
-        outState.putStringArrayList(MNEMONIC, keyWords);
-        super.onSaveInstanceState(outState);
-    }
-
-    public class DownloadKeysAsyncTask extends AsyncTask<Void, Void, String> {
-        private final WeakReference<Context> mWeakContext;
-
-        public DownloadKeysAsyncTask(Context context) {
-            mWeakContext = new WeakReference<>(context);
-        }
-
-        @Override
-        protected void onPreExecute() {
-            super.onPreExecute();
-
-            binding.encryptionStatus.setText(R.string.end_to_end_encryption_retrieving_keys);
-            positiveButton.setVisibility(View.INVISIBLE);
-        }
-
-        @Override
-        protected String doInBackground(Void... voids) {
-            // fetch private/public key
-            // if available
-            //  - store public key
-            //  - decrypt private key, store unencrypted private key in database
-
-            Context context = mWeakContext.get();
-            GetPublicKeyOperation publicKeyOperation = new GetPublicKeyOperation();
-            if (user != null) {
-                RemoteOperationResult<String> publicKeyResult = publicKeyOperation.execute(user, context);
-
-                if (publicKeyResult.isSuccess()) {
-                    Log_OC.d(TAG, "public key successful downloaded for " + user.getAccountName());
-
-                    String publicKeyFromServer = publicKeyResult.getResultData();
-                    if (arbitraryDataProvider != null) {
-                        arbitraryDataProvider.storeOrUpdateKeyValue(user.getAccountName(),
-                                                                    EncryptionUtils.PUBLIC_KEY,
-                                                                    publicKeyFromServer);
-                    } else {
-                        return null;
-                    }
-                } else {
-                    return null;
-                }
-
-                RemoteOperationResult<com.owncloud.android.lib.ocs.responses.PrivateKey> privateKeyResult =
-                    new GetPrivateKeyOperation().execute(user, context);
-
-                if (privateKeyResult.isSuccess()) {
-                    Log_OC.d(TAG, "private key successful downloaded for " + user.getAccountName());
-
-                    keyResult = KEY_EXISTING_USED;
-                    return privateKeyResult.getResultData().getKey();
-                }
-            }
-            return null;
-        }
-
-        @Override
-        protected void onPostExecute(String privateKey) {
-            super.onPostExecute(privateKey);
-
-            Context context = mWeakContext.get();
-            if (context == null) {
-                Log_OC.e(TAG, "Context lost after fetching private keys.");
-                return;
-            }
-
-            if (privateKey == null) {
-                // first show info
-                try {
-                    if (keyWords == null || keyWords.isEmpty()) {
-                        keyWords = EncryptionUtils.getRandomWords(12, context);
-                    }
-                    showMnemonicInfo();
-                } catch (IOException e) {
-                    binding.encryptionStatus.setText(R.string.common_error);
-                }
-            } else if (!privateKey.isEmpty()) {
-                binding.encryptionStatus.setText(R.string.end_to_end_encryption_enter_password);
-                binding.encryptionPasswordInputContainer.setVisibility(View.VISIBLE);
-                positiveButton.setVisibility(View.VISIBLE);
-            } else {
-                Log_OC.e(TAG, "Got empty private key string");
-            }
-        }
-    }
-
-    public class GenerateNewKeysAsyncTask extends AsyncTask<Void, Void, String> {
-
-        private final WeakReference<Context> mWeakContext;
-
-        public GenerateNewKeysAsyncTask(Context context) {
-            mWeakContext = new WeakReference<>(context);
-        }
-
-        @Override
-        protected void onPreExecute() {
-            super.onPreExecute();
-
-            binding.encryptionStatus.setText(R.string.end_to_end_encryption_generating_keys);
-        }
-
-        @Override
-        protected String doInBackground(Void... voids) {
-            //  - create CSR, push to server, store returned public key in database
-            //  - encrypt private key, push key to server, store unencrypted private key in database
-
-            try {
-                Context context  = mWeakContext.get();
-
-                String publicKeyString;
-
-                // Create public/private key pair
-                KeyPair keyPair = EncryptionUtils.generateKeyPair();
-
-                // create CSR
-                AccountManager accountManager = AccountManager.get(context);
-                String userId = accountManager.getUserData(user.toPlatformAccount(), AccountUtils.Constants.KEY_USER_ID);
-                String urlEncoded = CsrHelper.generateCsrPemEncodedString(keyPair, userId);
-
-                SendCSROperation operation = new SendCSROperation(urlEncoded);
-                RemoteOperationResult result = operation.execute(user, context);
-
-                if (result.isSuccess()) {
-                    publicKeyString = (String) result.getData().get(0);
-
-                    if (!EncryptionUtils.isMatchingKeys(keyPair, publicKeyString)) {
-                        throw new RuntimeException("Wrong CSR returned");
-                    }
-
-                    Log_OC.d(TAG, "public key success");
-                } else {
-                    keyResult = KEY_FAILED;
-                    return "";
-                }
-
-                PrivateKey privateKey = keyPair.getPrivate();
-                String privateKeyString = EncryptionUtils.encodeBytesToBase64String(privateKey.getEncoded());
-                String privatePemKeyString = EncryptionUtils.privateKeyToPEM(privateKey);
-                String encryptedPrivateKey = EncryptionUtils.encryptPrivateKey(privatePemKeyString,
-                                                                               generateMnemonicString(false));
-
-                // upload encryptedPrivateKey
-                StorePrivateKeyOperation storePrivateKeyOperation = new StorePrivateKeyOperation(encryptedPrivateKey);
-                RemoteOperationResult storePrivateKeyResult = storePrivateKeyOperation.execute(user, context);
-
-                if (storePrivateKeyResult.isSuccess()) {
-                    Log_OC.d(TAG, "private key success");
-
-                    arbitraryDataProvider.storeOrUpdateKeyValue(user.getAccountName(),
-                                                                EncryptionUtils.PRIVATE_KEY,
-                                                                privateKeyString);
-                    arbitraryDataProvider.storeOrUpdateKeyValue(user.getAccountName(),
-                                                                EncryptionUtils.PUBLIC_KEY,
-                                                                publicKeyString);
-                    arbitraryDataProvider.storeOrUpdateKeyValue(user.getAccountName(),
-                                                                EncryptionUtils.MNEMONIC,
-                                                                generateMnemonicString(true));
-
-                    keyResult = KEY_CREATED;
-                    return (String) storePrivateKeyResult.getData().get(0);
-                } else {
-                    DeletePublicKeyOperation deletePublicKeyOperation = new DeletePublicKeyOperation();
-                    deletePublicKeyOperation.execute(user, context);
-                }
-            } catch (Exception e) {
-                Log_OC.e(TAG, e.getMessage());
-            }
-
-            keyResult = KEY_FAILED;
-            return "";
-        }
-
-        @Override
-        protected void onPostExecute(String s) {
-            super.onPostExecute(s);
-
-            Context context = mWeakContext.get();
-            if (context == null) {
-                Log_OC.e(TAG, "Context lost after generating new private keys.");
-                return;
-            }
-
-            if (s.isEmpty()) {
-                errorSavingKeys();
-            } else {
-                if (getDialog() == null) {
-                    Log_OC.e(TAG, "Dialog is null cannot proceed further.");
-                    return;
-                }
-
-                requireDialog().dismiss();
-                notifyResult();
-            }
-        }
-    }
-
-    private String generateMnemonicString(boolean withWhitespace) {
-        StringBuilder stringBuilder = new StringBuilder();
-
-        for (String string : keyWords) {
-            stringBuilder.append(string);
-            if (withWhitespace) {
-                stringBuilder.append(' ');
-            }
-        }
-
-        return stringBuilder.toString();
-    }
-
-    @VisibleForTesting
-    public void showMnemonicInfo() {
-        if (getDialog() == null) {
-            Log_OC.e(TAG, "Dialog is null cannot proceed further.");
-            return;
-        }
-        requireDialog().setTitle(R.string.end_to_end_encryption_passphrase_title);
-
-        binding.encryptionStatus.setText(R.string.end_to_end_encryption_keywords_description);
-        viewThemeUtils.material.colorTextInputLayout(binding.encryptionPasswordInputContainer);
-
-        binding.encryptionPassphrase.setText(generateMnemonicString(true));
-
-        binding.encryptionPassphrase.setVisibility(View.VISIBLE);
-        positiveButton.setText(R.string.end_to_end_encryption_confirm_button);
-        positiveButton.setVisibility(View.VISIBLE);
-
-        neutralButton.setVisibility(View.VISIBLE);
-        viewThemeUtils.platform.colorTextButtons(positiveButton, neutralButton);
-
-        keyResult = KEY_GENERATE;
-    }
-
-    @VisibleForTesting
-    public void errorSavingKeys() {
-        if (getDialog() == null) {
-            Log_OC.e(TAG, "Dialog is null cannot proceed further.");
-            return;
-        }
-
-        keyResult = KEY_FAILED;
-
-        requireDialog().setTitle(R.string.common_error);
-        binding.encryptionStatus.setText(R.string.end_to_end_encryption_unsuccessful);
-        binding.encryptionPassphrase.setVisibility(View.GONE);
-        positiveButton.setText(R.string.end_to_end_encryption_dialog_close);
-        positiveButton.setVisibility(View.VISIBLE);
-        viewThemeUtils.platform.colorTextButtons(positiveButton);
-    }
-
-    @VisibleForTesting
-    public void setMnemonic(ArrayList<String> keyWords) {
-        this.keyWords = keyWords;
-    }
-}

+ 563 - 0
app/src/main/java/com/owncloud/android/ui/dialog/SetupEncryptionDialogFragment.kt

@@ -0,0 +1,563 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * @author TSI-mc
+ * Copyright (C) 2017 Tobias Kaminsky
+ * Copyright (C) 2017 Nextcloud GmbH.
+ * Copyright (C) 2023 TSI-mc
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.owncloud.android.ui.dialog
+
+import android.accounts.AccountManager
+import android.annotation.SuppressLint
+import android.app.Dialog
+import android.content.Context
+import android.content.DialogInterface
+import android.content.Intent
+import android.os.AsyncTask
+import android.os.Build
+import android.os.Bundle
+import android.view.View
+import androidx.annotation.VisibleForTesting
+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.account.User
+import com.nextcloud.client.di.Injectable
+import com.owncloud.android.R
+import com.owncloud.android.databinding.SetupEncryptionDialogBinding
+import com.owncloud.android.datamodel.ArbitraryDataProvider
+import com.owncloud.android.datamodel.ArbitraryDataProviderImpl
+import com.owncloud.android.lib.common.accounts.AccountUtils
+import com.owncloud.android.lib.common.utils.Log_OC
+import com.owncloud.android.lib.resources.users.DeletePublicKeyOperation
+import com.owncloud.android.lib.resources.users.GetPrivateKeyOperation
+import com.owncloud.android.lib.resources.users.GetPublicKeyOperation
+import com.owncloud.android.lib.resources.users.SendCSROperation
+import com.owncloud.android.lib.resources.users.StorePrivateKeyOperation
+import com.owncloud.android.utils.CsrHelper
+import com.owncloud.android.utils.EncryptionUtils
+import com.owncloud.android.utils.theme.ViewThemeUtils
+import java.io.IOException
+import java.lang.ref.WeakReference
+import java.util.Arrays
+import javax.inject.Inject
+
+/*
+ *  Dialog to setup encryption
+ */
+class SetupEncryptionDialogFragment : DialogFragment(), Injectable {
+
+    @JvmField
+    @Inject
+    var viewThemeUtils: ViewThemeUtils? = null
+
+    private var user: User? = null
+    private var arbitraryDataProvider: ArbitraryDataProvider? = null
+    private var positiveButton: MaterialButton? = null
+    private var negativeButton: MaterialButton? = null
+    private var task: DownloadKeysAsyncTask? = null
+    private var keyResult: String? = null
+    private var keyWords: ArrayList<String>? = null
+
+    private lateinit var binding: SetupEncryptionDialogBinding
+
+    override fun onStart() {
+        super.onStart()
+
+        setupAlertDialog()
+        executeTask()
+    }
+
+    private fun setupAlertDialog() {
+        val alertDialog = dialog as AlertDialog?
+
+        if (alertDialog != null) {
+            positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE) as MaterialButton?
+            negativeButton = alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE) as MaterialButton?
+
+            if (positiveButton != null) {
+                viewThemeUtils?.material?.colorMaterialButtonPrimaryTonal(positiveButton!!)
+            }
+
+            if (negativeButton != null) {
+                viewThemeUtils?.material?.colorMaterialButtonPrimaryBorderless(negativeButton!!)
+            }
+        }
+    }
+
+    private fun executeTask() {
+        task = DownloadKeysAsyncTask(requireContext())
+        task?.execute()
+    }
+
+    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+        checkNotNull(arguments) { "Arguments may not be null" }
+
+        user = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            requireArguments().getParcelable(ARG_USER, User::class.java)
+        } else {
+            @Suppress("DEPRECATION")
+            requireArguments().getParcelable(ARG_USER)
+        }
+
+        if (savedInstanceState != null) {
+            keyWords = savedInstanceState.getStringArrayList(EncryptionUtils.MNEMONIC)
+        }
+
+        arbitraryDataProvider = ArbitraryDataProviderImpl(context)
+
+        // Inflate the layout for the dialog
+        val inflater = requireActivity().layoutInflater
+        binding = SetupEncryptionDialogBinding.inflate(inflater, null, false)
+
+        // Setup layout
+        viewThemeUtils?.material?.colorTextInputLayout(binding.encryptionPasswordInputContainer)
+
+        return createDialog(binding.root)
+    }
+
+    private fun createDialog(v: View): Dialog {
+        val builder = MaterialAlertDialogBuilder(v.context)
+
+        builder
+            .setView(v)
+            .setPositiveButton(R.string.common_ok, null)
+            .setNegativeButton(R.string.common_cancel) { dialog: DialogInterface, _: Int -> dialog.cancel() }
+            .setTitle(R.string.end_to_end_encryption_title)
+
+        viewThemeUtils?.dialog?.colorMaterialAlertDialogBackground(v.context, builder)
+
+        val dialog: Dialog = builder.create()
+        dialog.setCanceledOnTouchOutside(false)
+        dialog.setOnShowListener { dialog1: DialogInterface ->
+            val button = (dialog1 as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE)
+            button.setOnClickListener { positiveButtonOnClick(dialog) }
+        }
+
+        return dialog
+    }
+
+    private fun positiveButtonOnClick(dialog: DialogInterface) {
+        when (keyResult) {
+            KEY_CREATED -> {
+                Log_OC.d(TAG, "New keys generated and stored.")
+                dialog.dismiss()
+                notifyResult()
+            }
+            KEY_EXISTING_USED -> {
+                decryptPrivateKey(dialog)
+            }
+
+            KEY_GENERATE -> {
+                generateKey()
+            }
+            else -> dialog.dismiss()
+        }
+    }
+
+    @Suppress("TooGenericExceptionCaught", "TooGenericExceptionThrown")
+    private fun decryptPrivateKey(dialog: DialogInterface) {
+        Log_OC.d(TAG, "Decrypt private key")
+        binding.encryptionStatus.setText(R.string.end_to_end_encryption_decrypting)
+
+        try {
+            val privateKey = task?.get()
+            val mnemonicUnchanged = binding.encryptionPasswordInput.text.toString()
+            val mnemonic =
+                binding.encryptionPasswordInput.text.toString().replace("\\s".toRegex(), "")
+                    .lowercase()
+            val decryptedPrivateKey = EncryptionUtils.decryptPrivateKey(
+                privateKey,
+                mnemonic
+            )
+
+            val accountName = user?.accountName ?: return
+
+            arbitraryDataProvider?.storeOrUpdateKeyValue(
+                accountName,
+                EncryptionUtils.PRIVATE_KEY,
+                decryptedPrivateKey
+            )
+            dialog.dismiss()
+
+            Log_OC.d(TAG, "Private key successfully decrypted and stored")
+
+            arbitraryDataProvider?.storeOrUpdateKeyValue(
+                accountName,
+                EncryptionUtils.MNEMONIC,
+                mnemonicUnchanged
+            )
+
+            // check if private key and public key match
+            val publicKey = arbitraryDataProvider?.getValue(
+                accountName,
+                EncryptionUtils.PUBLIC_KEY
+            )
+
+            val firstKey = EncryptionUtils.generateKey()
+            val base64encodedKey = EncryptionUtils.encodeBytesToBase64String(firstKey)
+            val encryptedString = EncryptionUtils.encryptStringAsymmetric(
+                base64encodedKey,
+                publicKey
+            )
+            val decryptedString = EncryptionUtils.decryptStringAsymmetric(
+                encryptedString,
+                decryptedPrivateKey
+            )
+            val secondKey = EncryptionUtils.decodeStringToBase64Bytes(decryptedString)
+
+            if (!Arrays.equals(firstKey, secondKey)) {
+                throw Exception("Keys do not match")
+            }
+
+            notifyResult()
+        } catch (e: Exception) {
+            binding.encryptionStatus.setText(R.string.end_to_end_encryption_wrong_password)
+            Log_OC.d(TAG, "Error while decrypting private key: " + e.message)
+        }
+    }
+
+    private fun generateKey() {
+        binding.encryptionPassphrase.visibility = View.GONE
+        positiveButton?.visibility = View.GONE
+        negativeButton?.visibility = View.GONE
+
+        dialog?.setTitle(R.string.end_to_end_encryption_storing_keys)
+
+        val newKeysTask = GenerateNewKeysAsyncTask(requireContext())
+        newKeysTask.execute()
+    }
+
+    private fun notifyResult() {
+        val targetFragment = targetFragment
+        targetFragment?.onActivityResult(
+            targetRequestCode,
+            SETUP_ENCRYPTION_RESULT_CODE,
+            resultIntent
+        )
+        parentFragmentManager.setFragmentResult(RESULT_REQUEST_KEY, resultBundle)
+    }
+
+    private val resultIntent: Intent
+        get() {
+            val intentCreated = Intent()
+            intentCreated.putExtra(SUCCESS, true)
+            intentCreated.putExtra(ARG_POSITION, requireArguments().getInt(ARG_POSITION))
+            return intentCreated
+        }
+    private val resultBundle: Bundle
+        get() {
+            val bundle = Bundle()
+            bundle.putBoolean(SUCCESS, true)
+            bundle.putInt(ARG_POSITION, requireArguments().getInt(ARG_POSITION))
+            return bundle
+        }
+
+    override fun onCancel(dialog: DialogInterface) {
+        super.onCancel(dialog)
+        val bundle = Bundle()
+        bundle.putBoolean(RESULT_KEY_CANCELLED, true)
+        parentFragmentManager.setFragmentResult(RESULT_REQUEST_KEY, bundle)
+    }
+
+    override fun onSaveInstanceState(outState: Bundle) {
+        outState.putStringArrayList(EncryptionUtils.MNEMONIC, keyWords)
+        super.onSaveInstanceState(outState)
+    }
+
+    @SuppressLint("StaticFieldLeak")
+    inner class DownloadKeysAsyncTask(context: Context) : AsyncTask<Void?, Void?, String?>() {
+        private val mWeakContext: WeakReference<Context>
+
+        init {
+            mWeakContext = WeakReference(context)
+        }
+
+        @Suppress("ReturnCount")
+        @Deprecated("Deprecated in Java")
+        override fun doInBackground(vararg params: Void?): String? {
+            // fetch private/public key
+            // if available
+            //  - store public key
+            //  - decrypt private key, store unencrypted private key in database
+            val context = mWeakContext.get()
+            val publicKeyOperation = GetPublicKeyOperation()
+            val user = user ?: return null
+
+            val publicKeyResult = publicKeyOperation.execute(user, context)
+
+            if (publicKeyResult.isSuccess) {
+                Log_OC.d(TAG, "public key successful downloaded for " + user.accountName)
+
+                val publicKeyFromServer = publicKeyResult.resultData
+                if (arbitraryDataProvider != null) {
+                    arbitraryDataProvider?.storeOrUpdateKeyValue(
+                        user.accountName,
+                        EncryptionUtils.PUBLIC_KEY,
+                        publicKeyFromServer
+                    )
+                } else {
+                    return null
+                }
+            } else {
+                return null
+            }
+
+            val privateKeyResult = GetPrivateKeyOperation().execute(user, context)
+            if (privateKeyResult.isSuccess) {
+                Log_OC.d(TAG, "private key successful downloaded for " + user!!.accountName)
+                keyResult = KEY_EXISTING_USED
+                return privateKeyResult.resultData.getKey()
+            }
+
+            return null
+        }
+
+        @Deprecated("Deprecated in Java")
+        override fun onPreExecute() {
+            super.onPreExecute()
+
+            binding.encryptionStatus.setText(R.string.end_to_end_encryption_retrieving_keys)
+            positiveButton?.visibility = View.INVISIBLE
+        }
+
+        @Deprecated("Deprecated in Java")
+        override fun onPostExecute(privateKey: String?) {
+            super.onPostExecute(privateKey)
+
+            val context = mWeakContext.get()
+            if (context == null) {
+                Log_OC.e(TAG, "Context lost after fetching private keys.")
+                return
+            }
+            if (privateKey == null) {
+                // first show info
+                try {
+                    if (keyWords == null || keyWords!!.isEmpty()) {
+                        keyWords = EncryptionUtils.getRandomWords(NUMBER_OF_WORDS, context)
+                    }
+                    showMnemonicInfo()
+                } catch (e: IOException) {
+                    binding.encryptionStatus.setText(R.string.common_error)
+                }
+            } else if (privateKey.isNotEmpty()) {
+                binding.encryptionStatus.setText(R.string.end_to_end_encryption_enter_password)
+                binding.encryptionPasswordInputContainer.visibility = View.VISIBLE
+                positiveButton?.visibility = View.VISIBLE
+            } else {
+                Log_OC.e(TAG, "Got empty private key string")
+            }
+        }
+    }
+
+    @SuppressLint("StaticFieldLeak")
+    inner class GenerateNewKeysAsyncTask(context: Context) : AsyncTask<Void?, Void?, String>() {
+        private val mWeakContext: WeakReference<Context>
+
+        init {
+            mWeakContext = WeakReference(context)
+        }
+
+        @Deprecated("Deprecated in Java")
+        override fun onPreExecute() {
+            super.onPreExecute()
+            binding.encryptionStatus.setText(R.string.end_to_end_encryption_generating_keys)
+        }
+
+        @Suppress("TooGenericExceptionCaught", "TooGenericExceptionThrown", "ReturnCount")
+        @Deprecated("Deprecated in Java")
+        override fun doInBackground(vararg voids: Void?): String {
+            //  - create CSR, push to server, store returned public key in database
+            //  - encrypt private key, push key to server, store unencrypted private key in database
+            try {
+                val context = mWeakContext.get()
+                val publicKeyString: String
+
+                // Create public/private key pair
+                val keyPair = EncryptionUtils.generateKeyPair()
+
+                // create CSR
+                val accountManager = AccountManager.get(context)
+                val user = user ?: return ""
+
+                val userId = accountManager.getUserData(user.toPlatformAccount(), AccountUtils.Constants.KEY_USER_ID)
+                val urlEncoded = CsrHelper.generateCsrPemEncodedString(keyPair, userId)
+                val operation = SendCSROperation(urlEncoded)
+                val result = operation.execute(user, context)
+
+                if (result.isSuccess) {
+                    publicKeyString = result.data[0] as String
+                    if (!EncryptionUtils.isMatchingKeys(keyPair, publicKeyString)) {
+                        throw RuntimeException("Wrong CSR returned")
+                    }
+                    Log_OC.d(TAG, "public key success")
+                } else {
+                    keyResult = KEY_FAILED
+                    return ""
+                }
+
+                val privateKey = keyPair.private
+                val privateKeyString = EncryptionUtils.encodeBytesToBase64String(privateKey.encoded)
+                val privatePemKeyString = EncryptionUtils.privateKeyToPEM(privateKey)
+                val encryptedPrivateKey = EncryptionUtils.encryptPrivateKey(
+                    privatePemKeyString,
+                    generateMnemonicString(false)
+                )
+
+                // upload encryptedPrivateKey
+                val storePrivateKeyOperation = StorePrivateKeyOperation(encryptedPrivateKey)
+                val storePrivateKeyResult = storePrivateKeyOperation.execute(user, context)
+                if (storePrivateKeyResult.isSuccess) {
+                    Log_OC.d(TAG, "private key success")
+                    arbitraryDataProvider?.storeOrUpdateKeyValue(
+                        user.accountName,
+                        EncryptionUtils.PRIVATE_KEY,
+                        privateKeyString
+                    )
+                    arbitraryDataProvider?.storeOrUpdateKeyValue(
+                        user.accountName,
+                        EncryptionUtils.PUBLIC_KEY,
+                        publicKeyString
+                    )
+                    arbitraryDataProvider?.storeOrUpdateKeyValue(
+                        user.accountName,
+                        EncryptionUtils.MNEMONIC,
+                        generateMnemonicString(true)
+                    )
+                    keyResult = KEY_CREATED
+
+                    return storePrivateKeyResult.data[0] as String
+                } else {
+                    val deletePublicKeyOperation = DeletePublicKeyOperation()
+                    deletePublicKeyOperation.execute(user, context)
+                }
+            } catch (e: Exception) {
+                Log_OC.e(TAG, e.message)
+            }
+            keyResult = KEY_FAILED
+            return ""
+        }
+
+        @Deprecated("Deprecated in Java")
+        override fun onPostExecute(s: String) {
+            super.onPostExecute(s)
+            val context = mWeakContext.get()
+            if (context == null) {
+                Log_OC.e(TAG, "Context lost after generating new private keys.")
+                return
+            }
+            if (s.isEmpty()) {
+                errorSavingKeys()
+            } else {
+                if (dialog == null) {
+                    Log_OC.e(TAG, "Dialog is null cannot proceed further.")
+                    return
+                }
+                requireDialog().dismiss()
+                notifyResult()
+            }
+        }
+    }
+
+    private fun generateMnemonicString(withWhitespace: Boolean): String {
+        val stringBuilder = StringBuilder()
+        for (string in keyWords!!) {
+            stringBuilder.append(string)
+            if (withWhitespace) {
+                stringBuilder.append(' ')
+            }
+        }
+        return stringBuilder.toString()
+    }
+
+    @VisibleForTesting
+    fun showMnemonicInfo() {
+        if (dialog == null) {
+            Log_OC.e(TAG, "Dialog is null cannot proceed further.")
+            return
+        }
+        requireDialog().setTitle(R.string.end_to_end_encryption_passphrase_title)
+        binding.encryptionStatus.setText(R.string.end_to_end_encryption_keywords_description)
+        viewThemeUtils!!.material.colorTextInputLayout(binding.encryptionPasswordInputContainer)
+        binding.encryptionPassphrase.text = generateMnemonicString(true)
+        binding.encryptionPassphrase.visibility = View.VISIBLE
+        positiveButton!!.setText(R.string.end_to_end_encryption_confirm_button)
+        positiveButton!!.visibility = View.VISIBLE
+        negativeButton!!.visibility = View.VISIBLE
+        viewThemeUtils!!.platform.colorTextButtons(positiveButton!!, negativeButton!!)
+        keyResult = KEY_GENERATE
+    }
+
+    @VisibleForTesting
+    fun errorSavingKeys() {
+        if (dialog == null) {
+            Log_OC.e(TAG, "Dialog is null cannot proceed further.")
+            return
+        }
+
+        keyResult = KEY_FAILED
+        requireDialog().setTitle(R.string.common_error)
+        binding.encryptionStatus.setText(R.string.end_to_end_encryption_unsuccessful)
+        binding.encryptionPassphrase.visibility = View.GONE
+
+        positiveButton?.setText(R.string.end_to_end_encryption_dialog_close)
+        positiveButton?.visibility = View.VISIBLE
+
+        if (positiveButton != null) {
+            viewThemeUtils?.platform?.colorTextButtons(positiveButton!!)
+        }
+    }
+
+    @VisibleForTesting
+    fun setMnemonic(keyWords: ArrayList<String>?) {
+        this.keyWords = keyWords
+    }
+
+    companion object {
+        const val SUCCESS = "SUCCESS"
+        const val SETUP_ENCRYPTION_RESULT_CODE = 101
+        const val SETUP_ENCRYPTION_REQUEST_CODE = 100
+        const val SETUP_ENCRYPTION_DIALOG_TAG = "SETUP_ENCRYPTION_DIALOG_TAG"
+        const val ARG_POSITION = "ARG_POSITION"
+        const val RESULT_REQUEST_KEY = "RESULT_REQUEST"
+        const val RESULT_KEY_CANCELLED = "IS_CANCELLED"
+        private const val NUMBER_OF_WORDS = 12
+        private const val ARG_USER = "ARG_USER"
+        private val TAG = SetupEncryptionDialogFragment::class.java.simpleName
+        private const val KEY_CREATED = "KEY_CREATED"
+        private const val KEY_EXISTING_USED = "KEY_EXISTING_USED"
+        private const val KEY_FAILED = "KEY_FAILED"
+        private const val KEY_GENERATE = "KEY_GENERATE"
+
+        /**
+         * Public factory method to create new SetupEncryptionDialogFragment instance
+         *
+         * @return Dialog ready to show.
+         */
+        @JvmStatic
+        fun newInstance(user: User?, position: Int): SetupEncryptionDialogFragment {
+            val fragment = SetupEncryptionDialogFragment()
+            val args = Bundle()
+            args.putParcelable(ARG_USER, user)
+            args.putInt(ARG_POSITION, position)
+            fragment.arguments = args
+            return fragment
+        }
+    }
+}

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

@@ -693,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().
@@ -708,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().

+ 69 - 61
app/src/main/java/com/owncloud/android/ui/fragment/FileDetailsSharingProcessFragment.kt

@@ -30,6 +30,8 @@ import android.view.View
 import android.view.ViewGroup
 import androidx.fragment.app.Fragment
 import com.nextcloud.client.di.Injectable
+import com.nextcloud.utils.extensions.getParcelableArgument
+import com.nextcloud.utils.extensions.getSerializableArgument
 import com.owncloud.android.R
 import com.owncloud.android.databinding.FileDetailsSharingProcessFragmentBinding
 import com.owncloud.android.datamodel.OCFile
@@ -123,7 +125,7 @@ class FileDetailsSharingProcessFragment :
     private var chosenExpDateInMills: Long = -1 // for no expiry date
 
     private var share: OCShare? = null
-    private var isReshareShown: Boolean = true // show or hide reshare option
+    private var isReShareShown: Boolean = true // show or hide reShare option
     private var isExpDateShown: Boolean = true // show or hide expiry date option
 
     private var expirationDatePickerFragment: ExpirationDatePickerDialogFragment? = null
@@ -139,18 +141,20 @@ class FileDetailsSharingProcessFragment :
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
+
         arguments?.let {
-            file = it.getParcelable(ARG_OCFILE)
+            file = it.getParcelableArgument(ARG_OCFILE, OCFile::class.java)
             shareeName = it.getString(ARG_SHAREE_NAME)
-            share = it.getParcelable(ARG_OCSHARE)
+            share = it.getParcelableArgument(ARG_OCSHARE, OCShare::class.java)
+
             if (it.containsKey(ARG_SHARE_TYPE)) {
-                shareType = it.getSerializable(ARG_SHARE_TYPE) as ShareType
+                shareType = it.getSerializableArgument(ARG_SHARE_TYPE, ShareType::class.java)!!
             } else if (share != null) {
                 shareType = share!!.shareType!!
             }
 
             shareProcessStep = it.getInt(ARG_SCREEN_TYPE, SCREEN_TYPE_PERMISSION)
-            isReshareShown = it.getBoolean(ARG_RESHARE_SHOWN, true)
+            isReShareShown = it.getBoolean(ARG_RESHARE_SHOWN, true)
             isExpDateShown = it.getBoolean(ARG_EXP_DATE_SHOWN, true)
         }
 
@@ -178,8 +182,8 @@ class FileDetailsSharingProcessFragment :
     }
 
     private fun themeView() {
-        viewThemeUtils.platform.colorPrimaryTextViewElement(binding.shareProcessEditShareLink)
-        viewThemeUtils.platform.colorPrimaryTextViewElement(binding.shareProcessAdvancePermissionTitle)
+        viewThemeUtils.platform.colorTextView(binding.shareProcessEditShareLink)
+        viewThemeUtils.platform.colorTextView(binding.shareProcessAdvancePermissionTitle)
 
         viewThemeUtils.platform.themeRadioButton(binding.shareProcessPermissionReadOnly)
         viewThemeUtils.platform.themeRadioButton(binding.shareProcessPermissionUploadEditing)
@@ -218,27 +222,13 @@ class FileDetailsSharingProcessFragment :
         binding.shareProcessEditShareLink.visibility = View.VISIBLE
         binding.shareProcessGroupTwo.visibility = View.GONE
 
-        if (share != null) {
-            setupModificationUI()
-        } else {
-            setupUpdateUI()
-        }
-
-        // show or hide expiry date
-        if (isExpDateShown) {
-            binding.shareProcessSetExpDateSwitch.visibility = View.VISIBLE
-        } else {
-            binding.shareProcessSetExpDateSwitch.visibility = View.GONE
-        }
+        if (share != null) setupModificationUI() else setupUpdateUI()
+        binding.shareProcessSetExpDateSwitch.visibility = if (isExpDateShown) View.VISIBLE else View.GONE
         shareProcessStep = SCREEN_TYPE_PERMISSION
     }
 
     private fun setupModificationUI() {
-        if (share?.isFolder == true) {
-            updateViewForFolder()
-        } else {
-            updateViewForFile()
-        }
+        if (share?.isFolder == true) updateViewForFolder() else updateViewForFile()
 
         // read only / allow upload and editing / file drop
         if (SharingMenuHelper.isUploadAndEditingAllowed(share)) {
@@ -250,13 +240,19 @@ class FileDetailsSharingProcessFragment :
         }
 
         shareType = share?.shareType ?: ShareType.NO_SHARED
+
         // show different text for link share and other shares
         // because we have link to share in Public Link
-        if (shareType == ShareType.PUBLIC_LINK) {
-            binding.shareProcessBtnNext.text = requireContext().resources.getString(R.string.share_copy_link)
-        } else {
-            binding.shareProcessBtnNext.text = requireContext().resources.getString(R.string.common_confirm)
-        }
+        val resources = requireContext().resources
+
+        binding.shareProcessBtnNext.text = resources.getString(
+            if (shareType == ShareType.PUBLIC_LINK) {
+                R.string.share_copy_link
+            } else {
+                R.string.common_confirm
+            }
+        )
+
         updateViewForShareType()
         binding.shareProcessSetPasswordSwitch.isChecked = share?.isPasswordProtected == true
         showPasswordInput(binding.shareProcessSetPasswordSwitch.isChecked)
@@ -278,39 +274,50 @@ class FileDetailsSharingProcessFragment :
         showExpirationDateInput(binding.shareProcessSetExpDateSwitch.isChecked)
     }
 
-    /**
-     * method to update views on the basis of Share type
-     */
     private fun updateViewForShareType() {
-        // external share
-        if (shareType == ShareType.EMAIL) {
-            binding.shareProcessChangeNameSwitch.visibility = View.GONE
-            binding.shareProcessChangeNameContainer.visibility = View.GONE
-            updateViewForExternalAndLinkShare()
-        }
-        // link share
-        else if (shareType == ShareType.PUBLIC_LINK) {
-            updateViewForExternalAndLinkShare()
-            binding.shareProcessChangeNameSwitch.visibility = View.VISIBLE
-            if (share != null) {
-                binding.shareProcessChangeName.setText(share?.label)
-                binding.shareProcessChangeNameSwitch.isChecked = !TextUtils.isEmpty(share?.label)
+        when (shareType) {
+            ShareType.EMAIL -> {
+                updateViewForExternalShare()
+            }
+
+            ShareType.PUBLIC_LINK -> {
+                updateViewForLinkShare()
+            }
+
+            else -> {
+                updateViewForInternalShare()
             }
-            showChangeNameInput(binding.shareProcessChangeNameSwitch.isChecked)
         }
-        // internal share
-        else {
-            binding.shareProcessChangeNameSwitch.visibility = View.GONE
-            binding.shareProcessChangeNameContainer.visibility = View.GONE
-            binding.shareProcessHideDownloadCheckbox.visibility = View.GONE
-            binding.shareProcessAllowResharingCheckbox.visibility = View.VISIBLE
-            binding.shareProcessSetPasswordSwitch.visibility = View.GONE
-            if (share != null) {
-                if (!isReshareShown) {
-                    binding.shareProcessAllowResharingCheckbox.visibility = View.GONE
-                }
-                binding.shareProcessAllowResharingCheckbox.isChecked = SharingMenuHelper.canReshare(share)
+    }
+
+    private fun updateViewForExternalShare() {
+        binding.shareProcessChangeNameSwitch.visibility = View.GONE
+        binding.shareProcessChangeNameContainer.visibility = View.GONE
+        updateViewForExternalAndLinkShare()
+    }
+
+    private fun updateViewForLinkShare() {
+        updateViewForExternalAndLinkShare()
+        binding.shareProcessChangeNameSwitch.visibility = View.VISIBLE
+        if (share != null) {
+            binding.shareProcessChangeName.setText(share?.label)
+            binding.shareProcessChangeNameSwitch.isChecked = !TextUtils.isEmpty(share?.label)
+        }
+        showChangeNameInput(binding.shareProcessChangeNameSwitch.isChecked)
+    }
+
+    private fun updateViewForInternalShare() {
+        binding.shareProcessChangeNameSwitch.visibility = View.GONE
+        binding.shareProcessChangeNameContainer.visibility = View.GONE
+        binding.shareProcessHideDownloadCheckbox.visibility = View.GONE
+        binding.shareProcessAllowResharingCheckbox.visibility = View.VISIBLE
+        binding.shareProcessSetPasswordSwitch.visibility = View.GONE
+
+        if (share != null) {
+            if (!isReShareShown) {
+                binding.shareProcessAllowResharingCheckbox.visibility = View.GONE
             }
+            binding.shareProcessAllowResharingCheckbox.isChecked = SharingMenuHelper.canReshare(share)
         }
     }
 
@@ -337,7 +344,7 @@ class FileDetailsSharingProcessFragment :
      */
     private fun updateExpirationDateView() {
         if (share != null) {
-            if (share?.expirationDate ?: 0 > 0) {
+            if ((share?.expirationDate ?: 0) > 0) {
                 chosenExpDateInMills = share?.expirationDate ?: -1
                 binding.shareProcessSetExpDateSwitch.isChecked = true
                 binding.shareProcessSelectExpDate.text = (
@@ -464,7 +471,7 @@ class FileDetailsSharingProcessFragment :
         fileActivity?.supportFragmentManager?.beginTransaction()?.remove(this)?.commit()
     }
 
-    private fun getResharePermission(): Int {
+    private fun getReSharePermission(): Int {
         val spb = SharePermissionsBuilder()
         spb.setSharePermission(true)
         return spb.build()
@@ -516,12 +523,13 @@ class FileDetailsSharingProcessFragment :
      *  get the permissions on the basis of selection
      */
     private fun getSelectedPermission() = when {
-        binding.shareProcessAllowResharingCheckbox.isChecked -> getResharePermission()
+        binding.shareProcessAllowResharingCheckbox.isChecked -> getReSharePermission()
         binding.shareProcessPermissionReadOnly.isChecked -> OCShare.READ_PERMISSION_FLAG
         binding.shareProcessPermissionUploadEditing.isChecked -> when {
             file?.isFolder == true || share?.isFolder == true -> OCShare.MAXIMUM_PERMISSIONS_FOR_FOLDER
             else -> OCShare.MAXIMUM_PERMISSIONS_FOR_FILE
         }
+
         binding.shareProcessPermissionFileDrop.isChecked -> OCShare.CREATE_PERMISSION_FLAG
         else -> permission
     }

+ 22 - 21
app/src/main/java/com/owncloud/android/ui/fragment/GalleryFragmentBottomSheetDialog.kt

@@ -24,24 +24,24 @@ import android.os.Bundle
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
-import com.google.android.material.bottomsheet.BottomSheetBehavior
 import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+import com.nextcloud.android.common.ui.theme.utils.ColorRole
 import com.nextcloud.client.di.Injectable
+import com.owncloud.android.R
 import com.owncloud.android.databinding.FragmentGalleryBottomSheetBinding
 import com.owncloud.android.utils.theme.ViewThemeUtils
 import javax.inject.Inject
 
 class GalleryFragmentBottomSheetDialog(
     private val actions: GalleryFragmentBottomSheetActions
-) : BottomSheetDialogFragment(), Injectable {
+) : BottomSheetDialogFragment(R.layout.fragment_gallery_bottom_sheet), Injectable {
     @Inject
     lateinit var viewThemeUtils: ViewThemeUtils
 
     private lateinit var binding: FragmentGalleryBottomSheetBinding
-    private lateinit var mBottomBehavior: BottomSheetBehavior<*>
     private var currentMediaState: MediaState = MediaState.MEDIA_STATE_DEFAULT
 
-    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
         binding = FragmentGalleryBottomSheetBinding.inflate(layoutInflater, container, false)
         return binding.root
     }
@@ -50,43 +50,44 @@ class GalleryFragmentBottomSheetDialog(
         super.onViewCreated(view, savedInstanceState)
         setupLayout()
         setupClickListener()
-        mBottomBehavior = BottomSheetBehavior.from(binding.root.parent as View)
     }
 
-    public override fun onStart() {
-        super.onStart()
-        mBottomBehavior.state = BottomSheetBehavior.STATE_EXPANDED
-    }
+    private fun setupLayout() {
+        viewThemeUtils.platform.colorViewBackground(binding.bottomSheet, ColorRole.SURFACE)
 
-    fun setupLayout() {
         listOf(
             binding.tickMarkShowImages,
-            binding.tickMarkShowVideo,
-            binding.hideImagesImageview,
-            binding.hideVideoImageView,
-            binding.selectMediaFolderImageView
+            binding.tickMarkShowVideos
+        ).forEach {
+            viewThemeUtils.platform.colorImageView(it, ColorRole.PRIMARY)
+        }
+
+        listOf(
+            binding.btnSelectMediaFolder,
+            binding.btnHideVideos,
+            binding.btnHideImages
         ).forEach {
-            viewThemeUtils.platform.colorImageView(it)
+            viewThemeUtils.material.colorMaterialButtonText(it)
         }
 
         when (currentMediaState) {
             MediaState.MEDIA_STATE_PHOTOS_ONLY -> {
                 binding.tickMarkShowImages.visibility = View.VISIBLE
-                binding.tickMarkShowVideo.visibility = View.GONE
+                binding.tickMarkShowVideos.visibility = View.GONE
             }
             MediaState.MEDIA_STATE_VIDEOS_ONLY -> {
                 binding.tickMarkShowImages.visibility = View.GONE
-                binding.tickMarkShowVideo.visibility = View.VISIBLE
+                binding.tickMarkShowVideos.visibility = View.VISIBLE
             }
             else -> {
                 binding.tickMarkShowImages.visibility = View.VISIBLE
-                binding.tickMarkShowVideo.visibility = View.VISIBLE
+                binding.tickMarkShowVideos.visibility = View.VISIBLE
             }
         }
     }
 
     private fun setupClickListener() {
-        binding.hideImages.setOnClickListener { v: View? ->
+        binding.btnHideImages.setOnClickListener {
             currentMediaState = if (currentMediaState == MediaState.MEDIA_STATE_VIDEOS_ONLY) {
                 MediaState.MEDIA_STATE_DEFAULT
             } else {
@@ -95,7 +96,7 @@ class GalleryFragmentBottomSheetDialog(
             notifyStateChange()
             dismiss()
         }
-        binding.hideVideo.setOnClickListener { v: View? ->
+        binding.btnHideVideos.setOnClickListener {
             currentMediaState = if (currentMediaState == MediaState.MEDIA_STATE_PHOTOS_ONLY) {
                 MediaState.MEDIA_STATE_DEFAULT
             } else {
@@ -104,7 +105,7 @@ class GalleryFragmentBottomSheetDialog(
             notifyStateChange()
             dismiss()
         }
-        binding.selectMediaFolder.setOnClickListener { v: View? ->
+        binding.btnSelectMediaFolder.setOnClickListener {
             actions.selectMediaFolder()
             dismiss()
         }

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

@@ -810,7 +810,7 @@ public class FileOperationsHelper {
         FragmentTransaction ft = fm.beginTransaction();
         ft.addToBackStack(null);
 
-        SendFilesDialog sendFilesDialog = SendFilesDialog.newInstance(files);
+        SendFilesDialog sendFilesDialog = SendFilesDialog.Companion.newInstance(files);
         sendFilesDialog.show(ft, "TAG_SEND_SHARE_DIALOG");
     }
 

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

+ 3 - 4
app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaFragment.java

@@ -673,10 +673,9 @@ public class PreviewMediaFragment extends FileFragment implements OnTouchListene
     }
 
     public void stopPreview(boolean stopAudio) {
-        OCFile file = getFile();
-        if (MimeTypeUtil.isAudio(file) && stopAudio) {
-            mediaPlayerServiceConnection.pause();
-        } else if (MimeTypeUtil.isVideo(file)) {
+        if (stopAudio && mediaPlayerServiceConnection != null) {
+            mediaPlayerServiceConnection.stop();
+        } else if (exoPlayer != null) {
             savedPlaybackPosition = exoPlayer.getCurrentPosition();
             exoPlayer.stop();
         }

+ 54 - 52
app/src/main/res/layout/file_actions_bottom_sheet.xml

@@ -19,73 +19,75 @@
   ~ License along with this program.  If not, see <http://www.gnu.org/licenses/>.
   ~
   -->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<FrameLayout
+    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/bottom_sheet"
+    style="@style/Widget.Material3.BottomSheet"
     android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:orientation="vertical"
-    android:minHeight="@dimen/bottom_sheet_min_height"
+    android:layout_height="wrap_content"
+    android:layout_gravity="bottom"
     app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
 
-    <com.google.android.material.bottomsheet.BottomSheetDragHandleView
+    <LinearLayout
+        android:id="@+id/bottom_sheet"
         android:layout_width="match_parent"
-        android:layout_height="wrap_content" />
-
-    <com.google.android.material.progressindicator.CircularProgressIndicator
-        android:indeterminate="true"
-        android:id="@+id/bottom_sheet_loading"
-        android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:layout_gravity="center"
-        tools:visibility="gone" />
+        android:orientation="vertical">
 
-    <androidx.constraintlayout.widget.ConstraintLayout
-        android:id="@+id/bottom_sheet_content"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:orientation="vertical"
-        android:visibility="gone"
-        tools:visibility="visible">
+        <com.google.android.material.bottomsheet.BottomSheetDragHandleView
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content" />
 
-        <FrameLayout
-            android:id="@+id/thumbnail_container"
+        <com.google.android.material.progressindicator.CircularProgressIndicator
+            android:id="@+id/bottom_sheet_loading"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:layout_marginStart="@dimen/standard_padding"
-            app:layout_constraintBottom_toBottomOf="@+id/title"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintTop_toTopOf="@+id/title">
+            android:layout_gravity="center"
+            android:layout_marginBottom="@dimen/standard_margin"
+            android:indeterminate="true"
+            tools:visibility="gone" />
+
+        <LinearLayout
+            android:id="@+id/bottom_sheet_header"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="@dimen/standard_margin"
+            android:gravity="center"
+            android:orientation="horizontal"
+            android:visibility="visible">
 
             <include
                 android:id="@+id/thumbnail_layout"
-                layout="@layout/file_thumbnail" />
-        </FrameLayout>
+                layout="@layout/file_thumbnail"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginStart="12dp" />
 
-        <TextView
-            android:id="@+id/title"
-            android:layout_width="0dp"
-            android:layout_height="wrap_content"
-            android:ellipsize="middle"
-            android:lines="1"
-            android:padding="@dimen/standard_padding"
-            android:textAppearance="@style/TextAppearance.Material3.HeadlineSmall"
-            app:layout_constraintEnd_toEndOf="parent"
-            app:layout_constraintStart_toEndOf="@id/thumbnail_container"
-            app:layout_constraintTop_toTopOf="parent"
-            tools:text="Test file name which is very very very very very long.pdf" />
+            <TextView
+                android:id="@+id/title"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginStart="24dp"
+                android:ellipsize="middle"
+                android:lines="1"
+                android:textAppearance="@style/TextAppearance.Material3.HeadlineSmall"
+                tools:text="Test file name which is very very very very very long.pdf" />
 
-    </androidx.constraintlayout.widget.ConstraintLayout>
+        </LinearLayout>
 
-    <androidx.core.widget.NestedScrollView
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content">
-
-        <LinearLayout
-            android:id="@+id/file_actions_list"
+        <androidx.core.widget.NestedScrollView
             android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:orientation="vertical" />
-    </androidx.core.widget.NestedScrollView>
-</LinearLayout>
+            android:layout_height="wrap_content">
+
+            <LinearLayout
+                android:id="@+id/file_actions_list"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="vertical" />
+
+        </androidx.core.widget.NestedScrollView>
+
+    </LinearLayout>
+
+</FrameLayout>

+ 17 - 20
app/src/main/res/layout/file_details_sharing_process_fragment.xml

@@ -62,23 +62,23 @@
                 app:layout_constraintStart_toStartOf="parent"
                 app:layout_constraintTop_toBottomOf="@+id/share_process_edit_share_link">
 
-                <androidx.appcompat.widget.AppCompatRadioButton
+                <com.google.android.material.radiobutton.MaterialRadioButton
                     android:id="@+id/share_process_permission_read_only"
-                    android:layout_width="wrap_content"
+                    android:layout_width="match_parent"
                     android:layout_height="wrap_content"
                     android:minHeight="@dimen/minimum_size_for_touchable_area"
                     android:text="@string/link_share_view_only" />
 
-                <androidx.appcompat.widget.AppCompatRadioButton
+                <com.google.android.material.radiobutton.MaterialRadioButton
                     android:id="@+id/share_process_permission_upload_editing"
-                    android:layout_width="wrap_content"
+                    android:layout_width="match_parent"
                     android:layout_height="wrap_content"
                     android:minHeight="@dimen/minimum_size_for_touchable_area"
                     android:text="@string/link_share_allow_upload_and_editing" />
 
-                <androidx.appcompat.widget.AppCompatRadioButton
+                <com.google.android.material.radiobutton.MaterialRadioButton
                     android:id="@+id/share_process_permission_file_drop"
-                    android:layout_width="wrap_content"
+                    android:layout_width="match_parent"
                     android:layout_height="wrap_content"
                     android:minHeight="@dimen/minimum_size_for_touchable_area"
                     android:text="@string/link_share_file_drop" />
@@ -97,19 +97,18 @@
                 app:layout_constraintStart_toStartOf="parent"
                 app:layout_constraintTop_toBottomOf="@+id/share_process_permission_radio_group" />
 
-            <androidx.appcompat.widget.AppCompatCheckBox
+            <com.google.android.material.checkbox.MaterialCheckBox
                 android:id="@+id/share_process_allow_resharing_checkbox"
-                android:layout_width="0dp"
+                android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:minHeight="@dimen/minimum_size_for_touchable_area"
                 android:text="@string/allow_resharing"
                 android:visibility="gone"
-                app:layout_constraintEnd_toEndOf="parent"
                 app:layout_constraintStart_toStartOf="parent"
                 app:layout_constraintTop_toBottomOf="@+id/share_process_advance_permission_title"
                 tools:visibility="visible" />
 
-            <androidx.appcompat.widget.SwitchCompat
+            <com.google.android.material.materialswitch.MaterialSwitch
                 android:id="@+id/share_process_set_password_switch"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
@@ -144,7 +143,7 @@
 
             </com.google.android.material.textfield.TextInputLayout>
 
-            <androidx.appcompat.widget.SwitchCompat
+            <com.google.android.material.materialswitch.MaterialSwitch
                 android:id="@+id/share_process_set_exp_date_switch"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
@@ -178,7 +177,7 @@
                 app:layout_constraintStart_toStartOf="parent"
                 app:layout_constraintTop_toBottomOf="@+id/share_process_select_exp_date" />
 
-            <androidx.appcompat.widget.SwitchCompat
+            <com.google.android.material.materialswitch.MaterialSwitch
                 android:id="@+id/share_process_hide_download_checkbox"
                 android:layout_width="0dp"
                 android:layout_height="wrap_content"
@@ -191,7 +190,7 @@
                 app:layout_constraintTop_toBottomOf="@+id/share_process_exp_date_divider"
                 tools:visibility="visible" />
 
-            <androidx.appcompat.widget.SwitchCompat
+            <com.google.android.material.materialswitch.MaterialSwitch
                 android:id="@+id/share_process_change_name_switch"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
@@ -285,8 +284,8 @@
 
     <com.google.android.material.button.MaterialButton
         android:id="@+id/share_process_btn_cancel"
-        style="@style/OutlinedButton"
-        android:layout_width="0dp"
+        style="@style/Widget.Material3.Button.OutlinedButton"
+        android:layout_width="@dimen/button_width"
         android:layout_height="wrap_content"
         android:layout_marginStart="@dimen/standard_margin"
         android:layout_marginEnd="@dimen/standard_half_margin"
@@ -295,21 +294,19 @@
         app:cornerRadius="@dimen/button_corner_radius"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintEnd_toStartOf="@+id/share_process_btn_next"
-        app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toTopOf="@+id/share_process_btn_next" />
 
     <com.google.android.material.button.MaterialButton
         android:id="@+id/share_process_btn_next"
-        android:layout_width="0dp"
+        android:layout_width="@dimen/button_extra_width"
         android:layout_height="wrap_content"
         android:layout_marginStart="@dimen/standard_half_margin"
         android:layout_marginEnd="@dimen/standard_margin"
         android:layout_marginBottom="@dimen/standard_margin"
         android:text="@string/common_next"
-        android:theme="@style/Button.Primary"
+        android:theme="@style/Widget.Material3.Button.IconButton.Filled"
         app:cornerRadius="@dimen/button_corner_radius"
         app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintStart_toEndOf="@+id/share_process_btn_cancel" />
+        app:layout_constraintEnd_toEndOf="parent" />
 
 </androidx.constraintlayout.widget.ConstraintLayout>

+ 1 - 1
app/src/main/res/layout/first_run_activity.xml

@@ -93,7 +93,7 @@
                 app:cornerRadius="@dimen/button_corner_radius" />
         </LinearLayout>
 
-        <TextView
+        <com.google.android.material.textview.MaterialTextView
             android:id="@+id/host_own_server"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"

+ 119 - 118
app/src/main/res/layout/fragment_gallery_bottom_sheet.xml

@@ -1,5 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
+<?xml version="1.0" encoding="utf-8"?><!--
  Nextcloud Android client application
 
  @author TSI-mc
@@ -19,129 +18,131 @@
  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/>.
  -->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<androidx.coordinatorlayout.widget.CoordinatorLayout
+    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:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:orientation="vertical"
-    android:paddingBottom="@dimen/standard_half_padding">
+    android:layout_height="match_parent">
 
-    <RelativeLayout
-        android:id="@+id/hideImages"
+    <FrameLayout
+        style="@style/Widget.Material3.BottomSheet"
         android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:background="?android:attr/selectableItemBackground"
-        android:orientation="horizontal"
-        android:paddingLeft="@dimen/standard_padding"
-        android:paddingTop="@dimen/standard_padding"
-        android:paddingRight="@dimen/standard_padding"
-        android:paddingBottom="@dimen/standard_half_padding">
-
-        <ImageView
-            android:id="@+id/hideImagesImageview"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:contentDescription="@null"
-            android:src="@drawable/ic_camera"
-            app:tint="@color/primary" />
+        android:layout_height="match_parent"
+        app:behavior_peekHeight="350dp"
+        app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
 
-        <TextView
-            android:id="@+id/hideImagesTextview"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_gravity="center_vertical"
-            android:layout_marginStart="@dimen/standard_margin"
-            android:layout_marginEnd="30dp"
-            android:layout_toEndOf="@id/hideImagesImageview"
-            android:text="@string/show_images"
-            android:textColor="@color/text_color"
-            android:textSize="@dimen/bottom_sheet_text_size" />
-
-        <ImageView
-            android:id="@+id/tickMarkShowImages"
-            android:layout_width="24dp"
-            android:layout_height="24dp"
-            android:layout_alignParentEnd="true"
-            android:contentDescription="@null"
-            android:src="@drawable/ic_tick"
-            android:visibility="gone"
-            app:tint="@color/primary"
-            tools:visibility="visible" />
-
-
-    </RelativeLayout>
-
-    <RelativeLayout
-        android:id="@+id/hideVideo"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:background="?android:attr/selectableItemBackground"
-        android:orientation="horizontal"
-        android:paddingLeft="@dimen/standard_padding"
-        android:paddingTop="@dimen/standard_half_padding"
-        android:paddingRight="@dimen/standard_padding"
-        android:paddingBottom="@dimen/standard_half_padding">
-
-        <ImageView
-            android:id="@+id/hideVideoImageView"
-            android:layout_width="wrap_content"
+        <LinearLayout
+            android:id="@+id/bottom_sheet"
+            android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:contentDescription="@null"
-            android:src="@drawable/ic_video_camera"
-            app:tint="@color/primary" />
+            android:padding="@dimen/standard_padding"
+            android:orientation="vertical">
+
+            <com.google.android.material.bottomsheet.BottomSheetDragHandleView
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content" />
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:gravity="center"
+                android:orientation="horizontal">
+
+                <com.google.android.material.button.MaterialButton
+                    android:id="@+id/btn_hide_images"
+                    style="@style/Widget.Material3.Button.IconButton"
+                    android:layout_width="wrap_content"
+                    android:layout_weight="8"
+                    android:gravity="start"
+                    android:layout_height="wrap_content"
+                    android:text="@string/show_images"
+                    app:iconPadding="@dimen/standard_half_padding"
+                    android:textSize="@dimen/bottom_sheet_text_size"
+                    app:icon="@drawable/ic_camera" />
+
+                <View
+                    android:layout_width="0dp"
+                    android:layout_height="0dp"
+                    android:layout_weight="1"/>
+
+                <ImageView
+                    android:id="@+id/tickMarkShowImages"
+                    android:layout_width="24dp"
+                    android:layout_height="24dp"
+                    android:contentDescription="@null"
+                    android:src="@drawable/ic_tick"
+                    android:visibility="gone"
+                    app:tint="@color/primary"
+                    tools:visibility="visible" />
+
+
+            </LinearLayout>
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:gravity="center"
+                android:orientation="horizontal">
+
+                <com.google.android.material.button.MaterialButton
+                    android:id="@+id/btn_hide_videos"
+                    style="@style/Widget.Material3.Button.IconButton"
+                    android:layout_width="wrap_content"
+                    android:layout_weight="8"
+                    android:gravity="start"
+                    android:layout_height="wrap_content"
+                    android:text="@string/show_video"
+                    app:iconPadding="@dimen/standard_half_padding"
+                    android:textSize="@dimen/bottom_sheet_text_size"
+                    app:icon="@drawable/ic_video_camera" />
+
+                <View
+                    android:layout_width="0dp"
+                    android:layout_height="0dp"
+                    android:layout_weight="1"/>
+
+                <ImageView
+                    android:id="@+id/tickMarkShowVideos"
+                    android:layout_width="24dp"
+                    android:layout_height="24dp"
+                    android:contentDescription="@null"
+                    android:src="@drawable/ic_tick"
+                    android:visibility="gone"
+                    app:tint="@color/primary"
+                    tools:visibility="visible" />
+
+            </LinearLayout>
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:gravity="center"
+                android:orientation="horizontal">
+
+                <com.google.android.material.button.MaterialButton
+                    android:id="@+id/btn_select_media_folder"
+                    style="@style/Widget.Material3.Button.IconButton"
+                    android:layout_width="match_parent"
+                    android:gravity="start"
+                    android:layout_height="wrap_content"
+                    app:iconPadding="@dimen/standard_half_padding"
+                    android:text="@string/select_media_folder"
+                    android:textSize="@dimen/bottom_sheet_text_size"
+                    app:icon="@drawable/nav_photos" />
+
+                <View
+                    android:layout_width="0dp"
+                    android:layout_height="0dp"
+                    android:layout_weight="1"/>
+
+            </LinearLayout>
+
+        </LinearLayout>
+
+    </FrameLayout>
+
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
 
-        <TextView
-            android:id="@+id/hideVideoTextview"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_gravity="center_vertical"
-            android:layout_marginStart="@dimen/standard_margin"
-            android:layout_toEndOf="@id/hideVideoImageView"
-            android:text="@string/show_video"
-            android:textColor="@color/text_color"
-            android:textSize="@dimen/bottom_sheet_text_size" />
-
-        <ImageView
-            android:id="@+id/tickMarkShowVideo"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_alignParentEnd="true"
-            android:contentDescription="@null"
-            android:src="@drawable/ic_tick"
-            android:visibility="gone"
-            app:tint="@color/primary"
-            tools:visibility="visible" />
 
-    </RelativeLayout>
 
-    <LinearLayout
-        android:id="@+id/selectMediaFolder"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:background="?android:attr/selectableItemBackground"
-        android:orientation="horizontal"
-        android:paddingLeft="@dimen/standard_padding"
-        android:paddingTop="@dimen/standard_half_padding"
-        android:paddingRight="@dimen/standard_padding"
-        android:paddingBottom="@dimen/standard_half_padding">
-
-        <ImageView
-            android:id="@+id/selectMediaFolderImageView"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:contentDescription="@null"
-            android:src="@drawable/nav_photos"
-            app:tint="@color/primary" />
-
-        <TextView
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_gravity="center_vertical"
-            android:layout_marginStart="@dimen/standard_margin"
-            android:text="@string/select_media_folder"
-            android:textColor="@color/text_color"
-            android:textSize="@dimen/bottom_sheet_text_size" />
-    </LinearLayout>
-
-</LinearLayout>

+ 26 - 7
app/src/main/res/layout/send_files_fragment.xml

@@ -18,19 +18,38 @@
  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/>.
 -->
-<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
+
+<FrameLayout
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/standard_bottom_sheet"
+    style="@style/Widget.Material3.BottomSheet"
     android:layout_width="match_parent"
-    android:layout_height="match_parent">
+    android:layout_height="wrap_content"
+    android:layout_gravity="bottom"
+    app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
 
-    <RelativeLayout
+    <LinearLayout
+        android:id="@+id/bottom_sheet"
         android:layout_width="match_parent"
-        android:layout_height="match_parent">
+        android:layout_height="wrap_content"
+        android:orientation="vertical">
+
+        <com.google.android.material.bottomsheet.BottomSheetDragHandleView
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"/>
 
         <androidx.recyclerview.widget.RecyclerView
             android:id="@+id/send_button_recycler_view"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            tools:listitem="@layout/send_button" />
-    </RelativeLayout>
-</androidx.coordinatorlayout.widget.CoordinatorLayout>
+            tools:listitem="@layout/send_button"/>
+
+        <View
+            android:layout_width="match_parent"
+            android:layout_height="@dimen/standard_padding"/>
+
+    </LinearLayout>
+
+</FrameLayout>

+ 43 - 77
app/src/main/res/layout/send_share_fragment.xml

@@ -19,102 +19,68 @@
  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/>.
 -->
-<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<FrameLayout
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/standard_bottom_sheet"
+    style="@style/Widget.Material3.BottomSheet"
     android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:background="@color/bg_default">
+    android:layout_height="wrap_content"
+    android:layout_gravity="bottom"
+    app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
 
-    <RelativeLayout
+    <LinearLayout
+        android:id="@+id/bottom_sheet"
         android:layout_width="match_parent"
-        android:layout_height="match_parent">
+        android:layout_height="wrap_content"
+        android:orientation="vertical">
+
+        <com.google.android.material.bottomsheet.BottomSheetDragHandleView
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"/>
 
         <LinearLayout
             android:id="@+id/send_share_buttons"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:layout_marginEnd="@dimen/send_share_fragment_layout_margin"
-            android:layout_marginLeft="@dimen/send_share_fragment_layout_margin"
-            android:layout_marginRight="@dimen/send_share_fragment_layout_margin"
-            android:layout_marginStart="@dimen/send_share_fragment_layout_margin"
+            android:gravity="center"
             android:orientation="horizontal"
-            android:baselineAligned="false"
-            android:padding="@dimen/send_share_fragment_layout_padding">
-
-        <LinearLayout
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_weight="1"
-            android:orientation="vertical"
-            tools:ignore="UseCompoundDrawables">
+            android:paddingBottom="@dimen/standard_margin">
 
-            <ImageView
-                android:id="@+id/share_people_icon"
-                android:layout_width="@dimen/share_icon_size"
-                android:layout_height="@dimen/share_icon_size"
-                android:layout_gravity="center_horizontal"
-                android:contentDescription="@string/share"
-                android:src="@drawable/shared_via_users"
-                android:paddingTop="@dimen/share_people_icon_layout_padding"
-                android:paddingEnd="@dimen/share_people_icon_layout_padding"
-                android:paddingBottom="@dimen/share_people_icon_layout_padding"
-                android:paddingStart="@dimen/standard_half_padding"
-                android:background="@drawable/round_bgnd"/>
-
-            <TextView
-                android:id="@+id/share_people_button"
+            <com.google.android.material.button.MaterialButton
+                android:id="@+id/btn_share"
+                style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
-                android:layout_gravity="center_horizontal"
-                android:drawable="@drawable/ic_share"
                 android:text="@string/share"
-                android:paddingTop="@dimen/standard_half_padding"
-                android:textColor="@color/text_color"/>
-        </LinearLayout>
-
-        <LinearLayout
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_weight="1"
-            android:orientation="vertical"
-            tools:ignore="UseCompoundDrawables">
-
-            <ImageView
-                android:id="@+id/share_link_icon"
-                android:layout_width="@dimen/share_icon_size"
-                android:layout_height="@dimen/share_icon_size"
-                android:layout_gravity="center_horizontal"
-                android:contentDescription="@string/link"
-                android:src="@drawable/shared_via_link"
-                android:padding="12dp"
-                android:background="@drawable/round_bgnd"/>
+                app:iconPadding="@dimen/standard_half_padding"
+                app:icon="@drawable/shared_via_users" />
 
-            <TextView
-                android:id="@+id/share_link_button"
+            <com.google.android.material.button.MaterialButton
+                android:id="@+id/btn_link"
+                style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
-                android:layout_gravity="center_horizontal"
-                android:drawable="@drawable/ic_share"
+                app:iconPadding="@dimen/standard_half_padding"
                 android:text="@string/link"
-                android:paddingTop="@dimen/standard_half_padding"
-                android:textColor="@color/text_color"/>
-        </LinearLayout>
+                app:icon="@drawable/shared_via_link" />
 
-    </LinearLayout>
+        </LinearLayout>
 
-    <View
-        android:id="@+id/divider"
-        android:layout_width="match_parent"
-        android:layout_height="1dp"
-        android:layout_below="@id/send_share_buttons"
-        android:alpha="0.3"
-        android:background="@color/background_color_inverse"/>
+        <View
+            android:id="@+id/divider"
+            android:layout_width="match_parent"
+            android:layout_height="1dp"
+            android:alpha="0.3"
+            android:background="@color/background_color_inverse"/>
 
         <androidx.recyclerview.widget.RecyclerView
-        android:id="@+id/send_button_recycler_view"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_below="@id/divider"
-        tools:listitem="@layout/send_button" />
-    </RelativeLayout>
-</androidx.coordinatorlayout.widget.CoordinatorLayout>
+            android:id="@+id/send_button_recycler_view"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            tools:listitem="@layout/send_button" />
+
+    </LinearLayout>
+
+</FrameLayout>

+ 2 - 2
app/src/main/res/layout/setup_encryption_dialog.xml

@@ -26,14 +26,14 @@
     android:orientation="vertical"
     android:padding="@dimen/dialog_padding">
 
-    <TextView
+    <com.google.android.material.textview.MaterialTextView
         android:id="@+id/encryption_status"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:layout_marginBottom="@dimen/standard_margin"
         tools:text="@string/end_to_end_encryption_keywords_description" />
 
-    <TextView
+    <com.google.android.material.textview.MaterialTextView
         android:id="@+id/encryption_passphrase"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"

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

@@ -18,6 +18,7 @@
     <string name="actionbar_copy">نسخ</string>
     <string name="actionbar_mkdir">مجلد جديد</string>
     <string name="actionbar_move">نقل</string>
+    <string name="actionbar_move_or_copy">أنقُل أو انسَخ</string>
     <string name="actionbar_open_with">فتح باستخدام</string>
     <string name="actionbar_search">بحث</string>
     <string name="actionbar_see_details">تفاصيل</string>
@@ -324,6 +325,7 @@
     <string name="file_delete">حذف </string>
     <string name="file_detail_activity_error">خطأ في استرداد النشاطات للملف</string>
     <string name="file_details_no_content">حدث خطأ في تحميل التفاصيل</string>
+    <string name="file_downloader_notification_title_prefix">تنزيل \u0020</string>
     <string name="file_icon">الملف</string>
     <string name="file_keep">حفظ</string>
     <string name="file_list_empty">قم برفع بعض المحتوى أو زامن مع أجهزتك</string>
@@ -392,6 +394,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>
@@ -947,8 +952,11 @@
     <string name="version_dev_download">تنزيل</string>
     <string name="video_overlay_icon">أيقونة التركيب على الفيديو</string>
     <string name="wait_a_moment">انتظر لحظة…</string>
-    <string name="wait_checking_credentials">التحقق من معلومات تسجيل الدخول المخزنة</string>
+    <string name="wait_checking_credentials">التحقق من معلومات تسجيل الدخول المحفوظة</string>
     <string name="wait_for_tmp_copy_from_private_storage">نسخ الملف من التخزين الخاص</string>
+    <string name="webview_version_check_alert_dialog_message">يرجى تحديث تطبيق  WebView في نظام أندرويد لتسجيل الدخول</string>
+    <string name="webview_version_check_alert_dialog_positive_button_title">تحديث</string>
+    <string name="webview_version_check_alert_dialog_title">تحديث تطبيق  WebView في نظام أندرويد</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-b+en+001/strings.xml

@@ -392,6 +392,9 @@
     <string name="folder_confirm_create">Create</string>
     <string name="folder_list_empty_headline">No folders here</string>
     <string name="folder_picker_choose_button_text">Choose</string>
+    <string name="folder_picker_choose_caption_text">Choose target folder</string>
+    <string name="folder_picker_copy_button_text">Copy</string>
+    <string name="folder_picker_move_button_text">Move</string>
     <string name="forbidden_permissions">You are not permitted %s</string>
     <string name="forbidden_permissions_copy">to copy this file</string>
     <string name="forbidden_permissions_create">to create this file</string>
@@ -941,6 +944,7 @@
     <string name="wait_a_moment">Wait a moment…</string>
     <string name="wait_checking_credentials">Checking stored credentials</string>
     <string name="wait_for_tmp_copy_from_private_storage">Copying file from private storage</string>
+    <string name="webview_version_check_alert_dialog_positive_button_title">Update</string>
     <string name="what_s_new_image">What\'s new image</string>
     <string name="whats_new_skip">Skip</string>
     <string name="whats_new_title">New in %1$s</string>

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

@@ -386,6 +386,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>
@@ -912,6 +915,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>

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

@@ -326,6 +326,8 @@
     <string name="folder_confirm_create">Krouiñ</string>
     <string name="folder_list_empty_headline">Emañ heuliad ebet amañ</string>
     <string name="folder_picker_choose_button_text">Dibab</string>
+    <string name="folder_picker_choose_caption_text">Dibab an teuliad moned</string>
+    <string name="folder_picker_move_button_text">Diplasañ</string>
     <string name="forbidden_permissions">N\'oc\'h ket aotreet %s</string>
     <string name="forbidden_permissions_copy">eit eilan ar restr</string>
     <string name="forbidden_permissions_create">evit krouiñ ar restr</string>
@@ -766,6 +768,7 @@
     <string name="wait_a_moment">Kortozit un tamm amzer...</string>
     <string name="wait_checking_credentials">O gwiriañ an titouroù identitelez entollet</string>
     <string name="wait_for_tmp_copy_from_private_storage">eilan ar restr diouzh un teuliad prevez</string>
+    <string name="webview_version_check_alert_dialog_positive_button_title">Hizivaat</string>
     <string name="what_s_new_image">Peseurt skeudenn nevez</string>
     <string name="whats_new_skip">Tremen</string>
     <string name="whats_new_title">Nevez e %1$s</string>

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

@@ -377,6 +377,9 @@
     <string name="folder_confirm_create">Crea</string>
     <string name="folder_list_empty_headline">No hi ha carpetes</string>
     <string name="folder_picker_choose_button_text">Escolliu</string>
+    <string name="folder_picker_choose_caption_text">Trieu la carpeta de destinació</string>
+    <string name="folder_picker_copy_button_text">Còpia</string>
+    <string name="folder_picker_move_button_text">Mou</string>
     <string name="forbidden_permissions">No teniu permisos%s</string>
     <string name="forbidden_permissions_copy">per copiar aquest fitxer</string>
     <string name="forbidden_permissions_create">per crear aquest fitxer</string>
@@ -901,6 +904,7 @@
     <string name="wait_a_moment">Espereu un moment…</string>
     <string name="wait_checking_credentials">S\'estan comprovant les credencials emmagatzemades</string>
     <string name="wait_for_tmp_copy_from_private_storage">S\'està copiant el fitxer des de l\'emmagatzematge privat</string>
+    <string name="webview_version_check_alert_dialog_positive_button_title">Actualitza</string>
     <string name="what_s_new_image">Imatge de novetats</string>
     <string name="whats_new_skip">Omet</string>
     <string name="whats_new_title">Nou a %1$s</string>

+ 8 - 0
app/src/main/res/values-cs-rCZ/strings.xml

@@ -18,6 +18,7 @@
     <string name="actionbar_copy">Zkopírovat</string>
     <string name="actionbar_mkdir">Nová složka</string>
     <string name="actionbar_move">Přesunout</string>
+    <string name="actionbar_move_or_copy">Přesunout</string>
     <string name="actionbar_open_with">Otevřít pomocí</string>
     <string name="actionbar_search">Hledat</string>
     <string name="actionbar_see_details">Podrobnosti</string>
@@ -324,6 +325,7 @@
     <string name="file_delete">Smazat</string>
     <string name="file_detail_activity_error">Při načítání aktivit u souboru došlo k chybě</string>
     <string name="file_details_no_content">Nepodařilo se načíst podrobnosti.</string>
+    <string name="file_downloader_notification_title_prefix">Stahování \u0020</string>
     <string name="file_icon">Soubor</string>
     <string name="file_keep">Ponechat</string>
     <string name="file_list_empty">Nahrajte nějaký obsah, nebo synchronizujte s vašimi zařízeními.</string>
@@ -392,6 +394,9 @@
     <string name="folder_confirm_create">Vytvořit</string>
     <string name="folder_list_empty_headline">Nejsou zde žádné složky</string>
     <string name="folder_picker_choose_button_text">Vybrat</string>
+    <string name="folder_picker_choose_caption_text">Zvolte cílovou složku</string>
+    <string name="folder_picker_copy_button_text">Zkopírovat</string>
+    <string name="folder_picker_move_button_text">Přesunout</string>
     <string name="forbidden_permissions">Nemáte oprávnění %s</string>
     <string name="forbidden_permissions_copy">pro zkopírování tohoto souboru</string>
     <string name="forbidden_permissions_create">pro vytvoření tohoto souboru</string>
@@ -941,6 +946,9 @@
     <string name="wait_a_moment">Chvilku strpení…</string>
     <string name="wait_checking_credentials">Ověřování uložených přihlašovacích údajů</string>
     <string name="wait_for_tmp_copy_from_private_storage">Kopírování souboru ze soukromého úložiště</string>
+    <string name="webview_version_check_alert_dialog_message">Aby se bylo možné přihlásit, zaktualizujte systémovou aplikaci WebView Androidu</string>
+    <string name="webview_version_check_alert_dialog_positive_button_title">Aktualizovat</string>
+    <string name="webview_version_check_alert_dialog_title">Zaktualizovat Android System WebView</string>
     <string name="what_s_new_image">Obrázek „Co je nového“</string>
     <string name="whats_new_skip">Přeskočit</string>
     <string name="whats_new_title">Co je v %1$s nového</string>

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

@@ -388,6 +388,9 @@
     <string name="folder_confirm_create">Opret</string>
     <string name="folder_list_empty_headline">Ingen mappe her</string>
     <string name="folder_picker_choose_button_text">Vælg</string>
+    <string name="folder_picker_choose_caption_text">Vælg destinationsmappe</string>
+    <string name="folder_picker_copy_button_text">Kopiér</string>
+    <string name="folder_picker_move_button_text">Flyt</string>
     <string name="forbidden_permissions">Du har ikke tilladelse %s</string>
     <string name="forbidden_permissions_copy">til at kopiere denne fil</string>
     <string name="forbidden_permissions_create">for at slette denne fil</string>
@@ -924,6 +927,7 @@ Enheds legitimationsoplysninger er sat op
     <string name="wait_a_moment">Vent et øjeblik...</string>
     <string name="wait_checking_credentials">Undersøger lagrede certificeringer</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">Opdatér</string>
     <string name="what_s_new_image">Hvad nyt billede</string>
     <string name="whats_new_skip">Spring over</string>
     <string name="whats_new_title">Nyt i %1$s</string>

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

@@ -18,6 +18,7 @@
     <string name="actionbar_copy">Kopieren</string>
     <string name="actionbar_mkdir">Neuer Ordner</string>
     <string name="actionbar_move">Verschieben</string>
+    <string name="actionbar_move_or_copy">Verschieben oder kopieren</string>
     <string name="actionbar_open_with">Öffnen mit</string>
     <string name="actionbar_search">Suche</string>
     <string name="actionbar_see_details">Details</string>
@@ -324,6 +325,7 @@
     <string name="file_delete">Löschen</string>
     <string name="file_detail_activity_error">Fehler beim Abrufen der Aktivitäten für die Datei</string>
     <string name="file_details_no_content">Fehler beim Laden der Details</string>
+    <string name="file_downloader_notification_title_prefix">Herunterladen \u0020</string>
     <string name="file_icon">Datei</string>
     <string name="file_keep">Behalten</string>
     <string name="file_list_empty">Laden Sie Inhalt hoch oder synchronisieren Sie mit Ihren Geräten.</string>
@@ -392,6 +394,9 @@
     <string name="folder_confirm_create">Erstellen</string>
     <string name="folder_list_empty_headline">Keine Ordner vorhanden</string>
     <string name="folder_picker_choose_button_text">Auswählen</string>
+    <string name="folder_picker_choose_caption_text">Zielordner auswählen</string>
+    <string name="folder_picker_copy_button_text">Kopieren</string>
+    <string name="folder_picker_move_button_text">Verschieben</string>
     <string name="forbidden_permissions">Ihnen fehlt die Erlaubnis %s</string>
     <string name="forbidden_permissions_copy">diese Datei zu kopieren</string>
     <string name="forbidden_permissions_create">diese Datei zu erstellen</string>
@@ -942,6 +947,9 @@
     <string name="wait_a_moment">Bitte warten…</string>
     <string name="wait_checking_credentials">Überprüfe gespeicherte Anmeldeinformationen</string>
     <string name="wait_for_tmp_copy_from_private_storage">Kopiere Datei von privatem Speicher</string>
+    <string name="webview_version_check_alert_dialog_message">Bitte aktualisieren Sie die Android System WebView-App für eine Anmeldung</string>
+    <string name="webview_version_check_alert_dialog_positive_button_title">Aktualisieren</string>
+    <string name="webview_version_check_alert_dialog_title">Android System WebView aktualisieren</string>
     <string name="what_s_new_image">Was-gibt\'s-Neues-Bild</string>
     <string name="whats_new_skip">Überspringen</string>
     <string name="whats_new_title">Neu in %1$s</string>

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

@@ -390,6 +390,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>
@@ -916,6 +919,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>

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

@@ -279,6 +279,8 @@
     <string name="folder_confirm_create">Krei</string>
     <string name="folder_list_empty_headline">Neniu dosierujo ĉi tie</string>
     <string name="folder_picker_choose_button_text">Elekti</string>
+    <string name="folder_picker_choose_caption_text">Elekti celan dosierujon</string>
+    <string name="folder_picker_move_button_text">Movi</string>
     <string name="forbidden_permissions">Vi ne estas permesita %s</string>
     <string name="forbidden_permissions_copy">kopii ĉi tiun dosieron</string>
     <string name="forbidden_permissions_create">krei ĉi tiun dosieron</string>
@@ -660,6 +662,7 @@
     <string name="wait_a_moment">Atendu momenton...</string>
     <string name="wait_checking_credentials">Kontrolado de konservitaj akreditiloj</string>
     <string name="wait_for_tmp_copy_from_private_storage">Kopio de dosiero el malpublika konservejo</string>
+    <string name="webview_version_check_alert_dialog_positive_button_title">Ĝisdatigi</string>
     <string name="what_s_new_image">Bildo pri Kio nova</string>
     <string name="whats_new_skip">Preterpasi</string>
     <string name="whats_new_title">Novaĵoj en %1$s</string>

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

@@ -392,6 +392,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 le permite %s</string>
     <string name="forbidden_permissions_copy">para copiar este archivo</string>
     <string name="forbidden_permissions_create">para crear el archivo</string>
@@ -890,6 +892,7 @@
     <string name="wait_a_moment">Espere un momento…</string>
     <string name="wait_checking_credentials">Verificando credenciales almacenadas</string>
     <string name="wait_for_tmp_copy_from_private_storage">Copiando el archivo desde almacenamiento privado</string>
+    <string name="webview_version_check_alert_dialog_positive_button_title">Actualizar</string>
     <string name="what_s_new_image">Lo que es nueva imagen</string>
     <string name="whats_new_skip">Saltar</string>
     <string name="whats_new_title">Nuevo en %1$s</string>

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

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

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

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

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

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

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

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

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

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

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

@@ -392,6 +392,9 @@
     <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">Elegir</string>
+    <string name="folder_picker_choose_caption_text">Elegir carpeta de destino</string>
+    <string name="folder_picker_copy_button_text">Copia</string>
+    <string name="folder_picker_move_button_text">Mover</string>
     <string name="forbidden_permissions">No tienes permiso %s</string>
     <string name="forbidden_permissions_copy">copiar este archivo</string>
     <string name="forbidden_permissions_create">para crear este archivo</string>
@@ -941,6 +944,7 @@
     <string name="wait_a_moment">Espera un momento…</string>
     <string name="wait_checking_credentials">Comprobando las credenciales guardadas</string>
     <string name="wait_for_tmp_copy_from_private_storage">Copiando el archivo desde el almacenamiento privado.</string>
+    <string name="webview_version_check_alert_dialog_positive_button_title">Actualizar</string>
     <string name="what_s_new_image">Imagen de Qué hay de nuevo</string>
     <string name="whats_new_skip">Omitir</string>
     <string name="whats_new_title">Nuevo en %1$s</string>

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

@@ -18,6 +18,7 @@
     <string name="actionbar_copy">Copier</string>
     <string name="actionbar_mkdir">Nouveau dossier</string>
     <string name="actionbar_move">Déplacer</string>
+    <string name="actionbar_move_or_copy">Déplacer ou copier</string>
     <string name="actionbar_open_with">Ouvrir avec</string>
     <string name="actionbar_search">Rechercher</string>
     <string name="actionbar_see_details">Propriétés</string>
@@ -326,6 +327,7 @@ Attention, la suppression est irréversible.</string>
     <string name="file_delete">Supprimer</string>
     <string name="file_detail_activity_error">Erreur lors de la récupération de l’activité du fichier</string>
     <string name="file_details_no_content">Impossible de charger les détails</string>
+    <string name="file_downloader_notification_title_prefix">Téléchargement \u0020</string>
     <string name="file_icon">Fichier</string>
     <string name="file_keep">Conserver</string>
     <string name="file_list_empty">Déposez du contenu ou synchronisez vos appareils.</string>
@@ -441,7 +443,7 @@ Attention, la suppression est irréversible.</string>
     <string name="invalid_url">URL invalide</string>
     <string name="invisible">Invisible</string>
     <string name="label_empty">Le libellé ne peut pas être vide</string>
-    <string name="last_backup">Dernière sauvegarde: %1$s</string>
+    <string name="last_backup">Dernière sauvegarde : %1$s</string>
     <string name="link">Lien</string>
     <string name="link_name">Nom du lien</string>
     <string name="link_share_allow_upload_and_editing">Autoriser le téléversement et la modification</string>
@@ -946,6 +948,9 @@ Attention, la suppression est irréversible.</string>
     <string name="wait_a_moment">Veuillez patienter…</string>
     <string name="wait_checking_credentials">Vérification des identifiants enregistrés</string>
     <string name="wait_for_tmp_copy_from_private_storage">Copie du fichier depuis le stockage privé</string>
+    <string name="webview_version_check_alert_dialog_message">Veuillez mettre à jour l\'application Android System WebView pour vous connecter</string>
+    <string name="webview_version_check_alert_dialog_positive_button_title">Mise à jour</string>
+    <string name="webview_version_check_alert_dialog_title">Mettre à jour Android System WebView</string>
     <string name="what_s_new_image">Image quoi de neuf</string>
     <string name="whats_new_skip">Ignorer</string>
     <string name="whats_new_title">Nouveautés dans %1$s</string>

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

@@ -18,6 +18,7 @@
     <string name="actionbar_copy">Копировать</string>
     <string name="actionbar_mkdir">Создать папку</string>
     <string name="actionbar_move">Переместить</string>
+    <string name="actionbar_move_or_copy">Переместить или скопировать</string>
     <string name="actionbar_open_with">Открыть с помощью</string>
     <string name="actionbar_search">Поиск</string>
     <string name="actionbar_see_details">Подробности</string>
@@ -324,6 +325,7 @@
     <string name="file_delete">Удалить</string>
     <string name="file_detail_activity_error">Ошибка получения истории событий, связанных с файлом</string>
     <string name="file_details_no_content">Не удалось получить подробные сведения</string>
+    <string name="file_downloader_notification_title_prefix">Скачивание \u0020</string>
     <string name="file_icon">Файл</string>
     <string name="file_keep">Сохранить</string>
     <string name="file_list_empty">Добавьте что-нибудь или синхронизируйте со своими устройствами!</string>
@@ -945,7 +947,9 @@
     <string name="wait_a_moment">Подождите немного…</string>
     <string name="wait_checking_credentials">Проверка сохранённых реквизитов учётных данных</string>
     <string name="wait_for_tmp_copy_from_private_storage">Копирование файла из частного хранилища</string>
+    <string name="webview_version_check_alert_dialog_message">Обновите приложение Android System WebView для входа в систему</string>
     <string name="webview_version_check_alert_dialog_positive_button_title">Изменение</string>
+    <string name="webview_version_check_alert_dialog_title">Обновите приложение Android System WebView</string>
     <string name="what_s_new_image">Изображение «что нового»</string>
     <string name="whats_new_skip">Пропустить</string>
     <string name="whats_new_title">Новое в %1$s</string>

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

@@ -18,6 +18,7 @@
     <string name="actionbar_copy">Копирај</string>
     <string name="actionbar_mkdir">Нова фасцикла</string>
     <string name="actionbar_move">Премести</string>
+    <string name="actionbar_move_or_copy">Премести или копирај</string>
     <string name="actionbar_open_with">Отвори помоћу</string>
     <string name="actionbar_search">Претрага</string>
     <string name="actionbar_see_details">Детаљи</string>
@@ -324,6 +325,7 @@
     <string name="file_delete">Обриши</string>
     <string name="file_detail_activity_error">Грешка при добављању активности за фајл</string>
     <string name="file_details_no_content">Грешка при учитавању детаља</string>
+    <string name="file_downloader_notification_title_prefix">Преузима се \u0020</string>
     <string name="file_icon">Фајл</string>
     <string name="file_keep">Задржи</string>
     <string name="file_list_empty">Отпремите неки садржај или синхронизујте са вашим уређајима.</string>
@@ -944,7 +946,9 @@
     <string name="wait_a_moment">Сачекајте мало…</string>
     <string name="wait_checking_credentials">Проверавам сачуване акредитиве</string>
     <string name="wait_for_tmp_copy_from_private_storage">Копирам фајл из личног складишта</string>
+    <string name="webview_version_check_alert_dialog_message">Да бисте се пријавили, молимо вас да ажурирате Андроид апликацију System WebView</string>
     <string name="webview_version_check_alert_dialog_positive_button_title">Ажурирај</string>
+    <string name="webview_version_check_alert_dialog_title">Ажурирај Андроид System WebView</string>
     <string name="what_s_new_image">Слика шта је ново</string>
     <string name="whats_new_skip">Прескочи</string>
     <string name="whats_new_title">Ново у %1$s</string>

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

@@ -18,6 +18,7 @@
     <string name="actionbar_copy">Kopiera</string>
     <string name="actionbar_mkdir">Ny mapp</string>
     <string name="actionbar_move">Flytta</string>
+    <string name="actionbar_move_or_copy">Flytta eller kopiera</string>
     <string name="actionbar_open_with">Öppna med</string>
     <string name="actionbar_search">Sök</string>
     <string name="actionbar_see_details">Detaljer</string>
@@ -324,6 +325,7 @@
     <string name="file_delete">Ta bort</string>
     <string name="file_detail_activity_error">Fel vid hämtning av aktiviteter för fil</string>
     <string name="file_details_no_content">Kunde inte läsa in detaljer</string>
+    <string name="file_downloader_notification_title_prefix">Laddar ner \u0020</string>
     <string name="file_icon">Fil</string>
     <string name="file_keep">Behåll</string>
     <string name="file_list_empty">Ladda upp något eller synkronisera med dina enheter</string>
@@ -617,6 +619,7 @@
     <string name="prefs_remove_e2e">Ta bort kryptering lokalt</string>
     <string name="prefs_setup_e2e">Sätt upp end-to-end-kryptering</string>
     <string name="prefs_show_ecosystem_apps">Visa appväxlare</string>
+    <string name="prefs_show_ecosystem_apps_summary">Nextcloud-appförslag i navigeringsrubriken</string>
     <string name="prefs_show_hidden_files">Visa dolda filer</string>
     <string name="prefs_sourcecode">Hämta källkod</string>
     <string name="prefs_storage_path">Datalagringsmapp</string>
@@ -944,7 +947,9 @@
     <string name="wait_a_moment">Vänligen vänta…</string>
     <string name="wait_checking_credentials">Kontrollerar lagrade inloggningsuppgifter</string>
     <string name="wait_for_tmp_copy_from_private_storage">Kopierar fil från privat lagring</string>
+    <string name="webview_version_check_alert_dialog_message">Uppdatera Android System WebView-appen för inloggning</string>
     <string name="webview_version_check_alert_dialog_positive_button_title">Uppdatera</string>
+    <string name="webview_version_check_alert_dialog_title">Uppdatera Android System WebView</string>
     <string name="what_s_new_image">Vad är nytt-bild</string>
     <string name="whats_new_skip">Hoppa över</string>
     <string name="whats_new_title">Ny i %1$s</string>

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

@@ -18,6 +18,7 @@
     <string name="actionbar_copy">Kopyala</string>
     <string name="actionbar_mkdir">Yeni klasör</string>
     <string name="actionbar_move">Taşı</string>
+    <string name="actionbar_move_or_copy">Taşı ya da kopyala</string>
     <string name="actionbar_open_with">Birlikte aç</string>
     <string name="actionbar_search">Arama</string>
     <string name="actionbar_see_details">Ayrıntılar</string>
@@ -324,6 +325,7 @@
     <string name="file_delete">Sil</string>
     <string name="file_detail_activity_error">Dosya işlemleri alınırken sorun çıktı</string>
     <string name="file_details_no_content">Ayrıntılar yüklenemedi</string>
+    <string name="file_downloader_notification_title_prefix">İndiriliyor \u0020</string>
     <string name="file_icon">Dosya</string>
     <string name="file_keep">Tut</string>
     <string name="file_list_empty">Bazı içerikler yükleyin ya da aygıtlarınızla eşitleyin.</string>
@@ -944,7 +946,9 @@
     <string name="wait_a_moment">Lütfen bekleyin …</string>
     <string name="wait_checking_credentials">Kayıtlı kimlik bilgileri denetleniyor</string>
     <string name="wait_for_tmp_copy_from_private_storage">Dosya kişisel depolamadan kopyalanıyor</string>
+    <string name="webview_version_check_alert_dialog_message">Lütfen oturum açmak için Android Sistem Web Görünümü uygulamasını güncelleyin</string>
     <string name="webview_version_check_alert_dialog_positive_button_title">Güncelle</string>
+    <string name="webview_version_check_alert_dialog_title">Android Sistem Web Görünümü uygulamasını güncelle</string>
     <string name="what_s_new_image">Yenilikler görseli</string>
     <string name="whats_new_skip">Atla</string>
     <string name="whats_new_title">%1$s yenilikleri</string>

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

@@ -18,6 +18,7 @@
     <string name="actionbar_copy">复制</string>
     <string name="actionbar_mkdir">新建文件夹</string>
     <string name="actionbar_move">移动</string>
+    <string name="actionbar_move_or_copy">移动或复制</string>
     <string name="actionbar_open_with">打开方式</string>
     <string name="actionbar_search">搜索</string>
     <string name="actionbar_see_details">详细信息</string>
@@ -324,6 +325,7 @@
     <string name="file_delete">删除</string>
     <string name="file_detail_activity_error">获取文件动态时出错</string>
     <string name="file_details_no_content">加载详情失败</string>
+    <string name="file_downloader_notification_title_prefix">下载中 \u0020</string>
     <string name="file_icon">文件</string>
     <string name="file_keep">保留</string>
     <string name="file_list_empty">上传一些内容或与您的设备同步。</string>
@@ -947,7 +949,9 @@
     <string name="wait_a_moment">请稍等…</string>
     <string name="wait_checking_credentials">正在检查保存的证书</string>
     <string name="wait_for_tmp_copy_from_private_storage">正在从私有存储中复制文件</string>
+    <string name="webview_version_check_alert_dialog_message">请更新 Android System WebView 应用程序以进行登录</string>
     <string name="webview_version_check_alert_dialog_positive_button_title">更新</string>
+    <string name="webview_version_check_alert_dialog_title">更新 Android System WebView</string>
     <string name="what_s_new_image">有什么新图片</string>
     <string name="whats_new_skip">跳过</string>
     <string name="whats_new_title">新建%1$s</string>

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

@@ -18,6 +18,7 @@
     <string name="actionbar_copy">複製</string>
     <string name="actionbar_mkdir">新資料夾</string>
     <string name="actionbar_move">移動</string>
+    <string name="actionbar_move_or_copy">移動或複製</string>
     <string name="actionbar_open_with">開啟方式</string>
     <string name="actionbar_search">搜尋</string>
     <string name="actionbar_see_details">詳細資料</string>
@@ -324,6 +325,7 @@
     <string name="file_delete">刪除</string>
     <string name="file_detail_activity_error">取得檔案活動時發生錯誤</string>
     <string name="file_details_no_content">載入詳細資訊失敗</string>
+    <string name="file_downloader_notification_title_prefix">正在下載 \u0020</string>
     <string name="file_icon">檔案</string>
     <string name="file_keep">保留</string>
     <string name="file_list_empty">上傳一些內容或與您的裝置同步。</string>
@@ -944,7 +946,9 @@
     <string name="wait_a_moment">稍候……</string>
     <string name="wait_checking_credentials">正在檢查儲存的憑證</string>
     <string name="wait_for_tmp_copy_from_private_storage">從私有儲存空間複製檔案中</string>
+    <string name="webview_version_check_alert_dialog_message">請更新 Android System WebView 應用程式以進行登入</string>
     <string name="webview_version_check_alert_dialog_positive_button_title">更新</string>
+    <string name="webview_version_check_alert_dialog_title">更新 Android System WebView</string>
     <string name="what_s_new_image">有什麼新圖片?</string>
     <string name="whats_new_skip">略過</string>
     <string name="whats_new_title">新增到 %1$s </string>

+ 2 - 4
app/src/main/res/values/dims.xml

@@ -23,7 +23,6 @@
     <dimen name="bottom_sheet_text_start_margin">40dp</dimen>
     <dimen name="bottom_sheet_item_height">56dp</dimen>
     <dimen name="bottom_sheet_menu_item_divider_standard_margin">80dp</dimen>
-    <dimen name="bottom_sheet_min_height">112dp</dimen>
     <dimen name="file_icon_size">40dp</dimen>
     <dimen name="file_icon_size_grid">128dp</dimen>
     <dimen name="file_icon_rounded_corner_radius">8dp</dimen>
@@ -40,6 +39,8 @@
     <dimen name="standard_double_margin">32dp</dimen>
     <dimen name="standard_half_margin">8dp</dimen>
     <dimen name="standard_quarter_margin">4dp</dimen>
+    <dimen name="button_width">140dp</dimen>
+    <dimen name="button_extra_width">180dp</dimen>
     <dimen name="standard_eighth_margin">2dp</dimen>
     <dimen name="min_list_item_size">56dp</dimen>
     <dimen name="standard_list_item_size">72dp</dimen>
@@ -122,9 +123,6 @@
     <dimen name="preview_error_image_layout_width">72dp</dimen>
     <dimen name="preview_error_image_layout_height">72dp</dimen>
     <dimen name="preview_error_text_size">26sp</dimen>
-    <dimen name="send_share_fragment_layout_margin">24dp</dimen>
-    <dimen name="send_share_fragment_layout_padding">24dp</dimen>
-    <dimen name="share_people_icon_layout_padding">12dp</dimen>
     <dimen name="synced_folders_item_type_layout_width">32dp</dimen>
     <dimen name="synced_folders_item_type_layout_height">32dp</dimen>
     <dimen name="synced_folders_item_type_layout_right_end_margin">24dp</dimen>