org.chromium.chrome.browser.payments.ui.PaymentRequestSection.java Source code

Java tutorial

Introduction

Here is the source code for org.chromium.chrome.browser.payments.ui.PaymentRequestSection.java

Source

// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.payments.ui;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.support.v4.view.animation.LinearOutSlowInInterpolator;
import android.support.v7.widget.GridLayout;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.TextUtils.TruncateAt;
import android.text.style.AbsoluteSizeSpan;
import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RadioButton;
import android.widget.TextView;

import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.widget.DualControlLayout;
import org.chromium.chrome.browser.widget.TintedDrawable;

import java.util.ArrayList;
import java.util.List;

import javax.annotation.Nullable;

/**
 * Represents a single section in the {@link PaymentRequestUI} that flips between multiple states.
 *
 * The row is broken up into three major, vertically-centered sections:
 * .............................................................................................
 * . TITLE                                                          |                | CHEVRON .
 * .................................................................|                |    or   .
 * . LEFT SUMMARY TEXT                        |  RIGHT SUMMARY TEXT |           LOGO |   ADD   .
 * .................................................................|                |    or   .
 * . MAIN SECTION CONTENT                                           |                |  SELECT .
 * .............................................................................................
 *
 * 1) MAIN CONTENT
 *    The main content is on the left side of the UI.  This includes the title of the section and
 *    two bits of optional summary text.  Subclasses may extend this class to append more controls
 *    via the {@link #createMainSectionContent} function.
 *
 * 2) LOGO
 *    Displays an optional logo (e.g. a credit card image) that floats to the right of the main
 *    content.
 *
 * 3) CHEVRON or ADD or SELECT
 *    Drawn to indicate that the current section may be expanded.  Displayed only when the view is
 *    in the {@link #DISPLAY_MODE_EXPANDABLE} state and only if an ADD or SELECT button isn't shown.
 *
 * There are three states that the UI may flip between; see {@link #DISPLAY_MODE_NORMAL},
 * {@link #DISPLAY_MODE_EXPANDABLE}, and {@link #DISPLAY_MODE_FOCUSED} for details.
 */
public abstract class PaymentRequestSection extends LinearLayout implements View.OnClickListener {
    public static final String TAG = "PaymentRequestUI";

    /** Handles clicks on the widgets and providing data to the PaymentsRequestSection. */
    public interface SectionDelegate extends View.OnClickListener {
        /**
         * Called when the user selects a radio button option from an {@link OptionSection}.
         *
         * @param section Section that was changed.
         * @param option  {@link PaymentOption} that was selected.
         */
        void onPaymentOptionChanged(PaymentRequestSection section, PaymentOption option);

        /** Called when the user clicks the edit icon of the selected PaymentOption. */
        void onEditPaymentOption(PaymentRequestSection section, PaymentOption option);

        /** Called when the user requests adding a new PaymentOption to a given section. */
        void onAddPaymentOption(PaymentRequestSection section);

        /** Checks whether or not the text should be formatted with a bold label. */
        boolean isBoldLabelNeeded(PaymentRequestSection section);

        /** Checks whether or not the user should be allowed to click on controls. */
        boolean isAcceptingUserInput();

        /** Returns any additional text that needs to be displayed. */
        @Nullable
        String getAdditionalText(PaymentRequestSection section);

        /** Returns true if the additional text should be stylized as a warning instead of info. */
        boolean isAdditionalTextDisplayingWarning(PaymentRequestSection section);

        /** Called when a section has been clicked. */
        void onSectionClicked(PaymentRequestSection section);
    }

    /** Edit button mode: Hide the button. */
    public static final int EDIT_BUTTON_GONE = 0;

    /** Edit button mode: Indicate that the section requires a selection. */
    public static final int EDIT_BUTTON_SELECT = 1;

    /** Edit button mode: Indicate that the section requires adding an option. */
    public static final int EDIT_BUTTON_ADD = 2;

    /** Normal mode: White background, displays the item assuming the user accepts it as is. */
    static final int DISPLAY_MODE_NORMAL = 3;

    /** Editable mode: White background, displays the item with an edit chevron. */
    static final int DISPLAY_MODE_EXPANDABLE = 4;

    /** Focused mode: Gray background, more padding, no edit chevron. */
    static final int DISPLAY_MODE_FOCUSED = 5;

    /** Checking mode: Gray background, spinner overlay hides everything except the title. */
    static final int DISPLAY_MODE_CHECKING = 6;

    protected final SectionDelegate mDelegate;
    protected final int mLargeSpacing;
    protected final Button mEditButtonView;
    protected final boolean mIsLayoutInitialized;

    protected int mDisplayMode = DISPLAY_MODE_NORMAL;

    private final int mVerticalSpacing;
    private final int mFocusedBackgroundColor;
    private final LinearLayout mMainSection;
    private final ImageView mLogoView;
    private final ImageView mChevronView;

    private TextView mTitleView;
    private LinearLayout mSummaryLayout;
    private TextView mSummaryLeftTextView;
    private TextView mSummaryRightTextView;

    private Drawable mLogo;
    private boolean mIsSummaryAllowed = true;

    /**
     * Constructs a PaymentRequestSection.
     *
     * @param context     Context to pull resources from.
     * @param sectionName Title of the section to display.
     * @param delegate    Delegate to alert when something changes in the dialog.
     */
    private PaymentRequestSection(Context context, String sectionName, SectionDelegate delegate) {
        super(context);
        mDelegate = delegate;
        setOnClickListener(delegate);
        setOrientation(HORIZONTAL);
        setGravity(Gravity.CENTER_VERTICAL);

        // Set the styling of the view.
        mFocusedBackgroundColor = ApiCompatibilityUtils.getColor(getResources(),
                R.color.payments_section_edit_background);
        mLargeSpacing = getResources().getDimensionPixelSize(R.dimen.payments_section_large_spacing);
        mVerticalSpacing = getResources().getDimensionPixelSize(R.dimen.payments_section_vertical_spacing);
        setPadding(mLargeSpacing, mVerticalSpacing, mLargeSpacing, mVerticalSpacing);

        // Create the main content.
        mMainSection = prepareMainSection(sectionName);
        mLogoView = isLogoNecessary() ? createAndAddLogoView(this, mLargeSpacing) : null;
        mEditButtonView = createAndAddEditButton(this);
        mChevronView = createAndAddChevron(this);
        mIsLayoutInitialized = true;
        setDisplayMode(DISPLAY_MODE_NORMAL);
    }

    /**
     * Sets what logo should be displayed.
     *
     * @param logo       The logo to display.
     * @param drawBorder Whether draw border background for the logo.
     */
    protected void setLogoDrawable(Drawable logo, boolean drawBorder) {
        assert isLogoNecessary();
        mLogo = logo;

        if (drawBorder) {
            mLogoView.setBackgroundResource(R.drawable.payments_ui_logo_bg);
        } else {
            mLogoView.setBackgroundResource(0);
        }
        mLogoView.setImageDrawable(mLogo);
    }

    /** Returns the LinearLayout containing the summary texts of the section. */
    protected LinearLayout getSummaryLayout() {
        assert mSummaryLayout != null;
        return mSummaryLayout;
    }

    /** Returns the right summary TextView. */
    protected TextView getSummaryRightTextView() {
        assert mSummaryRightTextView != null;
        return mSummaryRightTextView;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        // Allow touches to propagate to children only if the layout can be interacted with.
        return !mDelegate.isAcceptingUserInput();
    }

    @Override
    public final void onClick(View v) {
        if (!mDelegate.isAcceptingUserInput())
            return;

        // Handle clicking on "ADD" or "SELECT".
        if (v == mEditButtonView) {
            if (getEditButtonState() == EDIT_BUTTON_ADD) {
                mDelegate.onAddPaymentOption(this);
            } else {
                mDelegate.onSectionClicked(this);
            }
            return;
        }

        handleClick(v);
        updateControlLayout();
    }

    /** Handles clicks on the PaymentRequestSection. */
    protected void handleClick(View v) {
    }

    /**
     * Called when the UI is telling the section that it has either gained or lost focus.
     */
    public void focusSection(boolean shouldFocus) {
        setDisplayMode(shouldFocus ? DISPLAY_MODE_FOCUSED : DISPLAY_MODE_EXPANDABLE);
    }

    /**
     * Updates what Views are displayed and how they look.
     *
     * @param displayMode What mode the widget is being displayed in.
     */
    public void setDisplayMode(int displayMode) {
        mDisplayMode = displayMode;
        updateControlLayout();
    }

    /**
     * Changes what is being displayed in the summary.
     *
     * @param leftText  Text to display on the left side.  If null, the whole row hides.
     * @param rightText Text to display on the right side.  If null, only the right View hides.
     */
    public void setSummaryText(@Nullable CharSequence leftText, @Nullable CharSequence rightText) {
        mSummaryLeftTextView.setText(leftText);
        mSummaryRightTextView.setText(rightText);
        mSummaryRightTextView.setVisibility(TextUtils.isEmpty(rightText) ? GONE : VISIBLE);
        updateControlLayout();
    }

    /**
     * Sets how the summary text should be displayed.
     *
     * @param leftTruncate      How to truncate the left summary text.  Set to null to clear.
     * @param leftIsSingleLine  Whether the left summary text should be a single line.
     * @param rightTruncate     How to truncate the right summary text.  Set to null to clear.
     * @param rightIsSingleLine Whether the right summary text should be a single line.
     */
    public void setSummaryProperties(@Nullable TruncateAt leftTruncate, boolean leftIsSingleLine,
            @Nullable TruncateAt rightTruncate, boolean rightIsSingleLine) {
        mSummaryLeftTextView.setEllipsize(leftTruncate);
        mSummaryLeftTextView.setSingleLine(leftIsSingleLine);

        mSummaryRightTextView.setEllipsize(rightTruncate);
        mSummaryRightTextView.setSingleLine(rightIsSingleLine);
    }

    /**
     * Subclasses may override this method to add additional controls to the layout.
     *
     * @param mainSectionLayout Layout containing all of the main content of the section.
     */
    protected abstract void createMainSectionContent(LinearLayout mainSectionLayout);

    /**
     * Sets whether the edit button may be interacted with.
     *
     * @param isEnabled Whether the button may be interacted with.
     */
    public void setIsEditButtonEnabled(boolean isEnabled) {
        mEditButtonView.setEnabled(isEnabled);
    }

    /**
     * Sets whether the summary text can be displayed.
     *
     * @param isAllowed Whether to display the summary text when needed.
     */
    protected void setIsSummaryAllowed(boolean isAllowed) {
        mIsSummaryAllowed = isAllowed;
    }

    /** @return Whether or not the logo should be displayed. */
    protected boolean isLogoNecessary() {
        return false;
    }

    /**
     * Returns the state of the edit button, which is hidden by default.
     *
     * @return State of the edit button.
     */
    public int getEditButtonState() {
        return EDIT_BUTTON_GONE;
    }

    /**
     * Creates the main section.  Subclasses must call super#createMainSection() immediately to
     * guarantee that Views are added in the correct order.
     *
     * @param sectionName Title to display for the section.
     */
    private LinearLayout prepareMainSection(String sectionName) {
        // The main section is a vertical linear layout that subclasses can append to.
        LinearLayout mainSectionLayout = new LinearLayout(getContext());
        mainSectionLayout.setOrientation(VERTICAL);
        LinearLayout.LayoutParams mainParams = new LayoutParams(0, LayoutParams.WRAP_CONTENT);
        mainParams.weight = 1;
        addView(mainSectionLayout, mainParams);

        // The title is always displayed for the row at the top of the main section.
        mTitleView = new TextView(getContext());
        mTitleView.setText(sectionName);
        ApiCompatibilityUtils.setTextAppearance(mTitleView, R.style.PaymentsUiSectionHeader);
        mainSectionLayout.addView(mTitleView,
                new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));

        // Create the two TextViews for showing the summary text.
        mSummaryLeftTextView = new TextView(getContext());
        mSummaryLeftTextView.setId(R.id.payments_left_summary_label);
        ApiCompatibilityUtils.setTextAppearance(mSummaryLeftTextView, R.style.PaymentsUiSectionDefaultText);

        mSummaryRightTextView = new TextView(getContext());
        ApiCompatibilityUtils.setTextAppearance(mSummaryRightTextView, R.style.PaymentsUiSectionDefaultText);
        ApiCompatibilityUtils.setTextAlignment(mSummaryRightTextView, TEXT_ALIGNMENT_TEXT_END);

        // The main TextView sucks up all the available space.
        LinearLayout.LayoutParams leftLayoutParams = new LinearLayout.LayoutParams(0, LayoutParams.WRAP_CONTENT);
        leftLayoutParams.weight = 1;

        LinearLayout.LayoutParams rightLayoutParams = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT,
                LayoutParams.WRAP_CONTENT);
        ApiCompatibilityUtils.setMarginStart(rightLayoutParams,
                getContext().getResources().getDimensionPixelSize(R.dimen.payments_section_small_spacing));

        // The summary section displays up to two TextViews side by side.
        mSummaryLayout = new LinearLayout(getContext());
        mSummaryLayout.addView(mSummaryLeftTextView, leftLayoutParams);
        mSummaryLayout.addView(mSummaryRightTextView, rightLayoutParams);
        mainSectionLayout.addView(mSummaryLayout,
                new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
        setSummaryText(null, null);

        createMainSectionContent(mainSectionLayout);
        return mainSectionLayout;
    }

    private static ImageView createAndAddLogoView(ViewGroup parent, int startMargin) {
        ImageView view = new ImageView(parent.getContext());
        view.setMaxWidth(
                parent.getContext().getResources().getDimensionPixelSize(R.dimen.payments_section_logo_width));
        view.setAdjustViewBounds(true);

        // The logo has a pre-defined height and width.
        LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
        ApiCompatibilityUtils.setMarginStart(params, startMargin);
        parent.addView(view, params);
        return view;
    }

    private Button createAndAddEditButton(ViewGroup parent) {
        Resources resources = parent.getResources();
        Button view = DualControlLayout.createButtonForLayout(parent.getContext(), true,
                resources.getString(R.string.select), this);
        view.setId(R.id.payments_section);

        LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
        ApiCompatibilityUtils.setMarginStart(params, mLargeSpacing);
        parent.addView(view, params);
        return view;
    }

    private ImageView createAndAddChevron(ViewGroup parent) {
        Resources resources = parent.getResources();
        TintedDrawable chevron = TintedDrawable.constructTintedDrawable(resources, R.drawable.ic_expanded,
                R.color.payments_section_chevron);

        ImageView view = new ImageView(parent.getContext());
        view.setImageDrawable(chevron);

        // Wrap whatever image is passed in.
        LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
        ApiCompatibilityUtils.setMarginStart(params, mLargeSpacing);
        parent.addView(view, params);
        return view;
    }

    /**
     * Called when the section's controls need to be updated after configuration changes.
     *
     * Because of the complicated special casing of what controls hide other controls, all calls to
     * update just one of the controls causes the visibility logic to trigger for all of them.
     *
     * Subclasses should call the super method after they update their own controls.
     */
    protected void updateControlLayout() {
        if (!mIsLayoutInitialized)
            return;

        boolean isExpanded = mDisplayMode == DISPLAY_MODE_FOCUSED || mDisplayMode == DISPLAY_MODE_CHECKING;
        setBackgroundColor(isExpanded ? mFocusedBackgroundColor : Color.WHITE);

        // Update whether the logo is displayed.
        if (mLogoView != null) {
            boolean show = mLogo != null && mDisplayMode != DISPLAY_MODE_FOCUSED;
            mLogoView.setVisibility(show ? VISIBLE : GONE);
        }

        // The button takes precedence over the summary text and the chevron.
        int editButtonState = getEditButtonState();
        if (editButtonState == EDIT_BUTTON_GONE) {
            mEditButtonView.setVisibility(GONE);
            mChevronView.setVisibility(mDisplayMode == DISPLAY_MODE_EXPANDABLE ? VISIBLE : GONE);

            // Update whether the summary is displayed.
            boolean showSummary = mIsSummaryAllowed && !TextUtils.isEmpty(mSummaryLeftTextView.getText());
            mSummaryLayout.setVisibility(showSummary ? VISIBLE : GONE);
        } else {
            // Show the edit button and hide the chevron and the summary.
            boolean isButtonAllowed = mDisplayMode == DISPLAY_MODE_EXPANDABLE
                    || mDisplayMode == DISPLAY_MODE_NORMAL;
            mSummaryLayout.setVisibility(GONE);
            mChevronView.setVisibility(GONE);
            mEditButtonView.setVisibility(isButtonAllowed ? VISIBLE : GONE);
            mEditButtonView.setText(editButtonState == EDIT_BUTTON_SELECT ? R.string.select : R.string.add);
        }

        // The title gains extra spacing when there is another visible view in the main section.
        int numVisibleMainViews = 0;
        for (int i = 0; i < mMainSection.getChildCount(); i++) {
            if (mMainSection.getChildAt(i).getVisibility() == VISIBLE)
                numVisibleMainViews += 1;
        }

        boolean isTitleMarginNecessary = numVisibleMainViews > 1 && isExpanded;
        int oldMargin = ((ViewGroup.MarginLayoutParams) mTitleView.getLayoutParams()).bottomMargin;
        int newMargin = isTitleMarginNecessary ? mVerticalSpacing : 0;

        if (oldMargin != newMargin) {
            ((ViewGroup.MarginLayoutParams) mTitleView.getLayoutParams()).bottomMargin = newMargin;
            requestLayout();
        }
    }

    /**
     * Section with three extra TextViews beneath the summary to show additional details.
     *
     * ............................................................................
     * . TITLE                                                          |         .
     * .................................................................|         .
     * . LEFT SUMMARY TEXT                        |  RIGHT SUMMARY TEXT | CHEVRON .
     * .................................................................|    or   .
     * . EXTRA TEXT ONE                                                 |   ADD   .
     * .................................................................|    or   .
     * . EXTRA TEXT TWO                                                 |  SELECT .
     * .................................................................|         .
     * . EXTRA TEXT THREE                                               |         .
     * ............................................................................
     */
    public static class ExtraTextsSection extends PaymentRequestSection {
        private TextView[] mExtraTextViews;
        private int mEditButtonState = EDIT_BUTTON_GONE;

        public ExtraTextsSection(Context context, String sectionName, SectionDelegate delegate) {
            super(context, sectionName, delegate);
            setExtraTexts(new String[] { null, null, null });
        }

        @Override
        protected void createMainSectionContent(LinearLayout mainSectionLayout) {
            Context context = mainSectionLayout.getContext();

            mExtraTextViews = new TextView[3];
            for (int i = 0; i < mExtraTextViews.length; i++) {
                mExtraTextViews[i] = new TextView(context);
                ApiCompatibilityUtils.setTextAppearance(mExtraTextViews[i], R.style.PaymentsUiSectionDefaultText);
                mainSectionLayout.addView(mExtraTextViews[i],
                        new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
            }
        }

        /**
         * Sets the CharSequences that are displayed in the extra TextViews.
         *
         * @param extraTexts Texts to display in the extra TextViews.
         */
        public void setExtraTexts(CharSequence[] extraTexts) {
            assert extraTexts.length == mExtraTextViews.length;

            for (int i = 0; i < mExtraTextViews.length; i++) {
                mExtraTextViews[i].setText(extraTexts[i]);
                mExtraTextViews[i].setVisibility(TextUtils.isEmpty(extraTexts[i]) ? GONE : VISIBLE);
            }
        }

        /**
         * Sets how the extra texts should be displayed.
         *
         * @param textsTruncate How to truncate the extra texts. Set the element to null to clear.
         * @param textsAreSingleLine Whether the extra texts should be displayed in a single line.
         */
        public void setExtraTextsProperties(TruncateAt[] textsTruncate, boolean[] textsAreSingleLine) {
            assert textsTruncate.length == mExtraTextViews.length;
            assert textsAreSingleLine.length == mExtraTextViews.length;

            for (int i = 0; i < mExtraTextViews.length; i++) {
                mExtraTextViews[i].setEllipsize(textsTruncate[i]);
                mExtraTextViews[i].setSingleLine(textsAreSingleLine[i]);
            }
        }

        /** Sets the state of the edit button. */
        public void setEditButtonState(int state) {
            mEditButtonState = state;
            updateControlLayout();
        }

        @Override
        public int getEditButtonState() {
            return mEditButtonState;
        }
    }

    /**
     * Section with an additional Layout for showing a total and how it is broken down.
     *
     * Normal mode:     Just the summary is displayed.
     *                  If no option is selected, the "empty label" is displayed in its place.
     * Expandable mode: Same as Normal, but shows the chevron.
     * Focused mode:    Hides the summary and chevron, then displays the full set of options.
     *
     * ............................................................................
     * . TITLE                                                          |         .
     * .................................................................| CHERVON .
     * . LEFT SUMMARY TEXT          | UPDATE TEXT |  RIGHT SUMMARY TEXT |    or   .
     * .................................................................|   ADD   .
     * .                                      | Line item 1 |    $13.99 |    or   .
     * .                                      | Line item 2 |      $.99 |  SELECT .
     * .                                      | Line item 3 |     $2.99 |         .
     * ............................................................................
     */
    public static class LineItemBreakdownSection extends PaymentRequestSection {
        /** The duration of the animation to show and hide the update text. */
        static final int UPDATE_TEXT_ANIMATION_DURATION_MS = 500;

        /** The amount of time where the update text is visible before fading out. */
        static final int UPDATE_TEXT_VISIBILITY_DURATION_MS = 5000;

        /** The GridLayout that shows a breakdown of all the items in the user's card. */
        private GridLayout mBreakdownLayout;

        /**
         * The TextView that is used to display the updated message to the user when the total price
         * of their cart changes. It's the second child of the mSummaryLayout.
         */
        private TextView mUpdatedView;

        /** The runnable used to fade out the mUpdatedView. */
        private Runnable mFadeOutRunnable = new Runnable() {
            @Override
            public void run() {
                Animation out = new AlphaAnimation(mUpdatedView.getAlpha(), 0.0f);
                out.setDuration(UPDATE_TEXT_ANIMATION_DURATION_MS);
                out.setInterpolator(new LinearOutSlowInInterpolator());
                out.setFillAfter(true);
                mUpdatedView.startAnimation(out);
            }
        };

        /** The Handler used to post the mFadeOutRunnables. */
        private Handler mHandler = new Handler();

        public LineItemBreakdownSection(Context context, String sectionName, SectionDelegate delegate,
                String updatedText) {
            super(context, sectionName, delegate);

            // The mUpdatedView should have been created in the base constructor's call to
            // createMainSectionContent(...).
            assert mUpdatedView != null;
            mUpdatedView.setText(updatedText);
        }

        // This method is called in PaymentRequestSection's constructor.
        @Override
        protected void createMainSectionContent(LinearLayout mainSectionLayout) {
            Context context = mainSectionLayout.getContext();

            // Add a label that will be used to indicate that the total cart price has been updated.
            addUpdateText(mainSectionLayout);

            // The breakdown is represented by an end-aligned GridLayout that takes up only as much
            // space as it needs.  The GridLayout ensures a consistent margin between the columns.
            mBreakdownLayout = new GridLayout(context);
            mBreakdownLayout.setColumnCount(2);
            LayoutParams breakdownParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
            breakdownParams.gravity = Gravity.END;
            mainSectionLayout.addView(mBreakdownLayout, breakdownParams);
        }

        /**
         * Adds a text view to the summary layout that will be used to indicate that the total price
         * of the card been updated. The text to display should be set later in the constructor.
         *
         * @param mainSectionLayout The layout of this section.
         */
        private void addUpdateText(LinearLayout mainSectionLayout) {
            assert mUpdatedView == null;

            Context context = mainSectionLayout.getContext();

            // Create the view and set the text appearance and layout parameters.
            mUpdatedView = new TextView(context);
            ApiCompatibilityUtils.setTextAppearance(mUpdatedView, R.style.PaymentsUiSectionDefaultText);
            LinearLayout.LayoutParams updatedLayoutParams = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT,
                    LayoutParams.WRAP_CONTENT);
            ApiCompatibilityUtils.setTextAlignment(mUpdatedView, TEXT_ALIGNMENT_TEXT_END);
            mUpdatedView
                    .setTextColor(ApiCompatibilityUtils.getColor(context.getResources(), R.color.google_green_700));
            ApiCompatibilityUtils.setMarginEnd(updatedLayoutParams,
                    context.getResources().getDimensionPixelSize(R.dimen.payments_section_small_spacing));

            // Set the view to initially be invisible.
            mUpdatedView.setVisibility(View.INVISIBLE);

            // Add the update text just before the last summary text.
            getSummaryLayout().addView(mUpdatedView, getSummaryLayout().getChildCount() - 1, updatedLayoutParams);
        }

        /**
         * Updates the total and how it's broken down.
         *
         * @param cart The shopping cart contents and the total.
         */
        public void update(ShoppingCart cart) {
            Context context = mBreakdownLayout.getContext();

            CharSequence totalPrice = createValueString(cart.getTotal().getCurrency(), cart.getTotal().getPrice(),
                    true);

            // Show the updated text view if the total changed.
            showUpdateIfTextChanged(totalPrice);

            // Update the summary to display information about the total.
            setSummaryText(cart.getTotal().getLabel(), totalPrice);

            mBreakdownLayout.removeAllViews();
            if (cart.getContents() == null)
                return;

            int maximumDescriptionWidthPx = ((View) mBreakdownLayout.getParent()).getWidth() * 2 / 3;

            // Update the breakdown, using one row per {@link LineItem}.
            int numItems = cart.getContents().size();
            mBreakdownLayout.setRowCount(numItems);
            for (int i = 0; i < numItems; i++) {
                LineItem item = cart.getContents().get(i);

                TextView description = new TextView(context);
                ApiCompatibilityUtils.setTextAppearance(description,
                        item.getIsPending() ? R.style.PaymentsUiSectionPendingTextEndAligned
                                : R.style.PaymentsUiSectionDescriptiveTextEndAligned);
                description.setText(item.getLabel());
                description.setEllipsize(TruncateAt.END);
                description.setMaxLines(2);
                if (maximumDescriptionWidthPx > 0) {
                    description.setMaxWidth(maximumDescriptionWidthPx);
                }

                TextView amount = new TextView(context);
                ApiCompatibilityUtils.setTextAppearance(amount,
                        item.getIsPending() ? R.style.PaymentsUiSectionPendingTextEndAligned
                                : R.style.PaymentsUiSectionDescriptiveTextEndAligned);
                amount.setText(createValueString(item.getCurrency(), item.getPrice(), false));

                // Each item is represented by a row in the GridLayout.
                GridLayout.LayoutParams descriptionParams = new GridLayout.LayoutParams(
                        GridLayout.spec(i, 1, GridLayout.END), GridLayout.spec(0, 1, GridLayout.END));
                GridLayout.LayoutParams amountParams = new GridLayout.LayoutParams(
                        GridLayout.spec(i, 1, GridLayout.END), GridLayout.spec(1, 1, GridLayout.END));
                ApiCompatibilityUtils.setMarginStart(amountParams, context.getResources()
                        .getDimensionPixelSize(R.dimen.payments_section_descriptive_item_spacing));

                mBreakdownLayout.addView(description, descriptionParams);
                mBreakdownLayout.addView(amount, amountParams);
            }
        }

        /**
         * Show the update text if the cart total has changed. Should be called before changing the
         * cart total because the old total is needed for comparison.
         *
         * @param rightText The new cart total that will replace the one currently displayed.
         */
        private void showUpdateIfTextChanged(@Nullable CharSequence rightText) {
            // If either the old or new text was null do nothing.
            if (rightText == null || getSummaryRightTextView().getText() == null)
                return;

            // Show the update text only if the current and new cart totals are different and if the
            // old total was visible to the user.
            if (!TextUtils.equals(getSummaryRightTextView().getText(), rightText)
                    && getSummaryRightTextView().getVisibility() == VISIBLE) {
                startUpdateViewAnimation();
            }
        }

        /**
         * Starts the animation to make the update text view fade in then fade out.
         */
        private void startUpdateViewAnimation() {
            // Create and start a fade in anmiation for the mUpdatedView. Re-use the current alpha
            // to avoid restarting a previous or current fade in animation.
            Animation in = new AlphaAnimation(mUpdatedView.getAlpha(), 1.0f);
            in.setDuration(UPDATE_TEXT_ANIMATION_DURATION_MS);
            in.setInterpolator(new LinearOutSlowInInterpolator());
            in.setFillAfter(true);
            mUpdatedView.startAnimation(in);

            // Cancel all pending fade out animations and create a new on to be executed a little
            // while after the fade in.
            mHandler.removeCallbacks(mFadeOutRunnable);
            mHandler.postDelayed(mFadeOutRunnable, UPDATE_TEXT_VISIBILITY_DURATION_MS);
        }

        /**
         * Builds a CharSequence that displays a value in a particular currency.
         *
         * @param currency    Currency of the value being displayed.
         * @param value       Value to display.
         * @param isValueBold Whether or not to bold the item.
         * @return CharSequence that represents the whole value.
         */
        private CharSequence createValueString(String currency, String value, boolean isValueBold) {
            SpannableStringBuilder valueBuilder = new SpannableStringBuilder();
            valueBuilder.append(currency);
            valueBuilder.append(" ");

            int boldStartIndex = valueBuilder.length();
            valueBuilder.append(value);

            if (isValueBold) {
                valueBuilder.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), boldStartIndex,
                        boldStartIndex + value.length(), 0);
            }

            return valueBuilder;
        }

        @Override
        protected void updateControlLayout() {
            if (!mIsLayoutInitialized)
                return;

            mBreakdownLayout.setVisibility(mDisplayMode == DISPLAY_MODE_FOCUSED ? VISIBLE : GONE);
            super.updateControlLayout();
        }
    }

    /**
     * Section that allows selecting one thing from a set of mutually-exclusive options.
     *
     * Normal mode:     The summary text displays the selected option, and the icon for the option
     *                  is displayed in the logo section (if it exists).
     *                  If no option is selected, the "empty label" is displayed in its place.
     *                  This is important for shipping options (e.g.) because there will be no
     *                  option selected by default and a prompt can be displayed.
     * Expandable mode: Same as Normal, but shows the chevron.
     * Focused mode:    Hides the summary and chevron, then displays the full set of options.
     *
     * .............................................................................................
     * . TITLE                                                          |                |         .
     * .................................................................|                |         .
     * . LEFT SUMMARY TEXT                        |  RIGHT SUMMARY TEXT |                |         .
     * .................................................................|                | CHEVRON .
     * . Descriptive text that spans all three columns because it can.  |                |    or   .
     * . ! Warning text that displays a big scary warning and icon.     |           LOGO |   ADD   .
     * . O Option 1                                  ICON 1 | Edit Icon |                |    or   .
     * . O Option 2                                  ICON 2 | Edit Icon |                |  SELECT .
     * . O Option 3                                  ICON 3 | Edit Icon |                |         .
     * . + ADD THING                                                    |                |         .
     * .............................................................................................
     */
    public static class OptionSection extends PaymentRequestSection {

        private static final int INVALID_OPTION_INDEX = -1;

        private final List<TextView> mLabelsForTest = new ArrayList<>();
        private boolean mCanAddItems = true;

        /**
         * Observer to be notified when the OptionSection changes focus state.
         */
        public interface FocusChangedObserver {
            /*
             * Called when the OptionSection view gets or loses focus.
             *
             * @param dataType  The type of the data contained in the section.
             * @param willFocus Whether the section is getting the focus.
             */
            void onFocusChanged(@PaymentRequestUI.DataType int dataType, boolean willFocus);
        }

        /**
         * Displays a row representing either a selectable option or some flavor text.
         *
         * + The "button" is on the left and shows either an icon or a radio button to represent th
         *   row type.
         * + The "label" is text describing the row.
         * + The "icon" is a logo representing the option, like a credit card.
         * + The "edit icon" is a pencil icon with a vertical separator to indicate the option is
         *   editable, clicking on it brings up corresponding editor.
         */
        public class OptionRow {
            private static final int OPTION_ROW_TYPE_OPTION = 0;
            private static final int OPTION_ROW_TYPE_ADD = 1;
            private static final int OPTION_ROW_TYPE_DESCRIPTION = 2;
            private static final int OPTION_ROW_TYPE_WARNING = 3;

            private final int mRowType;
            private final PaymentOption mOption;
            private final View mButton;
            private final TextView mLabel;
            private final View mOptionIcon;
            private final View mEditIcon;

            public OptionRow(GridLayout parent, int rowIndex, int rowType, PaymentOption item, boolean isSelected) {
                boolean optionIconExists = item != null && item.getDrawableIcon() != null;
                boolean editIconExists = item != null && item.isEditable() && isSelected;
                boolean isEnabled = item != null && item.isValid();
                mRowType = rowType;
                mOption = item;
                mButton = createButton(parent, rowIndex, isSelected, isEnabled);
                mLabel = createLabel(parent, rowIndex, optionIconExists, editIconExists, isEnabled);
                mOptionIcon = optionIconExists ? createOptionIcon(parent, rowIndex, editIconExists) : null;
                mEditIcon = editIconExists ? createEditIcon(parent, rowIndex) : null;
            }

            /** Sets the selected state of this item, alerting the delegate if selected. */
            public void setChecked(boolean isChecked) {
                if (mOption == null)
                    return;

                ((RadioButton) mButton).setChecked(isChecked);
                if (isChecked) {
                    updateSelectedItem(mOption);
                    mDelegate.onPaymentOptionChanged(OptionSection.this, mOption);
                }
            }

            /** Returns whether this OptionRow's RadioButton is checked. */
            public boolean isChecked() {
                return ((RadioButton) mButton).isChecked();
            }

            /** Change the label for the row. */
            public void setLabel(int stringId) {
                setLabel(getContext().getString(stringId));
            }

            /** Change the label for the row. */
            public void setLabel(CharSequence string) {
                mLabel.setText(string);
            }

            /** Set the button identifier for the option. */
            public void setButtonId(int id) {
                mButton.setId(id);
            }

            /** @return the label for the row. */
            @VisibleForTesting
            public CharSequence getLabelText() {
                return mLabel.getText();
            }

            private View createButton(GridLayout parent, int rowIndex, boolean isSelected, boolean isEnabled) {
                if (mRowType == OPTION_ROW_TYPE_DESCRIPTION)
                    return null;

                Context context = parent.getContext();
                View view;

                if (mRowType == OPTION_ROW_TYPE_OPTION) {
                    // Show a radio button indicating whether the PaymentOption is selected.
                    RadioButton button = new RadioButton(context);
                    button.setChecked(isSelected && isEnabled);
                    button.setEnabled(isEnabled);
                    view = button;
                } else {
                    // Show an icon representing the row type, defaulting to the add button.
                    int drawableId;
                    int drawableTint;
                    if (mRowType == OPTION_ROW_TYPE_WARNING) {
                        drawableId = R.drawable.ic_warning_white_24dp;
                        drawableTint = R.color.error_text_color;
                    } else {
                        drawableId = R.drawable.plus;
                        drawableTint = R.color.light_active_color;
                    }

                    TintedDrawable tintedDrawable = TintedDrawable.constructTintedDrawable(context.getResources(),
                            drawableId, drawableTint);
                    ImageButton button = new ImageButton(context);
                    button.setBackground(null);
                    button.setImageDrawable(tintedDrawable);
                    button.setPadding(0, 0, 0, 0);
                    view = button;
                }

                // The button hugs left.
                GridLayout.LayoutParams buttonParams = new GridLayout.LayoutParams(
                        GridLayout.spec(rowIndex, 1, GridLayout.CENTER), GridLayout.spec(0, 1, GridLayout.CENTER));
                buttonParams.topMargin = mVerticalMargin;
                ApiCompatibilityUtils.setMarginEnd(buttonParams, mLargeSpacing);
                parent.addView(view, buttonParams);

                view.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
                view.setOnClickListener(OptionSection.this);
                return view;
            }

            private TextView createLabel(GridLayout parent, int rowIndex, boolean optionIconExists,
                    boolean editIconExists, boolean isEnabled) {
                Context context = parent.getContext();
                Resources resources = context.getResources();

                // By default, the label appears to the right of the "button" in the second column.
                // + If there is no button, no option and edit icon, the label spans the whole row.
                // + If there is no option and edit icon, the label spans three columns.
                // + If there is no edit icon or option icon, the label spans two columns.
                // + Otherwise, the label occupies only its own column.
                int columnStart = 1;
                int columnSpan = 1;
                if (!optionIconExists)
                    columnSpan++;
                if (!editIconExists)
                    columnSpan++;

                TextView labelView = new TextView(context);
                if (mRowType == OPTION_ROW_TYPE_OPTION) {
                    // Show the string representing the PaymentOption.
                    ApiCompatibilityUtils.setTextAppearance(labelView,
                            isEnabled ? R.style.PaymentsUiSectionDefaultText
                                    : R.style.PaymentsUiSectionDisabledText);
                    labelView.setText(convertOptionToString(mOption,
                            mDelegate.isBoldLabelNeeded(OptionSection.this), false /* singleLine */));
                    labelView.setEnabled(isEnabled);
                } else if (mRowType == OPTION_ROW_TYPE_ADD) {
                    // Shows string saying that the user can add a new option, e.g. credit card no.
                    String typeface = resources.getString(R.string.roboto_medium_typeface);
                    int textStyle = resources.getInteger(R.integer.roboto_medium_textstyle);
                    int buttonHeight = resources.getDimensionPixelSize(R.dimen.payments_section_add_button_height);

                    ApiCompatibilityUtils.setTextAppearance(labelView, R.style.PaymentsUiSectionAddButtonLabel);
                    labelView.setMinimumHeight(buttonHeight);
                    labelView.setGravity(Gravity.CENTER_VERTICAL);
                    labelView.setTypeface(Typeface.create(typeface, textStyle));
                } else if (mRowType == OPTION_ROW_TYPE_DESCRIPTION) {
                    // The description spans all the columns.
                    columnStart = 0;
                    columnSpan = 4;

                    ApiCompatibilityUtils.setTextAppearance(labelView, R.style.PaymentsUiSectionDescriptiveText);
                } else if (mRowType == OPTION_ROW_TYPE_WARNING) {
                    // Warnings use three columns.
                    columnSpan = 3;
                    ApiCompatibilityUtils.setTextAppearance(labelView, R.style.PaymentsUiSectionWarningText);
                }

                // The label spans two columns if no option or edit icon, or spans three columns if
                // no option and edit icons. Setting the view width to 0 forces it to stretch.
                GridLayout.LayoutParams labelParams = new GridLayout.LayoutParams(
                        GridLayout.spec(rowIndex, 1, GridLayout.CENTER),
                        GridLayout.spec(columnStart, columnSpan, GridLayout.FILL, 1f));
                labelParams.topMargin = mVerticalMargin;
                labelParams.width = 0;
                if (optionIconExists) {
                    // Margin at the end of the label instead of the start of the option icon to
                    // allow option icon in the the next row align with the end of label (include
                    // end margin) when edit icon exits in that row, like below:
                    // ---Label---------------------[label margin]|---option icon---|
                    // ---Label---[label margin]|---option icon---|----edit icon----|
                    ApiCompatibilityUtils.setMarginEnd(labelParams, mLargeSpacing);
                }
                parent.addView(labelView, labelParams);

                labelView.setOnClickListener(OptionSection.this);
                return labelView;
            }

            private View createOptionIcon(GridLayout parent, int rowIndex, boolean editIconExists) {
                // The icon has a pre-defined width.
                ImageView optionIcon = new ImageView(parent.getContext());
                optionIcon.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
                if (mOption.isEditable()) {
                    // Draw border background for the icon if the option is editable.
                    optionIcon.setBackgroundResource(R.drawable.payments_ui_logo_bg);
                    optionIcon.setMaxWidth(mEditableOptionIconMaxWidth);
                } else {
                    optionIcon.setMaxWidth(mNonEditableOptionIconMaxWidth);
                }
                optionIcon.setAdjustViewBounds(true);
                optionIcon.setImageDrawable(mOption.getDrawableIcon());

                // Place option icon at column three if no edit icon.
                int columnStart = editIconExists ? 2 : 3;
                GridLayout.LayoutParams iconParams = new GridLayout.LayoutParams(
                        GridLayout.spec(rowIndex, 1, GridLayout.CENTER),
                        GridLayout.spec(columnStart, 1, GridLayout.CENTER));
                iconParams.topMargin = mVerticalMargin;
                parent.addView(optionIcon, iconParams);

                optionIcon.setOnClickListener(OptionSection.this);
                return optionIcon;
            }

            private View createEditIcon(GridLayout parent, int rowIndex) {
                View editorIcon = LayoutInflater.from(parent.getContext())
                        .inflate(R.layout.payment_option_edit_icon, null);

                // The icon floats to the right of everything.
                GridLayout.LayoutParams iconParams = new GridLayout.LayoutParams(
                        GridLayout.spec(rowIndex, 1, GridLayout.CENTER), GridLayout.spec(3, 1, GridLayout.CENTER));
                iconParams.topMargin = mVerticalMargin;
                parent.addView(editorIcon, iconParams);

                editorIcon.setOnClickListener(OptionSection.this);
                return editorIcon;
            }
        }

        /** Top and bottom margins for each item. */
        private final int mVerticalMargin;

        /** All the possible PaymentOptions in Layout form, then one row for adding new options. */
        private final ArrayList<OptionRow> mOptionRows = new ArrayList<>();

        /** Width that the editable option icon takes. */
        private final int mEditableOptionIconMaxWidth;

        /** Width that the non editable option icon takes. */
        private final int mNonEditableOptionIconMaxWidth;

        /** Layout containing all the {@link OptionRow}s. */
        private GridLayout mOptionLayout;

        /** A spinner to show when the user selection is being checked. */
        private View mCheckingProgress;

        /** SectionInformation that is used to populate the views in this section. */
        private SectionInformation mSectionInformation;

        /** Indicates whether the summary should be a single line. */
        private boolean mSummaryInSingleLine;

        private FocusChangedObserver mFocusChangedObserver;

        /**
         * Constructs an OptionSection.
         *
         * @param context     Context to pull resources from.
         * @param sectionName Title of the section to display.
         * @param delegate    Delegate to alert when something changes in the dialog.
         */
        public OptionSection(Context context, String sectionName, SectionDelegate delegate) {
            super(context, sectionName, delegate);
            mVerticalMargin = context.getResources().getDimensionPixelSize(R.dimen.payments_section_small_spacing);
            mEditableOptionIconMaxWidth = context.getResources()
                    .getDimensionPixelSize(R.dimen.payments_section_logo_width);
            mNonEditableOptionIconMaxWidth = context.getResources()
                    .getDimensionPixelSize(R.dimen.payments_favicon_size);
            setSummaryText(null, null);
        }

        /**
         * Registers the delegate to be notified when this OptionSection gains or loses focus.
         *
         * @param delegate The delegate to notify.
         */
        public void setOptionSectionFocusChangedObserver(FocusChangedObserver observer) {
            mFocusChangedObserver = observer;
        }

        @Override
        public void handleClick(View v) {
            for (int i = 0; i < mOptionRows.size(); i++) {
                OptionRow row = mOptionRows.get(i);
                boolean clickedSelect = row.mButton == v || row.mLabel == v || row.mOptionIcon == v;
                // Handle click on the "ADD THING" button.
                if (row.mOption == null && clickedSelect) {
                    mDelegate.onAddPaymentOption(this);
                    return;
                }

                // Handle click on the edit icon.
                if (row.mOption != null && row.mEditIcon == v) {
                    mDelegate.onEditPaymentOption(this, row.mOption);
                    return;
                }
            }

            // Update the radio button state: checked/unchecked.
            for (int i = 0; i < mOptionRows.size(); i++) {
                OptionRow row = mOptionRows.get(i);
                boolean clickedSelect = row.mButton == v || row.mLabel == v || row.mOptionIcon == v;
                if (row.mOption != null)
                    row.setChecked(clickedSelect);
            }
        }

        @Override
        public void focusSection(boolean shouldFocus) {
            // Override expansion of the section if there's no options to show.
            boolean mayFocus = mSectionInformation != null && mSectionInformation.getSize() > 0;
            if (!mayFocus && shouldFocus) {
                setDisplayMode(PaymentRequestSection.DISPLAY_MODE_NORMAL);
                return;
            }

            // Notify the observer that the focus is going to change.
            if (mFocusChangedObserver != null) {
                mFocusChangedObserver.onFocusChanged(mSectionInformation.getDataType(), shouldFocus);
            }

            super.focusSection(shouldFocus);
        }

        @Override
        protected boolean isLogoNecessary() {
            return true;
        }

        @Override
        public void setSummaryProperties(@Nullable TruncateAt leftTruncate, boolean leftIsSingleLine,
                @Nullable TruncateAt rightTruncate, boolean rightIsSingleLine) {
            super.setSummaryProperties(leftTruncate, leftIsSingleLine, rightTruncate, rightIsSingleLine);

            // Updates the summary if necessary after properties are changed.
            mSummaryInSingleLine = leftIsSingleLine;
            if (mSectionInformation != null) {
                updateSelectedItem(mSectionInformation.getSelectedItem());
            }
        }

        @Override
        protected void createMainSectionContent(LinearLayout mainSectionLayout) {
            Context context = mainSectionLayout.getContext();
            mCheckingProgress = createLoadingSpinner();

            mOptionLayout = new GridLayout(context);
            mOptionLayout.setColumnCount(4);
            mainSectionLayout.addView(mOptionLayout,
                    new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
        }

        /** @param canAddItems If false, this section will not show [+ ADD THING] button. */
        public void setCanAddItems(boolean canAddItems) {
            mCanAddItems = canAddItems;
        }

        /** Updates the View to account for the new {@link SectionInformation} being passed in. */
        public void update(SectionInformation information) {
            mSectionInformation = information;
            PaymentOption selectedItem = information.getSelectedItem();
            updateSelectedItem(selectedItem);
            updateOptionList(information, selectedItem);
            updateControlLayout();
        }

        private View createLoadingSpinner() {
            ViewGroup spinnyLayout = (ViewGroup) LayoutInflater.from(getContext())
                    .inflate(R.layout.payment_request_spinny, null);

            TextView textView = (TextView) spinnyLayout.findViewById(R.id.message);
            textView.setText(getContext().getString(R.string.payments_checking_option));

            return spinnyLayout;
        }

        private void setSpinnerVisibility(boolean visibility) {
            if (visibility) {
                if (mCheckingProgress.getParent() != null)
                    return;

                ViewGroup parent = (ViewGroup) mOptionLayout.getParent();
                int optionLayoutIndex = parent.indexOfChild(mOptionLayout);
                parent.addView(mCheckingProgress, optionLayoutIndex);

                MarginLayoutParams params = (MarginLayoutParams) mCheckingProgress.getLayoutParams();
                params.width = LayoutParams.MATCH_PARENT;
                params.height = LayoutParams.WRAP_CONTENT;
                params.bottomMargin = getContext().getResources()
                        .getDimensionPixelSize(R.dimen.payments_section_checking_spacing);
                mCheckingProgress.requestLayout();
            } else {
                if (mCheckingProgress.getParent() == null)
                    return;

                ViewGroup parent = (ViewGroup) mCheckingProgress.getParent();
                parent.removeView(mCheckingProgress);
            }
        }

        @Override
        protected void updateControlLayout() {
            if (!mIsLayoutInitialized)
                return;

            if (mDisplayMode == DISPLAY_MODE_FOCUSED) {
                setIsSummaryAllowed(false);
                mOptionLayout.setVisibility(VISIBLE);
                setSpinnerVisibility(false);
            } else if (mDisplayMode == DISPLAY_MODE_CHECKING) {
                setIsSummaryAllowed(false);
                mOptionLayout.setVisibility(GONE);
                setSpinnerVisibility(true);
            } else {
                setIsSummaryAllowed(true);
                mOptionLayout.setVisibility(GONE);
                setSpinnerVisibility(false);
            }

            super.updateControlLayout();
        }

        @Override
        public int getEditButtonState() {
            if (mSectionInformation == null)
                return EDIT_BUTTON_GONE;

            if (mSectionInformation.getSize() == 0 && mCanAddItems) {
                // There aren't any PaymentOptions.  Ask the user to add a new one.
                return EDIT_BUTTON_ADD;
            } else if (mSectionInformation.getSelectedItem() == null) {
                // The user hasn't selected any available PaymentOptions.  Ask the user to pick one.
                return EDIT_BUTTON_SELECT;
            } else {
                return EDIT_BUTTON_GONE;
            }
        }

        private void updateSelectedItem(PaymentOption selectedItem) {
            if (selectedItem == null) {
                setLogoDrawable(null, false);
                setIsSummaryAllowed(false);
                setSummaryText(null, null);
            } else {
                setLogoDrawable(selectedItem.getDrawableIcon(), selectedItem.isEditable());
                setSummaryText(convertOptionToString(selectedItem, false /* useBoldLabel */, mSummaryInSingleLine),
                        null);
            }

            updateControlLayout();
        }

        private void updateOptionList(SectionInformation information, PaymentOption selectedItem) {
            mOptionLayout.removeAllViews();
            mOptionRows.clear();
            mLabelsForTest.clear();

            // Show any additional text requested by the layout.
            String additionalText = mDelegate.getAdditionalText(this);
            if (!TextUtils.isEmpty(additionalText)) {
                OptionRow descriptionRow = new OptionRow(mOptionLayout, mOptionRows.size(),
                        mDelegate.isAdditionalTextDisplayingWarning(this) ? OptionRow.OPTION_ROW_TYPE_WARNING
                                : OptionRow.OPTION_ROW_TYPE_DESCRIPTION,
                        null, false);
                mOptionRows.add(descriptionRow);
                descriptionRow.setLabel(additionalText);
            }

            // List out known payment options.
            int firstOptionIndex = INVALID_OPTION_INDEX;
            for (int i = 0; i < information.getSize(); i++) {
                int currentRow = mOptionRows.size();
                if (firstOptionIndex == INVALID_OPTION_INDEX)
                    firstOptionIndex = currentRow;

                PaymentOption item = information.getItem(i);
                OptionRow currentOptionRow = new OptionRow(mOptionLayout, currentRow,
                        OptionRow.OPTION_ROW_TYPE_OPTION, item, item == selectedItem);
                mOptionRows.add(currentOptionRow);

                // For testing, keep the labels in a list for easy access.
                mLabelsForTest.add(currentOptionRow.mLabel);
            }

            // TODO(crbug.com/627186): Find another way to give access to this resource in tests.
            // For testing.
            if (firstOptionIndex != INVALID_OPTION_INDEX) {
                mOptionRows.get(firstOptionIndex).setButtonId(R.id.payments_first_radio_button);
            }

            // If the user is allowed to add new options, show the button for it.
            if (information.getAddStringId() != 0 && mCanAddItems) {
                OptionRow addRow = new OptionRow(mOptionLayout, mOptionLayout.getChildCount(),
                        OptionRow.OPTION_ROW_TYPE_ADD, null, false);
                addRow.setLabel(information.getAddStringId());
                addRow.setButtonId(R.id.payments_add_option_button);
                mOptionRows.add(addRow);
            }
        }

        private CharSequence convertOptionToString(PaymentOption item, boolean useBoldLabel, boolean singleLine) {
            SpannableStringBuilder builder = new SpannableStringBuilder(item.getLabel());
            String labelSeparator = singleLine ? getContext().getString(R.string.autofill_address_summary_separator)
                    : "\n";
            if (useBoldLabel) {
                builder.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), 0, builder.length(), 0);
            }

            if (!TextUtils.isEmpty(item.getSublabel())) {
                if (builder.length() > 0)
                    builder.append(labelSeparator);
                builder.append(item.getSublabel());
            }

            if (!TextUtils.isEmpty(item.getTertiaryLabel())) {
                if (builder.length() > 0)
                    builder.append(labelSeparator);
                builder.append(item.getTertiaryLabel());
            }

            if (!item.isComplete() && !TextUtils.isEmpty(item.getEditMessage())) {
                if (builder.length() > 0)
                    builder.append(labelSeparator);
                String editMessage = item.getEditMessage();
                builder.append(editMessage);
                Object foregroundSpanner = new ForegroundColorSpan(
                        ApiCompatibilityUtils.getColor(getContext().getResources(), R.color.google_blue_700));
                Object sizeSpanner = new AbsoluteSizeSpan(14, true);
                int startIndex = builder.length() - editMessage.length();
                builder.setSpan(foregroundSpanner, startIndex, builder.length(), 0);
                builder.setSpan(sizeSpanner, startIndex, builder.length(), 0);
            }

            return builder;
        }

        /**
         * Returns the label at the specified |labelIndex|. Returns null if there is no label at
         * that index.
         */
        @VisibleForTesting
        public TextView getOptionLabelsForTest(int labelIndex) {
            return mLabelsForTest.get(labelIndex);
        }

        /** Returns the number of option labels. */
        @VisibleForTesting
        public int getNumberOfOptionLabelsForTest() {
            return mLabelsForTest.size();
        }

        /** Returns the OptionRow at the specified |index|. */
        @VisibleForTesting
        public OptionRow getOptionRowAtIndex(int index) {
            return mOptionRows.get(index);
        }
    }

    /**
     * Drawn as a 1dp separator.  Initially drawn without being expanded to the full width of the
     * UI, but can be expanded to separate sections fully.
     */
    public static class SectionSeparator extends View {
        /** Creates the View and adds it to the parent. */
        public SectionSeparator(ViewGroup parent) {
            this(parent, -1);
        }

        /** Creates the View and adds it to the parent at the given index. */
        public SectionSeparator(ViewGroup parent, int index) {
            super(parent.getContext());
            Resources resources = parent.getContext().getResources();
            setBackgroundColor(ApiCompatibilityUtils.getColor(resources, R.color.payments_section_separator));
            LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT,
                    resources.getDimensionPixelSize(R.dimen.payments_section_separator_height));

            int margin = resources.getDimensionPixelSize(R.dimen.payments_section_large_spacing);
            ApiCompatibilityUtils.setMarginStart(params, margin);
            ApiCompatibilityUtils.setMarginEnd(params, margin);
            parent.addView(this, index, params);
        }

        /** Expand the separator to be the full width of the dialog. */
        public void expand() {
            LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) getLayoutParams();
            ApiCompatibilityUtils.setMarginStart(params, 0);
            ApiCompatibilityUtils.setMarginEnd(params, 0);
        }
    }
}