| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // 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_assistant.payment; |
| |
| import android.app.Activity; |
| import android.text.TextUtils; |
| import android.view.View; |
| |
| import org.chromium.chrome.browser.ChromeVersionInfo; |
| import org.chromium.chrome.browser.autofill.PersonalDataManager; |
| import org.chromium.chrome.browser.payments.AddressEditor; |
| import org.chromium.chrome.browser.payments.AutofillPaymentApp; |
| import org.chromium.chrome.browser.payments.AutofillPaymentInstrument; |
| import org.chromium.chrome.browser.payments.BasicCardUtils; |
| import org.chromium.chrome.browser.payments.CardEditor; |
| import org.chromium.chrome.browser.payments.ContactEditor; |
| import org.chromium.chrome.browser.payments.PaymentInstrument; |
| import org.chromium.chrome.browser.widget.prefeditor.EditorDialog; |
| import org.chromium.components.url_formatter.UrlFormatter; |
| import org.chromium.content_public.browser.WebContents; |
| import org.chromium.payments.mojom.PaymentMethodData; |
| import org.chromium.ui.modelutil.PropertyKey; |
| import org.chromium.ui.modelutil.PropertyModelChangeProcessor; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * This class is responsible for pushing updates to the Autofill Assistant payment request. These |
| * updates are pulled from the {@link AssistantPaymentRequestModel} when a notification of an update |
| * is received. |
| */ |
| class AssistantPaymentRequestBinder |
| implements PropertyModelChangeProcessor.ViewBinder<AssistantPaymentRequestModel, |
| AssistantPaymentRequestBinder.ViewHolder, PropertyKey> { |
| /** |
| * Helper class that compares instances of {@link PersonalDataManager.AutofillProfile} by |
| * completeness in regards to the current state of the given {@link |
| * AssistantPaymentRequestModel}. |
| */ |
| class CompletenessComparator implements Comparator<PersonalDataManager.AutofillProfile> { |
| final boolean mRequestName; |
| final boolean mRequestShipping; |
| final boolean mRequestEmail; |
| final boolean mRequestPhone; |
| CompletenessComparator(AssistantPaymentRequestModel model) { |
| mRequestName = model.get(AssistantPaymentRequestModel.REQUEST_NAME); |
| mRequestShipping = model.get(AssistantPaymentRequestModel.REQUEST_SHIPPING_ADDRESS); |
| mRequestEmail = model.get(AssistantPaymentRequestModel.REQUEST_EMAIL); |
| mRequestPhone = model.get(AssistantPaymentRequestModel.REQUEST_PHONE); |
| } |
| |
| @Override |
| public int compare( |
| PersonalDataManager.AutofillProfile a, PersonalDataManager.AutofillProfile b) { |
| int result = countCompleteFields(a) - countCompleteFields(b); |
| // Fallback, compare by name alphabetically. |
| if (result == 0) { |
| return a.getFullName().compareTo(b.getFullName()); |
| } |
| return result; |
| } |
| |
| private int countCompleteFields(PersonalDataManager.AutofillProfile profile) { |
| int completeFields = 0; |
| if (mRequestName && !TextUtils.isEmpty((profile.getFullName()))) { |
| completeFields++; |
| } |
| if (mRequestShipping && !TextUtils.isEmpty(profile.getStreetAddress())) { |
| completeFields++; |
| } |
| if (mRequestEmail && !TextUtils.isEmpty(profile.getEmailAddress())) { |
| completeFields++; |
| } |
| if (mRequestPhone && !TextUtils.isEmpty(profile.getPhoneNumber())) { |
| completeFields++; |
| } |
| return completeFields; |
| } |
| } |
| |
| /** |
| * A wrapper class that holds the different views of the payment request. |
| */ |
| static class ViewHolder { |
| private final View mRootView; |
| private final AssistantVerticalExpanderAccordion mPaymentRequestExpanderAccordion; |
| private final int mSectionToSectionPadding; |
| private final AssistantPaymentRequestContactDetailsSection mContactDetailsSection; |
| private final AssistantPaymentRequestPaymentMethodSection mPaymentMethodSection; |
| private final AssistantPaymentRequestShippingAddressSection mShippingAddressSection; |
| private final AssistantPaymentRequestTermsSection mTermsSection; |
| private final Object mDividerTag; |
| private final Activity mActivity; |
| private PersonalDataManager.PersonalDataManagerObserver mPersonalDataManagerObserver; |
| |
| public ViewHolder(View rootView, AssistantVerticalExpanderAccordion accordion, |
| int sectionPadding, |
| AssistantPaymentRequestContactDetailsSection contactDetailsSection, |
| AssistantPaymentRequestPaymentMethodSection paymentMethodSection, |
| AssistantPaymentRequestShippingAddressSection shippingAddressSection, |
| AssistantPaymentRequestTermsSection termsSection, Object dividerTag, |
| Activity activity) { |
| mRootView = rootView; |
| mPaymentRequestExpanderAccordion = accordion; |
| mSectionToSectionPadding = sectionPadding; |
| mContactDetailsSection = contactDetailsSection; |
| mPaymentMethodSection = paymentMethodSection; |
| mShippingAddressSection = shippingAddressSection; |
| mTermsSection = termsSection; |
| mDividerTag = dividerTag; |
| mActivity = activity; |
| } |
| |
| /** |
| * Explicitly clean up. |
| */ |
| public void destroy() { |
| stopListenToPersonalDataManager(); |
| } |
| |
| private void startListenToPersonalDataManager( |
| PersonalDataManager.PersonalDataManagerObserver observer) { |
| if (mPersonalDataManagerObserver != null) { |
| return; |
| } |
| mPersonalDataManagerObserver = observer; |
| PersonalDataManager.getInstance().registerDataObserver(mPersonalDataManagerObserver); |
| } |
| |
| private void stopListenToPersonalDataManager() { |
| if (mPersonalDataManagerObserver == null) { |
| return; |
| } |
| PersonalDataManager.getInstance().unregisterDataObserver(mPersonalDataManagerObserver); |
| mPersonalDataManagerObserver = null; |
| } |
| } |
| |
| @Override |
| public void bind(AssistantPaymentRequestModel model, ViewHolder view, PropertyKey propertyKey) { |
| boolean handled = updateEditors(model, propertyKey, view); |
| handled = updateSectionPaddings(model, propertyKey, view) || handled; |
| handled = updateRootVisibility(model, propertyKey, view) || handled; |
| handled = updateSectionVisibility(model, propertyKey, view) || handled; |
| handled = updateSectionContents(model, propertyKey, view) || handled; |
| handled = updateSectionSelectedItem(model, propertyKey, view) || handled; |
| |
| if (propertyKey == AssistantPaymentRequestModel.DELEGATE) { |
| AssistantPaymentRequestDelegate delegate = |
| model.get(AssistantPaymentRequestModel.DELEGATE); |
| view.mTermsSection.setListener( |
| delegate != null ? delegate::onTermsAndConditionsChanged : null); |
| view.mContactDetailsSection.setListener( |
| delegate != null ? delegate::onContactInfoChanged : null); |
| view.mPaymentMethodSection.setListener( |
| delegate != null ? delegate::onPaymentMethodChanged : null); |
| view.mShippingAddressSection.setListener( |
| delegate != null ? delegate::onShippingAddressChanged : null); |
| } else if (propertyKey == AssistantPaymentRequestModel.SUPPORTED_BASIC_CARD_NETWORKS) { |
| updateAvailablePaymentMethods(model); |
| } else if (propertyKey == AssistantPaymentRequestModel.SUPPORTED_PAYMENT_METHODS) { |
| updateAvailableAutofillPaymentMethods(model); |
| } else |
| assert handled : "Unhandled property detected in AssistantPaymentRequestBinder!"; |
| } |
| |
| private boolean shouldShowContactDetails(AssistantPaymentRequestModel model) { |
| return model.get(AssistantPaymentRequestModel.REQUEST_NAME) |
| || model.get(AssistantPaymentRequestModel.REQUEST_PHONE) |
| || model.get(AssistantPaymentRequestModel.REQUEST_EMAIL); |
| } |
| |
| /** |
| * Updates the available items for each PR section. |
| * @return whether the property key was handled. |
| */ |
| private boolean updateSectionContents( |
| AssistantPaymentRequestModel model, PropertyKey propertyKey, ViewHolder view) { |
| if (propertyKey == AssistantPaymentRequestModel.AVAILABLE_AUTOFILL_PAYMENT_METHODS) { |
| List<AutofillPaymentInstrument> availablePaymentMethods = |
| model.get(AssistantPaymentRequestModel.AVAILABLE_AUTOFILL_PAYMENT_METHODS); |
| if (availablePaymentMethods == null) availablePaymentMethods = Collections.emptyList(); |
| view.mPaymentMethodSection.onAvailablePaymentMethodsChanged(availablePaymentMethods); |
| return true; |
| } else if (propertyKey == AssistantPaymentRequestModel.AVAILABLE_PROFILES) { |
| List<PersonalDataManager.AutofillProfile> autofillProfiles = |
| model.get(AssistantPaymentRequestModel.AVAILABLE_PROFILES); |
| if (autofillProfiles == null) { |
| autofillProfiles = Collections.emptyList(); |
| } |
| view.mContactDetailsSection.onProfilesChanged(autofillProfiles); |
| view.mPaymentMethodSection.onProfilesChanged(autofillProfiles); |
| view.mShippingAddressSection.onProfilesChanged(autofillProfiles); |
| return true; |
| } |
| return false; |
| } |
| |
| /** |
| * Updates visibility of PR sections. |
| * @return whether the property key was handled. |
| */ |
| private boolean updateSectionVisibility( |
| AssistantPaymentRequestModel model, PropertyKey propertyKey, ViewHolder view) { |
| if ((propertyKey == AssistantPaymentRequestModel.REQUEST_NAME) |
| || (propertyKey == AssistantPaymentRequestModel.REQUEST_EMAIL) |
| || (propertyKey == AssistantPaymentRequestModel.REQUEST_PHONE)) { |
| view.mContactDetailsSection.setVisible(shouldShowContactDetails(model)); |
| return true; |
| } else if (propertyKey == AssistantPaymentRequestModel.REQUEST_SHIPPING_ADDRESS) { |
| view.mShippingAddressSection.setVisible( |
| model.get(AssistantPaymentRequestModel.REQUEST_SHIPPING_ADDRESS)); |
| return true; |
| } else if (propertyKey == AssistantPaymentRequestModel.REQUEST_PAYMENT) { |
| view.mPaymentMethodSection.setVisible( |
| (model.get(AssistantPaymentRequestModel.REQUEST_PAYMENT))); |
| return true; |
| } |
| return false; |
| } |
| |
| /** |
| * Updates visibility of the root widget. |
| * @return whether the property key was handled. |
| */ |
| private boolean updateRootVisibility( |
| AssistantPaymentRequestModel model, PropertyKey propertyKey, ViewHolder view) { |
| if ((propertyKey != AssistantPaymentRequestModel.VISIBLE) |
| && (propertyKey != AssistantPaymentRequestModel.FORCE_INVISIBLE)) { |
| return false; |
| } |
| int visibility = model.get(AssistantPaymentRequestModel.VISIBLE) |
| && !model.get(AssistantPaymentRequestModel.FORCE_INVISIBLE) |
| ? View.VISIBLE |
| : View.GONE; |
| if (view.mRootView.getVisibility() != visibility) { |
| if (visibility == View.VISIBLE) { |
| // Update available profiles and credit cards before PR is made visible. |
| updateAvailableProfiles(model, view); |
| updateAvailablePaymentMethods(model); |
| WebContents webContents = model.get(AssistantPaymentRequestModel.WEB_CONTENTS); |
| if (webContents != null) { |
| view.mTermsSection.setOrigin(UrlFormatter.formatUrlForSecurityDisplayOmitScheme( |
| webContents.getLastCommittedUrl())); |
| } |
| view.startListenToPersonalDataManager( |
| () |
| -> AssistantPaymentRequestBinder.this.updateAvailableProfiles( |
| model, view)); |
| } else { |
| view.stopListenToPersonalDataManager(); |
| } |
| view.mRootView.setVisibility(visibility); |
| } |
| return true; |
| } |
| |
| /** |
| * Updates the currently selected item in each PR section. |
| * @return whether the property key was handled. |
| */ |
| private boolean updateSectionSelectedItem( |
| AssistantPaymentRequestModel model, PropertyKey propertyKey, ViewHolder view) { |
| if (propertyKey == AssistantPaymentRequestModel.SHIPPING_ADDRESS) { |
| view.mShippingAddressSection.addOrUpdateItem( |
| model.get(AssistantPaymentRequestModel.SHIPPING_ADDRESS), true); |
| return true; |
| } else if (propertyKey == AssistantPaymentRequestModel.PAYMENT_METHOD) { |
| view.mPaymentMethodSection.addOrUpdateItem( |
| model.get(AssistantPaymentRequestModel.PAYMENT_METHOD), true); |
| return true; |
| } else if (propertyKey == AssistantPaymentRequestModel.CONTACT_DETAILS) { |
| view.mContactDetailsSection.addOrUpdateItem( |
| model.get(AssistantPaymentRequestModel.CONTACT_DETAILS), true); |
| return true; |
| } else if (propertyKey == AssistantPaymentRequestModel.TERMS_STATUS) { |
| view.mTermsSection.setTermsStatus(model.get(AssistantPaymentRequestModel.TERMS_STATUS)); |
| return true; |
| } |
| return false; |
| } |
| |
| // TODO(crbug.com/806868): Move this logic to native. |
| private void updateAvailableProfiles(AssistantPaymentRequestModel model, ViewHolder view) { |
| List<PersonalDataManager.AutofillProfile> autofillProfiles = |
| PersonalDataManager.getInstance().getProfilesToSuggest( |
| /* includeNameInLabel= */ false); |
| |
| // Suggest complete profiles first. |
| Collections.sort(autofillProfiles, new CompletenessComparator(model)); |
| model.set(AssistantPaymentRequestModel.AVAILABLE_PROFILES, autofillProfiles); |
| } |
| |
| /** |
| * Updates the paddings between sections and section dividers. |
| * @return whether the property key was handled. |
| */ |
| private boolean updateSectionPaddings( |
| AssistantPaymentRequestModel model, PropertyKey propertyKey, ViewHolder view) { |
| if ((propertyKey != AssistantPaymentRequestModel.REQUEST_SHIPPING_ADDRESS) |
| && (propertyKey != AssistantPaymentRequestModel.REQUEST_NAME) |
| && (propertyKey != AssistantPaymentRequestModel.REQUEST_EMAIL) |
| && (propertyKey != AssistantPaymentRequestModel.REQUEST_PHONE) |
| && (propertyKey != AssistantPaymentRequestModel.REQUEST_PAYMENT) |
| && (propertyKey != AssistantPaymentRequestModel.EXPANDED_SECTION)) { |
| return false; |
| } |
| |
| // Update section paddings such that the first and last section are flush to the top/bottom, |
| // and all other sections have the same amount of padding in-between them. |
| if (shouldShowContactDetails(model)) { |
| view.mContactDetailsSection.setPaddings(0, view.mSectionToSectionPadding); |
| view.mPaymentMethodSection.setPaddings( |
| view.mSectionToSectionPadding, view.mSectionToSectionPadding); |
| view.mShippingAddressSection.setPaddings( |
| view.mSectionToSectionPadding, view.mSectionToSectionPadding); |
| } else if (model.get(AssistantPaymentRequestModel.REQUEST_PAYMENT)) { |
| view.mPaymentMethodSection.setPaddings(0, view.mSectionToSectionPadding); |
| view.mShippingAddressSection.setPaddings( |
| view.mSectionToSectionPadding, view.mSectionToSectionPadding); |
| } else if (model.get(AssistantPaymentRequestModel.REQUEST_SHIPPING_ADDRESS)) { |
| view.mShippingAddressSection.setPaddings(0, view.mSectionToSectionPadding); |
| } |
| view.mTermsSection.setPaddings(view.mSectionToSectionPadding, 0); |
| |
| // Hide dividers for currently invisible sections and after the expanded section, if any. |
| boolean prevSectionIsExpandedOrInvisible = false; |
| for (int i = 0; i < view.mPaymentRequestExpanderAccordion.getChildCount(); i++) { |
| View child = view.mPaymentRequestExpanderAccordion.getChildAt(i); |
| if (child instanceof AssistantVerticalExpander) { |
| prevSectionIsExpandedOrInvisible = ((AssistantVerticalExpander) child).isExpanded() |
| || child.getVisibility() != View.VISIBLE; |
| } else if (child.getTag() == view.mDividerTag) { |
| child.setVisibility(prevSectionIsExpandedOrInvisible ? View.GONE : View.VISIBLE); |
| } |
| } |
| return true; |
| } |
| |
| /** |
| * Updates/recreates section editors. |
| * @return whether the property key was handled. |
| */ |
| private boolean updateEditors( |
| AssistantPaymentRequestModel model, PropertyKey propertyKey, ViewHolder view) { |
| if ((propertyKey != AssistantPaymentRequestModel.WEB_CONTENTS) |
| && (propertyKey != AssistantPaymentRequestModel.REQUEST_NAME) |
| && (propertyKey != AssistantPaymentRequestModel.REQUEST_EMAIL) |
| && (propertyKey != AssistantPaymentRequestModel.REQUEST_PHONE) |
| && (propertyKey |
| != AssistantPaymentRequestModel.AVAILABLE_AUTOFILL_PAYMENT_METHODS)) { |
| return false; |
| } |
| |
| WebContents webContents = model.get(AssistantPaymentRequestModel.WEB_CONTENTS); |
| if (webContents == null) { |
| view.mContactDetailsSection.setEditor(null); |
| view.mPaymentMethodSection.setEditor(null); |
| view.mShippingAddressSection.setEditor(null); |
| return true; |
| } |
| |
| if (shouldShowContactDetails(model)) { |
| ContactEditor contactEditor = |
| new ContactEditor(model.get(AssistantPaymentRequestModel.REQUEST_NAME), |
| model.get(AssistantPaymentRequestModel.REQUEST_PHONE), |
| model.get(AssistantPaymentRequestModel.REQUEST_EMAIL), |
| !webContents.isIncognito()); |
| contactEditor.setEditorDialog(new EditorDialog(view.mActivity, null, |
| /*deleteRunnable =*/null)); |
| view.mContactDetailsSection.setEditor(contactEditor); |
| } |
| |
| AddressEditor addressEditor = new AddressEditor(AddressEditor.Purpose.PAYMENT_REQUEST, |
| /* saveToDisk= */ !webContents.isIncognito()); |
| addressEditor.setEditorDialog(new EditorDialog(view.mActivity, null, |
| /*deleteRunnable =*/null)); |
| |
| CardEditor cardEditor = new CardEditor(webContents, addressEditor, |
| /* includeOrgLabel= */ false, /* observerForTest= */ null); |
| Map<String, PaymentMethodData> paymentMethods = |
| model.get(AssistantPaymentRequestModel.SUPPORTED_PAYMENT_METHODS); |
| if (paymentMethods != null) { |
| for (Map.Entry<String, PaymentMethodData> entry : paymentMethods.entrySet()) { |
| cardEditor.addAcceptedPaymentMethodIfRecognized(entry.getValue()); |
| } |
| } |
| |
| EditorDialog cardEditorDialog = new EditorDialog(view.mActivity, null, |
| /*deleteRunnable =*/null); |
| if (ChromeVersionInfo.isBetaBuild() || ChromeVersionInfo.isStableBuild()) { |
| cardEditorDialog.disableScreenshots(); |
| } |
| cardEditor.setEditorDialog(cardEditorDialog); |
| |
| view.mShippingAddressSection.setEditor(addressEditor); |
| view.mPaymentMethodSection.setEditor(cardEditor); |
| return true; |
| } |
| |
| /** |
| * Updates the map of supported payment methods (identifier -> methodData), filtered by |
| * |SUPPORTED_BASIC_CARD_NETWORKS|. |
| */ |
| // TODO(crbug.com/806868): Move the logic to retrieve and filter payment methods to native. |
| private void updateAvailablePaymentMethods(AssistantPaymentRequestModel model) { |
| WebContents webContents = model.get(AssistantPaymentRequestModel.WEB_CONTENTS); |
| if (webContents == null) { |
| model.set( |
| AssistantPaymentRequestModel.SUPPORTED_PAYMENT_METHODS, Collections.emptyMap()); |
| return; |
| } |
| |
| // Only enable 'basic-card' payment method. |
| PaymentMethodData methodData = new PaymentMethodData(); |
| methodData.supportedMethod = BasicCardUtils.BASIC_CARD_METHOD_NAME; |
| |
| // Apply basic-card filter if specified |
| List<String> supportedBasicCardNetworks = |
| model.get(AssistantPaymentRequestModel.SUPPORTED_BASIC_CARD_NETWORKS); |
| if (supportedBasicCardNetworks != null && supportedBasicCardNetworks.size() > 0) { |
| ArrayList<Integer> filteredNetworks = new ArrayList<>(); |
| Map<String, Integer> networks = BasicCardUtils.getNetworkIdentifiers(); |
| for (int i = 0; i < supportedBasicCardNetworks.size(); i++) { |
| assert networks.containsKey(supportedBasicCardNetworks.get(i)); |
| filteredNetworks.add(networks.get(supportedBasicCardNetworks.get(i))); |
| } |
| |
| methodData.supportedNetworks = new int[filteredNetworks.size()]; |
| for (int i = 0; i < filteredNetworks.size(); i++) { |
| methodData.supportedNetworks[i] = filteredNetworks.get(i); |
| } |
| } |
| |
| Map<String, PaymentMethodData> supportedPaymentMethods = new HashMap<>(); |
| supportedPaymentMethods.put(BasicCardUtils.BASIC_CARD_METHOD_NAME, methodData); |
| model.set(AssistantPaymentRequestModel.SUPPORTED_PAYMENT_METHODS, supportedPaymentMethods); |
| } |
| |
| /** |
| * Updates the list of available autofill payment methods (i.e., the list of available payment |
| * methods available to the user as items in the UI). |
| */ |
| // TODO(crbug.com/806868): Move this logic to native. |
| private void updateAvailableAutofillPaymentMethods(AssistantPaymentRequestModel model) { |
| WebContents webContents = model.get(AssistantPaymentRequestModel.WEB_CONTENTS); |
| if (webContents == null) { |
| model.set(AssistantPaymentRequestModel.AVAILABLE_AUTOFILL_PAYMENT_METHODS, |
| Collections.emptyList()); |
| return; |
| } |
| |
| AutofillPaymentApp autofillPaymentApp = new AutofillPaymentApp(webContents); |
| Map<String, PaymentMethodData> supportedPaymentMethods = |
| model.get(AssistantPaymentRequestModel.SUPPORTED_PAYMENT_METHODS); |
| List<PaymentInstrument> paymentMethods = autofillPaymentApp.getInstruments( |
| supportedPaymentMethods != null ? supportedPaymentMethods : Collections.emptyMap(), |
| /*forceReturnServerCards=*/true); |
| |
| List<AutofillPaymentInstrument> availablePaymentMethods = new ArrayList<>(); |
| for (PaymentInstrument method : paymentMethods) { |
| if (method instanceof AutofillPaymentInstrument) { |
| availablePaymentMethods.add(((AutofillPaymentInstrument) method)); |
| } |
| } |
| model.set(AssistantPaymentRequestModel.AVAILABLE_AUTOFILL_PAYMENT_METHODS, |
| availablePaymentMethods); |
| } |
| } |