diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index a13d6f9ee4..28e1953d67 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -1,4 +1,4 @@
{
"name": "NextcloudAndroid",
- "dockerFile": "Dockerfile",
+ "dockerFile": "Dockerfile"
}
diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithOneAction.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithOneAction.png
new file mode 100644
index 0000000000..3977140e8e
Binary files /dev/null and b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithOneAction.png differ
diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithThreeAction.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithThreeAction.png
new file mode 100644
index 0000000000..1ffb0f987e
Binary files /dev/null and b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithThreeAction.png differ
diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithThreeActionRTL.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithThreeActionRTL.png
new file mode 100644
index 0000000000..365aed3a29
Binary files /dev/null and b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithThreeActionRTL.png differ
diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithTwoAction.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithTwoAction.png
new file mode 100644
index 0000000000..e9fd77fa38
Binary files /dev/null and b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithTwoAction.png differ
diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testFileActionsBottomSheet.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testFileActionsBottomSheet.png
index ab19cd1bc5..72d4ba359f 100644
Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testFileActionsBottomSheet.png and b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testFileActionsBottomSheet.png differ
diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testLoadingDialog.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testLoadingDialog.png
index 509802a905..237dcd156a 100644
Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testLoadingDialog.png and b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testLoadingDialog.png differ
diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialogDifferentTypes_Screenshot.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialogDifferentTypes_Screenshot.png
index 6b0f9ad6ce..f8d7757680 100644
Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialogDifferentTypes_Screenshot.png and b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialogDifferentTypes_Screenshot.png differ
diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialog_Screenshot.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialog_Screenshot.png
index 6b0f9ad6ce..f8d7757680 100644
Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialog_Screenshot.png and b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialog_Screenshot.png differ
diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendShareDialogTest_showDialog.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendShareDialogTest_showDialog.png
index 172bbde365..188f61ca86 100644
Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendShareDialogTest_showDialog.png and b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendShareDialogTest_showDialog.png differ
diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_error.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_error.png
index 857de92bec..2ced2a1665 100644
Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_error.png and b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_error.png differ
diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_showMnemonic.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_showMnemonic.png
index 7b5befd0c5..75c1bdc09f 100644
Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_showMnemonic.png and b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_showMnemonic.png differ
diff --git a/app/src/androidTest/java/com/owncloud/android/AbstractIT.java b/app/src/androidTest/java/com/owncloud/android/AbstractIT.java
index 98fe9c75e5..f3c6e4c764 100644
--- a/app/src/androidTest/java/com/owncloud/android/AbstractIT.java
+++ b/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) {
diff --git a/app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.java b/app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.java
index 3853215b77..beefce3cda 100644
--- a/app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.java
+++ b/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() {
diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/LoadingDialog.kt b/app/src/main/java/com/owncloud/android/ui/dialog/LoadingDialog.kt
index 5383c95dac..a1854907e9 100644
--- a/app/src/main/java/com/owncloud/android/ui/dialog/LoadingDialog.kt
+++ b/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
}
diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/SetupEncryptionDialogFragment.java b/app/src/main/java/com/owncloud/android/ui/dialog/SetupEncryptionDialogFragment.java
deleted file mode 100644
index 634cebb63f..0000000000
--- a/app/src/main/java/com/owncloud/android/ui/dialog/SetupEncryptionDialogFragment.java
+++ /dev/null
@@ -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 .
- */
-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 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 {
- private final WeakReference 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 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 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 {
-
- private final WeakReference 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 keyWords) {
- this.keyWords = keyWords;
- }
-}
diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/SetupEncryptionDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/SetupEncryptionDialogFragment.kt
new file mode 100644
index 0000000000..c78f4ed771
--- /dev/null
+++ b/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 .
+ */
+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? = 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() {
+ private val mWeakContext: WeakReference
+
+ 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() {
+ private val mWeakContext: WeakReference
+
+ 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?) {
+ 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
+ }
+ }
+}
diff --git a/app/src/main/res/layout/setup_encryption_dialog.xml b/app/src/main/res/layout/setup_encryption_dialog.xml
index 988fbc7c16..733b7a8b14 100644
--- a/app/src/main/res/layout/setup_encryption_dialog.xml
+++ b/app/src/main/res/layout/setup_encryption_dialog.xml
@@ -26,14 +26,14 @@
android:orientation="vertical"
android:padding="@dimen/dialog_padding">
-
-
URL invalide
Invisible
Le libellé ne peut pas être vide
- Dernière sauvegarde: %1$s
+ Dernière sauvegarde : %1$s
Lien
Nom du lien
Autoriser le téléversement et la modification
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 32f36352e9..8a2916edba 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -18,6 +18,7 @@
复制
新建文件夹
移动
+ 移动或复制
打开方式
搜索
详细信息
@@ -324,6 +325,7 @@
删除
获取文件动态时出错
加载详情失败
+ 下载中 \u0020
文件
保留
上传一些内容或与您的设备同步。
@@ -947,7 +949,9 @@
请稍等…
正在检查保存的证书
正在从私有存储中复制文件
+ 请更新 Android System WebView 应用程序以进行登录
更新
+ 更新 Android System WebView
有什么新图片
跳过
新建%1$s
diff --git a/scripts/analysis/lint-results.txt b/scripts/analysis/lint-results.txt
index 7fa79531c2..aefe635326 100644
--- a/scripts/analysis/lint-results.txt
+++ b/scripts/analysis/lint-results.txt
@@ -1,2 +1,2 @@
DO NOT TOUCH; GENERATED BY DRONE
- Lint Report: 78 warnings
+ Lint Report: 77 warnings