Implement header list item.

Bug: 143234489
Test: CarUiListItemTest

Change-Id: I1304ff34535666afd3fcc82653c8e196cb7586f4
diff --git a/car-ui-lib/res/layout/car_ui_header_list_item.xml b/car-ui-lib/res/layout/car_ui_header_list_item.xml
new file mode 100644
index 0000000..c1fa1dc
--- /dev/null
+++ b/car-ui-lib/res/layout/car_ui_header_list_item.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright 2019 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.
+  -->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="@dimen/car_ui_header_list_item_height">
+
+    <androidx.constraintlayout.widget.Guideline
+        android:id="@+id/car_ui_list_item_start_guideline"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        app:layout_constraintGuide_begin="@dimen/car_ui_list_item_header_start_inset" />
+
+    <TextView
+        android:id="@+id/title"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:textAppearance="@style/TextAppearance.CarUi.ListItem"
+        app:layout_constraintBottom_toTopOf="@+id/body"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="@id/car_ui_list_item_start_guideline"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintVertical_chainStyle="packed" />
+
+    <TextView
+        android:id="@+id/body"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:textAppearance="@style/TextAppearance.CarUi.ListItem.Body"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="@id/car_ui_list_item_start_guideline"
+        app:layout_constraintTop_toBottomOf="@+id/title" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/car-ui-lib/res/values/dimens.xml b/car-ui-lib/res/values/dimens.xml
index e025301..1ffa471 100644
--- a/car-ui-lib/res/values/dimens.xml
+++ b/car-ui-lib/res/values/dimens.xml
@@ -176,6 +176,8 @@
     <!-- List item  -->
 
     <dimen name="car_ui_list_item_height">116dp</dimen>
+    <dimen name="car_ui_header_list_item_height">116dp</dimen>
+    <dimen name="car_ui_list_item_header_start_inset">0dp</dimen>
     <dimen name="car_ui_list_item_start_inset">0dp</dimen>
     <dimen name="car_ui_list_item_end_inset">0dp</dimen>
     <dimen name="car_ui_list_item_text_start_margin">0dp</dimen>
diff --git a/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiContentListItem.java b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiContentListItem.java
new file mode 100644
index 0000000..b1ff760
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiContentListItem.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright 2019 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 com.android.car.ui.recyclerview;
+
+import android.graphics.drawable.Drawable;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * Definition of list items that can be inserted into {@link CarUiListItemAdapter}.
+ */
+public class CarUiContentListItem extends CarUiListItem {
+
+    /**
+     * Callback to be invoked when the checked state of a list item changed.
+     */
+    public interface OnCheckedChangedListener {
+        /**
+         * Called when the checked state of a list item has changed.
+         *
+         * @param isChecked new checked state of list item.
+         */
+        void onCheckedChanged(boolean isChecked);
+    }
+
+    /**
+     * Enum of secondary action types of a list item.
+     */
+    public enum Action {
+        /**
+         * For an action value of NONE, no action element is shown for a list item.
+         */
+        NONE,
+        /**
+         * For an action value of SWITCH, a switch is shown for the action element of the list item.
+         */
+        SWITCH,
+        /**
+         * For an action value of CHECK_BOX, a checkbox is shown for the action element of the list
+         * item.
+         */
+        CHECK_BOX,
+        /**
+         * For an action value of ICON, an icon is shown for the action element of the list item.
+         */
+        ICON
+    }
+
+    private Drawable mIcon;
+    @Nullable
+    private Drawable mSupplementalIcon;
+    private CharSequence mTitle;
+    private CharSequence mBody;
+    private Action mAction;
+    private boolean mIsActionDividerVisible;
+    private boolean mIsChecked;
+    private OnCheckedChangedListener mOnCheckedChangedListener;
+    private View.OnClickListener mSupplementalIconOnClickListener;
+
+    public CarUiContentListItem() {
+        mAction = Action.NONE;
+    }
+
+    /**
+     * Returns the title of the item.
+     */
+    @Nullable
+    public CharSequence getTitle() {
+        return mTitle;
+    }
+
+    /**
+     * Sets the title of the item.
+     *
+     * @param title text to display as title.
+     */
+    public void setTitle(@NonNull CharSequence title) {
+        mTitle = title;
+    }
+
+    /**
+     * Returns the body text of the item.
+     */
+    @Nullable
+    public CharSequence getBody() {
+        return mBody;
+    }
+
+    /**
+     * Sets the body of the item.
+     *
+     * @param body text to display as body text.
+     */
+    public void setBody(@NonNull CharSequence body) {
+        mBody = body;
+    }
+
+    /**
+     * Returns the icon of the item.
+     */
+    @Nullable
+    public Drawable getIcon() {
+        return mIcon;
+    }
+
+    /**
+     * Sets the icon of the item.
+     *
+     * @param icon the icon to display.
+     */
+    public void setIcon(@Nullable Drawable icon) {
+        mIcon = icon;
+    }
+
+    /**
+     * Returns {@code true} if the item is checked. Will always return {@code false} when the action
+     * type for the item is {@code Action.NONE}.
+     */
+    public boolean isChecked() {
+        return mIsChecked;
+    }
+
+    /**
+     * Sets the checked state of the item.
+     *
+     * @param checked the checked state for the item.
+     */
+    public void setChecked(boolean checked) {
+        // Checked state can only be set when action type is checkbox or switch.
+        if (mAction == Action.CHECK_BOX || mAction == Action.SWITCH) {
+            mIsChecked = checked;
+        }
+    }
+
+    /**
+     * Sets the visibility of the action divider.
+     *
+     * @param visible visibility of the action divider.
+     */
+    public void setActionDividerVisible(boolean visible) {
+        mIsActionDividerVisible = visible;
+    }
+
+    /**
+     * Returns {@code true} if the action divider is visible.
+     */
+    public boolean isActionDividerVisible() {
+        return mIsActionDividerVisible;
+    }
+
+    /**
+     * Returns the action type for the item.
+     */
+    public Action getAction() {
+        return mAction;
+    }
+
+    /**
+     * Sets the action type for the item.
+     *
+     * @param action the action type for the item.
+     */
+    public void setAction(Action action) {
+        mAction = action;
+
+        // Cannot have checked state be true when there action type is not checkbox or switch.
+        if (mAction != Action.CHECK_BOX && mAction != Action.SWITCH) {
+            mIsChecked = false;
+        }
+    }
+
+    /**
+     * Returns the supplemental icon for the item.
+     */
+    @Nullable
+    public Drawable getSupplementalIcon() {
+        if (mAction != Action.ICON) {
+            return null;
+        }
+
+        return mSupplementalIcon;
+    }
+
+    /**
+     * Sets supplemental icon to be displayed in a list item.
+     *
+     * @param icon the Drawable to set as the icon, or null to clear the content.
+     */
+    public void setSupplementalIcon(@Nullable Drawable icon) {
+        setSupplementalIcon(icon, null);
+    }
+
+    /**
+     * Sets supplemental icon to be displayed in a list item.
+     *
+     * @param icon     the Drawable to set as the icon, or null to clear the content.
+     * @param listener the callback that is invoked when the icon is clicked.
+     */
+    public void setSupplementalIcon(@Nullable Drawable icon,
+            @Nullable View.OnClickListener listener) {
+        mAction = Action.ICON;
+
+        // Cannot have checked state when action type is {@code Action.ICON}.
+        mIsChecked = false;
+
+        mSupplementalIcon = icon;
+        mSupplementalIconOnClickListener = listener;
+    }
+
+    View.OnClickListener getSupplementalIconOnClickListener() {
+        return mSupplementalIconOnClickListener;
+    }
+
+    /**
+     * Registers a callback to be invoked when the checked state of list item changes.
+     *
+     * <p>Checked state changes can take place when the action type is {@code Action.SWITCH} or
+     * {@code Action.CHECK_BOX}.
+     *
+     * @param listener callback to be invoked when the checked state shown in the UI changes.
+     */
+    public void setOnCheckedChangedListener(
+            @NonNull OnCheckedChangedListener listener) {
+        mOnCheckedChangedListener = listener;
+    }
+
+    @Nullable
+    OnCheckedChangedListener getOnCheckedChangedListener() {
+        return mOnCheckedChangedListener;
+    }
+}
diff --git a/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiHeaderListItem.java b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiHeaderListItem.java
new file mode 100644
index 0000000..57fd755
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiHeaderListItem.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2019 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 com.android.car.ui.recyclerview;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Definition of list item header that can be inserted into {@link CarUiListItemAdapter}.
+ */
+public class CarUiHeaderListItem extends CarUiListItem {
+
+    private CharSequence mTitle;
+    private CharSequence mBody;
+
+    public CarUiHeaderListItem(@NonNull CharSequence title) {
+        this(title, "");
+    }
+
+    public CarUiHeaderListItem(@NonNull CharSequence title, @NonNull CharSequence body) {
+        mTitle = title;
+        mBody = body;
+    }
+
+    /**
+     * Returns the title text for the header.
+     */
+    public CharSequence getTitle() {
+        return mTitle;
+    }
+
+    /**
+     * Returns the body text for the header.
+     */
+    public CharSequence getBody() {
+        return mBody;
+    }
+}
diff --git a/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiListItem.java b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiListItem.java
index 88cd938..c894774 100644
--- a/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiListItem.java
+++ b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiListItem.java
@@ -1,247 +1,7 @@
-/*
- * Copyright 2019 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 com.android.car.ui.recyclerview;
 
-import android.graphics.drawable.Drawable;
-import android.view.View;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
 /**
- * Definition of items that can be inserted into {@link CarUiListItemAdapter}.
+ * All items that can be inserted into {@link CarUiListItemAdapter} must extend this class.
  */
-public class CarUiListItem {
-
-    /**
-     * Callback to be invoked when the checked state of a list item changed.
-     */
-    public interface OnCheckedChangedListener {
-        /**
-         * Called when the checked state of a list item has changed.
-         *
-         * @param isChecked new checked state of list item.
-         */
-        void onCheckedChanged(boolean isChecked);
-    }
-
-    /**
-     * Enum of secondary action types of a list item.
-     */
-    public enum Action {
-        /**
-         * For an action value of NONE, no action element is shown for a list item.
-         */
-        NONE,
-        /**
-         * For an action value of SWITCH, a switch is shown for the action element of the list item.
-         */
-        SWITCH,
-        /**
-         * For an action value of CHECK_BOX, a checkbox is shown for the action element of the list
-         * item.
-         */
-        CHECK_BOX,
-        /**
-         * For an action value of ICON, an icon is shown for the action element of the list item.
-         */
-        ICON
-    }
-
-    private Drawable mIcon;
-    @Nullable
-    private Drawable mSupplementalIcon;
-    private CharSequence mTitle;
-    private CharSequence mBody;
-    private Action mAction;
-    private boolean mIsActionDividerVisible;
-    private boolean mIsChecked;
-    private OnCheckedChangedListener mOnCheckedChangedListener;
-    private View.OnClickListener mSupplementalIconOnClickListener;
-
-    public CarUiListItem() {
-        mAction = Action.NONE;
-    }
-
-    /**
-     * Returns the title of the item.
-     */
-    @Nullable
-    public CharSequence getTitle() {
-        return mTitle;
-    }
-
-    /**
-     * Sets the title of the item.
-     *
-     * @param title text to display as title.
-     */
-    public void setTitle(@NonNull CharSequence title) {
-        mTitle = title;
-    }
-
-    /**
-     * Returns the body text of the item.
-     */
-    @Nullable
-    public CharSequence getBody() {
-        return mBody;
-    }
-
-    /**
-     * Sets the body of the item.
-     *
-     * @param body text to display as body text.
-     */
-    public void setBody(@NonNull CharSequence body) {
-        mBody = body;
-    }
-
-    /**
-     * Returns the icon of the item.
-     */
-    @Nullable
-    public Drawable getIcon() {
-        return mIcon;
-    }
-
-    /**
-     * Sets the icon of the item.
-     *
-     * @param icon the icon to display.
-     */
-    public void setIcon(@Nullable Drawable icon) {
-        mIcon = icon;
-    }
-
-    /**
-     * Returns {@code true} if the item is checked. Will always return {@code false} when the action
-     * type for the item is {@code Action.NONE}.
-     */
-    public boolean isChecked() {
-        return mIsChecked;
-    }
-
-    /**
-     * Sets the checked state of the item.
-     *
-     * @param checked the checked state for the item.
-     */
-    public void setChecked(boolean checked) {
-        // Checked state can only be set when action type is checkbox or switch.
-        if (mAction == Action.CHECK_BOX || mAction == Action.SWITCH) {
-            mIsChecked = checked;
-        }
-    }
-
-    /**
-     * Sets the visibility of the action divider.
-     *
-     * @param visible visibility of the action divider.
-     */
-    public void setActionDividerVisible(boolean visible) {
-        mIsActionDividerVisible = visible;
-    }
-
-    /**
-     * Returns {@code true} if the action divider is visible.
-     */
-    public boolean isActionDividerVisible() {
-        return mIsActionDividerVisible;
-    }
-
-    /**
-     * Returns the action type for the item.
-     */
-    public Action getAction() {
-        return mAction;
-    }
-
-    /**
-     * Sets the action type for the item.
-     *
-     * @param action the action type for the item.
-     */
-    public void setAction(Action action) {
-        mAction = action;
-
-        // Cannot have checked state be true when there action type is not checkbox or switch.
-        if (mAction != Action.CHECK_BOX && mAction != Action.SWITCH) {
-            mIsChecked = false;
-        }
-    }
-
-    /**
-     * Returns the supplemental icon for the item.
-     */
-    @Nullable
-    public Drawable getSupplementalIcon() {
-        if (mAction != Action.ICON) {
-            return null;
-        }
-
-        return mSupplementalIcon;
-    }
-
-    /**
-     * Sets supplemental icon to be displayed in a list item.
-     *
-     * @param icon the Drawable to set as the icon, or null to clear the content.
-     */
-    public void setSupplementalIcon(@Nullable Drawable icon) {
-        setSupplementalIcon(icon, null);
-    }
-
-    /**
-     * Sets supplemental icon to be displayed in a list item.
-     *
-     * @param icon the Drawable to set as the icon, or null to clear the content.
-     * @param listener the callback that is invoked when the icon is clicked.
-     */
-    public void setSupplementalIcon(@Nullable Drawable icon,
-            @Nullable View.OnClickListener listener) {
-        mAction = Action.ICON;
-
-        // Cannot have checked state when action type is {@code Action.ICON}.
-        mIsChecked = false;
-
-        mSupplementalIcon = icon;
-        mSupplementalIconOnClickListener = listener;
-    }
-
-    View.OnClickListener getSupplementalIconOnClickListener() {
-        return mSupplementalIconOnClickListener;
-    }
-
-    /**
-     * Registers a callback to be invoked when the checked state of list item changes.
-     *
-     * <p>Checked state changes can take place when the action type is {@code Action.SWITCH} or
-     * {@code Action.CHECK_BOX}.
-     *
-     * @param listener callback to be invoked when the checked state shown in the UI changes.
-     */
-    public void setOnCheckedChangedListener(
-            @NonNull OnCheckedChangedListener listener) {
-        mOnCheckedChangedListener = listener;
-    }
-
-    @Nullable
-    OnCheckedChangedListener getOnCheckedChangedListener() {
-        return mOnCheckedChangedListener;
-    }
+public abstract class CarUiListItem {
 }
diff --git a/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiListItemAdapter.java b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiListItemAdapter.java
index 5a594e3..af705c9 100644
--- a/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiListItemAdapter.java
+++ b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiListItemAdapter.java
@@ -34,16 +34,20 @@
 import java.util.List;
 
 /**
- * Adapter for {@link CarUiRecyclerView} to display {@link CarUiListItem}.
+ * Adapter for {@link CarUiRecyclerView} to display {@link CarUiContentListItem} and {@link
+ * CarUiHeaderListItem}.
  *
  * <ul>
  * <li> Implements {@link CarUiRecyclerView.ItemCap} - defaults to unlimited item count.
  * </ul>
  */
 public class CarUiListItemAdapter extends
-        RecyclerView.Adapter<CarUiListItemAdapter.ViewHolder> implements
+        RecyclerView.Adapter<RecyclerView.ViewHolder> implements
         CarUiRecyclerView.ItemCap {
 
+    private static final int VIEW_TYPE_LIST_ITEM = 1;
+    private static final int VIEW_TYPE_LIST_HEADER = 2;
+
     private List<CarUiListItem> mItems;
     private int mMaxItems = CarUiRecyclerView.ItemCap.UNLIMITED;
 
@@ -53,11 +57,20 @@
 
     @NonNull
     @Override
-    public ViewHolder onCreateViewHolder(
+    public RecyclerView.ViewHolder onCreateViewHolder(
             @NonNull ViewGroup parent, int viewType) {
         LayoutInflater inflater = LayoutInflater.from(parent.getContext());
-        View view = inflater.inflate(R.layout.car_ui_list_item, parent, false);
-        return new ViewHolder(view);
+
+        switch (viewType) {
+            case VIEW_TYPE_LIST_ITEM:
+                return new ListItemViewHolder(
+                        inflater.inflate(R.layout.car_ui_list_item, parent, false));
+            case VIEW_TYPE_LIST_HEADER:
+                return new HeaderViewHolder(
+                        inflater.inflate(R.layout.car_ui_header_list_item, parent, false));
+            default:
+                throw new IllegalStateException("Unknown item type.");
+        }
     }
 
     /**
@@ -72,8 +85,54 @@
     }
 
     @Override
-    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
-        CarUiListItem item = mItems.get(position);
+    public int getItemViewType(int position) {
+        if (mItems.get(position) instanceof CarUiContentListItem) {
+            return VIEW_TYPE_LIST_ITEM;
+        } else if (mItems.get(position) instanceof CarUiHeaderListItem) {
+            return VIEW_TYPE_LIST_HEADER;
+        }
+
+        throw new IllegalStateException("Unknown view type.");
+    }
+
+    @Override
+    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
+        switch (holder.getItemViewType()) {
+            case VIEW_TYPE_LIST_ITEM:
+                if (!(holder instanceof ListItemViewHolder)) {
+                    throw new IllegalStateException("Incorrect view holder type for list item.");
+                }
+
+                CarUiListItem item = mItems.get(position);
+                if (!(item instanceof CarUiContentListItem)) {
+                    throw new IllegalStateException(
+                            "Expected item to be bound to viewholder to be instance of "
+                                    + "CarUiContentListItem.");
+                }
+
+                onBindListItemViewHolder((ListItemViewHolder) holder, (CarUiContentListItem) item);
+                break;
+            case VIEW_TYPE_LIST_HEADER:
+                if (!(holder instanceof HeaderViewHolder)) {
+                    throw new IllegalStateException("Incorrect view holder type for list item.");
+                }
+
+                CarUiListItem header = mItems.get(position);
+                if (!(header instanceof CarUiHeaderListItem)) {
+                    throw new IllegalStateException(
+                            "Expected item to be bound to viewholder to be instance of "
+                                    + "CarUiHeaderListItem.");
+                }
+
+                onBindHeaderViewHolder((HeaderViewHolder) holder, (CarUiHeaderListItem) header);
+                break;
+            default:
+                throw new IllegalStateException("Unknown item view type.");
+        }
+    }
+
+    private void onBindListItemViewHolder(@NonNull ListItemViewHolder holder,
+            @NonNull CarUiContentListItem item) {
         CharSequence title = item.getTitle();
         CharSequence body = item.getBody();
         Drawable icon = item.getIcon();
@@ -116,7 +175,7 @@
                 switchWidget.setOnCheckedChangeListener(
                         (buttonView, isChecked) -> {
                             item.setChecked(isChecked);
-                            CarUiListItem.OnCheckedChangedListener itemListener =
+                            CarUiContentListItem.OnCheckedChangedListener itemListener =
                                     item.getOnCheckedChangedListener();
                             if (itemListener != null) {
                                 itemListener.onCheckedChanged(isChecked);
@@ -132,7 +191,7 @@
                 checkBox.setOnCheckedChangeListener(
                         (buttonView, isChecked) -> {
                             item.setChecked(isChecked);
-                            CarUiListItem.OnCheckedChangedListener itemListener =
+                            CarUiContentListItem.OnCheckedChangedListener itemListener =
                                     item.getOnCheckedChangedListener();
                             if (itemListener != null) {
                                 itemListener.onCheckedChanged(isChecked);
@@ -160,6 +219,18 @@
         }
     }
 
+    private void onBindHeaderViewHolder(@NonNull HeaderViewHolder holder,
+            @NonNull CarUiHeaderListItem item) {
+        holder.getTitle().setText(item.getTitle());
+
+        CharSequence body = item.getBody();
+        if (!TextUtils.isEmpty(body)) {
+            holder.getBody().setText(body);
+        } else {
+            holder.getBody().setVisibility(View.GONE);
+        }
+    }
+
     @Override
     public int getItemCount() {
         return mMaxItems == CarUiRecyclerView.ItemCap.UNLIMITED
@@ -173,9 +244,9 @@
     }
 
     /**
-     * Holds views of {@link CarUiListItem}.
+     * Holds views of {@link CarUiContentListItem}.
      */
-    static class ViewHolder extends RecyclerView.ViewHolder {
+    static class ListItemViewHolder extends RecyclerView.ViewHolder {
 
         private TextView mTitle;
         private TextView mBody;
@@ -187,7 +258,7 @@
         private CheckBox mCheckBox;
         private ImageView mSupplementalIcon;
 
-        ViewHolder(@NonNull View itemView) {
+        ListItemViewHolder(@NonNull View itemView) {
             super(itemView);
             mTitle = itemView.requireViewById(R.id.title);
             mBody = itemView.requireViewById(R.id.body);
@@ -246,4 +317,29 @@
         }
 
     }
+
+    /**
+     * Holds views of {@link CarUiHeaderListItem}.
+     */
+    static class HeaderViewHolder extends RecyclerView.ViewHolder {
+
+        private TextView mTitle;
+        private TextView mBody;
+
+        HeaderViewHolder(@NonNull View itemView) {
+            super(itemView);
+            mTitle = itemView.requireViewById(R.id.title);
+            mBody = itemView.requireViewById(R.id.body);
+        }
+
+        @NonNull
+        TextView getTitle() {
+            return mTitle;
+        }
+
+        @NonNull
+        TextView getBody() {
+            return mBody;
+        }
+    }
 }
diff --git a/car-ui-lib/tests/apitest/current.xml b/car-ui-lib/tests/apitest/current.xml
index d0dfd57..6d809ca 100644
--- a/car-ui-lib/tests/apitest/current.xml
+++ b/car-ui-lib/tests/apitest/current.xml
@@ -46,11 +46,13 @@
   <public type="dimen" name="car_ui_dialog_edittext_margin_end"/>
   <public type="dimen" name="car_ui_dialog_edittext_margin_start"/>
   <public type="dimen" name="car_ui_dialog_edittext_margin_top"/>
+  <public type="dimen" name="car_ui_header_list_item_height"/>
   <public type="dimen" name="car_ui_letter_spacing_body1"/>
   <public type="dimen" name="car_ui_letter_spacing_body3"/>
   <public type="dimen" name="car_ui_list_item_action_divider_height"/>
   <public type="dimen" name="car_ui_list_item_action_divider_width"/>
   <public type="dimen" name="car_ui_list_item_end_inset"/>
+  <public type="dimen" name="car_ui_list_item_header_start_inset"/>
   <public type="dimen" name="car_ui_list_item_height"/>
   <public type="dimen" name="car_ui_list_item_icon_container_width"/>
   <public type="dimen" name="car_ui_list_item_icon_size"/>
diff --git a/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/caruirecyclerview/CarUiListItemActivity.java b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/caruirecyclerview/CarUiListItemActivity.java
index 0d65ee9..636b0ba 100644
--- a/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/caruirecyclerview/CarUiListItemActivity.java
+++ b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/caruirecyclerview/CarUiListItemActivity.java
@@ -22,13 +22,17 @@
 import android.widget.Toast;
 
 import com.android.car.ui.paintbooth.R;
+import com.android.car.ui.recyclerview.CarUiContentListItem;
+import com.android.car.ui.recyclerview.CarUiHeaderListItem;
 import com.android.car.ui.recyclerview.CarUiListItem;
 import com.android.car.ui.recyclerview.CarUiListItemAdapter;
 import com.android.car.ui.recyclerview.CarUiRecyclerView;
 
 import java.util.ArrayList;
 
-/** Activity that shows {@link CarUiRecyclerView} with dummy {@link CarUiListItem} entries. */
+/**
+ * Activity that shows {@link CarUiRecyclerView} with dummy {@link CarUiContentListItem} entries
+ */
 public class CarUiListItemActivity extends Activity {
 
     private final ArrayList<CarUiListItem> mData = new ArrayList<>();
@@ -46,56 +50,61 @@
     private ArrayList<CarUiListItem> generateDummyData() {
         Context context = this;
 
-        CarUiListItem item = new CarUiListItem();
+        CarUiHeaderListItem header = new CarUiHeaderListItem("First header");
+        mData.add(header);
+
+        CarUiContentListItem item = new CarUiContentListItem();
         item.setTitle("Test title");
         item.setBody("Test body");
         mData.add(item);
 
-        item = new CarUiListItem();
+        item = new CarUiContentListItem();
         item.setTitle("Test title with no body");
         mData.add(item);
 
-        item = new CarUiListItem();
+        header = new CarUiHeaderListItem("Random header", "with header body");
+        mData.add(header);
+
+        item = new CarUiContentListItem();
         item.setBody("Test body with no title");
         mData.add(item);
 
-        item = new CarUiListItem();
+        item = new CarUiContentListItem();
         item.setTitle("Test Title");
         item.setIcon(getDrawable(R.drawable.ic_launcher));
         mData.add(item);
 
-        item = new CarUiListItem();
+        item = new CarUiContentListItem();
         item.setTitle("Test Title");
         item.setBody("Test body text");
         item.setIcon(getDrawable(R.drawable.ic_launcher));
         mData.add(item);
 
-        item = new CarUiListItem();
+        item = new CarUiContentListItem();
         item.setIcon(getDrawable(R.drawable.ic_launcher));
         item.setTitle("Title -- Item with checkbox");
         item.setBody("Will present toast on change of selection state.");
         item.setOnCheckedChangedListener(
                 (isChecked) -> Toast.makeText(context,
                         "Item checked state is: " + isChecked, Toast.LENGTH_SHORT).show());
-        item.setAction(CarUiListItem.Action.CHECK_BOX);
+        item.setAction(CarUiContentListItem.Action.CHECK_BOX);
         mData.add(item);
 
-        item = new CarUiListItem();
+        item = new CarUiContentListItem();
         item.setIcon(getDrawable(R.drawable.ic_launcher));
-        item.setBody("Body -- Item with switch -- and divider");
-        item.setActionDividerVisible(true);
-        item.setAction(CarUiListItem.Action.SWITCH);
+        item.setBody("Body -- Item with switch");
+        item.setAction(CarUiContentListItem.Action.SWITCH);
         mData.add(item);
 
-        item = new CarUiListItem();
+        item = new CarUiContentListItem();
         item.setIcon(getDrawable(R.drawable.ic_launcher));
         item.setTitle("Title -- Item with checkbox");
         item.setBody("Item is initially checked");
-        item.setAction(CarUiListItem.Action.CHECK_BOX);
+        item.setAction(CarUiContentListItem.Action.CHECK_BOX);
         item.setChecked(true);
         mData.add(item);
 
-        item = new CarUiListItem();
+        item = new CarUiContentListItem();
         item.setIcon(getDrawable(R.drawable.ic_launcher));
         item.setTitle("Title");
         item.setBody("Random body text -- with action divider");
@@ -104,14 +113,14 @@
         item.setChecked(true);
         mData.add(item);
 
-        item = new CarUiListItem();
+        item = new CarUiContentListItem();
         item.setIcon(getDrawable(R.drawable.ic_launcher));
         item.setTitle("Null supplemental icon");
-        item.setAction(CarUiListItem.Action.ICON);
+        item.setAction(CarUiContentListItem.Action.ICON);
         item.setChecked(true);
         mData.add(item);
 
-        item = new CarUiListItem();
+        item = new CarUiContentListItem();
         item.setTitle("Supplemental icon with listener");
         item.setSupplementalIcon(getDrawable(R.drawable.ic_launcher),
                 v -> Toast.makeText(context, "Clicked supplemental icon",
diff --git a/car-ui-lib/tests/robotests/src/com/android/car/ui/recyclerview/CarUiListItemTest.java b/car-ui-lib/tests/robotests/src/com/android/car/ui/recyclerview/CarUiListItemTest.java
index 972b7c5..b89daa2 100644
--- a/car-ui-lib/tests/robotests/src/com/android/car/ui/recyclerview/CarUiListItemTest.java
+++ b/car-ui-lib/tests/robotests/src/com/android/car/ui/recyclerview/CarUiListItemTest.java
@@ -49,7 +49,7 @@
     private Context mContext;
 
     @Mock
-    CarUiListItem.OnCheckedChangedListener mOnCheckedChangedListener;
+    CarUiContentListItem.OnCheckedChangedListener mOnCheckedChangedListener;
 
     @Before
     public void setUp() {
@@ -58,8 +58,13 @@
         mListView = new CarUiRecyclerView(mContext);
     }
 
-    private CarUiListItemAdapter.ViewHolder getViewHolderAtPosition(int position) {
-        return (CarUiListItemAdapter.ViewHolder) mListView.findViewHolderForAdapterPosition(
+    private CarUiListItemAdapter.ListItemViewHolder getListItemViewHolderAtPosition(int position) {
+        return (CarUiListItemAdapter.ListItemViewHolder) mListView.findViewHolderForAdapterPosition(
+                position);
+    }
+
+    private CarUiListItemAdapter.HeaderViewHolder getHeaderViewHolderAtPosition(int position) {
+        return (CarUiListItemAdapter.HeaderViewHolder) mListView.findViewHolderForAdapterPosition(
                 position);
     }
 
@@ -83,17 +88,21 @@
     public void testItemVisibility_withTitle() {
         List<CarUiListItem> items = new ArrayList<>();
 
-        CarUiListItem item = new CarUiListItem();
+        CarUiContentListItem item = new CarUiContentListItem();
         item.setTitle("Test title");
         items.add(item);
 
         updateRecyclerViewAdapter(new CarUiListItemAdapter(items));
 
-        assertThat(getViewHolderAtPosition(0).getTitle().getVisibility()).isEqualTo(View.VISIBLE);
-        assertThat(getViewHolderAtPosition(0).getBody().getVisibility()).isNotEqualTo(View.VISIBLE);
-        assertThat(getViewHolderAtPosition(0).getIconContainer().getVisibility()).isNotEqualTo(
+        assertThat(getListItemViewHolderAtPosition(0).getTitle().getVisibility()).isEqualTo(
                 View.VISIBLE);
-        assertThat(getViewHolderAtPosition(0).getActionContainer().getVisibility()).isNotEqualTo(
+        assertThat(getListItemViewHolderAtPosition(0).getBody().getVisibility()).isNotEqualTo(
+                View.VISIBLE);
+        assertThat(getListItemViewHolderAtPosition(
+                0).getIconContainer().getVisibility()).isNotEqualTo(
+                View.VISIBLE);
+        assertThat(getListItemViewHolderAtPosition(
+                0).getActionContainer().getVisibility()).isNotEqualTo(
                 View.VISIBLE);
     }
 
@@ -101,18 +110,22 @@
     public void testItemVisibility_withTitle_withBody() {
         List<CarUiListItem> items = new ArrayList<>();
 
-        CarUiListItem item = new CarUiListItem();
+        CarUiContentListItem item = new CarUiContentListItem();
         item.setTitle("Test title");
         item.setBody("Test body");
         items.add(item);
 
         updateRecyclerViewAdapter(new CarUiListItemAdapter(items));
 
-        assertThat(getViewHolderAtPosition(0).getTitle().getVisibility()).isEqualTo(View.VISIBLE);
-        assertThat(getViewHolderAtPosition(0).getBody().getVisibility()).isEqualTo(View.VISIBLE);
-        assertThat(getViewHolderAtPosition(0).getIconContainer().getVisibility()).isNotEqualTo(
+        assertThat(getListItemViewHolderAtPosition(0).getTitle().getVisibility()).isEqualTo(
                 View.VISIBLE);
-        assertThat(getViewHolderAtPosition(0).getActionContainer().getVisibility()).isNotEqualTo(
+        assertThat(getListItemViewHolderAtPosition(0).getBody().getVisibility()).isEqualTo(
+                View.VISIBLE);
+        assertThat(getListItemViewHolderAtPosition(
+                0).getIconContainer().getVisibility()).isNotEqualTo(
+                View.VISIBLE);
+        assertThat(getListItemViewHolderAtPosition(
+                0).getActionContainer().getVisibility()).isNotEqualTo(
                 View.VISIBLE);
     }
 
@@ -120,19 +133,23 @@
     public void testItemVisibility_withTitle_withIcon() {
         List<CarUiListItem> items = new ArrayList<>();
 
-        CarUiListItem item = new CarUiListItem();
+        CarUiContentListItem item = new CarUiContentListItem();
         item.setTitle("Test title");
         item.setIcon(mContext.getDrawable(R.drawable.car_ui_icon_close));
         items.add(item);
 
         updateRecyclerViewAdapter(new CarUiListItemAdapter(items));
 
-        assertThat(getViewHolderAtPosition(0).getTitle().getVisibility()).isEqualTo(View.VISIBLE);
-        assertThat(getViewHolderAtPosition(0).getBody().getVisibility()).isNotEqualTo(View.VISIBLE);
-        assertThat(getViewHolderAtPosition(0).getIconContainer().getVisibility()).isEqualTo(
+        assertThat(getListItemViewHolderAtPosition(0).getTitle().getVisibility()).isEqualTo(
                 View.VISIBLE);
-        assertThat(getViewHolderAtPosition(0).getIcon().getVisibility()).isEqualTo(View.VISIBLE);
-        assertThat(getViewHolderAtPosition(0).getActionContainer().getVisibility()).isNotEqualTo(
+        assertThat(getListItemViewHolderAtPosition(0).getBody().getVisibility()).isNotEqualTo(
+                View.VISIBLE);
+        assertThat(getListItemViewHolderAtPosition(0).getIconContainer().getVisibility()).isEqualTo(
+                View.VISIBLE);
+        assertThat(getListItemViewHolderAtPosition(0).getIcon().getVisibility()).isEqualTo(
+                View.VISIBLE);
+        assertThat(getListItemViewHolderAtPosition(
+                0).getActionContainer().getVisibility()).isNotEqualTo(
                 View.VISIBLE);
     }
 
@@ -140,47 +157,56 @@
     public void testItemVisibility_withTitle_withCheckbox() {
         List<CarUiListItem> items = new ArrayList<>();
 
-        CarUiListItem item = new CarUiListItem();
+        CarUiContentListItem item = new CarUiContentListItem();
         item.setTitle("Test title");
-        item.setAction(CarUiListItem.Action.CHECK_BOX);
+        item.setAction(CarUiContentListItem.Action.CHECK_BOX);
         items.add(item);
 
         updateRecyclerViewAdapter(new CarUiListItemAdapter(items));
 
-        assertThat(getViewHolderAtPosition(0).getTitle().getVisibility()).isEqualTo(View.VISIBLE);
-        assertThat(getViewHolderAtPosition(0).getBody().getVisibility()).isNotEqualTo(View.VISIBLE);
-        assertThat(getViewHolderAtPosition(0).getIconContainer().getVisibility()).isNotEqualTo(
+        assertThat(getListItemViewHolderAtPosition(0).getTitle().getVisibility()).isEqualTo(
                 View.VISIBLE);
-        assertThat(getViewHolderAtPosition(0).getActionContainer().getVisibility()).isEqualTo(
+        assertThat(getListItemViewHolderAtPosition(0).getBody().getVisibility()).isNotEqualTo(
                 View.VISIBLE);
-        assertThat(getViewHolderAtPosition(0).getSwitch().getVisibility()).isNotEqualTo(
+        assertThat(getListItemViewHolderAtPosition(
+                0).getIconContainer().getVisibility()).isNotEqualTo(
                 View.VISIBLE);
-        assertThat(getViewHolderAtPosition(0).getCheckBox().getVisibility()).isEqualTo(
+        assertThat(getListItemViewHolderAtPosition(
+                0).getActionContainer().getVisibility()).isEqualTo(
                 View.VISIBLE);
-        assertThat(getViewHolderAtPosition(0).getCheckBox().isChecked()).isEqualTo(false);
+        assertThat(getListItemViewHolderAtPosition(0).getSwitch().getVisibility()).isNotEqualTo(
+                View.VISIBLE);
+        assertThat(getListItemViewHolderAtPosition(0).getCheckBox().getVisibility()).isEqualTo(
+                View.VISIBLE);
+        assertThat(getListItemViewHolderAtPosition(0).getCheckBox().isChecked()).isEqualTo(false);
     }
 
     @Test
     public void testItemVisibility_withTitle_withBody_withSwitch() {
         List<CarUiListItem> items = new ArrayList<>();
 
-        CarUiListItem item = new CarUiListItem();
+        CarUiContentListItem item = new CarUiContentListItem();
         item.setTitle("Test title");
         item.setBody("Body text");
-        item.setAction(CarUiListItem.Action.SWITCH);
+        item.setAction(CarUiContentListItem.Action.SWITCH);
         items.add(item);
 
         updateRecyclerViewAdapter(new CarUiListItemAdapter(items));
 
-        assertThat(getViewHolderAtPosition(0).getTitle().getVisibility()).isEqualTo(View.VISIBLE);
-        assertThat(getViewHolderAtPosition(0).getBody().getVisibility()).isEqualTo(View.VISIBLE);
-        assertThat(getViewHolderAtPosition(0).getIconContainer().getVisibility()).isNotEqualTo(
+        assertThat(getListItemViewHolderAtPosition(0).getTitle().getVisibility()).isEqualTo(
                 View.VISIBLE);
-        assertThat(getViewHolderAtPosition(0).getActionContainer().getVisibility()).isEqualTo(
+        assertThat(getListItemViewHolderAtPosition(0).getBody().getVisibility()).isEqualTo(
                 View.VISIBLE);
-        assertThat(getViewHolderAtPosition(0).getSwitch().getVisibility()).isEqualTo(View.VISIBLE);
-        assertThat(getViewHolderAtPosition(0).getSwitch().isChecked()).isEqualTo(false);
-        assertThat(getViewHolderAtPosition(0).getCheckBox().getVisibility()).isNotEqualTo(
+        assertThat(getListItemViewHolderAtPosition(
+                0).getIconContainer().getVisibility()).isNotEqualTo(
+                View.VISIBLE);
+        assertThat(getListItemViewHolderAtPosition(
+                0).getActionContainer().getVisibility()).isEqualTo(
+                View.VISIBLE);
+        assertThat(getListItemViewHolderAtPosition(0).getSwitch().getVisibility()).isEqualTo(
+                View.VISIBLE);
+        assertThat(getListItemViewHolderAtPosition(0).getSwitch().isChecked()).isEqualTo(false);
+        assertThat(getListItemViewHolderAtPosition(0).getCheckBox().getVisibility()).isNotEqualTo(
                 View.VISIBLE);
     }
 
@@ -188,16 +214,16 @@
     public void testCheckedState_switch() {
         List<CarUiListItem> items = new ArrayList<>();
 
-        CarUiListItem item = new CarUiListItem();
+        CarUiContentListItem item = new CarUiContentListItem();
         item.setTitle("Test title");
-        item.setChecked(true);
         item.setOnCheckedChangedListener(mOnCheckedChangedListener);
-        item.setAction(CarUiListItem.Action.SWITCH);
+        item.setAction(CarUiContentListItem.Action.SWITCH);
+        item.setChecked(true);
         items.add(item);
 
         updateRecyclerViewAdapter(new CarUiListItemAdapter(items));
 
-        Switch switchWidget = getViewHolderAtPosition(0).getSwitch();
+        Switch switchWidget = getListItemViewHolderAtPosition(0).getSwitch();
 
         assertThat(switchWidget.isChecked()).isEqualTo(true);
         switchWidget.performClick();
@@ -209,19 +235,56 @@
     public void testCheckedState_checkbox() {
         List<CarUiListItem> items = new ArrayList<>();
 
-        CarUiListItem item = new CarUiListItem();
+        CarUiContentListItem item = new CarUiContentListItem();
         item.setTitle("Test title");
-        item.setAction(CarUiListItem.Action.CHECK_BOX);
+        item.setAction(CarUiContentListItem.Action.CHECK_BOX);
         item.setOnCheckedChangedListener(mOnCheckedChangedListener);
         items.add(item);
 
         updateRecyclerViewAdapter(new CarUiListItemAdapter(items));
 
-        CheckBox checkBox = getViewHolderAtPosition(0).getCheckBox();
+        CheckBox checkBox = getListItemViewHolderAtPosition(0).getCheckBox();
 
         assertThat(checkBox.isChecked()).isEqualTo(false);
         checkBox.performClick();
         assertThat(checkBox.isChecked()).isEqualTo(true);
         verify(mOnCheckedChangedListener, times(1)).onCheckedChanged(true);
     }
+
+    @Test
+    public void testHeader_onlyTitle() {
+        List<CarUiListItem> items = new ArrayList<>();
+
+        CharSequence title = "Test header";
+        CarUiHeaderListItem header = new CarUiHeaderListItem(title);
+        items.add(header);
+
+        updateRecyclerViewAdapter(new CarUiListItemAdapter(items));
+
+        CarUiListItemAdapter.HeaderViewHolder viewHolder = getHeaderViewHolderAtPosition(0);
+
+        assertThat(viewHolder.getTitle().getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(viewHolder.getTitle().getText()).isEqualTo(title);
+        assertThat(viewHolder.getBody().getVisibility()).isNotEqualTo(View.VISIBLE);
+    }
+
+    @Test
+    public void testHeader_titleAndBody() {
+        List<CarUiListItem> items = new ArrayList<>();
+
+        CharSequence title = "Test header";
+        CharSequence body = "With body text";
+
+        CarUiHeaderListItem header = new CarUiHeaderListItem(title, body);
+        items.add(header);
+
+        updateRecyclerViewAdapter(new CarUiListItemAdapter(items));
+
+        CarUiListItemAdapter.HeaderViewHolder viewHolder = getHeaderViewHolderAtPosition(0);
+
+        assertThat(viewHolder.getTitle().getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(viewHolder.getTitle().getText()).isEqualTo(title);
+        assertThat(viewHolder.getBody().getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(viewHolder.getBody().getText()).isEqualTo(body);
+    }
 }