chromium/chrome/android/java/src/org/chromium/chrome/browser/autofill/settings/AutofillPaymentMethodsFragment.java

// Copyright 2017 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.autofill.settings;

import static org.chromium.chrome.browser.autofill.AutofillUiUtils.getCardIcon;

import android.content.Context;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.hardware.biometrics.BiometricManager;
import android.os.Build;
import android.os.Bundle;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.style.ForegroundColorSpan;
import android.util.Pair;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;

import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.core.hardware.fingerprint.FingerprintManagerCompat;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;

import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.BuildInfo;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.autofill.AutofillEditorBase;
import org.chromium.chrome.browser.autofill.AutofillUiUtils;
import org.chromium.chrome.browser.autofill.PersonalDataManager;
import org.chromium.chrome.browser.autofill.PersonalDataManager.CreditCard;
import org.chromium.chrome.browser.autofill.PersonalDataManager.Iban;
import org.chromium.chrome.browser.autofill.PersonalDataManagerFactory;
import org.chromium.chrome.browser.device_reauth.DeviceAuthSource;
import org.chromium.chrome.browser.device_reauth.ReauthenticatorBridge;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.payments.ServiceWorkerPaymentAppBridge;
import org.chromium.chrome.browser.settings.ChromeBaseSettingsFragment;
import org.chromium.chrome.browser.settings.ChromeManagedPreferenceDelegate;
import org.chromium.chrome.browser.settings.SettingsLauncherFactory;
import org.chromium.components.autofill.MandatoryReauthAuthenticationFlowEvent;
import org.chromium.components.autofill.VirtualCardEnrollmentState;
import org.chromium.components.browser_ui.modaldialog.AppModalPresenter;
import org.chromium.components.browser_ui.settings.ChromeBasePreference;
import org.chromium.components.browser_ui.settings.ChromeSwitchPreference;
import org.chromium.components.browser_ui.settings.SettingsLauncher;
import org.chromium.components.browser_ui.styles.SemanticColorUtils;
import org.chromium.components.payments.AndroidPaymentAppFactory;
import org.chromium.ui.modaldialog.ModalDialogManager;
import org.chromium.ui.modaldialog.ModalDialogManager.ModalDialogType;

/**
 * Autofill credit cards fragment, which allows the user to edit credit cards and control payment
 * apps.
 */
public class AutofillPaymentMethodsFragment extends ChromeBaseSettingsFragment
        implements PersonalDataManager.PersonalDataManagerObserver {
    // The Fido pref is used as a key on the settings toggle. This key helps in the retrieval of the
    // Fido toggle during tests.
    static final String PREF_FIDO = "fido";
    static final String PREF_DELETE_SAVED_CVCS = "delete_saved_cvcs";
    static final String PREF_MANDATORY_REAUTH = "mandatory_reauth";
    static final String PREF_SAVE_CVC = "save_cvc";
    static final String PREF_ADD_IBAN = "add_iban";
    static final String PREF_IBAN = "iban";
    static final String PREF_CARD_BENEFITS = "card_benefits";
    private static final String PREF_PAYMENT_APPS = "payment_apps";

    @VisibleForTesting
    static final String PREF_FINANCIAL_ACCOUNTS_MANAGEMENT = "financial_accounts_management";

    static final String MANDATORY_REAUTH_EDIT_CARD_HISTOGRAM =
            "Autofill.PaymentMethods.MandatoryReauth.AuthEvent.SettingsPage.EditCard";
    static final String MANDATORY_REAUTH_OPT_IN_HISTOGRAM =
            "Autofill.PaymentMethods.MandatoryReauth.OptChangeEvent.SettingsPage.OptIn";
    static final String MANDATORY_REAUTH_OPT_OUT_HISTOGRAM =
            "Autofill.PaymentMethods.MandatoryReauth.OptChangeEvent.SettingsPage.OptOut";

    @Nullable private ReauthenticatorBridge mReauthenticatorBridge;
    @Nullable private AutofillPaymentMethodsDelegate mAutofillPaymentMethodsDelegate;
    private final ObservableSupplierImpl<String> mPageTitle = new ObservableSupplierImpl<>();

    @Override
    public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
        mPageTitle.set(getString(R.string.autofill_payment_methods));
        setHasOptionsMenu(true);
        PreferenceScreen screen = getPreferenceManager().createPreferenceScreen(getStyledContext());
        // Suppresses unwanted animations while Preferences are removed from and re-added to the
        // screen.
        screen.setShouldUseGeneratedIds(false);

        setPreferenceScreen(screen);
    }

    @Override
    public ObservableSupplier<String> getPageTitle() {
        return mPageTitle;
    }

    @Override
    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        menu.clear();
        MenuItem help =
                menu.add(Menu.NONE, R.id.menu_id_targeted_help, Menu.NONE, R.string.menu_help);
        help.setIcon(R.drawable.ic_help_and_feedback);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if (item.getItemId() == R.id.menu_id_targeted_help) {
            getHelpAndFeedbackLauncher()
                    .show(
                            getActivity(),
                            getActivity().getString(R.string.help_context_autofill),
                            null);
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    @Override
    public void onResume() {
        super.onResume();
        // Always rebuild our list of credit cards.  Although we could detect if credit cards are
        // added or deleted, the credit card summary (number) might be different.  To be safe, we
        // update all.
        rebuildPage();
    }

    private void rebuildPage() {
        getPreferenceScreen().removeAll();
        getPreferenceScreen().setOrderingAsAdded(true);

        PersonalDataManager personalDataManager =
                PersonalDataManagerFactory.getForProfile(getProfile());
        ChromeSwitchPreference autofillSwitch =
                new ChromeSwitchPreference(getStyledContext(), null);
        autofillSwitch.setTitle(R.string.autofill_enable_credit_cards_toggle_label);
        autofillSwitch.setSummary(R.string.autofill_enable_credit_cards_toggle_sublabel);
        autofillSwitch.setChecked(personalDataManager.isAutofillCreditCardEnabled());
        autofillSwitch.setOnPreferenceChangeListener(
                (preference, newValue) -> {
                    personalDataManager.setAutofillCreditCardEnabled((boolean) newValue);
                    return true;
                });
        autofillSwitch.setManagedPreferenceDelegate(
                new ChromeManagedPreferenceDelegate(getProfile()) {
                    @Override
                    public boolean isPreferenceControlledByPolicy(Preference preference) {
                        return personalDataManager.isAutofillCreditCardManaged();
                    }

                    @Override
                    public boolean isPreferenceClickDisabled(Preference preference) {
                        return personalDataManager.isAutofillCreditCardManaged()
                                && !personalDataManager.isAutofillCreditCardEnabled();
                    }
                });
        getPreferenceScreen().addPreference(autofillSwitch);

        if (ChromeFeatureList.isEnabled(
                ChromeFeatureList.AUTOFILL_ENABLE_SYNCING_OF_PIX_BANK_ACCOUNTS)) {
            Pair<Integer, String> otherFinancialAccountTypes =
                    getOtherFinancialAccountsTypes(personalDataManager);
            if (otherFinancialAccountTypes.first != 0) {
                Preference otherFinancialAccountsPref = new Preference(getStyledContext());
                otherFinancialAccountsPref.setKey(PREF_FINANCIAL_ACCOUNTS_MANAGEMENT);
                otherFinancialAccountsPref.setSingleLineTitle(false);
                otherFinancialAccountsPref.setTitle(
                        getResources()
                                .getString(
                                        R.string.settings_manage_other_financial_accounts_title,
                                        otherFinancialAccountTypes.second));
                otherFinancialAccountsPref.setSummary(
                        getResources()
                                .getQuantityString(
                                        R.plurals
                                                .settings_manage_other_financial_accounts_description,
                                        otherFinancialAccountTypes.first,
                                        otherFinancialAccountTypes.second));
                getPreferenceScreen().addPreference(otherFinancialAccountsPref);
                otherFinancialAccountsPref.setOnPreferenceClickListener(
                        this::showOtherFinancialAccountsFragment);
            }
        }

        // TODO(crbug.com/40261690): Confirm with Product on the order of the toggles.
        // Don't show the toggle to enable mandatory reauth on automotive,
        // as the feature is always enabled for automotive builds.
        if (BuildInfo.getInstance().isAutomotive) {
            // The ReauthenticatorBridge is still needed for reauthentication to view/edit
            // payment methods.
            createReauthenticatorBridge();
        } else {
            createReauthenticatorBridge();
            createMandatoryReauthSwitch();
        }

        if (ChromeFeatureList.isEnabled(ChromeFeatureList.AUTOFILL_ENABLE_CVC_STORAGE)) {
            ChromeSwitchPreference saveCvcSwitch =
                    new ChromeSwitchPreference(getStyledContext(), null);
            saveCvcSwitch.setTitle(R.string.autofill_settings_page_enable_cvc_storage_label);
            saveCvcSwitch.setSummary(R.string.autofill_settings_page_enable_cvc_storage_sublabel);
            saveCvcSwitch.setKey(PREF_SAVE_CVC);
            // When "Save And Fill Payments Methods" is disabled, we disable this cvc storage
            // toggle.
            saveCvcSwitch.setEnabled(personalDataManager.isAutofillCreditCardEnabled());
            saveCvcSwitch.setOnPreferenceChangeListener(
                    (preference, newValue) -> {
                        personalDataManager.setAutofillPaymentCvcStorage((boolean) newValue);
                        return true;
                    });
            getPreferenceScreen().addPreference(saveCvcSwitch);

            // When "Save And Fill Payments Methods" is disabled, we override this toggle's value to
            // off (but not change the underlying pref value). When "Save And Fill Payments Methods"
            // is ON, show the cvc storage pref value.
            saveCvcSwitch.setChecked(
                    personalDataManager.isAutofillCreditCardEnabled()
                            && personalDataManager.isPaymentCvcStorageEnabled());

            // Add the deletion button for saved CVCs. Note that this button's presence doesn't
            // depend on the value of the "Save and fill payment methods" toggle, since we would
            // like to allow the user to delete saved CVCs even when the toggle is disabled.
            // Conditionally show the deletion button based on whether there are any CVCs stored.
            if (personalDataManager.getCreditCardsForSettings().stream()
                    .anyMatch(card -> !card.getCvc().isEmpty())) {
                createDeleteSavedCvcsButton();
            }
        }

        if (personalDataManager.isAutofillCreditCardEnabled()
                && (ChromeFeatureList.isEnabled(
                                ChromeFeatureList
                                        .AUTOFILL_ENABLE_CARD_BENEFITS_FOR_AMERICAN_EXPRESS)
                        || ChromeFeatureList.isEnabled(
                                ChromeFeatureList.AUTOFILL_ENABLE_CARD_BENEFITS_FOR_CAPITAL_ONE))) {
            Preference cardBenefitsPref = new Preference(getStyledContext());
            cardBenefitsPref.setTitle(
                    R.string.autofill_settings_page_card_benefits_preference_label);
            cardBenefitsPref.setSummary(
                    R.string.autofill_settings_page_card_benefits_preference_summary);
            cardBenefitsPref.setKey(PREF_CARD_BENEFITS);
            cardBenefitsPref.setFragment(AutofillCardBenefitsFragment.class.getName());
            getPreferenceScreen().addPreference(cardBenefitsPref);
        }

        for (CreditCard card : personalDataManager.getCreditCardsForSettings()) {
            // Add a preference for the credit card.
            Preference card_pref = new Preference(getStyledContext());
            // Make the card_pref multi-line, since cards with long nicknames won't fit on a single
            // line.
            card_pref.setSingleLineTitle(false);
            card_pref.setTitle(card.getCardLabel());

            // Show virtual card enabled status for enrolled cards, expiration date otherwise.
            if (card.getVirtualCardEnrollmentState() == VirtualCardEnrollmentState.ENROLLED
                    && ChromeFeatureList.isEnabled(
                            ChromeFeatureList.AUTOFILL_ENABLE_VIRTUAL_CARD_METADATA)) {
                card_pref.setSummary(R.string.autofill_virtual_card_enrolled_text);
            } else {
                if (ChromeFeatureList.isEnabled(ChromeFeatureList.AUTOFILL_ENABLE_CVC_STORAGE)
                        && !card.getCvc().isEmpty()) {
                    card_pref.setSummary(
                            card.getFormattedExpirationDateWithCvcSavedMessage(getActivity()));
                } else {
                    card_pref.setSummary(card.getFormattedExpirationDate(getActivity()));
                }
            }

            // Set card icon. It can be either a custom card art or a network icon.
            card_pref.setIcon(
                    getCardIcon(
                            getStyledContext(),
                            personalDataManager,
                            card.getCardArtUrl(),
                            card.getIssuerIconDrawableId(),
                            AutofillUiUtils.CardIconSize.LARGE,
                            ChromeFeatureList.isEnabled(
                                    ChromeFeatureList.AUTOFILL_ENABLE_CARD_ART_IMAGE)));

            if (card.getIsLocal()) {
                card_pref.setOnPreferenceClickListener(
                        this::showLocalCardEditPageAfterAuthenticationIfRequired);
            } else {
                card_pref.setFragment(AutofillServerCardEditor.class.getName());
                if (ChromeFeatureList.isEnabled(
                        ChromeFeatureList.AUTOFILL_ENABLE_VIRTUAL_CARD_METADATA)) {
                    card_pref.setWidgetLayoutResource(R.layout.autofill_server_data_label);
                } else {
                    card_pref.setWidgetLayoutResource(R.layout.autofill_server_data_text_label);
                }
            }

            Bundle args = card_pref.getExtras();
            args.putString(AutofillEditorBase.AUTOFILL_GUID, card.getGUID());
            getPreferenceScreen().addPreference(card_pref);
        }

        // Display local IBANs.
        for (Iban iban : personalDataManager.getLocalIbansForSettings()) {
            Preference iban_pref = new Preference(getStyledContext());
            iban_pref.setIcon(R.drawable.iban_icon);
            iban_pref.setSingleLineTitle(false);
            iban_pref.setTitle(iban.getLabel());
            iban_pref.setSummary(iban.getNickname());
            iban_pref.setFragment(AutofillLocalIbanEditor.class.getName());
            Bundle args = iban_pref.getExtras();
            args.putString(AutofillEditorBase.AUTOFILL_GUID, iban.getGuid());
            getPreferenceScreen().addPreference(iban_pref);
            iban_pref.setKey(PREF_IBAN);
        }

        // Add 'Add credit card' button. Tap of it brings up card editor which allows users type in
        // new credit cards.
        if (personalDataManager.isAutofillCreditCardEnabled()) {
            Preference add_card_pref = new Preference(getStyledContext());
            Drawable plusIcon = ApiCompatibilityUtils.getDrawable(getResources(), R.drawable.plus);
            plusIcon.mutate();
            plusIcon.setColorFilter(
                    SemanticColorUtils.getDefaultControlColorActive(getContext()),
                    PorterDuff.Mode.SRC_IN);
            add_card_pref.setIcon(plusIcon);
            add_card_pref.setTitle(R.string.autofill_create_credit_card);
            add_card_pref.setFragment(AutofillLocalCardEditor.class.getName());
            getPreferenceScreen().addPreference(add_card_pref);
        }

        // Add 'Add IBAN' button. Tapping it brings up the IBAN editor which allows users to type in
        // a new IBAN.
        if (ChromeFeatureList.isEnabled(ChromeFeatureList.AUTOFILL_ENABLE_LOCAL_IBAN)
                && personalDataManager.isAutofillCreditCardEnabled()) {
            Preference add_iban_pref = new Preference(getStyledContext());
            Drawable plusIcon = ApiCompatibilityUtils.getDrawable(getResources(), R.drawable.plus);
            plusIcon.mutate();
            plusIcon.setColorFilter(
                    SemanticColorUtils.getDefaultControlColorActive(getContext()),
                    PorterDuff.Mode.SRC_IN);
            add_iban_pref.setIcon(plusIcon);
            add_iban_pref.setTitle(R.string.autofill_add_local_iban);
            add_iban_pref.setKey(PREF_ADD_IBAN);
            add_iban_pref.setFragment(AutofillLocalIbanEditor.class.getName());
            getPreferenceScreen().addPreference(add_iban_pref);
        }

        // Add the link to payment apps only after the credit card list is rebuilt.
        Preference payment_apps_pref = new Preference(getStyledContext());
        payment_apps_pref.setTitle(R.string.payment_apps_title);
        payment_apps_pref.setFragment(AndroidPaymentAppsFragment.class.getCanonicalName());
        payment_apps_pref.setShouldDisableView(true);
        payment_apps_pref.setKey(PREF_PAYMENT_APPS);
        getPreferenceScreen().addPreference(payment_apps_pref);
        refreshPaymentAppsPrefForAndroidPaymentApps(payment_apps_pref);
    }

    private void createReauthenticatorBridge() {
        if (mReauthenticatorBridge == null) {
            mReauthenticatorBridge =
                    ReauthenticatorBridge.create(
                            this.getActivity(), getProfile(), DeviceAuthSource.AUTOFILL);
        }
    }

    private void createMandatoryReauthSwitch() {
        ChromeSwitchPreference mandatoryReauthSwitch =
                new ChromeSwitchPreference(getStyledContext(), null);
        mandatoryReauthSwitch.setTitle(
                R.string.autofill_settings_page_enable_payment_method_mandatory_reauth_label);
        mandatoryReauthSwitch.setSummary(
                R.string.autofill_settings_page_enable_payment_method_mandatory_reauth_sublabel);
        mandatoryReauthSwitch.setKey(PREF_MANDATORY_REAUTH);
        PersonalDataManager personalDataManager =
                PersonalDataManagerFactory.getForProfile(getProfile());
        // We always display the toggle, but the toggle is only enabled when Autofill credit
        // card is enabled AND the device supports biometric auth or screen lock. If either of
        // these is not met, we will grey out the toggle.
        boolean enableReauthSwitch =
                personalDataManager.isAutofillCreditCardEnabled()
                        && mReauthenticatorBridge.canUseAuthenticationWithBiometricOrScreenLock();
        mandatoryReauthSwitch.setEnabled(enableReauthSwitch);
        mandatoryReauthSwitch.setOnPreferenceChangeListener(this::onMandatoryReauthSwitchToggled);
        getPreferenceScreen().addPreference(mandatoryReauthSwitch);

        // Every {@link SwitchPreferenceCompat} on a {@link PreferenceScreen} has a pref that is
        // automatically added to the {@link SharedPreferences}. When a switch is added, by
        // default its checked state is reset to the saved pref value irrespective of whether or
        // not the switch's checked state was set before adding the switch. Setting the checked
        // state after adding the switch updates the underlying pref as well.
        // If a user opts in to mandatory reauth during the checkout flow, since the switch's
        // underlying pref is still false, the switch does not reflect the opt-in. Set switch's
        // checked state after adding it to the screen so the underlying pref value is also
        // updated and is in sync with the mandatory reauth user pref.
        mandatoryReauthSwitch.setChecked(
                personalDataManager.isPaymentMethodsMandatoryReauthEnabled());
    }

    private Context getStyledContext() {
        return getPreferenceManager().getContext();
    }

    private void refreshPaymentAppsPrefForAndroidPaymentApps(Preference pref) {
        if (AndroidPaymentAppFactory.hasAndroidPaymentApps()) {
            setPaymentAppsPrefStatus(pref, true);
        } else {
            refreshPaymentAppsPrefForServiceWorkerPaymentApps(pref);
        }
    }

    private void refreshPaymentAppsPrefForServiceWorkerPaymentApps(Preference pref) {
        ServiceWorkerPaymentAppBridge.hasServiceWorkerPaymentApps(
                new ServiceWorkerPaymentAppBridge.HasServiceWorkerPaymentAppsCallback() {
                    @Override
                    public void onHasServiceWorkerPaymentAppsResponse(boolean hasPaymentApps) {
                        setPaymentAppsPrefStatus(pref, hasPaymentApps);
                    }
                });
    }

    private void setPaymentAppsPrefStatus(Preference pref, boolean enabled) {
        if (enabled) {
            pref.setSummary(null);
            pref.setEnabled(true);
        } else {
            pref.setSummary(R.string.payment_no_apps_summary);
            pref.setEnabled(false);
        }
    }

    private boolean isBiometricAvailable() {
        // Only Android versions 9 and above are supported.
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
            return false;
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            BiometricManager biometricManager =
                    getStyledContext().getSystemService(BiometricManager.class);
            return biometricManager != null
                    && biometricManager.canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS;
        } else {
            // For API level < Q, we will use FingerprintManagerCompat to check enrolled
            // fingerprints. Note that for API level lower than 23, FingerprintManagerCompat behaves
            // like no fingerprint hardware and no enrolled fingerprints.
            FingerprintManagerCompat fingerprintManager =
                    FingerprintManagerCompat.from(getStyledContext());
            return fingerprintManager != null
                    && fingerprintManager.isHardwareDetected()
                    && fingerprintManager.hasEnrolledFingerprints();
        }
    }

    /** Handle preference changes from mandatory reauth toggle */
    private boolean onMandatoryReauthSwitchToggled(Preference preference, Object newValue) {
        assert preference.getKey().equals(PREF_MANDATORY_REAUTH);

        ChromeSwitchPreference mandatoryReauthSwitch = (ChromeSwitchPreference) preference;
        // If the user preference update is successful, toggle the switch to the success state.
        boolean userIntendedState = !mandatoryReauthSwitch.isChecked();
        String histogramName =
                userIntendedState
                        ? MANDATORY_REAUTH_OPT_IN_HISTOGRAM
                        : MANDATORY_REAUTH_OPT_OUT_HISTOGRAM;
        RecordHistogram.recordEnumeratedHistogram(
                histogramName,
                MandatoryReauthAuthenticationFlowEvent.FLOW_STARTED,
                MandatoryReauthAuthenticationFlowEvent.MAX_VALUE + 1);
        // We require user authentication every time user tries to change this
        // preference. Set useLastValidAuth=false to skip the grace period.
        mReauthenticatorBridge.reauthenticate(
                success -> {
                    if (success) {
                        // Only set the preference to new value when user passes the
                        // authentication.
                        PersonalDataManagerFactory.getForProfile(getProfile())
                                .setAutofillPaymentMethodsMandatoryReauth((boolean) newValue);

                        // When the preference is updated, the page is expected to refresh and show
                        // the updated preference. Fallback if the page does not load.
                        mandatoryReauthSwitch.setChecked(userIntendedState);
                        RecordHistogram.recordEnumeratedHistogram(
                                histogramName,
                                MandatoryReauthAuthenticationFlowEvent.FLOW_SUCCEEDED,
                                MandatoryReauthAuthenticationFlowEvent.MAX_VALUE + 1);
                    } else {
                        RecordHistogram.recordEnumeratedHistogram(
                                histogramName,
                                MandatoryReauthAuthenticationFlowEvent.FLOW_FAILED,
                                MandatoryReauthAuthenticationFlowEvent.MAX_VALUE + 1);
                    }
                });
        // Returning false here holds the toggle to still display the old value while
        // waiting for biometric auth. Once biometric is completed (either succeed or
        // fail), OnResume will reload the page with the pref value, which will switch
        // to the new value if biometric auth succeeded.
        return false;
    }

    /**
     * If mandatory reauth is enabled, trigger device authentication before user can view/edit local
     * card. Else show the local card edit page.
     *
     * @param preference The {@link Preference} for the local card.
     * @return true if the click was handled, false otherwise.
     */
    private boolean showLocalCardEditPageAfterAuthenticationIfRequired(Preference preference) {
        if (!PersonalDataManagerFactory.getForProfile(getProfile())
                .isPaymentMethodsMandatoryReauthEnabled()) {
            showLocalCardEditPage(preference);
            return true;
        }

        // mReauthenticatorBridge should be initiated already when determining whether to show the
        // mandatory reauth toggle.
        assert mReauthenticatorBridge != null;
        RecordHistogram.recordEnumeratedHistogram(
                MANDATORY_REAUTH_EDIT_CARD_HISTOGRAM,
                MandatoryReauthAuthenticationFlowEvent.FLOW_STARTED,
                MandatoryReauthAuthenticationFlowEvent.MAX_VALUE + 1);
        // When mandatory reauth is enabled, offer device authentication challenge.
        mReauthenticatorBridge.reauthenticate(
                success -> {
                    // If authentication is successful, manually trigger the local card edit page.
                    // Else, stay on this page.
                    if (success) {
                        RecordHistogram.recordEnumeratedHistogram(
                                MANDATORY_REAUTH_EDIT_CARD_HISTOGRAM,
                                MandatoryReauthAuthenticationFlowEvent.FLOW_SUCCEEDED,
                                MandatoryReauthAuthenticationFlowEvent.MAX_VALUE + 1);
                        showLocalCardEditPage(preference);
                    } else {
                        RecordHistogram.recordEnumeratedHistogram(
                                MANDATORY_REAUTH_EDIT_CARD_HISTOGRAM,
                                MandatoryReauthAuthenticationFlowEvent.FLOW_FAILED,
                                MandatoryReauthAuthenticationFlowEvent.MAX_VALUE + 1);
                    }
                });
        return true;
    }

    /**
     * Show the local card edit page for the given local card.
     *
     * @param preference The {@link Preference} for the local card.
     */
    private void showLocalCardEditPage(Preference preference) {
        SettingsLauncher settingsLauncher = SettingsLauncherFactory.createSettingsLauncher();
        settingsLauncher.launchSettingsActivity(
                getActivity(), AutofillLocalCardEditor.class, preference.getExtras());
    }

    /**
     * Create a clickable "Delete saved cvcs" button and add it to the preference screen.
     * No divider line above this preference.
     */
    private void createDeleteSavedCvcsButton() {
        ChromeBasePreference deleteSavedCvcs = new ChromeBasePreference(getStyledContext());
        deleteSavedCvcs.setKey(PREF_DELETE_SAVED_CVCS);
        SpannableString spannableString =
                new SpannableString(
                        getResources()
                                .getString(R.string.autofill_settings_page_bulk_remove_cvc_label));
        spannableString.setSpan(
                new ForegroundColorSpan(SemanticColorUtils.getDefaultTextColorLink(getContext())),
                0,
                spannableString.length(),
                Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        deleteSavedCvcs.setSummary(spannableString);
        deleteSavedCvcs.setDividerAllowedAbove(false);
        deleteSavedCvcs.setOnPreferenceClickListener(
                preference -> {
                    showDeleteSavedCvcsConfirmationDialog();
                    return true;
                });
        getPreferenceScreen().addPreference(deleteSavedCvcs);
    }

    private void showDeleteSavedCvcsConfirmationDialog() {
        AutofillDeleteSavedCvcsConfirmationDialog dialog =
                new AutofillDeleteSavedCvcsConfirmationDialog(
                        getActivity(),
                        new ModalDialogManager(
                                new AppModalPresenter(getActivity()), ModalDialogType.APP),
                        deleteRequested -> {
                            if (deleteRequested) {
                                if (mAutofillPaymentMethodsDelegate == null) {
                                    mAutofillPaymentMethodsDelegate =
                                            new AutofillPaymentMethodsDelegate(getProfile());
                                }
                                mAutofillPaymentMethodsDelegate.deleteSavedCvcs();
                            }
                        });
        dialog.show();
    }

    /**
     * Returns a pair of the number of types of financial accounts and the string to be displayed in
     * the settings page.
     */
    private Pair<Integer, String> getOtherFinancialAccountsTypes(
            PersonalDataManager personalDataManager) {
        return personalDataManager.getMaskedBankAccounts().length == 0
                ? new Pair<>(0, "")
                : new Pair<>(
                        1,
                        getResources()
                                .getString(R.string.settings_manage_other_financial_accounts_pix));
    }

    /** Show the page for managing other finiancial accounts. */
    private boolean showOtherFinancialAccountsFragment(Preference preference) {
        Bundle args = preference.getExtras();
        args.putString(
                FinancialAccountsManagementFragment.TITLE_KEY, preference.getTitle().toString());
        SettingsLauncher settingsLauncher = SettingsLauncherFactory.createSettingsLauncher();
        settingsLauncher.launchSettingsActivity(
                getActivity(), FinancialAccountsManagementFragment.class, args);
        return true;
    }

    @Override
    public void onPersonalDataChanged() {
        rebuildPage();
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        PersonalDataManagerFactory.getForProfile(getProfile()).registerDataObserver(this);
    }

    @Override
    public void onDestroyView() {
        PersonalDataManagerFactory.getForProfile(getProfile()).unregisterDataObserver(this);
        if (mReauthenticatorBridge != null) {
            mReauthenticatorBridge.destroy();
        }
        super.onDestroyView();
    }
}