chromium/chrome/android/java/src/org/chromium/chrome/browser/payments/ui/PaymentRequestSection.java

// Copyright 2016 The Chromium Authors
// 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.drawable.Drawable;
import android.os.Handler;
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 androidx.annotation.ColorInt;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.core.view.MarginLayoutParamsCompat;
import androidx.gridlayout.widget.GridLayout;

import org.chromium.chrome.R;
import org.chromium.chrome.browser.ui.theme.ChromeSemanticColorUtils;
import org.chromium.components.autofill.EditableOption;
import org.chromium.components.browser_ui.styles.SemanticColorUtils;
import org.chromium.components.browser_ui.widget.DualControlLayout;
import org.chromium.components.browser_ui.widget.DualControlLayout.ButtonType;
import org.chromium.components.browser_ui.widget.TintedDrawable;
import org.chromium.ui.HorizontalListDividerDrawable;
import org.chromium.ui.base.ViewUtils;
import org.chromium.ui.interpolators.Interpolators;

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

/**
 * 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                                           |                |  CHOOSE .
 * .............................................................................................
 *
 * 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 CHOOSE
 *    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 CHOOSE 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 EditableOption} that was selected.
         */
        void onEditableOptionChanged(PaymentRequestSection section, EditableOption option);

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

        /** Called when the user requests adding a new EditableOption to a given section. */
        void onAddEditableOption(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_CHOOSE = 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. */
    public static final int DISPLAY_MODE_NORMAL = 3;

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

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

    /** Checking mode: Gray background, spinner overlay hides everything except the title. */
    public 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 @ColorInt int mUnfocusedBackgroundColor;
    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.
        mUnfocusedBackgroundColor = ChromeSemanticColorUtils.getPaymentRequestBg(context);
        mFocusedBackgroundColor = SemanticColorUtils.getDefaultBgColorElev1(context);
        mLargeSpacing =
                getResources().getDimensionPixelSize(R.dimen.editor_dialog_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.
     */
    protected void setLogoDrawable(Drawable logo) {
        assert isLogoNecessary();
        mLogo = logo;
        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;
    }

    /** Returns the left summary TextView. */
    protected TextView getSummaryLeftTextView() {
        assert mSummaryLeftTextView != null;
        return mSummaryLeftTextView;
    }

    @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 "CHOOSE".
        if (v == mEditButtonView) {
            if (getEditButtonState() == EDIT_BUTTON_ADD) {
                mDelegate.onAddEditableOption(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();
    }

    /**
     * Changes the appearance of the title.
     *
     * @param resId @see android.widget.TextView#setTextAppearance(int id).
     */
    protected void setTitleAppearance(int resId) {
        mTitleView.setTextAppearance(resId);
    }

    /**
     * Changes the appearance of the summary.
     *
     * @param resId @see android.widget.TextView#setTextAppearance(int id).
     */
    protected void setSummaryAppearance(int leftResId, int rightResId) {
        mSummaryLeftTextView.setTextAppearance(leftResId);
        mSummaryRightTextView.setTextAppearance(rightResId);
    }

    /**
     * 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);
        mTitleView.setTextAppearance(R.style.TextAppearance_TextMedium_Accent1);
        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);
        mSummaryLeftTextView.setTextAppearance(R.style.TextAppearance_TextLarge_Primary);

        mSummaryRightTextView = new TextView(getContext());
        mSummaryRightTextView.setTextAppearance(R.style.TextAppearance_TextLarge_Primary);
        mSummaryRightTextView.setTextAlignment(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);
        MarginLayoutParamsCompat.setMarginStart(
                rightLayoutParams,
                getContext()
                        .getResources()
                        .getDimensionPixelSize(R.dimen.editor_dialog_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.editable_option_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);
        MarginLayoutParamsCompat.setMarginStart(params, startMargin);
        parent.addView(view, params);
        return view;
    }

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

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

    private ImageView createAndAddChevron(ViewGroup parent) {
        TintedDrawable chevron =
                TintedDrawable.constructTintedDrawable(
                        parent.getContext(),
                        R.drawable.ic_expand_more_black_24dp,
                        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);
        MarginLayoutParamsCompat.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 : mUnfocusedBackgroundColor);

        // 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);
        } else {
            // Show the edit button and hide the chevron.
            boolean isButtonAllowed =
                    mDisplayMode == DISPLAY_MODE_EXPANDABLE || mDisplayMode == DISPLAY_MODE_NORMAL;
            mChevronView.setVisibility(GONE);
            mEditButtonView.setVisibility(isButtonAllowed ? VISIBLE : GONE);
            mEditButtonView.setText(
                    editButtonState == EDIT_BUTTON_CHOOSE ? R.string.choose : R.string.add);
        }

        // Update whether the summary is displayed.
        mSummaryLayout.setVisibility(mIsSummaryAllowed ? VISIBLE : GONE);

        // 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;
            ViewUtils.requestLayout(this, "PaymentRequestSection.UpdateControlLayout");
        }
    }

    /**
     * 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 |  CHOOSE .
     * .                                      | 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;

        private final List<TextView> mLineItemAmountsForTest = new ArrayList<>();

        /** 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(Interpolators.LINEAR_OUT_SLOW_IN_INTERPOLATOR);
                        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);

            // Sets the summary right text view takes the same available space as the summary left
            // text view.
            LinearLayout.LayoutParams rightTextViewLayoutParams =
                    (LinearLayout.LayoutParams) getSummaryRightTextView().getLayoutParams();
            rightTextViewLayoutParams.width = 0;
            rightTextViewLayoutParams.weight = 1f;
        }

        /**
         * 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);
            mUpdatedView.setTextAppearance(R.style.TextAppearance_TextLarge_Primary);
            LinearLayout.LayoutParams updatedLayoutParams =
                    new LinearLayout.LayoutParams(
                            LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
            mUpdatedView.setTextAlignment(TEXT_ALIGNMENT_TEXT_END);
            mUpdatedView.setTextColor(context.getColor(R.color.google_green_600));
            MarginLayoutParamsCompat.setMarginStart(
                    updatedLayoutParams,
                    context.getResources()
                            .getDimensionPixelSize(R.dimen.editor_dialog_section_small_spacing));
            MarginLayoutParamsCompat.setMarginEnd(
                    updatedLayoutParams,
                    context.getResources()
                            .getDimensionPixelSize(R.dimen.editor_dialog_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();
            mLineItemAmountsForTest.clear();
            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);
                description.setTextAppearance(
                        item.getIsPending()
                                ? R.style.TextAppearance_PaymentsUiSectionPendingTextEndAligned
                                : R.style
                                        .TextAppearance_PaymentsUiSectionDescriptiveTextEndAligned);
                description.setText(item.getLabel());
                description.setEllipsize(TruncateAt.END);
                description.setMaxLines(2);
                if (maximumDescriptionWidthPx > 0) {
                    description.setMaxWidth(maximumDescriptionWidthPx);
                }

                TextView amount = new TextView(context);
                amount.setTextAppearance(
                        item.getIsPending()
                                ? R.style.TextAppearance_PaymentsUiSectionPendingTextEndAligned
                                : R.style
                                        .TextAppearance_PaymentsUiSectionDescriptiveTextEndAligned);
                amount.setText(createValueString(item.getCurrency(), item.getPrice(), false));
                mLineItemAmountsForTest.add(amount);

                // 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));
                MarginLayoutParamsCompat.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(Interpolators.LINEAR_OUT_SLOW_IN_INTERPOLATOR);
            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
        public void setDisplayMode(int displayMode) {
            // Displays the summary left text view in at most three lines if in focus mode,
            // otherwise display it in a single line.
            if (displayMode == DISPLAY_MODE_FOCUSED) {
                setSummaryProperties(
                        TruncateAt.END,
                        /* leftIsSingleLine= */ false,
                        /* rightTruncate= */ null,
                        /* rightIsSingleLine= */ false);
                getSummaryLeftTextView().setMaxLines(3);
            } else {
                setSummaryProperties(
                        TruncateAt.END,
                        /* leftIsSingleLine= */ true,
                        /* rightTruncate= */ null,
                        /* rightIsSingleLine= */ false);
                getSummaryLeftTextView().setMaxLines(1);
            }

            super.setDisplayMode(displayMode);
        }

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

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

        /**
         * Returns the line item amount at the specified |index|. Returns null if there is no amount
         * at that index.
         */
        public TextView getLineItemAmountForTest(int index) {
            return mLineItemAmountsForTest.get(index);
        }

        /** @return The number of line items. */
        public int getNumberOfLineItemsForTest() {
            return mLineItemAmountsForTest.size();
        }
    }

    /**
     * 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 |                |  CHOOSE .
     * . 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;
            @Nullable private final EditableOption 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,
                    @Nullable EditableOption item,
                    boolean isSelected) {
                assert item != null || rowType != OPTION_ROW_TYPE_OPTION;
                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.onEditableOptionChanged(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 EditableOption 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.default_text_color_error;
                    } else {
                        drawableId = R.drawable.plus;
                        drawableTint = R.color.default_icon_color_accent1_tint_list;
                    }

                    TintedDrawable tintedDrawable =
                            TintedDrawable.constructTintedDrawable(
                                    context, 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;
                MarginLayoutParamsCompat.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 EditableOption.
                    labelView.setText(
                            convertOptionToString(
                                    mOption,
                                    false, /* excludeMainLabel */
                                    mDelegate.isBoldLabelNeeded(OptionSection.this),
                                    /* singleLine= */ false));
                    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.
                    int buttonHeight =
                            resources.getDimensionPixelSize(
                                    R.dimen.payments_section_add_button_height);

                    labelView.setTextAppearance(
                            R.style.TextAppearance_EditorDialogSectionAddButton);
                    labelView.setMinimumHeight(buttonHeight);
                    labelView.setGravity(Gravity.CENTER_VERTICAL);
                } else if (mRowType == OPTION_ROW_TYPE_DESCRIPTION) {
                    // The description spans all the columns.
                    columnStart = 0;
                    columnSpan = 4;

                    labelView.setTextAppearance(R.style.TextAppearance_TextMedium_Secondary);
                    labelView.setId(R.id.payments_description_label);
                } else if (mRowType == OPTION_ROW_TYPE_WARNING) {
                    // Warnings use three columns.
                    columnSpan = 3;
                    labelView.setTextAppearance(
                            R.style.TextAppearance_PaymentsUiSectionWarningText);
                    labelView.setId(R.id.payments_warning_label);
                }

                // 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----|
                    MarginLayoutParamsCompat.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()) {
                    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;
            }

            /** Returns the edit icon for the option row. */
            public View getEditIconForTest() {
                return mEditIcon;
            }
        }

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

        /** All the possible EditableOptions 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 is displayed in a single line. */
        private boolean mSummaryInSingleLine;

        /**
         * Indicates whether the summary is set to display in a single line in DISPLAY_MODE_NORMAL
         * by caller.
         */
        private boolean mSetDisplaySummaryInSingleLineInNormalMode = true;

        /**
         * Indicates whether the summary should be split to display in left and right summary
         * text views in {@link DISPLAY_MODE_NORMAL}.
         */
        private boolean mSplitSummaryInDisplayModeNormal;

        /** Indicates whether the summary is set to descriptive or title text style. */
        private boolean mSummaryInDescriptiveText;

        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.editor_dialog_section_small_spacing);
            mEditableOptionIconMaxWidth =
                    context.getResources()
                            .getDimensionPixelSize(R.dimen.editable_option_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.onAddEditableOption(this);
                    return;
                }

                // Handle click on the edit icon.
                if (row.mOption != null && row.mEditIcon == v) {
                    mDelegate.onEditEditableOption(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);
            }

            int previousDisplayMode = mDisplayMode;
            super.focusSection(shouldFocus);

            // Update summary when display mode changed from DISPLAY_MODE_NORMAL to other modes.
            if (mSectionInformation != null && previousDisplayMode == DISPLAY_MODE_NORMAL) {
                updateSelectedItem(mSectionInformation.getSelectedItem());
            }
        }

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

        @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;
        }

        /**
         * @param singleLine If true, sets the summary text to display in a single line
         *                   in {@link #DISPLAY_MODE_NORMAL} when there is a valid selected
         *                   option, otherwise sets the summary text to display in multiple lines.
         */
        public void setDisplaySummaryInSingleLineInNormalMode(boolean singleLine) {
            mSetDisplaySummaryInSingleLineInNormalMode = singleLine;
        }

        /**
         * Specify whether the summary should be split to display under DISPLAY_MODE_NORMAL.
         *
         * @param splitSummary If true split the display of summary in the left and right
         *                     text views in {@link DISPLAY_MODE_NORMAL}, the summary is
         *                     split into 'label' and the rest('sublabel', 'Tertiary label').
         *                     Otherwise the entire summary is displayed in the left text view.
         */
        public void setSplitSummaryInDisplayModeNormal(boolean splitSummary) {
            mSplitSummaryInDisplayModeNormal = splitSummary;
        }

        /** Updates the View to account for the new {@link SectionInformation} being passed in. */
        public void update(SectionInformation information) {
            mSectionInformation = information;
            EditableOption 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 = 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);
                ViewUtils.requestLayout(
                        mCheckingProgress, "PaymentRequestSection.OptionRow.setSpinnerVisibility");
            } 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 EditableOptions.  Ask the user to add a new one.
                return EDIT_BUTTON_ADD;
            } else if (mSectionInformation.getSelectedItem() == null) {
                // The user hasn't selected any available EditableOptions.  Ask the user to pick
                // one.
                return EDIT_BUTTON_CHOOSE;
            } else {
                return EDIT_BUTTON_GONE;
            }
        }

        private void updateSelectedItem(EditableOption selectedItem) {
            // Only left TextView in the summary section is used in this section.
            // Summary is displayed in multiple lines by default unless:
            // 1. nothing is selected or
            // 2. the display mode is DISPLAY_MODE_NORMAL without caller explicitly set to display
            //    summary in multiple lines.
            if (selectedItem == null
                    || (mDisplayMode == DISPLAY_MODE_NORMAL
                            && mSetDisplaySummaryInSingleLineInNormalMode)) {
                if (!mSummaryInSingleLine) {
                    setSummaryProperties(
                            TruncateAt.END,
                            /* leftIsSingleLine= */ true,
                            /* rightTruncate= */ null,
                            /* rightIsSingleLine= */ false);
                    mSummaryInSingleLine = true;
                }
            } else if (mSummaryInSingleLine) {
                setSummaryProperties(
                        /* leftTruncate= */ null,
                        /* leftIsSingleLine= */ false,
                        /* rightTruncate= */ null,
                        /* rightIsSingleLine= */ false);
                mSummaryInSingleLine = false;
            }

            if (selectedItem == null) {
                setLogoDrawable(null);
                // Section summary should be displayed as descriptive text style.
                if (!mSummaryInDescriptiveText) {
                    TextView view = getSummaryLeftTextView();
                    view.setTextAppearance(R.style.TextAppearance_TextMedium_Secondary);
                    mSummaryInDescriptiveText = true;
                }
                SectionUiUtils.showSectionSummaryInTextViewInSingeLine(
                        getContext(), mSectionInformation, getSummaryLeftTextView());
            } else {
                setLogoDrawable(selectedItem.getDrawableIcon());
                // Selected item summary should be displayed as
                // R.style.TextAppearance_TextLarge_Primary.
                if (mSummaryInDescriptiveText) {
                    TextView view = getSummaryLeftTextView();
                    view.setTextAppearance(R.style.TextAppearance_TextLarge_Primary);
                    mSummaryInDescriptiveText = false;
                }
                // Split summary in DISPLAY_MODE_NORMAL if caller specified. The first part is
                // displayed on the left summary text view aligned to the left. The second part is
                // displayed on the right summary text view aligned to the right.
                boolean splitSummary =
                        mSplitSummaryInDisplayModeNormal && (mDisplayMode == DISPLAY_MODE_NORMAL);
                if (splitSummary) {
                    setSummaryText(
                            selectedItem.getLabel(),
                            convertOptionToString(
                                    selectedItem,
                                    /* excludeMainLabel= */ true,
                                    /* useBoldLabel= */ false,
                                    mSummaryInSingleLine));
                } else {
                    setSummaryText(
                            convertOptionToString(
                                    selectedItem,
                                    /* excludeMainLabel= */ false,
                                    /* useBoldLabel= */ false,
                                    mSummaryInSingleLine),
                            null);
                }
            }

            updateControlLayout();
        }

        private void updateOptionList(SectionInformation information, EditableOption 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;

                EditableOption 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/40476067): 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(
                EditableOption item,
                boolean excludeMainLabel,
                boolean useBoldLabel,
                boolean singleLine) {
            SpannableStringBuilder builder = new SpannableStringBuilder();
            if (!excludeMainLabel) {
                builder.append(item.getLabel());
                if (useBoldLabel) {
                    builder.setSpan(
                            new StyleSpan(android.graphics.Typeface.BOLD), 0, builder.length(), 0);
                }
            }

            String labelSeparator =
                    singleLine
                            ? getContext().getString(R.string.autofill_address_summary_separator)
                            : "\n";
            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 (!TextUtils.isEmpty(item.getPromoMessage())) {
                if (builder.length() > 0) builder.append(labelSeparator);
                builder.append(item.getPromoMessage());
            }

            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(
                                SemanticColorUtils.getDefaultTextColorLink(getContext()));
                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.
         */
        public TextView getOptionLabelsForTest(int labelIndex) {
            return mLabelsForTest.get(labelIndex);
        }

        /** Returns the label of the section summary. */
        public TextView getLeftSummaryLabelForTest() {
            return getSummaryLeftTextView();
        }

        /** Returns the right summary text view. */
        public TextView getRightSummaryLabelForTest() {
            return getSummaryRightTextView();
        }

        /** Returns the number of option labels. */
        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();
            setBackground(HorizontalListDividerDrawable.create(getContext()));
            LinearLayout.LayoutParams params =
                    new LinearLayout.LayoutParams(
                            LayoutParams.MATCH_PARENT,
                            resources.getDimensionPixelSize(R.dimen.divider_height));

            int margin =
                    resources.getDimensionPixelSize(R.dimen.editor_dialog_section_large_spacing);
            MarginLayoutParamsCompat.setMarginStart(params, margin);
            MarginLayoutParamsCompat.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();
            MarginLayoutParamsCompat.setMarginStart(params, 0);
            MarginLayoutParamsCompat.setMarginEnd(params, 0);
        }
    }
}