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 android.content.Context; 8 import android.content.res.Resources; 9 import android.graphics.drawable.Drawable; 10 import android.os.Handler; 11 import android.text.SpannableStringBuilder; 12 import android.text.TextUtils; 13 import android.text.TextUtils.TruncateAt; 14 import android.text.style.AbsoluteSizeSpan; 15 import android.text.style.ForegroundColorSpan; 16 import android.text.style.StyleSpan; 17 import android.view.Gravity; 18 import android.view.LayoutInflater; 19 import android.view.MotionEvent; 20 import android.view.View; 21 import android.view.ViewGroup; 22 import android.view.animation.AlphaAnimation; 23 import android.view.animation.Animation; 24 import android.widget.Button; 25 import android.widget.ImageButton; 26 import android.widget.ImageView; 27 import android.widget.LinearLayout; 28 import android.widget.RadioButton; 29 import android.widget.TextView; 30 31 import androidx.annotation.ColorInt; 32 import androidx.annotation.Nullable; 33 import androidx.annotation.VisibleForTesting; 34 import androidx.core.view.MarginLayoutParamsCompat; 35 import androidx.gridlayout.widget.GridLayout; 36 37 import org.chromium.base.ApiCompatibilityUtils; 38 import org.chromium.chrome.R; 39 import org.chromium.components.autofill.EditableOption; 40 import org.chromium.components.browser_ui.widget.DualControlLayout; 41 import org.chromium.components.browser_ui.widget.TintedDrawable; 42 import org.chromium.components.browser_ui.widget.animation.Interpolators; 43 import org.chromium.ui.HorizontalListDividerDrawable; 44 import org.chromium.ui.UiUtils; 45 46 import java.util.ArrayList; 47 import java.util.List; 48 49 /** 50 * Represents a single section in the {@link PaymentRequestUI} that flips between multiple states. 51 * 52 * The row is broken up into three major, vertically-centered sections: 53 * ............................................................................................. 54 * . TITLE | | CHEVRON . 55 * .................................................................| | or . 56 * . LEFT SUMMARY TEXT | RIGHT SUMMARY TEXT | LOGO | ADD . 57 * .................................................................| | or . 58 * . MAIN SECTION CONTENT | | CHOOSE . 59 * ............................................................................................. 60 * 61 * 1) MAIN CONTENT 62 * The main content is on the left side of the UI. This includes the title of the section and 63 * two bits of optional summary text. Subclasses may extend this class to append more controls 64 * via the {@link #createMainSectionContent} function. 65 * 66 * 2) LOGO 67 * Displays an optional logo (e.g. a credit card image) that floats to the right of the main 68 * content. 69 * 70 * 3) CHEVRON or ADD or CHOOSE 71 * Drawn to indicate that the current section may be expanded. Displayed only when the view is 72 * in the {@link #DISPLAY_MODE_EXPANDABLE} state and only if an ADD or CHOOSE button isn't shown. 73 * 74 * There are three states that the UI may flip between; see {@link #DISPLAY_MODE_NORMAL}, 75 * {@link #DISPLAY_MODE_EXPANDABLE}, and {@link #DISPLAY_MODE_FOCUSED} for details. 76 */ 77 public abstract class PaymentRequestSection extends LinearLayout implements View.OnClickListener { 78 public static final String TAG = "PaymentRequestUI"; 79 80 /** Handles clicks on the widgets and providing data to the PaymentsRequestSection. */ 81 public interface SectionDelegate extends View.OnClickListener { 82 /** 83 * Called when the user selects a radio button option from an {@link OptionSection}. 84 * 85 * @param section Section that was changed. 86 * @param option {@link EditableOption} that was selected. 87 */ onEditableOptionChanged(PaymentRequestSection section, EditableOption option)88 void onEditableOptionChanged(PaymentRequestSection section, EditableOption option); 89 90 /** Called when the user clicks the edit icon of the selected EditableOption. */ onEditEditableOption(PaymentRequestSection section, EditableOption option)91 void onEditEditableOption(PaymentRequestSection section, EditableOption option); 92 93 /** Called when the user requests adding a new EditableOption to a given section. */ onAddEditableOption(PaymentRequestSection section)94 void onAddEditableOption(PaymentRequestSection section); 95 96 /** Checks whether or not the text should be formatted with a bold label. */ isBoldLabelNeeded(PaymentRequestSection section)97 boolean isBoldLabelNeeded(PaymentRequestSection section); 98 99 /** Checks whether or not the user should be allowed to click on controls. */ isAcceptingUserInput()100 boolean isAcceptingUserInput(); 101 102 /** Returns any additional text that needs to be displayed. */ getAdditionalText(PaymentRequestSection section)103 @Nullable String getAdditionalText(PaymentRequestSection section); 104 105 /** Returns true if the additional text should be stylized as a warning instead of info. */ isAdditionalTextDisplayingWarning(PaymentRequestSection section)106 boolean isAdditionalTextDisplayingWarning(PaymentRequestSection section); 107 108 /** Called when a section has been clicked. */ onSectionClicked(PaymentRequestSection section)109 void onSectionClicked(PaymentRequestSection section); 110 } 111 112 /** Edit button mode: Hide the button. */ 113 public static final int EDIT_BUTTON_GONE = 0; 114 115 /** Edit button mode: Indicate that the section requires a selection. */ 116 public static final int EDIT_BUTTON_CHOOSE = 1; 117 118 /** Edit button mode: Indicate that the section requires adding an option. */ 119 public static final int EDIT_BUTTON_ADD = 2; 120 121 /** Normal mode: White background, displays the item assuming the user accepts it as is. */ 122 public static final int DISPLAY_MODE_NORMAL = 3; 123 124 /** Editable mode: White background, displays the item with an edit chevron. */ 125 public static final int DISPLAY_MODE_EXPANDABLE = 4; 126 127 /** Focused mode: Gray background, more padding, no edit chevron. */ 128 public static final int DISPLAY_MODE_FOCUSED = 5; 129 130 /** Checking mode: Gray background, spinner overlay hides everything except the title. */ 131 public static final int DISPLAY_MODE_CHECKING = 6; 132 133 protected final SectionDelegate mDelegate; 134 protected final int mLargeSpacing; 135 protected final Button mEditButtonView; 136 protected final boolean mIsLayoutInitialized; 137 138 protected int mDisplayMode = DISPLAY_MODE_NORMAL; 139 140 private final int mVerticalSpacing; 141 private final @ColorInt int mUnfocusedBackgroundColor; 142 private final int mFocusedBackgroundColor; 143 private final LinearLayout mMainSection; 144 private final ImageView mLogoView; 145 private final ImageView mChevronView; 146 147 private TextView mTitleView; 148 private LinearLayout mSummaryLayout; 149 private TextView mSummaryLeftTextView; 150 private TextView mSummaryRightTextView; 151 152 private Drawable mLogo; 153 private boolean mIsSummaryAllowed = true; 154 155 /** 156 * Constructs a PaymentRequestSection. 157 * 158 * @param context Context to pull resources from. 159 * @param sectionName Title of the section to display. 160 * @param delegate Delegate to alert when something changes in the dialog. 161 */ PaymentRequestSection(Context context, String sectionName, SectionDelegate delegate)162 private PaymentRequestSection(Context context, String sectionName, SectionDelegate delegate) { 163 super(context); 164 mDelegate = delegate; 165 setOnClickListener(delegate); 166 setOrientation(HORIZONTAL); 167 setGravity(Gravity.CENTER_VERTICAL); 168 169 // Set the styling of the view. 170 mUnfocusedBackgroundColor = 171 ApiCompatibilityUtils.getColor(getResources(), R.color.payment_request_bg); 172 mFocusedBackgroundColor = ApiCompatibilityUtils.getColor( 173 getResources(), R.color.payments_section_edit_background); 174 mLargeSpacing = 175 getResources().getDimensionPixelSize(R.dimen.editor_dialog_section_large_spacing); 176 mVerticalSpacing = 177 getResources().getDimensionPixelSize(R.dimen.payments_section_vertical_spacing); 178 setPadding(mLargeSpacing, mVerticalSpacing, mLargeSpacing, mVerticalSpacing); 179 180 // Create the main content. 181 mMainSection = prepareMainSection(sectionName); 182 mLogoView = isLogoNecessary() ? createAndAddLogoView(this, mLargeSpacing) : null; 183 mEditButtonView = createAndAddEditButton(this); 184 mChevronView = createAndAddChevron(this); 185 mIsLayoutInitialized = true; 186 setDisplayMode(DISPLAY_MODE_NORMAL); 187 } 188 189 /** 190 * Sets what logo should be displayed. 191 * 192 * @param logo The logo to display. 193 */ setLogoDrawable(Drawable logo)194 protected void setLogoDrawable(Drawable logo) { 195 assert isLogoNecessary(); 196 mLogo = logo; 197 mLogoView.setBackgroundResource(0); 198 mLogoView.setImageDrawable(mLogo); 199 } 200 201 /** Returns the LinearLayout containing the summary texts of the section. */ getSummaryLayout()202 protected LinearLayout getSummaryLayout() { 203 assert mSummaryLayout != null; 204 return mSummaryLayout; 205 } 206 207 /** Returns the right summary TextView. */ getSummaryRightTextView()208 protected TextView getSummaryRightTextView() { 209 assert mSummaryRightTextView != null; 210 return mSummaryRightTextView; 211 } 212 213 /** Returns the left summary TextView. */ getSummaryLeftTextView()214 protected TextView getSummaryLeftTextView() { 215 assert mSummaryLeftTextView != null; 216 return mSummaryLeftTextView; 217 } 218 219 @Override onInterceptTouchEvent(MotionEvent event)220 public boolean onInterceptTouchEvent(MotionEvent event) { 221 // Allow touches to propagate to children only if the layout can be interacted with. 222 return !mDelegate.isAcceptingUserInput(); 223 } 224 225 @Override onClick(View v)226 public final void onClick(View v) { 227 if (!mDelegate.isAcceptingUserInput()) return; 228 229 // Handle clicking on "ADD" or "CHOOSE". 230 if (v == mEditButtonView) { 231 if (getEditButtonState() == EDIT_BUTTON_ADD) { 232 mDelegate.onAddEditableOption(this); 233 } else { 234 mDelegate.onSectionClicked(this); 235 } 236 return; 237 } 238 239 handleClick(v); 240 updateControlLayout(); 241 } 242 243 /** Handles clicks on the PaymentRequestSection. */ handleClick(View v)244 protected void handleClick(View v) { } 245 246 /** 247 * Called when the UI is telling the section that it has either gained or lost focus. 248 */ focusSection(boolean shouldFocus)249 public void focusSection(boolean shouldFocus) { 250 setDisplayMode(shouldFocus ? DISPLAY_MODE_FOCUSED : DISPLAY_MODE_EXPANDABLE); 251 } 252 253 /** 254 * Updates what Views are displayed and how they look. 255 * 256 * @param displayMode What mode the widget is being displayed in. 257 */ setDisplayMode(int displayMode)258 public void setDisplayMode(int displayMode) { 259 mDisplayMode = displayMode; 260 updateControlLayout(); 261 } 262 263 /** 264 * Changes what is being displayed in the summary. 265 * 266 * @param leftText Text to display on the left side. If null, the whole row hides. 267 * @param rightText Text to display on the right side. If null, only the right View hides. 268 */ setSummaryText( @ullable CharSequence leftText, @Nullable CharSequence rightText)269 public void setSummaryText( 270 @Nullable CharSequence leftText, @Nullable CharSequence rightText) { 271 mSummaryLeftTextView.setText(leftText); 272 mSummaryRightTextView.setText(rightText); 273 mSummaryRightTextView.setVisibility(TextUtils.isEmpty(rightText) ? GONE : VISIBLE); 274 updateControlLayout(); 275 } 276 277 /** 278 * Changes the appearance of the title. 279 * 280 * @param resId @see android.widget.TextView#setTextAppearance(int id). 281 */ setTitleAppearance(int resId)282 protected void setTitleAppearance(int resId) { 283 ApiCompatibilityUtils.setTextAppearance(mTitleView, resId); 284 } 285 286 /** 287 * Changes the appearance of the summary. 288 * 289 * @param resId @see android.widget.TextView#setTextAppearance(int id). 290 */ setSummaryAppearance(int leftResId, int rightResId)291 protected void setSummaryAppearance(int leftResId, int rightResId) { 292 ApiCompatibilityUtils.setTextAppearance(mSummaryLeftTextView, leftResId); 293 ApiCompatibilityUtils.setTextAppearance(mSummaryRightTextView, rightResId); 294 } 295 296 /** 297 * Sets how the summary text should be displayed. 298 * 299 * @param leftTruncate How to truncate the left summary text. Set to null to clear. 300 * @param leftIsSingleLine Whether the left summary text should be a single line. 301 * @param rightTruncate How to truncate the right summary text. Set to null to clear. 302 * @param rightIsSingleLine Whether the right summary text should be a single line. 303 */ setSummaryProperties(@ullable TruncateAt leftTruncate, boolean leftIsSingleLine, @Nullable TruncateAt rightTruncate, boolean rightIsSingleLine)304 public void setSummaryProperties(@Nullable TruncateAt leftTruncate, boolean leftIsSingleLine, 305 @Nullable TruncateAt rightTruncate, boolean rightIsSingleLine) { 306 mSummaryLeftTextView.setEllipsize(leftTruncate); 307 mSummaryLeftTextView.setSingleLine(leftIsSingleLine); 308 309 mSummaryRightTextView.setEllipsize(rightTruncate); 310 mSummaryRightTextView.setSingleLine(rightIsSingleLine); 311 } 312 313 /** 314 * Subclasses may override this method to add additional controls to the layout. 315 * 316 * @param mainSectionLayout Layout containing all of the main content of the section. 317 */ createMainSectionContent(LinearLayout mainSectionLayout)318 protected abstract void createMainSectionContent(LinearLayout mainSectionLayout); 319 320 /** 321 * Sets whether the edit button may be interacted with. 322 * 323 * @param isEnabled Whether the button may be interacted with. 324 */ setIsEditButtonEnabled(boolean isEnabled)325 public void setIsEditButtonEnabled(boolean isEnabled) { 326 mEditButtonView.setEnabled(isEnabled); 327 } 328 329 /** 330 * Sets whether the summary text can be displayed. 331 * 332 * @param isAllowed Whether to display the summary text when needed. 333 */ setIsSummaryAllowed(boolean isAllowed)334 protected void setIsSummaryAllowed(boolean isAllowed) { 335 mIsSummaryAllowed = isAllowed; 336 } 337 338 /** @return Whether or not the logo should be displayed. */ isLogoNecessary()339 protected boolean isLogoNecessary() { 340 return false; 341 } 342 343 /** 344 * Returns the state of the edit button, which is hidden by default. 345 * 346 * @return State of the edit button. 347 */ getEditButtonState()348 public int getEditButtonState() { 349 return EDIT_BUTTON_GONE; 350 } 351 352 /** 353 * Creates the main section. Subclasses must call super#createMainSection() immediately to 354 * guarantee that Views are added in the correct order. 355 * 356 * @param sectionName Title to display for the section. 357 */ prepareMainSection(String sectionName)358 private LinearLayout prepareMainSection(String sectionName) { 359 // The main section is a vertical linear layout that subclasses can append to. 360 LinearLayout mainSectionLayout = new LinearLayout(getContext()); 361 mainSectionLayout.setOrientation(VERTICAL); 362 LinearLayout.LayoutParams mainParams = new LayoutParams(0, LayoutParams.WRAP_CONTENT); 363 mainParams.weight = 1; 364 addView(mainSectionLayout, mainParams); 365 366 // The title is always displayed for the row at the top of the main section. 367 mTitleView = new TextView(getContext()); 368 mTitleView.setText(sectionName); 369 ApiCompatibilityUtils.setTextAppearance(mTitleView, R.style.TextAppearance_TextMedium_Blue); 370 mainSectionLayout.addView( 371 mTitleView, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); 372 373 // Create the two TextViews for showing the summary text. 374 mSummaryLeftTextView = new TextView(getContext()); 375 mSummaryLeftTextView.setId(R.id.payments_left_summary_label); 376 ApiCompatibilityUtils.setTextAppearance( 377 mSummaryLeftTextView, R.style.TextAppearance_TextLarge_Primary); 378 379 mSummaryRightTextView = new TextView(getContext()); 380 ApiCompatibilityUtils.setTextAppearance( 381 mSummaryRightTextView, R.style.TextAppearance_TextLarge_Primary); 382 mSummaryRightTextView.setTextAlignment(TEXT_ALIGNMENT_TEXT_END); 383 384 // The main TextView sucks up all the available space. 385 LinearLayout.LayoutParams leftLayoutParams = new LinearLayout.LayoutParams( 386 0, LayoutParams.WRAP_CONTENT); 387 leftLayoutParams.weight = 1; 388 389 LinearLayout.LayoutParams rightLayoutParams = new LinearLayout.LayoutParams( 390 LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); 391 MarginLayoutParamsCompat.setMarginStart(rightLayoutParams, 392 getContext().getResources().getDimensionPixelSize( 393 R.dimen.editor_dialog_section_small_spacing)); 394 395 // The summary section displays up to two TextViews side by side. 396 mSummaryLayout = new LinearLayout(getContext()); 397 mSummaryLayout.addView(mSummaryLeftTextView, leftLayoutParams); 398 mSummaryLayout.addView(mSummaryRightTextView, rightLayoutParams); 399 mainSectionLayout.addView(mSummaryLayout, new LinearLayout.LayoutParams( 400 LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); 401 setSummaryText(null, null); 402 403 createMainSectionContent(mainSectionLayout); 404 return mainSectionLayout; 405 } 406 createAndAddLogoView(ViewGroup parent, int startMargin)407 private static ImageView createAndAddLogoView(ViewGroup parent, int startMargin) { 408 ImageView view = new ImageView(parent.getContext()); 409 view.setMaxWidth(parent.getContext().getResources().getDimensionPixelSize( 410 R.dimen.editable_option_section_logo_width)); 411 view.setAdjustViewBounds(true); 412 413 // The logo has a pre-defined height and width. 414 LayoutParams params = 415 new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); 416 MarginLayoutParamsCompat.setMarginStart(params, startMargin); 417 parent.addView(view, params); 418 return view; 419 } 420 createAndAddEditButton(ViewGroup parent)421 private Button createAndAddEditButton(ViewGroup parent) { 422 Resources resources = parent.getResources(); 423 Button view = DualControlLayout.createButtonForLayout( 424 parent.getContext(), true, resources.getString(R.string.choose), this); 425 view.setId(R.id.payments_section); 426 427 LayoutParams params = 428 new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); 429 MarginLayoutParamsCompat.setMarginStart(params, mLargeSpacing); 430 parent.addView(view, params); 431 return view; 432 } 433 createAndAddChevron(ViewGroup parent)434 private ImageView createAndAddChevron(ViewGroup parent) { 435 TintedDrawable chevron = TintedDrawable.constructTintedDrawable(parent.getContext(), 436 R.drawable.ic_expand_more_black_24dp, R.color.payments_section_chevron); 437 438 ImageView view = new ImageView(parent.getContext()); 439 view.setImageDrawable(chevron); 440 441 // Wrap whatever image is passed in. 442 LayoutParams params = 443 new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); 444 MarginLayoutParamsCompat.setMarginStart(params, mLargeSpacing); 445 parent.addView(view, params); 446 return view; 447 } 448 449 /** 450 * Called when the section's controls need to be updated after configuration changes. 451 * 452 * Because of the complicated special casing of what controls hide other controls, all calls to 453 * update just one of the controls causes the visibility logic to trigger for all of them. 454 * 455 * Subclasses should call the super method after they update their own controls. 456 */ updateControlLayout()457 protected void updateControlLayout() { 458 if (!mIsLayoutInitialized) return; 459 460 boolean isExpanded = 461 mDisplayMode == DISPLAY_MODE_FOCUSED || mDisplayMode == DISPLAY_MODE_CHECKING; 462 setBackgroundColor(isExpanded ? mFocusedBackgroundColor : mUnfocusedBackgroundColor); 463 464 // Update whether the logo is displayed. 465 if (mLogoView != null) { 466 boolean show = mLogo != null && mDisplayMode != DISPLAY_MODE_FOCUSED; 467 mLogoView.setVisibility(show ? VISIBLE : GONE); 468 } 469 470 // The button takes precedence over the summary text and the chevron. 471 int editButtonState = getEditButtonState(); 472 if (editButtonState == EDIT_BUTTON_GONE) { 473 mEditButtonView.setVisibility(GONE); 474 mChevronView.setVisibility( 475 mDisplayMode == DISPLAY_MODE_EXPANDABLE ? VISIBLE : GONE); 476 } else { 477 // Show the edit button and hide the chevron. 478 boolean isButtonAllowed = mDisplayMode == DISPLAY_MODE_EXPANDABLE 479 || mDisplayMode == DISPLAY_MODE_NORMAL; 480 mChevronView.setVisibility(GONE); 481 mEditButtonView.setVisibility(isButtonAllowed ? VISIBLE : GONE); 482 mEditButtonView.setText( 483 editButtonState == EDIT_BUTTON_CHOOSE ? R.string.choose : R.string.add); 484 } 485 486 // Update whether the summary is displayed. 487 mSummaryLayout.setVisibility(mIsSummaryAllowed ? VISIBLE : GONE); 488 489 // The title gains extra spacing when there is another visible view in the main section. 490 int numVisibleMainViews = 0; 491 for (int i = 0; i < mMainSection.getChildCount(); i++) { 492 if (mMainSection.getChildAt(i).getVisibility() == VISIBLE) numVisibleMainViews += 1; 493 } 494 495 boolean isTitleMarginNecessary = numVisibleMainViews > 1 && isExpanded; 496 int oldMargin = 497 ((ViewGroup.MarginLayoutParams) mTitleView.getLayoutParams()).bottomMargin; 498 int newMargin = isTitleMarginNecessary ? mVerticalSpacing : 0; 499 500 if (oldMargin != newMargin) { 501 ((ViewGroup.MarginLayoutParams) mTitleView.getLayoutParams()).bottomMargin = 502 newMargin; 503 requestLayout(); 504 } 505 } 506 507 /** 508 * Section with an additional Layout for showing a total and how it is broken down. 509 * 510 * Normal mode: Just the summary is displayed. 511 * If no option is selected, the "empty label" is displayed in its place. 512 * Expandable mode: Same as Normal, but shows the chevron. 513 * Focused mode: Hides the summary and chevron, then displays the full set of options. 514 * 515 * ............................................................................ 516 * . TITLE | . 517 * .................................................................| CHERVON . 518 * . LEFT SUMMARY TEXT | UPDATE TEXT | RIGHT SUMMARY TEXT | or . 519 * .................................................................| ADD . 520 * . | Line item 1 | $13.99 | or . 521 * . | Line item 2 | $.99 | CHOOSE . 522 * . | Line item 3 | $2.99 | . 523 * ............................................................................ 524 */ 525 public static class LineItemBreakdownSection extends PaymentRequestSection { 526 /** The duration of the animation to show and hide the update text. */ 527 static final int UPDATE_TEXT_ANIMATION_DURATION_MS = 500; 528 529 /** The amount of time where the update text is visible before fading out. */ 530 static final int UPDATE_TEXT_VISIBILITY_DURATION_MS = 5000; 531 532 /** The GridLayout that shows a breakdown of all the items in the user's card. */ 533 private GridLayout mBreakdownLayout; 534 535 /** 536 * The TextView that is used to display the updated message to the user when the total price 537 * of their cart changes. It's the second child of the mSummaryLayout. 538 */ 539 private TextView mUpdatedView; 540 541 private final List<TextView> mLineItemAmountsForTest = new ArrayList<>(); 542 543 /** The runnable used to fade out the mUpdatedView. */ 544 private Runnable mFadeOutRunnable = new Runnable() { 545 @Override 546 public void run() { 547 Animation out = new AlphaAnimation(mUpdatedView.getAlpha(), 0.0f); 548 out.setDuration(UPDATE_TEXT_ANIMATION_DURATION_MS); 549 out.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN_INTERPOLATOR); 550 out.setFillAfter(true); 551 mUpdatedView.startAnimation(out); 552 } 553 }; 554 555 /** The Handler used to post the mFadeOutRunnables. */ 556 private Handler mHandler = new Handler(); 557 LineItemBreakdownSection( Context context, String sectionName, SectionDelegate delegate, String updatedText)558 public LineItemBreakdownSection( 559 Context context, String sectionName, SectionDelegate delegate, String updatedText) { 560 super(context, sectionName, delegate); 561 562 // The mUpdatedView should have been created in the base constructor's call to 563 // createMainSectionContent(...). 564 assert mUpdatedView != null; 565 mUpdatedView.setText(updatedText); 566 } 567 568 // This method is called in PaymentRequestSection's constructor. 569 @Override createMainSectionContent(LinearLayout mainSectionLayout)570 protected void createMainSectionContent(LinearLayout mainSectionLayout) { 571 Context context = mainSectionLayout.getContext(); 572 573 // Add a label that will be used to indicate that the total cart price has been updated. 574 addUpdateText(mainSectionLayout); 575 576 // The breakdown is represented by an end-aligned GridLayout that takes up only as much 577 // space as it needs. The GridLayout ensures a consistent margin between the columns. 578 mBreakdownLayout = new GridLayout(context); 579 mBreakdownLayout.setColumnCount(2); 580 LayoutParams breakdownParams = 581 new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); 582 breakdownParams.gravity = Gravity.END; 583 mainSectionLayout.addView(mBreakdownLayout, breakdownParams); 584 585 // Sets the summary right text view takes the same available space as the summary left 586 // text view. 587 LinearLayout.LayoutParams rightTextViewLayoutParams = 588 (LinearLayout.LayoutParams) getSummaryRightTextView().getLayoutParams(); 589 rightTextViewLayoutParams.width = 0; 590 rightTextViewLayoutParams.weight = 1f; 591 } 592 593 /** 594 * Adds a text view to the summary layout that will be used to indicate that the total price 595 * of the card been updated. The text to display should be set later in the constructor. 596 * 597 * @param mainSectionLayout The layout of this section. 598 */ addUpdateText(LinearLayout mainSectionLayout)599 private void addUpdateText(LinearLayout mainSectionLayout) { 600 assert mUpdatedView == null; 601 602 Context context = mainSectionLayout.getContext(); 603 604 // Create the view and set the text appearance and layout parameters. 605 mUpdatedView = new TextView(context); 606 ApiCompatibilityUtils.setTextAppearance( 607 mUpdatedView, R.style.TextAppearance_TextLarge_Primary); 608 LinearLayout.LayoutParams updatedLayoutParams = new LinearLayout.LayoutParams( 609 LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); 610 mUpdatedView.setTextAlignment(TEXT_ALIGNMENT_TEXT_END); 611 mUpdatedView.setTextColor(ApiCompatibilityUtils.getColor( 612 context.getResources(), R.color.google_green_600)); 613 MarginLayoutParamsCompat.setMarginStart(updatedLayoutParams, 614 context.getResources().getDimensionPixelSize( 615 R.dimen.editor_dialog_section_small_spacing)); 616 MarginLayoutParamsCompat.setMarginEnd(updatedLayoutParams, 617 context.getResources().getDimensionPixelSize( 618 R.dimen.editor_dialog_section_small_spacing)); 619 620 // Set the view to initially be invisible. 621 mUpdatedView.setVisibility(View.INVISIBLE); 622 623 // Add the update text just before the last summary text. 624 getSummaryLayout().addView( 625 mUpdatedView, getSummaryLayout().getChildCount() - 1, updatedLayoutParams); 626 } 627 628 /** 629 * Updates the total and how it's broken down. 630 * 631 * @param cart The shopping cart contents and the total. 632 */ update(ShoppingCart cart)633 public void update(ShoppingCart cart) { 634 Context context = mBreakdownLayout.getContext(); 635 636 CharSequence totalPrice = createValueString( 637 cart.getTotal().getCurrency(), cart.getTotal().getPrice(), true); 638 639 // Show the updated text view if the total changed. 640 showUpdateIfTextChanged(totalPrice); 641 642 // Update the summary to display information about the total. 643 setSummaryText(cart.getTotal().getLabel(), totalPrice); 644 645 mBreakdownLayout.removeAllViews(); 646 mLineItemAmountsForTest.clear(); 647 if (cart.getContents() == null) return; 648 649 int maximumDescriptionWidthPx = 650 ((View) mBreakdownLayout.getParent()).getWidth() * 2 / 3; 651 652 // Update the breakdown, using one row per {@link LineItem}. 653 int numItems = cart.getContents().size(); 654 mBreakdownLayout.setRowCount(numItems); 655 for (int i = 0; i < numItems; i++) { 656 LineItem item = cart.getContents().get(i); 657 658 TextView description = new TextView(context); 659 ApiCompatibilityUtils.setTextAppearance(description, 660 item.getIsPending() 661 ? R.style.TextAppearance_PaymentsUiSectionPendingTextEndAligned 662 : R.style.TextAppearance_PaymentsUiSectionDescriptiveTextEndAligned); 663 description.setText(item.getLabel()); 664 description.setEllipsize(TruncateAt.END); 665 description.setMaxLines(2); 666 if (maximumDescriptionWidthPx > 0) { 667 description.setMaxWidth(maximumDescriptionWidthPx); 668 } 669 670 TextView amount = new TextView(context); 671 ApiCompatibilityUtils.setTextAppearance(amount, 672 item.getIsPending() 673 ? R.style.TextAppearance_PaymentsUiSectionPendingTextEndAligned 674 : R.style.TextAppearance_PaymentsUiSectionDescriptiveTextEndAligned); 675 amount.setText(createValueString(item.getCurrency(), item.getPrice(), false)); 676 mLineItemAmountsForTest.add(amount); 677 678 // Each item is represented by a row in the GridLayout. 679 GridLayout.LayoutParams descriptionParams = new GridLayout.LayoutParams( 680 GridLayout.spec(i, 1, GridLayout.END), 681 GridLayout.spec(0, 1, GridLayout.END)); 682 GridLayout.LayoutParams amountParams = new GridLayout.LayoutParams( 683 GridLayout.spec(i, 1, GridLayout.END), 684 GridLayout.spec(1, 1, GridLayout.END)); 685 MarginLayoutParamsCompat.setMarginStart(amountParams, 686 context.getResources().getDimensionPixelSize( 687 R.dimen.payments_section_descriptive_item_spacing)); 688 689 mBreakdownLayout.addView(description, descriptionParams); 690 mBreakdownLayout.addView(amount, amountParams); 691 } 692 } 693 694 /** 695 * Show the update text if the cart total has changed. Should be called before changing the 696 * cart total because the old total is needed for comparison. 697 * 698 * @param rightText The new cart total that will replace the one currently displayed. 699 */ showUpdateIfTextChanged(@ullable CharSequence rightText)700 private void showUpdateIfTextChanged(@Nullable CharSequence rightText) { 701 // If either the old or new text was null do nothing. 702 if (rightText == null || getSummaryRightTextView().getText() == null) return; 703 704 // Show the update text only if the current and new cart totals are different and if the 705 // old total was visible to the user. 706 if (!TextUtils.equals(getSummaryRightTextView().getText(), rightText) 707 && getSummaryRightTextView().getVisibility() == VISIBLE) { 708 startUpdateViewAnimation(); 709 } 710 } 711 712 /** 713 * Starts the animation to make the update text view fade in then fade out. 714 */ startUpdateViewAnimation()715 private void startUpdateViewAnimation() { 716 // Create and start a fade in anmiation for the mUpdatedView. Re-use the current alpha 717 // to avoid restarting a previous or current fade in animation. 718 Animation in = new AlphaAnimation(mUpdatedView.getAlpha(), 1.0f); 719 in.setDuration(UPDATE_TEXT_ANIMATION_DURATION_MS); 720 in.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN_INTERPOLATOR); 721 in.setFillAfter(true); 722 mUpdatedView.startAnimation(in); 723 724 // Cancel all pending fade out animations and create a new on to be executed a little 725 // while after the fade in. 726 mHandler.removeCallbacks(mFadeOutRunnable); 727 mHandler.postDelayed(mFadeOutRunnable, UPDATE_TEXT_VISIBILITY_DURATION_MS); 728 } 729 730 /** 731 * Builds a CharSequence that displays a value in a particular currency. 732 * 733 * @param currency Currency of the value being displayed. 734 * @param value Value to display. 735 * @param isValueBold Whether or not to bold the item. 736 * @return CharSequence that represents the whole value. 737 */ createValueString(String currency, String value, boolean isValueBold)738 private CharSequence createValueString(String currency, String value, boolean isValueBold) { 739 SpannableStringBuilder valueBuilder = new SpannableStringBuilder(); 740 valueBuilder.append(currency); 741 valueBuilder.append(" "); 742 743 int boldStartIndex = valueBuilder.length(); 744 valueBuilder.append(value); 745 746 if (isValueBold) { 747 valueBuilder.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), boldStartIndex, 748 boldStartIndex + value.length(), 0); 749 } 750 751 return valueBuilder; 752 } 753 754 @Override setDisplayMode(int displayMode)755 public void setDisplayMode(int displayMode) { 756 // Displays the summary left text view in at most three lines if in focus mode, 757 // otherwise display it in a single line. 758 if (displayMode == DISPLAY_MODE_FOCUSED) { 759 setSummaryProperties(TruncateAt.END, false /* leftIsSingleLine */, 760 null /* rightTruncate */, false /* rightIsSingleLine */); 761 getSummaryLeftTextView().setMaxLines(3); 762 } else { 763 setSummaryProperties(TruncateAt.END, true /* leftIsSingleLine */, 764 null /* rightTruncate */, false /* rightIsSingleLine */); 765 getSummaryLeftTextView().setMaxLines(1); 766 } 767 768 super.setDisplayMode(displayMode); 769 } 770 771 @Override updateControlLayout()772 protected void updateControlLayout() { 773 if (!mIsLayoutInitialized) return; 774 775 mBreakdownLayout.setVisibility(mDisplayMode == DISPLAY_MODE_FOCUSED ? VISIBLE : GONE); 776 super.updateControlLayout(); 777 } 778 779 /** 780 * Returns the line item amount at the specified |index|. Returns null if there is no amount 781 * at that index. 782 */ 783 @VisibleForTesting getLineItemAmountForTest(int index)784 public TextView getLineItemAmountForTest(int index) { 785 return mLineItemAmountsForTest.get(index); 786 } 787 788 /** @return The number of line items. */ 789 @VisibleForTesting getNumberOfLineItemsForTest()790 public int getNumberOfLineItemsForTest() { 791 return mLineItemAmountsForTest.size(); 792 } 793 } 794 795 /** 796 * Section that allows selecting one thing from a set of mutually-exclusive options. 797 * 798 * Normal mode: The summary text displays the selected option, and the icon for the option 799 * is displayed in the logo section (if it exists). 800 * If no option is selected, the "empty label" is displayed in its place. 801 * This is important for shipping options (e.g.) because there will be no 802 * option selected by default and a prompt can be displayed. 803 * Expandable mode: Same as Normal, but shows the chevron. 804 * Focused mode: Hides the summary and chevron, then displays the full set of options. 805 * 806 * ............................................................................................. 807 * . TITLE | | . 808 * .................................................................| | . 809 * . LEFT SUMMARY TEXT | RIGHT SUMMARY TEXT | | . 810 * .................................................................| | CHEVRON . 811 * . Descriptive text that spans all three columns because it can. | | or . 812 * . ! Warning text that displays a big scary warning and icon. | LOGO | ADD . 813 * . O Option 1 ICON 1 | Edit Icon | | or . 814 * . O Option 2 ICON 2 | Edit Icon | | CHOOSE . 815 * . O Option 3 ICON 3 | Edit Icon | | . 816 * . + ADD THING | | . 817 * ............................................................................................. 818 */ 819 public static class OptionSection extends PaymentRequestSection { 820 821 private static final int INVALID_OPTION_INDEX = -1; 822 823 private final List<TextView> mLabelsForTest = new ArrayList<>(); 824 private boolean mCanAddItems = true; 825 826 /** 827 * Observer to be notified when the OptionSection changes focus state. 828 */ 829 public interface FocusChangedObserver { 830 /* 831 * Called when the OptionSection view gets or loses focus. 832 * 833 * @param dataType The type of the data contained in the section. 834 * @param willFocus Whether the section is getting the focus. 835 */ onFocusChanged(@aymentRequestUI.DataType int dataType, boolean willFocus)836 void onFocusChanged(@PaymentRequestUI.DataType int dataType, boolean willFocus); 837 } 838 839 /** 840 * Displays a row representing either a selectable option or some flavor text. 841 * 842 * + The "button" is on the left and shows either an icon or a radio button to represent th 843 * row type. 844 * + The "label" is text describing the row. 845 * + The "icon" is a logo representing the option, like a credit card. 846 * + The "edit icon" is a pencil icon with a vertical separator to indicate the option is 847 * editable, clicking on it brings up corresponding editor. 848 */ 849 public class OptionRow { 850 private static final int OPTION_ROW_TYPE_OPTION = 0; 851 private static final int OPTION_ROW_TYPE_ADD = 1; 852 private static final int OPTION_ROW_TYPE_DESCRIPTION = 2; 853 private static final int OPTION_ROW_TYPE_WARNING = 3; 854 855 private final int mRowType; 856 @Nullable 857 private final EditableOption mOption; 858 private final View mButton; 859 private final TextView mLabel; 860 private final View mOptionIcon; 861 private final View mEditIcon; 862 OptionRow(GridLayout parent, int rowIndex, int rowType, @Nullable EditableOption item, boolean isSelected)863 public OptionRow(GridLayout parent, int rowIndex, int rowType, 864 @Nullable EditableOption item, boolean isSelected) { 865 assert item != null || rowType != OPTION_ROW_TYPE_OPTION; 866 boolean optionIconExists = item != null && item.getDrawableIcon() != null; 867 boolean editIconExists = item != null && item.isEditable() && isSelected; 868 boolean isEnabled = item != null && item.isValid(); 869 mRowType = rowType; 870 mOption = item; 871 mButton = createButton(parent, rowIndex, isSelected, isEnabled); 872 mLabel = createLabel(parent, rowIndex, optionIconExists, editIconExists, isEnabled); 873 mOptionIcon = optionIconExists 874 ? createOptionIcon(parent, rowIndex, editIconExists) : null; 875 mEditIcon = editIconExists ? createEditIcon(parent, rowIndex) : null; 876 } 877 878 /** Sets the selected state of this item, alerting the delegate if selected. */ setChecked(boolean isChecked)879 public void setChecked(boolean isChecked) { 880 if (mOption == null) return; 881 882 ((RadioButton) mButton).setChecked(isChecked); 883 if (isChecked) { 884 updateSelectedItem(mOption); 885 mDelegate.onEditableOptionChanged(OptionSection.this, mOption); 886 } 887 } 888 889 /** Returns whether this OptionRow's RadioButton is checked. */ isChecked()890 public boolean isChecked() { 891 return ((RadioButton) mButton).isChecked(); 892 } 893 894 /** Change the label for the row. */ setLabel(int stringId)895 public void setLabel(int stringId) { 896 setLabel(getContext().getString(stringId)); 897 } 898 899 /** Change the label for the row. */ setLabel(CharSequence string)900 public void setLabel(CharSequence string) { 901 mLabel.setText(string); 902 } 903 904 /** Set the button identifier for the option. */ setButtonId(int id)905 public void setButtonId(int id) { 906 mButton.setId(id); 907 } 908 909 /** @return the label for the row. */ 910 @VisibleForTesting getLabelText()911 public CharSequence getLabelText() { 912 return mLabel.getText(); 913 } 914 createButton( GridLayout parent, int rowIndex, boolean isSelected, boolean isEnabled)915 private View createButton( 916 GridLayout parent, int rowIndex, boolean isSelected, boolean isEnabled) { 917 if (mRowType == OPTION_ROW_TYPE_DESCRIPTION) return null; 918 919 Context context = parent.getContext(); 920 View view; 921 922 if (mRowType == OPTION_ROW_TYPE_OPTION) { 923 // Show a radio button indicating whether the EditableOption is selected. 924 RadioButton button = new RadioButton(context); 925 button.setChecked(isSelected && isEnabled); 926 button.setEnabled(isEnabled); 927 view = button; 928 } else { 929 // Show an icon representing the row type, defaulting to the add button. 930 int drawableId; 931 int drawableTint; 932 if (mRowType == OPTION_ROW_TYPE_WARNING) { 933 drawableId = R.drawable.ic_warning_white_24dp; 934 drawableTint = R.color.default_text_color_error; 935 } else { 936 drawableId = R.drawable.plus; 937 drawableTint = R.color.default_icon_color_blue; 938 } 939 940 TintedDrawable tintedDrawable = TintedDrawable.constructTintedDrawable( 941 context, drawableId, drawableTint); 942 ImageButton button = new ImageButton(context); 943 button.setBackground(null); 944 button.setImageDrawable(tintedDrawable); 945 button.setPadding(0, 0, 0, 0); 946 view = button; 947 } 948 949 // The button hugs left. 950 GridLayout.LayoutParams buttonParams = new GridLayout.LayoutParams( 951 GridLayout.spec(rowIndex, 1, GridLayout.CENTER), 952 GridLayout.spec(0, 1, GridLayout.CENTER)); 953 buttonParams.topMargin = mVerticalMargin; 954 MarginLayoutParamsCompat.setMarginEnd(buttonParams, mLargeSpacing); 955 parent.addView(view, buttonParams); 956 957 view.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); 958 view.setOnClickListener(OptionSection.this); 959 return view; 960 } 961 createLabel(GridLayout parent, int rowIndex, boolean optionIconExists, boolean editIconExists, boolean isEnabled)962 private TextView createLabel(GridLayout parent, int rowIndex, boolean optionIconExists, 963 boolean editIconExists, boolean isEnabled) { 964 Context context = parent.getContext(); 965 Resources resources = context.getResources(); 966 967 // By default, the label appears to the right of the "button" in the second column. 968 // + If there is no button, no option and edit icon, the label spans the whole row. 969 // + If there is no option and edit icon, the label spans three columns. 970 // + If there is no edit icon or option icon, the label spans two columns. 971 // + Otherwise, the label occupies only its own column. 972 int columnStart = 1; 973 int columnSpan = 1; 974 if (!optionIconExists) columnSpan++; 975 if (!editIconExists) columnSpan++; 976 977 TextView labelView = new TextView(context); 978 if (mRowType == OPTION_ROW_TYPE_OPTION) { 979 // Show the string representing the EditableOption. 980 labelView.setText(convertOptionToString(mOption, false, /* excludeMainLabel */ 981 mDelegate.isBoldLabelNeeded(OptionSection.this), 982 false /* singleLine */)); 983 labelView.setEnabled(isEnabled); 984 } else if (mRowType == OPTION_ROW_TYPE_ADD) { 985 // Shows string saying that the user can add a new option, e.g. credit card no. 986 int buttonHeight = resources.getDimensionPixelSize( 987 R.dimen.payments_section_add_button_height); 988 989 ApiCompatibilityUtils.setTextAppearance( 990 labelView, R.style.TextAppearance_EditorDialogSectionAddButton); 991 labelView.setMinimumHeight(buttonHeight); 992 labelView.setGravity(Gravity.CENTER_VERTICAL); 993 labelView.setTypeface(UiUtils.createRobotoMediumTypeface()); 994 } else if (mRowType == OPTION_ROW_TYPE_DESCRIPTION) { 995 // The description spans all the columns. 996 columnStart = 0; 997 columnSpan = 4; 998 999 ApiCompatibilityUtils.setTextAppearance( 1000 labelView, R.style.TextAppearance_TextMedium_Secondary); 1001 labelView.setId(R.id.payments_description_label); 1002 } else if (mRowType == OPTION_ROW_TYPE_WARNING) { 1003 // Warnings use three columns. 1004 columnSpan = 3; 1005 ApiCompatibilityUtils.setTextAppearance( 1006 labelView, R.style.TextAppearance_PaymentsUiSectionWarningText); 1007 labelView.setId(R.id.payments_warning_label); 1008 } 1009 1010 // The label spans two columns if no option or edit icon, or spans three columns if 1011 // no option and edit icons. Setting the view width to 0 forces it to stretch. 1012 GridLayout.LayoutParams labelParams = 1013 new GridLayout.LayoutParams(GridLayout.spec(rowIndex, 1, GridLayout.CENTER), 1014 GridLayout.spec(columnStart, columnSpan, GridLayout.FILL, 1f)); 1015 labelParams.topMargin = mVerticalMargin; 1016 labelParams.width = 0; 1017 if (optionIconExists) { 1018 // Margin at the end of the label instead of the start of the option icon to 1019 // allow option icon in the the next row align with the end of label (include 1020 // end margin) when edit icon exits in that row, like below: 1021 // ---Label---------------------[label margin]|---option icon---| 1022 // ---Label---[label margin]|---option icon---|----edit icon----| 1023 MarginLayoutParamsCompat.setMarginEnd(labelParams, mLargeSpacing); 1024 } 1025 parent.addView(labelView, labelParams); 1026 1027 labelView.setOnClickListener(OptionSection.this); 1028 return labelView; 1029 } 1030 createOptionIcon(GridLayout parent, int rowIndex, boolean editIconExists)1031 private View createOptionIcon(GridLayout parent, int rowIndex, boolean editIconExists) { 1032 // The icon has a pre-defined width. 1033 ImageView optionIcon = new ImageView(parent.getContext()); 1034 optionIcon.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); 1035 if (mOption.isEditable()) { 1036 optionIcon.setMaxWidth(mEditableOptionIconMaxWidth); 1037 } else { 1038 optionIcon.setMaxWidth(mNonEditableOptionIconMaxWidth); 1039 } 1040 optionIcon.setAdjustViewBounds(true); 1041 optionIcon.setImageDrawable(mOption.getDrawableIcon()); 1042 1043 // Place option icon at column three if no edit icon. 1044 int columnStart = editIconExists ? 2 : 3; 1045 GridLayout.LayoutParams iconParams = 1046 new GridLayout.LayoutParams(GridLayout.spec(rowIndex, 1, GridLayout.CENTER), 1047 GridLayout.spec(columnStart, 1, GridLayout.CENTER)); 1048 iconParams.topMargin = mVerticalMargin; 1049 parent.addView(optionIcon, iconParams); 1050 1051 optionIcon.setOnClickListener(OptionSection.this); 1052 return optionIcon; 1053 } 1054 createEditIcon(GridLayout parent, int rowIndex)1055 private View createEditIcon(GridLayout parent, int rowIndex) { 1056 View editorIcon = LayoutInflater.from(parent.getContext()) 1057 .inflate(R.layout.payment_option_edit_icon, null); 1058 1059 // The icon floats to the right of everything. 1060 GridLayout.LayoutParams iconParams = 1061 new GridLayout.LayoutParams(GridLayout.spec(rowIndex, 1, GridLayout.CENTER), 1062 GridLayout.spec(3, 1, GridLayout.CENTER)); 1063 iconParams.topMargin = mVerticalMargin; 1064 parent.addView(editorIcon, iconParams); 1065 1066 editorIcon.setOnClickListener(OptionSection.this); 1067 return editorIcon; 1068 } 1069 1070 /** Returns the edit icon for the option row. */ 1071 @VisibleForTesting getEditIconForTest()1072 public View getEditIconForTest() { 1073 return mEditIcon; 1074 } 1075 } 1076 1077 /** Top and bottom margins for each item. */ 1078 private final int mVerticalMargin; 1079 1080 /** All the possible EditableOptions in Layout form, then one row for adding new options. */ 1081 private final ArrayList<OptionRow> mOptionRows = new ArrayList<>(); 1082 1083 /** Width that the editable option icon takes. */ 1084 private final int mEditableOptionIconMaxWidth; 1085 1086 /** Width that the non editable option icon takes. */ 1087 private final int mNonEditableOptionIconMaxWidth; 1088 1089 /** Layout containing all the {@link OptionRow}s. */ 1090 private GridLayout mOptionLayout; 1091 1092 /** A spinner to show when the user selection is being checked. */ 1093 private View mCheckingProgress; 1094 1095 /** SectionInformation that is used to populate the views in this section. */ 1096 private SectionInformation mSectionInformation; 1097 1098 /** Indicates whether the summary is displayed in a single line. */ 1099 private boolean mSummaryInSingleLine; 1100 1101 /** 1102 * Indicates whether the summary is set to display in a single line in DISPLAY_MODE_NORMAL 1103 * by caller. 1104 */ 1105 private boolean mSetDisplaySummaryInSingleLineInNormalMode = true; 1106 1107 /** 1108 * Indicates whether the summary should be split to display in left and right summary 1109 * text views in {@link DISPLAY_MODE_NORMAL}. 1110 */ 1111 private boolean mSplitSummaryInDisplayModeNormal; 1112 1113 /** Indicates whether the summary is set to descriptive or title text style. */ 1114 private boolean mSummaryInDescriptiveText; 1115 1116 private FocusChangedObserver mFocusChangedObserver; 1117 1118 /** 1119 * Constructs an OptionSection. 1120 * 1121 * @param context Context to pull resources from. 1122 * @param sectionName Title of the section to display. 1123 * @param delegate Delegate to alert when something changes in the dialog. 1124 */ OptionSection(Context context, String sectionName, SectionDelegate delegate)1125 public OptionSection(Context context, String sectionName, SectionDelegate delegate) { 1126 super(context, sectionName, delegate); 1127 mVerticalMargin = context.getResources().getDimensionPixelSize( 1128 R.dimen.editor_dialog_section_small_spacing); 1129 mEditableOptionIconMaxWidth = context.getResources().getDimensionPixelSize( 1130 R.dimen.editable_option_section_logo_width); 1131 mNonEditableOptionIconMaxWidth = 1132 context.getResources().getDimensionPixelSize(R.dimen.payments_favicon_size); 1133 setSummaryText(null, null); 1134 } 1135 1136 /** 1137 * Registers the delegate to be notified when this OptionSection gains or loses focus. 1138 * 1139 * @param delegate The delegate to notify. 1140 */ setOptionSectionFocusChangedObserver(FocusChangedObserver observer)1141 public void setOptionSectionFocusChangedObserver(FocusChangedObserver observer) { 1142 mFocusChangedObserver = observer; 1143 } 1144 1145 @Override handleClick(View v)1146 public void handleClick(View v) { 1147 for (int i = 0; i < mOptionRows.size(); i++) { 1148 OptionRow row = mOptionRows.get(i); 1149 boolean clickedSelect = row.mButton == v || row.mLabel == v || row.mOptionIcon == v; 1150 // Handle click on the "ADD THING" button. 1151 if (row.mOption == null && clickedSelect) { 1152 mDelegate.onAddEditableOption(this); 1153 return; 1154 } 1155 1156 // Handle click on the edit icon. 1157 if (row.mOption != null && row.mEditIcon == v) { 1158 mDelegate.onEditEditableOption(this, row.mOption); 1159 return; 1160 } 1161 } 1162 1163 // Update the radio button state: checked/unchecked. 1164 for (int i = 0; i < mOptionRows.size(); i++) { 1165 OptionRow row = mOptionRows.get(i); 1166 boolean clickedSelect = row.mButton == v || row.mLabel == v || row.mOptionIcon == v; 1167 if (row.mOption != null) row.setChecked(clickedSelect); 1168 } 1169 } 1170 1171 @Override focusSection(boolean shouldFocus)1172 public void focusSection(boolean shouldFocus) { 1173 // Override expansion of the section if there's no options to show. 1174 boolean mayFocus = mSectionInformation != null && mSectionInformation.getSize() > 0; 1175 if (!mayFocus && shouldFocus) { 1176 setDisplayMode(PaymentRequestSection.DISPLAY_MODE_NORMAL); 1177 return; 1178 } 1179 1180 // Notify the observer that the focus is going to change. 1181 if (mFocusChangedObserver != null) { 1182 mFocusChangedObserver.onFocusChanged( 1183 mSectionInformation.getDataType(), shouldFocus); 1184 } 1185 1186 int previousDisplayMode = mDisplayMode; 1187 super.focusSection(shouldFocus); 1188 1189 // Update summary when display mode changed from DISPLAY_MODE_NORMAL to other modes. 1190 if (mSectionInformation != null && previousDisplayMode == DISPLAY_MODE_NORMAL) { 1191 updateSelectedItem(mSectionInformation.getSelectedItem()); 1192 } 1193 } 1194 1195 @Override isLogoNecessary()1196 protected boolean isLogoNecessary() { 1197 return true; 1198 } 1199 1200 @Override createMainSectionContent(LinearLayout mainSectionLayout)1201 protected void createMainSectionContent(LinearLayout mainSectionLayout) { 1202 Context context = mainSectionLayout.getContext(); 1203 mCheckingProgress = createLoadingSpinner(); 1204 1205 mOptionLayout = new GridLayout(context); 1206 mOptionLayout.setColumnCount(4); 1207 mainSectionLayout.addView(mOptionLayout, new LinearLayout.LayoutParams( 1208 LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); 1209 } 1210 1211 /** @param canAddItems If false, this section will not show [+ ADD THING] button. */ setCanAddItems(boolean canAddItems)1212 public void setCanAddItems(boolean canAddItems) { 1213 mCanAddItems = canAddItems; 1214 } 1215 1216 /** 1217 * @param singleLine If true, sets the summary text to display in a single line 1218 * in {@link #DISPLAY_MODE_NORMAL} when there is a valid selected 1219 * option, otherwise sets the summary text to display in multiple lines. 1220 */ setDisplaySummaryInSingleLineInNormalMode(boolean singleLine)1221 public void setDisplaySummaryInSingleLineInNormalMode(boolean singleLine) { 1222 mSetDisplaySummaryInSingleLineInNormalMode = singleLine; 1223 } 1224 1225 /** 1226 * Specify whether the summary should be split to display under DISPLAY_MODE_NORMAL. 1227 * 1228 * @param splitSummary If true split the display of summary in the left and right 1229 * text views in {@link DISPLAY_MODE_NORMAL}, the summary is 1230 * split into 'label' and the rest('sublabel', 'Tertiary label'). 1231 * Otherwise the entire summary is displayed in the left text view. 1232 */ setSplitSummaryInDisplayModeNormal(boolean splitSummary)1233 public void setSplitSummaryInDisplayModeNormal(boolean splitSummary) { 1234 mSplitSummaryInDisplayModeNormal = splitSummary; 1235 } 1236 1237 /** Updates the View to account for the new {@link SectionInformation} being passed in. */ update(SectionInformation information)1238 public void update(SectionInformation information) { 1239 mSectionInformation = information; 1240 EditableOption selectedItem = information.getSelectedItem(); 1241 updateSelectedItem(selectedItem); 1242 updateOptionList(information, selectedItem); 1243 updateControlLayout(); 1244 } 1245 createLoadingSpinner()1246 private View createLoadingSpinner() { 1247 ViewGroup spinnyLayout = (ViewGroup) LayoutInflater.from(getContext()).inflate( 1248 R.layout.payment_request_spinny, null); 1249 1250 TextView textView = (TextView) spinnyLayout.findViewById(R.id.message); 1251 textView.setText(getContext().getString(R.string.payments_checking_option)); 1252 1253 return spinnyLayout; 1254 } 1255 setSpinnerVisibility(boolean visibility)1256 private void setSpinnerVisibility(boolean visibility) { 1257 if (visibility) { 1258 if (mCheckingProgress.getParent() != null) return; 1259 1260 ViewGroup parent = (ViewGroup) mOptionLayout.getParent(); 1261 int optionLayoutIndex = parent.indexOfChild(mOptionLayout); 1262 parent.addView(mCheckingProgress, optionLayoutIndex); 1263 1264 MarginLayoutParams params = 1265 (MarginLayoutParams) mCheckingProgress.getLayoutParams(); 1266 params.width = LayoutParams.MATCH_PARENT; 1267 params.height = LayoutParams.WRAP_CONTENT; 1268 params.bottomMargin = getContext().getResources().getDimensionPixelSize( 1269 R.dimen.payments_section_checking_spacing); 1270 mCheckingProgress.requestLayout(); 1271 } else { 1272 if (mCheckingProgress.getParent() == null) return; 1273 1274 ViewGroup parent = (ViewGroup) mCheckingProgress.getParent(); 1275 parent.removeView(mCheckingProgress); 1276 } 1277 } 1278 1279 @Override updateControlLayout()1280 protected void updateControlLayout() { 1281 if (!mIsLayoutInitialized) return; 1282 1283 if (mDisplayMode == DISPLAY_MODE_FOCUSED) { 1284 setIsSummaryAllowed(false); 1285 mOptionLayout.setVisibility(VISIBLE); 1286 setSpinnerVisibility(false); 1287 } else if (mDisplayMode == DISPLAY_MODE_CHECKING) { 1288 setIsSummaryAllowed(false); 1289 mOptionLayout.setVisibility(GONE); 1290 setSpinnerVisibility(true); 1291 } else { 1292 setIsSummaryAllowed(true); 1293 mOptionLayout.setVisibility(GONE); 1294 setSpinnerVisibility(false); 1295 } 1296 1297 super.updateControlLayout(); 1298 } 1299 1300 @Override getEditButtonState()1301 public int getEditButtonState() { 1302 if (mSectionInformation == null) return EDIT_BUTTON_GONE; 1303 1304 if (mSectionInformation.getSize() == 0 && mCanAddItems) { 1305 // There aren't any EditableOptions. Ask the user to add a new one. 1306 return EDIT_BUTTON_ADD; 1307 } else if (mSectionInformation.getSelectedItem() == null) { 1308 // The user hasn't selected any available EditableOptions. Ask the user to pick 1309 // one. 1310 return EDIT_BUTTON_CHOOSE; 1311 } else { 1312 return EDIT_BUTTON_GONE; 1313 } 1314 } 1315 updateSelectedItem(EditableOption selectedItem)1316 private void updateSelectedItem(EditableOption selectedItem) { 1317 // Only left TextView in the summary section is used in this section. 1318 // Summary is displayed in multiple lines by default unless: 1319 // 1. nothing is selected or 1320 // 2. the display mode is DISPLAY_MODE_NORMAL without caller explicitly set to display 1321 // summary in multiple lines. 1322 if (selectedItem == null 1323 || (mDisplayMode == DISPLAY_MODE_NORMAL 1324 && mSetDisplaySummaryInSingleLineInNormalMode)) { 1325 if (!mSummaryInSingleLine) { 1326 setSummaryProperties(TruncateAt.END, true /* leftIsSingleLine */, 1327 null /* rightTruncate */, false /* rightIsSingleLine */); 1328 mSummaryInSingleLine = true; 1329 } 1330 } else if (mSummaryInSingleLine) { 1331 setSummaryProperties(null /* leftTruncate */, false /* leftIsSingleLine */, 1332 null /* rightTruncate */, false /* rightIsSingleLine */); 1333 mSummaryInSingleLine = false; 1334 } 1335 1336 if (selectedItem == null) { 1337 setLogoDrawable(null); 1338 // Section summary should be displayed as descriptive text style. 1339 if (!mSummaryInDescriptiveText) { 1340 ApiCompatibilityUtils.setTextAppearance( 1341 getSummaryLeftTextView(), R.style.TextAppearance_TextMedium_Secondary); 1342 mSummaryInDescriptiveText = true; 1343 } 1344 SectionUiUtils.showSectionSummaryInTextViewInSingeLine( 1345 getContext(), mSectionInformation, getSummaryLeftTextView()); 1346 } else { 1347 setLogoDrawable(selectedItem.getDrawableIcon()); 1348 // Selected item summary should be displayed as 1349 // R.style.TextAppearance_TextLarge_Primary. 1350 if (mSummaryInDescriptiveText) { 1351 ApiCompatibilityUtils.setTextAppearance( 1352 getSummaryLeftTextView(), R.style.TextAppearance_TextLarge_Primary); 1353 mSummaryInDescriptiveText = false; 1354 } 1355 // Split summary in DISPLAY_MODE_NORMAL if caller specified. The first part is 1356 // displayed on the left summary text view aligned to the left. The second part is 1357 // displayed on the right summary text view aligned to the right. 1358 boolean splitSummary = 1359 mSplitSummaryInDisplayModeNormal && (mDisplayMode == DISPLAY_MODE_NORMAL); 1360 if (splitSummary) { 1361 setSummaryText(selectedItem.getLabel(), 1362 convertOptionToString(selectedItem, true /* excludeMainLabel */, 1363 false /* useBoldLabel */, mSummaryInSingleLine)); 1364 } else { 1365 setSummaryText(convertOptionToString(selectedItem, false /* excludeMainLabel */, 1366 false /* useBoldLabel */, mSummaryInSingleLine), 1367 null); 1368 } 1369 } 1370 1371 updateControlLayout(); 1372 } 1373 updateOptionList(SectionInformation information, EditableOption selectedItem)1374 private void updateOptionList(SectionInformation information, EditableOption selectedItem) { 1375 mOptionLayout.removeAllViews(); 1376 mOptionRows.clear(); 1377 mLabelsForTest.clear(); 1378 1379 // Show any additional text requested by the layout. 1380 String additionalText = mDelegate.getAdditionalText(this); 1381 if (!TextUtils.isEmpty(additionalText)) { 1382 OptionRow descriptionRow = new OptionRow(mOptionLayout, 1383 mOptionRows.size(), 1384 mDelegate.isAdditionalTextDisplayingWarning(this) 1385 ? OptionRow.OPTION_ROW_TYPE_WARNING 1386 : OptionRow.OPTION_ROW_TYPE_DESCRIPTION, 1387 null, false); 1388 mOptionRows.add(descriptionRow); 1389 descriptionRow.setLabel(additionalText); 1390 } 1391 1392 // List out known payment options. 1393 int firstOptionIndex = INVALID_OPTION_INDEX; 1394 for (int i = 0; i < information.getSize(); i++) { 1395 int currentRow = mOptionRows.size(); 1396 if (firstOptionIndex == INVALID_OPTION_INDEX) firstOptionIndex = currentRow; 1397 1398 EditableOption item = information.getItem(i); 1399 OptionRow currentOptionRow = new OptionRow(mOptionLayout, currentRow, 1400 OptionRow.OPTION_ROW_TYPE_OPTION, item, item == selectedItem); 1401 mOptionRows.add(currentOptionRow); 1402 1403 // For testing, keep the labels in a list for easy access. 1404 mLabelsForTest.add(currentOptionRow.mLabel); 1405 } 1406 1407 // TODO(crbug.com/627186): Find another way to give access to this resource in tests. 1408 // For testing. 1409 if (firstOptionIndex != INVALID_OPTION_INDEX) { 1410 mOptionRows.get(firstOptionIndex).setButtonId(R.id.payments_first_radio_button); 1411 } 1412 1413 // If the user is allowed to add new options, show the button for it. 1414 if (information.getAddStringId() != 0 && mCanAddItems) { 1415 OptionRow addRow = new OptionRow(mOptionLayout, mOptionLayout.getChildCount(), 1416 OptionRow.OPTION_ROW_TYPE_ADD, null, false); 1417 addRow.setLabel(information.getAddStringId()); 1418 addRow.setButtonId(R.id.payments_add_option_button); 1419 mOptionRows.add(addRow); 1420 } 1421 } 1422 convertOptionToString(EditableOption item, boolean excludeMainLabel, boolean useBoldLabel, boolean singleLine)1423 private CharSequence convertOptionToString(EditableOption item, boolean excludeMainLabel, 1424 boolean useBoldLabel, boolean singleLine) { 1425 SpannableStringBuilder builder = new SpannableStringBuilder(); 1426 if (!excludeMainLabel) { 1427 builder.append(item.getLabel()); 1428 if (useBoldLabel) { 1429 builder.setSpan( 1430 new StyleSpan(android.graphics.Typeface.BOLD), 0, builder.length(), 0); 1431 } 1432 } 1433 1434 String labelSeparator = singleLine 1435 ? getContext().getString(R.string.autofill_address_summary_separator) 1436 : "\n"; 1437 if (!TextUtils.isEmpty(item.getSublabel())) { 1438 if (builder.length() > 0) builder.append(labelSeparator); 1439 builder.append(item.getSublabel()); 1440 } 1441 1442 if (!TextUtils.isEmpty(item.getTertiaryLabel())) { 1443 if (builder.length() > 0) builder.append(labelSeparator); 1444 builder.append(item.getTertiaryLabel()); 1445 } 1446 1447 if (!TextUtils.isEmpty(item.getPromoMessage())) { 1448 if (builder.length() > 0) builder.append(labelSeparator); 1449 builder.append(item.getPromoMessage()); 1450 } 1451 1452 if (!item.isComplete() && !TextUtils.isEmpty(item.getEditMessage())) { 1453 if (builder.length() > 0) builder.append(labelSeparator); 1454 String editMessage = item.getEditMessage(); 1455 builder.append(editMessage); 1456 Object foregroundSpanner = new ForegroundColorSpan(ApiCompatibilityUtils.getColor( 1457 getContext().getResources(), R.color.default_text_color_link)); 1458 Object sizeSpanner = new AbsoluteSizeSpan(14, true); 1459 int startIndex = builder.length() - editMessage.length(); 1460 builder.setSpan(foregroundSpanner, startIndex, builder.length(), 0); 1461 builder.setSpan(sizeSpanner, startIndex, builder.length(), 0); 1462 } 1463 1464 return builder; 1465 } 1466 1467 /** 1468 * Returns the label at the specified |labelIndex|. Returns null if there is no label at 1469 * that index. 1470 */ 1471 @VisibleForTesting getOptionLabelsForTest(int labelIndex)1472 public TextView getOptionLabelsForTest(int labelIndex) { 1473 return mLabelsForTest.get(labelIndex); 1474 } 1475 1476 /** 1477 * Returns the label of the section summary. 1478 */ 1479 @VisibleForTesting getLeftSummaryLabelForTest()1480 public TextView getLeftSummaryLabelForTest() { 1481 return getSummaryLeftTextView(); 1482 } 1483 1484 /** 1485 * Returns the right summary text view. 1486 */ 1487 @VisibleForTesting getRightSummaryLabelForTest()1488 public TextView getRightSummaryLabelForTest() { 1489 return getSummaryRightTextView(); 1490 } 1491 1492 /** Returns the number of option labels. */ 1493 @VisibleForTesting getNumberOfOptionLabelsForTest()1494 public int getNumberOfOptionLabelsForTest() { 1495 return mLabelsForTest.size(); 1496 } 1497 1498 /** Returns the OptionRow at the specified |index|. */ 1499 @VisibleForTesting getOptionRowAtIndex(int index)1500 public OptionRow getOptionRowAtIndex(int index) { 1501 return mOptionRows.get(index); 1502 } 1503 } 1504 1505 /** 1506 * Drawn as a 1dp separator. Initially drawn without being expanded to the full width of the 1507 * UI, but can be expanded to separate sections fully. 1508 */ 1509 public static class SectionSeparator extends View { 1510 /** Creates the View and adds it to the parent. */ SectionSeparator(ViewGroup parent)1511 public SectionSeparator(ViewGroup parent) { 1512 this(parent, -1); 1513 } 1514 1515 /** Creates the View and adds it to the parent at the given index. */ SectionSeparator(ViewGroup parent, int index)1516 public SectionSeparator(ViewGroup parent, int index) { 1517 super(parent.getContext()); 1518 Resources resources = parent.getContext().getResources(); 1519 setBackground(HorizontalListDividerDrawable.create(getContext())); 1520 LinearLayout.LayoutParams params = 1521 new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, 1522 resources.getDimensionPixelSize(R.dimen.divider_height)); 1523 1524 int margin = 1525 resources.getDimensionPixelSize(R.dimen.editor_dialog_section_large_spacing); 1526 MarginLayoutParamsCompat.setMarginStart(params, margin); 1527 MarginLayoutParamsCompat.setMarginEnd(params, margin); 1528 parent.addView(this, index, params); 1529 } 1530 1531 /** Expand the separator to be the full width of the dialog. */ expand()1532 public void expand() { 1533 LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) getLayoutParams(); 1534 MarginLayoutParamsCompat.setMarginStart(params, 0); 1535 MarginLayoutParamsCompat.setMarginEnd(params, 0); 1536 } 1537 } 1538 } 1539