Merge "Don't display UI when playback state is STATE_NONE" into pi-car-dev
diff --git a/car-apps-common/src/com/android/car/apps/common/imaging/ImageViewBinder.java b/car-apps-common/src/com/android/car/apps/common/imaging/ImageViewBinder.java
index f346de0..9b13438 100644
--- a/car-apps-common/src/com/android/car/apps/common/imaging/ImageViewBinder.java
+++ b/car-apps-common/src/com/android/car/apps/common/imaging/ImageViewBinder.java
@@ -21,7 +21,6 @@
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.util.Size;
-import android.view.View;
 import android.widget.ImageView;
 
 import com.android.car.apps.common.CommonFlags;
@@ -57,7 +56,6 @@
     protected void setDrawable(@Nullable Drawable drawable) {
         if (mImageView != null) {
             mImageView.setImageDrawable(drawable);
-            mImageView.setVisibility((drawable != null) ? View.VISIBLE : View.GONE);
             if (mFlagBitmaps) {
                 CommonFlags flags = CommonFlags.getInstance(mImageView.getContext());
                 if (flags.shouldFlagImproperImageRefs()) {
diff --git a/car-arch-common/src/com/android/car/arch/common/preference/PreferenceLiveData.java b/car-arch-common/src/com/android/car/arch/common/preference/PreferenceLiveData.java
deleted file mode 100644
index c9a9972..0000000
--- a/car-arch-common/src/com/android/car/arch/common/preference/PreferenceLiveData.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.arch.common.preference;
-
-import android.content.SharedPreferences;
-
-import androidx.annotation.NonNull;
-import androidx.lifecycle.LiveData;
-
-import java.util.Objects;
-
-/**
- * A LiveData that populates its value from {@link SharedPreferences}. When active, this LiveData
- * will listen for changes to the supplied key and update its value.
- *
- * @param <T> The type this PreferenceLiveData will emit.
- */
-public abstract class PreferenceLiveData<T> extends LiveData<T> {
-
-    private final SharedPreferences mPreferences;
-    private final String mKey;
-
-    private final SharedPreferences.OnSharedPreferenceChangeListener mListener =
-            (sharedPreferences, key) -> {
-                if (Objects.equals(key, PreferenceLiveData.this.mKey)) {
-                    setValue(fetchValue(sharedPreferences, key));
-                }
-            };
-
-    public PreferenceLiveData(
-            @NonNull SharedPreferences preferences, @NonNull String key) {
-        mPreferences = preferences;
-        mKey = key;
-    }
-
-    /**
-     * Subclasses should extract the required value from {@code preferences} corresponding to the
-     * given {@code key} and return it. Subclasses should not directly call {@link #setValue(T)}
-     * since it will be called when appropriate.
-     */
-    protected abstract T fetchValue(@NonNull SharedPreferences preferences, @NonNull String key);
-
-    @Override
-    protected void onActive() {
-        super.onActive();
-        setValue(fetchValue(mPreferences, mKey));
-        mPreferences.registerOnSharedPreferenceChangeListener(mListener);
-    }
-
-    @Override
-    protected void onInactive() {
-        super.onInactive();
-        mPreferences.unregisterOnSharedPreferenceChangeListener(mListener);
-    }
-}
diff --git a/car-arch-common/src/com/android/car/arch/common/preference/StringPreferenceLiveData.java b/car-arch-common/src/com/android/car/arch/common/preference/StringPreferenceLiveData.java
deleted file mode 100644
index 79f56ac..0000000
--- a/car-arch-common/src/com/android/car/arch/common/preference/StringPreferenceLiveData.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.arch.common.preference;
-
-import android.content.SharedPreferences;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-/**
- * A LiveData whose value is backed by a String preference in a {@link SharedPreferences}
- */
-public class StringPreferenceLiveData extends PreferenceLiveData<String> {
-
-    private final String mDefaultValue;
-
-    /**
-     * Creates a new StringPreferenceLiveData.
-     *
-     * @param preferences  The SharedPreferences to fetch data from.
-     * @param key          The String key to find the data.
-     * @param defaultValue The value to emit when {@code preferences} does not contain data for the
-     *                     given key
-     */
-    public StringPreferenceLiveData(
-            @NonNull SharedPreferences preferences, @NonNull String key,
-            @Nullable String defaultValue) {
-        super(preferences, key);
-        mDefaultValue = defaultValue;
-    }
-
-    @Override
-    protected String fetchValue(@NonNull SharedPreferences preferences, @NonNull String key) {
-        return preferences.getString(key, mDefaultValue);
-    }
-}
diff --git a/car-media-common/res/values/arrays.xml b/car-media-common/res/values/arrays.xml
index 7bd071c..f9c1da2 100644
--- a/car-media-common/res/values/arrays.xml
+++ b/car-media-common/res/values/arrays.xml
@@ -36,4 +36,12 @@
         <item>@color/placeholder_color_6</item>
         <item>@color/placeholder_color_7</item>
     </array>
+
+
+    <!-- These media sources are displayed by the AppSelectionFragment in the order defined here.
+        Other sources follow in alphabetical order. -->
+    <string-array name="preferred_media_sources" translatable="false">
+        <item>com.android.car.radio/.service.RadioAppService</item>
+    </string-array>
+
 </resources>
\ No newline at end of file
diff --git a/car-media-common/src/com/android/car/media/common/MediaConstants.java b/car-media-common/src/com/android/car/media/common/MediaConstants.java
index c7520db..967680c 100644
--- a/car-media-common/src/com/android/car/media/common/MediaConstants.java
+++ b/car-media-common/src/com/android/car/media/common/MediaConstants.java
@@ -120,15 +120,37 @@
     /**
      * These constants are from
      * @see <a href=https://developer.android.com/training/auto/audio/#required-actions></a>
+     *
+     * @deprecated this flag has been replaced by {@link #PLAYBACK_SLOT_RESERVATION_SKIP_TO_NEXT}
      */
+    @Deprecated
     public static final String SLOT_RESERVATION_SKIP_TO_NEXT =
             "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_NEXT";
+    /**
+     * @deprecated this flag has been replaced by {@link #PLAYBACK_SLOT_RESERVATION_SKIP_TO_NEXT}
+     */
+    @Deprecated
     public static final String SLOT_RESERVATION_SKIP_TO_PREV =
             "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_PREVIOUS";
+    /**
+     * @deprecated this flag has been replaced by {@link #PLAYBACK_SLOT_RESERVATION_SKIP_TO_NEXT}
+     */
+    @Deprecated
     public static final String SLOT_RESERVATION_QUEUE =
             "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_QUEUE";
 
     /**
+     * These constants are from
+     * @see <a href=https://developer.android.com/training/auto/audio/#required-actions></a>
+     */
+    public static final String PLAYBACK_SLOT_RESERVATION_SKIP_TO_NEXT =
+            "android.media.playback.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_NEXT";
+    public static final String PLAYBACK_SLOT_RESERVATION_SKIP_TO_PREV =
+            "android.media.playback.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_PREVIOUS";
+    public static final String PLAYBACK_SLOT_RESERVATION_QUEUE =
+            "android.media.playback.ALWAYS_RESERVE_SPACE_FOR.ACTION_QUEUE";
+
+    /**
      * Bundle extra of type 'boolean' indicating that an item should show the 'explicit' symbol.
      */
     public static final String EXTRA_IS_EXPLICIT = "android.media.IS_EXPLICIT";
diff --git a/car-media-common/src/com/android/car/media/common/MediaItemMetadata.java b/car-media-common/src/com/android/car/media/common/MediaItemMetadata.java
index 5ca60d1..00a4416 100644
--- a/car-media-common/src/com/android/car/media/common/MediaItemMetadata.java
+++ b/car-media-common/src/com/android/car/media/common/MediaItemMetadata.java
@@ -236,6 +236,13 @@
     }
 
     /**
+     * @return optional extras that can include extra information about the media item to be played.
+     */
+    public Bundle getExtras() {
+        return mMediaDescription.getExtras();
+    }
+
+    /**
      * @return boolean that indicate if media is explicit.
      */
     public boolean isExplicit() {
diff --git a/car-media-common/src/com/android/car/media/common/MetadataController.java b/car-media-common/src/com/android/car/media/common/MetadataController.java
index 341c129..49e5a26 100644
--- a/car-media-common/src/com/android/car/media/common/MetadataController.java
+++ b/car-media-common/src/com/android/car/media/common/MetadataController.java
@@ -120,6 +120,8 @@
                         ViewUtils.setVisible(artist, !TextUtils.isEmpty(artistName));
                     }
 
+                    ViewUtils.setVisible(albumArt, true);
+
                     mAlbumArtBinder.setImage(context, metadata.getArtworkKey());
                 });
 
diff --git a/car-media-common/src/com/android/car/media/common/playback/PlaybackViewModel.java b/car-media-common/src/com/android/car/media/common/playback/PlaybackViewModel.java
index b23b652..5a655ef 100644
--- a/car-media-common/src/com/android/car/media/common/playback/PlaybackViewModel.java
+++ b/car-media-common/src/com/android/car/media/common/playback/PlaybackViewModel.java
@@ -600,12 +600,12 @@
         }
 
         /**
-         * Starts playing a given media item. This id corresponds to {@link
-         * MediaItemMetadata#getId()}.
+         * Starts playing a given media item.
          */
-        public void playItem(String mediaItemId) {
+        public void playItem(MediaItemMetadata item) {
             if (mMediaController != null) {
-                mMediaController.getTransportControls().playFromMediaId(mediaItemId, null);
+                mMediaController.getTransportControls().playFromMediaId(item.getId(),
+                        item.getExtras());
             }
         }
 
diff --git a/car-media-common/src/com/android/car/media/common/source/MediaSource.java b/car-media-common/src/com/android/car/media/common/source/MediaSource.java
index 09dcbb5..bbc96dc 100644
--- a/car-media-common/src/com/android/car/media/common/source/MediaSource.java
+++ b/car-media-common/src/com/android/car/media/common/source/MediaSource.java
@@ -167,8 +167,7 @@
     }
 
     /**
-     * @return a {@link ComponentName} referencing this media source's {@link MediaBrowserService},
-     * or NULL if this media source doesn't implement such service.
+     * @return a {@link ComponentName} referencing this media source's {@link MediaBrowserService}.
      */
     @NonNull
     public ComponentName getBrowseServiceComponentName() {
diff --git a/car-media-common/src/com/android/car/media/common/source/MediaSourcesLiveData.java b/car-media-common/src/com/android/car/media/common/source/MediaSourcesLiveData.java
index 6abdab2..7d423c8 100644
--- a/car-media-common/src/com/android/car/media/common/source/MediaSourcesLiveData.java
+++ b/car-media-common/src/com/android/car/media/common/source/MediaSourcesLiveData.java
@@ -30,7 +30,12 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
+import com.android.car.media.common.R;
+
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Comparator;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Objects;
@@ -86,10 +91,27 @@
         mAppContext.registerReceiver(mAppInstallUninstallReceiver, filter);
     }
 
-    /** Returns the alphabetically sorted list of available media sources. */
+    /**
+     * Returns the sorted list of available media sources. Sources listed in the array resource
+     * R.array.preferred_media_sources are included first. Other sources follow in alphabetical
+     * order.
+     */
     public List<MediaSource> getList() {
         if (mMediaSources == null) {
-            mMediaSources = getComponentNames().stream()
+            // Get the flattened components to display first.
+            String[] preferredFlats = mAppContext.getResources().getStringArray(
+                    R.array.preferred_media_sources);
+
+            // Make a map of components to display first (the value is the component's index).
+            HashMap<ComponentName, Integer> preferredComps = new HashMap<>(preferredFlats.length);
+            for (int i = 0; i < preferredFlats.length; i++) {
+                preferredComps.put(ComponentName.unflattenFromString(preferredFlats[i]), i);
+            }
+
+            // Prepare an array of the sources to display first (unavailable preferred components
+            // will be excluded).
+            MediaSource[] preferredSources = new MediaSource[preferredFlats.length];
+            List<MediaSource> sortedSources = getComponentNames().stream()
                     .filter(Objects::nonNull)
                     .map(componentName -> MediaSource.create(mAppContext, componentName))
                     .filter(mediaSource -> {
@@ -97,11 +119,23 @@
                             Log.w(TAG, "Media source is null");
                             return false;
                         }
+                        ComponentName srcComp = mediaSource.getBrowseServiceComponentName();
+                        if (preferredComps.containsKey(srcComp)) {
+                            // Record the source in the preferred array...
+                            preferredSources[preferredComps.get(srcComp)] = mediaSource;
+                            // And exclude it from the alpha sort.
+                            return false;
+                        }
                         return true;
                     })
                     .sorted(Comparator.comparing(
                             mediaSource -> mediaSource.getDisplayName().toString()))
                     .collect(Collectors.toList());
+
+            // Concatenate the non null preferred sources and the sorted ones into the result.
+            mMediaSources = new ArrayList<>(sortedSources.size() + preferredFlats.length);
+            Arrays.stream(preferredSources).filter(Objects::nonNull).forEach(mMediaSources::add);
+            mMediaSources.addAll(sortedSources);
         }
         return mMediaSources;
     }
diff --git a/car-ui-lib/generate_rros.mk b/car-ui-lib/generate_rros.mk
index 4e7931a..7e93c36 100644
--- a/car-ui-lib/generate_rros.mk
+++ b/car-ui-lib/generate_rros.mk
@@ -14,7 +14,7 @@
 # limitations under the License.
 #
 
-# Generates one RRO for a given package
+# Generates one RRO for a given package.
 # $(1) target package name
 # $(2) name of the RRO set (e.g. "base")
 # $(3) resources folder
@@ -23,8 +23,8 @@
 
   rro_package_name := $(2)-$(subst .,-,$(1))
   LOCAL_RESOURCE_DIR := $(3)
+  LOCAL_RRO_THEME := $$(rro_package_name)
   LOCAL_PACKAGE_NAME := $$(rro_package_name)
-  LOCAL_PRODUCT_MODULE := true
   LOCAL_CERTIFICATE := platform
   LOCAL_SDK_VERSION := current
 
diff --git a/car-ui-lib/res/layout-port/car_ui_toolbar.xml b/car-ui-lib/res/layout-port/car_ui_toolbar.xml
index aedecc8..1eb6758 100644
--- a/car-ui-lib/res/layout-port/car_ui_toolbar.xml
+++ b/car-ui-lib/res/layout-port/car_ui_toolbar.xml
@@ -20,6 +20,16 @@
     android:layout_width="match_parent"
     android:layout_height="wrap_content">
 
+    <!-- When the user finishes searching, we call clearFocus() on the editText in the search bar.
+         clearFocus() will actually send the focus to the first focusable thing in the layout.
+         If that focusable thing is still the search bar it will just reselect it, and the user won't
+         be able to deselect. So make a focusable view here to guarantee that we can clear the focus -->
+    <View
+        android:layout_width="1dp"
+        android:layout_height="1dp"
+        android:focusable="true"
+        android:focusableInTouchMode="true"/>
+
     <androidx.constraintlayout.widget.Guideline
         android:id="@+id/row_separator_guideline"
         android:layout_width="0dp"
diff --git a/car-ui-lib/res/layout/car_ui_preference_fragment.xml b/car-ui-lib/res/layout/car_ui_preference_fragment.xml
index b70bfa5..766196d 100644
--- a/car-ui-lib/res/layout/car_ui_preference_fragment.xml
+++ b/car-ui-lib/res/layout/car_ui_preference_fragment.xml
@@ -15,10 +15,18 @@
     limitations under the License.
 -->
 
-<FrameLayout
+<LinearLayout
     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="match_parent">
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <com.android.car.ui.toolbar.Toolbar
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:id="@+id/toolbar"
+        app:state="subpage"/>
 
     <FrameLayout
         android:id="@android:id/list_container"
@@ -31,4 +39,4 @@
             android:scrollbars="none"/>
     </FrameLayout>
 
-</FrameLayout>
+</LinearLayout>
diff --git a/car-ui-lib/res/layout/car_ui_preference_widget_switch.xml b/car-ui-lib/res/layout/car_ui_preference_widget_switch.xml
index 9ec6697..f610a63 100644
--- a/car-ui-lib/res/layout/car_ui_preference_widget_switch.xml
+++ b/car-ui-lib/res/layout/car_ui_preference_widget_switch.xml
@@ -15,11 +15,16 @@
     limitations under the License.
 -->
 
-<Switch
+<com.android.car.ui.preference.PreferenceSwitchWidget
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@android:id/switch_widget"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
-    android:background="@null"
-    android:clickable="false"
-    android:focusable="false"/>
+    android:background="@drawable/car_ui_switch_preference_background_drawable"
+    android:foreground="?android:attr/selectableItemBackground"
+    android:showText="@bool/car_ui_preference_switch_toggle_show_text"
+    android:textColor="@color/car_ui_preference_switch_text_color"
+    android:textOff="@string/car_ui_switch_preference_off"
+    android:textOn="@string/car_ui_switch_preference_on"
+    android:thumb="@drawable/car_ui_switch_preference_toggle_thumb_drawable"
+    android:track="@drawable/car_ui_switch_preference_toggle_track_drawable"/>
diff --git a/car-ui-lib/res/layout/car_ui_search_view.xml b/car-ui-lib/res/layout/car_ui_search_view.xml
index cdfa4b9..b328378 100644
--- a/car-ui-lib/res/layout/car_ui_search_view.xml
+++ b/car-ui-lib/res/layout/car_ui_search_view.xml
@@ -38,12 +38,13 @@
         android:id="@+id/search_bar"
         android:layout_height="match_parent"
         android:layout_width="match_parent"
-        android:paddingLeft="@dimen/car_ui_touch_target_width"
+        android:paddingStart="@dimen/car_ui_touch_target_width"
+        android:paddingEnd="@dimen/car_ui_touch_target_width"
         android:hint="@string/car_ui_toolbar_default_search_hint"
         android:textColorHint="@color/car_ui_toolbar_search_hint_text_color"
         android:inputType="text"
         android:singleLine="true"
-        android:imeOptions="actionDone"
+        android:imeOptions="actionSearch"
         app:layout_constraintTop_toTopOf="parent"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintStart_toStartOf="parent"
diff --git a/car-ui-lib/res/layout/car_ui_toolbar.xml b/car-ui-lib/res/layout/car_ui_toolbar.xml
index e3bcb73..7ee939f 100644
--- a/car-ui-lib/res/layout/car_ui_toolbar.xml
+++ b/car-ui-lib/res/layout/car_ui_toolbar.xml
@@ -20,14 +20,51 @@
     android:layout_width="match_parent"
     android:layout_height="@dimen/car_ui_toolbar_first_row_height">
 
+    <!-- When the user finishes searching, we call clearFocus() on the editText in the search bar.
+         clearFocus() will actually send the focus to the first focusable thing in the layout.
+         If that focusable thing is still the search bar it will just reselect it, and the user won't
+         be able to deselect. So make a focusable view here to guarantee that we can clear the focus -->
+    <View
+        android:layout_width="1dp"
+        android:layout_height="1dp"
+        android:focusable="true"
+        android:focusableInTouchMode="true"/>
+
+    <androidx.constraintlayout.widget.Guideline
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:id="@+id/toolbar_start_guideline"
+        app:layout_constraintGuide_begin="@dimen/car_ui_toolbar_start_inset"
+        android:orientation="vertical"/>
+
+    <androidx.constraintlayout.widget.Guideline
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:id="@+id/toolbar_top_guideline"
+        app:layout_constraintGuide_begin="@dimen/car_ui_toolbar_top_inset"
+        android:orientation="horizontal"/>
+
+    <androidx.constraintlayout.widget.Guideline
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:id="@+id/toolbar_end_guideline"
+        app:layout_constraintGuide_end="@dimen/car_ui_toolbar_end_inset"
+        android:orientation="vertical"/>
+
+    <androidx.constraintlayout.widget.Guideline
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:id="@+id/toolbar_bottom_guideline"
+        app:layout_constraintGuide_end="@dimen/car_ui_toolbar_bottom_inset"
+        android:orientation="horizontal"/>
     <FrameLayout
         android:id="@+id/nav_icon_container"
         android:layout_width="@dimen/car_ui_toolbar_nav_button_width"
         android:layout_height="0dp"
         android:background="?android:attr/selectableItemBackground"
-        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintTop_toTopOf="@id/toolbar_top_guideline"
         app:layout_constraintBottom_toTopOf="@+id/bottom_styleable"
-        app:layout_constraintStart_toStartOf="parent">
+        app:layout_constraintStart_toStartOf="@id/toolbar_start_guideline">
         <ImageView
             android:id="@+id/nav_icon"
             android:layout_width="@dimen/car_ui_toolbar_icon_size"
@@ -49,7 +86,7 @@
         android:layout_height="wrap_content"
         android:layout_marginStart="@dimen/car_ui_toolbar_title_margin_start"
         style="@style/TextAppearance.CarUi.Widget.Toolbar.Title"
-        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintTop_toTopOf="@id/toolbar_top_guideline"
         app:layout_constraintBottom_toTopOf="@+id/bottom_styleable"
         app:layout_constraintStart_toEndOf="@+id/nav_icon_container"
         app:layout_constraintEnd_toStartOf="@+id/menu_items_container"/>
@@ -59,7 +96,7 @@
         android:layout_width="wrap_content"
         android:layout_height="0dp"
         android:layout_marginEnd="@dimen/car_ui_toolbar_menu_item_margin"
-        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintTop_toTopOf="@id/toolbar_top_guideline"
         app:layout_constraintBottom_toTopOf="@+id/bottom_styleable"
         app:layout_constraintStart_toEndOf="@+id/nav_icon_container"
         app:layout_constraintEnd_toStartOf="@+id/menu_items_container"
@@ -70,7 +107,7 @@
         android:layout_width="0dp"
         android:layout_height="0dp"
         android:visibility="gone"
-        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintTop_toTopOf="@id/toolbar_top_guideline"
         app:layout_constraintBottom_toTopOf="@+id/bottom_styleable"
         app:layout_constraintStart_toEndOf="@+id/nav_icon_container"
         app:layout_constraintEnd_toStartOf="@+id/menu_items_container"/>
@@ -82,7 +119,7 @@
         android:orientation="horizontal"
         android:divider="@drawable/car_ui_toolbar_menu_item_divider"
         android:showDividers="beginning|middle|end"
-        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintTop_toTopOf="@id/toolbar_top_guideline"
         app:layout_constraintBottom_toTopOf="@+id/bottom_styleable"
         app:layout_constraintEnd_toStartOf="@+id/car_ui_toolbar_overflow_button"/>
 
@@ -93,9 +130,9 @@
         android:layout_marginLeft="@dimen/car_ui_toolbar_menu_item_margin"
         android:layout_marginRight="@dimen/car_ui_toolbar_menu_item_margin"
         android:visibility="gone"
-        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintTop_toTopOf="@id/toolbar_top_guideline"
         app:layout_constraintBottom_toTopOf="@id/bottom_styleable"
-        app:layout_constraintEnd_toEndOf="parent">
+        app:layout_constraintEnd_toEndOf="@id/toolbar_end_guideline">
         <ImageView
             android:src="@drawable/car_ui_icon_overflow_menu"
             android:layout_width="@dimen/car_ui_primary_icon_size"
@@ -111,7 +148,7 @@
         android:layout_width="0dp"
         android:layout_height="match_parent"
         android:visibility="gone"
-        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintTop_toTopOf="@id/toolbar_top_guideline"
         app:layout_constraintBottom_toTopOf="@+id/bottom_styleable"
         app:layout_constraintStart_toEndOf="@+id/nav_icon_container"
         app:layout_constraintEnd_toStartOf="@+id/menu_items_container"/>
@@ -121,8 +158,7 @@
         android:id="@+id/bottom_styleable"
         android:layout_width="match_parent"
         android:layout_height="0.01dp"
-        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintBottom_toBottomOf="@id/toolbar_bottom_guideline"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintEnd_toEndOf="parent"/>
-
 </androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/car-ui-lib/res/values/bools.xml b/car-ui-lib/res/values/bools.xml
index c7a7f48..906ccab 100644
--- a/car-ui-lib/res/values/bools.xml
+++ b/car-ui-lib/res/values/bools.xml
@@ -28,4 +28,7 @@
     <bool name="car_ui_preference_allow_divider_below">false</bool>
     <bool name="car_ui_preference_category_allow_divider_above">false</bool>
     <bool name="car_ui_preference_category_allow_divider_below">false</bool>
+
+    <bool name="car_ui_preference_switch_toggle_show_text">false</bool>
+    <bool name="car_ui_preference_switch_toggle_show_animation">true</bool>
 </resources>
diff --git a/car-ui-lib/res/values/colors.xml b/car-ui-lib/res/values/colors.xml
index 2a92da5..cb748a7 100644
--- a/car-ui-lib/res/values/colors.xml
+++ b/car-ui-lib/res/values/colors.xml
@@ -49,4 +49,7 @@
     <color name="car_ui_preference_edit_text_dialog_message_text_color">#ffdadce0</color>
 
     <color name="car_ui_preference_icon_color">@android:color/white</color>
+
+    <color name="car_ui_preference_switch_text_color">#000000</color>
+
 </resources>
diff --git a/car-ui-lib/res/values/dimens.xml b/car-ui-lib/res/values/dimens.xml
index eb4b9e1..d92801d 100644
--- a/car-ui-lib/res/values/dimens.xml
+++ b/car-ui-lib/res/values/dimens.xml
@@ -50,6 +50,10 @@
     <dimen name="car_ui_toolbar_first_row_height">@dimen/car_ui_toolbar_row_height</dimen>
     <dimen name="car_ui_toolbar_second_row_height">@dimen/car_ui_toolbar_row_height</dimen>
     <dimen name="car_ui_toolbar_nav_button_width">@dimen/car_ui_margin</dimen>
+    <dimen name="car_ui_toolbar_start_inset">0dp</dimen>
+    <dimen name="car_ui_toolbar_end_inset">0dp</dimen>
+    <dimen name="car_ui_toolbar_top_inset">0dp</dimen>
+    <dimen name="car_ui_toolbar_bottom_inset">0dp</dimen>
     <dimen name="car_ui_toolbar_icon_size">@dimen/car_ui_primary_icon_size</dimen>
     <dimen name="car_ui_toolbar_title_margin_start">@dimen/car_ui_padding_2</dimen>
     <dimen name="car_ui_toolbar_menu_item_margin">@dimen/car_ui_padding_2</dimen>
diff --git a/car-ui-lib/res/values/values.xml b/car-ui-lib/res/values/values.xml
index 9dba256..5d4d22f 100644
--- a/car-ui-lib/res/values/values.xml
+++ b/car-ui-lib/res/values/values.xml
@@ -18,4 +18,8 @@
 <resources>
     <bool name="car_ui_tab_flexible_layout">false</bool>
     <item name="car_ui_tab_item_layout" type="layout">@layout/car_ui_tab_item</item>
+
+    <item name="car_ui_switch_preference_toggle_thumb_drawable" type="drawable">@*android:drawable/switch_thumb_material_anim</item>
+    <item name="car_ui_switch_preference_toggle_track_drawable" type="drawable">@*android:drawable/switch_track_material</item>
+    <item name="car_ui_switch_preference_background_drawable" type="drawable">@null</item>
 </resources>
diff --git a/car-ui-lib/src/com/android/car/ui/pagedrecyclerview/DefaultScrollBar.java b/car-ui-lib/src/com/android/car/ui/pagedrecyclerview/DefaultScrollBar.java
index c98d114..3dbb6aa 100644
--- a/car-ui-lib/src/com/android/car/ui/pagedrecyclerview/DefaultScrollBar.java
+++ b/car-ui-lib/src/com/android/car/ui/pagedrecyclerview/DefaultScrollBar.java
@@ -29,6 +29,7 @@
 import android.widget.ImageView;
 
 import androidx.annotation.IntRange;
+import androidx.annotation.VisibleForTesting;
 import androidx.recyclerview.widget.OrientationHelper;
 import androidx.recyclerview.widget.RecyclerView;
 
@@ -43,16 +44,19 @@
  * been ported from the PLV with minor updates.
  */
 class DefaultScrollBar implements ScrollBar {
+
+    @VisibleForTesting
+    int mPaddingStart;
+    @VisibleForTesting
+    int mPaddingEnd;
+
     private float mButtonDisabledAlpha;
-    private static final String TAG = "DefaultScrollBar";
     private PagedSnapHelper mSnapHelper;
 
     private ImageView mUpButton;
     private View mScrollView;
     private View mScrollThumb;
     private ImageView mDownButton;
-    private int mPaddingStart;
-    private int mPaddingEnd;
 
     private int mSeparatingMargin;
 
@@ -75,7 +79,7 @@
             @ScrollBarPosition int scrollBarPosition,
             boolean scrollBarAboveRecyclerView) {
 
-        this.mRecyclerView = rv;
+        mRecyclerView = rv;
 
         LayoutInflater inflater =
                 (LayoutInflater) rv.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
@@ -84,7 +88,7 @@
 
         mScrollView = inflater.inflate(R.layout.car_ui_pagedrecyclerview_scrollbar, parent, false);
         mScrollView.setLayoutParams(
-                new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT));
+                new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT));
 
         Resources res = rv.getContext().getResources();
 
diff --git a/car-ui-lib/src/com/android/car/ui/pagedrecyclerview/PagedRecyclerView.java b/car-ui-lib/src/com/android/car/ui/pagedrecyclerview/PagedRecyclerView.java
index 52bc541..adcd526 100644
--- a/car-ui-lib/src/com/android/car/ui/pagedrecyclerview/PagedRecyclerView.java
+++ b/car-ui-lib/src/com/android/car/ui/pagedrecyclerview/PagedRecyclerView.java
@@ -28,12 +28,12 @@
 import android.util.SparseArray;
 import android.view.ContextThemeWrapper;
 import android.view.View;
-import android.view.ViewGroup;
 import android.view.ViewTreeObserver.OnGlobalLayoutListener;
 
 import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 import androidx.recyclerview.widget.GridLayoutManager;
 import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
@@ -45,6 +45,7 @@
 import com.android.car.ui.pagedrecyclerview.decorations.linear.LinearOffsetItemDecoration;
 import com.android.car.ui.pagedrecyclerview.decorations.linear.LinearOffsetItemDecoration.OffsetPosition;
 import com.android.car.ui.toolbar.Toolbar;
+import com.android.car.ui.utils.CarUxRestrictionsUtil;
 
 import java.lang.annotation.Retention;
 
@@ -76,7 +77,8 @@
     @Gutter
     private int mGutter;
     private int mGutterSize;
-    private RecyclerView mNestedRecyclerView;
+    @VisibleForTesting
+    RecyclerView mNestedRecyclerView;
     private Adapter<?> mAdapter;
     private ScrollBar mScrollBar;
     private int mInitialTopPadding;
@@ -375,7 +377,6 @@
                                         == null) {
                                     return;
                                 }
-
                                 PagedRecyclerView.this.getViewTreeObserver()
                                         .removeOnGlobalLayoutListener(this);
                                 initNestedRecyclerView();
@@ -390,10 +391,6 @@
                                                         mNestedRecyclerView
                                                                 .getViewTreeObserver()
                                                                 .removeOnGlobalLayoutListener(this);
-                                                        ViewGroup.LayoutParams params =
-                                                                getLayoutParams();
-                                                        params.height = getMeasuredHeight();
-                                                        setLayoutParams(params);
                                                         createScrollBarFromConfig();
                                                         if (mInitialTopPadding == 0) {
                                                             mInitialTopPadding = getPaddingTop();
@@ -452,11 +449,11 @@
         return super.getLayoutManager();
     }
 
+    /**
+     * Refer to {@link PagedRecyclerView#getEffectiveLayoutManager()} for usage in applications.
+     */
     @Override
     public LayoutManager getLayoutManager() {
-        if (mScrollBarEnabled) {
-            return mNestedRecyclerView.getLayoutManager();
-        }
         return super.getLayoutManager();
     }
 
diff --git a/car-ui-lib/src/com/android/car/ui/pagedrecyclerview/PagedSmoothScroller.java b/car-ui-lib/src/com/android/car/ui/pagedrecyclerview/PagedSmoothScroller.java
index 1b73b7a..1cb421d 100644
--- a/car-ui-lib/src/com/android/car/ui/pagedrecyclerview/PagedSmoothScroller.java
+++ b/car-ui-lib/src/com/android/car/ui/pagedrecyclerview/PagedSmoothScroller.java
@@ -20,12 +20,12 @@
 import android.view.animation.DecelerateInterpolator;
 import android.view.animation.Interpolator;
 
+import androidx.annotation.VisibleForTesting;
 import androidx.recyclerview.widget.LinearSmoothScroller;
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.car.ui.R;
 import com.android.car.ui.utils.ResourceUtils;
-
 /**
  * Code drop from {androidx.car.widget.PagedSmoothScroller}
  *
@@ -37,12 +37,16 @@
  * </ul>
  */
 public class PagedSmoothScroller extends LinearSmoothScroller {
-    private float mMillisecondsPerInch;
-    private float mDecelerationTimeDivisor;
-    private float mMillisecondsPerPixel;
-
-    private Interpolator mInterpolator;
-    private int mDensityDpi;
+    @VisibleForTesting
+    float mMillisecondsPerInch;
+    @VisibleForTesting
+    float mDecelerationTimeDivisor;
+    @VisibleForTesting
+    float mMillisecondsPerPixel;
+    @VisibleForTesting
+    Interpolator mInterpolator;
+    @VisibleForTesting
+    int mDensityDpi;
 
     public PagedSmoothScroller(Context context) {
         super(context);
diff --git a/car-ui-lib/src/com/android/car/ui/preference/PreferenceSwitchWidget.java b/car-ui-lib/src/com/android/car/ui/preference/PreferenceSwitchWidget.java
new file mode 100644
index 0000000..39ad79a
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/preference/PreferenceSwitchWidget.java
@@ -0,0 +1,68 @@
+/*
+ * 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.preference;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.Switch;
+
+import com.android.car.ui.R;
+
+/**
+ * Switch preference widget. This widget is exactly similar to switch widget just that it calls
+ * {@link Switch#jumpDrawablesToCurrentState} on each click.
+ */
+public class PreferenceSwitchWidget extends Switch {
+
+    private Context mContext;
+
+    public PreferenceSwitchWidget(Context context) {
+        super(context);
+        mContext = context;
+    }
+
+    public PreferenceSwitchWidget(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mContext = context;
+    }
+
+    public PreferenceSwitchWidget(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        mContext = context;
+    }
+
+    public PreferenceSwitchWidget(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        mContext = context;
+    }
+
+    @Override
+    public void setChecked(boolean checked) {
+        super.setChecked(checked);
+
+        if (mContext == null) {
+            return;
+        }
+
+        boolean enableAnimation = mContext.getResources().getBoolean(
+                R.bool.car_ui_preference_switch_toggle_show_animation);
+        if (!enableAnimation) {
+            jumpDrawablesToCurrentState();
+        }
+    }
+}
diff --git a/car-ui-lib/src/com/android/car/ui/toolbar/SearchView.java b/car-ui-lib/src/com/android/car/ui/toolbar/SearchView.java
index ef29354..35898db 100644
--- a/car-ui-lib/src/com/android/car/ui/toolbar/SearchView.java
+++ b/car-ui-lib/src/com/android/car/ui/toolbar/SearchView.java
@@ -19,6 +19,7 @@
 import android.graphics.Bitmap;
 import android.graphics.drawable.Drawable;
 import android.text.Editable;
+import android.text.TextUtils;
 import android.text.TextWatcher;
 import android.util.AttributeSet;
 import android.view.LayoutInflater;
@@ -41,7 +42,22 @@
 public class SearchView extends ConstraintLayout {
     private final ImageView mIcon;
     private final EditText mSearchText;
+    private final View mCloseIcon;
     private final Set<Toolbar.OnSearchListener> mListeners = new HashSet<>();
+    private final TextWatcher mTextWatcher = new TextWatcher() {
+        @Override
+        public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+        }
+
+        @Override
+        public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+        }
+
+        @Override
+        public void afterTextChanged(Editable editable) {
+            onSearch(editable.toString());
+        }
+    };
 
     public SearchView(Context context) {
         this(context, null);
@@ -58,9 +74,9 @@
         inflater.inflate(R.layout.car_ui_search_view, this, true);
 
         mSearchText = requireViewById(R.id.search_bar);
-        View closeIcon = requireViewById(R.id.search_close);
-        closeIcon.setOnClickListener(view -> mSearchText.getText().clear());
         mIcon = requireViewById(R.id.icon);
+        mCloseIcon = requireViewById(R.id.search_close);
+        mCloseIcon.setOnClickListener(view -> mSearchText.getText().clear());
 
         mSearchText.setOnFocusChangeListener(
                 (view, hasFocus) -> {
@@ -74,20 +90,8 @@
                                 .hideSoftInputFromWindow(view.getWindowToken(), 0);
                     }
                 });
-        mSearchText.addTextChangedListener(new TextWatcher() {
-            @Override
-            public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
-            }
 
-            @Override
-            public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
-            }
-
-            @Override
-            public void afterTextChanged(Editable editable) {
-                onSearch(editable.toString());
-            }
-        });
+        mSearchText.addTextChangedListener(mTextWatcher);
 
         mSearchText.setOnEditorActionListener((v, actionId, event) -> {
             if (actionId == EditorInfo.IME_ACTION_DONE
@@ -98,6 +102,22 @@
         });
     }
 
+    @Override
+    public void setVisibility(int visibility) {
+        boolean showing = visibility == View.VISIBLE && getVisibility() != View.VISIBLE;
+
+        super.setVisibility(visibility);
+
+        if (showing) {
+            mSearchText.removeTextChangedListener(mTextWatcher);
+            mSearchText.getText().clear();
+            mSearchText.addTextChangedListener(mTextWatcher);
+            mCloseIcon.setVisibility(View.GONE);
+
+            mSearchText.requestFocus();
+        }
+    }
+
     /**
      * Adds a listener for the search text changing.
      * See also {@link #unregisterOnSearchListener(Toolbar.OnSearchListener)}
@@ -157,6 +177,8 @@
     }
 
     private void onSearch(String query) {
+        mCloseIcon.setVisibility(TextUtils.isEmpty(query) ? View.GONE : View.VISIBLE);
+
         for (Toolbar.OnSearchListener listener : mListeners) {
             listener.onSearch(query);
         }
diff --git a/car-ui-lib/src/com/android/car/ui/toolbar/Toolbar.java b/car-ui-lib/src/com/android/car/ui/toolbar/Toolbar.java
index e656aa0..ba92c40 100644
--- a/car-ui-lib/src/com/android/car/ui/toolbar/Toolbar.java
+++ b/car-ui-lib/src/com/android/car/ui/toolbar/Toolbar.java
@@ -23,11 +23,13 @@
 import android.graphics.drawable.Drawable;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
 import android.widget.FrameLayout;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
@@ -60,6 +62,7 @@
     public interface OnHeightChangedListener {
         /**
          * Will be called when the height of the toolbar is changed.
+         *
          * @param height new height of the toolbar
          */
         void onHeightChanged(int height);
@@ -158,8 +161,6 @@
     };
     private AlertDialog mOverflowDialog;
 
-
-
     public Toolbar(Context context) {
         this(context, null);
     }
@@ -262,7 +263,15 @@
             }
         });
 
-        handleToolbarHeightChangeListeners(getHeight());
+        getViewTreeObserver().addOnGlobalLayoutListener(
+                new ViewTreeObserver.OnGlobalLayoutListener() {
+                    @Override
+                    public void onGlobalLayout() {
+                        for (OnHeightChangedListener listener : mOnHeightChangedListeners) {
+                            listener.onHeightChanged(getHeight());
+                        }
+                    }
+                });
     }
 
     @Override
@@ -309,35 +318,47 @@
 
         SavedState(Parcel in) {
             super(in);
-            mTitle = in.readCharSequence();
+            mTitle = readCharSequence(in);
             mNavButtonMode = NavButtonMode.valueOf(in.readString());
-            mSearchHint = in.readCharSequence();
-            mBackgroundShown = in.readBoolean();
-            mShowMenuItemsWhileSearching = in.readBoolean();
+            mSearchHint = readCharSequence(in);
+            mBackgroundShown = in.readInt() != 0;
+            mShowMenuItemsWhileSearching = in.readInt() != 0;
             mState = State.valueOf(in.readString());
         }
 
         @Override
         public void writeToParcel(Parcel out, int flags) {
             super.writeToParcel(out, flags);
-            out.writeCharSequence(mTitle);
+            writeCharSequence(out, mTitle);
             out.writeString(mNavButtonMode.name());
-            out.writeCharSequence(mSearchHint);
-            out.writeBoolean(mBackgroundShown);
-            out.writeBoolean(mShowMenuItemsWhileSearching);
+            writeCharSequence(out, mSearchHint);
+            out.writeInt(mBackgroundShown ? 1 : 0);
+            out.writeInt(mShowMenuItemsWhileSearching ? 1 : 0);
             out.writeString(mState.name());
         }
 
         public static final Parcelable.Creator<SavedState> CREATOR =
                 new Parcelable.Creator<SavedState>() {
+            @Override
             public SavedState createFromParcel(Parcel in) {
                 return new SavedState(in);
             }
 
+            @Override
             public SavedState[] newArray(int size) {
                 return new SavedState[size];
             }
         };
+
+        /** Replacement of hidden Parcel#readCharSequence(Parcel) */
+        private static CharSequence readCharSequence(Parcel in) {
+            return TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
+        }
+
+        /** Replacement of hidden Parcel#writeCharSequence(Parcel, CharSequence) */
+        private static void writeCharSequence(Parcel dest, CharSequence val) {
+            TextUtils.writeToParcel(val, dest, 0);
+        }
     }
 
     /**
@@ -642,8 +663,8 @@
         mNavIconContainer.setOnClickListener(state != State.HOME ? backClickListener : null);
         mNavIconContainer.setClickable(state != State.HOME);
         boolean hasTabs = mTabLayout.getTabCount() > 0;
-        boolean showTitle = state == State.SUBPAGE || state == State.HOME
-                && (!mTitleAndTabsAreMutuallyExclusive || !hasTabs);
+        boolean showTitle = state == State.SUBPAGE
+                || (state == State.HOME && (!mTitleAndTabsAreMutuallyExclusive || !hasTabs));
         mTitle.setVisibility(showTitle ? VISIBLE : GONE);
         mTabLayout.setVisibility(state == State.HOME && hasTabs ? VISIBLE : GONE);
         mSearchView.setVisibility(state == State.SEARCH ? VISIBLE : GONE);
diff --git a/car-ui-lib/src/com/android/car/ui/pagedrecyclerview/CarUxRestrictionsUtil.java b/car-ui-lib/src/com/android/car/ui/utils/CarUxRestrictionsUtil.java
similarity index 93%
rename from car-ui-lib/src/com/android/car/ui/pagedrecyclerview/CarUxRestrictionsUtil.java
rename to car-ui-lib/src/com/android/car/ui/utils/CarUxRestrictionsUtil.java
index dd74116..355050a 100644
--- a/car-ui-lib/src/com/android/car/ui/pagedrecyclerview/CarUxRestrictionsUtil.java
+++ b/car-ui-lib/src/com/android/car/ui/utils/CarUxRestrictionsUtil.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.car.ui.pagedrecyclerview;
+package com.android.car.ui.utils;
 
 import static android.car.drivingstate.CarUxRestrictions.UX_RESTRICTIONS_LIMIT_STRING_LENGTH;
 
@@ -56,13 +56,13 @@
         CarUxRestrictionsManager.OnUxRestrictionsChangedListener listener =
                 (carUxRestrictions) -> {
                     if (carUxRestrictions == null) {
-                        this.mCarUxRestrictions = getDefaultRestrictions();
+                        mCarUxRestrictions = getDefaultRestrictions();
                     } else {
-                        this.mCarUxRestrictions = carUxRestrictions;
+                        mCarUxRestrictions = carUxRestrictions;
                     }
 
                     for (OnUxRestrictionsChangedListener observer : mObservers) {
-                        observer.onRestrictionsChanged(this.mCarUxRestrictions);
+                        observer.onRestrictionsChanged(mCarUxRestrictions);
                     }
                 };
 
@@ -121,7 +121,7 @@
     }
 
     /**
-     * Returns whether any of the given flags is blocked by the current restrictions. If null is
+     * Returns whether any of the given flags are blocked by the specified restrictions. If null is
      * given, the method returns true for safety.
      */
     public static boolean isRestricted(
diff --git a/car-ui-lib/tests/paintbooth/AndroidManifest-gradle.xml b/car-ui-lib/tests/paintbooth/AndroidManifest-gradle.xml
index b79960e..23d42ca 100644
--- a/car-ui-lib/tests/paintbooth/AndroidManifest-gradle.xml
+++ b/car-ui-lib/tests/paintbooth/AndroidManifest-gradle.xml
@@ -21,7 +21,7 @@
   <application
       android:icon="@drawable/ic_launcher"
       android:label="@string/app_name"
-      android:theme="@style/CarUiTheme">
+      android:theme="@style/Theme.CarUi">
     <activity
         android:name=".MainActivity"
         android:exported="true">
@@ -32,8 +32,26 @@
     </activity>
 
     <activity
-        android:name=".DialogsActivity"
+        android:name=".dialogs.DialogsActivity"
         android:exported="false"
         android:parentActivityName=".MainActivity"/>
+    <activity
+        android:name=".pagedrecyclerview.PagedRecyclerViewActivity"
+        android:exported="false"
+        android:parentActivityName=".MainActivity"/>
+    <activity
+        android:name=".pagedrecyclerview.GridPagedRecyclerViewActivity"
+        android:exported="false"
+        android:parentActivityName=".MainActivity"/>
+    <activity
+        android:name=".preferences.PreferenceActivity"
+        android:exported="false"
+        android:parentActivityName=".MainActivity"/>
+    <activity
+        android:name=".toolbar.ToolbarActivity"
+        android:exported="false"
+        android:parentActivityName=".MainActivity">
+      <meta-data android:name="distractionOptimized" android:value="true"/>
+    </activity>
   </application>
 </manifest>
diff --git a/car-ui-lib/tests/paintbooth/AndroidManifest.xml b/car-ui-lib/tests/paintbooth/AndroidManifest.xml
index 8b3cce7..7d5e392 100644
--- a/car-ui-lib/tests/paintbooth/AndroidManifest.xml
+++ b/car-ui-lib/tests/paintbooth/AndroidManifest.xml
@@ -54,6 +54,8 @@
     <activity
         android:name=".toolbar.ToolbarActivity"
         android:exported="false"
-        android:parentActivityName=".MainActivity"/>
+        android:parentActivityName=".MainActivity">
+      <meta-data android:name="distractionOptimized" android:value="true"/>
+    </activity>
   </application>
 </manifest>
diff --git a/car-ui-lib/tests/paintbooth/gradlew b/car-ui-lib/tests/paintbooth/gradlew
new file mode 100644
index 0000000..cccdd3d
--- /dev/null
+++ b/car-ui-lib/tests/paintbooth/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+    echo "$*"
+}
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+  NONSTOP* )
+    nonstop=true
+    ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+    JAVACMD=`cygpath --unix "$JAVACMD"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=$((i+1))
+    done
+    case $i in
+        (0) set -- ;;
+        (1) set -- "$args0" ;;
+        (2) set -- "$args0" "$args1" ;;
+        (3) set -- "$args0" "$args1" "$args2" ;;
+        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Escape application args
+save () {
+    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+    echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+  cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/car-ui-lib/tests/paintbooth/gradlew.bat b/car-ui-lib/tests/paintbooth/gradlew.bat
new file mode 100644
index 0000000..e95643d
--- /dev/null
+++ b/car-ui-lib/tests/paintbooth/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off

+@rem ##########################################################################

+@rem

+@rem  Gradle startup script for Windows

+@rem

+@rem ##########################################################################

+

+@rem Set local scope for the variables with windows NT shell

+if "%OS%"=="Windows_NT" setlocal

+

+set DIRNAME=%~dp0

+if "%DIRNAME%" == "" set DIRNAME=.

+set APP_BASE_NAME=%~n0

+set APP_HOME=%DIRNAME%

+

+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.

+set DEFAULT_JVM_OPTS=

+

+@rem Find java.exe

+if defined JAVA_HOME goto findJavaFromJavaHome

+

+set JAVA_EXE=java.exe

+%JAVA_EXE% -version >NUL 2>&1

+if "%ERRORLEVEL%" == "0" goto init

+

+echo.

+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.

+echo.

+echo Please set the JAVA_HOME variable in your environment to match the

+echo location of your Java installation.

+

+goto fail

+

+:findJavaFromJavaHome

+set JAVA_HOME=%JAVA_HOME:"=%

+set JAVA_EXE=%JAVA_HOME%/bin/java.exe

+

+if exist "%JAVA_EXE%" goto init

+

+echo.

+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%

+echo.

+echo Please set the JAVA_HOME variable in your environment to match the

+echo location of your Java installation.

+

+goto fail

+

+:init

+@rem Get command-line arguments, handling Windows variants

+

+if not "%OS%" == "Windows_NT" goto win9xME_args

+

+:win9xME_args

+@rem Slurp the command line arguments.

+set CMD_LINE_ARGS=

+set _SKIP=2

+

+:win9xME_args_slurp

+if "x%~1" == "x" goto execute

+

+set CMD_LINE_ARGS=%*

+

+:execute

+@rem Setup the command line

+

+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar

+

+@rem Execute Gradle

+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%

+

+:end

+@rem End local scope for the variables with windows NT shell

+if "%ERRORLEVEL%"=="0" goto mainEnd

+

+:fail

+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of

+rem the _cmd.exe /c_ return code!

+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1

+exit /b 1

+

+:mainEnd

+if "%OS%"=="Windows_NT" endlocal

+

+:omega

diff --git a/car-ui-lib/tests/paintbooth/res/xml/preference_samples.xml b/car-ui-lib/tests/paintbooth/res/xml/preference_samples.xml
index 9c7cf1a..88fb060 100644
--- a/car-ui-lib/tests/paintbooth/res/xml/preference_samples.xml
+++ b/car-ui-lib/tests/paintbooth/res/xml/preference_samples.xml
@@ -37,7 +37,7 @@
         android:title="@string/title_checkbox_preference"
         android:summary="@string/summary_checkbox_preference"/>
 
-    <SwitchPreferenceCompat
+    <SwitchPreference
         android:key="switch"
         android:title="@string/title_switch_preference"
         android:summary="@string/summary_switch_preference"/>
@@ -101,18 +101,18 @@
 
     </Preference>
 
-    <SwitchPreferenceCompat
+    <SwitchPreference
         android:key="parent"
         android:title="@string/title_parent_preference"
         android:summary="@string/summary_parent_preference"/>
 
-    <SwitchPreferenceCompat
+    <SwitchPreference
         android:key="child"
         android:dependency="parent"
         android:title="@string/title_child_preference"
         android:summary="@string/summary_child_preference"/>
 
-    <SwitchPreferenceCompat
+    <SwitchPreference
         android:key="toggle_summary"
         android:title="@string/title_toggle_summary_preference"
         android:summaryOn="@string/summary_on_toggle_summary_preference"
diff --git a/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/toolbar/ToolbarActivity.java b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/toolbar/ToolbarActivity.java
index 531c3a8..e2d3346 100644
--- a/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/toolbar/ToolbarActivity.java
+++ b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/toolbar/ToolbarActivity.java
@@ -19,6 +19,7 @@
 import android.app.AlertDialog;
 import android.os.Bundle;
 import android.text.InputType;
+import android.util.Pair;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -27,7 +28,6 @@
 import android.widget.Toast;
 
 import androidx.annotation.NonNull;
-import androidx.core.util.Pair;
 
 import com.android.car.ui.pagedrecyclerview.PagedRecyclerView;
 import com.android.car.ui.paintbooth.R;
@@ -91,7 +91,7 @@
             new AlertDialog.Builder(this)
                     .setView(textBox)
                     .setTitle("Enter the index of the MenuItem to toggle")
-                    .setPositiveButton("Ok", ((dialog, which) -> {
+                    .setPositiveButton("Ok", (dialog, which) -> {
                         try {
                             MenuItem item = mMenuItems.get(
                                     Integer.parseInt(textBox.getText().toString()));
@@ -102,7 +102,7 @@
                                     + "\", valid range is 0 to " + (mMenuItems.size() - 1),
                                     Toast.LENGTH_LONG).show();
                         }
-                    }))
+                    })
                     .show();
         }));
 
@@ -161,7 +161,7 @@
     }
 
     private PagedRecyclerView.Adapter mAdapter = new PagedRecyclerView.Adapter() {
-
+        @Override
         public int getItemCount() {
             return mButtons.size();
         }
diff --git a/car-ui-lib/tests/robotests/Android.mk b/car-ui-lib/tests/robotests/Android.mk
new file mode 100644
index 0000000..b924900
--- /dev/null
+++ b/car-ui-lib/tests/robotests/Android.mk
@@ -0,0 +1,92 @@
+#
+# Copyright (C) 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.
+#
+
+LOCAL_PATH := $(call my-dir)
+
+############################################################
+# CarUi lib just for Robolectric test target.     #
+############################################################
+include $(CLEAR_VARS)
+
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res \
+    $(LOCAL_PATH)/tests/robotests/res \
+
+LOCAL_PACKAGE_NAME := CarUi
+LOCAL_PRIVATE_PLATFORM_APIS := true
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_USE_AAPT2 := true
+
+LOCAL_PRIVILEGED_MODULE := true
+
+LOCAL_JAVA_LIBRARIES := android.car
+
+LOCAL_STATIC_ANDROID_LIBRARIES := \
+    car-ui-lib
+
+include $(BUILD_PACKAGE)
+
+################################################
+# Car Ui Robolectric test target. #
+################################################
+include $(CLEAR_VARS)
+
+LOCAL_MODULE := CarUiRoboTests
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_JAVA_RESOURCE_DIRS := config
+
+# Include the testing libraries
+LOCAL_JAVA_LIBRARIES := \
+    android.car \
+    robolectric_android-all-stub \
+    Robolectric_all-target \
+    mockito-robolectric-prebuilt \
+    testng \
+    truth-prebuilt
+
+
+LOCAL_INSTRUMENTATION_FOR := CarUi
+
+LOCAL_MODULE_TAGS := optional
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
+##################################################################
+# Car Ui runner target to run the previous target. #
+##################################################################
+include $(CLEAR_VARS)
+
+LOCAL_MODULE := RunCarUiRoboTests
+
+LOCAL_JAVA_LIBRARIES := \
+    android.car \
+    CarUiRoboTests \
+    robolectric_android-all-stub \
+    Robolectric_all-target \
+    mockito-robolectric-prebuilt \
+    testng \
+    truth-prebuilt
+
+LOCAL_TEST_PACKAGE := CarUi
+
+LOCAL_ROBOTEST_FILES := $(filter-out %/BaseRobolectricTest.java,\
+    $(call find-files-in-subdirs,$(LOCAL_PATH)/src,*Test.java,.))
+
+LOCAL_INSTRUMENT_SOURCE_DIRS := $(dir $(LOCAL_PATH))../src
+
+include external/robolectric-shadows/run_robotests.mk
diff --git a/car-ui-lib/tests/robotests/AndroidManifest.xml b/car-ui-lib/tests/robotests/AndroidManifest.xml
new file mode 100644
index 0000000..64926b8
--- /dev/null
+++ b/car-ui-lib/tests/robotests/AndroidManifest.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2019 Google Inc.
+
+    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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.car.ui.robotests">
+</manifest>
diff --git a/car-ui-lib/tests/robotests/config/robolectric.properties b/car-ui-lib/tests/robotests/config/robolectric.properties
new file mode 100644
index 0000000..8768f6b
--- /dev/null
+++ b/car-ui-lib/tests/robotests/config/robolectric.properties
@@ -0,0 +1,17 @@
+#
+# Copyright (C) 2019 Google Inc.
+#
+# 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.
+#
+manifest=packages/apps/Car/libs/car-ui-lib/tests/robotests/AndroidManifest.xml
+sdk=NEWEST_SDK
diff --git a/car-ui-lib/tests/robotests/res/layout/test_grid_paged_recycler_view.xml b/car-ui-lib/tests/robotests/res/layout/test_grid_paged_recycler_view.xml
new file mode 100644
index 0000000..92e16df
--- /dev/null
+++ b/car-ui-lib/tests/robotests/res/layout/test_grid_paged_recycler_view.xml
@@ -0,0 +1,27 @@
+<!--
+  ~ Copyright (C) 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.
+  -->
+<FrameLayout
+    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="match_parent">
+    <com.android.car.ui.pagedrecyclerview.PagedRecyclerView
+        android:id="@+id/test_prv"
+        app:layoutStyle="grid"
+        app:numOfColumns="4"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"/>
+</FrameLayout>
\ No newline at end of file
diff --git a/car-ui-lib/tests/robotests/res/layout/test_linear_paged_recycler_view.xml b/car-ui-lib/tests/robotests/res/layout/test_linear_paged_recycler_view.xml
new file mode 100644
index 0000000..f2c9416
--- /dev/null
+++ b/car-ui-lib/tests/robotests/res/layout/test_linear_paged_recycler_view.xml
@@ -0,0 +1,24 @@
+<!--
+  ~ Copyright (C) 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.
+  -->
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+    <com.android.car.ui.pagedrecyclerview.PagedRecyclerView
+        android:id="@+id/test_prv"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"/>
+</FrameLayout>
\ No newline at end of file
diff --git a/car-ui-lib/tests/robotests/src/com/android/car/ui/CarUiRobolectricTestRunner.java b/car-ui-lib/tests/robotests/src/com/android/car/ui/CarUiRobolectricTestRunner.java
new file mode 100644
index 0000000..7070a88
--- /dev/null
+++ b/car-ui-lib/tests/robotests/src/com/android/car/ui/CarUiRobolectricTestRunner.java
@@ -0,0 +1,120 @@
+/*
+ * 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;
+
+import androidx.annotation.NonNull;
+
+import org.junit.runners.model.InitializationError;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.manifest.AndroidManifest;
+import org.robolectric.res.Fs;
+import org.robolectric.res.ResourcePath;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Custom test runner for CarUi. This is needed because the default behavior for
+ * robolectric is just to grab the resource directory in the target package.
+ * We want to override this to add several spanning different projects.
+ */
+public class CarUiRobolectricTestRunner extends RobolectricTestRunner {
+    private static final Map<String, String> AAR_VERSIONS;
+    private static final String SUPPORT_RESOURCE_PATH_TEMPLATE =
+            "jar:file:prebuilts/sdk/current/androidx/m2repository/androidx/"
+                    + "%1$s/%1$s/%2$s/%1$s-%2$s.aar!/res";
+    // contraint-layout aar lives in separate path.
+    // Note its path contains a hyphen.
+    private static final String CONSTRAINT_LAYOUT_RESOURCE_PATH_TEMPLATE =
+            "jar:file:prebuilts/sdk/current/extras/constraint-layout-x/"
+                    + "%1$s/%2$s/%1$s-%2$s.aar!/res";
+
+    static {
+        AAR_VERSIONS = new HashMap<>();
+        AAR_VERSIONS.put("appcompat", "1.1.0-alpha01");
+        AAR_VERSIONS.put("constraintlayout", "1.1.2");
+        AAR_VERSIONS.put("preference", "1.1.0-alpha02");
+    }
+
+    public CarUiRobolectricTestRunner(Class<?> testClass) throws InitializationError {
+        super(testClass);
+    }
+
+    private static ResourcePath createResourcePath(@NonNull String filePath) {
+        try {
+            return new ResourcePath(null, Fs.fromURL(new URL(filePath)), null);
+        } catch (MalformedURLException e) {
+            throw new RuntimeException("CarUiRobolectricTestRunner failure", e);
+        }
+    }
+
+    /**
+     * Create the resource path for a support library component's JAR.
+     */
+    private static String createSupportResourcePathFromJar(@NonNull String componentId) {
+        if (!AAR_VERSIONS.containsKey(componentId)) {
+            throw new IllegalArgumentException("Unknown component " + componentId
+                    + ". Update test with appropriate component name and version.");
+        }
+        if (componentId.equals("constraintlayout")) {
+            return String.format(CONSTRAINT_LAYOUT_RESOURCE_PATH_TEMPLATE, componentId,
+                    AAR_VERSIONS.get(componentId));
+        }
+        return String.format(SUPPORT_RESOURCE_PATH_TEMPLATE, componentId,
+                AAR_VERSIONS.get(componentId));
+    }
+
+    /**
+     * We modify the AndroidManifest such that we can add required resources.
+     */
+    @Override
+    protected AndroidManifest getAppManifest(Config config) {
+        try {
+            // Using the manifest file's relative path, we can figure out the application directory.
+            URL appRoot = new URL("file:packages/apps/Car/libs/car-ui-lib/");
+            URL manifestPath = new URL(appRoot, "AndroidManifest.xml");
+            URL resDir = new URL(appRoot, "tests/robotests/res");
+            URL assetsDir = new URL(appRoot, config.assetDir());
+
+            // By adding any resources from libraries we need to the AndroidManifest, we can access
+            // them from within the parallel universe's resource loader.
+            return new AndroidManifest(Fs.fromURL(manifestPath), Fs.fromURL(resDir),
+                    Fs.fromURL(assetsDir)) {
+                @Override
+                public List<ResourcePath> getIncludedResourcePaths() {
+                    List<ResourcePath> paths = super.getIncludedResourcePaths();
+                    paths.add(createResourcePath("file:packages/apps/Car/libs/car-ui-lib/res"));
+
+                    // Support library resources. These need to point to the prebuilts of support
+                    // library and not the source.
+                    paths.add(createResourcePath(createSupportResourcePathFromJar("appcompat")));
+                    paths.add(createResourcePath(
+                            createSupportResourcePathFromJar("constraintlayout")));
+                    paths.add(createResourcePath(createSupportResourcePathFromJar("preference")));
+
+                    return paths;
+                }
+            };
+        } catch (MalformedURLException e) {
+            throw new RuntimeException("CarUiRobolectricTestRunner failure", e);
+        }
+    }
+}
diff --git a/car-ui-lib/tests/robotests/src/com/android/car/ui/TestConfig.java b/car-ui-lib/tests/robotests/src/com/android/car/ui/TestConfig.java
new file mode 100644
index 0000000..46a9d0c
--- /dev/null
+++ b/car-ui-lib/tests/robotests/src/com/android/car/ui/TestConfig.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 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;
+
+public class TestConfig {
+    public static final int SDK_VERSION = 28;
+    public static final String MANIFEST_PATH =
+            "packages/apps/Car/car-ui-lib/AndroidManifest.xml";
+}
diff --git a/car-ui-lib/tests/robotests/src/com/android/car/ui/pagedrecyclerview/DefaultScrollBarTest.java b/car-ui-lib/tests/robotests/src/com/android/car/ui/pagedrecyclerview/DefaultScrollBarTest.java
new file mode 100644
index 0000000..dded5c6
--- /dev/null
+++ b/car-ui-lib/tests/robotests/src/com/android/car/ui/pagedrecyclerview/DefaultScrollBarTest.java
@@ -0,0 +1,131 @@
+/*
+ * 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.pagedrecyclerview;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertThrows;
+
+import android.content.Context;
+import android.widget.FrameLayout;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.car.ui.CarUiRobolectricTestRunner;
+import com.android.car.ui.TestConfig;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+@RunWith(CarUiRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public class DefaultScrollBarTest {
+
+    private Context mContext;
+    private ScrollBar mScrollBar;
+
+    @Mock
+    private RecyclerView mRecyclerView;
+    @Mock
+    private FrameLayout mParent;
+    @Mock
+    private FrameLayout.LayoutParams mLayoutParams;
+    @Mock
+    private RecyclerView.RecycledViewPool mRecycledViewPool;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mContext = RuntimeEnvironment.application;
+
+        mScrollBar = new DefaultScrollBar();
+    }
+
+    @Test
+    public void initialize_shouldInitializeScrollListener() {
+        when(mRecyclerView.getContext()).thenReturn(mContext);
+        when(mRecyclerView.getParent()).thenReturn(mParent);
+        when(mRecyclerView.getRecycledViewPool()).thenReturn(mRecycledViewPool);
+        when(mParent.generateLayoutParams(any())).thenReturn(mLayoutParams);
+
+        mScrollBar.initialize(mRecyclerView, 10, PagedRecyclerView.ScrollBarPosition.START, true);
+
+        // called once in DefaultScrollBar and once in SnapHelper while setting up the call backs
+        // when we use attachToRecyclerView(recyclerview)
+        verify(mRecyclerView, times(2)).addOnScrollListener(
+                any(RecyclerView.OnScrollListener.class));
+    }
+
+    @Test
+    public void initialize_shouldSetMaxRecyclerViews() {
+        when(mRecyclerView.getContext()).thenReturn(mContext);
+        when(mRecyclerView.getParent()).thenReturn(mParent);
+        when(mRecyclerView.getRecycledViewPool()).thenReturn(mRecycledViewPool);
+        when(mParent.generateLayoutParams(any())).thenReturn(mLayoutParams);
+
+        mScrollBar.initialize(mRecyclerView, 10, PagedRecyclerView.ScrollBarPosition.START, true);
+
+        verify(mRecycledViewPool).setMaxRecycledViews(0, 12);
+    }
+
+    @Test
+    public void initialize_shouldNotHaveFlingListener() {
+        when(mRecyclerView.getContext()).thenReturn(mContext);
+        when(mRecyclerView.getParent()).thenReturn(mParent);
+        when(mRecyclerView.getRecycledViewPool()).thenReturn(mRecycledViewPool);
+        when(mParent.generateLayoutParams(any())).thenReturn(mLayoutParams);
+
+        mScrollBar.initialize(mRecyclerView, 10, PagedRecyclerView.ScrollBarPosition.START, true);
+
+        verify(mRecyclerView).setOnFlingListener(null);
+    }
+
+    @Test
+    public void setPadding_shouldSetStartAndEndPadding() {
+        when(mRecyclerView.getContext()).thenReturn(mContext);
+        when(mRecyclerView.getParent()).thenReturn(mParent);
+        when(mRecyclerView.getRecycledViewPool()).thenReturn(mRecycledViewPool);
+        when(mParent.generateLayoutParams(any())).thenReturn(mLayoutParams);
+
+        mScrollBar.initialize(mRecyclerView, 10, PagedRecyclerView.ScrollBarPosition.START, true);
+        mScrollBar.setPadding(10, 20);
+
+        DefaultScrollBar defaultScrollBar = (DefaultScrollBar) mScrollBar;
+
+        assertThat(defaultScrollBar.mPaddingStart).isEqualTo(10);
+        assertThat(defaultScrollBar.mPaddingEnd).isEqualTo(20);
+    }
+
+    @Test
+    public void setPadding_shouldThrowErrorWithoutInitialization() {
+        assertThrows(NullPointerException.class, () -> mScrollBar.setPadding(10, 20));
+    }
+
+    @Test
+    public void requestLayout_shouldThrowErrorWithoutInitialization() {
+        assertThrows(NullPointerException.class, () -> mScrollBar.requestLayout());
+    }
+}
diff --git a/car-ui-lib/tests/robotests/src/com/android/car/ui/pagedrecyclerview/PagedRecyclerViewAdapterTest.java b/car-ui-lib/tests/robotests/src/com/android/car/ui/pagedrecyclerview/PagedRecyclerViewAdapterTest.java
new file mode 100644
index 0000000..93e90df
--- /dev/null
+++ b/car-ui-lib/tests/robotests/src/com/android/car/ui/pagedrecyclerview/PagedRecyclerViewAdapterTest.java
@@ -0,0 +1,74 @@
+/*
+ * 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.pagedrecyclerview;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.view.ViewGroup;
+
+import com.android.car.ui.CarUiRobolectricTestRunner;
+import com.android.car.ui.TestConfig;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+@RunWith(CarUiRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public class PagedRecyclerViewAdapterTest {
+
+    private Context mContext;
+    private PagedRecyclerViewAdapter mPagedRecyclerViewAdapter;
+
+    @Mock
+    private ViewGroup mParent;
+    @Mock
+    private ViewGroup.LayoutParams mLayoutParams;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        mContext = RuntimeEnvironment.application;
+        mPagedRecyclerViewAdapter = new PagedRecyclerViewAdapter();
+    }
+
+    @Test
+    public void getItemCount_shouldAlwaysBeOne() {
+        assertThat(mPagedRecyclerViewAdapter.getItemCount()).isEqualTo(1);
+    }
+
+    @Test
+    public void onCreateViewHolder_frameLayoutNotNull() {
+
+        when(mParent.getContext()).thenReturn(mContext);
+        when(mParent.generateLayoutParams(any())).thenReturn(mLayoutParams);
+
+        PagedRecyclerViewAdapter.NestedRowViewHolder nestedRowViewHolder =
+                mPagedRecyclerViewAdapter.onCreateViewHolder(mParent, 0);
+
+        assertThat(nestedRowViewHolder.frameLayout).isNotNull();
+    }
+}
diff --git a/car-ui-lib/tests/robotests/src/com/android/car/ui/pagedrecyclerview/PagedRecyclerViewTest.java b/car-ui-lib/tests/robotests/src/com/android/car/ui/pagedrecyclerview/PagedRecyclerViewTest.java
new file mode 100644
index 0000000..b4243b6
--- /dev/null
+++ b/car-ui-lib/tests/robotests/src/com/android/car/ui/pagedrecyclerview/PagedRecyclerViewTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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.pagedrecyclerview;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.car.ui.CarUiRobolectricTestRunner;
+import com.android.car.ui.R;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(CarUiRobolectricTestRunner.class)
+public class PagedRecyclerViewTest {
+
+    private Context mContext;
+    private View mView;
+    private PagedRecyclerView mPagedRecyclerView;
+
+    @Mock
+    private RecyclerView.Adapter mAdapter;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        mContext = RuntimeEnvironment.application;
+    }
+
+    @Test
+    public void onHeightChanged_shouldAddTheValueToInitialTopValue() {
+        mView = LayoutInflater.from(mContext)
+                .inflate(R.layout.test_linear_paged_recycler_view, null);
+
+        mPagedRecyclerView = mView.findViewById(R.id.test_prv);
+
+        assertThat(mPagedRecyclerView.getPaddingBottom()).isEqualTo(0);
+        assertThat(mPagedRecyclerView.getPaddingTop()).isEqualTo(0);
+        assertThat(mPagedRecyclerView.getPaddingStart()).isEqualTo(0);
+        assertThat(mPagedRecyclerView.getPaddingEnd()).isEqualTo(0);
+
+        mPagedRecyclerView.onHeightChanged(10);
+
+        assertThat(mPagedRecyclerView.getPaddingTop()).isEqualTo(10);
+        assertThat(mPagedRecyclerView.getPaddingBottom()).isEqualTo(0);
+        assertThat(mPagedRecyclerView.getPaddingStart()).isEqualTo(0);
+        assertThat(mPagedRecyclerView.getPaddingEnd()).isEqualTo(0);
+    }
+
+    @Test
+    public void setAdapter_shouldInitializeLinearLayoutManager() {
+        mView = LayoutInflater.from(mContext)
+                .inflate(R.layout.test_linear_paged_recycler_view, null);
+
+        mPagedRecyclerView = mView.findViewById(R.id.test_prv);
+        mPagedRecyclerView.setAdapter(mAdapter);
+
+        assertThat(mPagedRecyclerView.getLayoutManager()).isInstanceOf(LinearLayoutManager.class);
+    }
+
+    @Test
+    public void setAdapter_shouldInitializeGridLayoutManager() {
+        mView = LayoutInflater.from(mContext)
+                .inflate(R.layout.test_grid_paged_recycler_view, null);
+
+        mPagedRecyclerView = mView.findViewById(R.id.test_prv);
+        mPagedRecyclerView.setAdapter(mAdapter);
+
+        assertThat(mPagedRecyclerView.getLayoutManager()).isInstanceOf(GridLayoutManager.class);
+    }
+
+    @Test
+    public void init_shouldContainNestedRecyclerView() {
+        mView = LayoutInflater.from(mContext)
+                .inflate(R.layout.test_grid_paged_recycler_view, null);
+
+        mPagedRecyclerView = mView.findViewById(R.id.test_prv);
+
+        assertThat(mPagedRecyclerView.mNestedRecyclerView).isNotNull();
+    }
+}
diff --git a/car-ui-lib/tests/robotests/src/com/android/car/ui/pagedrecyclerview/PagedSmoothScrollerTest.java b/car-ui-lib/tests/robotests/src/com/android/car/ui/pagedrecyclerview/PagedSmoothScrollerTest.java
new file mode 100644
index 0000000..126883e
--- /dev/null
+++ b/car-ui-lib/tests/robotests/src/com/android/car/ui/pagedrecyclerview/PagedSmoothScrollerTest.java
@@ -0,0 +1,68 @@
+/*
+ * 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.pagedrecyclerview;
+
+import static androidx.recyclerview.widget.LinearSmoothScroller.SNAP_TO_START;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+
+import com.android.car.ui.CarUiRobolectricTestRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(CarUiRobolectricTestRunner.class)
+public class PagedSmoothScrollerTest {
+
+    private Context mContext;
+    private PagedSmoothScroller mPagedSmoothScroller;
+
+    @Before
+    public void setUp() {
+        mContext = RuntimeEnvironment.application;
+        mPagedSmoothScroller = new PagedSmoothScroller(mContext);
+    }
+
+    @Test
+    public void calculateTimeForScrolling_shouldInitializeAllValues() {
+        assertThat(mPagedSmoothScroller.mMillisecondsPerInch).isNotEqualTo(0);
+        assertThat(mPagedSmoothScroller.mDecelerationTimeDivisor).isNotEqualTo(0);
+        assertThat(mPagedSmoothScroller.mMillisecondsPerPixel).isNotEqualTo(0);
+        assertThat(mPagedSmoothScroller.mInterpolator).isNotNull();
+        assertThat(mPagedSmoothScroller.mDensityDpi).isNotEqualTo(0);
+    }
+
+    @Test
+    public void getVerticalSnapPreference_shouldReturnSnapToStart() {
+        assertThat(mPagedSmoothScroller.getVerticalSnapPreference()).isEqualTo(SNAP_TO_START);
+    }
+
+    @Test
+    public void calculateTimeForScrolling_shouldReturnMultiplierOfMillisecondsPerPixel() {
+        assertThat(mPagedSmoothScroller.calculateTimeForScrolling(20)).isEqualTo(
+                (int) Math.ceil(Math.abs(20) * mPagedSmoothScroller.mMillisecondsPerPixel));
+    }
+
+    @Test
+    public void calculateTimeForDeceleration_shouldReturnNotBeZero() {
+        assertThat(mPagedSmoothScroller.calculateTimeForDeceleration(20)).isNotEqualTo(0);
+    }
+}
diff --git a/car-ui-lib/tests/robotests/src/com/android/car/ui/pagedrecyclerview/PagedSnapHelperTest.java b/car-ui-lib/tests/robotests/src/com/android/car/ui/pagedrecyclerview/PagedSnapHelperTest.java
new file mode 100644
index 0000000..30d68b2
--- /dev/null
+++ b/car-ui-lib/tests/robotests/src/com/android/car/ui/pagedrecyclerview/PagedSnapHelperTest.java
@@ -0,0 +1,186 @@
+/*
+ * 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.pagedrecyclerview;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.graphics.PointF;
+import android.view.View;
+
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.car.ui.CarUiRobolectricTestRunner;
+import com.android.car.ui.TestConfig;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+@RunWith(CarUiRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public class PagedSnapHelperTest {
+
+    private Context mContext;
+    private PagedSnapHelper mPagedSnapHelper;
+
+    @Mock
+    private RecyclerView mRecyclerView;
+    @Mock
+    private LinearLayoutManager mLayoutManager;
+    @Mock
+    private RecyclerView.Adapter mAdapter;
+    @Mock
+    private View mChild;
+    @Mock
+    private RecyclerView.LayoutParams mLayoutParams;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mContext = RuntimeEnvironment.application;
+
+        mPagedSnapHelper = new PagedSnapHelper(mContext);
+
+        when(mRecyclerView.getContext()).thenReturn(mContext);
+        mPagedSnapHelper.attachToRecyclerView(mRecyclerView);
+    }
+
+    @Test
+    public void smoothScrollBy_invalidSnapPosition_shouldCallRecylerViewSmoothScrollBy() {
+        when(mRecyclerView.getLayoutManager()).thenReturn(mLayoutManager);
+
+        mPagedSnapHelper.smoothScrollBy(10);
+
+        verify(mRecyclerView).smoothScrollBy(0, 10);
+    }
+
+    @Test
+    public void smoothScrollBy_invalidSnapPositionNoItem_shouldCallRecylerViewSmoothScrollBy() {
+        when(mRecyclerView.getLayoutManager()).thenReturn(mLayoutManager);
+        when(mLayoutManager.getItemCount()).thenReturn(0);
+
+        mPagedSnapHelper.smoothScrollBy(10);
+
+        verify(mRecyclerView).smoothScrollBy(0, 10);
+    }
+
+    @Test
+    public void smoothScrollBy_invalidSnapPositionNoView_shouldCallRecylerViewSmoothScrollBy() {
+        when(mRecyclerView.getLayoutManager()).thenReturn(mLayoutManager);
+        when(mLayoutManager.getItemCount()).thenReturn(10);
+        when(mLayoutManager.canScrollVertically()).thenReturn(false);
+        when(mLayoutManager.canScrollHorizontally()).thenReturn(false);
+
+        mPagedSnapHelper.smoothScrollBy(10);
+
+        verify(mRecyclerView).smoothScrollBy(0, 10);
+    }
+
+    @Test
+    public void smoothScrollBy_invalidSnapPositionNoVectore_shouldCallRecylerViewSmoothScrollBy() {
+        when(mRecyclerView.getLayoutManager()).thenReturn(mLayoutManager);
+        when(mLayoutManager.getItemCount()).thenReturn(10);
+        when(mLayoutManager.canScrollVertically()).thenReturn(true);
+        when(mLayoutManager.getChildCount()).thenReturn(1);
+        when(mChild.getLayoutParams()).thenReturn(mLayoutParams);
+        when(mLayoutManager.getChildAt(0)).thenReturn(mChild);
+
+        mPagedSnapHelper.smoothScrollBy(10);
+
+        verify(mRecyclerView).smoothScrollBy(0, 10);
+    }
+
+    @Test
+    public void smoothScrollBy_invalidSnapPositionNoDelta_shouldCallRecylerViewSmoothScrollBy() {
+        when(mRecyclerView.getLayoutManager()).thenReturn(mLayoutManager);
+        when(mLayoutManager.getItemCount()).thenReturn(1);
+        when(mLayoutManager.canScrollVertically()).thenReturn(true);
+        when(mLayoutManager.getChildCount()).thenReturn(1);
+        // no delta
+        when(mLayoutManager.getDecoratedBottom(any())).thenReturn(0);
+        when(mChild.getLayoutParams()).thenReturn(mLayoutParams);
+        when(mLayoutManager.getChildAt(0)).thenReturn(mChild);
+
+        PointF vectorForEnd = new PointF(100, 100);
+        when(mLayoutManager.computeScrollVectorForPosition(0)).thenReturn(vectorForEnd);
+
+        mPagedSnapHelper.smoothScrollBy(10);
+
+        verify(mRecyclerView).smoothScrollBy(0, 10);
+    }
+
+    @Test
+    public void smoothScrollBy_validSnapPosition_shouldCallRecylerViewSmoothScrollBy() {
+        when(mRecyclerView.getLayoutManager()).thenReturn(mLayoutManager);
+        when(mLayoutManager.getItemCount()).thenReturn(1);
+        when(mLayoutManager.canScrollVertically()).thenReturn(true);
+        when(mLayoutManager.getChildCount()).thenReturn(1);
+        // some delta
+        when(mLayoutManager.getDecoratedBottom(any())).thenReturn(10);
+        when(mChild.getLayoutParams()).thenReturn(mLayoutParams);
+        when(mLayoutManager.getChildAt(0)).thenReturn(mChild);
+
+        PointF vectorForEnd = new PointF(100, 100);
+        when(mLayoutManager.computeScrollVectorForPosition(0)).thenReturn(vectorForEnd);
+
+        mPagedSnapHelper.smoothScrollBy(10);
+
+        verify(mLayoutManager).startSmoothScroll(any(RecyclerView.SmoothScroller.class));
+    }
+
+    @Test
+    public void calculateDistanceToFinalSnap_shouldReturnTopMarginDifference() {
+        when(mRecyclerView.getLayoutManager()).thenReturn(mLayoutManager);
+        when(mLayoutManager.getItemCount()).thenReturn(1);
+        when(mLayoutManager.canScrollVertically()).thenReturn(true);
+        when(mLayoutManager.getChildCount()).thenReturn(1);
+        // some delta
+        when(mLayoutManager.getDecoratedTop(any())).thenReturn(10);
+        when(mChild.getLayoutParams()).thenReturn(mLayoutParams);
+
+        int[] distance = mPagedSnapHelper.calculateDistanceToFinalSnap(mLayoutManager, mChild);
+
+        assertThat(distance[1]).isEqualTo(10);
+    }
+
+    @Test
+    public void calculateScrollDistance_shouldScrollHeightOfView() {
+        when(mRecyclerView.getLayoutManager()).thenReturn(mLayoutManager);
+        when(mLayoutManager.getItemCount()).thenReturn(1);
+        when(mLayoutManager.canScrollVertically()).thenReturn(true);
+        when(mLayoutManager.getChildCount()).thenReturn(1);
+        // some delta
+        when(mLayoutManager.getDecoratedTop(any())).thenReturn(10);
+        when(mChild.getLayoutParams()).thenReturn(mLayoutParams);
+        when(mLayoutManager.getChildAt(0)).thenReturn(mChild);
+        when(mLayoutManager.getHeight()).thenReturn(-50);
+
+        int[] distance = mPagedSnapHelper.calculateScrollDistance(0, 10);
+
+        assertThat(distance[1]).isEqualTo(50);
+    }
+}
diff --git a/car-ui-lib/tests/robotests/src/com/android/car/ui/utils/CarUxRestrictionsUtilTest.java b/car-ui-lib/tests/robotests/src/com/android/car/ui/utils/CarUxRestrictionsUtilTest.java
new file mode 100644
index 0000000..fe6f80c
--- /dev/null
+++ b/car-ui-lib/tests/robotests/src/com/android/car/ui/utils/CarUxRestrictionsUtilTest.java
@@ -0,0 +1,85 @@
+/*
+ * 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.utils;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.car.drivingstate.CarUxRestrictions;
+
+import com.android.car.ui.TestConfig;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public class CarUxRestrictionsUtilTest {
+    private int[] mRestrictionsArray;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mRestrictionsArray = new int[]{
+                CarUxRestrictions.UX_RESTRICTIONS_NO_DIALPAD,
+                CarUxRestrictions.UX_RESTRICTIONS_NO_KEYBOARD,
+                CarUxRestrictions.UX_RESTRICTIONS_NO_DIALPAD
+                        | CarUxRestrictions.UX_RESTRICTIONS_NO_KEYBOARD,
+                CarUxRestrictions.UX_RESTRICTIONS_FULLY_RESTRICTED
+        };
+    }
+
+    @Test
+    public void testNullActiveRestriction() {
+        CarUxRestrictions activeRestrictions = null;
+        boolean[] expectedResults = {true, true, true, true};
+        for (int i = 0; i < mRestrictionsArray.length; i++) {
+            boolean actualResult = CarUxRestrictionsUtil.isRestricted(mRestrictionsArray[i],
+                    activeRestrictions);
+            assertThat(actualResult == expectedResults[i]).isTrue();
+        }
+    }
+
+    @Test
+    public void testOneActiveRestriction() {
+        CarUxRestrictions activeRestrictions = new CarUxRestrictions.Builder(/* reqOpt= */true,
+                CarUxRestrictions.UX_RESTRICTIONS_NO_DIALPAD, /* timestamp= */0).build();
+        boolean[] expectedResults = {true, false, true, true};
+        for (int i = 0; i < mRestrictionsArray.length; i++) {
+            boolean actualResult = CarUxRestrictionsUtil.isRestricted(mRestrictionsArray[i],
+                    activeRestrictions);
+            assertThat(actualResult == expectedResults[i]).isTrue();
+        }
+    }
+
+    @Test
+    public void testMultipleActiveRestrictions() {
+        CarUxRestrictions activeRestrictions = new CarUxRestrictions.Builder(/* reqOpt= */true,
+                CarUxRestrictions.UX_RESTRICTIONS_NO_DIALPAD
+                        | CarUxRestrictions.UX_RESTRICTIONS_NO_TEXT_MESSAGE, /* timestamp= */
+                0).build();
+        boolean[] expectedResults = {true, false, true, true};
+        for (int i = 0; i < mRestrictionsArray.length; i++) {
+            boolean actualResult = CarUxRestrictionsUtil.isRestricted(mRestrictionsArray[i],
+                    activeRestrictions);
+            assertThat(actualResult == expectedResults[i]).isTrue();
+        }
+    }
+}