1 // Copyright 2016 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.chrome.browser.payments.ui;
6 
7 import static org.chromium.chrome.browser.payments.ui.PaymentRequestSection.EDIT_BUTTON_GONE;
8 
9 import android.animation.Animator;
10 import android.animation.AnimatorListenerAdapter;
11 import android.animation.ObjectAnimator;
12 import android.animation.ValueAnimator;
13 import android.animation.ValueAnimator.AnimatorUpdateListener;
14 import android.app.Activity;
15 import android.app.Dialog;
16 import android.content.Context;
17 import android.graphics.Bitmap;
18 import android.os.Handler;
19 import android.text.SpannableString;
20 import android.text.TextUtils;
21 import android.text.method.LinkMovementMethod;
22 import android.view.LayoutInflater;
23 import android.view.View;
24 import android.view.View.OnLayoutChangeListener;
25 import android.view.ViewGroup;
26 import android.view.ViewGroup.LayoutParams;
27 import android.widget.Button;
28 import android.widget.FrameLayout;
29 import android.widget.LinearLayout;
30 import android.widget.TextView;
31 
32 import androidx.annotation.IntDef;
33 import androidx.annotation.Nullable;
34 import androidx.annotation.VisibleForTesting;
35 import androidx.core.view.ViewCompat;
36 
37 import org.chromium.base.ApiCompatibilityUtils;
38 import org.chromium.base.Callback;
39 import org.chromium.chrome.R;
40 import org.chromium.chrome.browser.autofill.prefeditor.EditorDialog;
41 import org.chromium.chrome.browser.autofill.prefeditor.EditorObserverForTest;
42 import org.chromium.chrome.browser.lifecycle.PauseResumeWithNativeObserver;
43 import org.chromium.chrome.browser.payments.ShippingStrings;
44 import org.chromium.chrome.browser.payments.ui.PaymentRequestSection.LineItemBreakdownSection;
45 import org.chromium.chrome.browser.payments.ui.PaymentRequestSection.OptionSection;
46 import org.chromium.chrome.browser.payments.ui.PaymentRequestSection.SectionSeparator;
47 import org.chromium.chrome.browser.payments.ui.PaymentUiService.PaymentUisShowStateReconciler;
48 import org.chromium.chrome.browser.profiles.Profile;
49 import org.chromium.chrome.browser.signin.IdentityServicesProvider;
50 import org.chromium.chrome.browser.version.ChromeVersionInfo;
51 import org.chromium.components.autofill.EditableOption;
52 import org.chromium.components.browser_ui.widget.FadingEdgeScrollView;
53 import org.chromium.components.browser_ui.widget.animation.FocusAnimator;
54 import org.chromium.components.browser_ui.widget.animation.Interpolators;
55 import org.chromium.components.payments.PaymentApp;
56 import org.chromium.components.payments.PaymentFeatureList;
57 import org.chromium.components.signin.base.CoreAccountInfo;
58 import org.chromium.components.signin.identitymanager.ConsentLevel;
59 import org.chromium.components.signin.identitymanager.IdentityManager;
60 import org.chromium.ui.text.NoUnderlineClickableSpan;
61 import org.chromium.ui.text.SpanApplier;
62 import org.chromium.ui.text.SpanApplier.SpanInfo;
63 import org.chromium.ui.widget.TextViewWithClickableSpans;
64 
65 import java.lang.annotation.Retention;
66 import java.lang.annotation.RetentionPolicy;
67 import java.util.ArrayList;
68 import java.util.List;
69 
70 /**
71  * The PaymentRequest UI.
72  */
73 public class PaymentRequestUI implements DimmingDialog.OnDismissListener, View.OnClickListener,
74                                          PaymentRequestSection.SectionDelegate,
75                                          PauseResumeWithNativeObserver {
76     @IntDef({DataType.SHIPPING_ADDRESSES, DataType.SHIPPING_OPTIONS, DataType.CONTACT_DETAILS,
77             DataType.PAYMENT_METHODS})
78     @Retention(RetentionPolicy.SOURCE)
79     public @interface DataType {
80         int SHIPPING_ADDRESSES = 1;
81         int SHIPPING_OPTIONS = 2;
82         int CONTACT_DETAILS = 3;
83         int PAYMENT_METHODS = 4;
84     }
85 
86     @IntDef({SelectionResult.ASYNCHRONOUS_VALIDATION, SelectionResult.EDITOR_LAUNCH,
87             SelectionResult.NONE})
88     @Retention(RetentionPolicy.SOURCE)
89     public @interface SelectionResult {
90         int ASYNCHRONOUS_VALIDATION = 1;
91         int EDITOR_LAUNCH = 2;
92         int NONE = 3;
93     }
94 
95     /**
96      * The interface to be implemented by the consumer of the PaymentRequest UI.
97      */
98     public interface Client {
99         /**
100          * Asynchronously returns the default payment information.
101          * @param waitForUpdatedDetails Whether the payment details is pending for updating.
102          * @param callback Retrieves the data to show in the initial PaymentRequest UI.
103          */
getDefaultPaymentInformation( boolean waitForUpdatedDetails, Callback<PaymentInformation> callback)104         void getDefaultPaymentInformation(
105                 boolean waitForUpdatedDetails, Callback<PaymentInformation> callback);
106 
107         /**
108          * Asynchronously returns the full bill. Includes the total price and its breakdown into
109          * individual line items.
110          */
getShoppingCart(Callback<ShoppingCart> callback)111         void getShoppingCart(Callback<ShoppingCart> callback);
112 
113         /**
114          * Asynchronously returns the full list of options for the given type.
115          *
116          * @param optionType Data being updated.
117          * @param callback   Callback to run when the data has been fetched.
118          */
getSectionInformation( @ataType int optionType, Callback<SectionInformation> callback)119         void getSectionInformation(
120                 @DataType int optionType, Callback<SectionInformation> callback);
121 
122         /**
123          * Called when the user changes one of their payment options.
124          *
125          * If this method returns {@link SelectionResult.ASYNCHRONOUS_VALIDATION}, then:
126          * + The added option should be asynchronously verified.
127          * + The section should be disabled and a progress spinny should be shown while the option
128          *   is being verified.
129          * + The checkedCallback will be invoked with the results of the check and updated
130          *   information.
131          *
132          * If this method returns {@link SelectionResult.EDITOR_LAUNCH}, then:
133          * + Interaction with UI should be disabled until updateSection() is called.
134          *
135          * For example, if the website needs a shipping address to calculate shipping options, then
136          * calling onSectionOptionSelected(DataType.SHIPPING_ADDRESS, option, checkedCallback) will
137          * return true. When the website updates the shipping options, the checkedCallback will be
138          * invoked.
139          *
140          * @param optionType        Data being updated.
141          * @param option            Value of the data being updated.
142          * @param checkedCallback   The callback after an asynchronous check has completed.
143          * @return The result of the selection.
144          */
145         @SelectionResult
onSectionOptionSelected(@ataType int optionType, EditableOption option, Callback<PaymentInformation> checkedCallback)146         int onSectionOptionSelected(@DataType int optionType, EditableOption option,
147                 Callback<PaymentInformation> checkedCallback);
148 
149         /**
150          * Called when the user clicks edit icon (pencil icon) on the payment option in a section.
151          *
152          * If this method returns {@link SelectionResult.ASYNCHRONOUS_VALIDATION}, then:
153          * + The edited option should be asynchronously verified.
154          * + The section should be disabled and a progress spinny should be shown while the option
155          *   is being verified.
156          * + The checkedCallback will be invoked with the results of the check and updated
157          *   information.
158          *
159          * If this method returns {@link SelectionResult.EDITOR_LAUNCH}, then:
160          * + Interaction with UI should be disabled until updateSection() is called.
161          *
162          * @param optionType      Data being updated.
163          * @param option          The option to be edited.
164          * @param checkedCallback The callback after an asynchronous check has completed.
165          * @return The result of the edit request.
166          */
167         @SelectionResult
onSectionEditOption(@ataType int optionType, EditableOption option, Callback<PaymentInformation> checkedCallback)168         int onSectionEditOption(@DataType int optionType, EditableOption option,
169                 Callback<PaymentInformation> checkedCallback);
170 
171         /**
172          * Called when the user clicks on the "Add" button for a section.
173          *
174          * If this method returns {@link SelectionResult.ASYNCHRONOUS_VALIDATION}, then:
175          * + The added option should be asynchronously verified.
176          * + The section should be disabled and a progress spinny should be shown while the option
177          *   is being verified.
178          * + The checkedCallback will be invoked with the results of the check and updated
179          *   information.
180          *
181          * If this method returns {@link SelectionResult.EDITOR_LAUNCH}, then:
182          * + Interaction with UI should be disabled until updateSection() is called.
183          *
184          * @param optionType      Data being updated.
185          * @param checkedCallback The callback after an asynchronous check has completed.
186          * @return The result of the selection.
187          */
onSectionAddOption( @ataType int optionType, Callback<PaymentInformation> checkedCallback)188         @SelectionResult int onSectionAddOption(
189                 @DataType int optionType, Callback<PaymentInformation> checkedCallback);
190 
191         /**
192          * Called when the user clicks on the “Pay” button. If this method returns true, the UI is
193          * disabled and is showing a spinner. Otherwise, the UI is hidden.
194          */
onPayClicked(EditableOption selectedShippingAddress, EditableOption selectedShippingOption, EditableOption selectedPaymentMethod)195         boolean onPayClicked(EditableOption selectedShippingAddress,
196                 EditableOption selectedShippingOption, EditableOption selectedPaymentMethod);
197 
198         /**
199          * Called when the user dismisses the UI via the “back” button on their phone
200          * or the “X” button in UI.
201          */
onDismiss()202         void onDismiss();
203 
204         /**
205          * Called when the user clicks on 'Settings' to control card and address options.
206          */
onCardAndAddressSettingsClicked()207         void onCardAndAddressSettingsClicked();
208 
209         /**
210          * Returns true when shipping address is requested and the selected payment method cannot
211          * provide it.
212          */
shouldShowShippingSection()213         boolean shouldShowShippingSection();
214 
215         /**
216          * Returns true when payer's contact details is requested and the selected payment method
217          * cannot provide it.
218          */
shouldShowContactSection()219         boolean shouldShowContactSection();
220     }
221 
222     /**
223      * A test-only observer for PaymentRequest UI.
224      */
225     public interface PaymentRequestObserverForTest {
226         /**
227          * Called when clicks on the UI are possible.
228          */
onPaymentRequestReadyForInput(PaymentRequestUI ui)229         void onPaymentRequestReadyForInput(PaymentRequestUI ui);
230 
231         /**
232          * Called when clicks on the PAY button are possible.
233          */
onPaymentRequestReadyToPay(PaymentRequestUI ui)234         void onPaymentRequestReadyToPay(PaymentRequestUI ui);
235 
236         /**
237          * Called when the UI has been updated to reflect checking a selected option.
238          */
onPaymentRequestSelectionChecked(PaymentRequestUI ui)239         void onPaymentRequestSelectionChecked(PaymentRequestUI ui);
240 
241         /**
242          * Called when the result UI is showing.
243          */
onPaymentRequestResultReady(PaymentRequestUI ui)244         void onPaymentRequestResultReady(PaymentRequestUI ui);
245     }
246 
247     /** Helper to notify tests of an event only once. */
248     private static class NotifierForTest {
249         private final Handler mHandler;
250         private final Runnable mNotification;
251         private boolean mNotificationPending;
252 
253         /**
254          * Constructs the helper to notify tests for an event.
255          *
256          * @param notification The callback that notifies the test of an event.
257          */
NotifierForTest(final Runnable notification)258         public NotifierForTest(final Runnable notification) {
259             mHandler = new Handler();
260             mNotification = new Runnable() {
261                 @Override
262                 public void run() {
263                     notification.run();
264                     mNotificationPending = false;
265                 }
266             };
267         }
268 
269         /** Schedules a single notification for test, even if called only once. */
run()270         public void run() {
271             if (mNotificationPending) return;
272             mNotificationPending = true;
273             mHandler.post(mNotification);
274         }
275     }
276 
277     /**
278      * Length of the animation to either show the UI or expand it to full height.
279      * Note that click of 'Pay' button is not accepted until the animation is done, so this duration
280      * also serves the function of preventing the user from accidently double-clicking on the screen
281      * when triggering payment and thus authorizing unwanted transaction.
282      */
283     private static final int DIALOG_ENTER_ANIMATION_MS = 225;
284 
285     private static PaymentRequestObserverForTest sPaymentRequestObserverForTest;
286     private static EditorObserverForTest sEditorObserverForTest;
287 
288     /** Notifies tests that the [PAY] button can be clicked. */
289     private final NotifierForTest mReadyToPayNotifierForTest;
290 
291     private final Context mContext;
292     private final Client mClient;
293     private final boolean mShowDataSource;
294     private final PaymentUisShowStateReconciler mPaymentUisShowStateReconciler;
295     private final Profile mProfile;
296 
297     /**
298      * The top level container of this UI. When needing to call show() or hide(), use {@link
299      * PaymentUisShowStateReconciler}'s showPaymentRequestDialogWhenNoBottomSheet() and
300      * hidePaymentRequestDialog() instead.
301      */
302     private final DimmingDialog mDialog;
303     private final EditorDialog mEditorDialog;
304     private final EditorDialog mCardEditorDialog;
305     private final ViewGroup mRequestView;
306     private final Callback<PaymentInformation> mUpdateSectionsCallback;
307     private final ShippingStrings mShippingStrings;
308 
309     private FadingEdgeScrollView mPaymentContainer;
310     private LinearLayout mPaymentContainerLayout;
311     private TextView mRetryErrorView;
312     private ViewGroup mBottomBar;
313     private Button mEditButton;
314     private Button mPayButton;
315     private View mCloseButton;
316     private View mSpinnyLayout;
317 
318     private LineItemBreakdownSection mOrderSummarySection;
319     private OptionSection mShippingAddressSection;
320     private OptionSection mShippingOptionSection;
321     private OptionSection mContactDetailsSection;
322     private OptionSection mPaymentMethodSection;
323     private List<SectionSeparator> mSectionSeparators;
324 
325     private PaymentRequestSection mSelectedSection;
326     private boolean mIsExpandedToFullHeight;
327     private boolean mIsProcessingPayClicked;
328     private boolean mIsClientClosing;
329     private boolean mIsClientCheckingSelection;
330     private boolean mIsShowingSpinner;
331     private boolean mIsEditingPaymentItem;
332     private boolean mIsClosing;
333 
334     private SectionInformation mPaymentMethodSectionInformation;
335     private SectionInformation mShippingAddressSectionInformation;
336     private SectionInformation mShippingOptionsSectionInformation;
337     private SectionInformation mContactDetailsSectionInformation;
338 
339     private Animator mSheetAnimator;
340     private FocusAnimator mSectionAnimator;
341     private int mAnimatorTranslation;
342 
343     /**
344      * Builds the UI for PaymentRequest.
345      *
346      * @param activity              The activity on top of which the UI should be displayed.
347      * @param client                The consumer of the PaymentRequest UI.
348      * @param canAddCards           Whether the UI should show the [+ADD CARD] button. This can be
349      *                              false, for example, when the merchant does not accept credit
350      *                              cards, so there's no point in adding cards within PaymentRequest
351      *                              UI.
352      * @param showDataSource        Whether the UI should describe the source of Autofill data.
353      * @param title                 The title to show at the top of the UI. This can be, for
354      *                              example, the &lt;title&gt; of the merchant website. If the
355      *                              string is too long for UI, it elides at the end.
356      * @param origin                The origin (https://tools.ietf.org/html/rfc6454) to show under
357      *                              the title. For example, "https://shop.momandpop.com". If the
358      *                              origin is too long for the UI, it should elide according to:
359      * https://www.chromium.org/Home/chromium-security/enamel#TOC-Eliding-Origin-Names-And-Hostnames
360      * @param securityLevel   The security level of the page that invoked PaymentRequest.
361      * @param shippingStrings The string resource identifiers to use in the shipping sections.
362      * @param profile         The current profile that creates the PaymentRequestUI.
363      */
PaymentRequestUI(Activity activity, Client client, boolean canAddCards, boolean showDataSource, String title, String origin, int securityLevel, ShippingStrings shippingStrings, PaymentUisShowStateReconciler paymentUisShowStateReconciler, Profile profile)364     public PaymentRequestUI(Activity activity, Client client, boolean canAddCards,
365             boolean showDataSource, String title, String origin, int securityLevel,
366             ShippingStrings shippingStrings,
367             PaymentUisShowStateReconciler paymentUisShowStateReconciler, Profile profile) {
368         mContext = activity;
369         mClient = client;
370         mShowDataSource = showDataSource;
371         mAnimatorTranslation = mContext.getResources().getDimensionPixelSize(
372                 R.dimen.payments_ui_translation);
373         mProfile = profile;
374 
375         mReadyToPayNotifierForTest = new NotifierForTest(new Runnable() {
376             @Override
377             public void run() {
378                 if (sPaymentRequestObserverForTest != null && isAcceptingUserInput()
379                         && mPayButton.isEnabled()) {
380                     sPaymentRequestObserverForTest.onPaymentRequestReadyToPay(
381                             PaymentRequestUI.this);
382                 }
383             }
384         });
385 
386         // This callback will be fired if mIsClientCheckingSelection is true.
387         mUpdateSectionsCallback = new Callback<PaymentInformation>() {
388             @Override
389             public void onResult(PaymentInformation result) {
390                 mIsClientCheckingSelection = false;
391                 updateOrderSummarySection(result.getShoppingCart());
392                 if (mClient.shouldShowShippingSection()) {
393                     updateSection(DataType.SHIPPING_ADDRESSES, result.getShippingAddresses());
394                     updateSection(DataType.SHIPPING_OPTIONS, result.getShippingOptions());
395                 }
396                 if (mClient.shouldShowContactSection()) {
397                     updateSection(DataType.CONTACT_DETAILS, result.getContactDetails());
398                 }
399                 updateSection(DataType.PAYMENT_METHODS, result.getPaymentMethods());
400                 if (mShippingAddressSectionInformation != null
401                         && mShippingAddressSectionInformation.getSelectedItem() == null) {
402                     expand(mShippingAddressSection);
403                 } else {
404                     expand(null);
405                 }
406                 updatePayButtonEnabled();
407                 notifySelectionChecked();
408             }
409         };
410 
411         mShippingStrings = shippingStrings;
412 
413         mRequestView =
414                 (ViewGroup) LayoutInflater.from(mContext).inflate(R.layout.payment_request, null);
415         prepareRequestView(mContext, title, origin, securityLevel, canAddCards, profile);
416 
417         mEditorDialog = new EditorDialog(activity, /*deleteRunnable =*/null, profile);
418         DimmingDialog.setVisibleStatusBarIconColor(mEditorDialog.getWindow());
419 
420         mCardEditorDialog = new EditorDialog(activity, /*deleteRunnable =*/null, profile);
421         DimmingDialog.setVisibleStatusBarIconColor(mCardEditorDialog.getWindow());
422 
423         // Allow screenshots of the credit card number in Canary, Dev, and developer builds.
424         if (ChromeVersionInfo.isBetaBuild() || ChromeVersionInfo.isStableBuild()) {
425             mCardEditorDialog.disableScreenshots();
426         }
427 
428         mDialog = new DimmingDialog(activity, this);
429         mPaymentUisShowStateReconciler = paymentUisShowStateReconciler;
430     }
431 
432     /**
433      * Shows the PaymentRequest UI. This will dim the background behind the PaymentRequest UI.
434      * @param waitForUpdatedDetails Whether the payment details is pending to be updated.
435      */
show(boolean waitForUpdatedDetails)436     public void show(boolean waitForUpdatedDetails) {
437         mDialog.addBottomSheetView(mRequestView);
438         mPaymentUisShowStateReconciler.showPaymentRequestDialogWhenNoBottomSheet();
439         mClient.getDefaultPaymentInformation(
440                 waitForUpdatedDetails, new Callback<PaymentInformation>() {
441                     @Override
442                     public void onResult(PaymentInformation result) {
443                         updateOrderSummarySection(result.getShoppingCart());
444 
445                         if (mClient.shouldShowShippingSection()) {
446                             updateSection(
447                                     DataType.SHIPPING_ADDRESSES, result.getShippingAddresses());
448                             updateSection(DataType.SHIPPING_OPTIONS, result.getShippingOptions());
449                         }
450 
451                         if (mClient.shouldShowContactSection()) {
452                             updateSection(DataType.CONTACT_DETAILS, result.getContactDetails());
453                         }
454 
455                         mPaymentMethodSection.setDisplaySummaryInSingleLineInNormalMode(
456                                 result.getPaymentMethods()
457                                         .getDisplaySelectedItemSummaryInSingleLineInNormalMode());
458                         updateSection(DataType.PAYMENT_METHODS, result.getPaymentMethods());
459                         updatePayButtonEnabled();
460 
461                         // Hide the loading indicators and show the real sections.
462                         changeSpinnerVisibility(false);
463                         mRequestView.addOnLayoutChangeListener(new SheetEnlargingAnimator(false));
464                     }
465                 });
466     }
467 
468     /**
469      * Dim the background without showing any UI. No UI will be interactive. The dimming stops when
470      * close() is called. This is useful for the skip-ui scenario, i.e., launching a payment handler
471      * directly without showing a PaymentRequest UI first in cases where only one payment handler is
472      * available.
473      */
dimBackground()474     public void dimBackground() {
475         // Intentionally do not add the bottom sheet view to mDialog so that only the scrim part of
476         // the dialog will be shown.
477         mPaymentUisShowStateReconciler.showPaymentRequestDialogWhenNoBottomSheet();
478     }
479 
480     /**
481      * Prepares the PaymentRequestUI for initial display.
482      *
483      * TODO(dfalcantara): Ideally, everything related to the request and its views would just be put
484      *                    into its own class but that'll require yanking out a lot of this class.
485      *
486      * @param context       The application context.
487      * @param title         Title of the page.
488      * @param origin        The RFC6454 origin of the page.
489      * @param securityLevel The security level of the page that invoked PaymentRequest.
490      * @param canAddCards   Whether new cards can be added.
491      * @param profile       The current profile to pass PaymentRequestHeader.
492      */
prepareRequestView(Context context, String title, String origin, int securityLevel, boolean canAddCards, Profile profile)493     private void prepareRequestView(Context context, String title, String origin, int securityLevel,
494             boolean canAddCards, Profile profile) {
495         mSpinnyLayout = mRequestView.findViewById(R.id.payment_request_spinny);
496         assert mSpinnyLayout.getVisibility() == View.VISIBLE;
497         mIsShowingSpinner = true;
498 
499         // Indicate that we're preparing the dialog for display.
500         TextView messageView = (TextView) mRequestView.findViewById(R.id.message);
501         messageView.setText(R.string.payments_loading_message);
502 
503         ((PaymentRequestHeader) mRequestView.findViewById(R.id.header))
504                 .setTitleAndOrigin(title, origin, securityLevel, profile);
505 
506         // Set up the buttons.
507         mCloseButton = mRequestView.findViewById(R.id.close_button);
508         mCloseButton.setOnClickListener(this);
509         mBottomBar = (ViewGroup) mRequestView.findViewById(R.id.bottom_bar);
510         mPayButton = (Button) mBottomBar.findViewById(R.id.button_primary);
511         mPayButton.setOnClickListener(this);
512         mEditButton = (Button) mBottomBar.findViewById(R.id.button_secondary);
513         mEditButton.setOnClickListener(this);
514 
515         // Create all the possible sections.
516         mSectionSeparators = new ArrayList<>();
517         mPaymentContainer = (FadingEdgeScrollView) mRequestView.findViewById(R.id.option_container);
518         mPaymentContainerLayout =
519                 (LinearLayout) mRequestView.findViewById(R.id.payment_container_layout);
520         mRetryErrorView = mRequestView.findViewById(R.id.retry_error);
521         mOrderSummarySection = new LineItemBreakdownSection(context,
522                 context.getString(R.string.payments_order_summary_label), this,
523                 context.getString(R.string.payments_updated_label));
524         mShippingAddressSection = new OptionSection(
525                 context, context.getString(mShippingStrings.getAddressLabel()), this);
526         mShippingOptionSection = new OptionSection(
527                 context, context.getString(mShippingStrings.getOptionLabel()), this);
528         mContactDetailsSection = new OptionSection(
529                 context, context.getString(R.string.payments_contact_details_label), this);
530         mPaymentMethodSection = new OptionSection(
531                 context, context.getString(R.string.payments_method_of_payment_label), this);
532 
533         // Display the summary of the selected address in multiple lines on bottom sheet.
534         mShippingAddressSection.setDisplaySummaryInSingleLineInNormalMode(false);
535 
536         // Display selected shipping option name in the left summary text view and
537         // the cost in the right summary text view on bottom sheet.
538         mShippingOptionSection.setSplitSummaryInDisplayModeNormal(true);
539 
540         // Some sections conditionally allow adding new options.
541         mShippingOptionSection.setCanAddItems(false);
542         mPaymentMethodSection.setCanAddItems(canAddCards);
543 
544         // Put payment method section on top of address section for
545         // WEB_PAYMENTS_METHOD_SECTION_ORDER_V2.
546         boolean methodSectionOrderV2 = PaymentFeatureList.isEnabled(
547                 PaymentFeatureList.WEB_PAYMENTS_METHOD_SECTION_ORDER_V2);
548 
549         // Add the necessary sections to the layout.
550         mPaymentContainerLayout.addView(mOrderSummarySection, new LinearLayout.LayoutParams(
551                 LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
552         if (methodSectionOrderV2) {
553             mSectionSeparators.add(new SectionSeparator(mPaymentContainerLayout));
554             mPaymentContainerLayout.addView(mPaymentMethodSection,
555                     new LinearLayout.LayoutParams(
556                             LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
557         }
558 
559         SectionSeparator shippingSectionSeparator = new SectionSeparator(mPaymentContainerLayout);
560         mSectionSeparators.add(shippingSectionSeparator);
561         mPaymentContainerLayout.addView(mShippingAddressSection,
562                 new LinearLayout.LayoutParams(
563                         LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
564 
565         // The shipping breakout sections are visible only if they are needed.
566         if (!mClient.shouldShowShippingSection()) {
567             mShippingAddressSection.setVisibility(View.GONE);
568             shippingSectionSeparator.setVisibility(View.GONE);
569         }
570 
571         if (!methodSectionOrderV2) {
572             mSectionSeparators.add(new SectionSeparator(mPaymentContainerLayout));
573             mPaymentContainerLayout.addView(mPaymentMethodSection,
574                     new LinearLayout.LayoutParams(
575                             LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
576         }
577 
578         SectionSeparator contactSectionSeparator = new SectionSeparator(mPaymentContainerLayout);
579         mSectionSeparators.add(contactSectionSeparator);
580         mPaymentContainerLayout.addView(mContactDetailsSection,
581                 new LinearLayout.LayoutParams(
582                         LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
583 
584         // Contact details are optional, depending on the merchant website, and whether or not the
585         // selected payment app can provide them.
586         if (!mClient.shouldShowContactSection()) {
587             mContactDetailsSection.setVisibility(View.GONE);
588             contactSectionSeparator.setVisibility(View.GONE);
589         }
590 
591         mRequestView.addOnLayoutChangeListener(new PeekingAnimator());
592 
593         // Enabled in updatePayButtonEnabled() when the user has selected all payment options.
594         mPayButton.setEnabled(false);
595     }
596 
597     /**
598      * Closes the UI. Can be invoked in response to, for example:
599      * <ul>
600      *  <li>Successfully processing the payment.</li>
601      *  <li>Failure to process the payment.</li>
602      *  <li>The JavaScript calling the abort() method in PaymentRequest API.</li>
603      *  <li>The PaymentRequest JavaScript object being destroyed.</li>
604      * </ul>
605      *
606      * Does not call Client.onDismissed().
607      *
608      * Should not be called multiple times.
609      */
close()610     public void close() {
611         mIsClientClosing = true;
612 
613         dismissDialog(false);
614 
615         if (sPaymentRequestObserverForTest != null) {
616             sPaymentRequestObserverForTest.onPaymentRequestResultReady(this);
617         }
618     }
619 
620     /**
621      * Disables adding new cards during retry.
622      */
disableAddingNewCardsDuringRetry()623     public void disableAddingNewCardsDuringRetry() {
624         assert mPaymentMethodSection != null;
625         mPaymentMethodSection.setCanAddItems(false);
626         mPaymentMethodSection.update(mPaymentMethodSectionInformation);
627     }
628 
629     /**
630      * Sets the icon in the top left of the UI. This can be, for example, the favicon of the
631      * merchant website. This is not a part of the constructor because favicon retrieval is
632      * asynchronous.
633      *
634      * @param bitmap The bitmap to show next to the title.
635      */
setTitleBitmap(Bitmap bitmap)636     public void setTitleBitmap(Bitmap bitmap) {
637         ((PaymentRequestHeader) mRequestView.findViewById(R.id.header)).setTitleBitmap(bitmap);
638     }
639 
640     /**
641      * Sets the retry error message. This is used to display error message on the header UI when
642      * retry() is called on merchant side. The error message may be reset when users click 'Pay'
643      * button or expand any section.
644      *
645      * @param error The error message to display on the header.
646      */
setRetryErrorMessage(String error)647     public void setRetryErrorMessage(String error) {
648         if (mRetryErrorView == null) return;
649 
650         mRetryErrorView.setText(error);
651         if (TextUtils.isEmpty(error)) {
652             mRetryErrorView.setVisibility(View.GONE);
653         } else {
654             if (mIsExpandedToFullHeight) {
655                 // Add paddings instead of margin to let getMeasuredHeight return correct value for
656                 // section resize animation.
657                 int paddingSize = mContext.getResources().getDimensionPixelSize(
658                         R.dimen.editor_dialog_section_large_spacing);
659                 ViewCompat.setPaddingRelative(mRetryErrorView, 0, paddingSize, 0, paddingSize);
660             } else {
661                 ViewCompat.setPaddingRelative(mRetryErrorView, 0, 0, 0, 0);
662             }
663             mRetryErrorView.setVisibility(View.VISIBLE);
664         }
665     }
666 
667     /**
668      * Updates the line items in response to a changed shipping address or option.
669      *
670      * @param cart The shopping cart, including the line items and the total.
671      */
updateOrderSummarySection(ShoppingCart cart)672     public void updateOrderSummarySection(ShoppingCart cart) {
673         if (cart == null || cart.getTotal() == null) {
674             mOrderSummarySection.setVisibility(View.GONE);
675         } else {
676             mOrderSummarySection.setVisibility(View.VISIBLE);
677             mOrderSummarySection.update(cart);
678         }
679     }
680 
681     /**
682      * Updates the UI to account for changes in different sections information.
683      *
684      * @param whichSection The type of the updated section.
685      * @param section The updated section information.
686      */
updateSection(@ataType int whichSection, SectionInformation section)687     public void updateSection(@DataType int whichSection, SectionInformation section) {
688         if (whichSection == DataType.SHIPPING_ADDRESSES) {
689             mShippingAddressSectionInformation = section;
690             mShippingAddressSection.update(section);
691         } else if (whichSection == DataType.SHIPPING_OPTIONS) {
692             mShippingOptionsSectionInformation = section;
693             mShippingOptionSection.update(section);
694             addShippingOptionSectionIfNecessary();
695         } else if (whichSection == DataType.CONTACT_DETAILS) {
696             mContactDetailsSectionInformation = section;
697             mContactDetailsSection.update(section);
698         } else if (whichSection == DataType.PAYMENT_METHODS) {
699             mPaymentMethodSectionInformation = section;
700             mPaymentMethodSection.update(section);
701         }
702 
703         boolean isFinishingEditItem = mIsEditingPaymentItem;
704         mIsEditingPaymentItem = false;
705         updateSectionButtons();
706         updatePayButtonEnabled();
707 
708         // Notify ready for input for test if this is finishing editing item.
709         if (isFinishingEditItem) notifyReadyForInput();
710     }
711 
712     // Only add shipping option section once there are shipping options.
addShippingOptionSectionIfNecessary()713     private void addShippingOptionSectionIfNecessary() {
714         if (!mClient.shouldShowShippingSection() || mShippingOptionsSectionInformation.isEmpty()
715                 || mPaymentContainerLayout.indexOfChild(mShippingOptionSection) != -1) {
716             return;
717         }
718 
719         // Shipping option section is added below shipping address section.
720         int addressSectionIndex = mPaymentContainerLayout.indexOfChild(mShippingAddressSection);
721         SectionSeparator sectionSeparator =
722                 new SectionSeparator(mPaymentContainerLayout, addressSectionIndex + 1);
723         mSectionSeparators.add(sectionSeparator);
724         if (mIsExpandedToFullHeight) sectionSeparator.expand();
725         mPaymentContainerLayout.addView(mShippingOptionSection, addressSectionIndex + 2,
726                 new LinearLayout.LayoutParams(
727                         LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
728         mPaymentContainerLayout.requestLayout();
729     }
730 
731     /**
732      * Notifies the UI about the changes in selected payment method.
733      *
734      * @param paymentInformation The updated payment information.
735      */
selectedPaymentMethodUpdated(PaymentInformation paymentInformation)736     public void selectedPaymentMethodUpdated(PaymentInformation paymentInformation) {
737         if (mClient.shouldShowShippingSection()
738                 && mShippingAddressSection.getVisibility() == View.GONE) {
739             updateSection(DataType.SHIPPING_ADDRESSES, paymentInformation.getShippingAddresses());
740             updateSection(DataType.SHIPPING_OPTIONS, paymentInformation.getShippingOptions());
741 
742             // Show shipping address section and its separator.
743             mShippingAddressSection.setVisibility(View.VISIBLE);
744             int addressSectionIndex = mPaymentContainerLayout.indexOfChild(mShippingAddressSection);
745             mPaymentContainerLayout.getChildAt(addressSectionIndex - 1).setVisibility(View.VISIBLE);
746 
747             // Show shipping option section (if it exists) and its separator.
748             int shippingOptionSectionIndex =
749                     mPaymentContainerLayout.indexOfChild(mShippingOptionSection);
750             if (shippingOptionSectionIndex != -1) {
751                 mShippingOptionSection.setVisibility(View.VISIBLE);
752                 mPaymentContainerLayout.getChildAt(shippingOptionSectionIndex - 1)
753                         .setVisibility(View.VISIBLE);
754             }
755 
756         } else if (!mClient.shouldShowShippingSection()
757                 && mShippingAddressSection.getVisibility() == View.VISIBLE) {
758             // Hide shipping address section and its separator.
759             mShippingAddressSection.setVisibility(View.GONE);
760             int addressSectionIndex = mPaymentContainerLayout.indexOfChild(mShippingAddressSection);
761             mPaymentContainerLayout.getChildAt(addressSectionIndex - 1).setVisibility(View.GONE);
762 
763             // Hide shipping option section (if exists) and its separator.
764             int shippingOptionSectionIndex =
765                     mPaymentContainerLayout.indexOfChild(mShippingOptionSection);
766             if (shippingOptionSectionIndex != -1) {
767                 mShippingOptionSection.setVisibility(View.GONE);
768                 mPaymentContainerLayout.getChildAt(shippingOptionSectionIndex - 1)
769                         .setVisibility(View.GONE);
770             }
771         }
772         if (mClient.shouldShowContactSection()
773                 && mContactDetailsSection.getVisibility() == View.GONE) {
774             updateSection(DataType.CONTACT_DETAILS, paymentInformation.getContactDetails());
775 
776             // Show contact details section and its separator.
777             mContactDetailsSection.setVisibility(View.VISIBLE);
778             int contactSectionIndex = mPaymentContainerLayout.indexOfChild(mContactDetailsSection);
779             mPaymentContainerLayout.getChildAt(contactSectionIndex - 1).setVisibility(View.VISIBLE);
780         } else if (!mClient.shouldShowContactSection()
781                 && mContactDetailsSection.getVisibility() == View.VISIBLE) {
782             // Hide contact details section and its separator.
783             mContactDetailsSection.setVisibility(View.GONE);
784             int contactSectionIndex = mPaymentContainerLayout.indexOfChild(mContactDetailsSection);
785             mPaymentContainerLayout.getChildAt(contactSectionIndex - 1).setVisibility(View.GONE);
786         }
787 
788         mPaymentContainerLayout.requestLayout();
789     }
790 
791     @Override
onEditableOptionChanged( final PaymentRequestSection section, EditableOption option)792     public void onEditableOptionChanged(
793             final PaymentRequestSection section, EditableOption option) {
794         @SelectionResult
795         int result = SelectionResult.NONE;
796         if (section == mShippingAddressSection
797                 && mShippingAddressSectionInformation.getSelectedItem() != option) {
798             mShippingAddressSectionInformation.setSelectedItem(option);
799             result = mClient.onSectionOptionSelected(
800                     DataType.SHIPPING_ADDRESSES, option, mUpdateSectionsCallback);
801         } else if (section == mShippingOptionSection
802                 && mShippingOptionsSectionInformation.getSelectedItem() != option) {
803             mShippingOptionsSectionInformation.setSelectedItem(option);
804             result = mClient.onSectionOptionSelected(
805                     DataType.SHIPPING_OPTIONS, option, mUpdateSectionsCallback);
806         } else if (section == mContactDetailsSection) {
807             mContactDetailsSectionInformation.setSelectedItem(option);
808             result = mClient.onSectionOptionSelected(
809                     DataType.CONTACT_DETAILS, option, mUpdateSectionsCallback);
810         } else if (section == mPaymentMethodSection) {
811             mPaymentMethodSectionInformation.setSelectedItem(option);
812             result = mClient.onSectionOptionSelected(DataType.PAYMENT_METHODS, option, null);
813         }
814 
815         updateStateFromResult(section, result);
816     }
817 
818     @Override
onEditEditableOption(final PaymentRequestSection section, EditableOption option)819     public void onEditEditableOption(final PaymentRequestSection section, EditableOption option) {
820         @SelectionResult
821         int result = SelectionResult.NONE;
822 
823         assert section != mOrderSummarySection;
824         assert section != mShippingOptionSection;
825         if (section == mShippingAddressSection) {
826             assert mShippingAddressSectionInformation.getSelectedItem() == option;
827             result = mClient.onSectionEditOption(
828                     DataType.SHIPPING_ADDRESSES, option, mUpdateSectionsCallback);
829         }
830 
831         if (section == mContactDetailsSection) {
832             assert mContactDetailsSectionInformation.getSelectedItem() == option;
833             result = mClient.onSectionEditOption(DataType.CONTACT_DETAILS, option, null);
834         }
835 
836         if (section == mPaymentMethodSection) {
837             assert mPaymentMethodSectionInformation.getSelectedItem() == option;
838             result = mClient.onSectionEditOption(DataType.PAYMENT_METHODS, option, null);
839         }
840 
841         updateStateFromResult(section, result);
842     }
843 
844     @Override
onAddEditableOption(PaymentRequestSection section)845     public void onAddEditableOption(PaymentRequestSection section) {
846         assert section != mShippingOptionSection;
847 
848         @SelectionResult
849         int result = SelectionResult.NONE;
850         if (section == mShippingAddressSection) {
851             result = mClient.onSectionAddOption(
852                     DataType.SHIPPING_ADDRESSES, mUpdateSectionsCallback);
853         } else if (section == mContactDetailsSection) {
854             result = mClient.onSectionAddOption(DataType.CONTACT_DETAILS, null);
855         } else if (section == mPaymentMethodSection) {
856             result = mClient.onSectionAddOption(DataType.PAYMENT_METHODS, null);
857         }
858 
859         updateStateFromResult(section, result);
860     }
861 
updateStateFromResult(PaymentRequestSection section, @SelectionResult int result)862     void updateStateFromResult(PaymentRequestSection section, @SelectionResult int result) {
863         mIsClientCheckingSelection = result == SelectionResult.ASYNCHRONOUS_VALIDATION;
864         mIsEditingPaymentItem = result == SelectionResult.EDITOR_LAUNCH;
865 
866         if (mIsClientCheckingSelection) {
867             mSelectedSection = section;
868             updateSectionVisibility();
869             section.setDisplayMode(PaymentRequestSection.DISPLAY_MODE_CHECKING);
870         } else {
871             expand(null);
872         }
873 
874         updatePayButtonEnabled();
875     }
876 
877     @Override
isBoldLabelNeeded(PaymentRequestSection section)878     public boolean isBoldLabelNeeded(PaymentRequestSection section) {
879         return section == mShippingAddressSection;
880     }
881 
882     /** @return The common editor user interface. */
getEditorDialog()883     public EditorDialog getEditorDialog() {
884         return mEditorDialog;
885     }
886 
887     /** @return The card editor user interface. Distinct from the common editor user interface,
888      * because the credit card editor can launch the address editor. */
getCardEditorDialog()889     public EditorDialog getCardEditorDialog() {
890         return mCardEditorDialog;
891     }
892 
893     /**
894      * Called when user clicks anything in the dialog.
895      */
896     // View.OnClickListener implementation.
897     @Override
onClick(View v)898     public void onClick(View v) {
899         if (!isAcceptingCloseButton()) return;
900 
901         if (v == mCloseButton) {
902             dismissDialog(true);
903             return;
904         }
905 
906         if (!isAcceptingUserInput()) return;
907 
908         // Users can only expand incomplete sections by clicking on their edit buttons.
909         if (v instanceof PaymentRequestSection) {
910             PaymentRequestSection section = (PaymentRequestSection) v;
911             if (section.getEditButtonState() != EDIT_BUTTON_GONE) return;
912         }
913 
914         if (v == mOrderSummarySection) {
915             expand(mOrderSummarySection);
916         } else if (v == mShippingAddressSection) {
917             expand(mShippingAddressSection);
918         } else if (v == mShippingOptionSection) {
919             expand(mShippingOptionSection);
920         } else if (v == mContactDetailsSection) {
921             expand(mContactDetailsSection);
922         } else if (v == mPaymentMethodSection) {
923             expand(mPaymentMethodSection);
924         } else if (v == mPayButton) {
925             processPayButton();
926         } else if (v == mEditButton) {
927             if (mIsExpandedToFullHeight) {
928                 dismissDialog(true);
929             } else {
930                 expand(mOrderSummarySection);
931             }
932         }
933 
934         setRetryErrorMessage(null);
935 
936         updatePayButtonEnabled();
937     }
938 
939     /**
940      * Dismiss the dialog.
941      *
942      * @param isAnimated If true, the dialog dismissal is animated.
943      */
dismissDialog(boolean isAnimated)944     private void dismissDialog(boolean isAnimated) {
945         mIsClosing = true;
946         mDialog.dismiss(isAnimated);
947     }
948 
processPayButton()949     private void processPayButton() {
950         assert !mIsShowingSpinner;
951         mIsProcessingPayClicked = true;
952 
953         boolean shouldShowSpinner = mClient.onPayClicked(
954                 mShippingAddressSectionInformation == null
955                         ? null : mShippingAddressSectionInformation.getSelectedItem(),
956                 mShippingOptionsSectionInformation == null
957                         ? null : mShippingOptionsSectionInformation.getSelectedItem(),
958                 mPaymentMethodSectionInformation.getSelectedItem());
959 
960         if (shouldShowSpinner) {
961             changeSpinnerVisibility(true);
962         } else {
963             mPaymentUisShowStateReconciler.hidePaymentRequestDialog();
964         }
965     }
966 
967     /**
968      * Called when user cancelled out of the UI that was shown after they clicked [PAY] button.
969      */
onPayButtonProcessingCancelled()970     public void onPayButtonProcessingCancelled() {
971         assert mIsProcessingPayClicked;
972         mIsProcessingPayClicked = false;
973         changeSpinnerVisibility(false);
974         mPaymentUisShowStateReconciler.showPaymentRequestDialogWhenNoBottomSheet();
975         updatePayButtonEnabled();
976     }
977 
978     /**
979      *  Called to show the processing message after payment details have been loaded in the case the
980      *  payment request UI has been skipped.
981      */
showProcessingMessageAfterUiSkip()982     public void showProcessingMessageAfterUiSkip() {
983         // Button was clicked before but not marked as clicked because we skipped the UI.
984         mIsProcessingPayClicked = true;
985         showProcessingMessage();
986     }
987 
988     /**
989      * Called when the user has clicked on pay. The message is shown while the payment information
990      * is processed right until a confimation from the merchant is received.
991      */
showProcessingMessage()992     public void showProcessingMessage() {
993         assert mIsProcessingPayClicked;
994 
995         changeSpinnerVisibility(true);
996         mPaymentUisShowStateReconciler.showPaymentRequestDialogWhenNoBottomSheet();
997     }
998 
changeSpinnerVisibility(boolean showSpinner)999     private void changeSpinnerVisibility(boolean showSpinner) {
1000         if (mIsShowingSpinner == showSpinner) return;
1001         mIsShowingSpinner = showSpinner;
1002 
1003         if (showSpinner) {
1004             mPaymentContainer.setVisibility(View.GONE);
1005             mBottomBar.setVisibility(View.GONE);
1006             mCloseButton.setVisibility(View.GONE);
1007             mSpinnyLayout.setVisibility(View.VISIBLE);
1008 
1009             // Turn the bottom sheet back into a collapsed bottom sheet showing only the spinner.
1010             // TODO(dfalcantara): Animate this: https://crbug.com/621955
1011             ((FrameLayout.LayoutParams) mRequestView.getLayoutParams()).height =
1012                     LayoutParams.WRAP_CONTENT;
1013             mRequestView.requestLayout();
1014         } else {
1015             mPaymentContainer.setVisibility(View.VISIBLE);
1016             mBottomBar.setVisibility(View.VISIBLE);
1017             mCloseButton.setVisibility(View.VISIBLE);
1018             mSpinnyLayout.setVisibility(View.GONE);
1019 
1020             if (mIsExpandedToFullHeight) {
1021                 ((FrameLayout.LayoutParams) mRequestView.getLayoutParams()).height =
1022                         LayoutParams.MATCH_PARENT;
1023                 mRequestView.requestLayout();
1024             }
1025         }
1026     }
1027 
updatePayButtonEnabled()1028     private void updatePayButtonEnabled() {
1029         boolean contactInfoOk = !mClient.shouldShowContactSection()
1030                 || (mContactDetailsSectionInformation != null
1031                         && mContactDetailsSectionInformation.getSelectedItem() != null);
1032         boolean shippingInfoOk = !mClient.shouldShowShippingSection()
1033                 || (mShippingAddressSectionInformation != null
1034                         && mShippingAddressSectionInformation.getSelectedItem() != null);
1035         boolean shippingOptionInfoOk = !mClient.shouldShowShippingSection()
1036                 || (mShippingOptionsSectionInformation != null
1037                         && mShippingOptionsSectionInformation.getSelectedItem() != null);
1038         mPayButton.setEnabled(contactInfoOk && shippingInfoOk && shippingOptionInfoOk
1039                 && mPaymentMethodSectionInformation != null
1040                 && mPaymentMethodSectionInformation.getSelectedItem() != null
1041                 && !mIsClientCheckingSelection && !mIsEditingPaymentItem && !mIsClosing);
1042 
1043         PaymentApp selectedApp = mPaymentMethodSectionInformation == null
1044                 ? null
1045                 : (PaymentApp) mPaymentMethodSectionInformation.getSelectedItem();
1046         mPayButton.setText(selectedApp != null && !selectedApp.isAutofillInstrument()
1047                         ? R.string.payments_continue_button
1048                         : R.string.payments_pay_button);
1049         mReadyToPayNotifierForTest.run();
1050     }
1051 
1052     /** @return Whether or not the dialog can be closed via the X close button. */
isAcceptingCloseButton()1053     private boolean isAcceptingCloseButton() {
1054         return !mDialog.isAnimatingDisappearance() && mSheetAnimator == null
1055                 && mSectionAnimator == null && !mIsProcessingPayClicked && !mIsEditingPaymentItem
1056                 && !mIsClosing;
1057     }
1058 
1059     /** @return Whether or not the dialog is accepting user input. */
1060     @Override
isAcceptingUserInput()1061     public boolean isAcceptingUserInput() {
1062         return isAcceptingCloseButton() && mPaymentMethodSectionInformation != null
1063                 && !mIsClientCheckingSelection;
1064     }
1065 
1066     /**
1067      * Sets the observer to be called when the shipping address section gains or loses focus.
1068      *
1069      * @param observer The observer to notify.
1070      */
setShippingAddressSectionFocusChangedObserver( OptionSection.FocusChangedObserver observer)1071     public void setShippingAddressSectionFocusChangedObserver(
1072             OptionSection.FocusChangedObserver observer) {
1073         mShippingAddressSection.setOptionSectionFocusChangedObserver(observer);
1074     }
1075 
expand(PaymentRequestSection section)1076     private void expand(PaymentRequestSection section) {
1077         if (!mIsExpandedToFullHeight) {
1078             // Container now takes the full height of the screen, animating towards it.
1079             mRequestView.getLayoutParams().height = LayoutParams.MATCH_PARENT;
1080             mRequestView.addOnLayoutChangeListener(new SheetEnlargingAnimator(true));
1081 
1082             // New separators appear at the top and bottom of the list.
1083             mPaymentContainer.setEdgeVisibility(
1084                     FadingEdgeScrollView.EdgeType.HARD, FadingEdgeScrollView.EdgeType.FADING);
1085             mSectionSeparators.add(new SectionSeparator(mPaymentContainerLayout, -1));
1086 
1087             // Add a link to Autofill settings.
1088             addCardAndAddressOptionsSettingsView(mPaymentContainerLayout);
1089 
1090             // Expand all the dividers.
1091             for (int i = 0; i < mSectionSeparators.size(); i++) mSectionSeparators.get(i).expand();
1092             mPaymentContainerLayout.requestLayout();
1093 
1094             // Switch the 'edit' button to a 'cancel' button.
1095             mEditButton.setText(mContext.getString(R.string.cancel));
1096 
1097             // Disable all but the first button.
1098             updateSectionButtons();
1099 
1100             mIsExpandedToFullHeight = true;
1101         }
1102 
1103         // Update the section contents when they're selected.
1104         mSelectedSection = section;
1105         if (mSelectedSection == mOrderSummarySection) {
1106             mClient.getShoppingCart(new Callback<ShoppingCart>() {
1107                 @Override
1108                 public void onResult(ShoppingCart result) {
1109                     updateOrderSummarySection(result);
1110                     updateSectionVisibility();
1111                 }
1112             });
1113         } else if (mSelectedSection == mShippingAddressSection) {
1114             mClient.getSectionInformation(DataType.SHIPPING_ADDRESSES,
1115                     createUpdateSectionCallback(DataType.SHIPPING_ADDRESSES));
1116         } else if (mSelectedSection == mShippingOptionSection) {
1117             mClient.getSectionInformation(DataType.SHIPPING_OPTIONS,
1118                     createUpdateSectionCallback(DataType.SHIPPING_OPTIONS));
1119         } else if (mSelectedSection == mContactDetailsSection) {
1120             mClient.getSectionInformation(DataType.CONTACT_DETAILS,
1121                     createUpdateSectionCallback(DataType.CONTACT_DETAILS));
1122         } else if (mSelectedSection == mPaymentMethodSection) {
1123             mClient.getSectionInformation(DataType.PAYMENT_METHODS,
1124                     createUpdateSectionCallback(DataType.PAYMENT_METHODS));
1125         } else {
1126             updateSectionVisibility();
1127         }
1128     }
1129 
addCardAndAddressOptionsSettingsView(LinearLayout parent)1130     private void addCardAndAddressOptionsSettingsView(LinearLayout parent) {
1131         String message;
1132         if (!mShowDataSource) {
1133             message = mContext.getString(R.string.payments_card_and_address_settings);
1134         } else {
1135             String email = getEmail();
1136             if (email != null) {
1137                 message = mContext.getString(
1138                         R.string.payments_card_and_address_settings_signed_in, email);
1139             } else {
1140                 message =
1141                         mContext.getString(R.string.payments_card_and_address_settings_signed_out);
1142             }
1143         }
1144 
1145         NoUnderlineClickableSpan settingsSpan = new NoUnderlineClickableSpan(
1146                 mContext.getResources(), (widget) -> mClient.onCardAndAddressSettingsClicked());
1147         SpannableString spannableMessage = SpanApplier.applySpans(
1148                 message, new SpanInfo("BEGIN_LINK", "END_LINK", settingsSpan));
1149 
1150         TextView view = new TextViewWithClickableSpans(mContext);
1151         view.setText(spannableMessage);
1152         view.setMovementMethod(LinkMovementMethod.getInstance());
1153         ApiCompatibilityUtils.setTextAppearance(view, R.style.TextAppearance_TextMedium_Secondary);
1154 
1155         // Add paddings instead of margin to let getMeasuredHeight return correct value for section
1156         // resize animation.
1157         int paddingSize = mContext.getResources().getDimensionPixelSize(
1158                 R.dimen.editor_dialog_section_large_spacing);
1159         ViewCompat.setPaddingRelative(view, paddingSize, paddingSize, paddingSize, paddingSize);
1160         parent.addView(view);
1161     }
1162 
1163     /** @return The email of signed in user or null. */
1164     @Nullable
getEmail()1165     private String getEmail() {
1166         IdentityManager identityManager =
1167                 IdentityServicesProvider.get().getIdentityManager(mProfile);
1168         if (identityManager == null) return null;
1169         CoreAccountInfo info = identityManager.getPrimaryAccountInfo(ConsentLevel.SYNC);
1170         if (info == null) return null;
1171         return info.getEmail();
1172     }
1173 
createUpdateSectionCallback(@ataType final int type)1174     private Callback<SectionInformation> createUpdateSectionCallback(@DataType final int type) {
1175         return new Callback<SectionInformation>() {
1176             @Override
1177             public void onResult(SectionInformation result) {
1178                 updateSection(type, result);
1179                 updateSectionVisibility();
1180             }
1181         };
1182     }
1183 
1184     /** Update the display status of each expandable section in the full dialog. */
1185     private void updateSectionVisibility() {
1186         startSectionResizeAnimation();
1187         mOrderSummarySection.focusSection(mSelectedSection == mOrderSummarySection);
1188         if (mClient.shouldShowShippingSection()) {
1189             mShippingAddressSection.focusSection(mSelectedSection == mShippingAddressSection);
1190             mShippingOptionSection.focusSection(mSelectedSection == mShippingOptionSection);
1191         }
1192         if (mClient.shouldShowContactSection()) {
1193             mContactDetailsSection.focusSection(mSelectedSection == mContactDetailsSection);
1194         }
1195         mPaymentMethodSection.focusSection(mSelectedSection == mPaymentMethodSection);
1196         updateSectionButtons();
1197     }
1198 
1199     /**
1200      * Updates the enabled/disabled state of each section's edit button.
1201      *
1202      * Only the top-most button is enabled -- the others are disabled so the user is directed
1203      * through the form from top to bottom.
1204      */
1205     private void updateSectionButtons() {
1206         // Disable edit buttons when the client is checking a selection.
1207         boolean mayEnableButton = !mIsClientCheckingSelection;
1208         for (int i = 0; i < mPaymentContainerLayout.getChildCount(); i++) {
1209             View child = mPaymentContainerLayout.getChildAt(i);
1210             if (!(child instanceof PaymentRequestSection)) continue;
1211 
1212             PaymentRequestSection section = (PaymentRequestSection) child;
1213             section.setIsEditButtonEnabled(mayEnableButton);
1214             if (section.getEditButtonState() != EDIT_BUTTON_GONE) mayEnableButton = false;
1215         }
1216     }
1217 
1218     /**
1219      * Called when the dialog is dismissed. Can be caused by:
1220      * <ul>
1221      *  <li>User click on the "back" button on the phone.</li>
1222      *  <li>User click on the "X" button in the top-right corner of the dialog.</li>
1223      *  <li>User click on the "CANCEL" button on the bottom of the dialog.</li>
1224      *  <li>Successfully processing the payment.</li>
1225      *  <li>Failure to process the payment.</li>
1226      *  <li>The JavaScript calling the abort() method in PaymentRequest API.</li>
1227      *  <li>The PaymentRequest JavaScript object being destroyed.</li>
1228      *  <li>User closing all incognito windows with PaymentRequest UI open in an incognito
1229      *      window.</li>
1230      * </ul>
1231      */
1232     // DimmingDialog.OnDismissListener implementation.
1233     @Override
1234     public void onDismiss() {
1235         mIsClosing = true;
1236         if (mEditorDialog.isShowing()) mEditorDialog.dismiss();
1237         if (mCardEditorDialog.isShowing()) mCardEditorDialog.dismiss();
1238         if (sEditorObserverForTest != null) sEditorObserverForTest.onEditorDismiss();
1239         if (!mIsClientClosing) mClient.onDismiss();
1240     }
1241 
1242     @Override
1243     public String getAdditionalText(PaymentRequestSection section) {
1244         if (section == mShippingAddressSection) {
1245             int selectedItemIndex = mShippingAddressSectionInformation.getSelectedItemIndex();
1246             if (selectedItemIndex != SectionInformation.NO_SELECTION
1247                     && selectedItemIndex != SectionInformation.INVALID_SELECTION) {
1248                 return null;
1249             }
1250 
1251             String customErrorMessage = mShippingAddressSectionInformation.getErrorMessage();
1252             if (selectedItemIndex == SectionInformation.INVALID_SELECTION
1253                     && !TextUtils.isEmpty(customErrorMessage)) {
1254                 return customErrorMessage;
1255             }
1256 
1257             return mContext.getString(selectedItemIndex == SectionInformation.NO_SELECTION
1258                             ? mShippingStrings.getSelectPrompt()
1259                             : mShippingStrings.getUnsupported());
1260         } else if (section == mPaymentMethodSection) {
1261             return mPaymentMethodSectionInformation.getAdditionalText();
1262         } else {
1263             return null;
1264         }
1265     }
1266 
1267     @Override
1268     public boolean isAdditionalTextDisplayingWarning(PaymentRequestSection section) {
1269         return section == mShippingAddressSection
1270                 && mShippingAddressSectionInformation != null
1271                 && mShippingAddressSectionInformation.getSelectedItemIndex()
1272                         == SectionInformation.INVALID_SELECTION;
1273     }
1274 
1275     @Override
1276     public void onSectionClicked(PaymentRequestSection section) {
1277         expand(section);
1278     }
1279 
1280     /**
1281      * Animates the different sections of the dialog expanding and contracting into their final
1282      * positions.
1283      */
1284     private void startSectionResizeAnimation() {
1285         Runnable animationEndRunnable = new Runnable() {
1286             @Override
1287             public void run() {
1288                 mSectionAnimator = null;
1289                 notifyReadyForInput();
1290                 mReadyToPayNotifierForTest.run();
1291             }
1292         };
1293 
1294         mSectionAnimator =
1295                 new FocusAnimator(mPaymentContainerLayout, mSelectedSection, animationEndRunnable);
1296     }
1297 
1298     /**
1299      * Animates the bottom sheet UI translating upwards from the bottom of the screen.
1300      * Can be canceled when a {@link SheetEnlargingAnimator} starts and expands the dialog.
1301      */
1302     private class PeekingAnimator
1303             extends AnimatorListenerAdapter implements OnLayoutChangeListener {
1304         @Override
1305         public void onLayoutChange(View v, int left, int top, int right, int bottom,
1306                 int oldLeft, int oldTop, int oldRight, int oldBottom) {
1307             mRequestView.removeOnLayoutChangeListener(this);
1308 
1309             mSheetAnimator = ObjectAnimator.ofFloat(
1310                     mRequestView, View.TRANSLATION_Y, mAnimatorTranslation, 0);
1311             mSheetAnimator.setDuration(DIALOG_ENTER_ANIMATION_MS);
1312             mSheetAnimator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN_INTERPOLATOR);
1313             mSheetAnimator.addListener(this);
1314             mSheetAnimator.start();
1315         }
1316 
1317         @Override
1318         public void onAnimationEnd(Animator animation) {
1319             mSheetAnimator = null;
1320         }
1321     }
1322 
1323     /** Animates the bottom sheet expanding to a larger sheet. */
1324     private class SheetEnlargingAnimator
1325             extends AnimatorListenerAdapter implements OnLayoutChangeListener {
1326         private final boolean mIsBottomBarLockedInPlace;
1327         private int mContainerHeightDifference;
1328 
1329         public SheetEnlargingAnimator(boolean isBottomBarLockedInPlace) {
1330             mIsBottomBarLockedInPlace = isBottomBarLockedInPlace;
1331         }
1332 
1333         /**
1334          * Updates the animation.
1335          *
1336          * @param progress How far along the animation is.  In the range [0,1], with 1 being done.
1337          */
1338         private void update(float progress) {
1339             // The dialog container initially starts off translated downward, gradually decreasing
1340             // the translation until it is in the right place on screen.
1341             float containerTranslation = mContainerHeightDifference * progress;
1342             mRequestView.setTranslationY(containerTranslation);
1343 
1344             if (mIsBottomBarLockedInPlace) {
1345                 // The bottom bar is translated along the dialog so that is looks like it stays in
1346                 // place at the bottom while the entire bottom sheet is translating upwards.
1347                 mBottomBar.setTranslationY(-containerTranslation);
1348 
1349                 // The payment container is sandwiched between the header and the bottom bar.
1350                 // Expansion animates by changing where its "bottom" is, letting its shadows appear
1351                 // and disappear as it changes size.
1352                 int paymentContainerBottom =
1353                         Math.min(mPaymentContainer.getTop() + mPaymentContainer.getMeasuredHeight(),
1354                                 mBottomBar.getTop());
1355                 mPaymentContainer.setBottom(paymentContainerBottom);
1356             }
1357         }
1358 
1359         @Override
1360         public void onLayoutChange(View v, int left, int top, int right, int bottom,
1361                 int oldLeft, int oldTop, int oldRight, int oldBottom) {
1362             if (mSheetAnimator != null) mSheetAnimator.cancel();
1363 
1364             mRequestView.removeOnLayoutChangeListener(this);
1365             mContainerHeightDifference = (bottom - top) - (oldBottom - oldTop);
1366 
1367             ValueAnimator containerAnimator = ValueAnimator.ofFloat(1f, 0f);
1368             containerAnimator.addUpdateListener(new AnimatorUpdateListener() {
1369                 @Override
1370                 public void onAnimationUpdate(ValueAnimator animation) {
1371                     float alpha = (Float) animation.getAnimatedValue();
1372                     update(alpha);
1373                 }
1374             });
1375 
1376             mSheetAnimator = containerAnimator;
1377             mSheetAnimator.setDuration(DIALOG_ENTER_ANIMATION_MS);
1378             mSheetAnimator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN_INTERPOLATOR);
1379             mSheetAnimator.addListener(this);
1380             mSheetAnimator.start();
1381         }
1382 
1383         @Override
1384         public void onAnimationEnd(Animator animation) {
1385             // Reset the layout so that everything is in the expected place.
1386             mRequestView.setTranslationY(0);
1387             mBottomBar.setTranslationY(0);
1388             mRequestView.requestLayout();
1389 
1390             // Indicate that the dialog is ready to use.
1391             mSheetAnimator = null;
1392             notifyReadyForInput();
1393             mReadyToPayNotifierForTest.run();
1394         }
1395     }
1396 
1397     @VisibleForTesting
1398     public static void setEditorObserverForTest(EditorObserverForTest editorObserverForTest) {
1399         sEditorObserverForTest = editorObserverForTest;
1400         EditorDialog.setEditorObserverForTest(sEditorObserverForTest);
1401     }
1402 
1403     @VisibleForTesting
1404     public static void setPaymentRequestObserverForTest(
1405             PaymentRequestObserverForTest paymentRequestObserverForTest) {
1406         sPaymentRequestObserverForTest = paymentRequestObserverForTest;
1407     }
1408 
1409     @VisibleForTesting
1410     public Dialog getDialogForTest() {
1411         return mDialog.getDialogForTest();
1412     }
1413 
1414     @VisibleForTesting
1415     public TextView getOrderSummaryTotalTextViewForTest() {
1416         return mOrderSummarySection.getSummaryRightTextView();
1417     }
1418 
1419     @VisibleForTesting
1420     public LineItemBreakdownSection getOrderSummarySectionForTest() {
1421         return mOrderSummarySection;
1422     }
1423 
1424     @VisibleForTesting
1425     public OptionSection getShippingAddressSectionForTest() {
1426         return mShippingAddressSection;
1427     }
1428 
1429     @VisibleForTesting
1430     public OptionSection getShippingOptionSectionForTest() {
1431         return mShippingOptionSection;
1432     }
1433 
1434     @VisibleForTesting
1435     public ViewGroup getPaymentMethodSectionForTest() {
1436         return mPaymentMethodSection;
1437     }
1438 
1439     @VisibleForTesting
1440     public PaymentRequestSection getContactDetailsSectionForTest() {
1441         return mContactDetailsSection;
1442     }
1443 
1444     private void notifyReadyForInput() {
1445         if (sPaymentRequestObserverForTest != null && isAcceptingUserInput()) {
1446             sPaymentRequestObserverForTest.onPaymentRequestReadyForInput(this);
1447         }
1448     }
1449 
1450     private void notifySelectionChecked() {
1451         if (sPaymentRequestObserverForTest != null) {
1452             sPaymentRequestObserverForTest.onPaymentRequestSelectionChecked(this);
1453         }
1454     }
1455 
1456     /**
1457      * Set the visibility state of the dialog. Use {@link PaymentUisShowStateReconciler}'s
1458      * showPaymentRequestDialogWhenNoBottomSheet() and hidePaymentRequestDialog() instead of calling
1459      * this method directly.
1460      * @param visible True to show the dialog, false to hide the dialog.
1461      * @return Whether setting visibility is successful.
1462      */
1463     public boolean setVisible(boolean visible) {
1464         if (visible) {
1465             return mDialog.show();
1466         } else {
1467             mDialog.hide();
1468             return true;
1469         }
1470     }
1471 
1472     // Implement PauseResumeWithNativeObserver:
1473     @Override
1474     public void onResumeWithNative() {
1475         // When users come back from an external activity (e.g., app-picker/webauthn), the PR UI
1476         // somehow shows up even though it's set to GONE (crbug.com/1030416 and
1477         // crbug.com/1051786). Here we use a workaround to fix it - refresh the dialog window
1478         // from time to time to force the visual state to respect its visibility attribute.
1479         mDialog.refresh();
1480     }
1481 
1482     // Implement PauseResumeWithNativeObserver:
1483     @Override
1484     public void onPauseWithNative() {}
1485 }
1486