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