| // 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 static org.chromium.chrome.browser.autofill.AutofillUiUtils.getSettingsPageIconHeightId; |
| import static org.chromium.chrome.browser.autofill.AutofillUiUtils.getSettingsPageIconWidthId; |
| |
| 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.view.Menu; |
| import android.view.MenuInflater; |
| import android.view.MenuItem; |
| |
| import androidx.annotation.Nullable; |
| import androidx.core.hardware.fingerprint.FingerprintManagerCompat; |
| import androidx.preference.Preference; |
| import androidx.preference.PreferenceFragmentCompat; |
| import androidx.preference.PreferenceScreen; |
| |
| import org.chromium.base.ApiCompatibilityUtils; |
| import org.chromium.chrome.R; |
| import org.chromium.chrome.browser.autofill.AutofillEditorBase; |
| import org.chromium.chrome.browser.autofill.PersonalDataManager; |
| import org.chromium.chrome.browser.autofill.PersonalDataManager.CreditCard; |
| import org.chromium.chrome.browser.device_reauth.DeviceAuthRequester; |
| import org.chromium.chrome.browser.device_reauth.ReauthenticatorBridge; |
| import org.chromium.chrome.browser.feedback.FragmentHelpAndFeedbackLauncher; |
| import org.chromium.chrome.browser.feedback.HelpAndFeedbackLauncher; |
| import org.chromium.chrome.browser.flags.ChromeFeatureList; |
| import org.chromium.chrome.browser.payments.ServiceWorkerPaymentAppBridge; |
| import org.chromium.chrome.browser.settings.ChromeManagedPreferenceDelegate; |
| import org.chromium.chrome.browser.settings.SettingsLauncherImpl; |
| import org.chromium.components.autofill.VirtualCardEnrollmentState; |
| 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; |
| |
| /** |
| * Autofill credit cards fragment, which allows the user to edit credit cards and control |
| * payment apps. |
| */ |
| public class AutofillPaymentMethodsFragment |
| extends PreferenceFragmentCompat implements PersonalDataManager.PersonalDataManagerObserver, |
| FragmentHelpAndFeedbackLauncher { |
| static final String PREF_MANDATORY_REAUTH = "mandatory_reauth"; |
| private static final String PREF_PAYMENT_APPS = "payment_apps"; |
| |
| private HelpAndFeedbackLauncher mHelpAndFeedbackLauncher; |
| |
| @Nullable |
| private ReauthenticatorBridge mReauthenticatorBridge; |
| |
| @Override |
| public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { |
| getActivity().setTitle(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 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) { |
| mHelpAndFeedbackLauncher.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); |
| |
| 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() { |
| @Override |
| public boolean isPreferenceControlledByPolicy(Preference preference) { |
| return PersonalDataManager.isAutofillCreditCardManaged(); |
| } |
| |
| @Override |
| public boolean isPreferenceClickDisabled(Preference preference) { |
| return PersonalDataManager.isAutofillCreditCardManaged() |
| && !PersonalDataManager.isAutofillCreditCardEnabled(); |
| } |
| }); |
| getPreferenceScreen().addPreference(autofillSwitch); |
| |
| if (isBiometricAvailable() |
| && PersonalDataManager.getInstance().isFidoAuthenticationAvailable()) { |
| ChromeSwitchPreference fidoAuthSwitch = |
| new ChromeSwitchPreference(getStyledContext(), null); |
| fidoAuthSwitch.setTitle(R.string.enable_credit_card_fido_auth_label); |
| fidoAuthSwitch.setSummary(R.string.enable_credit_card_fido_auth_sublabel); |
| fidoAuthSwitch.setChecked(PersonalDataManager.isAutofillCreditCardFidoAuthEnabled()); |
| fidoAuthSwitch.setOnPreferenceChangeListener((preference, newValue) -> { |
| PersonalDataManager.setAutofillCreditCardFidoAuthEnabled((boolean) newValue); |
| return true; |
| }); |
| getPreferenceScreen().addPreference(fidoAuthSwitch); |
| } |
| |
| // TODO(crbug.com/1427216): Confirm with Product on the order of the toggles. |
| if (ChromeFeatureList.isEnabled( |
| ChromeFeatureList.AUTOFILL_ENABLE_PAYMENTS_MANDATORY_REAUTH)) { |
| if (mReauthenticatorBridge == null) { |
| mReauthenticatorBridge = ReauthenticatorBridge.create( |
| DeviceAuthRequester.PAYMENT_METHODS_REAUTH_IN_SETTINGS); |
| } |
| // We don't show the Reauth toggle when Autofill credit card is disabled or the device |
| // doesn't have biometric auth. |
| if (PersonalDataManager.isAutofillCreditCardEnabled() |
| && mReauthenticatorBridge.canUseAuthenticationWithBiometric()) { |
| 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.setChecked( |
| PersonalDataManager.isPaymentMethodsMandatoryReauthEnabled()); |
| mandatoryReauthSwitch.setKey(PREF_MANDATORY_REAUTH); |
| mandatoryReauthSwitch.setOnPreferenceChangeListener( |
| this::onMandatoryReauthSwitchToggled); |
| getPreferenceScreen().addPreference(mandatoryReauthSwitch); |
| } |
| } |
| |
| for (CreditCard card : PersonalDataManager.getInstance().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 { |
| 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(), card.getCardArtUrl(), |
| card.getIssuerIconDrawableId(), getSettingsPageIconWidthId(), |
| getSettingsPageIconHeightId(), R.dimen.card_art_corner_radius, |
| ChromeFeatureList.isEnabled(ChromeFeatureList.AUTOFILL_ENABLE_CARD_ART_IMAGE))); |
| |
| if (card.getIsLocal()) { |
| if (ChromeFeatureList.isEnabled( |
| ChromeFeatureList.AUTOFILL_ENABLE_PAYMENTS_MANDATORY_REAUTH) |
| && PersonalDataManager.isPaymentMethodsMandatoryReauthEnabled()) { |
| // When mandatory reauth is enabled, we require additional authentication before |
| // user can view/edit local card. |
| card_pref.setOnPreferenceClickListener( |
| this::authenticateBeforeShowingLocalCardEditDialog); |
| } else { |
| card_pref.setFragment(AutofillLocalCardEditor.class.getName()); |
| } |
| } 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); |
| } |
| |
| // 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 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 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); |
| // We require user authentication every time user trys 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. |
| PersonalDataManager.setAutofillPaymentMethodsMandatoryReauth((boolean) newValue); |
| } |
| }, /*useLastValidAuth=*/false); |
| // 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; |
| } |
| |
| /** |
| * Trigger additional authentication before user can view/edit local card. When authentication |
| * passes, we show local card edit dialog. Stays on this page if authentication fails/cancelled. |
| */ |
| private boolean authenticateBeforeShowingLocalCardEditDialog(Preference preference) { |
| // mReauthenticatorBridge should be initiated already when determining whether to show the |
| // mandatory reauth toggle. |
| assert mReauthenticatorBridge != null; |
| mReauthenticatorBridge.reauthenticate(success -> { |
| if (success) { |
| // Manually trigger the local card edit dialog after user passes the authentication. |
| SettingsLauncher settingsLauncher = new SettingsLauncherImpl(); |
| settingsLauncher.launchSettingsActivity( |
| getActivity(), AutofillLocalCardEditor.class, preference.getExtras()); |
| } |
| }, /*useLastValidAuth=*/false); |
| return true; |
| } |
| |
| @Override |
| public void onPersonalDataChanged() { |
| rebuildPage(); |
| } |
| |
| @Override |
| public void onActivityCreated(Bundle savedInstanceState) { |
| super.onActivityCreated(savedInstanceState); |
| PersonalDataManager.getInstance().registerDataObserver(this); |
| } |
| |
| @Override |
| public void onDestroyView() { |
| PersonalDataManager.getInstance().unregisterDataObserver(this); |
| super.onDestroyView(); |
| } |
| |
| @Override |
| public void setHelpAndFeedbackLauncher(HelpAndFeedbackLauncher helpAndFeedbackLauncher) { |
| mHelpAndFeedbackLauncher = helpAndFeedbackLauncher; |
| } |
| } |