// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.payments.ui;
import static org.chromium.chrome.browser.payments.ui.PaymentRequestSection.EDIT_BUTTON_GONE;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.Handler;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.method.LinkMovementMethod;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnLayoutChangeListener;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import org.chromium.base.Callback;
import org.chromium.base.ResettersForTesting;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.autofill.editors.EditorDialogView;
import org.chromium.chrome.browser.autofill.editors.EditorObserverForTest;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.lifecycle.PauseResumeWithNativeObserver;
import org.chromium.chrome.browser.payments.ShippingStrings;
import org.chromium.chrome.browser.payments.ui.PaymentRequestSection.LineItemBreakdownSection;
import org.chromium.chrome.browser.payments.ui.PaymentRequestSection.OptionSection;
import org.chromium.chrome.browser.payments.ui.PaymentRequestSection.SectionSeparator;
import org.chromium.chrome.browser.payments.ui.PaymentUiService.PaymentUisShowStateReconciler;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.signin.services.IdentityServicesProvider;
import org.chromium.components.autofill.EditableOption;
import org.chromium.components.browser_ui.widget.FadingEdgeScrollView;
import org.chromium.components.browser_ui.widget.animation.FocusAnimator;
import org.chromium.components.payments.InputProtector;
import org.chromium.components.signin.base.CoreAccountInfo;
import org.chromium.components.signin.identitymanager.ConsentLevel;
import org.chromium.components.signin.identitymanager.IdentityManager;
import org.chromium.ui.base.ViewUtils;
import org.chromium.ui.interpolators.Interpolators;
import org.chromium.ui.text.NoUnderlineClickableSpan;
import org.chromium.ui.text.SpanApplier;
import org.chromium.ui.text.SpanApplier.SpanInfo;
import org.chromium.ui.widget.TextViewWithClickableSpans;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
/** The PaymentRequest UI. */
public class PaymentRequestUI
implements DimmingDialog.OnDismissListener,
View.OnClickListener,
PaymentRequestSection.SectionDelegate,
PauseResumeWithNativeObserver {
@IntDef({
DataType.SHIPPING_ADDRESSES,
DataType.SHIPPING_OPTIONS,
DataType.CONTACT_DETAILS,
DataType.PAYMENT_METHODS
})
@Retention(RetentionPolicy.SOURCE)
public @interface DataType {
int SHIPPING_ADDRESSES = 1;
int SHIPPING_OPTIONS = 2;
int CONTACT_DETAILS = 3;
int PAYMENT_METHODS = 4;
}
@IntDef({
SelectionResult.ASYNCHRONOUS_VALIDATION,
SelectionResult.EDITOR_LAUNCH,
SelectionResult.NONE
})
@Retention(RetentionPolicy.SOURCE)
public @interface SelectionResult {
int ASYNCHRONOUS_VALIDATION = 1;
int EDITOR_LAUNCH = 2;
int NONE = 3;
}
/** The interface to be implemented by the consumer of the PaymentRequest UI. */
public interface Client {
/**
* Asynchronously returns the default payment information.
* @param waitForUpdatedDetails Whether the payment details is pending for updating.
* @param callback Retrieves the data to show in the initial PaymentRequest UI.
*/
void getDefaultPaymentInformation(
boolean waitForUpdatedDetails, Callback<PaymentInformation> callback);
/**
* Asynchronously returns the full bill. Includes the total price and its breakdown into
* individual line items.
*/
void getShoppingCart(Callback<ShoppingCart> callback);
/**
* Asynchronously returns the full list of options for the given type.
*
* @param optionType Data being updated.
* @param callback Callback to run when the data has been fetched.
*/
void getSectionInformation(@DataType int optionType, Callback<SectionInformation> callback);
/**
* Called when the user changes one of their payment options.
*
* If this method returns {@link SelectionResult.ASYNCHRONOUS_VALIDATION}, then:
* + The added option should be asynchronously verified.
* + The section should be disabled and a progress spinny should be shown while the option
* is being verified.
* + The checkedCallback will be invoked with the results of the check and updated
* information.
*
* If this method returns {@link SelectionResult.EDITOR_LAUNCH}, then:
* + Interaction with UI should be disabled until updateSection() is called.
*
* For example, if the website needs a shipping address to calculate shipping options, then
* calling onSectionOptionSelected(DataType.SHIPPING_ADDRESS, option, checkedCallback) will
* return true. When the website updates the shipping options, the checkedCallback will be
* invoked.
*
* @param optionType Data being updated.
* @param option Value of the data being updated.
* @param checkedCallback The callback after an asynchronous check has completed.
* @return The result of the selection.
*/
@SelectionResult
int onSectionOptionSelected(
@DataType int optionType,
EditableOption option,
Callback<PaymentInformation> checkedCallback);
/**
* Called when the user clicks edit icon (pencil icon) on the payment option in a section.
*
* If this method returns {@link SelectionResult.ASYNCHRONOUS_VALIDATION}, then:
* + The edited option should be asynchronously verified.
* + The section should be disabled and a progress spinny should be shown while the option
* is being verified.
* + The checkedCallback will be invoked with the results of the check and updated
* information.
*
* If this method returns {@link SelectionResult.EDITOR_LAUNCH}, then:
* + Interaction with UI should be disabled until updateSection() is called.
*
* @param optionType Data being updated.
* @param option The option to be edited.
* @param checkedCallback The callback after an asynchronous check has completed.
* @return The result of the edit request.
*/
@SelectionResult
int onSectionEditOption(
@DataType int optionType,
EditableOption option,
Callback<PaymentInformation> checkedCallback);
/**
* Called when the user clicks on the "Add" button for a section.
*
* If this method returns {@link SelectionResult.ASYNCHRONOUS_VALIDATION}, then:
* + The added option should be asynchronously verified.
* + The section should be disabled and a progress spinny should be shown while the option
* is being verified.
* + The checkedCallback will be invoked with the results of the check and updated
* information.
*
* If this method returns {@link SelectionResult.EDITOR_LAUNCH}, then:
* + Interaction with UI should be disabled until updateSection() is called.
*
* @param optionType Data being updated.
* @param checkedCallback The callback after an asynchronous check has completed.
* @return The result of the selection.
*/
@SelectionResult
int onSectionAddOption(
@DataType int optionType, Callback<PaymentInformation> checkedCallback);
/**
* Called when the user clicks on the “Pay” button. If this method returns true, the UI is
* disabled and is showing a spinner. Otherwise, the UI is hidden.
*/
boolean onPayClicked(
EditableOption selectedShippingAddress,
EditableOption selectedShippingOption,
EditableOption selectedPaymentMethod);
/**
* Called when the user dismisses the UI via the “back” button on their phone
* or the “X” button in UI.
*/
void onDismiss();
/** Called when the user clicks on 'Settings' to control card and address options. */
void onCardAndAddressSettingsClicked();
/**
* Returns true when shipping address is requested and the selected payment method cannot
* provide it.
*/
boolean shouldShowShippingSection();
/**
* Returns true when payer's contact details is requested and the selected payment method
* cannot provide it.
*/
boolean shouldShowContactSection();
}
/** A test-only observer for PaymentRequest UI. */
public interface PaymentRequestObserverForTest {
/** Called immediately when PaymentRequestUI#show() is called. */
void onPaymentRequestUIShow(PaymentRequestUI ui);
/** Called when clicks on the UI are possible. */
void onPaymentRequestReadyForInput(PaymentRequestUI ui);
/** Called when clicks on the PAY button are possible. */
void onPaymentRequestReadyToPay(PaymentRequestUI ui);
/** Called when the UI has been updated to reflect checking a selected option. */
void onPaymentRequestSelectionChecked(PaymentRequestUI ui);
/** Called when the result UI is showing. */
void onPaymentRequestResultReady(PaymentRequestUI ui);
}
/** Helper to notify tests of an event only once. */
private static class NotifierForTest {
private final Handler mHandler;
private final Runnable mNotification;
private boolean mNotificationPending;
/**
* Constructs the helper to notify tests for an event.
*
* @param notification The callback that notifies the test of an event.
*/
public NotifierForTest(final Runnable notification) {
mHandler = new Handler();
mNotification =
new Runnable() {
@Override
public void run() {
notification.run();
mNotificationPending = false;
}
};
}
/** Schedules a single notification for test, even if called only once. */
public void run() {
if (mNotificationPending) return;
mNotificationPending = true;
mHandler.post(mNotification);
}
}
/**
* Length of the animation to either show the UI or expand it to full height.
* Note that click of 'Pay' button is not accepted until the animation is done, so this duration
* also serves the function of preventing the user from accidentally double-clicking on the
* screen when triggering payment and thus authorizing unwanted transaction.
*/
private static final int DIALOG_ENTER_ANIMATION_MS = 225;
private static PaymentRequestObserverForTest sPaymentRequestObserverForTest;
private static EditorObserverForTest sEditorObserverForTest;
/** Notifies tests that the [PAY] button can be clicked. */
private final NotifierForTest mReadyToPayNotifierForTest;
private final Context mContext;
private final Client mClient;
private final boolean mShowDataSource;
private final PaymentUisShowStateReconciler mPaymentUisShowStateReconciler;
private final Profile mProfile;
/**
* The top level container of this UI. When needing to call show() or hide(), use {@link
* PaymentUisShowStateReconciler}'s showPaymentRequestDialogWhenNoBottomSheet() and
* hidePaymentRequestDialog() instead.
*/
private final DimmingDialog mDialog;
private final EditorDialogView mEditorDialog;
private final ViewGroup mRequestView;
private final Callback<PaymentInformation> mUpdateSectionsCallback;
private final ShippingStrings mShippingStrings;
private final int mAnimatorTranslation;
private FadingEdgeScrollView mPaymentContainer;
private LinearLayout mPaymentContainerLayout;
private TextView mRetryErrorView;
private ViewGroup mBottomBar;
private Button mEditButton;
private Button mPayButton;
private View mCloseButton;
private View mSpinnyLayout;
private LineItemBreakdownSection mOrderSummarySection;
private OptionSection mShippingAddressSection;
private OptionSection mShippingOptionSection;
private OptionSection mContactDetailsSection;
private OptionSection mPaymentMethodSection;
private List<SectionSeparator> mSectionSeparators;
private PaymentRequestSection mSelectedSection;
private boolean mIsExpandedToFullHeight;
private boolean mIsProcessingPayClicked;
private boolean mIsClientClosing;
private boolean mIsClientCheckingSelection;
private boolean mIsShowingSpinner;
private boolean mIsEditingPaymentItem;
private boolean mIsClosing;
private SectionInformation mPaymentMethodSectionInformation;
private SectionInformation mShippingAddressSectionInformation;
private SectionInformation mShippingOptionsSectionInformation;
private SectionInformation mContactDetailsSectionInformation;
private Animator mSheetAnimator;
private FocusAnimator mSectionAnimator;
private InputProtector mInputProtector = new InputProtector();
/**
* Builds the UI for PaymentRequest.
*
* @param activity The activity on top of which the UI should be displayed.
* @param client The consumer of the PaymentRequest UI.
* @param showDataSource Whether the UI should describe the source of Autofill data.
* @param title The title to show at the top of the UI. This can be, for
* example, the <title> of the merchant website. If the
* string is too long for UI, it elides at the end.
* @param origin The origin (https://tools.ietf.org/html/rfc6454) to show under
* the title. For example, "https://shop.momandpop.com". If the
* origin is too long for the UI, it should elide according to:
* https://www.chromium.org/Home/chromium-security/enamel#TOC-Eliding-Origin-Names-And-Hostnames
* @param securityLevel The security level of the page that invoked PaymentRequest.
* @param shippingStrings The string resource identifiers to use in the shipping sections.
* @param profile The current profile that creates the PaymentRequestUI.
*/
public PaymentRequestUI(
Activity activity,
Client client,
boolean showDataSource,
String title,
String origin,
int securityLevel,
ShippingStrings shippingStrings,
PaymentUisShowStateReconciler paymentUisShowStateReconciler,
Profile profile) {
mContext = activity;
mClient = client;
mShowDataSource = showDataSource;
mAnimatorTranslation =
mContext.getResources().getDimensionPixelSize(R.dimen.payments_ui_translation);
mProfile = profile;
mReadyToPayNotifierForTest =
new NotifierForTest(
new Runnable() {
@Override
public void run() {
if (sPaymentRequestObserverForTest != null
&& isAcceptingUserInput()
&& mPayButton.isEnabled()) {
sPaymentRequestObserverForTest.onPaymentRequestReadyToPay(
PaymentRequestUI.this);
}
}
});
// This callback will be fired if mIsClientCheckingSelection is true.
mUpdateSectionsCallback =
new Callback<PaymentInformation>() {
@Override
public void onResult(PaymentInformation result) {
mIsClientCheckingSelection = false;
updateOrderSummarySection(result.getShoppingCart());
if (mClient.shouldShowShippingSection()) {
updateSection(
DataType.SHIPPING_ADDRESSES, result.getShippingAddresses());
updateSection(DataType.SHIPPING_OPTIONS, result.getShippingOptions());
}
if (mClient.shouldShowContactSection()) {
updateSection(DataType.CONTACT_DETAILS, result.getContactDetails());
}
updateSection(DataType.PAYMENT_METHODS, result.getPaymentMethods());
if (mShippingAddressSectionInformation != null
&& mShippingAddressSectionInformation.getSelectedItem() == null) {
expand(mShippingAddressSection);
} else {
expand(null);
}
updatePayButtonEnabled();
notifySelectionChecked();
}
};
mShippingStrings = shippingStrings;
mRequestView =
(ViewGroup) LayoutInflater.from(mContext).inflate(R.layout.payment_request, null);
prepareRequestView(mContext, title, origin, securityLevel, profile);
mEditorDialog = new EditorDialogView(activity, profile);
DimmingDialog.setVisibleStatusBarIconColor(mEditorDialog.getWindow());
mDialog = new DimmingDialog(activity, this);
mPaymentUisShowStateReconciler = paymentUisShowStateReconciler;
}
/**
* Shows the PaymentRequest UI. This will dim the background behind the PaymentRequest UI.
* @param waitForUpdatedDetails Whether the payment details is pending to be updated.
*/
public void show(boolean waitForUpdatedDetails) {
mInputProtector.markShowTime();
mDialog.addBottomSheetView(mRequestView);
mPaymentUisShowStateReconciler.showPaymentRequestDialogWhenNoBottomSheet();
mClient.getDefaultPaymentInformation(
waitForUpdatedDetails,
new Callback<PaymentInformation>() {
@Override
public void onResult(PaymentInformation result) {
updateOrderSummarySection(result.getShoppingCart());
if (mClient.shouldShowShippingSection()) {
updateSection(
DataType.SHIPPING_ADDRESSES, result.getShippingAddresses());
updateSection(DataType.SHIPPING_OPTIONS, result.getShippingOptions());
}
if (mClient.shouldShowContactSection()) {
updateSection(DataType.CONTACT_DETAILS, result.getContactDetails());
}
mPaymentMethodSection.setDisplaySummaryInSingleLineInNormalMode(
result.getPaymentMethods()
.getDisplaySelectedItemSummaryInSingleLineInNormalMode());
updateSection(DataType.PAYMENT_METHODS, result.getPaymentMethods());
updatePayButtonEnabled();
// Hide the loading indicators and show the real sections.
changeSpinnerVisibility(false);
mRequestView.addOnLayoutChangeListener(new SheetEnlargingAnimator(false));
}
});
if (sPaymentRequestObserverForTest != null) {
sPaymentRequestObserverForTest.onPaymentRequestUIShow(PaymentRequestUI.this);
}
}
/**
* Dim the background without showing any UI. No UI will be interactive. The dimming stops when
* close() is called. This is useful for the skip-ui scenario, i.e., launching a payment handler
* directly without showing a PaymentRequest UI first in cases where only one payment handler is
* available.
*/
public void dimBackground() {
// Intentionally do not add the bottom sheet view to mDialog so that only the scrim part of
// the dialog will be shown.
mPaymentUisShowStateReconciler.showPaymentRequestDialogWhenNoBottomSheet();
}
/**
* Prepares the PaymentRequestUI for initial display.
*
* TODO(dfalcantara): Ideally, everything related to the request and its views would just be put
* into its own class but that'll require yanking out a lot of this class.
*
* @param context The application context.
* @param title Title of the page.
* @param origin The RFC6454 origin of the page.
* @param securityLevel The security level of the page that invoked PaymentRequest.
* @param profile The current profile to pass PaymentRequestHeader.
*/
private void prepareRequestView(
Context context, String title, String origin, int securityLevel, Profile profile) {
mSpinnyLayout = mRequestView.findViewById(R.id.payment_request_spinny);
assert mSpinnyLayout.getVisibility() == View.VISIBLE;
mIsShowingSpinner = true;
// Indicate that we're preparing the dialog for display.
TextView messageView = mRequestView.findViewById(R.id.message);
messageView.setText(R.string.payments_loading_message);
((PaymentRequestHeader) mRequestView.findViewById(R.id.header))
.setTitleAndOrigin(title, origin, securityLevel, profile);
// Set up the buttons.
mCloseButton = mRequestView.findViewById(R.id.close_button);
mCloseButton.setOnClickListener(this);
mBottomBar = mRequestView.findViewById(R.id.bottom_bar);
mPayButton = mBottomBar.findViewById(R.id.button_primary);
mPayButton.setOnClickListener(this);
mPayButton.setText(R.string.payments_continue_button);
mEditButton = mBottomBar.findViewById(R.id.button_secondary);
mEditButton.setOnClickListener(this);
// Create all the possible sections.
mSectionSeparators = new ArrayList<>();
mPaymentContainer = mRequestView.findViewById(R.id.option_container);
mPaymentContainerLayout = mRequestView.findViewById(R.id.payment_container_layout);
mRetryErrorView = mRequestView.findViewById(R.id.retry_error);
mOrderSummarySection =
new LineItemBreakdownSection(
context,
context.getString(R.string.payments_order_summary_label),
this,
context.getString(R.string.payments_updated_label));
mShippingAddressSection =
new OptionSection(
context, context.getString(mShippingStrings.getAddressLabel()), this);
mShippingOptionSection =
new OptionSection(
context, context.getString(mShippingStrings.getOptionLabel()), this);
mContactDetailsSection =
new OptionSection(
context, context.getString(R.string.payments_contact_details_label), this);
mPaymentMethodSection =
new OptionSection(
context,
context.getString(R.string.payments_method_of_payment_label),
this);
// Display the summary of the selected address in multiple lines on bottom sheet.
mShippingAddressSection.setDisplaySummaryInSingleLineInNormalMode(false);
// Display selected shipping option name in the left summary text view and
// the cost in the right summary text view on bottom sheet.
mShippingOptionSection.setSplitSummaryInDisplayModeNormal(true);
// The user cannot add new shipping options or payment methods.
mShippingOptionSection.setCanAddItems(false);
mPaymentMethodSection.setCanAddItems(false);
// Add the necessary sections to the layout.
mPaymentContainerLayout.addView(
mOrderSummarySection,
new LinearLayout.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
SectionSeparator shippingSectionSeparator = new SectionSeparator(mPaymentContainerLayout);
mSectionSeparators.add(shippingSectionSeparator);
mPaymentContainerLayout.addView(
mShippingAddressSection,
new LinearLayout.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
// The shipping breakout sections are visible only if they are needed.
if (!mClient.shouldShowShippingSection()) {
mShippingAddressSection.setVisibility(View.GONE);
shippingSectionSeparator.setVisibility(View.GONE);
}
mSectionSeparators.add(new SectionSeparator(mPaymentContainerLayout));
mPaymentContainerLayout.addView(
mPaymentMethodSection,
new LinearLayout.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
SectionSeparator contactSectionSeparator = new SectionSeparator(mPaymentContainerLayout);
mSectionSeparators.add(contactSectionSeparator);
mPaymentContainerLayout.addView(
mContactDetailsSection,
new LinearLayout.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
// Contact details are optional, depending on the merchant website, and whether or not the
// selected payment app can provide them.
if (!mClient.shouldShowContactSection()) {
mContactDetailsSection.setVisibility(View.GONE);
contactSectionSeparator.setVisibility(View.GONE);
}
mRequestView.addOnLayoutChangeListener(new PeekingAnimator());
// Enabled in updatePayButtonEnabled() when the user has selected all payment options.
mPayButton.setEnabled(false);
}
/**
* Closes the UI. Can be invoked in response to, for example:
* <ul>
* <li>Successfully processing the payment.</li>
* <li>Failure to process the payment.</li>
* <li>The JavaScript calling the abort() method in PaymentRequest API.</li>
* <li>The PaymentRequest JavaScript object being destroyed.</li>
* </ul>
*
* Does not call Client.onDismissed().
*
* Should not be called multiple times.
*/
public void close() {
mIsClientClosing = true;
dismissDialog(false);
if (sPaymentRequestObserverForTest != null) {
sPaymentRequestObserverForTest.onPaymentRequestResultReady(this);
}
}
/**
* Sets the icon in the top left of the UI. This can be, for example, the favicon of the
* merchant website. This is not a part of the constructor because favicon retrieval is
* asynchronous.
*
* @param bitmap The bitmap to show next to the title.
*/
public void setTitleBitmap(Bitmap bitmap) {
((PaymentRequestHeader) mRequestView.findViewById(R.id.header)).setTitleBitmap(bitmap);
}
/**
* Sets the retry error message. This is used to display error message on the header UI when
* retry() is called on merchant side. The error message may be reset when users click 'Pay'
* button or expand any section.
*
* @param error The error message to display on the header.
*/
public void setRetryErrorMessage(String error) {
if (mRetryErrorView == null) return;
mRetryErrorView.setText(error);
if (TextUtils.isEmpty(error)) {
mRetryErrorView.setVisibility(View.GONE);
} else {
if (mIsExpandedToFullHeight) {
// Add padding instead of margin to let getMeasuredHeight return correct value for
// section resize animation.
int paddingSize =
mContext.getResources()
.getDimensionPixelSize(R.dimen.editor_dialog_section_large_spacing);
mRetryErrorView.setPaddingRelative(0, paddingSize, 0, paddingSize);
} else {
mRetryErrorView.setPaddingRelative(0, 0, 0, 0);
}
mRetryErrorView.setVisibility(View.VISIBLE);
}
}
/**
* Updates the line items in response to a changed shipping address or option.
*
* @param cart The shopping cart, including the line items and the total.
*/
public void updateOrderSummarySection(ShoppingCart cart) {
if (cart == null || cart.getTotal() == null) {
mOrderSummarySection.setVisibility(View.GONE);
} else {
mOrderSummarySection.setVisibility(View.VISIBLE);
mOrderSummarySection.update(cart);
}
}
/**
* Updates the UI to account for changes in different sections information.
*
* @param whichSection The type of the updated section.
* @param section The updated section information.
*/
public void updateSection(@DataType int whichSection, SectionInformation section) {
if (whichSection == DataType.SHIPPING_ADDRESSES) {
mShippingAddressSectionInformation = section;
mShippingAddressSection.update(section);
} else if (whichSection == DataType.SHIPPING_OPTIONS) {
mShippingOptionsSectionInformation = section;
mShippingOptionSection.update(section);
addShippingOptionSectionIfNecessary();
} else if (whichSection == DataType.CONTACT_DETAILS) {
mContactDetailsSectionInformation = section;
mContactDetailsSection.update(section);
} else if (whichSection == DataType.PAYMENT_METHODS) {
mPaymentMethodSectionInformation = section;
mPaymentMethodSection.update(section);
}
boolean isFinishingEditItem = mIsEditingPaymentItem;
mIsEditingPaymentItem = false;
updateSectionButtons();
updatePayButtonEnabled();
// Notify ready for input for test if this is finishing editing item.
if (isFinishingEditItem) notifyReadyForInput();
}
// Only add shipping option section once there are shipping options.
private void addShippingOptionSectionIfNecessary() {
if (!mClient.shouldShowShippingSection()
|| mShippingOptionsSectionInformation.isEmpty()
|| mPaymentContainerLayout.indexOfChild(mShippingOptionSection) != -1) {
return;
}
// Shipping option section is added below shipping address section.
int addressSectionIndex = mPaymentContainerLayout.indexOfChild(mShippingAddressSection);
SectionSeparator sectionSeparator =
new SectionSeparator(mPaymentContainerLayout, addressSectionIndex + 1);
mSectionSeparators.add(sectionSeparator);
if (mIsExpandedToFullHeight) sectionSeparator.expand();
mPaymentContainerLayout.addView(
mShippingOptionSection,
addressSectionIndex + 2,
new LinearLayout.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
ViewUtils.requestLayout(
mPaymentContainerLayout, "PaymentRequestUI.addShippingOptionSectionIfNecessary");
}
/**
* Notifies the UI about the changes in selected payment method.
*
* @param paymentInformation The updated payment information.
*/
public void selectedPaymentMethodUpdated(PaymentInformation paymentInformation) {
if (mClient.shouldShowShippingSection()
&& mShippingAddressSection.getVisibility() == View.GONE) {
updateSection(DataType.SHIPPING_ADDRESSES, paymentInformation.getShippingAddresses());
updateSection(DataType.SHIPPING_OPTIONS, paymentInformation.getShippingOptions());
// Show shipping address section and its separator.
mShippingAddressSection.setVisibility(View.VISIBLE);
int addressSectionIndex = mPaymentContainerLayout.indexOfChild(mShippingAddressSection);
mPaymentContainerLayout.getChildAt(addressSectionIndex - 1).setVisibility(View.VISIBLE);
// Show shipping option section (if it exists) and its separator.
int shippingOptionSectionIndex =
mPaymentContainerLayout.indexOfChild(mShippingOptionSection);
if (shippingOptionSectionIndex != -1) {
mShippingOptionSection.setVisibility(View.VISIBLE);
mPaymentContainerLayout
.getChildAt(shippingOptionSectionIndex - 1)
.setVisibility(View.VISIBLE);
}
} else if (!mClient.shouldShowShippingSection()
&& mShippingAddressSection.getVisibility() == View.VISIBLE) {
// Hide shipping address section and its separator.
mShippingAddressSection.setVisibility(View.GONE);
int addressSectionIndex = mPaymentContainerLayout.indexOfChild(mShippingAddressSection);
mPaymentContainerLayout.getChildAt(addressSectionIndex - 1).setVisibility(View.GONE);
// Hide shipping option section (if exists) and its separator.
int shippingOptionSectionIndex =
mPaymentContainerLayout.indexOfChild(mShippingOptionSection);
if (shippingOptionSectionIndex != -1) {
mShippingOptionSection.setVisibility(View.GONE);
mPaymentContainerLayout
.getChildAt(shippingOptionSectionIndex - 1)
.setVisibility(View.GONE);
}
}
if (mClient.shouldShowContactSection()
&& mContactDetailsSection.getVisibility() == View.GONE) {
updateSection(DataType.CONTACT_DETAILS, paymentInformation.getContactDetails());
// Show contact details section and its separator.
mContactDetailsSection.setVisibility(View.VISIBLE);
int contactSectionIndex = mPaymentContainerLayout.indexOfChild(mContactDetailsSection);
mPaymentContainerLayout.getChildAt(contactSectionIndex - 1).setVisibility(View.VISIBLE);
} else if (!mClient.shouldShowContactSection()
&& mContactDetailsSection.getVisibility() == View.VISIBLE) {
// Hide contact details section and its separator.
mContactDetailsSection.setVisibility(View.GONE);
int contactSectionIndex = mPaymentContainerLayout.indexOfChild(mContactDetailsSection);
mPaymentContainerLayout.getChildAt(contactSectionIndex - 1).setVisibility(View.GONE);
}
ViewUtils.requestLayout(
mPaymentContainerLayout, "PaymentRequestUI.selectedPaymentMethodUpdated");
}
@Override
public void onEditableOptionChanged(
final PaymentRequestSection section, EditableOption option) {
@SelectionResult int result = SelectionResult.NONE;
if (section == mShippingAddressSection
&& mShippingAddressSectionInformation.getSelectedItem() != option) {
mShippingAddressSectionInformation.setSelectedItem(option);
result =
mClient.onSectionOptionSelected(
DataType.SHIPPING_ADDRESSES, option, mUpdateSectionsCallback);
} else if (section == mShippingOptionSection
&& mShippingOptionsSectionInformation.getSelectedItem() != option) {
mShippingOptionsSectionInformation.setSelectedItem(option);
result =
mClient.onSectionOptionSelected(
DataType.SHIPPING_OPTIONS, option, mUpdateSectionsCallback);
} else if (section == mContactDetailsSection) {
mContactDetailsSectionInformation.setSelectedItem(option);
result =
mClient.onSectionOptionSelected(
DataType.CONTACT_DETAILS, option, mUpdateSectionsCallback);
} else if (section == mPaymentMethodSection) {
mPaymentMethodSectionInformation.setSelectedItem(option);
result = mClient.onSectionOptionSelected(DataType.PAYMENT_METHODS, option, null);
}
updateStateFromResult(section, result);
}
@Override
public void onEditEditableOption(final PaymentRequestSection section, EditableOption option) {
@SelectionResult int result = SelectionResult.NONE;
assert section != mOrderSummarySection;
assert section != mShippingOptionSection;
if (section == mShippingAddressSection) {
assert mShippingAddressSectionInformation.getSelectedItem() == option;
result =
mClient.onSectionEditOption(
DataType.SHIPPING_ADDRESSES, option, mUpdateSectionsCallback);
}
if (section == mContactDetailsSection) {
assert mContactDetailsSectionInformation.getSelectedItem() == option;
result = mClient.onSectionEditOption(DataType.CONTACT_DETAILS, option, null);
}
if (section == mPaymentMethodSection) {
assert mPaymentMethodSectionInformation.getSelectedItem() == option;
result = mClient.onSectionEditOption(DataType.PAYMENT_METHODS, option, null);
}
updateStateFromResult(section, result);
}
@Override
public void onAddEditableOption(PaymentRequestSection section) {
assert section != mShippingOptionSection;
@SelectionResult int result = SelectionResult.NONE;
if (section == mShippingAddressSection) {
result =
mClient.onSectionAddOption(
DataType.SHIPPING_ADDRESSES, mUpdateSectionsCallback);
} else if (section == mContactDetailsSection) {
result = mClient.onSectionAddOption(DataType.CONTACT_DETAILS, null);
} else if (section == mPaymentMethodSection) {
result = mClient.onSectionAddOption(DataType.PAYMENT_METHODS, null);
}
updateStateFromResult(section, result);
}
void updateStateFromResult(PaymentRequestSection section, @SelectionResult int result) {
mIsClientCheckingSelection = result == SelectionResult.ASYNCHRONOUS_VALIDATION;
mIsEditingPaymentItem = result == SelectionResult.EDITOR_LAUNCH;
if (mIsClientCheckingSelection) {
mSelectedSection = section;
updateSectionVisibility();
section.setDisplayMode(PaymentRequestSection.DISPLAY_MODE_CHECKING);
} else {
expand(null);
}
updatePayButtonEnabled();
}
@Override
public boolean isBoldLabelNeeded(PaymentRequestSection section) {
return section == mShippingAddressSection;
}
/** @return The common editor user interface. */
public EditorDialogView getEditorDialog() {
return mEditorDialog;
}
/** Called when user clicks anything in the dialog. */
// View.OnClickListener implementation.
@Override
public void onClick(View v) {
if (!isAcceptingCloseButton()) return;
if (v == mCloseButton) {
dismissDialog(true);
return;
}
if (!isAcceptingUserInput()) return;
// Users can only expand incomplete sections by clicking on their edit buttons.
if (v instanceof PaymentRequestSection) {
PaymentRequestSection section = (PaymentRequestSection) v;
if (section.getEditButtonState() != EDIT_BUTTON_GONE) return;
}
if (v == mOrderSummarySection) {
expand(mOrderSummarySection);
} else if (v == mShippingAddressSection) {
expand(mShippingAddressSection);
} else if (v == mShippingOptionSection) {
expand(mShippingOptionSection);
} else if (v == mContactDetailsSection) {
expand(mContactDetailsSection);
} else if (v == mPaymentMethodSection) {
expand(mPaymentMethodSection);
} else if (v == mPayButton) {
processPayButton();
} else if (v == mEditButton) {
if (mIsExpandedToFullHeight) {
dismissDialog(true);
} else {
expand(mOrderSummarySection);
}
}
setRetryErrorMessage(null);
updatePayButtonEnabled();
}
/**
* Dismiss the dialog.
*
* @param isAnimated If true, the dialog dismissal is animated.
*/
private void dismissDialog(boolean isAnimated) {
mIsClosing = true;
mDialog.dismiss(isAnimated);
}
private void processPayButton() {
assert !mIsShowingSpinner;
mIsProcessingPayClicked = true;
boolean shouldShowSpinner =
mClient.onPayClicked(
mShippingAddressSectionInformation == null
? null
: mShippingAddressSectionInformation.getSelectedItem(),
mShippingOptionsSectionInformation == null
? null
: mShippingOptionsSectionInformation.getSelectedItem(),
mPaymentMethodSectionInformation.getSelectedItem());
if (shouldShowSpinner) {
changeSpinnerVisibility(true);
} else {
mPaymentUisShowStateReconciler.hidePaymentRequestDialog();
}
}
/** Called when user cancelled out of the UI that was shown after they clicked [PAY] button. */
public void onPayButtonProcessingCancelled() {
assert mIsProcessingPayClicked;
mIsProcessingPayClicked = false;
changeSpinnerVisibility(false);
mPaymentUisShowStateReconciler.showPaymentRequestDialogWhenNoBottomSheet();
updatePayButtonEnabled();
}
/**
* Called to show the processing message after payment details have been loaded in the case the
* payment request UI has been skipped.
*/
public void showProcessingMessageAfterUiSkip() {
// Button was clicked before but not marked as clicked because we skipped the UI.
mIsProcessingPayClicked = true;
showProcessingMessage();
}
/**
* Called when the user has clicked on pay. The message is shown while the payment information
* is processed right until a confirmation from the merchant is received.
*/
public void showProcessingMessage() {
assert mIsProcessingPayClicked;
changeSpinnerVisibility(true);
mPaymentUisShowStateReconciler.showPaymentRequestDialogWhenNoBottomSheet();
}
private void changeSpinnerVisibility(boolean showSpinner) {
if (mIsShowingSpinner == showSpinner) return;
mIsShowingSpinner = showSpinner;
if (showSpinner) {
mPaymentContainer.setVisibility(View.GONE);
mBottomBar.setVisibility(View.GONE);
mCloseButton.setVisibility(View.GONE);
mSpinnyLayout.setVisibility(View.VISIBLE);
// Turn the bottom sheet back into a collapsed bottom sheet showing only the spinner.
// TODO(dfalcantara): Animate this: https://crbug.com/621955
((FrameLayout.LayoutParams) mRequestView.getLayoutParams()).height =
LayoutParams.WRAP_CONTENT;
ViewUtils.requestLayout(mRequestView, "PaymentRequestUI.changeSpinnerVisibility show");
} else {
mPaymentContainer.setVisibility(View.VISIBLE);
mBottomBar.setVisibility(View.VISIBLE);
mCloseButton.setVisibility(View.VISIBLE);
mSpinnyLayout.setVisibility(View.GONE);
if (mIsExpandedToFullHeight) {
((FrameLayout.LayoutParams) mRequestView.getLayoutParams()).height =
LayoutParams.MATCH_PARENT;
ViewUtils.requestLayout(
mRequestView,
"PaymentRequestUI.changeSpinnerVisibility expanded to full height");
}
}
}
private void updatePayButtonEnabled() {
boolean contactInfoOk =
!mClient.shouldShowContactSection()
|| (mContactDetailsSectionInformation != null
&& mContactDetailsSectionInformation.getSelectedItem() != null);
boolean shippingInfoOk =
!mClient.shouldShowShippingSection()
|| (mShippingAddressSectionInformation != null
&& mShippingAddressSectionInformation.getSelectedItem() != null);
boolean shippingOptionInfoOk =
!mClient.shouldShowShippingSection()
|| (mShippingOptionsSectionInformation != null
&& mShippingOptionsSectionInformation.getSelectedItem() != null);
mPayButton.setEnabled(
contactInfoOk
&& shippingInfoOk
&& shippingOptionInfoOk
&& mPaymentMethodSectionInformation != null
&& mPaymentMethodSectionInformation.getSelectedItem() != null
&& !mIsClientCheckingSelection
&& !mIsEditingPaymentItem
&& !mIsClosing);
mReadyToPayNotifierForTest.run();
}
/** @return Whether or not the dialog can be closed via the X close button. */
private boolean isAcceptingCloseButton() {
assert mInputProtector != null;
return !mDialog.isAnimatingDisappearance()
&& mSheetAnimator == null
&& mSectionAnimator == null
&& !mIsProcessingPayClicked
&& !mIsEditingPaymentItem
&& !mIsClosing
&& mInputProtector.shouldInputBeProcessed();
}
/** @return Whether or not the dialog is accepting user input. */
@Override
public boolean isAcceptingUserInput() {
return isAcceptingCloseButton()
&& mPaymentMethodSectionInformation != null
&& !mIsClientCheckingSelection;
}
/**
* Sets the observer to be called when the shipping address section gains or loses focus.
*
* @param observer The observer to notify.
*/
public void setShippingAddressSectionFocusChangedObserver(
OptionSection.FocusChangedObserver observer) {
mShippingAddressSection.setOptionSectionFocusChangedObserver(observer);
}
private void expand(PaymentRequestSection section) {
if (!mIsExpandedToFullHeight) {
// Container now takes the full height of the screen, animating towards it.
mRequestView.getLayoutParams().height = LayoutParams.MATCH_PARENT;
mRequestView.addOnLayoutChangeListener(new SheetEnlargingAnimator(true));
// New separators appear at the top and bottom of the list.
mPaymentContainer.setEdgeVisibility(
FadingEdgeScrollView.EdgeType.HARD, FadingEdgeScrollView.EdgeType.FADING);
mSectionSeparators.add(new SectionSeparator(mPaymentContainerLayout, -1));
// Add a link to Autofill settings.
addCardAndAddressOptionsSettingsView(mPaymentContainerLayout);
// Expand all the dividers.
for (int i = 0; i < mSectionSeparators.size(); i++) mSectionSeparators.get(i).expand();
ViewUtils.requestLayout(mPaymentContainerLayout, "PaymentRequestUI.expand");
// Switch the 'edit' button to a 'cancel' button.
mEditButton.setText(mContext.getString(R.string.cancel));
// Disable all but the first button.
updateSectionButtons();
mIsExpandedToFullHeight = true;
}
// Update the section contents when they're selected.
mSelectedSection = section;
if (mSelectedSection == mOrderSummarySection) {
mClient.getShoppingCart(
new Callback<ShoppingCart>() {
@Override
public void onResult(ShoppingCart result) {
updateOrderSummarySection(result);
updateSectionVisibility();
}
});
} else if (mSelectedSection == mShippingAddressSection) {
mClient.getSectionInformation(
DataType.SHIPPING_ADDRESSES,
createUpdateSectionCallback(DataType.SHIPPING_ADDRESSES));
} else if (mSelectedSection == mShippingOptionSection) {
mClient.getSectionInformation(
DataType.SHIPPING_OPTIONS,
createUpdateSectionCallback(DataType.SHIPPING_OPTIONS));
} else if (mSelectedSection == mContactDetailsSection) {
mClient.getSectionInformation(
DataType.CONTACT_DETAILS,
createUpdateSectionCallback(DataType.CONTACT_DETAILS));
} else if (mSelectedSection == mPaymentMethodSection) {
mClient.getSectionInformation(
DataType.PAYMENT_METHODS,
createUpdateSectionCallback(DataType.PAYMENT_METHODS));
} else {
updateSectionVisibility();
}
}
private void addCardAndAddressOptionsSettingsView(LinearLayout parent) {
String message;
if (!mShowDataSource) {
message = mContext.getString(R.string.payments_card_and_address_settings);
} else {
String email = getSignedInUsersEmail();
if (email != null) {
message =
mContext.getString(
R.string.payments_card_and_address_settings_signed_in, email);
} else {
message =
mContext.getString(R.string.payments_card_and_address_settings_signed_out);
}
}
NoUnderlineClickableSpan settingsSpan =
new NoUnderlineClickableSpan(
mContext, (widget) -> mClient.onCardAndAddressSettingsClicked());
SpannableString spannableMessage =
SpanApplier.applySpans(
message, new SpanInfo("BEGIN_LINK", "END_LINK", settingsSpan));
TextView view = new TextViewWithClickableSpans(mContext);
view.setText(spannableMessage);
view.setMovementMethod(LinkMovementMethod.getInstance());
view.setTextAppearance(R.style.TextAppearance_TextMedium_Secondary);
// Add padding instead of margin to let getMeasuredHeight return correct value for section
// resize animation.
int paddingSize =
mContext.getResources()
.getDimensionPixelSize(R.dimen.editor_dialog_section_large_spacing);
view.setPaddingRelative(paddingSize, paddingSize, paddingSize, paddingSize);
parent.addView(view);
}
/**
* Get the email of the signed-in user, if possible. This is not necessarily the email shown or
* being used for contact details (if they were requested), but is the email that
* cards/addresses are being synced to.
*
* @return The email of signed in user or null.
*/
private @Nullable String getSignedInUsersEmail() {
if (mProfile.isOffTheRecord()) {
return null;
}
IdentityManager identityManager =
IdentityServicesProvider.get().getIdentityManager(mProfile);
if (identityManager == null) return null;
@ConsentLevel
int consentLevel =
ChromeFeatureList.isEnabled(
ChromeFeatureList.REPLACE_SYNC_PROMOS_WITH_SIGN_IN_PROMOS)
? ConsentLevel.SIGNIN
: ConsentLevel.SYNC;
CoreAccountInfo info = identityManager.getPrimaryAccountInfo(consentLevel);
return CoreAccountInfo.getEmailFrom(info);
}
private Callback<SectionInformation> createUpdateSectionCallback(@DataType final int type) {
return new Callback<SectionInformation>() {
@Override
public void onResult(SectionInformation result) {
updateSection(type, result);
updateSectionVisibility();
}
};
}
/** Update the display status of each expandable section in the full dialog. */
private void updateSectionVisibility() {
startSectionResizeAnimation();
mOrderSummarySection.focusSection(mSelectedSection == mOrderSummarySection);
if (mClient.shouldShowShippingSection()) {
mShippingAddressSection.focusSection(mSelectedSection == mShippingAddressSection);
mShippingOptionSection.focusSection(mSelectedSection == mShippingOptionSection);
}
if (mClient.shouldShowContactSection()) {
mContactDetailsSection.focusSection(mSelectedSection == mContactDetailsSection);
}
mPaymentMethodSection.focusSection(mSelectedSection == mPaymentMethodSection);
updateSectionButtons();
}
/**
* Updates the enabled/disabled state of each section's edit button.
*
* Only the top-most button is enabled -- the others are disabled so the user is directed
* through the form from top to bottom.
*/
private void updateSectionButtons() {
// Disable edit buttons when the client is checking a selection.
boolean mayEnableButton = !mIsClientCheckingSelection;
for (int i = 0; i < mPaymentContainerLayout.getChildCount(); i++) {
View child = mPaymentContainerLayout.getChildAt(i);
if (!(child instanceof PaymentRequestSection)) continue;
PaymentRequestSection section = (PaymentRequestSection) child;
section.setIsEditButtonEnabled(mayEnableButton);
if (section.getEditButtonState() != EDIT_BUTTON_GONE) mayEnableButton = false;
}
}
/**
* Called when the dialog is dismissed. Can be caused by:
* <ul>
* <li>User click on the "back" button on the phone.</li>
* <li>User click on the "X" button in the top-right corner of the dialog.</li>
* <li>User click on the "CANCEL" button on the bottom of the dialog.</li>
* <li>Successfully processing the payment.</li>
* <li>Failure to process the payment.</li>
* <li>The JavaScript calling the abort() method in PaymentRequest API.</li>
* <li>The PaymentRequest JavaScript object being destroyed.</li>
* <li>User closing all incognito windows with PaymentRequest UI open in an incognito
* window.</li>
* </ul>
*/
// DimmingDialog.OnDismissListener implementation.
@Override
public void onDismiss() {
mIsClosing = true;
if (mEditorDialog.isShowing()) mEditorDialog.dismiss();
if (sEditorObserverForTest != null) sEditorObserverForTest.onEditorDismiss();
if (!mIsClientClosing) mClient.onDismiss();
}
@Override
public String getAdditionalText(PaymentRequestSection section) {
if (section == mShippingAddressSection) {
int selectedItemIndex = mShippingAddressSectionInformation.getSelectedItemIndex();
if (selectedItemIndex != SectionInformation.NO_SELECTION
&& selectedItemIndex != SectionInformation.INVALID_SELECTION) {
return null;
}
String customErrorMessage = mShippingAddressSectionInformation.getErrorMessage();
if (selectedItemIndex == SectionInformation.INVALID_SELECTION
&& !TextUtils.isEmpty(customErrorMessage)) {
return customErrorMessage;
}
return mContext.getString(
selectedItemIndex == SectionInformation.NO_SELECTION
? mShippingStrings.getSelectPrompt()
: mShippingStrings.getUnsupported());
} else if (section == mPaymentMethodSection) {
return mPaymentMethodSectionInformation.getAdditionalText();
} else {
return null;
}
}
@Override
public boolean isAdditionalTextDisplayingWarning(PaymentRequestSection section) {
return section == mShippingAddressSection
&& mShippingAddressSectionInformation != null
&& mShippingAddressSectionInformation.getSelectedItemIndex()
== SectionInformation.INVALID_SELECTION;
}
@Override
public void onSectionClicked(PaymentRequestSection section) {
expand(section);
}
/**
* Animates the different sections of the dialog expanding and contracting into their final
* positions.
*/
private void startSectionResizeAnimation() {
Runnable animationEndRunnable =
new Runnable() {
@Override
public void run() {
mSectionAnimator = null;
notifyReadyForInput();
mReadyToPayNotifierForTest.run();
}
};
mSectionAnimator =
new FocusAnimator(mPaymentContainerLayout, mSelectedSection, animationEndRunnable);
}
/**
* Animates the bottom sheet UI translating upwards from the bottom of the screen.
* Can be canceled when a {@link SheetEnlargingAnimator} starts and expands the dialog.
*/
private class PeekingAnimator extends AnimatorListenerAdapter
implements OnLayoutChangeListener {
@Override
public void onLayoutChange(
View v,
int left,
int top,
int right,
int bottom,
int oldLeft,
int oldTop,
int oldRight,
int oldBottom) {
mRequestView.removeOnLayoutChangeListener(this);
mSheetAnimator =
ObjectAnimator.ofFloat(
mRequestView, View.TRANSLATION_Y, mAnimatorTranslation, 0);
mSheetAnimator.setDuration(DIALOG_ENTER_ANIMATION_MS);
mSheetAnimator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN_INTERPOLATOR);
mSheetAnimator.addListener(this);
mSheetAnimator.start();
}
@Override
public void onAnimationEnd(Animator animation) {
mSheetAnimator = null;
}
}
/** Animates the bottom sheet expanding to a larger sheet. */
private class SheetEnlargingAnimator extends AnimatorListenerAdapter
implements OnLayoutChangeListener {
private final boolean mIsBottomBarLockedInPlace;
private int mContainerHeightDifference;
public SheetEnlargingAnimator(boolean isBottomBarLockedInPlace) {
mIsBottomBarLockedInPlace = isBottomBarLockedInPlace;
}
/**
* Updates the animation.
*
* @param progress How far along the animation is. In the range [0,1], with 1 being done.
*/
private void update(float progress) {
// The dialog container initially starts off translated downward, gradually decreasing
// the translation until it is in the right place on screen.
float containerTranslation = mContainerHeightDifference * progress;
mRequestView.setTranslationY(containerTranslation);
if (mIsBottomBarLockedInPlace) {
// The bottom bar is translated along the dialog so that is looks like it stays in
// place at the bottom while the entire bottom sheet is translating upwards.
mBottomBar.setTranslationY(-containerTranslation);
// The payment container is sandwiched between the header and the bottom bar.
// Expansion animates by changing where its "bottom" is, letting its shadows appear
// and disappear as it changes size.
int paymentContainerBottom =
Math.min(
mPaymentContainer.getTop() + mPaymentContainer.getMeasuredHeight(),
mBottomBar.getTop());
mPaymentContainer.setBottom(paymentContainerBottom);
}
}
@Override
public void onLayoutChange(
View v,
int left,
int top,
int right,
int bottom,
int oldLeft,
int oldTop,
int oldRight,
int oldBottom) {
if (mSheetAnimator != null) mSheetAnimator.cancel();
mRequestView.removeOnLayoutChangeListener(this);
mContainerHeightDifference = (bottom - top) - (oldBottom - oldTop);
ValueAnimator containerAnimator = ValueAnimator.ofFloat(1f, 0f);
containerAnimator.addUpdateListener(
new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float alpha = (Float) animation.getAnimatedValue();
update(alpha);
}
});
mSheetAnimator = containerAnimator;
mSheetAnimator.setDuration(DIALOG_ENTER_ANIMATION_MS);
mSheetAnimator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN_INTERPOLATOR);
mSheetAnimator.addListener(this);
mSheetAnimator.start();
}
@Override
public void onAnimationEnd(Animator animation) {
// Reset the layout so that everything is in the expected place.
mRequestView.setTranslationY(0);
mBottomBar.setTranslationY(0);
ViewUtils.requestLayout(
mRequestView, "PaymentRequestUI.SheetEnlargingAnimator.onAnimationEnd");
// Indicate that the dialog is ready to use.
mSheetAnimator = null;
notifyReadyForInput();
mReadyToPayNotifierForTest.run();
}
}
public static void setEditorObserverForTest(EditorObserverForTest editorObserverForTest) {
sEditorObserverForTest = editorObserverForTest;
EditorDialogView.setEditorObserverForTest(sEditorObserverForTest);
}
public static void setPaymentRequestObserverForTest(
PaymentRequestObserverForTest paymentRequestObserverForTest) {
sPaymentRequestObserverForTest = paymentRequestObserverForTest;
ResettersForTesting.register(() -> sPaymentRequestObserverForTest = null);
}
public void setInputProtectorForTest(InputProtector inputProtector) {
mInputProtector = inputProtector;
}
public Dialog getDialogForTest() {
return mDialog.getDialogForTest();
}
public TextView getOrderSummaryTotalTextViewForTest() {
return mOrderSummarySection.getSummaryRightTextView();
}
public LineItemBreakdownSection getOrderSummarySectionForTest() {
return mOrderSummarySection;
}
public OptionSection getShippingAddressSectionForTest() {
return mShippingAddressSection;
}
public OptionSection getShippingOptionSectionForTest() {
return mShippingOptionSection;
}
public ViewGroup getPaymentMethodSectionForTest() {
return mPaymentMethodSection;
}
public PaymentRequestSection getContactDetailsSectionForTest() {
return mContactDetailsSection;
}
private void notifyReadyForInput() {
if (sPaymentRequestObserverForTest != null && isAcceptingUserInput()) {
sPaymentRequestObserverForTest.onPaymentRequestReadyForInput(this);
}
}
private void notifySelectionChecked() {
if (sPaymentRequestObserverForTest != null) {
sPaymentRequestObserverForTest.onPaymentRequestSelectionChecked(this);
}
}
/**
* Set the visibility state of the dialog. Use {@link PaymentUisShowStateReconciler}'s
* showPaymentRequestDialogWhenNoBottomSheet() and hidePaymentRequestDialog() instead of calling
* this method directly.
* @param visible True to show the dialog, false to hide the dialog.
* @return Whether setting visibility is successful.
*/
public boolean setVisible(boolean visible) {
if (visible) {
return mDialog.show();
} else {
mDialog.hide();
return true;
}
}
// Implement PauseResumeWithNativeObserver:
@Override
public void onResumeWithNative() {
// When users come back from an external activity (e.g., app-picker/webauthn), the PR UI
// somehow shows up even though it's set to GONE (crbug.com/1030416 and
// crbug.com/1051786). Here we use a workaround to fix it - refresh the dialog window
// from time to time to force the visual state to respect its visibility attribute.
mDialog.refresh();
}
// Implement PauseResumeWithNativeObserver:
@Override
public void onPauseWithNative() {}
}