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 <title> 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