浏览代码

Merge pull request #12077 from nextcloud/feature/use-m3-SetupEncryptionDialogFragment

Use Material Design 3 For Setup Encryption Dialog Fragment
Andy Scherzinger 1 年之前
父节点
当前提交
708e75bd1e
共有 18 个文件被更改,包括 572 次插入562 次删除
  1. 二进制
      app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithOneAction.png
  2. 二进制
      app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithThreeAction.png
  3. 二进制
      app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithThreeActionRTL.png
  4. 二进制
      app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithTwoAction.png
  5. 二进制
      app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testFileActionsBottomSheet.png
  6. 二进制
      app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testLoadingDialog.png
  7. 二进制
      app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialogDifferentTypes_Screenshot.png
  8. 二进制
      app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialog_Screenshot.png
  9. 二进制
      app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendShareDialogTest_showDialog.png
  10. 二进制
      app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_error.png
  11. 二进制
      app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_showMnemonic.png
  12. 4 3
      app/src/androidTest/java/com/owncloud/android/AbstractIT.java
  13. 1 4
      app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.java
  14. 1 1
      app/src/main/java/com/owncloud/android/ui/dialog/LoadingDialog.kt
  15. 0 551
      app/src/main/java/com/owncloud/android/ui/dialog/SetupEncryptionDialogFragment.java
  16. 563 0
      app/src/main/java/com/owncloud/android/ui/dialog/SetupEncryptionDialogFragment.kt
  17. 2 2
      app/src/main/res/layout/setup_encryption_dialog.xml
  18. 1 1
      scripts/analysis/lint-results.txt

二进制
app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithOneAction.png


二进制
app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithThreeAction.png


二进制
app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithThreeActionRTL.png


二进制
app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithTwoAction.png


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


二进制
app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testLoadingDialog.png


二进制
app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialogDifferentTypes_Screenshot.png


二进制
app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialog_Screenshot.png


二进制
app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendShareDialogTest_showDialog.png


二进制
app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_error.png


二进制
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() {

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

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

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