chromium/chrome/browser/password_entry_edit/android/internal/java/src/org/chromium/chrome/browser/password_entry_edit/CredentialEditMediator.java

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.password_entry_edit;

import static org.chromium.chrome.browser.password_entry_edit.CredentialEditMediator.CredentialEditError.DUPLICATE_USERNAME;
import static org.chromium.chrome.browser.password_entry_edit.CredentialEditMediator.CredentialEditError.EMPTY_PASSWORD;
import static org.chromium.chrome.browser.password_entry_edit.CredentialEditMediator.CredentialEditError.ERROR_COUNT;
import static org.chromium.chrome.browser.password_entry_edit.CredentialEditMediator.CredentialEntryAction.ACTION_COUNT;
import static org.chromium.chrome.browser.password_entry_edit.CredentialEditMediator.CredentialEntryAction.COPIED_PASSWORD;
import static org.chromium.chrome.browser.password_entry_edit.CredentialEditMediator.CredentialEntryAction.COPIED_USERNAME;
import static org.chromium.chrome.browser.password_entry_edit.CredentialEditMediator.CredentialEntryAction.DELETED;
import static org.chromium.chrome.browser.password_entry_edit.CredentialEditMediator.CredentialEntryAction.EDITED_PASSWORD;
import static org.chromium.chrome.browser.password_entry_edit.CredentialEditMediator.CredentialEntryAction.EDITED_USERNAME;
import static org.chromium.chrome.browser.password_entry_edit.CredentialEditMediator.CredentialEntryAction.EDITED_USERNAME_AND_PASSWORD;
import static org.chromium.chrome.browser.password_entry_edit.CredentialEditMediator.CredentialEntryAction.MASKED_PASSWORD;
import static org.chromium.chrome.browser.password_entry_edit.CredentialEditMediator.CredentialEntryAction.UNMASKED_PASSWORD;
import static org.chromium.chrome.browser.password_entry_edit.CredentialEditProperties.DUPLICATE_USERNAME_ERROR;
import static org.chromium.chrome.browser.password_entry_edit.CredentialEditProperties.EMPTY_PASSWORD_ERROR;
import static org.chromium.chrome.browser.password_entry_edit.CredentialEditProperties.FEDERATION_ORIGIN;
import static org.chromium.chrome.browser.password_entry_edit.CredentialEditProperties.PASSWORD;
import static org.chromium.chrome.browser.password_entry_edit.CredentialEditProperties.PASSWORD_VISIBLE;
import static org.chromium.chrome.browser.password_entry_edit.CredentialEditProperties.UI_DISMISSED_BY_NATIVE;
import static org.chromium.chrome.browser.password_entry_edit.CredentialEditProperties.URL_OR_APP;
import static org.chromium.chrome.browser.password_entry_edit.CredentialEditProperties.USERNAME;

import android.content.Context;
import android.content.res.Resources;

import androidx.annotation.IntDef;

import org.chromium.base.Callback;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.chrome.browser.password_entry_edit.CredentialEditCoordinator.CredentialActionDelegate;
import org.chromium.chrome.browser.password_entry_edit.CredentialEntryFragmentViewBase.UiActionHandler;
import org.chromium.chrome.browser.password_manager.ConfirmationDialogHelper;
import org.chromium.chrome.browser.password_manager.settings.PasswordAccessReauthenticationHelper;
import org.chromium.chrome.browser.password_manager.settings.PasswordAccessReauthenticationHelper.ReauthReason;
import org.chromium.ui.base.Clipboard;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.widget.Toast;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

/**
 * Contains the logic for the edit component. It  updates the model when needed and reacts to UI
 * events (e.g. button clicks).
 */
public class CredentialEditMediator implements UiActionHandler {
    static final String SAVED_PASSWORD_ACTION_HISTOGRAM =
            "PasswordManager.CredentialEntryActions.SavedPassword";
    static final String FEDERATED_CREDENTIAL_ACTION_HISTOGRAM =
            "PasswordManager.CredentialEntryActions.FederatedCredential";
    static final String BLOCKED_CREDENTIAL_ACTION_HISTOGRAM =
            "PasswordManager.CredentialEntryActions.BlockedCredential";
    private final PasswordAccessReauthenticationHelper mReauthenticationHelper;
    private final ConfirmationDialogHelper mDeleteDialogHelper;
    private final CredentialActionDelegate mCredentialActionDelegate;
    private final Runnable mHelpLauncher;
    private final boolean mIsBlockedCredential;
    private PropertyModel mModel;
    private String mOriginalUsername;
    private String mOriginalPassword;
    private boolean mIsInsecureCredential;
    private Set<String> mExistingUsernames;

    /**
     * The action that the user takes within the credential entry UI.
     *
     * These values are persisted to logs. Entries should not be renumbered and
     * numeric values should never be reused.
     */
    @IntDef({
        DELETED,
        COPIED_USERNAME,
        UNMASKED_PASSWORD,
        MASKED_PASSWORD,
        COPIED_PASSWORD,
        EDITED_USERNAME,
        EDITED_PASSWORD,
        EDITED_USERNAME_AND_PASSWORD,
        ACTION_COUNT
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface CredentialEntryAction {
        /**
         * The credential entry was deleted. Recorded after the user confirms it, when a
         * confirmation dialog is prompted.
         */
        int DELETED = 0;

        /** The username was copied. */
        int COPIED_USERNAME = 1;

        /** The password was unmasked. Recorded after successful reauth is one was performed. */
        int UNMASKED_PASSWORD = 2;

        /** The password was masked. */
        int MASKED_PASSWORD = 3;

        /** The password was copied. Recorded after successful reauth is one was performed. */
        int COPIED_PASSWORD = 4;

        /** The username was edited. Recorded after the user presses the save button". */
        int EDITED_USERNAME = 5;

        /** The password was edited. Recorded after the user presses the save button". */
        int EDITED_PASSWORD = 6;

        /**
         * Both username and password were edited. Recorded after the user presses the save button".
         */
        int EDITED_USERNAME_AND_PASSWORD = 7;

        int ACTION_COUNT = 8;
    }

    /**
     *  The error displayed in the UI while the user is editing a credential.
     *
     *  These values are persisted to logs. Entries should not be renumbered and
     *  numeric values should never be reused.
     */
    @IntDef({EMPTY_PASSWORD, DUPLICATE_USERNAME, ERROR_COUNT})
    @Retention(RetentionPolicy.SOURCE)
    public @interface CredentialEditError {
        /** The password field is empty. */
        int EMPTY_PASSWORD = 0;

        /** The username in the username field is already saved for this site/app. */
        int DUPLICATE_USERNAME = 1;

        int ERROR_COUNT = 2;
    }

    CredentialEditMediator(
            PasswordAccessReauthenticationHelper reauthenticationHelper,
            ConfirmationDialogHelper deleteDialogHelper,
            CredentialActionDelegate credentialActionDelegate,
            Runnable helpLauncher,
            boolean isBlockedCredential) {
        mReauthenticationHelper = reauthenticationHelper;
        mDeleteDialogHelper = deleteDialogHelper;
        mCredentialActionDelegate = credentialActionDelegate;
        mHelpLauncher = helpLauncher;
        mIsBlockedCredential = isBlockedCredential;
    }

    void initialize(PropertyModel model) {
        mModel = model;
    }

    void setCredential(String username, String password, boolean isInsecureCredential) {
        mOriginalUsername = username;
        mOriginalPassword = password;
        mIsInsecureCredential = isInsecureCredential;

        mModel.set(USERNAME, username);
        mModel.set(PASSWORD_VISIBLE, false);
        mModel.set(PASSWORD, password);
    }

    void setExistingUsernames(String[] existingUsernames) {
        mExistingUsernames = new HashSet<>(Arrays.asList(existingUsernames));
    }

    void dismiss() {
        mModel.set(UI_DISMISSED_BY_NATIVE, true);
    }

    @Override
    public void onMaskOrUnmaskPassword() {
        if (mModel.get(PASSWORD_VISIBLE)) {
            RecordHistogram.recordEnumeratedHistogram(
                    SAVED_PASSWORD_ACTION_HISTOGRAM, MASKED_PASSWORD, ACTION_COUNT);
            mModel.set(PASSWORD_VISIBLE, false);
            return;
        }
        reauthenticateUser(
                ReauthReason.VIEW_PASSWORD,
                (reauthSucceeded) -> {
                    if (!reauthSucceeded) return;
                    RecordHistogram.recordEnumeratedHistogram(
                            SAVED_PASSWORD_ACTION_HISTOGRAM, UNMASKED_PASSWORD, ACTION_COUNT);
                    mModel.set(PASSWORD_VISIBLE, true);
                });
    }

    @Override
    public void onSave() {
        recordSavedEdit();
        mCredentialActionDelegate.saveChanges(mModel.get(USERNAME), mModel.get(PASSWORD));
    }

    @Override
    public void onUsernameTextChanged(String username) {
        mModel.set(USERNAME, username);
        boolean hasError =
                !mOriginalUsername.equals(username) && mExistingUsernames.contains(username);
        mModel.set(DUPLICATE_USERNAME_ERROR, hasError);
    }

    @Override
    public void onPasswordTextChanged(String password) {
        mModel.set(PASSWORD, password);
        mModel.set(EMPTY_PASSWORD_ERROR, password.isEmpty());
    }

    @Override
    public void onCopyUsername(Context context) {
        recordUsernameCopied();
        Clipboard.getInstance().setText("username", mModel.get(USERNAME));
        Toast.makeText(
                        context,
                        R.string.password_entry_viewer_username_copied_into_clipboard,
                        Toast.LENGTH_SHORT)
                .show();
    }

    @Override
    public void onDelete() {
        if (mIsBlockedCredential) {
            recordDeleted();
            mCredentialActionDelegate.deleteCredential();
            return;
        }
        Resources resources = mDeleteDialogHelper.getResources();
        if (resources == null) return;
        String title =
                resources.getString(R.string.password_entry_edit_delete_credential_dialog_title);
        String message =
                resources.getString(
                        mIsInsecureCredential
                                ? R.string.password_check_delete_credential_dialog_body
                                : R.string.password_entry_edit_deletion_dialog_body,
                        mModel.get(URL_OR_APP));
        mDeleteDialogHelper.showConfirmation(
                title,
                message,
                R.string.password_entry_edit_delete_credential_dialog_confirm,
                () -> {
                    recordDeleted();
                    mCredentialActionDelegate.deleteCredential();
                });
    }

    @Override
    public void handleHelp() {
        mHelpLauncher.run();
    }

    @Override
    public void onCopyPassword(Context context) {
        reauthenticateUser(
                ReauthReason.COPY_PASSWORD,
                (reauthSucceeded) -> {
                    if (!reauthSucceeded) return;
                    RecordHistogram.recordEnumeratedHistogram(
                            SAVED_PASSWORD_ACTION_HISTOGRAM, COPIED_PASSWORD, ACTION_COUNT);
                    Clipboard.getInstance().setPassword(mModel.get(PASSWORD));
                    Toast.makeText(
                                    context,
                                    R.string.password_entry_viewer_password_copied_into_clipboard,
                                    Toast.LENGTH_SHORT)
                            .show();
                });
    }

    private void reauthenticateUser(@ReauthReason int reason, Callback<Boolean> action) {
        if (!mReauthenticationHelper.canReauthenticate()) {
            mReauthenticationHelper.showScreenLockToast(reason);
            return;
        }
        mReauthenticationHelper.reauthenticate(reason, action);
    }

    private void recordUsernameCopied() {
        String histogram =
                mModel.get(FEDERATION_ORIGIN).isEmpty()
                        ? SAVED_PASSWORD_ACTION_HISTOGRAM
                        : FEDERATED_CREDENTIAL_ACTION_HISTOGRAM;
        RecordHistogram.recordEnumeratedHistogram(histogram, COPIED_USERNAME, ACTION_COUNT);
    }

    private void recordDeleted() {
        String histogram = SAVED_PASSWORD_ACTION_HISTOGRAM;
        if (mIsBlockedCredential) {
            histogram = BLOCKED_CREDENTIAL_ACTION_HISTOGRAM;
        } else if (!mModel.get(FEDERATION_ORIGIN).isEmpty()) {
            histogram = FEDERATED_CREDENTIAL_ACTION_HISTOGRAM;
        }
        RecordHistogram.recordEnumeratedHistogram(histogram, DELETED, ACTION_COUNT);
    }

    private void recordSavedEdit() {
        boolean changedUsername = !mModel.get(USERNAME).equals(mOriginalUsername);
        boolean changedPassword = !mModel.get(PASSWORD).equals(mOriginalPassword);
        if (changedUsername && changedPassword) {
            RecordHistogram.recordEnumeratedHistogram(
                    SAVED_PASSWORD_ACTION_HISTOGRAM, EDITED_USERNAME_AND_PASSWORD, ACTION_COUNT);
            return;
        }

        if (changedUsername) {
            RecordHistogram.recordEnumeratedHistogram(
                    SAVED_PASSWORD_ACTION_HISTOGRAM, EDITED_USERNAME, ACTION_COUNT);
            return;
        }

        if (changedPassword) {
            RecordHistogram.recordEnumeratedHistogram(
                    SAVED_PASSWORD_ACTION_HISTOGRAM, EDITED_PASSWORD, ACTION_COUNT);
        }
    }
}