Merge "Prioritize exact match type converters" into oc-mr1-jetpack-dev
diff --git a/car/res/layout/car_list_dialog.xml b/car/res/layout/car_list_dialog.xml
new file mode 100644
index 0000000..cf36052
--- /dev/null
+++ b/car/res/layout/car_list_dialog.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 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.
+-->
+
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/container"
+    android:background="@android:color/transparent"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent" >
+
+    <!-- Note: the width is 0dp because ColumnCardView will automatically set a width based
+         on the number of columns it should take up. See ColumnCardView for more details. -->
+    <androidx.car.widget.ColumnCardView
+        android:layout_gravity="center"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/car_padding_4"
+        android:layout_marginBottom="@dimen/car_padding_4"
+        android:elevation="@dimen/car_dialog_elevation"
+        app:cardBackgroundColor="@color/car_card"
+        app:cardCornerRadius="@dimen/car_radius_3">
+
+        <!-- Hide the scrollbar for this PagedListView because it will be implemented by
+             @id/scrollbar. -->
+        <androidx.car.widget.PagedListView
+            android:id="@+id/list"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            app:gutter="none"
+            app:showPagedListViewDivider="true"
+            app:scrollBarEnabled="false" />
+    </androidx.car.widget.ColumnCardView>
+
+    <!-- Putting this as the last child for highest z-index. It is also clickable to reduce
+         the chance of clicks on the buttons accidentally dismissing the dialog. -->
+    <androidx.car.widget.PagedScrollBarView
+        android:id="@+id/scrollbar"
+        android:layout_width="@dimen/car_margin"
+        android:layout_height="match_parent"
+        android:layout_marginTop="@dimen/car_padding_4"
+        android:layout_marginBottom="@dimen/car_padding_4"
+        android:layout_gravity="start|top"
+        android:clickable="true"
+        android:visibility="invisible" />
+</FrameLayout>
diff --git a/car/src/main/java/androidx/car/app/CarListDialog.java b/car/src/main/java/androidx/car/app/CarListDialog.java
new file mode 100644
index 0000000..4a7becf
--- /dev/null
+++ b/car/src/main/java/androidx/car/app/CarListDialog.java
@@ -0,0 +1,390 @@
+/*
+ * Copyright (C) 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.app;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.graphics.Color;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.Window;
+import android.view.WindowManager;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import androidx.car.R;
+import androidx.car.widget.DayNightStyle;
+import androidx.car.widget.ListItem;
+import androidx.car.widget.ListItemAdapter;
+import androidx.car.widget.ListItemProvider;
+import androidx.car.widget.PagedListView;
+import androidx.car.widget.PagedScrollBarView;
+import androidx.car.widget.TextListItem;
+
+/**
+ * A subclass of {@link Dialog} that is tailored for the car environment. This dialog can display a
+ * fixed list of items. There is no affordance for setting titles or any other text.
+ *
+ * <p>Its functionality is similar to if a list has been set on
+ * {@link android.support.v7.app.AlertDialog}, but is styled so that it is more appropriate for
+ * displaying in vehicles.
+ *
+ * <p>Note that this dialog cannot be created with an empty list.
+ */
+public class CarListDialog extends Dialog {
+    private static final String TAG = "CarListDialog";
+
+    private ListItemAdapter mAdapter;
+    private PagedListView mList;
+    private PagedScrollBarView mScrollBarView;
+    private final DialogInterface.OnClickListener mOnClickListener;
+
+    /** Flag for if a touch on the scrim of the dialog will dismiss it. */
+    private boolean mDismissOnTouchOutside;
+
+    private final ViewTreeObserver.OnGlobalLayoutListener mLayoutListener =
+            new ViewTreeObserver.OnGlobalLayoutListener() {
+                @Override
+                public void onGlobalLayout() {
+                    updateScrollbar();
+                    // Remove this listener because the listener for the scroll state will be
+                    // enough to keep the scrollbar in sync.
+                    mList.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+                }
+            };
+
+    private CarListDialog(Context context, String[] items, OnClickListener listener) {
+        super(context);
+        mOnClickListener = listener;
+        initializeAdapter(items);
+    }
+
+    @Override
+    public void setTitle(CharSequence title) {
+        // Ideally this method should not exist; the list dialog does not support a title.
+        // Unfortunately, this method is defined with the Dialog itself and is public. So, throw
+        // an error if this method is ever called.
+        throw new UnsupportedOperationException("Title is not supported in the CarListDialog");
+    }
+
+    /**
+     * @see super#setCanceledOnTouchOutside(boolean)
+     */
+    @Override
+    public void setCanceledOnTouchOutside(boolean cancel) {
+        super.setCanceledOnTouchOutside(cancel);
+        // Need to override this method to save the value of cancel.
+        mDismissOnTouchOutside = cancel;
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        Window window = getWindow();
+        window.setContentView(R.layout.car_list_dialog);
+
+        // By default, the decor background is white. Set this to be transparent so that
+        // the dialog can have rounded corners and will show the background.
+        window.getDecorView().setBackgroundColor(Color.TRANSPARENT);
+
+        // Ensure that the dialog takes up the entire window. This is needed because the scrollbar
+        // needs to be drawn off the dialog.
+        WindowManager.LayoutParams layoutParams = window.getAttributes();
+        layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT;
+        layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT;
+        window.setAttributes(layoutParams);
+
+        // The container for this dialog takes up the entire screen. As a result, need to manually
+        // listen for clicks and dismiss the dialog when necessary.
+        window.findViewById(R.id.container).setOnClickListener(v -> handleTouchOutside());
+
+        initializeList();
+        initializeScrollbar();
+    }
+
+    @Override
+    protected void onStop() {
+        // Cleanup to ensure that no stray view observers are still attached.
+        if (mList != null) {
+            mList.getViewTreeObserver().removeOnGlobalLayoutListener(mLayoutListener);
+        }
+
+        super.onStop();
+    }
+
+    private void initializeList() {
+        mList = getWindow().findViewById(R.id.list);
+        mList.setAdapter(mAdapter);
+
+        // Ensure that when the list is scrolled, the scrollbar updates to reflect the new position.
+        mList.getRecyclerView().addOnScrollListener(new RecyclerView.OnScrollListener() {
+            @Override
+            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+                super.onScrolled(recyclerView, dx, dy);
+                updateScrollbar();
+            }
+        });
+
+        // Update if the scrollbar should be visible after the PagedListView has finished
+        // laying itself out. This is needed because the only way to the state of scrollbar is to
+        // see the items after they have been laid out.
+        mList.getViewTreeObserver().addOnGlobalLayoutListener(mLayoutListener);
+    }
+
+    /**
+     * Initializes the scrollbar that appears off the dialog. This scrollbar is not the one that
+     * usually appears with the PagedListView, but mimics it in functionality.
+     */
+    private void initializeScrollbar() {
+        mScrollBarView = getWindow().findViewById(R.id.scrollbar);
+        mScrollBarView.setDayNightStyle(DayNightStyle.FORCE_NIGHT);
+
+        mScrollBarView.setPaginationListener(direction -> {
+            switch (direction) {
+                case PagedScrollBarView.PaginationListener.PAGE_UP:
+                    mList.pageUp();
+                    break;
+                case PagedScrollBarView.PaginationListener.PAGE_DOWN:
+                    mList.pageDown();
+                    break;
+                default:
+                    Log.e(TAG, "Unknown pagination direction (" + direction + ")");
+            }
+        });
+    }
+
+    /**
+     * Handles if a touch has been detected outside of the dialog. If
+     * {@link #mDismissOnTouchOutside} has been set, then the dialog will be dismissed.
+     */
+    private void handleTouchOutside() {
+        if (mDismissOnTouchOutside) {
+            dismiss();
+        }
+    }
+
+    /**
+     * 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) {
+        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);
+        }
+
+        mAdapter = new ListItemAdapter(context, new ListItemProvider.ListProvider(listItems));
+    }
+
+    /**
+     * Check if a click listener has been set on this dialog and notify that a click has happened
+     * at the given item position, then dismisses this dialog. If no listener has been set, the
+     * dialog just dismisses.
+     */
+    private void onItemClick(int position) {
+        if (mOnClickListener != null) {
+            mOnClickListener.onClick(this /* dialog */, position);
+        }
+        dismiss();
+    }
+
+    /**
+     * Determines if scrollbar should be visible or not and shows/hides it accordingly.
+     *
+     * <p>If this is being called as a result of adapter changes, it should be called after the new
+     * layout has been calculated because the method of determining scrollbar visibility uses the
+     * current layout.
+     *
+     * <p>If this is called after an adapter change but before the new layout, the visibility
+     * determination may not be correct.
+     */
+    private void updateScrollbar() {
+        RecyclerView recyclerView = mList.getRecyclerView();
+        RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
+
+        boolean isAtStart = mList.isAtStart();
+        boolean isAtEnd = mList.isAtEnd();
+
+        if ((isAtStart && isAtEnd)) {
+            mScrollBarView.setVisibility(View.INVISIBLE);
+            return;
+        }
+
+        mScrollBarView.setVisibility(View.VISIBLE);
+        mScrollBarView.setUpEnabled(!isAtStart);
+        mScrollBarView.setDownEnabled(!isAtEnd);
+
+        // Assume the list scrolls vertically because we control the list and know the
+        // LayoutManager cannot change.
+        mScrollBarView.setParameters(
+                recyclerView.computeVerticalScrollRange(),
+                recyclerView.computeVerticalScrollOffset(),
+                recyclerView.computeVerticalScrollExtent(),
+                false /* animate */);
+
+        getWindow().getDecorView().invalidate();
+    }
+
+    /**
+     * Builder class that can be used to create a {@link CarListDialog} by configuring the
+     * options for the list and behavior of the dialog.
+     */
+    public static class Builder {
+        private final Context mContext;
+        private String[] mItems;
+        private DialogInterface.OnClickListener mOnClickListener;
+
+        private boolean mCancelable = true;
+        private OnCancelListener mOnCancelListener;
+        private OnDismissListener mOnDismissListener;
+
+        /**
+         * Creates a new instance of the {@code Builder}.
+         *
+         * @param context The {@code Context} that the dialog is to be created in.
+         */
+        public Builder(Context context) {
+            mContext = context;
+        }
+
+        /**
+         * Sets the items that should appear in the list. The dialog will automatically dismiss
+         * itself when an item in the list is clicked on.
+         *
+         * <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 items cannot be {@code null} or empty. Passing an empty list
+         * to this method will throw can exception.
+         *
+         * @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.
+         */
+        public Builder setItems(@NonNull String[] items,
+                @Nullable OnClickListener onClickListener) {
+            if (items == null || items.length == 0) {
+                throw new IllegalArgumentException("Provided list of items cannot be empty.");
+            }
+
+            mItems = items;
+            mOnClickListener = onClickListener;
+            return this;
+        }
+
+        /**
+         * Sets whether the dialog is cancelable or not. Default is {@code true}.
+         *
+         * @return This {@code Builder} object to allow for chaining of calls.
+         */
+        public Builder setCancelable(boolean cancelable) {
+            mCancelable = cancelable;
+            return this;
+        }
+
+        /**
+         * Sets the callback that will be called if the dialog is canceled.
+         *
+         * <p>Even in a cancelable dialog, the dialog may be dismissed for reasons other than
+         * being canceled or one of the supplied choices being selected.
+         * If you are interested in listening for all cases where the dialog is dismissed
+         * and not just when it is canceled, see {@link #setOnDismissListener(OnDismissListener)}.
+         *
+         * @param onCancelListener The listener to be invoked when this dialog is canceled.
+         * @return This {@code Builder} object to allow for chaining of calls.
+         *
+         * @see #setCancelable(boolean)
+         * @see #setOnDismissListener(OnDismissListener)
+         */
+        public Builder setOnCancelListener(OnCancelListener onCancelListener) {
+            mOnCancelListener = onCancelListener;
+            return this;
+        }
+
+        /**
+         * Sets the callback that will be called when the dialog is dismissed for any reason.
+         *
+         * @return This {@code Builder} object to allow for chaining of calls.
+         */
+        public Builder setOnDismissListener(OnDismissListener onDismissListener) {
+            mOnDismissListener = onDismissListener;
+            return this;
+        }
+
+        /**
+         * Creates an {@link CarListDialog} with the arguments supplied to this {@code Builder}.
+         *
+         * <p>If {@link #setItems(String[],DialogInterface.OnClickListener)} is never called, then
+         * calling this method will throw an exception.
+         *
+         * <p>Calling this method does not display the dialog. If no additional processing is
+         * needed, {@link #show()} may be called instead to both create and display the dialog.
+         */
+        public CarListDialog create() {
+            if (mItems == null || mItems.length == 0) {
+                throw new IllegalStateException(
+                        "CarListDialog must be created with a non-empty list.");
+            }
+
+            CarListDialog dialog = new CarListDialog(mContext, mItems, mOnClickListener);
+
+            dialog.setCancelable(mCancelable);
+            dialog.setCanceledOnTouchOutside(mCancelable);
+            dialog.setOnCancelListener(mOnCancelListener);
+            dialog.setOnDismissListener(mOnDismissListener);
+
+            return dialog;
+        }
+
+        /**
+         * Creates an {@link CarAlertDialog} with the arguments supplied to this {@code Builder}
+         * and immediately displays the dialog.
+         *
+         * <p>Calling this method is functionally identical to:
+         * <pre>
+         *     CarAlertDialog dialog = new CarAlertDialog.Builder().create();
+         *     dialog.show();
+         * </pre>
+         */
+        public CarListDialog show() {
+            CarListDialog dialog = create();
+            dialog.show();
+            return dialog;
+        }
+    }
+}
diff --git a/car/src/main/java/androidx/car/widget/PagedListView.java b/car/src/main/java/androidx/car/widget/PagedListView.java
index 1f0f9df..4347bab 100644
--- a/car/src/main/java/androidx/car/widget/PagedListView.java
+++ b/car/src/main/java/androidx/car/widget/PagedListView.java
@@ -16,6 +16,8 @@
 
 package androidx.car.widget;
 
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
 import android.content.Context;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
@@ -31,6 +33,7 @@
 import android.support.annotation.IntDef;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
 import android.support.annotation.UiThread;
 import android.support.annotation.VisibleForTesting;
 import android.support.v7.widget.LinearLayoutManager;
@@ -718,13 +721,21 @@
         return position / mRowsPerPage;
     }
 
-    /** Scrolls the contents of the RecyclerView up a page. */
-    private void pageUp() {
+    /**
+     * Scrolls the contents of the RecyclerView up a page.
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public void pageUp() {
         mRecyclerView.fling(0, FLING_UP_DISTANCE);
     }
 
-    /** Scrolls the contents of the RecyclerView down a page. */
-    private void pageDown() {
+    /**
+     * Scrolls the contents of the RecyclerView down a page.
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public void pageDown() {
         mRecyclerView.fling(0, FLING_DOWN_DISTANCE);
     }