ActionListItem.java

/*
 * Copyright 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.car.widget;

import static java.lang.annotation.RetentionPolicy.SOURCE;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.annotation.DimenRes;
import androidx.annotation.Dimension;
import androidx.annotation.DrawableRes;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.car.R;
import androidx.car.util.CarUxRestrictionsUtils;
import androidx.car.uxrestrictions.CarUxRestrictions;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.Guideline;

import java.lang.annotation.Retention;
import java.util.ArrayList;
import java.util.List;

/**
 * Class to build a list item that has up to two actions.
 *
 * <p>An item visually composes of 3 parts; each part may contain multiple views.
 * <ul>
 *     <li>{@code Primary Action}: represented by an icon of following types.
 *     <ul>
 *         <li>Primary Icon - icon size could be large or small.
 *         <li>No Icon - no icon is shown.
 *         <li>Empty Icon - {@code Text} offsets start space as if there was an icon.
 *     </ul>
 *     <li>{@code Text}: supports any combination of the following text views.
 *     <ul>
 *         <li>Title
 *         <li>Body
 *     </ul>
 *     <li>{@code Supplemental Action}: Up to two actions.
 * </ul>
 *
 * <p>{@code ActionListItem} binds data to {@link ViewHolder} based on components selected.
 *
 * <p>When conflicting setter methods are called (e.g. setting primary action to both primary icon
 * and no icon), the last called method wins.
 */
public final class ActionListItem extends ListItem<ActionListItem.ViewHolder> {
    @Retention(SOURCE)
    @IntDef({PRIMARY_ACTION_ICON_SIZE_SMALL, PRIMARY_ACTION_ICON_SIZE_MEDIUM,
            PRIMARY_ACTION_ICON_SIZE_LARGE})
    private @interface PrimaryActionIconSize {}

    /**
     * Small sized icon is the mostly commonly used size. It's the same as supplemental action icon.
     */
    public static final int PRIMARY_ACTION_ICON_SIZE_SMALL = 0;
    /**
     * Medium sized icon is slightly bigger than {@code SMALL} ones. It is intended for profile
     * pictures (avatar), in which case caller is responsible for passing in a circular image.
     */
    public static final int PRIMARY_ACTION_ICON_SIZE_MEDIUM = 1;
    /**
     * Large sized icon is as tall as a list item with only {@code title} text. It is intended for
     * album art.
     */
    public static final int PRIMARY_ACTION_ICON_SIZE_LARGE = 2;

    @Retention(SOURCE)
    @IntDef({
            PRIMARY_ACTION_TYPE_NO_ICON, PRIMARY_ACTION_TYPE_EMPTY_ICON,
            PRIMARY_ACTION_TYPE_ICON})
    private @interface PrimaryActionType {}

    private static final int PRIMARY_ACTION_TYPE_NO_ICON = 0;
    private static final int PRIMARY_ACTION_TYPE_EMPTY_ICON = 1;
    private static final int PRIMARY_ACTION_TYPE_ICON = 2;

    @PrimaryActionType private int mPrimaryActionType = PRIMARY_ACTION_TYPE_NO_ICON;
    private Drawable mPrimaryActionIconDrawable;
    @PrimaryActionIconSize private int mPrimaryActionIconSize = PRIMARY_ACTION_ICON_SIZE_SMALL;

    private final Context mContext;
    private boolean mIsEnabled = true;
    private final List<ViewBinder<ViewHolder>> mBinders = new ArrayList<>();

    private CharSequence mTitle;
    private CharSequence mBody;

    @Dimension
    private final int mSupplementalGuidelineBegin;

    private boolean mIsActionBorderless = true;
    private String mPrimaryActionText;
    private View.OnClickListener mPrimaryActionOnClickListener;
    private boolean mShowPrimaryActionDivider;

    private String mSecondaryActionText;
    private View.OnClickListener mSecondaryActionOnClickListener;
    private boolean mShowSecondaryActionDivider;

    private View.OnClickListener mOnClickListener;

    /**
     * Creates a {@link ActionListItem.ViewHolder}.
     */
    @NonNull
    public static ViewHolder createViewHolder(View itemView) {
        return new ViewHolder(itemView);
    }

    public ActionListItem(@NonNull Context context) {
        mContext = context;
        mSupplementalGuidelineBegin = mContext.getResources().getDimensionPixelSize(
                R.dimen.car_list_item_supplemental_guideline_top);
        markDirty();
    }

    /**
     * Used by {@link ListItemAdapter} to choose layout to inflate for view holder.
     */
    @Override
    public int getViewType() {
        return ListItemAdapter.LIST_ITEM_TYPE_ACTION;
    }

    /**
     * Resets all views in {@link ActionListItem.ViewHolder} then applies ViewBinders to
     * adjust view layout params.
     */
    @Override
    public void onBind(ActionListItem.ViewHolder viewHolder) {
        for (View v : viewHolder.getWidgetViews()) {
            v.setEnabled(mIsEnabled);
            v.setVisibility(View.GONE);
        }

        // ActionListItem supports clicking on the item so we also update the entire itemView.
        viewHolder.itemView.setEnabled(mIsEnabled);

        for (ViewBinder<ViewHolder> binder : mBinders) {
            binder.bind(viewHolder);
        }
    }

    @Override
    public void setEnabled(boolean enabled) {
        mIsEnabled = enabled;
    }

    /**
     * Calculates the layout params for views in {@link ViewHolder}.
     */
    @Override
    protected void resolveDirtyState() {
        mBinders.clear();

        // Create binders that adjust layout params of each view.
        setPrimaryAction();
        setText();
        setSupplementalActions();
        setOnClickListener();
    }

    @NonNull
    protected Context getContext() {
        return mContext;
    }

    private void setPrimaryAction() {
        setPrimaryIconContent();
        setPrimaryIconLayout();
    }

    private void setText() {
        setTextContent();
        setTextVerticalMargin();
        setTextStartMargin();
    }

    private void setOnClickListener() {
        mBinders.add(vh -> {
            vh.itemView.setOnClickListener(mOnClickListener);
            vh.itemView.setClickable(mOnClickListener != null);
        });
    }

    private void setPrimaryIconContent() {
        switch (mPrimaryActionType) {
            case PRIMARY_ACTION_TYPE_ICON:
                mBinders.add(vh -> {
                    vh.getPrimaryIcon().setVisibility(View.VISIBLE);
                    vh.getPrimaryIcon().setImageDrawable(mPrimaryActionIconDrawable);
                });
                break;
            case PRIMARY_ACTION_TYPE_EMPTY_ICON:
            case PRIMARY_ACTION_TYPE_NO_ICON:
                // Do nothing.
                break;
            default:
                throw new IllegalStateException("Unknown primary action type.");
        }
    }

    /**
     * Sets the size, start margin, and vertical position of primary icon.
     *
     * <p>Large icon will have no start margin, and always align center vertically.
     *
     * <p>Small/medium icon will have start margin, and uses a top margin such that it is "pinned"
     * at the same position in list item regardless of item height.
     */
    private void setPrimaryIconLayout() {
        if (mPrimaryActionType == PRIMARY_ACTION_TYPE_EMPTY_ICON
                || mPrimaryActionType == PRIMARY_ACTION_TYPE_NO_ICON) {
            return;
        }

        // Size of icon.
        @DimenRes int sizeResId;
        switch (mPrimaryActionIconSize) {
            case PRIMARY_ACTION_ICON_SIZE_SMALL:
                sizeResId = R.dimen.car_primary_icon_size;
                break;
            case PRIMARY_ACTION_ICON_SIZE_MEDIUM:
                sizeResId = R.dimen.car_avatar_icon_size;
                break;
            case PRIMARY_ACTION_ICON_SIZE_LARGE:
                sizeResId = R.dimen.car_single_line_list_item_height;
                break;
            default:
                throw new IllegalStateException("Unknown primary action icon size.");
        }

        int iconSize = mContext.getResources().getDimensionPixelSize(sizeResId);

        // Start margin of icon.
        int startMargin;
        switch (mPrimaryActionIconSize) {
            case PRIMARY_ACTION_ICON_SIZE_SMALL:
            case PRIMARY_ACTION_ICON_SIZE_MEDIUM:
                startMargin = mContext.getResources().getDimensionPixelSize(R.dimen.car_keyline_1);
                break;
            case PRIMARY_ACTION_ICON_SIZE_LARGE:
                startMargin = 0;
                break;
            default:
                throw new IllegalStateException("Unknown primary action icon size.");
        }

        mBinders.add(vh -> {
            ConstraintLayout.LayoutParams layoutParams =
                    (ConstraintLayout.LayoutParams) vh.getPrimaryIcon().getLayoutParams();
            layoutParams.height = layoutParams.width = iconSize;
            layoutParams.setMarginStart(startMargin);

            if (mPrimaryActionIconSize == PRIMARY_ACTION_ICON_SIZE_LARGE) {
                // A large icon is always vertically centered.
                layoutParams.verticalBias = 0.5f;
                layoutParams.topMargin = 0;
            } else {
                // Align the icon to the top of the parent. This allows the topMargin to shift it
                // down relative to the top.
                layoutParams.verticalBias = 0f;

                // For all other icon sizes, the icon should be centered within the height of
                // car_double_line_list_item_height. Note: the actual height of the item can be
                // larger than this.
                int itemHeight = mContext.getResources().getDimensionPixelSize(
                        R.dimen.car_double_line_list_item_height);
                layoutParams.topMargin = (itemHeight - iconSize) / 2;
            }

            vh.getPrimaryIcon().requestLayout();
        });
    }

    private void setTextContent() {
        boolean hasTitle = !TextUtils.isEmpty(mTitle);
        boolean hasBody = !TextUtils.isEmpty(mBody);

        if (!hasTitle && !hasBody) {
            return;
        }

        mBinders.add(vh -> {
            if (hasTitle) {
                vh.getTitle().setVisibility(View.VISIBLE);
                vh.getTitle().setText(mTitle);
            }

            if (hasBody) {
                vh.getBody().setVisibility(View.VISIBLE);
                vh.getBody().setText(mBody);
            }

            if (hasTitle && !hasBody) {
                // If only title, then center the supplemental actions.
                vh.getSupplementalGuideline().setGuidelineBegin(
                        ConstraintLayout.LayoutParams.UNSET);
                vh.getSupplementalGuideline().setGuidelinePercent(0.5f);
            } else {
                // Otherwise, position it a fixed distance from the top.
                vh.getSupplementalGuideline().setGuidelinePercent(
                        ConstraintLayout.LayoutParams.UNSET);
                vh.getSupplementalGuideline().setGuidelineBegin(
                        mSupplementalGuidelineBegin);
            }
        });
    }

    /**
     * Sets start margin of text view depending on icon type.
     */
    private void setTextStartMargin() {
        @DimenRes int startMarginResId;
        switch (mPrimaryActionType) {
            case PRIMARY_ACTION_TYPE_NO_ICON:
                startMarginResId = R.dimen.car_keyline_1;
                break;
            case PRIMARY_ACTION_TYPE_EMPTY_ICON:
                startMarginResId = R.dimen.car_keyline_3;
                break;
            case PRIMARY_ACTION_TYPE_ICON:
                startMarginResId = mPrimaryActionIconSize == PRIMARY_ACTION_ICON_SIZE_LARGE
                        ? R.dimen.car_keyline_4
                        : R.dimen.car_keyline_3;  // Small and medium sized icon.
                break;
            default:
                throw new IllegalStateException("Unknown primary action type.");
        }
        int startMargin = mContext.getResources().getDimensionPixelSize(startMarginResId);
        mBinders.add(vh -> {
            ViewGroup.MarginLayoutParams titleLayoutParams =
                    (ViewGroup.MarginLayoutParams) vh.getTitle().getLayoutParams();
            titleLayoutParams.setMarginStart(startMargin);
            vh.getTitle().requestLayout();

            ViewGroup.MarginLayoutParams bodyLayoutParams =
                    (ViewGroup.MarginLayoutParams) vh.getBody().getLayoutParams();
            bodyLayoutParams.setMarginStart(startMargin);
            vh.getBody().requestLayout();
        });
    }

    /**
     * Sets top/bottom margins of {@code Title} and {@code Body}.
     */
    private void setTextVerticalMargin() {
        // Set all relevant fields in layout params to avoid carried over params when the item
        // gets bound to a recycled view holder.
        if (!TextUtils.isEmpty(mTitle) && TextUtils.isEmpty(mBody)) {
            // Title only - view is aligned center vertically by itself.
            mBinders.add(vh -> {
                ViewGroup.MarginLayoutParams layoutParams =
                        (ViewGroup.MarginLayoutParams) vh.getTitle().getLayoutParams();
                layoutParams.topMargin = 0;
                vh.getTitle().requestLayout();
            });
        } else if (TextUtils.isEmpty(mTitle) && !TextUtils.isEmpty(mBody)) {
            mBinders.add(vh -> {
                // Body uses top and bottom margin.
                int margin = mContext.getResources().getDimensionPixelSize(
                        R.dimen.car_padding_3);
                ViewGroup.MarginLayoutParams layoutParams =
                        (ViewGroup.MarginLayoutParams) vh.getBody().getLayoutParams();
                layoutParams.topMargin = margin;
                layoutParams.bottomMargin = margin;
                vh.getBody().requestLayout();
            });
        } else {
            mBinders.add(vh -> {
                Resources resources = mContext.getResources();
                int padding2 = resources.getDimensionPixelSize(R.dimen.car_padding_2);

                // Title has a top margin
                ViewGroup.MarginLayoutParams titleLayoutParams =
                        (ViewGroup.MarginLayoutParams) vh.getTitle().getLayoutParams();
                titleLayoutParams.topMargin = padding2;
                vh.getTitle().requestLayout();

                // Body is below title with no margin and has bottom margin.
                ViewGroup.MarginLayoutParams bodyLayoutParams =
                        (ViewGroup.MarginLayoutParams) vh.getBody().getLayoutParams();
                bodyLayoutParams.topMargin = 0;
                bodyLayoutParams.bottomMargin = padding2;
                vh.getBody().requestLayout();
            });
        }
    }

    /**
     * Sets up view(s) for supplemental action.
     */
    private void setSupplementalActions() {
        boolean hasPrimaryAction = !TextUtils.isEmpty(mPrimaryActionText);
        boolean hasSecondaryAction = !TextUtils.isEmpty(mSecondaryActionText);

        if (!hasPrimaryAction && !hasSecondaryAction) {
            return;
        }

        mBinders.add(vh -> {
            vh.setActionBorderless(mIsActionBorderless);

            if (hasSecondaryAction) {
                Button secondaryAction = vh.getSecondaryAction();

                secondaryAction.setVisibility(View.VISIBLE);
                if (mShowSecondaryActionDivider) {
                    vh.getSecondaryActionDivider().setVisibility(View.VISIBLE);
                }

                secondaryAction.setText(mSecondaryActionText);
                secondaryAction.setOnClickListener(mSecondaryActionOnClickListener);

                // Add spacing between the buttons if there is a primary action.
                int endMargin = hasPrimaryAction
                        ? mContext.getResources().getDimensionPixelSize(R.dimen.car_padding_4)
                        : 0;

                ViewGroup.MarginLayoutParams layoutParams =
                        (ViewGroup.MarginLayoutParams) secondaryAction.getLayoutParams();
                layoutParams.setMarginEnd(endMargin);
                secondaryAction.requestLayout();
            }

            if (hasPrimaryAction) {
                Button primaryAction = vh.getPrimaryAction();

                primaryAction.setVisibility(View.VISIBLE);
                if (mShowPrimaryActionDivider) {
                    vh.getPrimaryActionDivider().setVisibility(View.VISIBLE);
                }

                primaryAction.setText(mPrimaryActionText);
                primaryAction.setOnClickListener(mPrimaryActionOnClickListener);
            }
        });
    }

    /**
     * Sets {@link View.OnClickListener} of {@code ActionListItem}.
     */
    public void setOnClickListener(View.OnClickListener listener) {
        mOnClickListener = listener;
        markDirty();
    }

    /**
     * Sets {@code Primary Action} to be represented by an icon.
     *
     * @param iconResId the resource identifier of the drawable.
     * @param size The size of the icon. Must be one of {@link #PRIMARY_ACTION_ICON_SIZE_SMALL},
     *             {@link #PRIMARY_ACTION_ICON_SIZE_MEDIUM}, or
     *             {@link #PRIMARY_ACTION_ICON_SIZE_LARGE}.
     */
    public void setPrimaryActionIcon(@DrawableRes int iconResId, @PrimaryActionIconSize int size) {
        setPrimaryActionIcon(mContext.getDrawable(iconResId), size);
    }

    /**
     * Sets {@code Primary Action} to be represented by an icon.
     *
     * @param drawable the Drawable to set, or null to clear the content.
     * @param size The size of the icon. Must be one of {@link #PRIMARY_ACTION_ICON_SIZE_SMALL},
     *             {@link #PRIMARY_ACTION_ICON_SIZE_MEDIUM}, or
     *             {@link #PRIMARY_ACTION_ICON_SIZE_LARGE}.
     */
    public void setPrimaryActionIcon(@Nullable Drawable drawable, @PrimaryActionIconSize int size) {
        mPrimaryActionType = PRIMARY_ACTION_TYPE_ICON;
        mPrimaryActionIconDrawable = drawable;
        mPrimaryActionIconSize = size;
        markDirty();
    }

    /**
     * Sets {@code Primary Action} to be empty icon.
     *
     * <p>{@code Text} would have a start margin as if {@code Primary Action} were set to primary
     * icon.
     */
    public void setPrimaryActionEmptyIcon() {
        mPrimaryActionType = PRIMARY_ACTION_TYPE_EMPTY_ICON;
        markDirty();
    }

    /**
     * Sets {@code Primary Action} to have no icon. Text would align to the start of item.
     */
    public void setPrimaryActionNoIcon() {
        mPrimaryActionType = PRIMARY_ACTION_TYPE_NO_ICON;
        markDirty();
    }

    /**
     * Sets the title of item.
     *
     * <p>{@code Title} text is limited to one line, and ellipsizes at the end.
     *
     * @param title text to display as title.
     */
    public void setTitle(@NonNull CharSequence title) {
        mTitle = title;
        markDirty();
    }

    /**
     * Sets the body text of item.
     *
     * <p>Text beyond length required by regulation will be truncated. Defaults {@code Title}
     * text as the primary.
     * @param body text to be displayed.
     */
    public void setBody(@NonNull CharSequence body) {
        mBody = body;
        markDirty();
    }

    /**
     * Sets the primary action of this {@code ListItem}.
     *
     * @param text button text to display.
     * @param showDivider whether to display a vertical bar that separates {@code Text} and
     *                    {@code Action Button}.
     * @param listener the callback that will run when action button is clicked.
     * @deprecated Use {@link #setPrimaryAction(String, boolean, View.OnClickListener)} or
     * {@link #setSecondaryAction(String, boolean, View.OnClickListener)} instead to individually
     * set the actions.
     */
    @Deprecated
    public void setAction(@NonNull String text, boolean showDivider,
            @NonNull View.OnClickListener listener) {
        setPrimaryAction(text, showDivider, listener);
    }

    /**
     * Sets the primary and secondary actions for this {@code ListItem}.
     *
     * @param primaryActionText The primary action text.
     * @param showPrimaryActionDivider Whether or not to show a divider before the primary action.
     * @param primaryActionOnClickListener The listener to be invoked when the primary action is
     *                                     triggered.
     * @param secondaryActionText The secondary action text.
     * @param showSecondaryActionDivider Whether or not to show a divider before the secondary
     *                                   action.
     * @param secondaryActionOnClickListener The listener to be invoked when the secondary action is
     *                                       triggered.
     * @deprecated Use {@link #setPrimaryAction(String, boolean, View.OnClickListener)} and
     * {@link #setSecondaryAction(String, boolean, View.OnClickListener)} to set both actions.
     */
    @Deprecated
    public void setActions(@NonNull String primaryActionText, boolean showPrimaryActionDivider,
            @NonNull View.OnClickListener primaryActionOnClickListener,
            @NonNull String secondaryActionText, boolean showSecondaryActionDivider,
            @NonNull View.OnClickListener secondaryActionOnClickListener) {
        setPrimaryAction(primaryActionText, showPrimaryActionDivider, primaryActionOnClickListener);
        setSecondaryAction(secondaryActionText, showSecondaryActionDivider,
                secondaryActionOnClickListener);
    }

    /**
     * Sets the primary action of this {@code ListItem}.
     *
     * @param primaryActionText Action text to display.
     * @param showPrimaryActionDivider Whether or not to display a vertical bar before the primary
     *                                 action.
     * @param primaryActionOnClickListener The callback that will run when the action is clicked.
     *
     * @throws IllegalArgumentException If {@code primaryActionText} is {@code null} or empty.
     * @throws IllegalArgumentException If {@code primaryActionOnClickListener} is {@code null}.
     */
    public void setPrimaryAction(@NonNull String primaryActionText,
            boolean showPrimaryActionDivider,
            @NonNull View.OnClickListener primaryActionOnClickListener) {
        if (TextUtils.isEmpty(primaryActionText)) {
            throw new IllegalArgumentException("Action text cannot be empty.");
        }
        if (primaryActionOnClickListener == null) {
            throw new IllegalArgumentException("Action OnClickListener cannot be null.");
        }

        mPrimaryActionText = primaryActionText;
        mPrimaryActionOnClickListener = primaryActionOnClickListener;
        mShowPrimaryActionDivider = showPrimaryActionDivider;

        markDirty();
    }

    /**
     * Sets the secondary action of this {@code ListItem}.
     *
     * <p>The secondary action will appear before the primary action if both are set.
     *
     * @param secondaryActionText Action text to display.
     * @param showSecondaryActionDivider Whether or not to display a vertical bar before the
     *                                   secondary action.
     * @param secondaryActionOnClickListener The callback that will run when the action is clicked.
     *
     * @throws IllegalArgumentException If {@code secondaryActionText} is {@code null} or empty.
     * @throws IllegalArgumentException If {@code secondaryActionOnClickListener} is {@code null}.
     */
    public void setSecondaryAction(@NonNull String secondaryActionText,
            boolean showSecondaryActionDivider,
            @NonNull View.OnClickListener secondaryActionOnClickListener) {
        if (TextUtils.isEmpty(secondaryActionText)) {
            throw new IllegalArgumentException("Action text cannot be empty.");
        }
        if (secondaryActionOnClickListener == null) {
            throw new IllegalArgumentException("Action OnClickListener cannot be null.");
        }

        mSecondaryActionText = secondaryActionText;
        mSecondaryActionOnClickListener = secondaryActionOnClickListener;
        mShowSecondaryActionDivider = showSecondaryActionDivider;

        markDirty();
    }

    /**
     * Sets whether or not the actions should be styled as borderless.
     *
     * <p>By default, this value is {@code true}.
     *
     * @param isActionBorderless {@code true} if the actions should be borderless. {@code false}
     *                           otherwise.
     */
    public void setActionBorderless(boolean isActionBorderless) {
        mIsActionBorderless = isActionBorderless;
    }

    /**
     * Holds the children views of {@link ActionListItem}.
     */
    public static final class ViewHolder extends ListItem.ViewHolder {
        private final View[] mWidgetViews;

        private ImageView mPrimaryIcon;

        private TextView mTitle;
        private TextView mBody;

        private boolean mIsActionBorderless = true;

        private Guideline mSupplementalGuideline;

        private Button mPrimaryActionBorderless;
        private Button mPrimaryAction;
        private View mPrimaryActionDivider;

        private Button mSecondaryActionBorderless;
        private Button mSecondaryAction;
        private View mSecondaryActionDivider;

        private View mClickInterceptor;

        public ViewHolder(@NonNull View itemView) {
            super(itemView);

            mPrimaryIcon = itemView.findViewById(R.id.primary_icon);

            mTitle = itemView.findViewById(R.id.title);
            mBody = itemView.findViewById(R.id.body);

            mSupplementalGuideline = itemView.findViewById(R.id.actions_guideline);

            mPrimaryAction = itemView.findViewById(R.id.primary_action);
            mPrimaryActionBorderless = itemView.findViewById(R.id.primary_action_borderless);
            mPrimaryActionDivider = itemView.findViewById(R.id.primary_action_divider);
            mSecondaryAction = itemView.findViewById(R.id.secondary_action);
            mSecondaryActionBorderless = itemView.findViewById(R.id.secondary_action_borderless);
            mSecondaryActionDivider = itemView.findViewById(R.id.secondary_action_divider);

            mClickInterceptor = itemView.findViewById(R.id.click_interceptor);

            // Each line groups relevant child views in an effort to help keep this view array
            // updated with actual child views in the ViewHolder.
            mWidgetViews = new View[] {
                    // Primary action.
                    mPrimaryIcon,
                    // Text.
                    mTitle, mBody,
                    // Supplemental actions
                    mPrimaryAction,
                    mPrimaryActionBorderless,
                    mPrimaryActionDivider,
                    mSecondaryAction,
                    mSecondaryActionBorderless,
                    mSecondaryActionDivider
            };
        }

        @Override
        public void onUxRestrictionsChanged(@NonNull CarUxRestrictions restrictions) {
            CarUxRestrictionsUtils.apply(itemView.getContext(), restrictions, getBody());
        }

        /**
         * Sets if the action returned is styled as borderless or non-borderless.
         *
         * <p>By default, this value is {@code true}.
         *
         * @param isBorderless Whether or not the action is borderless.
         */
        public void setActionBorderless(boolean isBorderless) {
            mIsActionBorderless = isBorderless;
        }

        @NonNull
        public ImageView getPrimaryIcon() {
            return mPrimaryIcon;
        }

        @NonNull
        public TextView getTitle() {
            return mTitle;
        }

        @NonNull
        public TextView getBody() {
            return mBody;
        }

        @NonNull
        public Button getPrimaryAction() {
            return mIsActionBorderless ? mPrimaryActionBorderless : mPrimaryAction;
        }

        @NonNull
        @VisibleForTesting
        Button getBorderlessPrimaryAction() {
            return mPrimaryActionBorderless;
        }

        @NonNull
        @VisibleForTesting
        Button getBorderedPrimaryAction() {
            return mPrimaryAction;
        }

        @NonNull
        public View getPrimaryActionDivider() {
            return mPrimaryActionDivider;
        }

        @NonNull
        public Button getSecondaryAction() {
            return mIsActionBorderless ? mSecondaryActionBorderless : mSecondaryAction;
        }

        @NonNull
        @VisibleForTesting
        Button getBorderlessSecondaryAction() {
            return mSecondaryActionBorderless;
        }

        @NonNull
        @VisibleForTesting
        Button getBorderedSecondaryAction() {
            return mSecondaryAction;
        }

        @NonNull
        public View getSecondaryActionDivider() {
            return mSecondaryActionDivider;
        }

        @NonNull
        View[] getWidgetViews() {
            return mWidgetViews;
        }

        /** Returns the Guideline that the actions should be centered upon. */
        @NonNull
        Guideline getSupplementalGuideline() {
            return mSupplementalGuideline;
        }

        /**
         * Returns the view that will intercept clicks beneath the supplemental icon and action
         * views.
         */
        @NonNull
        View getClickInterceptView() {
            return mClickInterceptor;
        }
    }
}