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