Add support for sections in CarListDialog.

A section has a title and list of items associated with it. To pass this
information to the CarListDialog, abstract it with a struct class.

Fixes: 77823219
Test: Test on sample application with sections.
Test: ./gradlew car:connectedCheck
Change-Id: I1f9dcf622d52d4fdd23987bbeecbc9be21139db0
diff --git a/car/api/current.txt b/car/api/current.txt
index c02a34d..0425614 100644
--- a/car/api/current.txt
+++ b/car/api/current.txt
@@ -28,12 +28,20 @@
     method public androidx.car.app.CarListDialog.Builder setCancelable(boolean);
     method public androidx.car.app.CarListDialog.Builder setInitialPosition(int);
     method public androidx.car.app.CarListDialog.Builder setItems(java.lang.String[], android.content.DialogInterface.OnClickListener);
+    method public androidx.car.app.CarListDialog.Builder setItems(androidx.car.app.CarListDialog.DialogSubSection[], android.content.DialogInterface.OnClickListener);
     method public androidx.car.app.CarListDialog.Builder setOnCancelListener(android.content.DialogInterface.OnCancelListener);
     method public androidx.car.app.CarListDialog.Builder setOnDismissListener(android.content.DialogInterface.OnDismissListener);
     method public androidx.car.app.CarListDialog.Builder setTitle(int);
     method public androidx.car.app.CarListDialog.Builder setTitle(java.lang.CharSequence);
   }
 
+  public static class CarListDialog.DialogSubSection {
+    ctor public CarListDialog.DialogSubSection(java.lang.String, java.lang.String[]);
+    method public int getItemCount();
+    method public java.lang.String[] getItems();
+    method public java.lang.String getTitle();
+  }
+
 }
 
 package androidx.car.drawer {
diff --git a/car/src/main/java/androidx/car/app/CarListDialog.java b/car/src/main/java/androidx/car/app/CarListDialog.java
index 6722ff0..6767890 100644
--- a/car/src/main/java/androidx/car/app/CarListDialog.java
+++ b/car/src/main/java/androidx/car/app/CarListDialog.java
@@ -41,6 +41,7 @@
 import androidx.car.widget.ListItemProvider;
 import androidx.car.widget.PagedListView;
 import androidx.car.widget.PagedScrollBarView;
+import androidx.car.widget.SubheaderListItem;
 import androidx.car.widget.TextListItem;
 import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
@@ -97,7 +98,12 @@
         mTitle = builder.mTitle;
         mTitleElevation =
                 context.getResources().getDimension(R.dimen.car_list_dialog_title_elevation);
-        initializeAdapter(builder.mItems);
+
+        if (builder.mSections != null) {
+            initializeWithSections(builder.mSections);
+        } else {
+            initializeWithItems(builder.mItems);
+        }
     }
 
     @Override
@@ -210,6 +216,7 @@
         mList = getWindow().findViewById(R.id.list);
         mList.setMaxPages(PagedListView.UNLIMITED_PAGES);
         mList.setAdapter(mAdapter);
+        mList.setDividerVisibilityManager(mAdapter);
 
         // The list will start at the 0 position, so no need to scroll.
         if (mInitialPosition != 0) {
@@ -274,22 +281,57 @@
      * Initializes {@link #mAdapter} to display the items in the given array. It utilizes the
      * {@link TextListItem} but only populates the title field with the the values in the array.
      */
-    private void initializeAdapter(String[] items) {
+    private void initializeWithItems(String[] items) {
         Context context = getContext();
         List<ListItem> listItems = new ArrayList<>();
 
         for (int i = 0; i < items.length; i++) {
-            TextListItem item = new TextListItem(getContext());
-            item.setTitle(items[i]);
-
-            // Save the position to pass to onItemClick().
-            final int position = i;
-            item.setOnClickListener(v -> onItemClick(position));
-
-            listItems.add(item);
+            listItems.add(createItem(/* text= */ items[i], /* position= */ i));
         }
 
         mAdapter = new ListItemAdapter(context, new ListItemProvider.ListProvider(listItems));
+
+    }
+
+    /**
+     * Initializes the {@link #mAdapter} to display the sections in the given array. It utilizes
+     * the {@link SubheaderListItem} to display the section title and {@link TextListItem} to
+     * display the individual items of a section.
+     */
+    private void initializeWithSections(DialogSubSection[] sections) {
+        Context context = getContext();
+        List<ListItem> listItems = new ArrayList<>();
+
+        for (DialogSubSection section : sections) {
+            SubheaderListItem header = new SubheaderListItem(getContext(), section.getTitle());
+            header.setHideDivider(true);
+
+            listItems.add(header);
+
+            String[] items = section.getItems();
+            // Now initialize all the items associated with this subsection.
+            for (int i = 0, length = items.length; i < length; i++) {
+                listItems.add(createItem(/* text= */ items[i], /* position= */ i));
+            }
+        }
+
+        mAdapter = new ListItemAdapter(context, new ListItemProvider.ListProvider(listItems));
+    }
+
+    /**
+     * Creates the {@link TextListItem} that represents an item in the {@code CarListDialog}.
+     *
+     * @param text The text to display as the title in {@code TextListItem}.
+     * @param position The position of the item in the list.
+     */
+    private TextListItem createItem(String text, int position) {
+        TextListItem item = new TextListItem(getContext());
+        item.setTitle(text);
+
+        // Save the position to pass to onItemClick().
+        item.setOnClickListener(v -> onItemClick(position));
+
+        return item;
     }
 
     /**
@@ -299,7 +341,7 @@
      */
     private void onItemClick(int position) {
         if (mOnClickListener != null) {
-            mOnClickListener.onClick(this /* dialog */, position);
+            mOnClickListener.onClick(/* dialog= */ this, position);
         }
         dismiss();
     }
@@ -316,7 +358,6 @@
      */
     private void updateScrollbar() {
         RecyclerView recyclerView = mList.getRecyclerView();
-        RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
 
         boolean isAtStart = mList.isAtStart();
         boolean isAtEnd = mList.isAtEnd();
@@ -336,7 +377,7 @@
                 recyclerView.computeVerticalScrollRange(),
                 recyclerView.computeVerticalScrollOffset(),
                 recyclerView.computeVerticalScrollExtent(),
-                false /* animate */);
+                /* animate= */ false);
 
         getWindow().getDecorView().invalidate();
     }
@@ -352,6 +393,60 @@
     }
 
     /**
+     * A struct that holds data for a section. A section is a combination of the section title and
+     * the list of items associated with that section.
+     */
+    public static class DialogSubSection {
+        private final String mTitle;
+        private final String[] mItems;
+
+        /**
+         * Creates a subsection.
+         *
+         * @param title The title of the section. Must be non-empty.
+         * @param items A list of items associated with this section. This list cannot be
+         *              {@code null} or empty.
+         */
+        public DialogSubSection(@NonNull String title, @NonNull String[] items) {
+            if (TextUtils.isEmpty(title)) {
+                throw new IllegalArgumentException("Title cannot be empty.");
+            }
+
+            if (items == null || items.length == 0) {
+                throw new IllegalArgumentException("Items cannot be empty.");
+            }
+
+            mTitle = title;
+            mItems = items;
+        }
+
+        /** Returns the section title. */
+        @NonNull
+        public String getTitle() {
+            return mTitle;
+        }
+
+        /** Returns the section items. */
+        @NonNull
+        public String[] getItems() {
+            return mItems;
+        }
+
+        /**
+         * Returns the total number of items related to this section. The length of the section is
+         * defined as the number of items plus an entry for the title of the section.
+         *
+         * <p>This value will always be greater than 0 due to the fact that the title must always
+         * be specified and the number of items passed to this {@code DialogSubSection} should
+         * always be greater than 0.
+         */
+        public int getItemCount() {
+            // Adding 1 to the length for account for the title.
+            return mItems.length + 1;
+        }
+    }
+
+    /**
      * Builder class that can be used to create a {@link CarListDialog} by configuring the
      * options for the list and behavior of the dialog.
      */
@@ -361,6 +456,7 @@
         private CharSequence mTitle;
         private int mInitialPosition;
         private String[] mItems;
+        private DialogSubSection[] mSections;
         private DialogInterface.OnClickListener mOnClickListener;
 
         private boolean mCancelable = true;
@@ -382,6 +478,7 @@
          * @param titleId The resource id of the string to be used as the title.
          * @return This {@code Builder} object to allow for chaining of calls.
          */
+        @NonNull
         public Builder setTitle(@StringRes int titleId) {
             mTitle = mContext.getString(titleId);
             return this;
@@ -393,6 +490,7 @@
          * @param title The string to be used as the title.
          * @return This {@code Builder} object to allow for chaining of calls.
          */
+        @NonNull
         public Builder setTitle(CharSequence title) {
             mTitle = title;
             return this;
@@ -411,10 +509,15 @@
          * <p>The provided list of items cannot be {@code null} or empty. Passing an empty list
          * to this method will throw can exception.
          *
+         * <p>If both this method and {@link #setItems(DialogSubSection[], OnClickListener)} are
+         * called, then the sections will take precedent, and the items set via this method will
+         * be ignored.
+         *
          * @param items The items that will appear in the list.
          * @param onClickListener The listener that will be notified of a click.
          * @return This {@code Builder} object to allow for chaining of calls.
          */
+        @NonNull
         public Builder setItems(@NonNull String[] items,
                 @Nullable OnClickListener onClickListener) {
             if (items == null || items.length == 0) {
@@ -427,15 +530,56 @@
         }
 
         /**
+         * Sets the items that should appear in the list, divided into sections. Each section has a
+         * title and a list of items associated with it. The dialog will automatically dismiss
+         * itself when an item in the list is clicked on; the title of the section is not
+         * clickable.
+         *
+         * <p>If a {@link DialogInterface.OnClickListener} is given, then it will be notified
+         * of the click. The dialog will still be dismissed afterwards. The {@code which}
+         * parameter of the {@link DialogInterface.OnClickListener#onClick(DialogInterface, int)}
+         * method will be the position of the item. This position maps to the index of the item in
+         * the given list.
+         *
+         * <p>The provided list of sections cannot be {@code null} or empty. The list of items
+         * within a section also cannot be empty. Passing an empty list to this method will
+         * throw can exception.
+         *
+         * <p>If both this method and {@link #setItems(String[], OnClickListener)} are called, then
+         * the sections will take precedent, and the items set via the other method will be
+         * ignored.
+         *
+         * @param sections The sections that will appear in the list.
+         * @param onClickListener The listener that will be notified of a click.
+         * @return This {@code Builder} object to allow for chaining of calls.
+         */
+        @NonNull
+        public Builder setItems(@NonNull DialogSubSection[] sections,
+                @Nullable OnClickListener onClickListener) {
+            if (sections == null || sections.length == 0) {
+                throw new IllegalArgumentException("Provided list of sections cannot be empty.");
+            }
+
+            mSections = sections;
+            mOnClickListener = onClickListener;
+            return this;
+        }
+
+        /**
          * Sets the initial position in the list that the {@code CarListDialog} will start at. When
          * the dialog is created, the list will animate to the given position.
          *
          * <p>The position uses zero-based indexing. So, to scroll to the fifth item in the list,
          * a value of four should be passed.
          *
+         * <p>If the items in this dialog was set by
+         * {@link #setItems(DialogSubSection[], OnClickListener)}, then note that the title of the
+         * section counts as an item in the list.
+         *
          * @param initialPosition The initial position in the list to display.
          * @return This {@code Builder} object to allow for chaining of calls.
          */
+        @NonNull
         public Builder setInitialPosition(int initialPosition) {
             if (initialPosition < 0) {
                 throw new IllegalArgumentException("Initial position cannot be negative.");
@@ -449,6 +593,7 @@
          *
          * @return This {@code Builder} object to allow for chaining of calls.
          */
+        @NonNull
         public Builder setCancelable(boolean cancelable) {
             mCancelable = cancelable;
             return this;
@@ -468,6 +613,7 @@
          * @see #setCancelable(boolean)
          * @see #setOnDismissListener(OnDismissListener)
          */
+        @NonNull
         public Builder setOnCancelListener(OnCancelListener onCancelListener) {
             mOnCancelListener = onCancelListener;
             return this;
@@ -478,6 +624,7 @@
          *
          * @return This {@code Builder} object to allow for chaining of calls.
          */
+        @NonNull
         public Builder setOnDismissListener(OnDismissListener onDismissListener) {
             mOnDismissListener = onDismissListener;
             return this;
@@ -493,12 +640,28 @@
          * {@link androidx.fragment.app.DialogFragment} to show the dialog.
          */
         public CarListDialog create() {
-            if (mItems == null || mItems.length == 0) {
+            // Check that the dialog was created with a list of either sections or items.
+            if ((mSections == null || mSections.length == 0)
+                    && (mItems == null || mItems.length == 0)) {
                 throw new IllegalStateException(
-                        "CarListDialog must be created with a non-empty list.");
+                        "CarListDialog cannot be created with a non-empty list.");
             }
 
-            if (mInitialPosition >= mItems.length) {
+            int numOfItems = 0;
+
+            // Subsections take precedent over items as both cannot be set at the same time.
+            if (mSections != null) {
+                mItems = null;
+
+                // Calculate the total number of items by adding up all the sections.
+                for (DialogSubSection section : mSections) {
+                    numOfItems += section.getItemCount();
+                }
+            } else {
+                numOfItems = mItems.length;
+            }
+
+            if (mInitialPosition >= numOfItems) {
                 throw new IllegalStateException("Initial position is greater than the number of "
                         + "items in the list.");
             }
diff --git a/samples/SupportCarDemos/src/main/java/com/example/androidx/car/CarListDialogDemo.java b/samples/SupportCarDemos/src/main/java/com/example/androidx/car/CarListDialogDemo.java
index 71aa104..20ebd8c 100644
--- a/samples/SupportCarDemos/src/main/java/com/example/androidx/car/CarListDialogDemo.java
+++ b/samples/SupportCarDemos/src/main/java/com/example/androidx/car/CarListDialogDemo.java
@@ -34,6 +34,7 @@
 public class CarListDialogDemo extends FragmentActivity {
     private static final String DIALOG_TAG = "list_dialog_tag";
 
+    private static final int DEFAULT_NUM_OF_SECTIONS = 0;
     private static final int DEFAULT_NUM_OF_ITEMS = 4;
     private static final int DEFAULT_INITIAL_POSITION = 0;
 
@@ -42,10 +43,16 @@
         super.onCreate(savedInstanceState);
         setContentView(R.layout.list_dialog_activity);
 
+        EditText numOfSectionsEdit = findViewById(R.id.num_of_sections_edit);
         EditText numOfItemsEdit = findViewById(R.id.num_of_items_edit);
         EditText initialPositionEdit = findViewById(R.id.initial_position_edit);
 
         findViewById(R.id.create_dialog).setOnClickListener(v -> {
+            CharSequence numOfSectionsText = numOfSectionsEdit.getText();
+            int numOfSections = TextUtils.isEmpty(numOfSectionsText)
+                    ? DEFAULT_NUM_OF_SECTIONS
+                    : Integer.parseInt(numOfSectionsText.toString());
+
             CharSequence numOfItemsText = numOfItemsEdit.getText();
             int numOfItems = TextUtils.isEmpty(numOfItemsText)
                     ? DEFAULT_NUM_OF_ITEMS
@@ -58,6 +65,7 @@
 
             ListDialogFragment alertDialog = ListDialogFragment.newInstance(
                     ((CheckBox) findViewById(R.id.has_title)).isChecked(),
+                    numOfSections,
                     numOfItems,
                     initialPosition);
 
@@ -68,13 +76,15 @@
     /** A {@link DialogFragment} that will inflate a {@link CarListDialog}. */
     public static class ListDialogFragment extends DialogFragment {
         private static final String HAS_TITLE_KEY = "has_title_key";
+        private static final String NUM_OF_SECTIONS_KEY = "num_of_sections_key";
         private static final String NUM_OF_ITEMS_KEY = "num_of_items_key";
         private static final String INITIAL_POSITION_KEY = "initial_position_key";
 
-        static ListDialogFragment newInstance(boolean hasTitle,
+        static ListDialogFragment newInstance(boolean hasTitle, int numOfSections,
                 int numOfItems, int initialPosition) {
             Bundle args = new Bundle();
             args.putBoolean(HAS_TITLE_KEY, hasTitle);
+            args.putInt(NUM_OF_SECTIONS_KEY, numOfSections);
             args.putInt(NUM_OF_ITEMS_KEY, numOfItems);
             args.putInt(INITIAL_POSITION_KEY, initialPosition);
 
@@ -87,9 +97,17 @@
         @Override
         public Dialog onCreateDialog(Bundle savedInstanceState) {
             CarListDialog.Builder builder = new CarListDialog.Builder(getContext())
-                    .setItems(getItems(), /* onClickListener= */ null)
                     .setInitialPosition(getArguments().getInt(INITIAL_POSITION_KEY));
 
+            int numOfSections = getArguments().getInt(NUM_OF_SECTIONS_KEY);
+            int numOfItems = getArguments().getInt(NUM_OF_ITEMS_KEY);
+
+            if (numOfSections != 0) {
+                builder.setItems(createSections(numOfSections, numOfItems), null);
+            } else {
+                builder.setItems(createItems(numOfItems), null);
+            }
+
             if (getArguments().getBoolean(HAS_TITLE_KEY)) {
                 builder.setTitle(getContext().getString(R.string.list_dialog_title));
             }
@@ -97,9 +115,20 @@
             return builder.create();
         }
 
-        private String[] getItems() {
-            int numOfItems = getArguments().getInt(NUM_OF_ITEMS_KEY);
+        private CarListDialog.DialogSubSection[] createSections(int numOfSections, int numOfItems) {
+            CarListDialog.DialogSubSection[] items =
+                    new CarListDialog.DialogSubSection[numOfSections];
 
+            for (int i = 0; i < numOfSections; i++) {
+                items[i] = new CarListDialog.DialogSubSection(
+                        /* title= */ "Section " + (i + 1),
+                        createItems(numOfItems));
+            }
+
+            return items;
+        }
+
+        private String[] createItems(int numOfItems) {
             String[] items = new String[numOfItems];
             for (int i = 0; i < numOfItems; i++) {
                 items[i] = "Item " + (i + 1);
diff --git a/samples/SupportCarDemos/src/main/res/layout/list_dialog_activity.xml b/samples/SupportCarDemos/src/main/res/layout/list_dialog_activity.xml
index a1e9bbf..1b67ddc 100644
--- a/samples/SupportCarDemos/src/main/res/layout/list_dialog_activity.xml
+++ b/samples/SupportCarDemos/src/main/res/layout/list_dialog_activity.xml
@@ -35,6 +35,22 @@
         android:text="@string/list_dialog_checkbox_title" />
 
     <com.google.android.material.textfield.TextInputLayout
+        android:id="@+id/num_of_sections"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/car_single_line_list_item_height"
+        android:hint="@string/list_dialog_num_of_sections_hint"
+        app:hintTextAppearance="@style/TextAppearance.Car.Hint">
+
+        <com.google.android.material.textfield.TextInputEditText
+            android:id="@+id/num_of_sections_edit"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:inputType="number"
+            android:maxLines="1"
+            android:textAppearance="@style/TextAppearance.Car.Body2"/>
+    </com.google.android.material.textfield.TextInputLayout>
+
+    <com.google.android.material.textfield.TextInputLayout
         android:id="@+id/num_of_items"
         android:layout_width="match_parent"
         android:layout_height="@dimen/car_single_line_list_item_height"
diff --git a/samples/SupportCarDemos/src/main/res/values/strings.xml b/samples/SupportCarDemos/src/main/res/values/strings.xml
index 2a66ffc..a7d6e8f 100644
--- a/samples/SupportCarDemos/src/main/res/values/strings.xml
+++ b/samples/SupportCarDemos/src/main/res/values/strings.xml
@@ -72,6 +72,7 @@
     <!-- Strings for CarListDialog Demo. -->
     <string name="list_dialog_checkbox_title">Title</string>
     <string name="list_dialog_title">Title</string>
+    <string name="list_dialog_num_of_sections_hint">Number of Sections</string>
     <string name="list_dialog_num_of_items_hint">Number of Items</string>
     <string name="list_dialog_initial_position_hint">Initial Position</string>
     <string name="create_list_dialog_button">Create CarListDialog</string>