blob: 5aa98057bd704755b8996db22611a7fafb299096 [file] [log] [blame]
// Copyright 2016 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.payments;
import android.content.Context;
import android.os.Handler;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.collection.ArrayMap;
import org.chromium.base.Callback;
import org.chromium.base.LocaleUtils;
import org.chromium.base.Log;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.ChromeTabbedActivity;
import org.chromium.chrome.browser.autofill.PersonalDataManager;
import org.chromium.chrome.browser.autofill.PersonalDataManager.AutofillProfile;
import org.chromium.chrome.browser.autofill.PersonalDataManager.CreditCard;
import org.chromium.chrome.browser.autofill.PersonalDataManager.NormalizedAddressRequestDelegate;
import org.chromium.chrome.browser.compositor.layouts.EmptyOverviewModeObserver;
import org.chromium.chrome.browser.compositor.layouts.OverviewModeBehavior;
import org.chromium.chrome.browser.compositor.layouts.OverviewModeBehavior.OverviewModeObserver;
import org.chromium.chrome.browser.payments.handler.PaymentHandlerCoordinator;
import org.chromium.chrome.browser.payments.handler.PaymentHandlerCoordinator.PaymentHandlerUiObserver;
import org.chromium.chrome.browser.payments.handler.PaymentHandlerCoordinator.PaymentHandlerWebContentsObserver;
import org.chromium.chrome.browser.payments.minimal.MinimalUICoordinator;
import org.chromium.chrome.browser.payments.ui.ContactDetailsSection;
import org.chromium.chrome.browser.payments.ui.LineItem;
import org.chromium.chrome.browser.payments.ui.PaymentInformation;
import org.chromium.chrome.browser.payments.ui.PaymentRequestSection.OptionSection.FocusChangedObserver;
import org.chromium.chrome.browser.payments.ui.PaymentRequestUI;
import org.chromium.chrome.browser.payments.ui.PaymentRequestUI.SelectionResult;
import org.chromium.chrome.browser.payments.ui.SectionInformation;
import org.chromium.chrome.browser.payments.ui.ShoppingCart;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.settings.SettingsLauncher;
import org.chromium.chrome.browser.settings.SettingsLauncherImpl;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabSelectionType;
import org.chromium.chrome.browser.tabmodel.EmptyTabModelSelectorObserver;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.browser.tabmodel.TabModelObserver;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.tabmodel.TabModelSelectorObserver;
import org.chromium.chrome.browser.ui.favicon.FaviconHelper;
import org.chromium.components.autofill.Completable;
import org.chromium.components.autofill.EditableOption;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetControllerProvider;
import org.chromium.components.embedder_support.util.UrlConstants;
import org.chromium.components.page_info.CertificateChainHelper;
import org.chromium.components.payments.AbortReason;
import org.chromium.components.payments.ComponentPaymentRequestImpl;
import org.chromium.components.payments.ComponentPaymentRequestImpl.ComponentPaymentRequestDelegate;
import org.chromium.components.payments.CurrencyFormatter;
import org.chromium.components.payments.ErrorMessageUtil;
import org.chromium.components.payments.ErrorStrings;
import org.chromium.components.payments.Event;
import org.chromium.components.payments.JourneyLogger;
import org.chromium.components.payments.MethodStrings;
import org.chromium.components.payments.NotShownReason;
import org.chromium.components.payments.OriginSecurityChecker;
import org.chromium.components.payments.PackageManagerDelegate;
import org.chromium.components.payments.PayerData;
import org.chromium.components.payments.PaymentApp;
import org.chromium.components.payments.PaymentAppType;
import org.chromium.components.payments.PaymentDetailsConverter;
import org.chromium.components.payments.PaymentDetailsUpdateServiceHelper;
import org.chromium.components.payments.PaymentFeatureList;
import org.chromium.components.payments.PaymentHandlerHost;
import org.chromium.components.payments.PaymentRequestSpec;
import org.chromium.components.payments.PaymentRequestUpdateEventListener;
import org.chromium.components.payments.PaymentValidator;
import org.chromium.components.payments.Section;
import org.chromium.components.payments.UrlUtil;
import org.chromium.components.security_state.SecurityStateModel;
import org.chromium.components.url_formatter.UrlFormatter;
import org.chromium.content_public.browser.RenderFrameHost;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.browser.WebContentsStatics;
import org.chromium.mojo.system.MojoException;
import org.chromium.payments.mojom.AddressErrors;
import org.chromium.payments.mojom.CanMakePaymentQueryResult;
import org.chromium.payments.mojom.HasEnrolledInstrumentQueryResult;
import org.chromium.payments.mojom.PayerDetail;
import org.chromium.payments.mojom.PayerErrors;
import org.chromium.payments.mojom.PaymentAddress;
import org.chromium.payments.mojom.PaymentComplete;
import org.chromium.payments.mojom.PaymentCurrencyAmount;
import org.chromium.payments.mojom.PaymentDetails;
import org.chromium.payments.mojom.PaymentDetailsModifier;
import org.chromium.payments.mojom.PaymentErrorReason;
import org.chromium.payments.mojom.PaymentItem;
import org.chromium.payments.mojom.PaymentMethodData;
import org.chromium.payments.mojom.PaymentOptions;
import org.chromium.payments.mojom.PaymentRequest;
import org.chromium.payments.mojom.PaymentRequestClient;
import org.chromium.payments.mojom.PaymentResponse;
import org.chromium.payments.mojom.PaymentShippingOption;
import org.chromium.payments.mojom.PaymentShippingType;
import org.chromium.payments.mojom.PaymentValidationErrors;
import org.chromium.url.GURL;
import org.chromium.url.Origin;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
/**
* This is the Clank specific parts of {@link PaymentRequest}, with the parts shared with WebLayer
* living in {@link ComponentPaymentRequestImpl}.
*/
public class PaymentRequestImpl
implements ComponentPaymentRequestDelegate, PaymentRequestUI.Client,
PaymentAppFactoryDelegate, PaymentAppFactoryParams,
PaymentRequestUpdateEventListener, PaymentApp.AbortCallback,
PaymentApp.InstrumentDetailsCallback,
PaymentResponseHelper.PaymentResponseRequesterDelegate, FocusChangedObserver,
NormalizedAddressRequestDelegate, SettingsAutofillAndPaymentsObserver.Observer,
PaymentDetailsConverter.MethodChecker, PaymentHandlerUiObserver {
/**
* A delegate to ask questions about the system, that allows tests to inject behaviour without
* having to modify the entire system. This partially mirrors a similar C++
* (Content)PaymentRequestDelegate for the C++ implementation, allowing the test harness to
* override behaviour in both in a similar fashion.
*/
public interface Delegate {
/**
* Returns whether the ChromeActivity is currently showing an OffTheRecord tab.
*/
boolean isOffTheRecord(ChromeActivity activity);
/**
* Returns a non-null string if there is an invalid SSL certificate on the currently
* loaded page.
*/
String getInvalidSslCertificateErrorMessage();
/**
* Returns true if the web contents that initiated the payment request is active.
*/
boolean isWebContentsActive(@NonNull ChromeActivity activity);
/**
* Returns whether the preferences allow CAN_MAKE_PAYMENT.
*/
boolean prefsCanMakePayment();
/**
* Returns true if the UI can be skipped for "basic-card" scenarios. This will only ever
* be true in tests.
*/
boolean skipUiForBasicCard();
/**
* If running inside of a Trusted Web Activity, returns the package name for Trusted Web
* Activity. Otherwise returns an empty string or null.
*/
@Nullable
String getTwaPackageName(@Nullable ChromeActivity activity);
}
/**
* This class is to coordinate the show state of a bottom sheet UI (either expandable payment
* handler or minimal UI) and the Payment Request UI so that these visibility rules are
* enforced:
* 1. At most one UI is shown at any moment in case the Payment Request UI obstructs the bottom
* sheet.
* 2. Bottom sheet is prioritized to show over Payment Request UI
*/
public class PaymentUisShowStateReconciler {
// Whether the bottom sheet is showing.
private boolean mShowingBottomSheet;
// Whether to show the Payment Request UI when the bottom sheet is not being shown.
private boolean mShouldShowDialog;
/**
* Show the Payment Request UI dialog when the bottom sheet is hidden, i.e., if the bottom
* sheet hidden, show the dialog immediately; otherwise, show the dialog after the bottom
* sheet hides.
*/
public void showPaymentRequestDialogWhenNoBottomSheet() {
mShouldShowDialog = true;
updatePaymentRequestDialogShowState();
}
/** Hide the Payment Request UI dialog. */
public void hidePaymentRequestDialog() {
mShouldShowDialog = false;
updatePaymentRequestDialogShowState();
}
/** A callback invoked when the Payment Request UI is closed. */
/* package */ void onPaymentRequestUiClosed() {
assert mUI == null;
mShouldShowDialog = false;
}
/** A callback invoked when the bottom sheet is shown, to enforce the visibility rules. */
public void onBottomSheetShown() {
mShowingBottomSheet = true;
updatePaymentRequestDialogShowState();
}
/** A callback invoked when the bottom sheet is hidden, to enforce the visibility rules. */
public void onBottomSheetClosed() {
mShowingBottomSheet = false;
updatePaymentRequestDialogShowState();
}
private void updatePaymentRequestDialogShowState() {
if (mUI == null) return;
mUI.setVisible(!mShowingBottomSheet && mShouldShowDialog);
}
}
/**
* A test-only observer for the PaymentRequest service implementation.
*/
public interface PaymentRequestServiceObserverForTest {
/**
* Called after an instance of PaymentRequestImpl has been created.
*
* @param paymentRequest The newly created instance of PaymentRequestImpl.
*/
void onPaymentRequestCreated(PaymentRequestImpl paymentRequest);
/**
* Called when an abort request was denied.
*/
void onPaymentRequestServiceUnableToAbort();
/**
* Called when the controller is notified of billing address change, but does not alter the
* editor UI.
*/
void onPaymentRequestServiceBillingAddressChangeProcessed();
/**
* Called when the controller is notified of an expiration month change.
*/
void onPaymentRequestServiceExpirationMonthChange();
/**
* Called when a show request failed. This can happen when:
* <ul>
* <li>The merchant requests only unsupported payment methods.</li>
* <li>The merchant requests only payment methods that don't have corresponding apps and
* are not able to add a credit card from PaymentRequest UI.</li>
* </ul>
*/
void onPaymentRequestServiceShowFailed();
/**
* Called when the canMakePayment() request has been responded to.
*/
void onPaymentRequestServiceCanMakePaymentQueryResponded();
/**
* Called when the hasEnrolledInstrument() request has been responded to.
*/
void onPaymentRequestServiceHasEnrolledInstrumentQueryResponded();
/**
* Called when the payment response is ready.
*/
void onPaymentResponseReady();
/**
* Called when the browser acknowledges the renderer's complete call, which indicates that
* the browser UI has closed.
*/
void onCompleteReplied();
/**
* Called when the renderer is closing the mojo connection (e.g. upon show promise
* rejection).
*/
void onRendererClosedMojoConnection();
}
/**
* An observer interface injected when running tests to allow them to observe events.
* This interface holds events that should be passed back to the native C++ test
* harness and mirrors the C++ PaymentRequest::ObserverForTest() interface. Its methods
* should be called in the same places that the C++ PaymentRequest object will call its
* ObserverForTest.
*/
public interface NativeObserverForTest {
void onCanMakePaymentCalled();
void onCanMakePaymentReturned();
void onHasEnrolledInstrumentCalled();
void onHasEnrolledInstrumentReturned();
void onAppListReady(@Nullable List<EditableOption> paymentApps, PaymentItem total);
void onNotSupportedError();
void onConnectionTerminated();
void onAbortCalled();
void onCompleteCalled();
void onMinimalUIReady();
}
/** Limit in the number of suggested items in a section. */
public static final int SUGGESTIONS_LIMIT = 4;
private static final String TAG = "PaymentRequest";
// Reverse order of the comparator to sort in descending order of completeness scores.
private static final Comparator<Completable> COMPLETENESS_COMPARATOR =
(a, b) -> (compareCompletablesByCompleteness(b, a));
private ComponentPaymentRequestImpl mComponentPaymentRequestImpl;
private PaymentOptions mPaymentOptions;
private boolean mRequestShipping;
private boolean mRequestPayerName;
private boolean mRequestPayerPhone;
private boolean mRequestPayerEmail;
/**
* Sorts the payment apps by several rules:
* Rule 1: Non-autofill before autofill.
* Rule 2: Complete apps before incomplete apps.
* Rule 3: When shipping address is requested, apps which will handle shipping address before
* others.
* Rule 4: When payer's contact information is requested, apps which will handle more required
* contact fields (name, email, phone) come before others.
* Rule 5: Preselectable apps before non-preselectable apps.
* Rule 6: Frequently and recently used apps before rarely and non-recently used apps.
*/
private final Comparator<PaymentApp> mPaymentAppComparator = (a, b) -> {
// Non-autofill apps first.
int autofill = (a.isAutofillInstrument() ? 1 : 0) - (b.isAutofillInstrument() ? 1 : 0);
if (autofill != 0) return autofill;
// Complete cards before cards with missing information.
int completeness = compareCompletablesByCompleteness(b, a);
if (completeness != 0) return completeness;
// Payment apps which handle shipping address before others.
if (mRequestShipping) {
int canHandleShipping =
(b.handlesShippingAddress() ? 1 : 0) - (a.handlesShippingAddress() ? 1 : 0);
if (canHandleShipping != 0) return canHandleShipping;
}
// Payment apps which handle more contact information fields come first.
int aSupportedContactDelegationsNum = 0;
int bSupportedContactDelegationsNum = 0;
if (mRequestPayerName) {
if (a.handlesPayerName()) aSupportedContactDelegationsNum++;
if (b.handlesPayerName()) bSupportedContactDelegationsNum++;
}
if (mRequestPayerEmail) {
if (a.handlesPayerEmail()) aSupportedContactDelegationsNum++;
if (b.handlesPayerEmail()) bSupportedContactDelegationsNum++;
}
if (mRequestPayerPhone) {
if (a.handlesPayerPhone()) aSupportedContactDelegationsNum++;
if (b.handlesPayerPhone()) bSupportedContactDelegationsNum++;
}
if (bSupportedContactDelegationsNum != aSupportedContactDelegationsNum) {
return bSupportedContactDelegationsNum - aSupportedContactDelegationsNum > 0 ? 1 : -1;
}
// Preselectable apps before non-preselectable apps.
// Note that this only affects service worker payment apps' apps for now
// since autofill cards have already been sorted by preselect after sorting by completeness.
// And the other payment apps can always be preselected.
int canPreselect = (b.canPreselect() ? 1 : 0) - (a.canPreselect() ? 1 : 0);
if (canPreselect != 0) return canPreselect;
// More frequently and recently used apps first.
return compareAppsByFrecency(b, a);
};
private static PaymentRequestServiceObserverForTest sObserverForTest;
private static boolean sIsLocalCanMakePaymentQueryQuotaEnforcedForTest;
/**
* Hold the currently showing PaymentRequest. Used to prevent showing more than one
* PaymentRequest UI per browser process.
*/
private static PaymentRequestImpl sShowingPaymentRequest;
/** Monitors changes in the TabModelSelector. */
private final TabModelSelectorObserver mSelectorObserver = new EmptyTabModelSelectorObserver() {
@Override
public void onTabModelSelected(TabModel newModel, TabModel oldModel) {
mJourneyLogger.setAborted(AbortReason.ABORTED_BY_USER);
disconnectFromClientWithDebugMessage(ErrorStrings.TAB_SWITCH);
}
};
/** Monitors changes in the current TabModel. */
private final TabModelObserver mTabModelObserver = new TabModelObserver() {
@Override
public void didSelectTab(Tab tab, @TabSelectionType int type, int lastId) {
if (tab == null || tab.getId() != lastId) {
mJourneyLogger.setAborted(AbortReason.ABORTED_BY_USER);
disconnectFromClientWithDebugMessage(ErrorStrings.TAB_SWITCH);
}
}
};
/** Monitors changes in the tab overview. */
private final OverviewModeObserver mOverviewModeObserver = new EmptyOverviewModeObserver() {
@Override
public void onOverviewModeStartedShowing(boolean showToolbar) {
mJourneyLogger.setAborted(AbortReason.ABORTED_BY_USER);
disconnectFromClientWithDebugMessage(ErrorStrings.TAB_OVERVIEW_MODE);
}
};
private final Handler mHandler = new Handler();
private final RenderFrameHost mRenderFrameHost;
private final Delegate mDelegate;
private final NativeObserverForTest mNativeObserverForTest;
private final WebContents mWebContents;
private final String mTopLevelOrigin;
private final String mPaymentRequestOrigin;
private final Origin mPaymentRequestSecurityOrigin;
private final String mMerchantName;
@Nullable
private final byte[][] mCertificateChain;
private final AddressEditor mAddressEditor;
private final CardEditor mCardEditor;
private final JourneyLogger mJourneyLogger;
private final boolean mIsOffTheRecord;
private List<AutofillProfile> mAutofillProfiles;
private boolean mHaveRequestedAutofillData = true;
private boolean mIsCanMakePaymentResponsePending;
private boolean mIsHasEnrolledInstrumentResponsePending;
private boolean mHasEnrolledInstrumentUsesPerMethodQuota;
private boolean mIsCurrentPaymentRequestShowing;
private boolean mWasRetryCalled;
private final Queue<Runnable> mRetryQueue = new LinkedList<>();
/**
* The raw total amount being charged, as it was received from the website. This data is passed
* to the payment app.
*/
private PaymentItem mRawTotal;
/**
* The raw items in the shopping cart, as they were received from the website. This data is
* passed to the payment app.
*/
private List<PaymentItem> mRawLineItems;
/**
* The raw shipping options, as it was received from the website. This data is passed to the
* payment app when the app is responsible for handling shipping address.
*/
private List<PaymentShippingOption> mRawShippingOptions;
/**
* A mapping from method names to modifiers, which include modified totals and additional line
* items. Used to display modified totals for each payment apps, modified total in order
* summary, and additional line items in order summary.
*/
private Map<String, PaymentDetailsModifier> mModifiers;
/**
* The UI model of the shopping cart, including the total. Each item includes a label and a
* price string. This data is passed to the UI.
*/
private ShoppingCart mUiShoppingCart;
/**
* The UI model for the shipping options. Includes the label and sublabel for each shipping
* option. Also keeps track of the selected shipping option. This data is passed to the UI.
*/
private SectionInformation mUiShippingOptions;
private PaymentRequestSpec mSpec;
private String mId;
private Map<String, PaymentMethodData> mMethodData;
private int mShippingType;
private SectionInformation mShippingAddressesSection;
private ContactDetailsSection mContactSection;
private boolean mIsFinishedQueryingPaymentApps;
private AutofillPaymentAppCreator mAutofillPaymentAppCreator;
private List<PaymentApp> mPendingApps = new ArrayList<>();
private SectionInformation mPaymentMethodsSection;
private PaymentRequestUI mUI;
private MinimalUICoordinator mMinimalUi;
private Callback<PaymentInformation> mPaymentInformationCallback;
private PaymentApp mInvokedPaymentApp;
private PaymentHandlerCoordinator mPaymentHandlerUi;
private PaymentUisShowStateReconciler mPaymentUisShowStateReconciler;
private boolean mMerchantSupportsAutofillCards;
private boolean mUserCanAddCreditCard;
private boolean mHideServerAutofillCards;
private ContactEditor mContactEditor;
private boolean mHasRecordedAbortReason;
private boolean mWaitForUpdatedDetails;
private Map<String, CurrencyFormatter> mCurrencyFormatterMap;
private TabModelSelector mObservedTabModelSelector;
private TabModel mObservedTabModel;
private OverviewModeBehavior mOverviewModeBehavior;
private PaymentHandlerHost mPaymentHandlerHost;
/**
* True when at least one url payment method identifier is specified in payment request.
*/
private boolean mURLPaymentMethodIdentifiersSupported;
/**
* A mapping of the payment method names to the corresponding payment method specific data. If
* STRICT_HAS_ENROLLED_AUTOFILL_INSTRUMENT is enabled, then the key "basic-card-payment-options"
* also maps to the following payment options:
* - requestPayerEmail
* - requestPayerName
* - requestPayerPhone
* - requestShipping
*/
private Map<String, PaymentMethodData> mQueryForQuota;
/**
* There are a few situations were the Payment Request can appear, from a code perspective, to
* be shown more than once. This boolean is used to make sure it is only logged once.
*/
private boolean mDidRecordShowEvent;
/** True if any of the requested payment methods are supported. */
private boolean mCanMakePayment;
/**
* True after at least one usable payment app has been found and the setting allows querying
* this value. This value can be used to respond to hasEnrolledInstrument(). Should be read only
* after all payment apps have been queried.
*/
private boolean mHasEnrolledInstrument;
/**
* Whether there's at least one app that is not an autofill card. Should be read only after all
* payment apps have been queried.
*/
private boolean mHasNonAutofillApp;
/**
* True if we should skip showing PaymentRequest UI.
*
* <p>In cases where there is a single payment app and the merchant does not request shipping
* or billing, we can skip showing UI as Payment Request UI is not benefiting the user at all.
*/
private boolean mShouldSkipShowingPaymentRequestUi;
/** Whether PaymentRequest.show() was invoked with a user gesture. */
private boolean mIsUserGestureShow;
/** The helper to create and fill the response to send to the merchant. */
private PaymentResponseHelper mPaymentResponseHelper;
/** If not empty, use this error message for rejecting PaymentRequest.show(). */
private String mRejectShowErrorMessage;
/**
* True when Payment Request is invoked on a prohibited origin (e.g., blob:) or with an invalid
* SSL certificate (e.g., self-signed).
*/
private boolean mIsProhibitedOriginOrInvalidSsl;
/** A helper to manage the Skip-to-GPay experimental flow. */
private SkipToGPayHelper mSkipToGPayHelper;
/**
* When true skip UI is available for non-url based payment method identifiers (e.g.
* basic-card).
*/
private boolean mSkipUiForNonUrlPaymentMethodIdentifiers;
/**
* Builds the PaymentRequest service implementation.
*
* @param renderFrameHost The host of the frame that has invoked the PaymentRequest API.
*/
public PaymentRequestImpl(RenderFrameHost renderFrameHost, Delegate delegate,
NativeObserverForTest nativeObserver) {
assert renderFrameHost != null;
mRenderFrameHost = renderFrameHost;
mDelegate = delegate;
mNativeObserverForTest = nativeObserver;
mWebContents = WebContentsStatics.fromRenderFrameHost(renderFrameHost);
mPaymentRequestOrigin =
UrlFormatter.formatUrlForSecurityDisplay(mRenderFrameHost.getLastCommittedURL());
mPaymentRequestSecurityOrigin = mRenderFrameHost.getLastCommittedOrigin();
mTopLevelOrigin =
UrlFormatter.formatUrlForSecurityDisplay(mWebContents.getLastCommittedUrl());
mMerchantName = mWebContents.getTitle();
mCertificateChain = CertificateChainHelper.getCertificateChain(mWebContents);
mIsOffTheRecord = mDelegate.isOffTheRecord(ChromeActivity.fromWebContents(mWebContents));
// Do not persist changes on disk in OffTheRecord mode.
mAddressEditor = new AddressEditor(
AddressEditor.Purpose.PAYMENT_REQUEST, /*saveToDisk=*/!mIsOffTheRecord);
// PaymentRequest card editor does not show the organization name in the dropdown with the
// billing address labels.
mCardEditor = new CardEditor(
mWebContents, mAddressEditor, /*includeOrgLabel=*/false, sObserverForTest);
mJourneyLogger = new JourneyLogger(mIsOffTheRecord, mWebContents);
mCurrencyFormatterMap = new HashMap<>();
mSkipUiForNonUrlPaymentMethodIdentifiers = mDelegate.skipUiForBasicCard();
if (sObserverForTest != null) sObserverForTest.onPaymentRequestCreated(this);
mPaymentUisShowStateReconciler = new PaymentUisShowStateReconciler();
}
// Implement ComponentPaymentRequestDelegate:
@Override
public void setComponentPaymentRequestImpl(
ComponentPaymentRequestImpl componentPaymentRequestImpl) {
assert mComponentPaymentRequestImpl == null;
assert componentPaymentRequestImpl != null;
mComponentPaymentRequestImpl = componentPaymentRequestImpl;
}
// Implement ComponentPaymentRequestDelegate:
/**
* Called by the merchant website to initialize the payment request data.
*/
@Override
public void init(PaymentMethodData[] methodData, PaymentDetails details, PaymentOptions options,
boolean googlePayBridgeEligible) {
assert getClient() != null;
mMethodData = new HashMap<>();
if (!OriginSecurityChecker.isOriginSecure(mWebContents.getLastCommittedUrl())) {
mJourneyLogger.setAborted(AbortReason.INVALID_DATA_FROM_RENDERER);
disconnectFromClientWithDebugMessage(ErrorStrings.NOT_IN_A_SECURE_ORIGIN);
return;
}
mPaymentOptions = options;
mRequestShipping = options != null && options.requestShipping;
mRequestPayerName = options != null && options.requestPayerName;
mRequestPayerPhone = options != null && options.requestPayerPhone;
mRequestPayerEmail = options != null && options.requestPayerEmail;
mShippingType = options == null ? PaymentShippingType.SHIPPING : options.shippingType;
// TODO(crbug.com/978471): Improve architecture for handling prohibited origins and invalid
// SSL certificates.
if (!UrlUtil.isOriginAllowedToUseWebPaymentApis(mWebContents.getLastCommittedUrl())) {
mIsProhibitedOriginOrInvalidSsl = true;
mRejectShowErrorMessage = ErrorStrings.PROHIBITED_ORIGIN;
Log.d(TAG, mRejectShowErrorMessage);
Log.d(TAG, ErrorStrings.PROHIBITED_ORIGIN_OR_INVALID_SSL_EXPLANATION);
// Don't show any UI. Resolve .canMakePayment() with "false". Reject .show() with
// "NotSupportedError".
mQueryForQuota = new HashMap<>();
onDoneCreatingPaymentApps(/*factory=*/null);
return;
}
mJourneyLogger.setRequestedInformation(
mRequestShipping, mRequestPayerEmail, mRequestPayerPhone, mRequestPayerName);
assert mRejectShowErrorMessage == null;
mRejectShowErrorMessage = mDelegate.getInvalidSslCertificateErrorMessage();
if (!TextUtils.isEmpty(mRejectShowErrorMessage)) {
mIsProhibitedOriginOrInvalidSsl = true;
Log.d(TAG, mRejectShowErrorMessage);
Log.d(TAG, ErrorStrings.PROHIBITED_ORIGIN_OR_INVALID_SSL_EXPLANATION);
// Don't show any UI. Resolve .canMakePayment() with "false". Reject .show() with
// "NotSupportedError".
mQueryForQuota = new HashMap<>();
onDoneCreatingPaymentApps(/*factory=*/null);
return;
}
boolean googlePayBridgeActivated = googlePayBridgeEligible
&& SkipToGPayHelper.canActivateExperiment(mWebContents, methodData);
mMethodData = getValidatedMethodData(methodData, googlePayBridgeActivated, mCardEditor);
if (mMethodData == null) {
mJourneyLogger.setAborted(AbortReason.INVALID_DATA_FROM_RENDERER);
disconnectFromClientWithDebugMessage(ErrorStrings.INVALID_PAYMENT_METHODS_OR_DATA);
return;
}
if (googlePayBridgeActivated) {
PaymentMethodData data = mMethodData.get(MethodStrings.GOOGLE_PAY);
mSkipToGPayHelper = new SkipToGPayHelper(options, data.gpayBridgeData);
}
mQueryForQuota = new HashMap<>(mMethodData);
if (mQueryForQuota.containsKey(MethodStrings.BASIC_CARD)
&& PaymentFeatureList.isEnabledOrExperimentalFeaturesEnabled(
PaymentFeatureList.STRICT_HAS_ENROLLED_AUTOFILL_INSTRUMENT)) {
PaymentMethodData paymentMethodData = new PaymentMethodData();
paymentMethodData.stringifiedData = String.format(
"{payerEmail:%s,payerName:%s,payerPhone:%s,shipping:%s}", mRequestPayerEmail,
mRequestPayerName, mRequestPayerPhone, mRequestShipping);
mQueryForQuota.put("basic-card-payment-options", paymentMethodData);
}
if (!parseAndValidateDetailsOrDisconnectFromClient(details)) return;
mSpec = new PaymentRequestSpec(mPaymentOptions, details, mMethodData.values(),
LocaleUtils.getDefaultLocaleString());
if (mRawTotal == null) {
mJourneyLogger.setAborted(AbortReason.INVALID_DATA_FROM_RENDERER);
disconnectFromClientWithDebugMessage(ErrorStrings.TOTAL_REQUIRED);
return;
}
mId = details.id;
// Checks whether the merchant supports autofill cards before show is called.
mMerchantSupportsAutofillCards =
AutofillPaymentAppFactory.merchantSupportsBasicCard(mMethodData);
// If in strict mode, don't give user an option to add an autofill card during the checkout
// to avoid the "unhappy" basic-card flow.
mUserCanAddCreditCard = mMerchantSupportsAutofillCards
&& !PaymentFeatureList.isEnabledOrExperimentalFeaturesEnabled(
PaymentFeatureList.STRICT_HAS_ENROLLED_AUTOFILL_INSTRUMENT);
if (mRequestShipping || mRequestPayerName || mRequestPayerPhone || mRequestPayerEmail) {
mAutofillProfiles = Collections.unmodifiableList(
PersonalDataManager.getInstance().getProfilesToSuggest(
false /* includeNameInLabel */));
}
if (mRequestShipping) {
boolean haveCompleteShippingAddress = false;
for (int i = 0; i < mAutofillProfiles.size(); i++) {
if (AutofillAddress.checkAddressCompletionStatus(
mAutofillProfiles.get(i), AutofillAddress.CompletenessCheckType.NORMAL)
== AutofillAddress.CompletionStatus.COMPLETE) {
haveCompleteShippingAddress = true;
break;
}
}
mHaveRequestedAutofillData &= haveCompleteShippingAddress;
}
if (mRequestPayerName || mRequestPayerPhone || mRequestPayerEmail) {
// Do not persist changes on disk in OffTheRecord mode.
mContactEditor = new ContactEditor(mRequestPayerName, mRequestPayerPhone,
mRequestPayerEmail, /*saveToDisk=*/!mIsOffTheRecord);
boolean haveCompleteContactInfo = false;
for (int i = 0; i < mAutofillProfiles.size(); i++) {
AutofillProfile profile = mAutofillProfiles.get(i);
if (mContactEditor.checkContactCompletionStatus(profile.getFullName(),
profile.getPhoneNumber(), profile.getEmailAddress())
== ContactEditor.COMPLETE) {
haveCompleteContactInfo = true;
break;
}
}
mHaveRequestedAutofillData &= haveCompleteContactInfo;
}
PaymentAppService.getInstance().create(/*delegate=*/this);
// Log the various types of payment methods that were requested by the merchant.
boolean requestedMethodGoogle = false;
// Not to record requestedMethodBasicCard because JourneyLogger ignore the case where the
// specified networks are unsupported. mMerchantSupportsAutofillCards better
// captures this group of interest than requestedMethodBasicCard.
boolean requestedMethodOther = false;
mURLPaymentMethodIdentifiersSupported = false;
for (String methodName : mMethodData.keySet()) {
switch (methodName) {
case MethodStrings.ANDROID_PAY:
case MethodStrings.GOOGLE_PAY:
mURLPaymentMethodIdentifiersSupported = true;
requestedMethodGoogle = true;
break;
case MethodStrings.BASIC_CARD:
// Not to record requestedMethodBasicCard because
// mMerchantSupportsAutofillCards is used instead.
break;
default:
// "Other" includes https url, http url(when certifate check is bypassed) and
// the unlisted methods defined in {@link MethodStrings}.
requestedMethodOther = true;
if (methodName.startsWith(UrlConstants.HTTPS_URL_PREFIX)
|| methodName.startsWith(UrlConstants.HTTP_URL_PREFIX)) {
mURLPaymentMethodIdentifiersSupported = true;
}
}
}
mJourneyLogger.setRequestedPaymentMethodTypes(
/*requestedBasicCard=*/mMerchantSupportsAutofillCards, requestedMethodGoogle,
requestedMethodOther);
}
/**
* Calculate whether the browser payment sheet should be skipped directly into the payment app.
*/
private void calculateWhetherShouldSkipShowingPaymentRequestUi() {
// This should be called after all payment apps are ready and request.show() is called,
// since only then whether or not should skip payment sheet UI is determined.
assert mIsFinishedQueryingPaymentApps;
assert mIsCurrentPaymentRequestShowing;
assert mPaymentMethodsSection != null;
PaymentApp selectedApp = (PaymentApp) mPaymentMethodsSection.getSelectedItem();
// If there is only a single payment app which can provide all merchant requested
// information, we can safely go directly to the payment app instead of showing Payment
// Request UI.
mShouldSkipShowingPaymentRequestUi =
PaymentFeatureList.isEnabled(PaymentFeatureList.WEB_PAYMENTS_SINGLE_APP_UI_SKIP)
// Only allowing payment apps that own their own UIs.
// This excludes AutofillPaymentInstrument as its UI is rendered inline in
// the payment request UI, thus can't be skipped.
&& (mURLPaymentMethodIdentifiersSupported
|| mSkipUiForNonUrlPaymentMethodIdentifiers)
&& mPaymentMethodsSection.getSize() >= 1
&& onlySingleAppCanProvideAllRequiredInformation()
// Skip to payment app only if it can be pre-selected.
&& selectedApp != null
// Skip to payment app only if user gesture is provided when it is required to
// skip-UI.
&& (mIsUserGestureShow || !selectedApp.isUserGestureRequiredToSkipUi());
}
/**
* @return true when there is exactly one available payment app which can provide all requested
* information including shipping address and payer's contact information whenever needed.
*/
private boolean onlySingleAppCanProvideAllRequiredInformation() {
assert mPaymentMethodsSection != null;
if (!mRequestShipping && !mRequestPayerName && !mRequestPayerPhone && !mRequestPayerEmail) {
return mPaymentMethodsSection.getSize() == 1
&& !((PaymentApp) mPaymentMethodsSection.getItem(0)).isAutofillInstrument();
}
boolean anAppCanProvideAllInfo = false;
int sectionSize = mPaymentMethodsSection.getSize();
for (int i = 0; i < sectionSize; i++) {
PaymentApp app = (PaymentApp) mPaymentMethodsSection.getItem(i);
if ((!mRequestShipping || app.handlesShippingAddress())
&& (!mRequestPayerName || app.handlesPayerName())
&& (!mRequestPayerPhone || app.handlesPayerPhone())
&& (!mRequestPayerEmail || app.handlesPayerEmail())) {
// There is more than one available app that can provide all merchant requested
// information information.
if (anAppCanProvideAllInfo) return false;
anAppCanProvideAllInfo = true;
}
}
return anAppCanProvideAllInfo;
}
/** @return Whether the UI was built. */
private boolean buildUI(ChromeActivity activity) {
// Payment methods section must be ready before building the rest of the UI. This is because
// shipping and contact sections (when requested by merchant) are populated depending on
// whether or not the selected payment app (if such exists) can provide the required
// information.
assert mPaymentMethodsSection != null;
assert activity != null;
// Catch any time the user switches tabs. Because the dialog is modal, a user shouldn't be
// allowed to switch tabs, which can happen if the user receives an external Intent.
mObservedTabModelSelector = activity.getTabModelSelector();
mObservedTabModel = activity.getCurrentTabModel();
mObservedTabModelSelector.addObserver(mSelectorObserver);
mObservedTabModel.addObserver(mTabModelObserver);
// Only the currently selected tab is allowed to show the payment UI.
if (!mDelegate.isWebContentsActive(activity)) {
mJourneyLogger.setNotShown(NotShownReason.OTHER);
disconnectFromClientWithDebugMessage(ErrorStrings.CANNOT_SHOW_IN_BACKGROUND_TAB);
if (sObserverForTest != null) sObserverForTest.onPaymentRequestServiceShowFailed();
return false;
}
// Catch any time the user enters the overview mode and dismiss the payment UI.
if (activity instanceof ChromeTabbedActivity) {
mOverviewModeBehavior = ((ChromeTabbedActivity) activity).getOverviewModeBehavior();
if (mOverviewModeBehavior.overviewVisible()) {
mJourneyLogger.setNotShown(NotShownReason.OTHER);
disconnectFromClientWithDebugMessage(ErrorStrings.TAB_OVERVIEW_MODE);
if (sObserverForTest != null) sObserverForTest.onPaymentRequestServiceShowFailed();
return false;
}
mOverviewModeBehavior.addOverviewModeObserver(mOverviewModeObserver);
}
if (shouldShowShippingSection() && !mWaitForUpdatedDetails) {
createShippingSection(activity, mAutofillProfiles);
}
if (shouldShowContactSection()) {
mContactSection = new ContactDetailsSection(
activity, mAutofillProfiles, mContactEditor, mJourneyLogger);
}
mUI = new PaymentRequestUI(activity, this, mMerchantSupportsAutofillCards,
!PaymentPreferencesUtil.isPaymentCompleteOnce(), mMerchantName, mTopLevelOrigin,
SecurityStateModel.getSecurityLevelForWebContents(mWebContents),
new ShippingStrings(mShippingType), mPaymentUisShowStateReconciler,
Profile.fromWebContents(mWebContents));
activity.getLifecycleDispatcher().register(
mUI); // registered as a PauseResumeWithNativeObserver
final FaviconHelper faviconHelper = new FaviconHelper();
faviconHelper.getLocalFaviconImageForURL(Profile.fromWebContents(mWebContents),
mWebContents.getLastCommittedUrl(),
activity.getResources().getDimensionPixelSize(R.dimen.payments_favicon_size),
(bitmap, iconUrl) -> {
PaymentRequestClient client = getClient();
if (client != null && bitmap == null) {
client.warnNoFavicon();
}
if (mUI != null && bitmap != null) mUI.setTitleBitmap(bitmap);
faviconHelper.destroy();
});
// Add the callback to change the label of shipping addresses depending on the focus.
if (mRequestShipping) mUI.setShippingAddressSectionFocusChangedObserver(this);
mAddressEditor.setEditorDialog(mUI.getEditorDialog());
mCardEditor.setEditorDialog(mUI.getCardEditorDialog());
if (mContactEditor != null) mContactEditor.setEditorDialog(mUI.getEditorDialog());
return true;
}
private void createShippingSection(
Context context, List<AutofillProfile> unmodifiableProfiles) {
List<AutofillAddress> addresses = new ArrayList<>();
for (int i = 0; i < unmodifiableProfiles.size(); i++) {
AutofillProfile profile = unmodifiableProfiles.get(i);
mAddressEditor.addPhoneNumberIfValid(profile.getPhoneNumber());
// Only suggest addresses that have a street address.
if (!TextUtils.isEmpty(profile.getStreetAddress())) {
addresses.add(new AutofillAddress(context, profile));
}
}
// Suggest complete addresses first.
Collections.sort(addresses, COMPLETENESS_COMPARATOR);
// Limit the number of suggestions.
addresses = addresses.subList(0, Math.min(addresses.size(), SUGGESTIONS_LIMIT));
// Load the validation rules for each unique region code.
Set<String> uniqueCountryCodes = new HashSet<>();
for (int i = 0; i < addresses.size(); ++i) {
String countryCode = AutofillAddress.getCountryCode(addresses.get(i).getProfile());
if (!uniqueCountryCodes.contains(countryCode)) {
uniqueCountryCodes.add(countryCode);
PersonalDataManager.getInstance().loadRulesForAddressNormalization(countryCode);
}
}
// Automatically select the first address if one is complete and if the merchant does
// not require a shipping address to calculate shipping costs.
boolean hasCompleteShippingAddress = !addresses.isEmpty() && addresses.get(0).isComplete();
int firstCompleteAddressIndex = SectionInformation.NO_SELECTION;
if (mUiShippingOptions.getSelectedItem() != null && hasCompleteShippingAddress) {
firstCompleteAddressIndex = 0;
// The initial label for the selected shipping address should not include the
// country.
addresses.get(firstCompleteAddressIndex).setShippingAddressLabelWithoutCountry();
}
// Log the number of suggested shipping addresses and whether at least one of them is
// complete.
mJourneyLogger.setNumberOfSuggestionsShown(
Section.SHIPPING_ADDRESS, addresses.size(), hasCompleteShippingAddress);
int missingFields = 0;
if (addresses.isEmpty()) {
// All fields are missing.
missingFields = AutofillAddress.CompletionStatus.INVALID_RECIPIENT
| AutofillAddress.CompletionStatus.INVALID_PHONE_NUMBER
| AutofillAddress.CompletionStatus.INVALID_ADDRESS;
} else {
missingFields = addresses.get(0).getMissingFieldsOfShippingProfile();
}
if (missingFields != 0) {
RecordHistogram.recordSparseHistogram(
"PaymentRequest.MissingShippingFields", missingFields);
}
mShippingAddressesSection = new SectionInformation(
PaymentRequestUI.DataType.SHIPPING_ADDRESSES, firstCompleteAddressIndex, addresses);
}
// Implement ComponentPaymentRequestDelegate:
/**
* Called by the merchant website to show the payment request to the user.
*/
@Override
public void show(boolean isUserGesture, boolean waitForUpdatedDetails) {
if (getClient() == null) return;
if (mUI != null || mMinimalUi != null) {
// Can be triggered only by a compromised renderer. In normal operation, calling show()
// twice on the same instance of PaymentRequest in JavaScript is rejected at the
// renderer level.
mJourneyLogger.setAborted(AbortReason.INVALID_DATA_FROM_RENDERER);
disconnectFromClientWithDebugMessage(ErrorStrings.CANNOT_SHOW_TWICE);
return;
}
if (getIsAnyPaymentRequestShowing()) {
// The renderer can create multiple instances of PaymentRequest and call show() on each
// one. Only the first one will be shown. This also prevents multiple tabs and windows
// from showing PaymentRequest UI at the same time.
mJourneyLogger.setNotShown(NotShownReason.CONCURRENT_REQUESTS);
disconnectFromClientWithDebugMessage(
ErrorStrings.ANOTHER_UI_SHOWING, PaymentErrorReason.ALREADY_SHOWING);
if (sObserverForTest != null) sObserverForTest.onPaymentRequestServiceShowFailed();
return;
}
setShowingPaymentRequest(this);
mIsCurrentPaymentRequestShowing = true;
mIsUserGestureShow = isUserGesture;
mWaitForUpdatedDetails = waitForUpdatedDetails;
mJourneyLogger.setTriggerTime();
if (disconnectIfNoPaymentMethodsSupported()) return;
ChromeActivity chromeActivity = ChromeActivity.fromWebContents(mWebContents);
if (chromeActivity == null) {
mJourneyLogger.setNotShown(NotShownReason.OTHER);
disconnectFromClientWithDebugMessage(ErrorStrings.ACTIVITY_NOT_FOUND);
if (sObserverForTest != null) sObserverForTest.onPaymentRequestServiceShowFailed();
return;
}
if (mIsFinishedQueryingPaymentApps) {
// Send AppListReady signal when all apps are created and request.show() is called.
if (mNativeObserverForTest != null) {
mNativeObserverForTest.onAppListReady(mPaymentMethodsSection.getItems(), mRawTotal);
}
// Calculate skip ui and build ui only after all payment apps are ready and
// request.show() is called.
calculateWhetherShouldSkipShowingPaymentRequestUi();
if (!buildUI(chromeActivity)) return;
if (!mShouldSkipShowingPaymentRequestUi && mSkipToGPayHelper == null) {
mUI.show();
}
}
triggerPaymentAppUiSkipIfApplicable(chromeActivity);
}
private void dimBackgroundIfNotBottomSheetPaymentHandler(PaymentApp selectedApp) {
// Putting isEnabled() last is intentional. It's to ensure not to confused the unexecuted
// group and the disabled in A/B testing.
if (selectedApp != null
&& selectedApp.getPaymentAppType() == PaymentAppType.SERVICE_WORKER_APP
&& PaymentHandlerCoordinator.isEnabled()) {
// When the Payment Handler (PH) UI is based on Activity, dimming the Payment
// Request (PR) UI does not dim the PH; when it's based on bottom-sheet, dimming
// the PR dims both UIs. As bottom-sheet itself has dimming effect, dimming PR
// is unnecessary for the bottom-sheet PH. For now, service worker based payment apps
// are the only ones that can open the bottom-sheet.
return;
}
mUI.dimBackground();
}
private void triggerPaymentAppUiSkipIfApplicable(ChromeActivity chromeActivity) {
// If we are skipping showing the Payment Request UI, we should call into the payment app
// immediately after we determine the apps are ready and UI is shown.
if ((mShouldSkipShowingPaymentRequestUi || mSkipToGPayHelper != null)
&& mIsFinishedQueryingPaymentApps && mIsCurrentPaymentRequestShowing
&& !mWaitForUpdatedDetails) {
assert !mPaymentMethodsSection.isEmpty();
assert mUI != null;
if (isMinimalUiApplicable()) {
triggerMinimalUi(chromeActivity);
return;
}
assert !mPaymentMethodsSection.isEmpty();
PaymentApp selectedApp = (PaymentApp) mPaymentMethodsSection.getSelectedItem();
dimBackgroundIfNotBottomSheetPaymentHandler(selectedApp);
mDidRecordShowEvent = true;
mJourneyLogger.setEventOccurred(Event.SKIPPED_SHOW);
assert mRawTotal != null;
// The total amount in details should be finalized at this point. So it is safe to
// record the triggered transaction amount.
assert !mWaitForUpdatedDetails;
mJourneyLogger.recordTransactionAmount(
mRawTotal.amount.currency, mRawTotal.amount.value, false /*completed*/);
onPayClicked(null /* selectedShippingAddress */, null /* selectedShippingOption */,
selectedApp);
}
}
/** @return Whether the minimal UI should be shown. */
private boolean isMinimalUiApplicable() {
if (!mIsUserGestureShow || mPaymentMethodsSection == null
|| mPaymentMethodsSection.getSize() != 1) {
return false;
}
PaymentApp app = (PaymentApp) mPaymentMethodsSection.getSelectedItem();
if (app == null || !app.isReadyForMinimalUI() || TextUtils.isEmpty(app.accountBalance())) {
return false;
}
return PaymentFeatureList.isEnabled(PaymentFeatureList.WEB_PAYMENTS_MINIMAL_UI);
}
/**
* Triggers the minimal UI.
* @param chromeActivity The Android activity for the Chrome UI that will host the minimal UI.
*/
private void triggerMinimalUi(ChromeActivity chromeActivity) {
// Do not show the Payment Request UI dialog even if the minimal UI is suppressed.
mPaymentUisShowStateReconciler.onBottomSheetShown();
mMinimalUi = new MinimalUICoordinator();
if (mMinimalUi.show(chromeActivity,
BottomSheetControllerProvider.from(chromeActivity.getWindowAndroid()),
(PaymentApp) mPaymentMethodsSection.getSelectedItem(),
mCurrencyFormatterMap.get(mRawTotal.amount.currency),
mUiShoppingCart.getTotal(), this::onMinimalUIReady, this::onMinimalUiConfirmed,
this::onMinimalUiDismissed)) {
mDidRecordShowEvent = true;
mJourneyLogger.setEventOccurred(Event.SHOWN);
return;
}
disconnectFromClientWithDebugMessage(ErrorStrings.MINIMAL_UI_SUPPRESSED);
}
private void onMinimalUIReady() {
if (mNativeObserverForTest != null) mNativeObserverForTest.onMinimalUIReady();
}
private void onMinimalUiConfirmed(PaymentApp app) {
mJourneyLogger.recordTransactionAmount(
mRawTotal.amount.currency, mRawTotal.amount.value, false /*completed*/);
app.disableShowingOwnUI();
onPayClicked(null /* selectedShippingAddress */, null /* selectedShippingOption */, app);
}
private void onMinimalUiDismissed() {
onDismiss();
}
private void onMinimalUiErroredAndClosed() {
if (getClient() == null) return;
closeClient();
closeUIAndDestroyNativeObjects();
}
private void onMinimalUiCompletedAndClosed() {
PaymentRequestClient client = getClient();
if (client != null) {
client.onComplete();
}
closeClient();
closeUIAndDestroyNativeObjects();
}
private static Map<String, PaymentMethodData> getValidatedMethodData(
PaymentMethodData[] methodData, boolean googlePayBridgeEligible,
CardEditor paymentMethodsCollector) {
// Payment methodData are required.
if (methodData == null || methodData.length == 0) return null;
Map<String, PaymentMethodData> result = new ArrayMap<>();
for (int i = 0; i < methodData.length; i++) {
String method = methodData[i].supportedMethod;
if (TextUtils.isEmpty(method)) return null;
if (googlePayBridgeEligible) {
// If skip-to-GPay flow is activated, ignore all other payment methods, which can be
// either "basic-card" or "https://android.com/pay". The latter is safe to ignore
// because merchant has already requested Google Pay.
if (!method.equals(MethodStrings.GOOGLE_PAY)) continue;
if (methodData[i].gpayBridgeData != null
&& !methodData[i].gpayBridgeData.stringifiedData.isEmpty()) {
methodData[i].stringifiedData = methodData[i].gpayBridgeData.stringifiedData;
}
}
result.put(method, methodData[i]);
paymentMethodsCollector.addAcceptedPaymentMethodIfRecognized(methodData[i]);
}
return Collections.unmodifiableMap(result);
}
/** Called by the payment app to get updated total based on the billing address, for example. */
@Override
public boolean changePaymentMethodFromInvokedApp(String methodName, String stringifiedDetails) {
PaymentRequestClient client = getClient();
if (TextUtils.isEmpty(methodName) || stringifiedDetails == null || client == null
|| mInvokedPaymentApp == null
|| mInvokedPaymentApp.isWaitingForPaymentDetailsUpdate()) {
return false;
}
client.onPaymentMethodChange(methodName, stringifiedDetails);
return true;
}
/**
* Called by the payment app to get updated payment details based on the shipping option.
*/
@Override
public boolean changeShippingOptionFromInvokedApp(String shippingOptionId) {
PaymentRequestClient client = getClient();
if (TextUtils.isEmpty(shippingOptionId) || client == null || mInvokedPaymentApp == null
|| mInvokedPaymentApp.isWaitingForPaymentDetailsUpdate() || !mRequestShipping
|| mRawShippingOptions == null) {
return false;
}
boolean isValidId = false;
for (PaymentShippingOption option : mRawShippingOptions) {
if (shippingOptionId.equals(option.id)) {
isValidId = true;
break;
}
}
if (!isValidId) return false;
client.onShippingOptionChange(shippingOptionId);
return true;
}
/**
* Called by payment app to get updated payment details based on the shipping address.
*/
@Override
public boolean changeShippingAddressFromInvokedApp(PaymentAddress shippingAddress) {
PaymentRequestClient client = getClient();
if (shippingAddress == null || client == null || mInvokedPaymentApp == null
|| mInvokedPaymentApp.isWaitingForPaymentDetailsUpdate() || !mRequestShipping) {
return false;
}
redactShippingAddress(shippingAddress);
client.onShippingAddressChange(shippingAddress);
return true;
}
/**
* Get the WebContents of the Expandable Payment Handler for testing purpose; return null if
* nonexistent.
*
* @return The WebContents of the Expandable Payment Handler.
*/
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
public static WebContents getPaymentHandlerWebContentsForTest() {
if (sShowingPaymentRequest == null) return null;
return sShowingPaymentRequest.getPaymentHandlerWebContentsForTestInternal();
}
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
private WebContents getPaymentHandlerWebContentsForTestInternal() {
if (mPaymentHandlerUi == null) return null;
return mPaymentHandlerUi.getWebContentsForTest();
}
/**
* Click the security icon of the Expandable Payment Handler for testing purpose; return false
* if failed.
*
* @return The WebContents of the Expandable Payment Handler.
*/
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
public static boolean clickPaymentHandlerSecurityIconForTest() {
if (sShowingPaymentRequest == null) return false;
return sShowingPaymentRequest.clickPaymentHandlerSecurityIconForTestInternal();
}
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
private boolean clickPaymentHandlerSecurityIconForTestInternal() {
if (mPaymentHandlerUi == null) return false;
mPaymentHandlerUi.clickSecurityIconForTest();
return true;
}
/**
* Confirms payment in minimal UI. Used only in test.
*
* @return Whether the payment was confirmed successfully.
*/
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
public static boolean confirmMinimalUIForTest() {
return sShowingPaymentRequest != null
&& sShowingPaymentRequest.confirmMinimalUIForTestInternal();
}
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
private boolean confirmMinimalUIForTestInternal() {
if (mMinimalUi == null) return false;
mMinimalUi.confirmForTest();
return true;
}
/**
* Dismisses the minimal UI. Used only in test.
*
* @return Whether the dismissal was successful.
*/
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
public static boolean dismissMinimalUIForTest() {
return sShowingPaymentRequest != null
&& sShowingPaymentRequest.dismissMinimalUIForTestInternal();
}
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
private boolean dismissMinimalUIForTestInternal() {
if (mMinimalUi == null) return false;
mMinimalUi.dismissForTest();
return true;
}
/**
* Called to open a new PaymentHandler UI on the showing PaymentRequest.
* @param url The url of the payment app to be displayed in the UI.
* @param paymentHandlerWebContentsObserver The observer of the WebContents of the
* PaymentHandler.
* @return Whether the opening is successful.
*/
public static boolean openPaymentHandlerWindow(
GURL url, PaymentHandlerWebContentsObserver paymentHandlerWebContentsObserver) {
return sShowingPaymentRequest != null
&& sShowingPaymentRequest.openPaymentHandlerWindowInternal(
url, paymentHandlerWebContentsObserver);
}
/**
* Called to open a new PaymentHandler UI on this PaymentRequest.
* @param url The url of the payment app to be displayed in the UI.
* @param paymentHandlerWebContentsObserver The observer of the WebContents of the
* PaymentHandler.
* @return Whether the opening is successful.
*/
private boolean openPaymentHandlerWindowInternal(
GURL url, PaymentHandlerWebContentsObserver paymentHandlerWebContentsObserver) {
assert mInvokedPaymentApp != null;
assert mInvokedPaymentApp.getPaymentAppType() == PaymentAppType.SERVICE_WORKER_APP;
if (mPaymentHandlerUi != null) return false;
mPaymentHandlerUi = new PaymentHandlerCoordinator();
ChromeActivity chromeActivity = ChromeActivity.fromWebContents(mWebContents);
if (chromeActivity == null) return false;
boolean success = mPaymentHandlerUi.show(chromeActivity, url, mIsOffTheRecord,
paymentHandlerWebContentsObserver, /*uiObserver=*/this);
if (success) {
// UKM for payment app origin should get recorded only when the origin of the invoked
// payment app is shown to the user.
mJourneyLogger.setPaymentAppUkmSourceId(mInvokedPaymentApp.getUkmSourceId());
}
return success;
}
@Override
public void onPaymentHandlerUiClosed() {
mPaymentUisShowStateReconciler.onBottomSheetClosed();
mPaymentHandlerUi = null;
}
@Override
public void onPaymentHandlerUiShown() {
assert mPaymentHandlerUi != null;
mPaymentUisShowStateReconciler.onBottomSheetShown();
}
@Override
public boolean isInvokedInstrumentValidForPaymentMethodIdentifier(String methodName) {
return mInvokedPaymentApp != null
&& mInvokedPaymentApp.isValidForPaymentMethodData(methodName, null);
}
// Implement ComponentPaymentRequestDelegate:
/**
* Called by merchant to update the shipping options and line items after the user has selected
* their shipping address or shipping option.
*/
@Override
public void updateWith(PaymentDetails details) {
if (getClient() == null) return;
if (mWaitForUpdatedDetails) {
initializeWithUpdatedDetails(details);
return;
}
if (mUI == null) {
mJourneyLogger.setAborted(AbortReason.INVALID_DATA_FROM_RENDERER);
disconnectFromClientWithDebugMessage(ErrorStrings.CANNOT_UPDATE_WITHOUT_SHOW);
return;
}
if (!mRequestShipping && !mRequestPayerName && !mRequestPayerEmail && !mRequestPayerPhone
&& (mInvokedPaymentApp == null
|| !mInvokedPaymentApp.isWaitingForPaymentDetailsUpdate())) {
mJourneyLogger.setAborted(AbortReason.INVALID_DATA_FROM_RENDERER);
disconnectFromClientWithDebugMessage(ErrorStrings.INVALID_STATE);
return;
}
if (!parseAndValidateDetailsOrDisconnectFromClient(details)) return;
mSpec.updateWith(details);
if (mInvokedPaymentApp != null && mInvokedPaymentApp.isWaitingForPaymentDetailsUpdate()) {
// After a payment app has been invoked, all of the merchant's calls to update the price
// via updateWith() should be forwarded to the invoked app, so it can reflect the
// updated price in its UI.
mInvokedPaymentApp.updateWith(
PaymentDetailsConverter.convertToPaymentRequestDetailsUpdate(details,
mInvokedPaymentApp.handlesShippingAddress() /* handlesShipping */,
this /* methodChecker */));
return;
}
if (shouldShowShippingSection()
&& (mUiShippingOptions.isEmpty() || !TextUtils.isEmpty(details.error))
&& mShippingAddressesSection.getSelectedItem() != null) {
mShippingAddressesSection.getSelectedItem().setInvalid();
mShippingAddressesSection.setSelectedItemIndex(SectionInformation.INVALID_SELECTION);
mShippingAddressesSection.setErrorMessage(details.error);
}
enableUserInterfaceAfterPaymentRequestUpdateEvent();
}
private void initializeWithUpdatedDetails(PaymentDetails details) {
assert mWaitForUpdatedDetails;
ChromeActivity chromeActivity = ChromeActivity.fromWebContents(mWebContents);
if (chromeActivity == null) {
mJourneyLogger.setNotShown(NotShownReason.OTHER);
disconnectFromClientWithDebugMessage(ErrorStrings.ACTIVITY_NOT_FOUND);
return;
}
if (!parseAndValidateDetailsOrDisconnectFromClient(details)) return;
mSpec.updateWith(details);
if (!TextUtils.isEmpty(details.error)) {
mJourneyLogger.setNotShown(NotShownReason.OTHER);
disconnectFromClientWithDebugMessage(ErrorStrings.INVALID_STATE);
return;
}
// Do not create shipping section When UI is not built yet. This happens when the show
// promise gets resolved before all apps are ready.
if (mUI != null && shouldShowShippingSection()) {
createShippingSection(chromeActivity, mAutofillProfiles);
}
mWaitForUpdatedDetails = false;
// Triggered tansaction amount gets recorded when both of the following conditions are met:
// 1- Either Event.Shown or Event.SKIPPED_SHOW bits are set showing that transaction is
// triggered (mDidRecordShowEvent == true). 2- The total amount in details won't change
// (mWaitForUpdatedDetails == false). Record the transaction amount only when the triggered
// condition is already met. Otherwise it will get recorded when triggered condition becomes
// true.
if (mDidRecordShowEvent) {
assert mRawTotal != null;
mJourneyLogger.recordTransactionAmount(
mRawTotal.amount.currency, mRawTotal.amount.value, false /*completed*/);
}
triggerPaymentAppUiSkipIfApplicable(chromeActivity);
if (mIsFinishedQueryingPaymentApps && !mShouldSkipShowingPaymentRequestUi) {
enableUserInterfaceAfterPaymentRequestUpdateEvent();
}
}
// Implement ComponentPaymentRequestDelegate:
/**
* Called when the merchant received a new shipping address, shipping option, or payment method
* info, but did not update the payment details in response.
*/
@Override
public void onPaymentDetailsNotUpdated() {
if (getClient() == null) return;
if (mUI == null) {
mJourneyLogger.setAborted(AbortReason.INVALID_DATA_FROM_RENDERER);
disconnectFromClientWithDebugMessage(ErrorStrings.CANNOT_UPDATE_WITHOUT_SHOW);
return;
}
if (mInvokedPaymentApp != null && mInvokedPaymentApp.isWaitingForPaymentDetailsUpdate()) {
mInvokedPaymentApp.onPaymentDetailsNotUpdated();
return;
}
enableUserInterfaceAfterPaymentRequestUpdateEvent();
}
private void enableUserInterfaceAfterPaymentRequestUpdateEvent() {
if (mPaymentInformationCallback != null && mPaymentMethodsSection != null) {
providePaymentInformation();
} else {
mUI.updateOrderSummarySection(mUiShoppingCart);
if (shouldShowShippingSection()) {
mUI.updateSection(PaymentRequestUI.DataType.SHIPPING_OPTIONS, mUiShippingOptions);
}
}
}
/**
* Sets the total, display line items, and shipping options based on input and returns the
* status boolean. That status is true for valid data, false for invalid data. If the input is
* invalid, disconnects from the client. Both raw and UI versions of data are updated.
*
* @param details The total, line items, and shipping options to parse, validate, and save in
* member variables.
* @return True if the data is valid. False if the data is invalid.
*/
private boolean parseAndValidateDetailsOrDisconnectFromClient(PaymentDetails details) {
if (!PaymentValidator.validatePaymentDetails(details)) {
mJourneyLogger.setAborted(AbortReason.INVALID_DATA_FROM_RENDERER);
disconnectFromClientWithDebugMessage(ErrorStrings.INVALID_PAYMENT_DETAILS);
return false;
}
if (details.total != null) {
mRawTotal = details.total;
}
if (mRawLineItems == null || details.displayItems != null) {
mRawLineItems = Collections.unmodifiableList(details.displayItems != null
? Arrays.asList(details.displayItems)
: new ArrayList<>());
}
loadCurrencyFormattersForPaymentDetails(details);
// Total is never pending.
CurrencyFormatter formatter = getOrCreateCurrencyFormatter(mRawTotal.amount);
LineItem uiTotal = new LineItem(mRawTotal.label, formatter.getFormattedCurrencyCode(),
formatter.format(mRawTotal.amount.value), /* isPending */ false);
List<LineItem> uiLineItems = getLineItems(mRawLineItems);
mUiShoppingCart = new ShoppingCart(uiTotal, uiLineItems);
if (mUiShippingOptions == null || details.shippingOptions != null) {
mUiShippingOptions = getShippingOptions(details.shippingOptions);
}
if (mSkipToGPayHelper != null && !mSkipToGPayHelper.setShippingOption(details)) {
return false;
}
if (details.modifiers != null) {
if (details.modifiers.length == 0 && mModifiers != null) mModifiers.clear();
for (int i = 0; i < details.modifiers.length; i++) {
PaymentDetailsModifier modifier = details.modifiers[i];
String method = modifier.methodData.supportedMethod;
if (mModifiers == null) mModifiers = new ArrayMap<>();
mModifiers.put(method, modifier);
}
}
if (details.shippingOptions != null) {
mRawShippingOptions =
Collections.unmodifiableList(Arrays.asList(details.shippingOptions));
} else if (mRawShippingOptions == null) {
mRawShippingOptions = Collections.unmodifiableList(new ArrayList<>());
}
updateAppModifiedTotals();
assert mRawTotal != null;
assert mRawLineItems != null;
return true;
}
/** Updates the modifiers for payment apps and order summary. */
private void updateAppModifiedTotals() {
if (!PaymentFeatureList.isEnabled(PaymentFeatureList.WEB_PAYMENTS_MODIFIERS)) return;
if (mModifiers == null) return;
if (mPaymentMethodsSection == null) return;
for (int i = 0; i < mPaymentMethodsSection.getSize(); i++) {
PaymentApp app = (PaymentApp) mPaymentMethodsSection.getItem(i);
PaymentDetailsModifier modifier = getModifier(app);
app.setModifiedTotal(modifier == null || modifier.total == null
? null
: getOrCreateCurrencyFormatter(modifier.total.amount)
.format(modifier.total.amount.value));
}
updateOrderSummary((PaymentApp) mPaymentMethodsSection.getSelectedItem());
}
/** Sets the modifier for the order summary based on the given app, if any. */
private void updateOrderSummary(@Nullable PaymentApp app) {
if (!PaymentFeatureList.isEnabled(PaymentFeatureList.WEB_PAYMENTS_MODIFIERS)) return;
PaymentDetailsModifier modifier = getModifier(app);
PaymentItem total = modifier == null ? null : modifier.total;
if (total == null) total = mRawTotal;
CurrencyFormatter formatter = getOrCreateCurrencyFormatter(total.amount);
mUiShoppingCart.setTotal(new LineItem(total.label, formatter.getFormattedCurrencyCode(),
formatter.format(total.amount.value), false /* isPending */));
mUiShoppingCart.setAdditionalContents(modifier == null
? null
: getLineItems(Arrays.asList(modifier.additionalDisplayItems)));
if (mUI != null) mUI.updateOrderSummarySection(mUiShoppingCart);
}
/** @return The first modifier that matches the given app, or null. */
@Nullable
private PaymentDetailsModifier getModifier(@Nullable PaymentApp app) {
if (mModifiers == null || app == null) return null;
// Makes a copy to ensure it is modifiable.
Set<String> methodNames = new HashSet<>(app.getInstrumentMethodNames());
methodNames.retainAll(mModifiers.keySet());
if (methodNames.isEmpty()) return null;
for (String methodName : methodNames) {
PaymentDetailsModifier modifier = mModifiers.get(methodName);
if (app.isValidForPaymentMethodData(methodName, modifier.methodData)) {
return modifier;
}
}
return null;
}
/**
* Converts a list of payment items and returns their parsed representation.
*
* @param items The payment items to parse. Can be null.
* @return A list of valid line items.
*/
private List<LineItem> getLineItems(@Nullable List<PaymentItem> items) {
// Line items are optional.
if (items == null) return new ArrayList<>();
List<LineItem> result = new ArrayList<>(items.size());
for (int i = 0; i < items.size(); i++) {
PaymentItem item = items.get(i);
CurrencyFormatter formatter = getOrCreateCurrencyFormatter(item.amount);
result.add(new LineItem(item.label,
isMixedOrChangedCurrency() ? formatter.getFormattedCurrencyCode() : "",
formatter.format(item.amount.value), item.pending));
}
return Collections.unmodifiableList(result);
}
/**
* Converts a list of shipping options and returns their parsed representation.
*
* @param options The raw shipping options to parse. Can be null.
* @return The UI representation of the shipping options.
*/
private SectionInformation getShippingOptions(@Nullable PaymentShippingOption[] options) {
// Shipping options are optional.
if (options == null || options.length == 0) {
return new SectionInformation(PaymentRequestUI.DataType.SHIPPING_OPTIONS);
}
List<EditableOption> result = new ArrayList<>();
int selectedItemIndex = SectionInformation.NO_SELECTION;
for (int i = 0; i < options.length; i++) {
PaymentShippingOption option = options[i];
CurrencyFormatter formatter = getOrCreateCurrencyFormatter(option.amount);
String currencyPrefix = isMixedOrChangedCurrency()
? formatter.getFormattedCurrencyCode() + "\u0020"
: "";
result.add(new EditableOption(option.id, option.label,
currencyPrefix + formatter.format(option.amount.value), null));
if (option.selected) selectedItemIndex = i;
}
return new SectionInformation(PaymentRequestUI.DataType.SHIPPING_OPTIONS, selectedItemIndex,
Collections.unmodifiableList(result));
}
/**
* Load required currency formatter for a given PaymentDetails.
*
* Note that the cache (mCurrencyFormatterMap) is not cleared for
* updated payment details so as to indicate the currency has been changed.
*
* @param details The given payment details.
*/
private void loadCurrencyFormattersForPaymentDetails(PaymentDetails details) {
if (details.total != null) {
getOrCreateCurrencyFormatter(details.total.amount);
}
if (details.displayItems != null) {
for (PaymentItem item : details.displayItems) {
getOrCreateCurrencyFormatter(item.amount);
}
}
if (details.shippingOptions != null) {
for (PaymentShippingOption option : details.shippingOptions) {
getOrCreateCurrencyFormatter(option.amount);
}
}
if (details.modifiers != null) {
for (PaymentDetailsModifier modifier : details.modifiers) {
if (modifier.total != null) getOrCreateCurrencyFormatter(modifier.total.amount);
for (PaymentItem displayItem : modifier.additionalDisplayItems) {
getOrCreateCurrencyFormatter(displayItem.amount);
}
}
}
}
private boolean isMixedOrChangedCurrency() {
return mCurrencyFormatterMap.size() > 1;
}
/**
* Gets currency formatter for a given PaymentCurrencyAmount,
* creates one if no existing instance is found.
*
* @amount The given payment amount.
*/
private CurrencyFormatter getOrCreateCurrencyFormatter(PaymentCurrencyAmount amount) {
String key = amount.currency;
CurrencyFormatter formatter = mCurrencyFormatterMap.get(key);
if (formatter == null) {
formatter = new CurrencyFormatter(amount.currency, Locale.getDefault());
mCurrencyFormatterMap.put(key, formatter);
}
return formatter;
}
/**
* Called to retrieve the data to show in the initial PaymentRequest UI.
*/
@Override
public void getDefaultPaymentInformation(Callback<PaymentInformation> callback) {
mPaymentInformationCallback = callback;
// mUI.show() is called only after request.show() is called and all payment apps are ready.
assert mIsCurrentPaymentRequestShowing;
assert mIsFinishedQueryingPaymentApps;
if (mWaitForUpdatedDetails) return;
mHandler.post(() -> {
if (mUI != null) providePaymentInformation();
});
}
private void providePaymentInformation() {
// Do not display service worker payment apps summary in single line so as to display its
// origin completely.
mPaymentMethodsSection.setDisplaySelectedItemSummaryInSingleLineInNormalMode(
getSelectedPaymentAppType() != PaymentAppType.SERVICE_WORKER_APP);
mPaymentInformationCallback.onResult(
new PaymentInformation(mUiShoppingCart, mShippingAddressesSection,
mUiShippingOptions, mContactSection, mPaymentMethodsSection));
mPaymentInformationCallback = null;
if (!mDidRecordShowEvent) {
mDidRecordShowEvent = true;
mJourneyLogger.setEventOccurred(Event.SHOWN);
// Record the triggered transaction amount only when the total amount in details is
// finalized (i.e. mWaitForUpdatedDetails == false). Otherwise it will get recorded when
// the updated details become available.
if (!mWaitForUpdatedDetails) {
assert mRawTotal != null;
mJourneyLogger.recordTransactionAmount(
mRawTotal.amount.currency, mRawTotal.amount.value, false /*completed*/);
}
}
}
@Override
public void getShoppingCart(final Callback<ShoppingCart> callback) {
mHandler.post(callback.bind(mUiShoppingCart));
}
@Override
public void getSectionInformation(@PaymentRequestUI.DataType final int optionType,
final Callback<SectionInformation> callback) {
SectionInformation result = null;
switch (optionType) {
case PaymentRequestUI.DataType.SHIPPING_ADDRESSES:
result = mShippingAddressesSection;
break;
case PaymentRequestUI.DataType.SHIPPING_OPTIONS:
result = mUiShippingOptions;
break;
case PaymentRequestUI.DataType.CONTACT_DETAILS:
result = mContactSection;
break;
case PaymentRequestUI.DataType.PAYMENT_METHODS:
result = mPaymentMethodsSection;
break;
default:
assert false;
}
mHandler.post(callback.bind(result));
}
@Override
@PaymentRequestUI.SelectionResult
public int onSectionOptionSelected(@PaymentRequestUI.DataType int optionType,
EditableOption option, Callback<PaymentInformation> callback) {
PaymentRequestClient client = getClient();
if (client == null) return SelectionResult.NONE;
if (optionType == PaymentRequestUI.DataType.SHIPPING_ADDRESSES) {
// Log the change of shipping address.
mJourneyLogger.incrementSelectionChanges(Section.SHIPPING_ADDRESS);
AutofillAddress address = (AutofillAddress) option;
if (address.isComplete()) {
mShippingAddressesSection.setSelectedItem(option);
startShippingAddressChangeNormalization(address);
} else {
editAddress(address);
}
mPaymentInformationCallback = callback;
return PaymentRequestUI.SelectionResult.ASYNCHRONOUS_VALIDATION;
} else if (optionType == PaymentRequestUI.DataType.SHIPPING_OPTIONS) {
// This may update the line items.
mUiShippingOptions.setSelectedItem(option);
client.onShippingOptionChange(option.getIdentifier());
mPaymentInformationCallback = callback;
return PaymentRequestUI.SelectionResult.ASYNCHRONOUS_VALIDATION;
} else if (optionType == PaymentRequestUI.DataType.CONTACT_DETAILS) {
// Log the change of contact info.
mJourneyLogger.incrementSelectionChanges(Section.CONTACT_INFO);
AutofillContact contact = (AutofillContact) option;
if (contact.isComplete()) {
mContactSection.setSelectedItem(option);
if (!mWasRetryCalled) return PaymentRequestUI.SelectionResult.NONE;
dispatchPayerDetailChangeEventIfNeeded(contact.toPayerDetail());
} else {
editContact(contact);
if (!mWasRetryCalled) return PaymentRequestUI.SelectionResult.EDITOR_LAUNCH;
}
mPaymentInformationCallback = callback;
return PaymentRequestUI.SelectionResult.ASYNCHRONOUS_VALIDATION;
} else if (optionType == PaymentRequestUI.DataType.PAYMENT_METHODS) {
if (shouldShowShippingSection() && mShippingAddressesSection == null) {
ChromeActivity activity = ChromeActivity.fromWebContents(mWebContents);
assert activity != null;
createShippingSection(activity, mAutofillProfiles);
}
if (shouldShowContactSection() && mContactSection == null) {
ChromeActivity activity = ChromeActivity.fromWebContents(mWebContents);
assert activity != null;
mContactSection = new ContactDetailsSection(
activity, mAutofillProfiles, mContactEditor, mJourneyLogger);
}
mUI.selectedPaymentMethodUpdated(
new PaymentInformation(mUiShoppingCart, mShippingAddressesSection,
mUiShippingOptions, mContactSection, mPaymentMethodsSection));
PaymentApp paymentApp = (PaymentApp) option;
if (paymentApp instanceof AutofillPaymentInstrument) {
AutofillPaymentInstrument card = (AutofillPaymentInstrument) paymentApp;
if (!card.isComplete()) {
editCard(card);
return PaymentRequestUI.SelectionResult.EDITOR_LAUNCH;
}
}
// Log the change of payment method.
mJourneyLogger.incrementSelectionChanges(Section.PAYMENT_METHOD);
updateOrderSummary(paymentApp);
mPaymentMethodsSection.setSelectedItem(option);
}
return PaymentRequestUI.SelectionResult.NONE;
}
@Override
@PaymentRequestUI.SelectionResult
public int onSectionEditOption(@PaymentRequestUI.DataType int optionType, EditableOption option,
Callback<PaymentInformation> callback) {
if (optionType == PaymentRequestUI.DataType.SHIPPING_ADDRESSES) {
editAddress((AutofillAddress) option);
mPaymentInformationCallback = callback;
return PaymentRequestUI.SelectionResult.ASYNCHRONOUS_VALIDATION;
}
if (optionType == PaymentRequestUI.DataType.CONTACT_DETAILS) {
editContact((AutofillContact) option);
return PaymentRequestUI.SelectionResult.EDITOR_LAUNCH;
}
if (optionType == PaymentRequestUI.DataType.PAYMENT_METHODS) {
editCard((AutofillPaymentInstrument) option);
return PaymentRequestUI.SelectionResult.EDITOR_LAUNCH;
}
assert false;
return PaymentRequestUI.SelectionResult.NONE;
}
@Override
@PaymentRequestUI.SelectionResult
public int onSectionAddOption(
@PaymentRequestUI.DataType int optionType, Callback<PaymentInformation> callback) {
if (optionType == PaymentRequestUI.DataType.SHIPPING_ADDRESSES) {
editAddress(null);
mPaymentInformationCallback = callback;
// Log the add of shipping address.
mJourneyLogger.incrementSelectionAdds(Section.SHIPPING_ADDRESS);
return PaymentRequestUI.SelectionResult.ASYNCHRONOUS_VALIDATION;
} else if (optionType == PaymentRequestUI.DataType.CONTACT_DETAILS) {
editContact(null);
// Log the add of contact info.
mJourneyLogger.incrementSelectionAdds(Section.CONTACT_INFO);
return PaymentRequestUI.SelectionResult.EDITOR_LAUNCH;
} else if (optionType == PaymentRequestUI.DataType.PAYMENT_METHODS) {
editCard(null);
// Log the add of credit card.
mJourneyLogger.incrementSelectionAdds(Section.PAYMENT_METHOD);
return PaymentRequestUI.SelectionResult.EDITOR_LAUNCH;
}
return PaymentRequestUI.SelectionResult.NONE;
}
@Override
public boolean shouldShowShippingSection() {
if (!mRequestShipping) return false;
if (mPaymentMethodsSection == null) return true;
PaymentApp selectedApp = (PaymentApp) mPaymentMethodsSection.getSelectedItem();
return selectedApp == null || !selectedApp.handlesShippingAddress();
}
@Override
public boolean shouldShowContactSection() {
PaymentApp selectedApp = (mPaymentMethodsSection == null)
? null
: (PaymentApp) mPaymentMethodsSection.getSelectedItem();
if (mRequestPayerName && (selectedApp == null || !selectedApp.handlesPayerName())) {
return true;
}
if (mRequestPayerPhone && (selectedApp == null || !selectedApp.handlesPayerPhone())) {
return true;
}
if (mRequestPayerEmail && (selectedApp == null || !selectedApp.handlesPayerEmail())) {
return true;
}
return false;
}
private void editAddress(final AutofillAddress toEdit) {
if (toEdit != null) {
// Log the edit of a shipping address.
mJourneyLogger.incrementSelectionEdits(Section.SHIPPING_ADDRESS);
}
mAddressEditor.edit(toEdit, new Callback<AutofillAddress>() {
@Override
public void onResult(AutofillAddress editedAddress) {
if (mUI == null) return;
if (editedAddress != null) {
mAddressEditor.setAddressErrors(null);
// Sets or updates the shipping address label.
editedAddress.setShippingAddressLabelWithCountry();
mCardEditor.updateBillingAddressIfComplete(editedAddress);
// A partial or complete address came back from the editor (could have been from
// adding/editing or cancelling out of the edit flow).
if (!editedAddress.isComplete()) {
// If the address is not complete, unselect it (editor can return incomplete
// information when cancelled).
mShippingAddressesSection.setSelectedItemIndex(
SectionInformation.NO_SELECTION);
providePaymentInformation();
} else {
if (toEdit == null) {
// Address is complete and user was in the "Add flow": add an item to
// the list.
mShippingAddressesSection.addAndSelectItem(editedAddress);
}
if (mContactSection != null) {
// Update |mContactSection| with the new/edited address, which will
// update an existing item or add a new one to the end of the list.
mContactSection.addOrUpdateWithAutofillAddress(editedAddress);
mUI.updateSection(
PaymentRequestUI.DataType.CONTACT_DETAILS, mContactSection);
}
startShippingAddressChangeNormalization(editedAddress);
}
} else {
providePaymentInformation();
}
if (!mRetryQueue.isEmpty()) mHandler.post(mRetryQueue.remove());
}
});
}
private void editContact(final AutofillContact toEdit) {
if (toEdit != null) {
// Log the edit of a contact info.
mJourneyLogger.incrementSelectionEdits(Section.CONTACT_INFO);
}
mContactEditor.edit(toEdit, new Callback<AutofillContact>() {
@Override
public void onResult(AutofillContact editedContact) {
if (mUI == null) return;
if (editedContact != null) {
mContactEditor.setPayerErrors(null);
// A partial or complete contact came back from the editor (could have been from
// adding/editing or cancelling out of the edit flow).
if (!editedContact.isComplete()) {
// If the contact is not complete according to the requirements of the flow,
// unselect it (editor can return incomplete information when cancelled).
mContactSection.setSelectedItemIndex(SectionInformation.NO_SELECTION);
} else if (toEdit == null) {
// Contact is complete and we were in the "Add flow": add an item to the
// list.
mContactSection.addAndSelectItem(editedContact);
} else {
dispatchPayerDetailChangeEventIfNeeded(editedContact.toPayerDetail());
}
// If contact is complete and (toEdit != null), no action needed: the contact
// was already selected in the UI.
}
// If |editedContact| is null, the user has cancelled out of the "Add flow". No
// action to take (if a contact was selected in the UI, it will stay selected).
mUI.updateSection(PaymentRequestUI.DataType.CONTACT_DETAILS, mContactSection);
if (!mRetryQueue.isEmpty()) mHandler.post(mRetryQueue.remove());
}
});
}
private void editCard(final AutofillPaymentInstrument toEdit) {
if (toEdit != null) {
// Log the edit of a credit card.
mJourneyLogger.incrementSelectionEdits(Section.PAYMENT_METHOD);
}
mCardEditor.edit(toEdit, new Callback<AutofillPaymentInstrument>() {
@Override
public void onResult(AutofillPaymentInstrument editedCard) {
if (mUI == null) return;
if (editedCard != null) {
// A partial or complete card came back from the editor (could have been from
// adding/editing or cancelling out of the edit flow).
if (!editedCard.isComplete()) {
// If the card is not complete, unselect it (editor can return incomplete
// information when cancelled).
mPaymentMethodsSection.setSelectedItemIndex(
SectionInformation.NO_SELECTION);
} else if (toEdit == null) {
// Card is complete and we were in the "Add flow": add an item to the list.
mPaymentMethodsSection.addAndSelectItem(editedCard);
}
// If card is complete and (toEdit != null), no action needed: the card was
// already selected in the UI.
}
// If |editedCard| is null, the user has cancelled out of the "Add flow". No action
// to take (if another card was selected prior to the add flow, it will stay
// selected).
updateAppModifiedTotals();
mUI.updateSection(
PaymentRequestUI.DataType.PAYMENT_METHODS, mPaymentMethodsSection);
}
});
}
@Override
public void onInstrumentDetailsLoadingWithoutUI() {
if (getClient() == null || mUI == null || mPaymentResponseHelper == null) {
return;
}
assert getSelectedPaymentAppType() == PaymentAppType.AUTOFILL;
mUI.showProcessingMessage();
}
private @PaymentAppType int getSelectedPaymentAppType() {
return mPaymentMethodsSection != null && mPaymentMethodsSection.getSelectedItem() != null
? ((PaymentApp) mPaymentMethodsSection.getSelectedItem()).getPaymentAppType()
: PaymentAppType.UNDEFINED;
}
@Override
public boolean onPayClicked(EditableOption selectedShippingAddress,
EditableOption selectedShippingOption, EditableOption selectedPaymentMethod) {
mInvokedPaymentApp = (PaymentApp) selectedPaymentMethod;
EditableOption selectedContact =
mContactSection != null ? mContactSection.getSelectedItem() : null;
mPaymentResponseHelper = new PaymentResponseHelper(selectedShippingAddress,
selectedShippingOption, selectedContact, mInvokedPaymentApp, mPaymentOptions,
mSkipToGPayHelper != null, this);
// Create maps that are subsets of mMethodData and mModifiers, that contain the payment
// methods supported by the selected payment app. If the intersection of method data
// contains more than one payment method, the payment app is at liberty to choose (or have
// the user choose) one of the methods.
Map<String, PaymentMethodData> methodData = new HashMap<>();
Map<String, PaymentDetailsModifier> modifiers = new HashMap<>();
boolean isGooglePaymentApp = false;
for (String paymentMethodName : mInvokedPaymentApp.getInstrumentMethodNames()) {
if (mMethodData.containsKey(paymentMethodName)) {
methodData.put(paymentMethodName, mMethodData.get(paymentMethodName));
}
if (mModifiers != null && mModifiers.containsKey(paymentMethodName)) {
modifiers.put(paymentMethodName, mModifiers.get(paymentMethodName));
}
if (paymentMethodName.equals(MethodStrings.ANDROID_PAY)
|| paymentMethodName.equals(MethodStrings.GOOGLE_PAY)) {
isGooglePaymentApp = true;
}
}
mInvokedPaymentApp.setPaymentHandlerHost(getPaymentHandlerHost());
// Only native apps can use PaymentDetailsUpdateService.
if (mInvokedPaymentApp.getPaymentAppType() == PaymentAppType.NATIVE_MOBILE_APP) {
PaymentDetailsUpdateServiceHelper.getInstance().initialize(new PackageManagerDelegate(),
((AndroidPaymentApp) mInvokedPaymentApp).packageName(),
this /* PaymentApp.PaymentRequestUpdateEventListener */);
}
// Create payment options for the invoked payment app.
PaymentOptions paymentOptions = new PaymentOptions();
paymentOptions.requestShipping =
mRequestShipping && mInvokedPaymentApp.handlesShippingAddress();
paymentOptions.requestPayerName =
mRequestPayerName && mInvokedPaymentApp.handlesPayerName();
paymentOptions.requestPayerPhone =
mRequestPayerPhone && mInvokedPaymentApp.handlesPayerPhone();
paymentOptions.requestPayerEmail =
mRequestPayerEmail && mInvokedPaymentApp.handlesPayerEmail();
paymentOptions.shippingType =
mRequestShipping && mInvokedPaymentApp.handlesShippingAddress()
? mShippingType
: PaymentShippingType.SHIPPING;
// Redact shipping options if the selected app cannot handle shipping.
List<PaymentShippingOption> redactedShippingOptions =
mInvokedPaymentApp.handlesShippingAddress()
? mRawShippingOptions
: Collections.unmodifiableList(new ArrayList<>());
mInvokedPaymentApp.invokePaymentApp(mId, mMerchantName, mTopLevelOrigin,
mPaymentRequestOrigin, mCertificateChain, Collections.unmodifiableMap(methodData),
mRawTotal, mRawLineItems, Collections.unmodifiableMap(modifiers), paymentOptions,
redactedShippingOptions, this);
mJourneyLogger.setEventOccurred(Event.PAY_CLICKED);
boolean isAutofillCard = mInvokedPaymentApp.isAutofillInstrument();
// Record what type of app was selected when "Pay" was clicked.
if (isAutofillCard) {
mJourneyLogger.setEventOccurred(Event.SELECTED_CREDIT_CARD);
} else if (isGooglePaymentApp) {
mJourneyLogger.setEventOccurred(Event.SELECTED_GOOGLE);
} else {
mJourneyLogger.setEventOccurred(Event.SELECTED_OTHER);
}
return !isAutofillCard;
}
private PaymentHandlerHost getPaymentHandlerHost() {
if (mPaymentHandlerHost == null) {
mPaymentHandlerHost = new PaymentHandlerHost(mWebContents, /*delegate=*/this);
}
return mPaymentHandlerHost;
}
@Override
public JourneyLogger getJourneyLogger() {
return mJourneyLogger;
}
@Override
public void onDismiss() {
mJourneyLogger.setAborted(AbortReason.ABORTED_BY_USER);
disconnectFromClientWithDebugMessage(ErrorStrings.USER_CANCELLED);
}
// Implement ComponentPaymentRequestDelegate:
// This method is not supposed to be used outside this class and
// ComponentPaymentRequestImpl.
@Override
public void disconnectFromClientWithDebugMessage(String debugMessage) {
disconnectFromClientWithDebugMessage(debugMessage, PaymentErrorReason.USER_CANCEL);
}
private void disconnectFromClientWithDebugMessage(String debugMessage, int reason) {
Log.d(TAG, debugMessage);
PaymentRequestClient client = getClient();
if (client != null) client.onError(reason, debugMessage);
closeClient();
closeUIAndDestroyNativeObjects();
if (mNativeObserverForTest != null) mNativeObserverForTest.onConnectionTerminated();
}
// Implement ComponentPaymentRequestDelegate:
/**
* Called by the merchant website to abort the payment.
*/
@Override
public void abort() {
if (getClient() == null) return;
if (mInvokedPaymentApp != null) {
mInvokedPaymentApp.abortPaymentApp(/*callback=*/this);
return;
}
onInstrumentAbortResult(true);
}
/** Called by the payment app in response to an abort request. */
@Override
public void onInstrumentAbortResult(boolean abortSucceeded) {
PaymentRequestClient client = getClient();
if (client == null) return;
client.onAbort(abortSucceeded);
if (abortSucceeded) {
closeClient();
mJourneyLogger.setAborted(AbortReason.ABORTED_BY_MERCHANT);
closeUIAndDestroyNativeObjects();
} else {
if (sObserverForTest != null) sObserverForTest.onPaymentRequestServiceUnableToAbort();
}
if (mNativeObserverForTest != null) mNativeObserverForTest.onAbortCalled();
}
// Implement ComponentPaymentRequestDelegate:
/**
* Called when the merchant website has processed the payment.
*/
@Override
public void complete(int result) {
if (getClient() == null) return;
if (result != PaymentComplete.FAIL) {
mJourneyLogger.setCompleted();
if (!PaymentPreferencesUtil.isPaymentCompleteOnce()) {
PaymentPreferencesUtil.setPaymentCompleteOnce();
}
assert mRawTotal != null;
mJourneyLogger.recordTransactionAmount(
mRawTotal.amount.currency, mRawTotal.amount.value, true /*completed*/);
}
/** Update records of the used payment app for sorting payment apps next time. */
EditableOption selectedPaymentMethod = mPaymentMethodsSection.getSelectedItem();
PaymentPreferencesUtil.increasePaymentAppUseCount(selectedPaymentMethod.getIdentifier());
PaymentPreferencesUtil.setPaymentAppLastUseDate(
selectedPaymentMethod.getIdentifier(), System.currentTimeMillis());
if (mMinimalUi != null) {
if (result == PaymentComplete.FAIL) {
mMinimalUi.showErrorAndClose(
this::onMinimalUiErroredAndClosed, R.string.payments_error_message);
} else {
mMinimalUi.showCompleteAndClose(this::onMinimalUiCompletedAndClosed);
}
return;
}
if (mNativeObserverForTest != null) {
mNativeObserverForTest.onCompleteCalled();
}
closeUIAndDestroyNativeObjects();
}
// Implement ComponentPaymentRequestDelegate:
@Override
public void retry(PaymentValidationErrors errors) {
if (getClient() == null) return;
if (!PaymentValidator.validatePaymentValidationErrors(errors)) {
mJourneyLogger.setAborted(AbortReason.INVALID_DATA_FROM_RENDERER);
disconnectFromClientWithDebugMessage(ErrorStrings.INVALID_VALIDATION_ERRORS);
return;
}
mSpec.retry(errors);
mWasRetryCalled = true;
// Remove all payment apps except the selected one.
assert mPaymentMethodsSection != null;
PaymentApp selectedApp = (PaymentApp) mPaymentMethodsSection.getSelectedItem();
assert selectedApp != null;
mPaymentMethodsSection = new SectionInformation(PaymentRequestUI.DataType.PAYMENT_METHODS,
/* selection = */ 0, new ArrayList<>(Arrays.asList(selectedApp)));
mUI.updateSection(PaymentRequestUI.DataType.PAYMENT_METHODS, mPaymentMethodsSection);
mUI.disableAddingNewCardsDuringRetry();
// Go back to the payment sheet
mUI.onPayButtonProcessingCancelled();
PaymentDetailsUpdateServiceHelper.getInstance().reset();
if (!TextUtils.isEmpty(errors.error)) {
mUI.setRetryErrorMessage(errors.error);
} else {
ChromeActivity activity = ChromeActivity.fromWebContents(mWebContents);
mUI.setRetryErrorMessage(
activity.getResources().getString(R.string.payments_error_message));
}
if (shouldShowShippingSection() && hasShippingAddressError(errors.shippingAddress)) {
mRetryQueue.add(() -> {
mAddressEditor.setAddressErrors(errors.shippingAddress);
AutofillAddress selectedAddress =
(AutofillAddress) mShippingAddressesSection.getSelectedItem();
editAddress(selectedAddress);
});
}
if (shouldShowContactSection() && hasPayerError(errors.payer)) {
mRetryQueue.add(() -> {
mContactEditor.setPayerErrors(errors.payer);
AutofillContact selectedContact =
(AutofillContact) mContactSection.getSelectedItem();
editContact(selectedContact);
});
}
if (!mRetryQueue.isEmpty()) mHandler.post(mRetryQueue.remove());
}
private boolean hasShippingAddressError(AddressErrors errors) {
return !TextUtils.isEmpty(errors.addressLine) || !TextUtils.isEmpty(errors.city)
|| !TextUtils.isEmpty(errors.country)
|| !TextUtils.isEmpty(errors.dependentLocality)
|| !TextUtils.isEmpty(errors.organization) || !TextUtils.isEmpty(errors.phone)
|| !TextUtils.isEmpty(errors.postalCode) || !TextUtils.isEmpty(errors.recipient)
|| !TextUtils.isEmpty(errors.region) || !TextUtils.isEmpty(errors.sortingCode);
}
private boolean hasPayerError(PayerErrors errors) {
return !TextUtils.isEmpty(errors.name) || !TextUtils.isEmpty(errors.phone)
|| !TextUtils.isEmpty(errors.email);
}
@Override
public void onCardAndAddressSettingsClicked() {
Context context = ChromeActivity.fromWebContents(mWebContents);
if (context == null) {
mJourneyLogger.setAborted(AbortReason.OTHER);
disconnectFromClientWithDebugMessage(ErrorStrings.ACTIVITY_NOT_FOUND);
return;
}
SettingsLauncher settingsLauncher = new SettingsLauncherImpl();
settingsLauncher.launchSettingsActivity(context);
}
@Override
public void onAddressUpdated(AutofillAddress address) {
if (getClient() == null) return;
address.setShippingAddressLabelWithCountry();
mCardEditor.updateBillingAddressIfComplete(address);
if (mShippingAddressesSection != null) {
mShippingAddressesSection.addAndSelectOrUpdateItem(address);
mUI.updateSection(
PaymentRequestUI.DataType.SHIPPING_ADDRESSES, mShippingAddressesSection);
}
if (mContactSection != null) {
mContactSection.addOrUpdateWithAutofillAddress(address);
mUI.updateSection(PaymentRequestUI.DataType.CONTACT_DETAILS, mContactSection);
}
}
@Override
public void onAddressDeleted(String guid) {
if (getClient() == null) return;
// TODO: Delete the address from mShippingAddressesSection and mContactSection. Note that we
// only displayed SUGGESTIONS_LIMIT addresses, so we may want to add back previously
// ignored addresses.
}
@Override
public void onCreditCardUpdated(CreditCard card) {
if (getClient() == null || !mMerchantSupportsAutofillCards || mPaymentMethodsSection == null
|| mAutofillPaymentAppCreator == null) {
return;
}
PaymentApp updatedAutofillCard = mAutofillPaymentAppCreator.createPaymentAppForCard(card);
// Can be null when the card added through settings does not match the requested card
// network or is invalid, because autofill settings do not perform the same level of
// validation as Basic Card implementation in Chrome.
if (updatedAutofillCard == null) return;
mPaymentMethodsSection.addAndSelectOrUpdateItem(updatedAutofillCard);
updateAppModifiedTotals();
if (mUI != null) {
mUI.updateSection(PaymentRequestUI.DataType.PAYMENT_METHODS, mPaymentMethodsSection);
}
}
@Override
public void onCreditCardDeleted(String guid) {
if (getClient() == null) return;
if (!mMerchantSupportsAutofillCards || mPaymentMethodsSection == null) return;
mPaymentMethodsSection.removeAndUnselectItem(guid);
updateAppModifiedTotals();
if (mUI != null) {
mUI.updateSection(PaymentRequestUI.DataType.PAYMENT_METHODS, mPaymentMethodsSection);
}
}
// Implement ComponentPaymentRequestDelegate:
/** Called by the merchant website to check if the user has complete payment apps. */
@Override
public void canMakePayment() {
if (getClient() == null) return;
if (mNativeObserverForTest != null) mNativeObserverForTest.onCanMakePaymentCalled();
if (mIsFinishedQueryingPaymentApps) {
respondCanMakePaymentQuery();
} else {
mIsCanMakePaymentResponsePending = true;
}
}
private void respondCanMakePaymentQuery() {
PaymentRequestClient client = getClient();
if (client == null) return;
mIsCanMakePaymentResponsePending = false;
boolean response = mCanMakePayment && mDelegate.prefsCanMakePayment();
client.onCanMakePayment(response ? CanMakePaymentQueryResult.CAN_MAKE_PAYMENT
: CanMakePaymentQueryResult.CANNOT_MAKE_PAYMENT);
mJourneyLogger.setCanMakePaymentValue(response || mIsOffTheRecord);
if (sObserverForTest != null) {
sObserverForTest.onPaymentRequestServiceCanMakePaymentQueryResponded();
}
if (mNativeObserverForTest != null) mNativeObserverForTest.onCanMakePaymentReturned();
}
// Implement ComponentPaymentRequestDelegate:
/** Called by the merchant website to check if the user has complete payment instruments. */
@Override
public void hasEnrolledInstrument(boolean perMethodQuota) {
if (getClient() == null) return;
if (mNativeObserverForTest != null) mNativeObserverForTest.onHasEnrolledInstrumentCalled();
mHasEnrolledInstrumentUsesPerMethodQuota = perMethodQuota;
if (mIsFinishedQueryingPaymentApps) {
respondHasEnrolledInstrumentQuery(mHasEnrolledInstrument);
} else {
mIsHasEnrolledInstrumentResponsePending = true;
}
}
private void respondHasEnrolledInstrumentQuery(boolean response) {
PaymentRequestClient client = getClient();
if (client == null) return;
mIsHasEnrolledInstrumentResponsePending = false;
if (CanMakePaymentQuery.canQuery(mWebContents, mTopLevelOrigin, mPaymentRequestOrigin,
mQueryForQuota, mHasEnrolledInstrumentUsesPerMethodQuota)) {
client.onHasEnrolledInstrument(response
? HasEnrolledInstrumentQueryResult.HAS_ENROLLED_INSTRUMENT
: HasEnrolledInstrumentQueryResult.HAS_NO_ENROLLED_INSTRUMENT);
} else if (shouldEnforceCanMakePaymentQueryQuota()) {
client.onHasEnrolledInstrument(HasEnrolledInstrumentQueryResult.QUERY_QUOTA_EXCEEDED);
} else {
client.onHasEnrolledInstrument(response
? HasEnrolledInstrumentQueryResult.WARNING_HAS_ENROLLED_INSTRUMENT
: HasEnrolledInstrumentQueryResult.WARNING_HAS_NO_ENROLLED_INSTRUMENT);
}
mJourneyLogger.setHasEnrolledInstrumentValue(response || mIsOffTheRecord);
if (sObserverForTest != null) {
sObserverForTest.onPaymentRequestServiceHasEnrolledInstrumentQueryResponded();
}
if (mNativeObserverForTest != null) {
mNativeObserverForTest.onHasEnrolledInstrumentReturned();
}
}
/**
* @return Whether canMakePayment() query quota should be enforced. By default, the quota is
* enforced only on https:// scheme origins. However, the tests also enable the quota on
* localhost and file:// scheme origins to verify its behavior.
*/
private boolean shouldEnforceCanMakePaymentQueryQuota() {
// If |mWebContents| is destroyed, don't bother checking the localhost or file:// scheme
// exemption. It doesn't really matter anyways.
return mWebContents.isDestroyed()
|| !UrlUtil.isLocalDevelopmentUrl(mWebContents.getLastCommittedUrl())
|| sIsLocalCanMakePaymentQueryQuotaEnforcedForTest;
}
// Implement ComponentPaymentRequestDelegate:
/**
* Called when the renderer closes the Mojo connection.
*/
@Override
public void close() {
if (getClient() == null) return;
closeClient();
mJourneyLogger.setAborted(AbortReason.MOJO_RENDERER_CLOSING);
if (sObserverForTest != null) sObserverForTest.onRendererClosedMojoConnection();
closeUIAndDestroyNativeObjects();
if (mNativeObserverForTest != null) mNativeObserverForTest.onConnectionTerminated();
}
// Implement ComponentPaymentRequestDelegate:
/**
* Called when the Mojo connection encounters an error.
*/
@Override
public void onConnectionError(MojoException e) {
if (getClient() == null) return;
closeClient();
mJourneyLogger.setAborted(AbortReason.MOJO_CONNECTION_ERROR);
closeUIAndDestroyNativeObjects();
if (mNativeObserverForTest != null) mNativeObserverForTest.onConnectionTerminated();
}
// PaymentAppFactoryParams implementation.
@Override
public WebContents getWebContents() {
return mWebContents;
}
// PaymentAppFactoryParams implementation.
@Override
public RenderFrameHost getRenderFrameHost() {
return mRenderFrameHost;
}
// PaymentAppFactoryParams implementation.
@Override
public Map<String, PaymentMethodData> getMethodData() {
return mMethodData;
}
// PaymentAppFactoryParams implementation.
@Override
public String getId() {
return mId;
}
// PaymentAppFactoryParams implementation.
@Override
public String getTopLevelOrigin() {
return mTopLevelOrigin;
}
// PaymentAppFactoryParams implementation.
@Override
public String getPaymentRequestOrigin() {
return mPaymentRequestOrigin;
}
// PaymentAppFactoryParams implementation.
@Override
public Origin getPaymentRequestSecurityOrigin() {
return mPaymentRequestSecurityOrigin;
}
// PaymentAppFactoryParams implementation.
@Override
@Nullable
public byte[][] getCertificateChain() {
return mCertificateChain;
}
// PaymentAppFactoryParams implementation.
@Override
public Map<String, PaymentDetailsModifier> getModifiers() {
return mModifiers == null ? new HashMap<>() : Collections.unmodifiableMap(mModifiers);
}
// PaymentAppFactoryParams implementation.
@Override
public boolean getMayCrawl() {
return !mUserCanAddCreditCard
|| PaymentFeatureList.isEnabledOrExperimentalFeaturesEnabled(
PaymentFeatureList.WEB_PAYMENTS_ALWAYS_ALLOW_JUST_IN_TIME_PAYMENT_APP);
}
// PaymentAppFactoryParams implementation.
@Override
public PaymentRequestUpdateEventListener getPaymentRequestUpdateEventListener() {
return this;
}
// PaymentAppFactoryParams implementation.
@Override
public String getTotalAmountCurrency() {
return mRawTotal.amount.currency;
}
// PaymentAppFactoryParams implementation.
@Override
public boolean requestShippingOrPayerContact() {
return mRequestShipping || mRequestPayerName || mRequestPayerPhone || mRequestPayerEmail;
}
// PaymentAppFactoryParams implementation.
@Override
public PaymentRequestSpec getSpec() {
return mSpec;
}
// PaymentAppFactoryDelegate implementation.
@Override
public PaymentAppFactoryParams getParams() {
return this;
}
// PaymentAppFactoryDelegate implementation.
@Override
public void onCanMakePaymentCalculated(boolean canMakePayment) {
if (getClient() == null) return;
mCanMakePayment = canMakePayment;
if (!mIsCanMakePaymentResponsePending) return;
// canMakePayment doesn't need to wait for all apps to be queried because it only needs to
// test the existence of a payment handler.
respondCanMakePaymentQuery();
}
// PaymentAppFactoryDelegate implementation.
@Override
public void onAutofillPaymentAppCreatorAvailable(AutofillPaymentAppCreator creator) {
mAutofillPaymentAppCreator = creator;
}
// PaymentAppFactoryDelegate implementation.
@Override
public void onPaymentAppCreated(PaymentApp paymentApp) {
if (getClient() == null) return;
mHideServerAutofillCards |= paymentApp.isServerAutofillInstrumentReplacement();
paymentApp.setHaveRequestedAutofillData(mHaveRequestedAutofillData);
mHasEnrolledInstrument |= paymentApp.canMakePayment();
mHasNonAutofillApp |= !paymentApp.isAutofillInstrument();
if (paymentApp.isAutofillInstrument()) {
mJourneyLogger.setEventOccurred(Event.AVAILABLE_METHOD_BASIC_CARD);
} else if (paymentApp.getInstrumentMethodNames().contains(MethodStrings.GOOGLE_PAY)
|| paymentApp.getInstrumentMethodNames().contains(MethodStrings.ANDROID_PAY)) {
mJourneyLogger.setEventOccurred(Event.AVAILABLE_METHOD_GOOGLE);
} else {
mJourneyLogger.setEventOccurred(Event.AVAILABLE_METHOD_OTHER);
}
mPendingApps.add(paymentApp);
}
// PaymentAppFactoryDelegate implementation.
@Override
public void onPaymentAppCreationError(String errorMessage) {
if (TextUtils.isEmpty(mRejectShowErrorMessage)) mRejectShowErrorMessage = errorMessage;
}
// PaymentAppFactoryDelegate implementation.
@Override
public void onDoneCreatingPaymentApps(PaymentAppFactoryInterface factory /* Unused */) {
mIsFinishedQueryingPaymentApps = true;
if (getClient() == null || disconnectIfNoPaymentMethodsSupported()) {
return;
}
// Always return false when can make payment is disabled.
mHasEnrolledInstrument &= mDelegate.prefsCanMakePayment();
if (mHideServerAutofillCards) {
List<PaymentApp> nonServerAutofillCards = new ArrayList<>();
int numberOfPendingApps = mPendingApps.size();
for (int i = 0; i < numberOfPendingApps; i++) {
if (!mPendingApps.get(i).isServerAutofillInstrument()) {
nonServerAutofillCards.add(mPendingApps.get(i));
}
}
mPendingApps = nonServerAutofillCards;
}
// Load the validation rules for each unique region code in the credit card billing
// addresses and check for validity.
Set<String> uniqueCountryCodes = new HashSet<>();
for (int i = 0; i < mPendingApps.size(); ++i) {
@Nullable
String countryCode = mPendingApps.get(i).getCountryCode();
if (countryCode != null && !uniqueCountryCodes.contains(countryCode)) {
uniqueCountryCodes.add(countryCode);
PersonalDataManager.getInstance().loadRulesForAddressNormalization(countryCode);
}
}
Collections.sort(mPendingApps, mPaymentAppComparator);
// Possibly pre-select the first app on the list.
int selection = !mPendingApps.isEmpty() && mPendingApps.get(0).canPreselect()
? 0
: SectionInformation.NO_SELECTION;
if (mIsCanMakePaymentResponsePending) {
respondCanMakePaymentQuery();
}
if (mIsHasEnrolledInstrumentResponsePending) {
respondHasEnrolledInstrumentQuery(mHasEnrolledInstrument);
}
ChromeActivity chromeActivity = ChromeActivity.fromWebContents(mWebContents);
if (chromeActivity == null) {
mJourneyLogger.setNotShown(NotShownReason.OTHER);
disconnectFromClientWithDebugMessage(ErrorStrings.ACTIVITY_NOT_FOUND);
if (sObserverForTest != null) sObserverForTest.onPaymentRequestServiceShowFailed();
return;
}
// The list of payment apps is ready to display.
mPaymentMethodsSection = new SectionInformation(PaymentRequestUI.DataType.PAYMENT_METHODS,
selection, new ArrayList<>(mPendingApps));
// Record the number suggested payment methods and whether at least one of them was
// complete.
mJourneyLogger.setNumberOfSuggestionsShown(Section.PAYMENT_METHOD, mPendingApps.size(),
!mPendingApps.isEmpty() && mPendingApps.get(0).isComplete());
int missingFields = 0;
if (mPendingApps.isEmpty()) {
if (mMerchantSupportsAutofillCards) {
// Record all fields if basic-card is supported but no card exists.
missingFields = AutofillPaymentInstrument.CompletionStatus.CREDIT_CARD_EXPIRED
| AutofillPaymentInstrument.CompletionStatus.CREDIT_CARD_NO_CARDHOLDER
| AutofillPaymentInstrument.CompletionStatus.CREDIT_CARD_NO_NUMBER
| AutofillPaymentInstrument.CompletionStatus.CREDIT_CARD_NO_BILLING_ADDRESS;
}
} else if (mPendingApps.get(0).isAutofillInstrument()) {
missingFields = ((AutofillPaymentInstrument) (mPendingApps.get(0))).getMissingFields();
}
if (missingFields != 0) {
RecordHistogram.recordSparseHistogram(
"PaymentRequest.MissingPaymentFields", missingFields);
}
mPendingApps.clear();
updateAppModifiedTotals();
SettingsAutofillAndPaymentsObserver.getInstance().registerObserver(this);
if (mIsCurrentPaymentRequestShowing) {
// Send AppListReady signal when all apps are created and request.show() is called.
if (mNativeObserverForTest != null) {
mNativeObserverForTest.onAppListReady(mPaymentMethodsSection.getItems(), mRawTotal);
}
// Calculate skip ui and build ui only after all payment apps are ready and
// request.show() is called.
calculateWhetherShouldSkipShowingPaymentRequestUi();
if (!buildUI(chromeActivity)) return;
if (!mShouldSkipShowingPaymentRequestUi && mSkipToGPayHelper == null) {
mUI.show();
}
}
triggerPaymentAppUiSkipIfApplicable(chromeActivity);
}
/**
* If no payment methods are supported, disconnect from the client and return true.
* @return Whether client has been disconnected.
*/
private boolean disconnectIfNoPaymentMethodsSupported() {
if (!mIsFinishedQueryingPaymentApps || !mIsCurrentPaymentRequestShowing) return false;
boolean havePaymentApps = !mPendingApps.isEmpty()
|| (mPaymentMethodsSection != null && !mPaymentMethodsSection.isEmpty());
if (!mCanMakePayment || (!havePaymentApps && !mMerchantSupportsAutofillCards)) {
// All factories have responded, but none of them have apps. It's possible to add credit
// cards, but the merchant does not support them either. The payment request must be
// rejected.
mJourneyLogger.setNotShown(mCanMakePayment
? NotShownReason.NO_MATCHING_PAYMENT_METHOD
: NotShownReason.NO_SUPPORTED_PAYMENT_METHOD);
if (mIsProhibitedOriginOrInvalidSsl) {
if (mNativeObserverForTest != null) mNativeObserverForTest.onNotSupportedError();
// Chrome always refuses payments with invalid SSL and in prohibited origin types.
disconnectFromClientWithDebugMessage(
mRejectShowErrorMessage, PaymentErrorReason.NOT_SUPPORTED);
} else if (mIsOffTheRecord) {
// If the user is in the OffTheRecord mode, hide the absence of their payment
// methods from the merchant site.
disconnectFromClientWithDebugMessage(
ErrorStrings.USER_CANCELLED, PaymentErrorReason.USER_CANCEL);
} else {
if (mNativeObserverForTest != null) mNativeObserverForTest.onNotSupportedError();
if (TextUtils.isEmpty(mRejectShowErrorMessage) && !isInTwa()
&& mMethodData.get(MethodStrings.GOOGLE_PLAY_BILLING) != null) {
mRejectShowErrorMessage = ErrorStrings.APP_STORE_METHOD_ONLY_SUPPORTED_IN_TWA;
}
disconnectFromClientWithDebugMessage(
ErrorMessageUtil.getNotSupportedErrorMessage(mMethodData.keySet())
+ (TextUtils.isEmpty(mRejectShowErrorMessage)
? ""
: " " + mRejectShowErrorMessage),
PaymentErrorReason.NOT_SUPPORTED);
}
if (sObserverForTest != null) sObserverForTest.onPaymentRequestServiceShowFailed();
return true;
}
return disconnectForStrictShow();
}
private boolean isInTwa() {
return !TextUtils.isEmpty(
mDelegate.getTwaPackageName(ChromeActivity.fromWebContents(mWebContents)));
}
/**
* If strict show() conditions are not satisfied, disconnect from client and return true.
* @return Whether client has been disconnected.
*/
private boolean disconnectForStrictShow() {
if (!mIsUserGestureShow || !mMethodData.containsKey(MethodStrings.BASIC_CARD)
|| mHasEnrolledInstrument || mHasNonAutofillApp
|| !PaymentFeatureList.isEnabledOrExperimentalFeaturesEnabled(
PaymentFeatureList.STRICT_HAS_ENROLLED_AUTOFILL_INSTRUMENT)) {
return false;
}
if (sObserverForTest != null) sObserverForTest.onPaymentRequestServiceShowFailed();
mRejectShowErrorMessage = ErrorStrings.STRICT_BASIC_CARD_SHOW_REJECT;
disconnectFromClientWithDebugMessage(
ErrorMessageUtil.getNotSupportedErrorMessage(mMethodData.keySet()) + " "
+ mRejectShowErrorMessage,
PaymentErrorReason.NOT_SUPPORTED);
return true;
}
/** Called after retrieving payment details. */
@Override
public void onInstrumentDetailsReady(
String methodName, String stringifiedDetails, PayerData payerData) {
assert methodName != null;
assert stringifiedDetails != null;
if (getClient() == null || mPaymentResponseHelper == null) {
return;
}
// If the payment method was an Autofill credit card with an identifier, record its use.
PaymentApp selectedPaymentMethod = (PaymentApp) mPaymentMethodsSection.getSelectedItem();
if (selectedPaymentMethod != null
&& selectedPaymentMethod.getPaymentAppType() == PaymentAppType.AUTOFILL
&& !selectedPaymentMethod.getIdentifier().isEmpty()) {
PersonalDataManager.getInstance().recordAndLogCreditCardUse(
selectedPaymentMethod.getIdentifier());
}
// Showing the payment request UI if we were previously skipping it so the loading
// spinner shows up until the merchant notifies that payment was completed.
if (mShouldSkipShowingPaymentRequestUi && mUI != null) {
mUI.showProcessingMessageAfterUiSkip();
}
mJourneyLogger.setEventOccurred(Event.RECEIVED_INSTRUMENT_DETAILS);
mPaymentResponseHelper.onPaymentDetailsReceived(methodName, stringifiedDetails, payerData);
}
@Override
public void onPaymentResponseReady(PaymentResponse response) {
if (mSkipToGPayHelper != null && !mSkipToGPayHelper.patchPaymentResponse(response)) {
disconnectFromClientWithDebugMessage(
ErrorStrings.PAYMENT_APP_INVALID_RESPONSE, PaymentErrorReason.NOT_SUPPORTED);
}
PaymentRequestClient client = getClient();
if (client == null) return;
client.onPaymentResponse(response);
mPaymentResponseHelper = null;
if (sObserverForTest != null) sObserverForTest.onPaymentResponseReady();
}
/** Called if unable to retrieve payment details. */
@Override
public void onInstrumentDetailsError(String errorMessage) {
if (getClient() == null) return;
mInvokedPaymentApp = null;
if (mMinimalUi != null) {
mJourneyLogger.setAborted(AbortReason.ABORTED_BY_USER);
mMinimalUi.showErrorAndClose(
this::onMinimalUiErroredAndClosed, R.string.payments_error_message);
return;
}
// When skipping UI, any errors/cancel from fetching payment details should abort payment.
if (mShouldSkipShowingPaymentRequestUi) {
assert !TextUtils.isEmpty(errorMessage);
mJourneyLogger.setAborted(AbortReason.ABORTED_BY_USER);
disconnectFromClientWithDebugMessage(errorMessage);
} else {
mUI.onPayButtonProcessingCancelled();
PaymentDetailsUpdateServiceHelper.getInstance().reset();
}
}
@Override
public void onFocusChanged(@PaymentRequestUI.DataType int dataType, boolean willFocus) {
assert dataType == PaymentRequestUI.DataType.SHIPPING_ADDRESSES;
if (mShippingAddressesSection.getSelectedItem() == null) return;
AutofillAddress selectedAddress =
(AutofillAddress) mShippingAddressesSection.getSelectedItem();
// The label should only include the country if the view is focused.
if (willFocus) {
selectedAddress.setShippingAddressLabelWithCountry();
} else {
selectedAddress.setShippingAddressLabelWithoutCountry();
}
mUI.updateSection(PaymentRequestUI.DataType.SHIPPING_ADDRESSES, mShippingAddressesSection);
}
@Override
public void onAddressNormalized(AutofillProfile profile) {
PaymentRequestClient client = getClient();
if (client == null) return;
ChromeActivity chromeActivity = ChromeActivity.fromWebContents(mWebContents);
// Can happen if the tab is closed during the normalization process.
if (chromeActivity == null) {
mJourneyLogger.setAborted(AbortReason.OTHER);
disconnectFromClientWithDebugMessage(ErrorStrings.ACTIVITY_NOT_FOUND);
if (sObserverForTest != null) sObserverForTest.onPaymentRequestServiceShowFailed();
return;
}
// Don't reuse the selected address because it is formatted for display.
AutofillAddress shippingAddress = new AutofillAddress(chromeActivity, profile);
PaymentAddress redactedAddress = shippingAddress.toPaymentAddress();
redactShippingAddress(redactedAddress);
// This updates the line items and the shipping options asynchronously.
client.onShippingAddressChange(redactedAddress);
}
@Override
public void onCouldNotNormalize(AutofillProfile profile) {
// Since the phone number is formatted in either case, this profile should be used.
onAddressNormalized(profile);
}
/**
* Starts the normalization of the new shipping address. Will call back into either
* onAddressNormalized or onCouldNotNormalize which will send the result to the merchant.
*/
private void startShippingAddressChangeNormalization(AutofillAddress address) {
PersonalDataManager.getInstance().normalizeAddress(address.getProfile(), this);
}
private void ensureHideAndResetPaymentHandlerUi() {
if (mPaymentHandlerUi == null) return;
mPaymentHandlerUi.hide();
mPaymentHandlerUi = null;
}
/**
* Closes the UI and destroys native objects. If the client is still connected, then it's
* notified of UI hiding. This PaymentRequestImpl object can't be reused after this function is
* called.
*/
private void closeUIAndDestroyNativeObjects() {
ensureHideAndResetPaymentHandlerUi();
if (mMinimalUi != null) {
mMinimalUi.hide();
mMinimalUi = null;
}
if (mUI != null) {
mUI.close();
PaymentRequestClient client = getClient();
if (client != null) {
if (sObserverForTest != null) sObserverForTest.onCompleteReplied();
client.onComplete();
closeClient();
}
ChromeActivity activity = ChromeActivity.fromWebContents(mWebContents);
if (activity != null) {
activity.getLifecycleDispatcher().unregister(mUI);
}
mUI = null;
mPaymentUisShowStateReconciler.onPaymentRequestUiClosed();
}
setShowingPaymentRequest(null);
mIsCurrentPaymentRequestShowing = false;
if (mPaymentMethodsSection != null) {
for (int i = 0; i < mPaymentMethodsSection.getSize(); i++) {
EditableOption option = mPaymentMethodsSection.getItem(i);
((PaymentApp) option).dismissInstrument();
}
mPaymentMethodsSection = null;
}
if (mObservedTabModelSelector != null) {
mObservedTabModelSelector.removeObserver(mSelectorObserver);
mObservedTabModelSelector = null;
}
if (mObservedTabModel != null) {
mObservedTabModel.removeObserver(mTabModelObserver);
mObservedTabModel = null;
}
if (mOverviewModeBehavior != null) {
mOverviewModeBehavior.removeOverviewModeObserver(mOverviewModeObserver);
mOverviewModeBehavior = null;
}
SettingsAutofillAndPaymentsObserver.getInstance().unregisterObserver(this);
// Destroy native objects.
for (CurrencyFormatter formatter : mCurrencyFormatterMap.values()) {
assert formatter != null;
// Ensures the native implementation of currency formatter does not leak.
formatter.destroy();
}
mJourneyLogger.destroy();
if (mPaymentHandlerHost != null) {
mPaymentHandlerHost.destroy();
mPaymentHandlerHost = null;
}
if (mSpec != null) {
mSpec.destroy();
mSpec = null;
}
PaymentDetailsUpdateServiceHelper.getInstance().reset();
}
private void dispatchPayerDetailChangeEventIfNeeded(PayerDetail detail) {
PaymentRequestClient client = getClient();
if (client == null || !mWasRetryCalled) return;
client.onPayerDetailChange(detail);
}
/**
* Redact shipping address before exposing it in ShippingAddressChangeEvent.
* https://w3c.github.io/payment-request/#shipping-address-changed-algorithm
* @param shippingAddress The shippingAddress to get redacted.
*/
private void redactShippingAddress(PaymentAddress shippingAddress) {
if (PaymentFeatureList.isEnabledOrExperimentalFeaturesEnabled(
PaymentFeatureList.WEB_PAYMENTS_REDACT_SHIPPING_ADDRESS)) {
shippingAddress.organization = "";
shippingAddress.phone = "";
shippingAddress.recipient = "";
shippingAddress.addressLine = new String[0];
}
}
/**
* @return Whether any instance of PaymentRequest has received a show() call.
* Don't use this function to check whether the current instance has
* received a show() call.
*/
private static boolean getIsAnyPaymentRequestShowing() {
return sShowingPaymentRequest != null;
}
/** @param paymentRequest The currently showing PaymentRequestImpl. */
private static void setShowingPaymentRequest(PaymentRequestImpl paymentRequest) {
assert sShowingPaymentRequest == null || paymentRequest == null;
sShowingPaymentRequest = paymentRequest;
}
@VisibleForTesting
public static void setObserverForTest(PaymentRequestServiceObserverForTest observerForTest) {
sObserverForTest = observerForTest;
}
@VisibleForTesting
public static void setIsLocalCanMakePaymentQueryQuotaEnforcedForTest() {
sIsLocalCanMakePaymentQueryQuotaEnforcedForTest = true;
}
@VisibleForTesting
/* package */ void setSkipUIForNonURLPaymentMethodIdentifiersForTest() {
mSkipUiForNonUrlPaymentMethodIdentifiers = true;
}
/**
* Compares two payment apps by frecency.
* Return negative value if a has strictly lower frecency score than b.
* Return zero if a and b have the same frecency score.
* Return positive value if a has strictly higher frecency score than b.
*/
private static int compareAppsByFrecency(PaymentApp a, PaymentApp b) {
int aCount = PaymentPreferencesUtil.getPaymentAppUseCount(a.getIdentifier());
int bCount = PaymentPreferencesUtil.getPaymentAppUseCount(b.getIdentifier());
long aDate = PaymentPreferencesUtil.getPaymentAppLastUseDate(a.getIdentifier());
long bDate = PaymentPreferencesUtil.getPaymentAppLastUseDate(a.getIdentifier());
return Double.compare(getFrecencyScore(aCount, aDate), getFrecencyScore(bCount, bDate));
}
/**
* Compares two Completable by completeness score.
* Return negative value if a has strictly lower completeness score than b.
* Return zero if a and b have the same completeness score.
* Return positive value if a has strictly higher completeness score than b.
*/
private static int compareCompletablesByCompleteness(Completable a, Completable b) {
return Integer.compare(a.getCompletenessScore(), b.getCompletenessScore());
}
/**
* The frecency score is calculated according to use count and last use date. The formula is
* the same as the one used in GetFrecencyScore in autofill_data_model.cc.
*/
private static final double getFrecencyScore(int count, long date) {
long currentTime = System.currentTimeMillis();
return -Math.log((currentTime - date) / (24 * 60 * 60 * 1000) + 2) / Math.log(count + 2);
}
@Nullable
private PaymentRequestClient getClient() {
return mComponentPaymentRequestImpl.getClient();
}
// Pre-condition: the client is not null.
private void closeClient() {
assert getClient() != null;
mComponentPaymentRequestImpl.closeClient();
}
}