Merge "Revert "Support buildIds in reportModifications task"" into oc-mr1-support-27.0-dev
am: 8ec204c195

Change-Id: I31f310a95e80ebdf2e6adc757e64886aae3d9793
diff --git a/annotations/src/main/java/android/support/annotation/AnyThread.java b/annotations/src/main/java/android/support/annotation/AnyThread.java
index 4c379d3..b006922 100644
--- a/annotations/src/main/java/android/support/annotation/AnyThread.java
+++ b/annotations/src/main/java/android/support/annotation/AnyThread.java
@@ -17,6 +17,7 @@
 
 import static java.lang.annotation.ElementType.CONSTRUCTOR;
 import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PARAMETER;
 import static java.lang.annotation.ElementType.TYPE;
 import static java.lang.annotation.RetentionPolicy.CLASS;
 
@@ -41,6 +42,6 @@
  */
 @Documented
 @Retention(CLASS)
-@Target({METHOD,CONSTRUCTOR,TYPE})
+@Target({METHOD,CONSTRUCTOR,TYPE,PARAMETER})
 public @interface AnyThread {
 }
diff --git a/annotations/src/main/java/android/support/annotation/BinderThread.java b/annotations/src/main/java/android/support/annotation/BinderThread.java
index 0b821d5..5d9a3c2 100644
--- a/annotations/src/main/java/android/support/annotation/BinderThread.java
+++ b/annotations/src/main/java/android/support/annotation/BinderThread.java
@@ -17,6 +17,7 @@
 
 import static java.lang.annotation.ElementType.CONSTRUCTOR;
 import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PARAMETER;
 import static java.lang.annotation.ElementType.TYPE;
 import static java.lang.annotation.RetentionPolicy.CLASS;
 
@@ -37,6 +38,6 @@
  */
 @Documented
 @Retention(CLASS)
-@Target({METHOD,CONSTRUCTOR,TYPE})
+@Target({METHOD,CONSTRUCTOR,TYPE,PARAMETER})
 public @interface BinderThread {
 }
\ No newline at end of file
diff --git a/annotations/src/main/java/android/support/annotation/MainThread.java b/annotations/src/main/java/android/support/annotation/MainThread.java
index 2f50306..78541d5 100644
--- a/annotations/src/main/java/android/support/annotation/MainThread.java
+++ b/annotations/src/main/java/android/support/annotation/MainThread.java
@@ -17,6 +17,7 @@
 
 import static java.lang.annotation.ElementType.CONSTRUCTOR;
 import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PARAMETER;
 import static java.lang.annotation.ElementType.TYPE;
 import static java.lang.annotation.RetentionPolicy.CLASS;
 
@@ -45,6 +46,6 @@
  */
 @Documented
 @Retention(CLASS)
-@Target({METHOD,CONSTRUCTOR,TYPE})
+@Target({METHOD,CONSTRUCTOR,TYPE,PARAMETER})
 public @interface MainThread {
 }
diff --git a/annotations/src/main/java/android/support/annotation/UiThread.java b/annotations/src/main/java/android/support/annotation/UiThread.java
index 0a9a0c1..1d7aeca 100644
--- a/annotations/src/main/java/android/support/annotation/UiThread.java
+++ b/annotations/src/main/java/android/support/annotation/UiThread.java
@@ -17,6 +17,7 @@
 
 import static java.lang.annotation.ElementType.CONSTRUCTOR;
 import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PARAMETER;
 import static java.lang.annotation.ElementType.TYPE;
 import static java.lang.annotation.RetentionPolicy.CLASS;
 
@@ -46,6 +47,6 @@
  */
 @Documented
 @Retention(CLASS)
-@Target({METHOD,CONSTRUCTOR,TYPE})
+@Target({METHOD,CONSTRUCTOR,TYPE,PARAMETER})
 public @interface UiThread {
 }
diff --git a/annotations/src/main/java/android/support/annotation/WorkerThread.java b/annotations/src/main/java/android/support/annotation/WorkerThread.java
index 237aa66..8b08b14 100644
--- a/annotations/src/main/java/android/support/annotation/WorkerThread.java
+++ b/annotations/src/main/java/android/support/annotation/WorkerThread.java
@@ -17,6 +17,7 @@
 
 import static java.lang.annotation.ElementType.CONSTRUCTOR;
 import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PARAMETER;
 import static java.lang.annotation.ElementType.TYPE;
 import static java.lang.annotation.RetentionPolicy.CLASS;
 
@@ -37,6 +38,6 @@
  */
 @Documented
 @Retention(CLASS)
-@Target({METHOD,CONSTRUCTOR,TYPE})
+@Target({METHOD,CONSTRUCTOR,TYPE,PARAMETER})
 public @interface WorkerThread {
 }
\ No newline at end of file
diff --git a/buildSrc/src/main/java/android/support/LibraryVersions.java b/buildSrc/src/main/java/android/support/LibraryVersions.java
index d328b7e..48a75d3 100644
--- a/buildSrc/src/main/java/android/support/LibraryVersions.java
+++ b/buildSrc/src/main/java/android/support/LibraryVersions.java
@@ -23,7 +23,7 @@
     /**
      * Version code of the support library components.
      */
-    public static final Version SUPPORT_LIBRARY = new Version("27.0.0");
+    public static final Version SUPPORT_LIBRARY = new Version("27.1.0-SNAPSHOT");
 
     /**
      * Version code for flatfoot 1.0 projects (room, lifecycles)
diff --git a/car/Android.mk b/car/Android.mk
new file mode 100644
index 0000000..fa20f26
--- /dev/null
+++ b/car/Android.mk
@@ -0,0 +1,39 @@
+# Copyright (C) 2017 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)
+
+# Here is the final static library that apps can link against.
+# Applications that use this library must specify
+#
+#   LOCAL_STATIC_ANDROID_LIBRARIES := \
+#       android-support-car
+#
+# in their makefiles to include the resources and their dependencies in their package.
+include $(CLEAR_VARS)
+LOCAL_USE_AAPT2 := true
+LOCAL_MODULE := android-support-car
+LOCAL_SDK_VERSION := $(SUPPORT_CURRENT_SDK_VERSION)
+LOCAL_SRC_FILES := $(call all-java-files-under,src/main/java)
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+LOCAL_SHARED_ANDROID_LIBRARIES := \
+        android-support-annotations \
+        android-support-v4 \
+        android-support-v7-appcompat \
+        android-support-v7-cardview \
+        android-support-v7-recyclerview
+LOCAL_JAR_EXCLUDE_FILES := none
+LOCAL_JAVA_LANGUAGE_VERSION := 1.8
+LOCAL_AAPT_FLAGS := --add-javadoc-annotation doconly
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/car/AndroidManifest.xml b/car/AndroidManifest.xml
new file mode 100644
index 0000000..4e6d80f
--- /dev/null
+++ b/car/AndroidManifest.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.support.car">
+</manifest>
diff --git a/car/README.txt b/car/README.txt
new file mode 100644
index 0000000..50a019b
--- /dev/null
+++ b/car/README.txt
@@ -0,0 +1 @@
+Library Project including Car Support UI Components and associated utilities.
diff --git a/car/build.gradle b/car/build.gradle
new file mode 100644
index 0000000..5a55b56
--- /dev/null
+++ b/car/build.gradle
@@ -0,0 +1,32 @@
+apply plugin: android.support.SupportAndroidLibraryPlugin
+
+dependencies {
+    api project(':appcompat-v7')
+    api project(':cardview-v7')
+    api project(':support-annotations')
+    api project(':support-v4')
+    api project(':recyclerview-v7')
+
+    androidTestImplementation libs.test_runner,      { exclude module: 'support-annotations' }
+    androidTestImplementation libs.espresso_core,    { exclude module: 'support-annotations' }
+    androidTestImplementation libs.mockito_core,     { exclude group: 'net.bytebuddy' } // DexMaker has it"s own MockMaker
+    androidTestImplementation libs.dexmaker_mockito, { exclude group: 'net.bytebuddy' } // DexMaker has it"s own MockMaker
+}
+
+android {
+    defaultConfig {
+        minSdkVersion 24
+    }
+
+    sourceSets {
+        main.res.srcDirs 'res', 'res-public'
+    }
+}
+
+supportLibrary {
+    name 'Android Car Support UI'
+    publish false
+    inceptionYear '2017'
+    description 'Android Car Support UI'
+    java8Library true
+}
diff --git a/car/lint-baseline.xml b/car/lint-baseline.xml
new file mode 100644
index 0000000..8bc6f6f
--- /dev/null
+++ b/car/lint-baseline.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="4" by="lint 3.0.0-alpha9">
+
+</issues>
diff --git a/car/res-public/values/public_attrs.xml b/car/res-public/values/public_attrs.xml
new file mode 100644
index 0000000..5351366
--- /dev/null
+++ b/car/res-public/values/public_attrs.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+
+<!-- Definitions of attributes to be exposed as public. -->
+<resources>
+    <!-- ColumnCardView -->
+    <public type="attr" name="columnSpan" />
+</resources>
diff --git a/car/res/drawable-hdpi/ic_list_view_disable.png b/car/res/drawable-hdpi/ic_list_view_disable.png
new file mode 100644
index 0000000..e82a74f
--- /dev/null
+++ b/car/res/drawable-hdpi/ic_list_view_disable.png
Binary files differ
diff --git a/car/res/drawable-mdpi/ic_list_view_disable.png b/car/res/drawable-mdpi/ic_list_view_disable.png
new file mode 100644
index 0000000..9887c8e
--- /dev/null
+++ b/car/res/drawable-mdpi/ic_list_view_disable.png
Binary files differ
diff --git a/car/res/drawable-xhdpi/ic_list_view_disable.png b/car/res/drawable-xhdpi/ic_list_view_disable.png
new file mode 100644
index 0000000..32edc30
--- /dev/null
+++ b/car/res/drawable-xhdpi/ic_list_view_disable.png
Binary files differ
diff --git a/car/res/drawable-xxhdpi/ic_list_view_disable.png b/car/res/drawable-xxhdpi/ic_list_view_disable.png
new file mode 100644
index 0000000..1f61690
--- /dev/null
+++ b/car/res/drawable-xxhdpi/ic_list_view_disable.png
Binary files differ
diff --git a/car/res/drawable/car_drawer_list_item_background.xml b/car/res/drawable/car_drawer_list_item_background.xml
new file mode 100644
index 0000000..c5fc36b
--- /dev/null
+++ b/car/res/drawable/car_drawer_list_item_background.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 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.
+  -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="@color/car_card_ripple_background">
+    <item android:id="@android:id/mask">
+        <color android:color="#ffffffff" />
+    </item>
+</ripple>
diff --git a/car/res/drawable/car_pagination_background.xml b/car/res/drawable/car_pagination_background.xml
new file mode 100644
index 0000000..6d3ad3e
--- /dev/null
+++ b/car/res/drawable/car_pagination_background.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 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.
+  -->
+<ripple
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="@color/car_card_ripple_background" />
\ No newline at end of file
diff --git a/car/res/drawable/car_pagination_background_day.xml b/car/res/drawable/car_pagination_background_day.xml
new file mode 100644
index 0000000..a4370e9
--- /dev/null
+++ b/car/res/drawable/car_pagination_background_day.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 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.
+  -->
+<ripple
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="@color/car_card_ripple_background_dark" />
\ No newline at end of file
diff --git a/car/res/drawable/car_pagination_background_inverse.xml b/car/res/drawable/car_pagination_background_inverse.xml
new file mode 100644
index 0000000..3c07ecf
--- /dev/null
+++ b/car/res/drawable/car_pagination_background_inverse.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 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.
+  -->
+<ripple
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="@color/car_card_ripple_background_inverse" />
\ No newline at end of file
diff --git a/car/res/drawable/car_pagination_background_night.xml b/car/res/drawable/car_pagination_background_night.xml
new file mode 100644
index 0000000..c1b03c1
--- /dev/null
+++ b/car/res/drawable/car_pagination_background_night.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 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.
+  -->
+<ripple
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="@color/car_card_ripple_background_light" />
\ No newline at end of file
diff --git a/car/res/drawable/ic_down.xml b/car/res/drawable/ic_down.xml
new file mode 100644
index 0000000..c6bb32d
--- /dev/null
+++ b/car/res/drawable/ic_down.xml
@@ -0,0 +1,16 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="76dp"
+        android:height="76dp"
+        android:viewportWidth="76.0"
+        android:viewportHeight="76.0">
+    <path
+        android:pathData="M38,0.96C17.01,0.96 0,17.75 0,38.47C0,59.18 17.01,75.97 38,75.97C58.99,75.97 76,59.18 76,38.47C76,17.75 58.99,0.96 38,0.96M38,3.3C57.64,3.3 73.62,19.08 73.62,38.47C73.62,57.85 57.64,73.63 38,73.63C18.36,73.63 2.38,57.86 2.38,38.47C2.38,19.08 18.36,3.3 38,3.3"
+        android:strokeColor="#00000000"
+        android:fillColor="#212121"
+        android:strokeWidth="1"/>
+    <path
+        android:pathData="M26.63,31.09l11.37,11.08l11.37,-11.08l3.5,3.42l-14.87,14.5l-14.87,-14.5z"
+        android:strokeColor="#00000000"
+        android:fillColor="#212121"
+        android:strokeWidth="1"/>
+</vector>
diff --git a/car/res/drawable/ic_up.xml b/car/res/drawable/ic_up.xml
new file mode 100644
index 0000000..05f69b9
--- /dev/null
+++ b/car/res/drawable/ic_up.xml
@@ -0,0 +1,16 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="76dp"
+        android:height="76dp"
+        android:viewportWidth="76.0"
+        android:viewportHeight="76.0">
+    <path
+        android:pathData="M38,75.04C58.99,75.04 76,58.27 76,37.57C76,16.88 58.99,0.11 38,0.11C17.01,0.11 0,16.88 0,37.57C0,58.27 17.01,75.04 38,75.04M38,72.7C18.36,72.7 2.38,56.94 2.38,37.57C2.38,18.21 18.36,2.45 38,2.45C57.64,2.45 73.62,18.21 73.62,37.57C73.62,56.94 57.64,72.7 38,72.7"
+        android:strokeColor="#00000000"
+        android:fillColor="#212121"
+        android:strokeWidth="1"/>
+    <path
+        android:pathData="M49.37,44.9l-11.37,-11.08l-11.37,11.08l-3.5,-3.42l14.87,-14.5l14.87,14.5z"
+        android:strokeColor="#00000000"
+        android:fillColor="#212121"
+        android:strokeWidth="1"/>
+</vector>
diff --git a/car/res/layout/car_drawer.xml b/car/res/layout/car_drawer.xml
new file mode 100644
index 0000000..a3b5c4f
--- /dev/null
+++ b/car/res/layout/car_drawer.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/drawer_content"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:layout_marginEnd="@dimen/car_drawer_margin_end"
+    android:background="@color/car_card"
+    android:paddingTop="@dimen/app_header_height" >
+
+  <android.support.car.widget.PagedListView
+      android:id="@+id/drawer_list"
+      android:layout_width="match_parent"
+      android:layout_height="match_parent"
+      app:listEndMargin="@dimen/car_drawer_margin_end"
+      app:offsetScrollBar="true" />
+
+  <ProgressBar
+      android:id="@+id/drawer_progress"
+      android:layout_width="@dimen/car_drawer_progress_bar_size"
+      android:layout_height="@dimen/car_drawer_progress_bar_size"
+      android:layout_gravity="center"
+      android:indeterminate="true"
+      android:visibility="gone" />
+</FrameLayout>
diff --git a/car/res/layout/car_drawer_activity.xml b/car/res/layout/car_drawer_activity.xml
new file mode 100644
index 0000000..751ef0d
--- /dev/null
+++ b/car/res/layout/car_drawer_activity.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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">
+
+    <android.support.v4.widget.DrawerLayout
+        android:id="@+id/drawer_layout"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+      <!-- The main content view. Fragments will be added here. -->
+      <FrameLayout
+          android:id="@+id/content_frame"
+          android:layout_width="match_parent"
+          android:layout_height="match_parent" />
+
+      <include
+          android:layout_width="match_parent"
+          android:layout_height="match_parent"
+          android:layout_gravity="start"
+          layout="@layout/car_drawer" />
+    </android.support.v4.widget.DrawerLayout>
+
+    <include layout="@layout/car_toolbar" />
+</FrameLayout>
diff --git a/car/res/layout/car_drawer_list_item_empty.xml b/car/res/layout/car_drawer_list_item_empty.xml
new file mode 100644
index 0000000..c2e35ac
--- /dev/null
+++ b/car/res/layout/car_drawer_list_item_empty.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/container"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:layout_marginStart="16dp"
+    android:focusable="false"
+    android:orientation="vertical"
+    android:background="@drawable/car_drawer_list_item_background" >
+    <FrameLayout
+        android:id="@+id/icon_container"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:visibility="visible">
+        <ImageView
+            android:id="@+id/icon"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_horizontal"
+            android:layout_marginTop="48dp"
+            android:layout_marginBottom="22dp" />
+    </FrameLayout>
+    <TextView
+        android:id="@+id/title"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="16dp"
+        android:gravity="center"
+        style="@style/CarBody1" />
+</LinearLayout>
diff --git a/car/res/layout/car_drawer_list_item_normal.xml b/car/res/layout/car_drawer_list_item_normal.xml
new file mode 100644
index 0000000..fa3b29f
--- /dev/null
+++ b/car/res/layout/car_drawer_list_item_normal.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="@dimen/car_drawer_list_item_height"
+    android:focusable="true"
+    android:orientation="horizontal"
+    android:background="@drawable/car_drawer_list_item_background" >
+    <ImageView
+        android:id="@+id/icon"
+        android:layout_width="@dimen/car_drawer_list_item_icon_size"
+        android:layout_height="@dimen/car_drawer_list_item_icon_size"
+        android:layout_marginEnd="@dimen/car_drawer_list_item_icon_end_margin"
+        android:layout_gravity="center_vertical"
+        android:scaleType="centerCrop" />
+    <LinearLayout
+        android:id="@+id/text_container"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_weight="1"
+        android:layout_gravity="center_vertical"
+        android:orientation="vertical" >
+        <TextView
+            android:id="@+id/title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="@dimen/car_text_vertical_margin"
+            android:maxLines="1"
+            style="@style/CarBody1" />
+        <TextView
+            android:id="@+id/text"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:ellipsize="end"
+            android:maxLines="1"
+            style="@style/CarBody2" />
+    </LinearLayout>
+    <ImageView
+        android:id="@+id/end_icon"
+        android:layout_width="@dimen/car_drawer_list_item_end_icon_size"
+        android:layout_height="@dimen/car_drawer_list_item_end_icon_size"
+        android:scaleType="fitCenter"
+        android:layout_marginEnd="@dimen/car_drawer_list_item_end_margin"
+        android:layout_gravity="center_vertical" />
+</LinearLayout>
diff --git a/car/res/layout/car_drawer_list_item_small.xml b/car/res/layout/car_drawer_list_item_small.xml
new file mode 100644
index 0000000..d7e7a75
--- /dev/null
+++ b/car/res/layout/car_drawer_list_item_small.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="@dimen/car_drawer_list_item_height_small"
+    android:focusable="true"
+    android:orientation="horizontal"
+    android:background="@drawable/car_drawer_list_item_background" >
+    <ImageView
+        android:id="@+id/icon"
+        android:layout_width="@dimen/car_drawer_list_item_small_icon_size"
+        android:layout_height="@dimen/car_drawer_list_item_small_icon_size"
+        android:layout_marginEnd="@dimen/car_drawer_list_item_icon_end_margin"
+        android:layout_gravity="center_vertical"
+        android:scaleType="centerCrop" />
+    <TextView
+        android:id="@+id/title"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_weight="1"
+        android:layout_gravity="center_vertical"
+        android:layout_marginBottom="@dimen/car_text_vertical_margin"
+        android:maxLines="1"
+        style="@style/CarBody1" />
+    <ImageView
+        android:id="@+id/end_icon"
+        android:layout_width="@dimen/car_drawer_list_item_end_icon_size"
+        android:layout_height="@dimen/car_drawer_list_item_end_icon_size"
+        android:scaleType="fitCenter"
+        android:layout_marginEnd="@dimen/car_drawer_list_item_end_margin"
+        android:layout_gravity="center_vertical"/>
+</LinearLayout>
diff --git a/car/res/layout/car_paged_recycler_view.xml b/car/res/layout/car_paged_recycler_view.xml
new file mode 100644
index 0000000..1b8a6ac
--- /dev/null
+++ b/car/res/layout/car_paged_recycler_view.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 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">
+
+    <android.support.car.widget.CarRecyclerView
+        android:id="@+id/recycler_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+
+    <!-- Putting this as the last child so that it can intercept any touch events on the
+         scroll buttons. -->
+    <android.support.car.widget.PagedScrollBarView
+        android:id="@+id/paged_scroll_view"
+        android:layout_width="@dimen/car_screen_margin_size"
+        android:layout_height="match_parent"
+        android:paddingBottom="@dimen/car_scroll_bar_padding"
+        android:paddingTop="@dimen/car_scroll_bar_padding"
+        android:visibility="invisible" />
+</FrameLayout>
diff --git a/car/res/layout/car_paged_scrollbar_buttons.xml b/car/res/layout/car_paged_scrollbar_buttons.xml
new file mode 100644
index 0000000..7dd213a
--- /dev/null
+++ b/car/res/layout/car_paged_scrollbar_buttons.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 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.
+  -->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:layout_gravity="center"
+    android:gravity="center"
+    android:orientation="vertical">
+
+    <ImageView
+        android:id="@+id/page_up"
+        android:layout_width="@dimen/car_scroll_bar_button_size"
+        android:layout_height="@dimen/car_scroll_bar_button_size"
+        android:background="@drawable/car_pagination_background"
+        android:focusable="false"
+        android:hapticFeedbackEnabled="false"
+        android:scaleType="center"
+        android:src="@drawable/ic_up" />
+
+    <FrameLayout
+        android:id="@+id/filler"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1"
+        android:layout_marginBottom="@dimen/car_paged_list_view_scrollbar_thumb_margin"
+        android:layout_marginTop="@dimen/car_paged_list_view_scrollbar_thumb_margin" >
+
+        <ImageView
+            android:id="@+id/scrollbar_thumb"
+            android:layout_width="@dimen/car_scroll_bar_thumb_width"
+            android:layout_height="0dp"
+            android:layout_gravity="center_horizontal"
+            android:background="@color/car_scrollbar_thumb" />
+    </FrameLayout>
+
+    <ImageView
+        android:id="@+id/page_down"
+        android:layout_width="@dimen/car_scroll_bar_button_size"
+        android:layout_height="@dimen/car_scroll_bar_button_size"
+        android:background="@drawable/car_pagination_background"
+        android:focusable="false"
+        android:hapticFeedbackEnabled="false"
+        android:scaleType="center"
+        android:src="@drawable/ic_down" />
+</LinearLayout>
diff --git a/car/res/layout/car_toolbar.xml b/car/res/layout/car_toolbar.xml
new file mode 100644
index 0000000..fb7769a
--- /dev/null
+++ b/car/res/layout/car_toolbar.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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="@dimen/app_header_height">
+    <android.support.v7.widget.Toolbar
+        android:id="@+id/car_toolbar"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_vertical"
+        style="@style/CarToolbarTheme" />
+</FrameLayout>
diff --git a/car/res/values-h480dp/dimens.xml b/car/res/values-h480dp/dimens.xml
new file mode 100644
index 0000000..8dd16fc
--- /dev/null
+++ b/car/res/values-h480dp/dimens.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+<resources>
+    <dimen name="app_header_height">112dp</dimen>
+</resources>
diff --git a/car/res/values-h600dp/dimens.xml b/car/res/values-h600dp/dimens.xml
new file mode 100644
index 0000000..7577d17
--- /dev/null
+++ b/car/res/values-h600dp/dimens.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+<resources>
+    <dimen name="car_title_size">32sp</dimen>
+    <dimen name="car_body1_size">40sp</dimen>
+    <dimen name="car_body2_size">32sp</dimen>
+
+    <dimen name="app_header_height">148dp</dimen>
+
+    <dimen name="car_drawer_list_item_icon_size">108dp</dimen>
+    <dimen name="car_drawer_list_item_small_icon_size">56dp</dimen>
+    <dimen name="car_drawer_list_item_end_icon_size">56dp</dimen>
+
+    <dimen name="car_drawer_list_item_height">128dp</dimen>
+    <dimen name="car_drawer_list_item_height_small">128dp</dimen>
+</resources>
diff --git a/car/res/values-night/colors.xml b/car/res/values-night/colors.xml
new file mode 100644
index 0000000..2ca5b02
--- /dev/null
+++ b/car/res/values-night/colors.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 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.
+  -->
+<resources>
+    <color name="car_title">@color/car_title_light</color>
+    <color name="car_body1">@color/car_body1_light</color>
+    <color name="car_body2">@color/car_body2_light</color>
+
+    <color name="car_tint">@color/car_tint_light</color>
+    <color name="car_tint_inverse">@color/car_tint_dark</color>
+
+    <color name="car_card">@color/car_card_dark</color>
+    <color name="car_card_ripple_background">@color/car_card_ripple_background_light</color>
+    <color name="car_card_ripple_background_inverse">@color/car_card_ripple_background_dark</color>
+
+    <color name="car_list_divider">@color/car_list_divider_dark</color>
+    <color name="car_scrollbar_thumb">@color/car_scrollbar_thumb_light</color>
+    <color name="car_scrollbar_thumb_inverse">@color/car_scrollbar_thumb_dark</color>
+</resources>
diff --git a/car/res/values-w1024dp/dimens.xml b/car/res/values-w1024dp/dimens.xml
new file mode 100644
index 0000000..b1ae5ba
--- /dev/null
+++ b/car/res/values-w1024dp/dimens.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+<resources>
+    <dimen name="car_screen_margin_size">112dp</dimen>
+</resources>
diff --git a/car/res/values-w1280dp/dimens.xml b/car/res/values-w1280dp/dimens.xml
new file mode 100644
index 0000000..1799010
--- /dev/null
+++ b/car/res/values-w1280dp/dimens.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+<resources>
+    <dimen name="car_screen_margin_size">148dp</dimen>
+    <dimen name="car_scroll_bar_button_size">76dp</dimen>
+</resources>
diff --git a/car/res/values-w1920dp/dimens.xml b/car/res/values-w1920dp/dimens.xml
new file mode 100644
index 0000000..decab1a
--- /dev/null
+++ b/car/res/values-w1920dp/dimens.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+<resources>
+    <dimen name="car_keyline_1">48dp</dimen>
+</resources>
diff --git a/car/res/values-w480dp/dimens.xml b/car/res/values-w480dp/dimens.xml
new file mode 100644
index 0000000..4077e0d
--- /dev/null
+++ b/car/res/values-w480dp/dimens.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+<resources>
+    <dimen name="car_screen_margin_size">24dp</dimen>
+</resources>
diff --git a/car/res/values-w600dp/integers.xml b/car/res/values-w600dp/integers.xml
new file mode 100644
index 0000000..5dcd8df
--- /dev/null
+++ b/car/res/values-w600dp/integers.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+<resources>
+    <integer name="car_screen_num_of_columns">8</integer>
+    <integer name="column_card_default_column_span">6</integer>
+</resources>
diff --git a/car/res/values-w720dp/dimens.xml b/car/res/values-w720dp/dimens.xml
new file mode 100644
index 0000000..b1ae5ba
--- /dev/null
+++ b/car/res/values-w720dp/dimens.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+<resources>
+    <dimen name="car_screen_margin_size">112dp</dimen>
+</resources>
diff --git a/car/res/values-w840dp/dimens.xml b/car/res/values-w840dp/dimens.xml
new file mode 100644
index 0000000..9d6167e
--- /dev/null
+++ b/car/res/values-w840dp/dimens.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+<resources>
+    <dimen name="car_keyline_1">32dp</dimen>
+    <dimen name="car_screen_gutter_size">24dp</dimen>
+</resources>
diff --git a/car/res/values-w840dp/integers.xml b/car/res/values-w840dp/integers.xml
new file mode 100644
index 0000000..38c0440
--- /dev/null
+++ b/car/res/values-w840dp/integers.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+<resources>
+    <integer name="car_screen_num_of_columns">12</integer>
+    <integer name="column_card_default_column_span">8</integer>
+</resources>
diff --git a/car/res/values/attrs.xml b/car/res/values/attrs.xml
new file mode 100644
index 0000000..17a1960
--- /dev/null
+++ b/car/res/values/attrs.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+<resources>
+    <!-- The configurable attributes for a ColumnCardView. -->
+    <declare-styleable name="ColumnCardView">
+        <!-- The number of columns that this ColumnCardView should span across. This value will
+             determine the width of the card. -->
+        <attr name="columnSpan" format="integer" />
+    </declare-styleable>
+
+    <!-- The configurable attributes in PagedListView. -->
+    <declare-styleable name="PagedListView">
+        <!-- Fade duration in ms -->
+        <attr name="fadeLastItem" format="boolean" />
+        <!-- Set to true/false to offset rows as they slide off screen. Defaults to true -->
+        <attr name="offsetRows" format="boolean" />
+        <!-- Whether or not to offset the list view by the width of scroll bar. Setting this to
+             true will ensure that any views within the list will not overlap the scroll bar. -->
+        <attr name="offsetScrollBar" format="boolean" />
+        <!-- Whether to display the scrollbar or not. Defaults to true. -->
+        <attr name="scrollBarEnabled" format="boolean" />
+        <!-- Whether or not to show a diving line between each item of the list. -->
+        <attr name="showPagedListViewDivider" format="boolean" />
+        <!-- An optional id that specifies a child View whose starting edge will be used to
+             determine the start position of the dividing line. -->
+        <attr name="alignDividerStartTo" format="reference" />
+        <!-- An optional id that specifies a child View whose ending edge will be used to
+             determine the end position of the dividing line. -->
+        <attr name="alignDividerEndTo" format="reference" />
+        <!-- A starting margin before the drawing of the dividing line. This margin will be an
+             offset from the view specified by "alignDividerStartTo" if given. -->
+        <attr name="dividerStartMargin" format="dimension" />
+        <!-- The width of the margin on the right side of the list -->
+        <attr name="listEndMargin" format="dimension" />
+    </declare-styleable>
+
+    <!-- The attributes for customizing the appearance of the hamburger and back arrow in the
+       drawer. -->
+    <declare-styleable name="DrawerArrowDrawable">
+        <!-- The color of the arrow. -->
+        <attr name="carArrowColor" format="color"/>
+        <!-- Whether the arrow will animate when switches directions. -->
+        <attr name="carArrowAnimate" format="boolean"/>
+        <!-- The size of the arrow's bounding box. -->
+        <attr name="carArrowSize" format="dimension"/>
+        <!-- The length of the top and bottom bars that merge to form the point of the arrow. -->
+        <attr name="carArrowHeadLength" format="dimension"/>
+        <!-- The length of arrow shaft. -->
+        <attr name="carArrowShaftLength" format="dimension"/>
+        <!-- The thickness of each of the bars that form the arrow. -->
+        <attr name="carArrowThickness" format="dimension"/>
+        <!-- The spacing between the menu bars (i.e. the "hamburger" icon). -->
+        <attr name="carMenuBarSpacing" format="dimension"/>
+        <!-- The size of the menu bars (i.e. the "hamburger" icon). -->
+        <attr name="carMenuBarThickness" format="dimension"/>
+    </declare-styleable>
+</resources>
diff --git a/car/res/values/colors.xml b/car/res/values/colors.xml
new file mode 100644
index 0000000..1ae98af
--- /dev/null
+++ b/car/res/values/colors.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 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.
+  -->
+<resources>
+    <!-- These colors are from
+         http://www.google.com/design/spec/style/color.html#color-ui-color-palette -->
+    <color name="car_grey_50">#fffafafa</color>
+    <color name="car_grey_100">#fff5f5f5</color>
+    <color name="car_grey_500">#ff9e9e9e</color>
+    <color name="car_grey_900">#ff212121</color>
+
+    <!-- Car specific colors that are not from the color palette. -->
+    <color name="car_grey_650">#ff6B6B6B</color>
+    <color name="car_darkbluegrey_700">#ff172026</color>
+
+    <!--  Various colors for text sizes. "Light" and "dark" here refer to the lighter or darker
+          shades. -->
+    <color name="car_title_light">@color/car_grey_100</color>
+    <color name="car_title_dark">@color/car_grey_900</color>
+    <color name="car_title">@color/car_title_dark</color>
+
+    <color name="car_body1_light">@color/car_grey_100</color>
+    <color name="car_body1_dark">@color/car_grey_900</color>
+    <color name="car_body1">@color/car_body1_dark</color>
+
+    <color name="car_body2_light">@color/car_grey_500</color>
+    <color name="car_body2_dark">@color/car_grey_650</color>
+    <color name="car_body2">@color/car_body2_dark</color>
+
+    <!-- The tinting colors to create a light- and dark-colored icon respectively. -->
+    <color name="car_tint_light">@color/car_grey_50</color>
+    <color name="car_tint_dark">@color/car_grey_900</color>
+
+    <!-- The tinting color for an icon. This icon is assumed to be on a light background. -->
+    <color name="car_tint">@color/car_tint_dark</color>
+
+    <!-- An inverted tinting from car_tint. -->
+    <color name="car_tint_inverse">@color/car_tint_light</color>
+
+    <!-- The color of the divider. The color here is a lighter shade. -->
+    <color name="car_list_divider_light">#1fffffff</color>
+
+    <!-- The color of the divider. The color here is a darker shade. -->
+    <color name="car_list_divider_dark">#1f000000</color>
+
+    <!-- The color of the dividers in the list. This color is assumed to be on a light colored
+         view. -->
+    <color name="car_list_divider">@color/car_list_divider_dark</color>
+
+    <!-- A light and dark colored card. -->
+    <color name="car_card_light">@color/car_grey_50</color>
+    <color name="car_card_dark">@color/car_darkbluegrey_700</color>
+
+    <!-- The default color of a card in car UI. -->
+    <color name="car_card">@color/car_card_light</color>
+
+    <!-- The ripple colors. The "dark" and "light" designation here refers to the color of the
+         ripple  itself. -->
+    <color name="car_card_ripple_background_dark">#17000000</color>
+    <color name="car_card_ripple_background_light">#27ffffff</color>
+
+    <!-- The ripple color for a light colored card. -->
+    <color name="car_card_ripple_background">@color/car_card_ripple_background_dark</color>
+
+    <!-- The ripple color for a dark-colored card. This color is the opposite of
+         car_card_ripple_background. -->
+    <color name="car_card_ripple_background_inverse">@color/car_card_ripple_background_light</color>
+
+    <!-- The top margin before the start of content in an application. -->
+    <dimen name="app_header_height">96dp</dimen>
+
+    <!-- The lighter and darker color for the scrollbar thumb. -->
+    <color name="car_scrollbar_thumb_light">#99ffffff</color>
+    <color name="car_scrollbar_thumb_dark">#7f0b0f12</color>
+
+    <!-- The color of the scroll bar indicator in the PagedListView. This color is assumed to be on
+         a light-colored background. -->
+    <color name="car_scrollbar_thumb">@color/car_scrollbar_thumb_dark</color>
+
+    <!-- The inverted color of the scroll bar indicator. This color is always the opposite of
+         car_scrollbar_thumb. -->
+    <color name="car_scrollbar_thumb_inverse">@color/car_scrollbar_thumb_light</color>
+</resources>
diff --git a/car/res/values/dimens.xml b/car/res/values/dimens.xml
new file mode 100644
index 0000000..09ba50f
--- /dev/null
+++ b/car/res/values/dimens.xml
@@ -0,0 +1,104 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+<resources>
+    <!-- Keylines for content. -->
+    <dimen name="car_keyline_1">24dp</dimen>
+
+    <!-- Various text stylings. -->
+    <dimen name="car_title_size">26sp</dimen>
+    <dimen name="car_body1_size">32sp</dimen>
+    <dimen name="car_body2_size">26sp</dimen>
+
+    <!-- The margin on both sides of the screen before the contents of the PagedListView. -->
+    <dimen name="car_card_margin">96dp</dimen>
+
+    <!-- The height of the dividers in the list. -->
+    <dimen name="car_divider_height">1dp</dimen>
+
+    <!-- Sample row height used for scroll bar calculations in the off chance that a view hasn't
+         been measured. It's highly unlikely that this value will actually be used for more than
+         a frame max. The sample row is a 96dp card + 16dp margin on either side. -->
+    <dimen name="car_sample_row_height">128dp</dimen>
+
+    <!-- The amount of space the LayoutManager will make sure the last item on the screen is
+         peeking before scrolling down -->
+    <dimen name="car_last_card_peek_amount">16dp</dimen>
+
+    <!-- The margin on both sizes of the screen. This margin limits the amount of space that
+         content can take up on screen. -->
+    <dimen name="car_screen_margin_size">16dp</dimen>
+
+    <!-- The spacing between each column that fits on the screen. The number of columns is
+         determined by integer/car_screen_num_of_columns. -->
+    <dimen name="car_screen_gutter_size">16dp</dimen>
+
+    <!-- The margin on both sizes of the scroll bar thumb. -->
+    <dimen name="car_paged_list_view_scrollbar_thumb_margin">8dp</dimen>
+
+    <!-- The size of the scroll bar up and down arrows. -->
+    <dimen name="car_scroll_bar_button_size">44dp</dimen>
+
+    <!-- The padding around the scroll bar. -->
+    <dimen name="car_scroll_bar_padding">16dp</dimen>
+
+    <!-- The width of the scroll bar thumb. -->
+    <dimen name="car_scroll_bar_thumb_width">6dp</dimen>
+
+    <!-- The minimum the scrollbar thumb can shrink to -->
+    <dimen name="min_thumb_height">48dp</dimen>
+
+    <!-- The maximum the scrollbar thumb can grow to -->
+    <dimen name="max_thumb_height">128dp</dimen>
+
+    <!-- Size of progress-bar in Drawer -->
+    <dimen name="car_drawer_progress_bar_size">48dp</dimen>
+
+    <!-- The ending margin of the drawer. Is is the amount that the navigation drawer does not
+       cover the screen. -->
+    <dimen name="car_drawer_margin_end">96dp</dimen>
+
+    <!-- Dimensions of the back arrow in the drawer. -->
+    <dimen name="car_arrow_size">96dp</dimen>
+    <dimen name="car_arrow_thickness">3dp</dimen>
+    <dimen name="car_arrow_shaft_length">34dp</dimen>
+    <dimen name="car_arrow_head_length">18dp</dimen>
+    <dimen name="car_menu_bar_spacing">6dp</dimen>
+    <dimen name="car_menu_bar_length">40dp</dimen>
+
+    <!-- The height of an individual item in the navigation drawer. -->
+    <dimen name="car_drawer_list_item_height">88dp</dimen>
+
+    <!-- The height of a small list item. -->
+    <dimen name="car_drawer_list_item_height_small">64dp</dimen>
+
+    <!-- The size of the starting icon. -->
+    <dimen name="car_drawer_list_item_icon_size">64dp</dimen>
+
+    <!-- The margin after the starting icon. -->
+    <dimen name="car_drawer_list_item_icon_end_margin">32dp</dimen>
+
+    <!-- The ending margin on a list view. -->
+    <dimen name="car_drawer_list_item_end_margin">32dp</dimen>
+
+    <!-- The size of the starting icon in a small list item.-->
+    <dimen name="car_drawer_list_item_small_icon_size">56dp</dimen>
+
+    <!-- The size of the ending icon in a list item. -->
+    <dimen name="car_drawer_list_item_end_icon_size">56dp</dimen>
+
+    <!-- The margin between text is lies on top of each other. -->
+    <dimen name="car_text_vertical_margin">2dp</dimen>
+</resources>
diff --git a/car/res/values/integers.xml b/car/res/values/integers.xml
new file mode 100644
index 0000000..575d646
--- /dev/null
+++ b/car/res/values/integers.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+<resources>
+    <!-- The number of columns that appear on-screen. -->
+    <integer name="car_screen_num_of_columns">4</integer>
+
+    <!-- The default number of columns that a ColumnCardView will span if columnSpan is not
+         specified.-->
+    <integer name="column_card_default_column_span">4</integer>
+</resources>
diff --git a/car/res/values/strings.xml b/car/res/values/strings.xml
new file mode 100644
index 0000000..65f08b6
--- /dev/null
+++ b/car/res/values/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+<resources>
+    <!-- NOTE: Although these strings won't really be used for accessibility
+         in an auto context, integration tests will use them to open/close
+         drawer. See:
+         google_testing/integration/libraries/app-helpers/first-party/auto/
+         -->
+    <string name="car_drawer_open" translatable="false">Open drawer</string>
+    <string name="car_drawer_close" translatable="false">Close drawer</string>
+</resources>
diff --git a/car/res/values/styles.xml b/car/res/values/styles.xml
new file mode 100644
index 0000000..2fd9348
--- /dev/null
+++ b/car/res/values/styles.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+<resources>
+    <!-- The styling for title text. The color of this text changes based on day/night mode. -->
+    <style name="CarTitle" >
+        <item name="android:textStyle">normal</item>
+        <item name="android:textSize">@dimen/car_title_size</item>
+        <item name="android:textColor">@color/car_title</item>
+    </style>
+
+    <!-- Title text that is permanently a dark color. -->
+    <style name="CarTitle.Dark" >
+        <item name="android:textColor">@color/car_title_dark</item>
+    </style>
+
+    <!-- Title text that is permanently a light color. -->
+    <style name="CarTitle.Light" >
+        <item name="android:textColor">@color/car_title_light</item>
+    </style>
+
+    <!-- The styling for body text. The color of this text changes based on the day/night mode. -->
+    <style name="CarBody1" >
+        <item name="android:textStyle">normal</item>
+        <item name="android:textSize">@dimen/car_body1_size</item>
+        <item name="android:textColor">@color/car_body1</item>
+    </style>
+
+    <!-- An alternate styling for body text that is both a different color and size than
+         CarBody1. -->
+    <style name="CarBody2" >
+        <item name="android:textStyle">normal</item>
+        <item name="android:textSize">@dimen/car_body2_size</item>
+        <item name="android:textColor">@color/car_body2</item>
+    </style>
+
+    <!-- The style for the menu bar (i.e. hamburger) and back arrow in the navigation drawer. -->
+    <style name="DrawerArrowStyle" parent="Widget.AppCompat.DrawerArrowToggle">
+        <item name="color">@color/car_title_light</item>
+        <item name="spinBars">true</item>
+        <item name="barLength">@dimen/car_menu_bar_length</item>
+        <item name="thickness">@dimen/car_arrow_thickness</item>
+        <item name="gapBetweenBars">@dimen/car_menu_bar_spacing</item>
+        <item name="arrowShaftLength">@dimen/car_arrow_shaft_length</item>
+        <item name="arrowHeadLength">@dimen/car_arrow_head_length</item>
+        <item name="drawableSize">@dimen/car_arrow_size</item>
+    </style>
+</resources>
diff --git a/car/res/values/themes.xml b/car/res/values/themes.xml
new file mode 100644
index 0000000..4244a22
--- /dev/null
+++ b/car/res/values/themes.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+<resources>
+    <!-- A Theme that activities should use to have correct arrow styling. -->
+    <style name="CarDrawerActivityTheme" parent="Theme.AppCompat.Light.NoActionBar">
+        <item name="drawerArrowStyle">@style/DrawerArrowStyle</item>
+    </style>
+
+    <!-- The styling for the action bar. -->
+    <style name="CarToolbarTheme">
+        <item name="titleTextAppearance">@style/CarTitle.Light</item>
+        <item name="contentInsetStart">@dimen/car_keyline_1</item>
+        <item name="contentInsetEnd">@dimen/car_keyline_1</item>
+    </style>
+</resources>
diff --git a/car/src/main/java/android/support/car/drawer/CarDrawerActivity.java b/car/src/main/java/android/support/car/drawer/CarDrawerActivity.java
new file mode 100644
index 0000000..b6cfc08
--- /dev/null
+++ b/car/src/main/java/android/support/car/drawer/CarDrawerActivity.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2017 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 android.support.car.drawer;
+
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.support.annotation.LayoutRes;
+import android.support.car.R;
+import android.support.v4.widget.DrawerLayout;
+import android.support.v7.app.ActionBarDrawerToggle;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Common base Activity for car apps that need to present a Drawer.
+ *
+ * <p>This Activity manages the overall layout. To use it, sub-classes need to:
+ *
+ * <ul>
+ *   <li>Provide the root-items for the Drawer by implementing {@link #getRootAdapter()}.
+ *   <li>Add their main content using {@link #setMainContent(int)} or {@link #setMainContent(View)}.
+ *       They can also add fragments to the main-content container by obtaining its id using
+ *       {@link #getContentContainerId()}
+ * </ul>
+ *
+ * <p>This class will take care of drawer toggling and display.
+ *
+ * <p>The rootAdapter can implement nested-navigation, in its click-handling, by passing the
+ * CarDrawerAdapter for the next level to
+ * {@link CarDrawerController#switchToAdapter(CarDrawerAdapter)}.
+ *
+ * <p>Any Activity's based on this class need to set their theme to CarDrawerActivityTheme or a
+ * derivative.
+ */
+public abstract class CarDrawerActivity extends AppCompatActivity {
+    protected CarDrawerController mDrawerController;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.car_drawer_activity);
+
+        DrawerLayout drawerLayout = findViewById(R.id.drawer_layout);
+        ActionBarDrawerToggle drawerToggle = new ActionBarDrawerToggle(
+                this /* activity */,
+                drawerLayout, /* DrawerLayout object */
+                R.string.car_drawer_open,
+                R.string.car_drawer_close);
+
+        Toolbar toolbar = findViewById(R.id.car_toolbar);
+        setSupportActionBar(toolbar);
+
+        mDrawerController = new CarDrawerController(toolbar, drawerLayout, drawerToggle);
+        mDrawerController.setRootAdapter(getRootAdapter());
+
+        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+        getSupportActionBar().setHomeButtonEnabled(true);
+    }
+
+    @Override
+    protected void onPostCreate(Bundle savedInstanceState) {
+        super.onPostCreate(savedInstanceState);
+        mDrawerController.syncState();
+    }
+
+    /**
+     * @return Adapter for root content of the Drawer.
+     */
+    protected abstract CarDrawerAdapter getRootAdapter();
+
+    /**
+     * Set main content to display in this Activity. It will be added to R.id.content_frame in
+     * car_drawer_activity.xml. NOTE: Do not use {@link #setContentView(View)}.
+     *
+     * @param view View to display as main content.
+     */
+    public void setMainContent(View view) {
+        ViewGroup parent = findViewById(getContentContainerId());
+        parent.addView(view);
+    }
+
+    /**
+     * Set main content to display in this Activity. It will be added to R.id.content_frame in
+     * car_drawer_activity.xml. NOTE: Do not use {@link #setContentView(int)}.
+     *
+     * @param resourceId Layout to display as main content.
+     */
+    public void setMainContent(@LayoutRes int resourceId) {
+        ViewGroup parent = findViewById(getContentContainerId());
+        LayoutInflater inflater = getLayoutInflater();
+        inflater.inflate(resourceId, parent, true);
+    }
+
+    /**
+     * Get the id of the main content Container which is a FrameLayout. Subclasses can add their own
+     * content/fragments inside here.
+     *
+     * @return Id of FrameLayout where main content of the subclass Activity can be added.
+     */
+    protected int getContentContainerId() {
+        return R.id.content_frame;
+    }
+
+    @Override
+    protected void onStop() {
+        super.onStop();
+        mDrawerController.closeDrawer();
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        mDrawerController.onConfigurationChanged(newConfig);
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        return mDrawerController.onOptionsItemSelected(item) || super.onOptionsItemSelected(item);
+    }
+}
diff --git a/car/src/main/java/android/support/car/drawer/CarDrawerAdapter.java b/car/src/main/java/android/support/car/drawer/CarDrawerAdapter.java
new file mode 100644
index 0000000..b0fd965
--- /dev/null
+++ b/car/src/main/java/android/support/car/drawer/CarDrawerAdapter.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2017 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 android.support.car.drawer;
+
+import android.content.Context;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.car.R;
+import android.support.car.widget.PagedListView;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Base adapter for displaying items in the car navigation drawer, which uses a
+ * {@link PagedListView}.
+ *
+ * <p>Subclasses must set the title that will be displayed when displaying the contents of the
+ * drawer via {@link #setTitle(CharSequence)}. The title can be updated at any point later on. The
+ * title of the root adapter will also be the main title showed in the toolbar when the drawer is
+ * closed. See {@link CarDrawerController#setRootAdapter(CarDrawerAdapter)} for more information.
+ *
+ * <p>This class also takes care of implementing the PageListView.ItemCamp contract and subclasses
+ * should implement {@link #getActualItemCount()}.
+ */
+public abstract class CarDrawerAdapter extends RecyclerView.Adapter<DrawerItemViewHolder>
+        implements PagedListView.ItemCap, DrawerItemClickListener {
+    private final boolean mShowDisabledListOnEmpty;
+    private final Drawable mEmptyListDrawable;
+    private int mMaxItems = PagedListView.ItemCap.UNLIMITED;
+    private CharSequence mTitle;
+    private TitleChangeListener mTitleChangeListener;
+
+    /**
+     * Interface for a class that will be notified a new title has been set on this adapter.
+     */
+    interface TitleChangeListener {
+        /**
+         * Called when {@link #setTitle(CharSequence)} has been called and the title has been
+         * changed.
+         */
+        void onTitleChanged(CharSequence newTitle);
+    }
+
+    protected CarDrawerAdapter(Context context, boolean showDisabledListOnEmpty) {
+        mShowDisabledListOnEmpty = showDisabledListOnEmpty;
+
+        mEmptyListDrawable = context.getDrawable(R.drawable.ic_list_view_disable);
+        mEmptyListDrawable.setColorFilter(context.getColor(R.color.car_tint),
+                PorterDuff.Mode.SRC_IN);
+    }
+
+    /** Returns the title set via {@link #setTitle(CharSequence)}. */
+    CharSequence getTitle() {
+        return mTitle;
+    }
+
+    /** Updates the title to display in the toolbar for this Adapter. */
+    public final void setTitle(@NonNull CharSequence title) {
+        if (title == null) {
+            throw new IllegalArgumentException("setTitle() cannot be passed a null title!");
+        }
+
+        mTitle = title;
+
+        if (mTitleChangeListener != null) {
+            mTitleChangeListener.onTitleChanged(mTitle);
+        }
+    }
+
+    /** Sets a listener to be notified whenever the title of this adapter has been changed. */
+    void setTitleChangeListener(@Nullable TitleChangeListener listener) {
+        mTitleChangeListener = listener;
+    }
+
+    @Override
+    public final void setMaxItems(int maxItems) {
+        mMaxItems = maxItems;
+    }
+
+    @Override
+    public final int getItemCount() {
+        if (shouldShowDisabledListItem()) {
+            return 1;
+        }
+        return mMaxItems >= 0 ? Math.min(mMaxItems, getActualItemCount()) : getActualItemCount();
+    }
+
+    /**
+     * Returns the absolute number of items that can be displayed in the list.
+     *
+     * <p>A class should implement this method to supply the number of items to be displayed.
+     * Returning 0 from this method will cause an empty list icon to be displayed in the drawer.
+     *
+     * <p>A class should override this method rather than {@link #getItemCount()} because that
+     * method is handling the logic of when to display the empty list icon. It will return 1 when
+     * {@link #getActualItemCount()} returns 0.
+     *
+     * @return The number of items to be displayed in the list.
+     */
+    protected abstract int getActualItemCount();
+
+    @Override
+    public final int getItemViewType(int position) {
+        if (shouldShowDisabledListItem()) {
+            return R.layout.car_drawer_list_item_empty;
+        }
+
+        return usesSmallLayout(position)
+                ? R.layout.car_drawer_list_item_small
+                : R.layout.car_drawer_list_item_normal;
+    }
+
+    /**
+     * Used to indicate the layout used for the Drawer item at given position. Subclasses can
+     * override this to use normal layout which includes text element below title.
+     *
+     * <p>A small layout is presented by the layout {@code R.layout.car_drawer_list_item_small}.
+     * Otherwise, the layout {@code R.layout.car_drawer_list_item_normal} will be used.
+     *
+     * @param position Adapter position of item.
+     * @return Whether the item at this position will use a small layout (default) or normal layout.
+     */
+    protected boolean usesSmallLayout(int position) {
+        return true;
+    }
+
+    @Override
+    public final DrawerItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+        View view = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false);
+        return new DrawerItemViewHolder(view);
+    }
+
+    @Override
+    public final void onBindViewHolder(DrawerItemViewHolder holder, int position) {
+        if (shouldShowDisabledListItem()) {
+            holder.getTitle().setText(null);
+            holder.getIcon().setImageDrawable(mEmptyListDrawable);
+            holder.setItemClickListener(null);
+        } else {
+            holder.setItemClickListener(this);
+            populateViewHolder(holder, position);
+        }
+    }
+
+    /**
+     * Whether or not this adapter should be displaying an empty list icon. The icon is shown if it
+     * has been configured to show and there are no items to be displayed.
+     */
+    private boolean shouldShowDisabledListItem() {
+        return mShowDisabledListOnEmpty && getActualItemCount() == 0;
+    }
+
+    /**
+     * Subclasses should set all elements in {@code holder} to populate the drawer-item. If some
+     * element is not used, it should be nulled out since these ViewHolder/View's are recycled.
+     */
+    protected abstract void populateViewHolder(DrawerItemViewHolder holder, int position);
+
+    /**
+     * Called when this adapter has been popped off the stack and is no longer needed. Subclasses
+     * can override to do any necessary cleanup.
+     */
+    public void cleanup() {}
+}
diff --git a/car/src/main/java/android/support/car/drawer/CarDrawerController.java b/car/src/main/java/android/support/car/drawer/CarDrawerController.java
new file mode 100644
index 0000000..b7c9817
--- /dev/null
+++ b/car/src/main/java/android/support/car/drawer/CarDrawerController.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2017 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 android.support.car.drawer;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.car.R;
+import android.support.car.widget.PagedListView;
+import android.support.v4.widget.DrawerLayout;
+import android.support.v7.app.ActionBarDrawerToggle;
+import android.support.v7.widget.Toolbar;
+import android.view.Gravity;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.ProgressBar;
+
+import java.util.Stack;
+
+/**
+ * A controller that will handle the set up of the navigation drawer. It will hook up the
+ * necessary buttons for up navigation, as well as expose methods to allow for a drill down
+ * navigation.
+ */
+public class CarDrawerController {
+    /** The amount that the drawer has been opened before its color should be switched. */
+    private static final float COLOR_SWITCH_SLIDE_OFFSET = 0.25f;
+
+    /**
+     * A representation of the hierarchy of navigation being displayed in the list. The ordering of
+     * this stack is the order that the user has visited each level. When the user navigates up,
+     * the adapters are poopped from this list.
+     */
+    private final Stack<CarDrawerAdapter> mAdapterStack = new Stack<>();
+
+    private final Context mContext;
+
+    private final Toolbar mToolbar;
+    private final DrawerLayout mDrawerLayout;
+    private final ActionBarDrawerToggle mDrawerToggle;
+
+    private final PagedListView mDrawerList;
+    private final ProgressBar mProgressBar;
+    private final View mDrawerContent;
+
+    /**
+     * Creates a {@link CarDrawerController} that will control the navigation of the drawer given by
+     * {@code drawerLayout}.
+     *
+     * <p>The given {@code drawerLayout} should either have a child View that is inflated from
+     * {@code R.layout.car_drawer} or ensure that it three children that have the IDs found in that
+     * layout.
+     *
+     * @param toolbar The {@link Toolbar} that will serve as the action bar for an Activity.
+     * @param drawerLayout The top-level container for the window content that shows the
+     * interactive drawer.
+     * @param drawerToggle The {@link ActionBarDrawerToggle} that bridges the given {@code toolbar}
+     * and {@code drawerLayout}.
+     */
+    public CarDrawerController(Toolbar toolbar,
+            DrawerLayout drawerLayout,
+            ActionBarDrawerToggle drawerToggle) {
+        mToolbar = toolbar;
+        mContext = drawerLayout.getContext();
+
+        mDrawerLayout = drawerLayout;
+
+        mDrawerContent = drawerLayout.findViewById(R.id.drawer_content);
+        mDrawerList = drawerLayout.findViewById(R.id.drawer_list);
+        mDrawerList.setMaxPages(PagedListView.ItemCap.UNLIMITED);
+
+        mProgressBar = drawerLayout.findViewById(R.id.drawer_progress);
+
+        mDrawerToggle = drawerToggle;
+        setupDrawerToggling();
+    }
+
+    /**
+     * Sets the {@link CarDrawerAdapter} that will function as the root adapter. The contents of
+     * this root adapter are shown when the drawer is first opened. It is also the top-most level of
+     * navigation in the drawer.
+     */
+    public void setRootAdapter(CarDrawerAdapter rootAdapter) {
+        mAdapterStack.push(rootAdapter);
+        setToolbarTitleFrom(rootAdapter);
+        mDrawerList.setAdapter(rootAdapter);
+    }
+
+    /**
+     * Switches to use the given {@link CarDrawerAdapter} as the one to supply the list to display
+     * in the navigation drawer. The title will also be updated from the adapter.
+     *
+     * <p>This switch is treated as a navigation to the next level in the drawer. Navigation away
+     * from this level will pop the given adapter off and surface contents of the previous adapter
+     * that was set via this method. If no such adapter exists, then the root adapter set by
+     * {@link #setRootAdapter(CarDrawerAdapter)} will be used instead.
+     *
+     * @param adapter Adapter for next level of content in the drawer.
+     */
+    public final void switchToAdapter(CarDrawerAdapter adapter) {
+        mAdapterStack.peek().setTitleChangeListener(null);
+        mAdapterStack.push(adapter);
+        switchToAdapterInternal(adapter);
+    }
+
+    /** Close the drawer. */
+    public void closeDrawer() {
+        if (mDrawerLayout.isDrawerOpen(Gravity.LEFT)) {
+            mDrawerLayout.closeDrawer(Gravity.LEFT);
+        }
+    }
+
+    /** Opens the drawer. */
+    public void openDrawer() {
+        if (!mDrawerLayout.isDrawerOpen(Gravity.LEFT)) {
+            mDrawerLayout.openDrawer(Gravity.LEFT);
+        }
+    }
+
+    /** Sets a listener to be notified of Drawer events. */
+    public void addDrawerListener(@NonNull DrawerLayout.DrawerListener listener) {
+        mDrawerLayout.addDrawerListener(listener);
+    }
+
+    /** Removes a listener to be notified of Drawer events. */
+    public void removeDrawerListener(@NonNull DrawerLayout.DrawerListener listener) {
+        mDrawerLayout.removeDrawerListener(listener);
+    }
+
+    /**
+     * Sets whether the loading progress bar is displayed in the navigation drawer. If {@code true},
+     * the progress bar is displayed and the navigation list is hidden and vice versa.
+     */
+    public void showLoadingProgressBar(boolean show) {
+        mDrawerList.setVisibility(show ? View.INVISIBLE : View.VISIBLE);
+        mProgressBar.setVisibility(show ? View.VISIBLE : View.GONE);
+    }
+
+    /** Scroll to given position in the list. */
+    public void scrollToPosition(int position) {
+        mDrawerList.getRecyclerView().smoothScrollToPosition(position);
+    }
+
+    /**
+     * Retrieves the title from the given {@link CarDrawerAdapter} and set its as the title of this
+     * controller's internal Toolbar.
+     */
+    private void setToolbarTitleFrom(CarDrawerAdapter adapter) {
+        if (adapter.getTitle() == null) {
+            throw new RuntimeException("CarDrawerAdapter must supply a title via setTitle()");
+        }
+
+        mToolbar.setTitle(adapter.getTitle());
+        adapter.setTitleChangeListener(mToolbar::setTitle);
+    }
+
+    /**
+     * Sets up the necessary listeners for {@link DrawerLayout} so that the navigation drawer
+     * hierarchy is properly displayed.
+     */
+    private void setupDrawerToggling() {
+        mDrawerLayout.addDrawerListener(mDrawerToggle);
+        mDrawerLayout.addDrawerListener(
+                new DrawerLayout.DrawerListener() {
+                    @Override
+                    public void onDrawerSlide(View drawerView, float slideOffset) {
+                        // Correctly set the title and arrow colors as they are different between
+                        // the open and close states.
+                        updateTitleAndArrowColor(slideOffset >= COLOR_SWITCH_SLIDE_OFFSET);
+                    }
+
+                    @Override
+                    public void onDrawerClosed(View drawerView) {
+                        // If drawer is closed, revert stack/drawer to initial root state.
+                        cleanupStackAndShowRoot();
+                        scrollToPosition(0);
+                    }
+
+                    @Override
+                    public void onDrawerOpened(View drawerView) {}
+
+                    @Override
+                    public void onDrawerStateChanged(int newState) {}
+                });
+    }
+
+    /** Sets the title and arrow color of the drawer depending on if it is open or not. */
+    private void updateTitleAndArrowColor(boolean drawerOpen) {
+        // When the drawer is open, use car_title, which resolves to appropriate color depending on
+        // day-night mode. When drawer is closed, we always use light color.
+        int titleColorResId = drawerOpen ? R.color.car_title : R.color.car_title_light;
+        int titleColor = mContext.getColor(titleColorResId);
+        mToolbar.setTitleTextColor(titleColor);
+        mDrawerToggle.getDrawerArrowDrawable().setColor(titleColor);
+    }
+
+    /**
+     * Synchronizes the display of the drawer with its linked {@link DrawerLayout}.
+     *
+     * <p>This should be called from the associated Activity's
+     * {@link android.support.v7.app.AppCompatActivity#onPostCreate(Bundle)} method to synchronize
+     * after teh DRawerLayout's instance state has been restored, and any other time when the
+     * state may have diverged in such a way that this controller's associated
+     * {@link ActionBarDrawerToggle} had not been notified.
+     */
+    public void syncState() {
+        mDrawerToggle.syncState();
+
+        // In case we're restarting after a config change (e.g. day, night switch), set colors
+        // again. Doing it here so that Drawer state is fully synced and we know if its open or not.
+        // NOTE: isDrawerOpen must be passed the second child of the DrawerLayout.
+        updateTitleAndArrowColor(mDrawerLayout.isDrawerOpen(mDrawerContent));
+    }
+
+    /**
+     * Notify this controller that device configurations may have changed.
+     *
+     * <p>This method should be called from the associated Activity's
+     * {@code onConfigurationChanged()} method.
+     */
+    public void onConfigurationChanged(Configuration newConfig) {
+        // Pass any configuration change to the drawer toggle.
+        mDrawerToggle.onConfigurationChanged(newConfig);
+    }
+
+    /**
+     * An analog to an Activity's {@code onOptionsItemSelected()}. This method should be called
+     * when the Activity's method is called and will return {@code true} if the selection has
+     * been handled.
+     *
+     * @return {@code true} if the item processing was handled by this class.
+     */
+    public boolean onOptionsItemSelected(MenuItem item) {
+        // Handle home-click and see if we can navigate up in the drawer.
+        if (item != null && item.getItemId() == android.R.id.home && maybeHandleUpClick()) {
+            return true;
+        }
+
+        // DrawerToggle gets next chance to handle up-clicks (and any other clicks).
+        return mDrawerToggle.onOptionsItemSelected(item);
+    }
+
+    /**
+     * Sets the navigation drawer's title to be the one supplied by the given adapter and updates
+     * the navigation drawer list with the adapter's contents.
+     */
+    private void switchToAdapterInternal(CarDrawerAdapter adapter) {
+        setToolbarTitleFrom(adapter);
+        // NOTE: We don't use swapAdapter() since different levels in the Drawer may switch between
+        // car_drawer_list_item_normal, car_drawer_list_item_small and car_list_empty layouts.
+        mDrawerList.getRecyclerView().setAdapter(adapter);
+        scrollToPosition(0);
+    }
+
+    /**
+     * Switches to the previous level in the drawer hierarchy if the current list being displayed
+     * is not the root adapter. This is analogous to a navigate up.
+     *
+     * @return {@code true} if a navigate up was possible and executed. {@code false} otherwise.
+     */
+    private boolean maybeHandleUpClick() {
+        // Check if already at the root level.
+        if (mAdapterStack.size() <= 1) {
+            return false;
+        }
+
+        CarDrawerAdapter adapter = mAdapterStack.pop();
+        adapter.setTitleChangeListener(null);
+        adapter.cleanup();
+        switchToAdapterInternal(mAdapterStack.peek());
+        return true;
+    }
+
+    /** Clears stack down to root adapter and switches to root adapter. */
+    private void cleanupStackAndShowRoot() {
+        while (mAdapterStack.size() > 1) {
+            CarDrawerAdapter adapter = mAdapterStack.pop();
+            adapter.setTitleChangeListener(null);
+            adapter.cleanup();
+        }
+        switchToAdapterInternal(mAdapterStack.peek());
+    }
+}
diff --git a/car/src/main/java/android/support/car/drawer/DrawerItemClickListener.java b/car/src/main/java/android/support/car/drawer/DrawerItemClickListener.java
new file mode 100644
index 0000000..d707dbd
--- /dev/null
+++ b/car/src/main/java/android/support/car/drawer/DrawerItemClickListener.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2017 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 android.support.car.drawer;
+
+/**
+ * Listener for handling clicks on items/views managed by {@link DrawerItemViewHolder}.
+ */
+public interface DrawerItemClickListener {
+    /**
+     * Callback when item is clicked.
+     *
+     * @param position Adapter position of the clicked item.
+     */
+    void onItemClick(int position);
+}
diff --git a/car/src/main/java/android/support/car/drawer/DrawerItemViewHolder.java b/car/src/main/java/android/support/car/drawer/DrawerItemViewHolder.java
new file mode 100644
index 0000000..d016b2d
--- /dev/null
+++ b/car/src/main/java/android/support/car/drawer/DrawerItemViewHolder.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2017 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 android.support.car.drawer;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.car.R;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+/**
+ * Re-usable {@link RecyclerView.ViewHolder} for displaying items in the
+ * {@link android.support.car.drawer.CarDrawerAdapter}.
+ */
+public class DrawerItemViewHolder extends RecyclerView.ViewHolder {
+    private final ImageView mIcon;
+    private final TextView mTitle;
+    private final TextView mText;
+    private final ImageView mEndIcon;
+
+    DrawerItemViewHolder(View view) {
+        super(view);
+        mIcon = view.findViewById(R.id.icon);
+        if (mIcon == null) {
+            throw new IllegalArgumentException("Icon view cannot be null!");
+        }
+
+        mTitle = view.findViewById(R.id.title);
+        if (mTitle == null) {
+            throw new IllegalArgumentException("Title view cannot be null!");
+        }
+
+        // Next two are optional and may be null.
+        mText = view.findViewById(R.id.text);
+        mEndIcon = view.findViewById(R.id.end_icon);
+    }
+
+    /** Returns the view that should be used to display the main icon. */
+    @NonNull
+    public ImageView getIcon() {
+        return mIcon;
+    }
+
+    /** Returns the view that will display the main title. */
+    @NonNull
+    public TextView getTitle() {
+        return mTitle;
+    }
+
+    /** Returns the view that is used for text that is smaller than the title text. */
+    @Nullable
+    public TextView getText() {
+        return mText;
+    }
+
+    /** Returns the icon that is displayed at the end of the view. */
+    @Nullable
+    public ImageView getEndIcon() {
+        return mEndIcon;
+    }
+
+    /**
+     * Sets the listener that will be notified when the view held by this ViewHolder has been
+     * clicked. Passing {@code null} will clear any previously set listeners.
+     */
+    void setItemClickListener(@Nullable DrawerItemClickListener listener) {
+        itemView.setOnClickListener(listener != null
+                ? v -> listener.onItemClick(getAdapterPosition())
+                : null);
+    }
+}
diff --git a/car/src/main/java/android/support/car/utils/ColumnCalculator.java b/car/src/main/java/android/support/car/utils/ColumnCalculator.java
new file mode 100644
index 0000000..96e081b
--- /dev/null
+++ b/car/src/main/java/android/support/car/utils/ColumnCalculator.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2017 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 android.support.car.utils;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.support.car.R;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.WindowManager;
+
+/**
+ * Utility class that calculates the size of the columns that will fit on the screen. A column's
+ * width is determined by the size of the margins and gutters (space between the columns) that fit
+ * on-screen.
+ *
+ * <p>Refer to the appropriate dimens and integers for the size of the margins and number of
+ * columns.
+ */
+public class ColumnCalculator {
+    private static final String TAG = "ColumnCalculator";
+
+    private static ColumnCalculator sInstance;
+    private static int sScreenWidth;
+
+    private int mNumOfColumns;
+    private int mNumOfGutters;
+    private int mColumnWidth;
+    private int mGutterSize;
+
+    /**
+     * Gets an instance of the {@link ColumnCalculator}. If this is the first time that this
+     * method has been called, then the given {@link Context} will be used to retrieve resources.
+     *
+     * @param context The current calling Context.
+     * @return An instance of {@link ColumnCalculator}.
+     */
+    public static ColumnCalculator getInstance(Context context) {
+        if (sInstance == null) {
+            WindowManager windowManager = (WindowManager) context.getSystemService(
+                    Context.WINDOW_SERVICE);
+            DisplayMetrics displayMetrics = new DisplayMetrics();
+            windowManager.getDefaultDisplay().getMetrics(displayMetrics);
+            sScreenWidth = displayMetrics.widthPixels;
+
+            sInstance = new ColumnCalculator(context);
+        }
+
+        return sInstance;
+    }
+
+    private ColumnCalculator(Context context) {
+        Resources res = context.getResources();
+        int marginSize = res.getDimensionPixelSize(R.dimen.car_screen_margin_size);
+        mGutterSize = res.getDimensionPixelSize(R.dimen.car_screen_gutter_size);
+        mNumOfColumns = res.getInteger(R.integer.car_screen_num_of_columns);
+
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, String.format("marginSize: %d; numOfColumns: %d; gutterSize: %d",
+                    marginSize, mNumOfColumns, mGutterSize));
+        }
+
+        // The gutters appear between each column. As a result, the number of gutters is one less
+        // than the number of columns.
+        mNumOfGutters = mNumOfColumns - 1;
+
+        // Determine the spacing that is allowed to be filled by the columns by subtracting margins
+        // on both size of the screen and the space taken up by the gutters.
+        int spaceForColumns = sScreenWidth - (2 * marginSize) - (mNumOfGutters * mGutterSize);
+
+        mColumnWidth = spaceForColumns / mNumOfColumns;
+
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, "mColumnWidth: " + mColumnWidth);
+        }
+    }
+
+    /**
+     * Returns the total number of columns that fit on the current screen.
+     *
+     * @return The total number of columns that fit on the screen.
+     */
+    public int getNumOfColumns() {
+        return mNumOfColumns;
+    }
+
+    /**
+     * Returns the size in pixels of each column. The column width is determined by the size of the
+     * screen divided by the number of columns, size of gutters and margins.
+     *
+     * @return The width of a single column in pixels.
+     */
+    public int getColumnWidth() {
+        return mColumnWidth;
+    }
+
+    /**
+     * Returns the total number of gutters that fit on screen. A gutter is the space between each
+     * column. This value is always one less than the number of columns.
+     *
+     * @return The number of gutters on screen.
+     */
+    public int getNumOfGutters() {
+        return mNumOfGutters;
+    }
+
+    /**
+     * Returns the size of each gutter in pixels. A gutter is the space between each column.
+     *
+     * @return The size of a single gutter in pixels.
+     */
+    public int getGutterSize() {
+        return mGutterSize;
+    }
+
+    /**
+     * Returns the size in pixels for the given number of columns. This value takes into account
+     * the size of the gutter between the columns as well. For example, for a column span of four,
+     * the size returned is the sum of four columns and three gutters.
+     *
+     * @return The size in pixels for a given column span.
+     */
+    public int getSizeForColumnSpan(int columnSpan) {
+        int gutterSpan = columnSpan - 1;
+        return columnSpan * mColumnWidth + gutterSpan * mGutterSize;
+    }
+}
diff --git a/car/src/main/java/android/support/car/widget/CarItemAnimator.java b/car/src/main/java/android/support/car/widget/CarItemAnimator.java
new file mode 100644
index 0000000..4dd3212
--- /dev/null
+++ b/car/src/main/java/android/support/car/widget/CarItemAnimator.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2017 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 android.support.car.widget;
+
+import android.support.v7.widget.DefaultItemAnimator;
+import android.support.v7.widget.RecyclerView;
+
+/** {@link DefaultItemAnimator} with a few minor changes where it had undesired behavior. */
+public class CarItemAnimator extends DefaultItemAnimator {
+
+    private final CarLayoutManager mLayoutManager;
+
+    public CarItemAnimator(CarLayoutManager layoutManager) {
+        mLayoutManager = layoutManager;
+    }
+
+    @Override
+    public boolean animateChange(RecyclerView.ViewHolder oldHolder,
+            RecyclerView.ViewHolder newHolder,
+            int fromX,
+            int fromY,
+            int toX,
+            int toY) {
+        // The default behavior will cross fade the old view and the new one. However, if we
+        // have a card on a colored background, it will make it appear as if a changing card
+        // fades in and out.
+        float alpha = 0f;
+        if (newHolder != null) {
+            alpha = newHolder.itemView.getAlpha();
+        }
+        boolean ret = super.animateChange(oldHolder, newHolder, fromX, fromY, toX, toY);
+        if (newHolder != null) {
+            newHolder.itemView.setAlpha(alpha);
+        }
+        return ret;
+    }
+
+    @Override
+    public void onMoveFinished(RecyclerView.ViewHolder item) {
+        // The item animator uses translation heavily internally. However, we also use translation
+        // to create the paging affect. When an item's move is animated, it will mess up the
+        // translation we have set on it so we must re-offset the rows once the animations finish.
+
+        // isRunning(ItemAnimationFinishedListener) is the awkward API used to determine when all
+        // animations have finished.
+        isRunning(mFinishedListener);
+    }
+
+    private final ItemAnimatorFinishedListener mFinishedListener =
+            new ItemAnimatorFinishedListener() {
+                @Override
+                public void onAnimationsFinished() {
+                    mLayoutManager.offsetRows();
+                }
+            };
+}
diff --git a/car/src/main/java/android/support/car/widget/CarLayoutManager.java b/car/src/main/java/android/support/car/widget/CarLayoutManager.java
new file mode 100644
index 0000000..d0d3a9e
--- /dev/null
+++ b/car/src/main/java/android/support/car/widget/CarLayoutManager.java
@@ -0,0 +1,1636 @@
+/*
+ * Copyright (C) 2017 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 android.support.car.widget;
+
+import android.content.Context;
+import android.graphics.PointF;
+import android.support.annotation.IntDef;
+import android.support.annotation.NonNull;
+import android.support.annotation.VisibleForTesting;
+import android.support.car.R;
+import android.support.v7.widget.LinearSmoothScroller;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.Recycler;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.LruCache;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.Animation;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.view.animation.Transformation;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+
+/**
+ * Custom {@link RecyclerView.LayoutManager} that behaves similar to LinearLayoutManager except that
+ * it has a few tricks up its sleeve.
+ *
+ * <ol>
+ *   <li>In a normal ListView, when views reach the top of the list, they are clipped. In
+ *       CarLayoutManager, views have the option of flying off of the top of the screen as the next
+ *       row settles in to place. This functionality can be enabled or disabled with {@link
+ *       #setOffsetRows(boolean)}.
+ *   <li>Standard list physics is disabled. Instead, when the user scrolls, it will settle on the
+ *       next page.
+ *   <li>Items can scroll past the bottom edge of the screen. This helps with pagination so that the
+ *       last page can be properly aligned.
+ * </ol>
+ *
+ * This LayoutManger should be used with {@link CarRecyclerView}.
+ */
+public class CarLayoutManager extends RecyclerView.LayoutManager {
+    private static final String TAG = "CarLayoutManager";
+
+    /**
+     * Any fling below the threshold will just scroll to the top fully visible row. The units is
+     * whatever {@link android.widget.Scroller} would return.
+     *
+     * <p>A reasonable value is ~200
+     *
+     * <p>This can be disabled by setting the threshold to -1.
+     */
+    private static final int FLING_THRESHOLD_TO_PAGINATE = -1;
+
+    /**
+     * Any fling shorter than this threshold (in px) will just scroll to the top fully visible row.
+     *
+     * <p>A reasonable value is 15.
+     *
+     * <p>This can be disabled by setting the distance to -1.
+     */
+    private static final int DRAG_DISTANCE_TO_PAGINATE = -1;
+
+    /**
+     * If you scroll really quickly, you can hit the end of the laid out rows before Android has a
+     * chance to layout more. To help counter this, we can layout a number of extra rows past
+     * wherever the focus is if necessary.
+     */
+    private static final int NUM_EXTRA_ROWS_TO_LAYOUT_PAST_FOCUS = 2;
+
+    /**
+     * Scroll bar calculation is a bit complicated. This basically defines the granularity we want
+     * our scroll bar to move. Set this to 1 means our scrollbar will have really jerky movement.
+     * Setting it too big will risk an overflow (although there is no performance impact). Ideally
+     * we want to set this higher than the height of our list view. We can't use our list view
+     * height directly though because we might run into situations where getHeight() returns 0,
+     * for example, when the view is not yet measured.
+     */
+    private static final int SCROLL_RANGE = 1000;
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({BEFORE, AFTER})
+    private @interface LayoutDirection {}
+
+    private static final int BEFORE = 0;
+    private static final int AFTER = 1;
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({ROW_OFFSET_MODE_INDIVIDUAL, ROW_OFFSET_MODE_PAGE})
+    public @interface RowOffsetMode {}
+
+    public static final int ROW_OFFSET_MODE_INDIVIDUAL = 0;
+    public static final int ROW_OFFSET_MODE_PAGE = 1;
+
+    private final AccelerateInterpolator mDanglingRowInterpolator = new AccelerateInterpolator(2);
+    private final Context mContext;
+
+    /** Determines whether or not rows will be offset as they slide off screen * */
+    private boolean mOffsetRows;
+
+    /** Determines whether rows will be offset individually or a page at a time * */
+    @RowOffsetMode private int mRowOffsetMode = ROW_OFFSET_MODE_PAGE;
+
+    /**
+     * The LayoutManager only gets {@link #onScrollStateChanged(int)} updates. This enables the
+     * scroll state to be used anywhere.
+     */
+    private int mScrollState = RecyclerView.SCROLL_STATE_IDLE;
+
+    /** Used to inspect the current scroll state to help with the various calculations. */
+    private CarSmoothScroller mSmoothScroller;
+
+    private PagedListView.OnScrollListener mOnScrollListener;
+
+    /** The distance that the list has actually scrolled in the most recent drag gesture. */
+    private int mLastDragDistance = 0;
+
+    /** {@code True} if the current drag was limited/capped because it was at some boundary. */
+    private boolean mReachedLimitOfDrag;
+
+    /** The index of the first item on the current page. */
+    private int mAnchorPageBreakPosition = 0;
+
+    /** The index of the first item on the previous page. */
+    private int mUpperPageBreakPosition = -1;
+
+    /** The index of the first item on the next page. */
+    private int mLowerPageBreakPosition = -1;
+
+    /** Used in the bookkeeping of mario style scrolling to prevent extra calculations. */
+    private int mLastChildPositionToRequestFocus = -1;
+
+    private int mSampleViewHeight = -1;
+
+    /** Used for onPageUp and onPageDown */
+    private int mViewsPerPage = 1;
+
+    private int mCurrentPage = 0;
+
+    private static final int MAX_ANIMATIONS_IN_CACHE = 30;
+    /**
+     * Cache of TranslateAnimation per child view. These are needed since using a single animation
+     * for all children doesn't apply the animation effect multiple times. Key = the view the
+     * animation will transform.
+     */
+    private LruCache<View, TranslateAnimation> mFlyOffscreenAnimations;
+
+    /** Set the anchor to the following position on the next layout pass. */
+    private int mPendingScrollPosition = -1;
+
+    public CarLayoutManager(Context context) {
+        mContext = context;
+    }
+
+    @Override
+    public RecyclerView.LayoutParams generateDefaultLayoutParams() {
+        return new RecyclerView.LayoutParams(
+                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+    }
+
+    @Override
+    public boolean canScrollVertically() {
+        return true;
+    }
+
+    /**
+     * onLayoutChildren is sort of like a "reset" for the layout state. At a high level, it should:
+     *
+     * <ol>
+     *   <li>Check the current views to get the current state of affairs
+     *   <li>Detach all views from the window (a lightweight operation) so that rows not re-added
+     *       will be removed after onLayoutChildren.
+     *   <li>Re-add rows as necessary.
+     * </ol>
+     *
+     * @see super#onLayoutChildren(RecyclerView.Recycler, RecyclerView.State)
+     */
+    @Override
+    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+        /*
+         * The anchor view is the first fully visible view on screen at the beginning of
+         * onLayoutChildren (or 0 if there is none). This row will be laid out first. After that,
+         * layoutNextRow will layout rows above and below it until the boundaries of what should be
+         * laid out have been reached. See shouldLayoutNextRow(View, int) for more info.
+         */
+        int anchorPosition = 0;
+        int anchorTop = -1;
+        if (mPendingScrollPosition == -1) {
+            View anchor = getFirstFullyVisibleChild();
+            if (anchor != null) {
+                anchorPosition = getPosition(anchor);
+                anchorTop = getDecoratedTop(anchor);
+            }
+        } else {
+            anchorPosition = mPendingScrollPosition;
+            mPendingScrollPosition = -1;
+            mAnchorPageBreakPosition = anchorPosition;
+            mUpperPageBreakPosition = -1;
+            mLowerPageBreakPosition = -1;
+        }
+
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(
+                    TAG,
+                    String.format(
+                            ":: onLayoutChildren anchorPosition:%s, anchorTop:%s,"
+                                    + " mPendingScrollPosition: %s, mAnchorPageBreakPosition:%s,"
+                                    + " mUpperPageBreakPosition:%s, mLowerPageBreakPosition:%s",
+                            anchorPosition,
+                            anchorTop,
+                            mPendingScrollPosition,
+                            mAnchorPageBreakPosition,
+                            mUpperPageBreakPosition,
+                            mLowerPageBreakPosition));
+        }
+
+        /*
+         * Detach all attached view for 2 reasons:
+         *
+         * 1) So that views are put in the scrap heap. This enables us to call {@link
+         *    RecyclerView.Recycler#getViewForPosition(int)} which will either return one of these
+         *    detached views if it is in the scrap heap, one from the recycled pool (will only call
+         *    onBind in the adapter), or create an entirely new row if needed (will call onCreate
+         *    and onBind in the adapter).
+         * 2) So that views are automatically removed if they are not manually re-added.
+         */
+        detachAndScrapAttachedViews(recycler);
+
+        /*
+         * Layout the views recursively.
+         *
+         * It's possible that this re-layout is triggered because an item gets removed. If the
+         * anchor view is at the end of the list, the anchor view position will be bigger than the
+         * number of available items. Correct that, and only start the layout if the anchor
+         * position is valid.
+         */
+        anchorPosition = Math.min(anchorPosition, getItemCount() - 1);
+        if (anchorPosition >= 0) {
+            View anchor = layoutAnchor(recycler, anchorPosition, anchorTop);
+            View adjacentRow = anchor;
+            while (shouldLayoutNextRow(state, adjacentRow, BEFORE)) {
+                adjacentRow = layoutNextRow(recycler, adjacentRow, BEFORE);
+            }
+            adjacentRow = anchor;
+            while (shouldLayoutNextRow(state, adjacentRow, AFTER)) {
+                adjacentRow = layoutNextRow(recycler, adjacentRow, AFTER);
+            }
+        }
+
+        updatePageBreakPositions();
+        offsetRows();
+
+        if (Log.isLoggable(TAG, Log.VERBOSE) && getChildCount() > 1) {
+            Log.v(TAG, "Currently showing "
+                    + getChildCount()
+                    + " views "
+                    + getPosition(getChildAt(0))
+                    + " to "
+                    + getPosition(getChildAt(getChildCount() - 1))
+                    + " anchor "
+                    + anchorPosition);
+        }
+        // Should be at least 1
+        mViewsPerPage =
+                Math.max(getLastFullyVisibleChildIndex() + 1 - getFirstFullyVisibleChildIndex(), 1);
+        mCurrentPage = getFirstFullyVisibleChildPosition() / mViewsPerPage;
+        Log.v(TAG, "viewsPerPage " + mViewsPerPage);
+    }
+
+    /**
+     * scrollVerticallyBy does the work of what should happen when the list scrolls in addition to
+     * handling cases where the list hits the end. It should be lighter weight than
+     * onLayoutChildren. It doesn't have to detach all views. It only looks at the end of the list
+     * and removes views that have gone out of bounds and lays out new ones that scroll in.
+     *
+     * @param dy The amount that the list is supposed to scroll. > 0 means the list is scrolling
+     *     down. < 0 means the list is scrolling up.
+     * @param recycler The recycler that enables views to be reused or created as they scroll in.
+     * @param state Various information about the current state of affairs.
+     * @return The amount the list actually scrolled.
+     * @see super#scrollVerticallyBy(int, RecyclerView.Recycler, RecyclerView.State)
+     */
+    @Override
+    public int scrollVerticallyBy(
+            int dy, @NonNull RecyclerView.Recycler recycler, @NonNull RecyclerView.State state) {
+        // If the list is empty, we can prevent the overscroll glow from showing by just
+        // telling RecycerView that we scrolled.
+        if (getItemCount() == 0) {
+            return dy;
+        }
+
+        // Prevent redundant computations if there is definitely nowhere to scroll to.
+        if (getChildCount() <= 1 || dy == 0) {
+            mReachedLimitOfDrag = true;
+            return 0;
+        }
+
+        View firstChild = getChildAt(0);
+        if (firstChild == null) {
+            mReachedLimitOfDrag = true;
+            return 0;
+        }
+        int firstChildPosition = getPosition(firstChild);
+        RecyclerView.LayoutParams firstChildParams = getParams(firstChild);
+        int firstChildTopWithMargin = getDecoratedTop(firstChild) - firstChildParams.topMargin;
+
+        View lastFullyVisibleView = getChildAt(getLastFullyVisibleChildIndex());
+        if (lastFullyVisibleView == null) {
+            mReachedLimitOfDrag = true;
+            return 0;
+        }
+        boolean isLastViewVisible = getPosition(lastFullyVisibleView) == getItemCount() - 1;
+
+        View firstFullyVisibleChild = getFirstFullyVisibleChild();
+        if (firstFullyVisibleChild == null) {
+            mReachedLimitOfDrag = true;
+            return 0;
+        }
+        int firstFullyVisiblePosition = getPosition(firstFullyVisibleChild);
+        RecyclerView.LayoutParams firstFullyVisibleChildParams = getParams(firstFullyVisibleChild);
+        int topRemainingSpace =
+                getDecoratedTop(firstFullyVisibleChild)
+                        - firstFullyVisibleChildParams.topMargin
+                        - getPaddingTop();
+
+        if (isLastViewVisible
+                && firstFullyVisiblePosition == mAnchorPageBreakPosition
+                && dy > topRemainingSpace
+                && dy > 0) {
+            // Prevent dragging down more than 1 page. As a side effect, this also prevents you
+            // from dragging past the bottom because if you are on the second to last page, it
+            // prevents you from dragging past the last page.
+            dy = topRemainingSpace;
+            mReachedLimitOfDrag = true;
+        } else if (dy < 0
+                && firstChildPosition == 0
+                && firstChildTopWithMargin + Math.abs(dy) > getPaddingTop()) {
+            // Prevent scrolling past the beginning
+            dy = firstChildTopWithMargin - getPaddingTop();
+            mReachedLimitOfDrag = true;
+        } else {
+            mReachedLimitOfDrag = false;
+        }
+
+        boolean isDragging = mScrollState == RecyclerView.SCROLL_STATE_DRAGGING;
+        if (isDragging) {
+            mLastDragDistance += dy;
+        }
+        // We offset by -dy because the views translate in the opposite direction that the
+        // list scrolls (think about it.)
+        offsetChildrenVertical(-dy);
+
+        // The last item in the layout should never scroll above the viewport
+        View view = getChildAt(getChildCount() - 1);
+        if (view.getTop() < 0) {
+            view.setTop(0);
+        }
+
+        // This is the meat of this function. We remove views on the trailing edge of the scroll
+        // and add views at the leading edge as necessary.
+        View adjacentRow;
+        if (dy > 0) {
+            recycleChildrenFromStart(recycler);
+            adjacentRow = getChildAt(getChildCount() - 1);
+            while (shouldLayoutNextRow(state, adjacentRow, AFTER)) {
+                adjacentRow = layoutNextRow(recycler, adjacentRow, AFTER);
+            }
+        } else {
+            recycleChildrenFromEnd(recycler);
+            adjacentRow = getChildAt(0);
+            while (shouldLayoutNextRow(state, adjacentRow, BEFORE)) {
+                adjacentRow = layoutNextRow(recycler, adjacentRow, BEFORE);
+            }
+        }
+        // Now that the correct views are laid out, offset rows as necessary so we can do whatever
+        // fancy animation we want such as having the top view fly off the screen as the next one
+        // settles in to place.
+        updatePageBreakPositions();
+        offsetRows();
+
+        if (getChildCount() > 1) {
+            if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                Log.v(
+                        TAG,
+                        String.format(
+                                "Currently showing  %d views (%d to %d)",
+                                getChildCount(),
+                                getPosition(getChildAt(0)),
+                                getPosition(getChildAt(getChildCount() - 1))));
+            }
+        }
+        updatePagedState();
+        return dy;
+    }
+
+    private void updatePagedState() {
+        int page = getFirstFullyVisibleChildPosition() / mViewsPerPage;
+        if (mOnScrollListener != null) {
+            if (page > mCurrentPage) {
+                mOnScrollListener.onPageDown();
+            } else if (page < mCurrentPage) {
+                mOnScrollListener.onPageUp();
+            }
+        }
+        mCurrentPage = page;
+    }
+
+    @Override
+    public void scrollToPosition(int position) {
+        mPendingScrollPosition = position;
+        requestLayout();
+    }
+
+    @Override
+    public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
+            int position) {
+        /*
+         * startSmoothScroll will handle stopping the old one if there is one. We only keep a copy
+         * of it to handle the translation of rows as they slide off the screen in
+         * offsetRowsWithPageBreak().
+         */
+        mSmoothScroller = new CarSmoothScroller(mContext, position);
+        mSmoothScroller.setTargetPosition(position);
+        startSmoothScroll(mSmoothScroller);
+    }
+
+    /** Miscellaneous bookkeeping. */
+    @Override
+    public void onScrollStateChanged(int state) {
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG, ":: onScrollStateChanged " + state);
+        }
+        if (state == RecyclerView.SCROLL_STATE_IDLE) {
+            // If the focused view is off screen, give focus to one that is.
+            // If the first fully visible view is first in the list, focus the first item.
+            // Otherwise, focus the second so that you have the first item as scrolling context.
+            View focusedChild = getFocusedChild();
+            if (focusedChild != null
+                    && (getDecoratedTop(focusedChild) >= getHeight() - getPaddingBottom()
+                    || getDecoratedBottom(focusedChild) <= getPaddingTop())) {
+                focusedChild.clearFocus();
+                requestLayout();
+            }
+
+        } else if (state == RecyclerView.SCROLL_STATE_DRAGGING) {
+            mLastDragDistance = 0;
+        }
+
+        if (state != RecyclerView.SCROLL_STATE_SETTLING) {
+            mSmoothScroller = null;
+        }
+
+        mScrollState = state;
+        updatePageBreakPositions();
+    }
+
+    @Override
+    public void onItemsChanged(RecyclerView recyclerView) {
+        super.onItemsChanged(recyclerView);
+        // When item changed, our sample view height is no longer accurate, and need to be
+        // recomputed.
+        mSampleViewHeight = -1;
+    }
+
+    /**
+     * Gives us the opportunity to override the order of the focused views. By default, it will just
+     * go from top to bottom. However, if there is no focused views, we take over the logic and
+     * start the focused views from the middle of what is visible and move from there until the
+     * end of the laid out views in the specified direction.
+     */
+    @Override
+    public boolean onAddFocusables(
+            RecyclerView recyclerView, ArrayList<View> views, int direction, int focusableMode) {
+        View focusedChild = getFocusedChild();
+        if (focusedChild != null) {
+            // If there is a view that already has focus, we can just return false and the normal
+            // Android addFocusables will work fine.
+            return false;
+        }
+
+        // Now we know that there isn't a focused view. We need to set up focusables such that
+        // instead of just focusing the first item that has been laid out, it focuses starting
+        // from a visible item.
+
+        int firstFullyVisibleChildIndex = getFirstFullyVisibleChildIndex();
+        if (firstFullyVisibleChildIndex == -1) {
+            // Somehow there is a focused view but there is no fully visible view. There shouldn't
+            // be a way for this to happen but we'd better stop here and return instead of
+            // continuing on with -1.
+            Log.w(TAG, "There is a focused child but no first fully visible child.");
+            return false;
+        }
+        View firstFullyVisibleChild = getChildAt(firstFullyVisibleChildIndex);
+        int firstFullyVisibleChildPosition = getPosition(firstFullyVisibleChild);
+
+        int firstFocusableChildIndex = firstFullyVisibleChildIndex;
+        if (firstFullyVisibleChildPosition > 0 && firstFocusableChildIndex + 1 < getItemCount()) {
+            // We are somewhere in the middle of the list. Instead of starting focus on the first
+            // item, start focus on the second item to give some context that we aren't at
+            // the beginning.
+            firstFocusableChildIndex++;
+        }
+
+        if (direction == View.FOCUS_FORWARD) {
+            // Iterate from the first focusable view to the end.
+            for (int i = firstFocusableChildIndex; i < getChildCount(); i++) {
+                views.add(getChildAt(i));
+            }
+            return true;
+        } else if (direction == View.FOCUS_BACKWARD) {
+            // Iterate from the first focusable view to the beginning.
+            for (int i = firstFocusableChildIndex; i >= 0; i--) {
+                views.add(getChildAt(i));
+            }
+            return true;
+        } else if (direction == View.FOCUS_DOWN) {
+            // Framework calls onAddFocusables with FOCUS_DOWN direction when the focus is first
+            // gained. Thereafter, it calls onAddFocusables with FOCUS_FORWARD or FOCUS_BACKWARD.
+            // First we try to put the focus back on the last focused item, if it is visible
+            int lastFocusedVisibleChildIndex = getLastFocusedChildIndexIfVisible();
+            if (lastFocusedVisibleChildIndex != -1) {
+                views.add(getChildAt(lastFocusedVisibleChildIndex));
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public View onFocusSearchFailed(
+            View focused, int direction, RecyclerView.Recycler recycler, RecyclerView.State state) {
+        // This doesn't seem to get called the way focus is handled in gearhead...
+        return null;
+    }
+
+    /**
+     * This is the function that decides where to scroll to when a new view is focused. You can get
+     * the position of the currently focused child through the child parameter. Once you have that,
+     * determine where to smooth scroll to and scroll there.
+     *
+     * @param parent The RecyclerView hosting this LayoutManager
+     * @param state Current state of RecyclerView
+     * @param child Direct child of the RecyclerView containing the newly focused view
+     * @param focused The newly focused view. This may be the same view as child or it may be null
+     * @return {@code true} if the default scroll behavior should be suppressed
+     */
+    @Override
+    public boolean onRequestChildFocus(
+            RecyclerView parent, RecyclerView.State state, View child, View focused) {
+        if (child == null) {
+            Log.w(TAG, "onRequestChildFocus with a null child!");
+            return true;
+        }
+
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG, String.format(":: onRequestChildFocus child: %s, focused: %s", child,
+                    focused));
+        }
+
+        return onRequestChildFocusMarioStyle(parent, child);
+    }
+
+    /**
+     * Goal: the scrollbar maintains the same size throughout scrolling and that the scrollbar
+     * reaches the bottom of the screen when the last item is fully visible. This is because there
+     * are multiple points that could be considered the bottom since the last item can scroll past
+     * the bottom edge of the screen.
+     *
+     * <p>To find the extent, we divide the number of items that can fit on screen by the number of
+     * items in total.
+     */
+    @Override
+    public int computeVerticalScrollExtent(RecyclerView.State state) {
+        if (getChildCount() <= 1) {
+            return 0;
+        }
+
+        int sampleViewHeight = getSampleViewHeight();
+        int availableHeight = getAvailableHeight();
+        int sampleViewsThatCanFitOnScreen = availableHeight / sampleViewHeight;
+
+        if (state.getItemCount() <= sampleViewsThatCanFitOnScreen) {
+            return SCROLL_RANGE;
+        } else {
+            return SCROLL_RANGE * sampleViewsThatCanFitOnScreen / state.getItemCount();
+        }
+    }
+
+    /**
+     * The scrolling offset is calculated by determining what position is at the top of the list.
+     * However, instead of using fixed integer positions for each row, the scroll position is
+     * factored in and the position is recalculated as a float that takes in to account the
+     * current scroll state. This results in a smooth animation for the scrollbar when the user
+     * scrolls the list.
+     */
+    @Override
+    public int computeVerticalScrollOffset(RecyclerView.State state) {
+        View firstChild = getFirstFullyVisibleChild();
+        if (firstChild == null) {
+            return 0;
+        }
+
+        RecyclerView.LayoutParams params = getParams(firstChild);
+        int firstChildPosition = getPosition(firstChild);
+        float previousChildHieght = (float) (getDecoratedMeasuredHeight(firstChild)
+                + params.topMargin + params.bottomMargin);
+
+        // Assume the previous view is the same height as the current one.
+        float percentOfPreviousViewShowing = (getDecoratedTop(firstChild) - params.topMargin)
+                / previousChildHieght;
+        // If the previous view is actually larger than the current one then this the percent
+        // can be greater than 1.
+        percentOfPreviousViewShowing = Math.min(percentOfPreviousViewShowing, 1);
+
+        float currentPosition = (float) firstChildPosition - percentOfPreviousViewShowing;
+
+        int sampleViewHeight = getSampleViewHeight();
+        int availableHeight = getAvailableHeight();
+        int numberOfSampleViewsThatCanFitOnScreen = availableHeight / sampleViewHeight;
+        int positionWhenLastItemIsVisible =
+                state.getItemCount() - numberOfSampleViewsThatCanFitOnScreen;
+
+        if (positionWhenLastItemIsVisible <= 0) {
+            return 0;
+        }
+
+        if (currentPosition >= positionWhenLastItemIsVisible) {
+            return SCROLL_RANGE;
+        }
+
+        return (int) (SCROLL_RANGE * currentPosition / positionWhenLastItemIsVisible);
+    }
+
+    /**
+     * The range of the scrollbar can be understood as the granularity of how we want the scrollbar
+     * to scroll.
+     */
+    @Override
+    public int computeVerticalScrollRange(RecyclerView.State state) {
+        return SCROLL_RANGE;
+    }
+
+    @Override
+    public void onAttachedToWindow(RecyclerView view) {
+        super.onAttachedToWindow(view);
+        // The purpose of calling this is so that any animation offsets are re-applied. These are
+        // cleared in View.onDetachedFromWindow().
+        // This fixes b/27672379
+        updatePageBreakPositions();
+        offsetRows();
+    }
+
+    @Override
+    public void onDetachedFromWindow(RecyclerView recyclerView, Recycler recycler) {
+        super.onDetachedFromWindow(recyclerView, recycler);
+    }
+
+    /**
+     * @return The first view that starts on screen. It assumes that it fully fits on the screen
+     *     though. If the first fully visible child is also taller than the screen then it will
+     *     still be returned. However, since the LayoutManager snaps to view starts, having a row
+     *     that tall would lead to a broken experience anyways.
+     */
+    public int getFirstFullyVisibleChildIndex() {
+        for (int i = 0; i < getChildCount(); i++) {
+            View child = getChildAt(i);
+            RecyclerView.LayoutParams params = getParams(child);
+            if (getDecoratedTop(child) - params.topMargin >= getPaddingTop()) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * @return The position of first visible child in the list. -1 will be returned if there is no
+     *     child.
+     */
+    public int getFirstFullyVisibleChildPosition() {
+        View child = getFirstFullyVisibleChild();
+        if (child == null) {
+            return -1;
+        }
+        return getPosition(child);
+    }
+
+    /**
+     * @return The position of last visible child in the list. -1 will be returned if there is no
+     *     child.
+     */
+    public int getLastFullyVisibleChildPosition() {
+        View child = getLastFullyVisibleChild();
+        if (child == null) {
+            return -1;
+        }
+        return getPosition(child);
+    }
+
+    /** @return The first View that is completely visible on-screen. */
+    public View getFirstFullyVisibleChild() {
+        int firstFullyVisibleChildIndex = getFirstFullyVisibleChildIndex();
+        View firstChild = null;
+        if (firstFullyVisibleChildIndex != -1) {
+            firstChild = getChildAt(firstFullyVisibleChildIndex);
+        }
+        return firstChild;
+    }
+
+    /** @return The last View that is completely visible on-screen. */
+    public View getLastFullyVisibleChild() {
+        int lastFullyVisibleChildIndex = getLastFullyVisibleChildIndex();
+        View lastChild = null;
+        if (lastFullyVisibleChildIndex != -1) {
+            lastChild = getChildAt(lastFullyVisibleChildIndex);
+        }
+        return lastChild;
+    }
+
+    /**
+     * @return The last view that ends on screen. It assumes that the start is also on screen
+     *     though. If the last fully visible child is also taller than the screen then it will
+     *     still be returned. However, since the LayoutManager snaps to view starts, having a row
+     *     that tall would lead to a broken experience anyways.
+     */
+    public int getLastFullyVisibleChildIndex() {
+        for (int i = getChildCount() - 1; i >= 0; i--) {
+            View child = getChildAt(i);
+            RecyclerView.LayoutParams params = getParams(child);
+            int childBottom = getDecoratedBottom(child) + params.bottomMargin;
+            int listBottom = getHeight() - getPaddingBottom();
+            if (childBottom <= listBottom) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * Returns the index of the child in the list that was last focused and is currently visible to
+     * the user. If no child is found, returns -1.
+     */
+    public int getLastFocusedChildIndexIfVisible() {
+        if (mLastChildPositionToRequestFocus == -1) {
+            return -1;
+        }
+        for (int i = 0; i < getChildCount(); i++) {
+            View child = getChildAt(i);
+            if (getPosition(child) == mLastChildPositionToRequestFocus) {
+                RecyclerView.LayoutParams params = getParams(child);
+                int childBottom = getDecoratedBottom(child) + params.bottomMargin;
+                int listBottom = getHeight() - getPaddingBottom();
+                if (childBottom <= listBottom) {
+                    return i;
+                }
+                break;
+            }
+        }
+        return -1;
+    }
+
+    /** @return Whether or not the first view is fully visible. */
+    public boolean isAtTop() {
+        // getFirstFullyVisibleChildIndex() can return -1 which indicates that there are no views
+        // and also means that the list is at the top.
+        return getFirstFullyVisibleChildIndex() <= 0;
+    }
+
+    /** @return Whether or not the last view is fully visible. */
+    public boolean isAtBottom() {
+        int lastFullyVisibleChildIndex = getLastFullyVisibleChildIndex();
+        if (lastFullyVisibleChildIndex == -1) {
+            return true;
+        }
+        View lastFullyVisibleChild = getChildAt(lastFullyVisibleChildIndex);
+        return getPosition(lastFullyVisibleChild) == getItemCount() - 1;
+    }
+
+    /**
+     * Sets whether or not the rows have an offset animation when it scrolls off-screen. The type
+     * of offset is determined by {@link #setRowOffsetMode(int)}.
+     *
+     * <p>A row being offset means that when they reach the top of the screen, the row is flung off
+     * respectively to the rest of the list. This creates a gap between the offset row(s) and the
+     * list.
+     *
+     * @param offsetRows {@code true} if the rows should be offset.
+     */
+    public void setOffsetRows(boolean offsetRows) {
+        mOffsetRows = offsetRows;
+        if (offsetRows) {
+            // Card animation offsets are only needed when we use the flying off the screen effect
+            if (mFlyOffscreenAnimations == null) {
+                mFlyOffscreenAnimations = new LruCache<>(MAX_ANIMATIONS_IN_CACHE);
+            }
+            offsetRows();
+        } else {
+            int childCount = getChildCount();
+            for (int i = 0; i < childCount; i++) {
+                setCardFlyingEffectOffset(getChildAt(i), 0);
+            }
+            mFlyOffscreenAnimations = null;
+        }
+    }
+
+    /**
+     * Sets the manner of offsetting the rows when they are scrolled off-screen. The rows are either
+     * offset individually or the entire page being scrolled off is offset.
+     *
+     * @param mode One of {@link #ROW_OFFSET_MODE_INDIVIDUAL} or {@link #ROW_OFFSET_MODE_PAGE}.
+     */
+    public void setRowOffsetMode(@RowOffsetMode int mode) {
+        if (mode == mRowOffsetMode) {
+            return;
+        }
+
+        mRowOffsetMode = mode;
+        offsetRows();
+    }
+
+    /**
+     * Sets the listener that will be notified of various scroll events in the list.
+     *
+     * @param listener The on-scroll listener.
+     */
+    public void setOnScrollListener(PagedListView.OnScrollListener listener) {
+        mOnScrollListener = listener;
+    }
+
+    /**
+     * Finish the pagination taking into account where the gesture started (not where we are now).
+     *
+     * @return Whether the list was scrolled as a result of the fling.
+     */
+    public boolean settleScrollForFling(RecyclerView parent, int flingVelocity) {
+        if (getChildCount() == 0) {
+            return false;
+        }
+
+        if (mReachedLimitOfDrag) {
+            return false;
+        }
+
+        // If the fling was too slow or too short, settle on the first fully visible row instead.
+        if (Math.abs(flingVelocity) <= FLING_THRESHOLD_TO_PAGINATE
+                || Math.abs(mLastDragDistance) <= DRAG_DISTANCE_TO_PAGINATE) {
+            int firstFullyVisibleChildIndex = getFirstFullyVisibleChildIndex();
+            if (firstFullyVisibleChildIndex != -1) {
+                int scrollPosition = getPosition(getChildAt(firstFullyVisibleChildIndex));
+                parent.smoothScrollToPosition(scrollPosition);
+                return true;
+            }
+            return false;
+        }
+
+        // Finish the pagination taking into account where the gesture
+        // started (not where we are now).
+        boolean isDownGesture = flingVelocity > 0 || (flingVelocity == 0 && mLastDragDistance >= 0);
+        boolean isUpGesture = flingVelocity < 0 || (flingVelocity == 0 && mLastDragDistance < 0);
+        if (isDownGesture && mLowerPageBreakPosition != -1) {
+            // If the last view is fully visible then only settle on the first fully visible view
+            // instead of the original page down position. However, don't page down if the last
+            // item has come fully into view.
+            parent.smoothScrollToPosition(mAnchorPageBreakPosition);
+            if (mOnScrollListener != null) {
+                mOnScrollListener.onGestureDown();
+            }
+            return true;
+        } else if (isUpGesture && mUpperPageBreakPosition != -1) {
+            parent.smoothScrollToPosition(mUpperPageBreakPosition);
+            if (mOnScrollListener != null) {
+                mOnScrollListener.onGestureUp();
+            }
+            return true;
+        } else {
+            Log.e(
+                    TAG,
+                    "Error setting scroll for fling! flingVelocity: \t"
+                            + flingVelocity
+                            + "\tlastDragDistance: "
+                            + mLastDragDistance
+                            + "\tpageUpAtStartOfDrag: "
+                            + mUpperPageBreakPosition
+                            + "\tpageDownAtStartOfDrag: "
+                            + mLowerPageBreakPosition);
+            // As a last resort, at the last smooth scroller target position if there is one.
+            if (mSmoothScroller != null) {
+                parent.smoothScrollToPosition(mSmoothScroller.getTargetPosition());
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /** @return The position that paging up from the current position would settle at. */
+    public int getPageUpPosition() {
+        return mUpperPageBreakPosition;
+    }
+
+    /** @return The position that paging down from the current position would settle at. */
+    public int getPageDownPosition() {
+        return mLowerPageBreakPosition;
+    }
+
+    /**
+     * Layout the anchor row. The anchor row is the first fully visible row.
+     *
+     * @param anchorTop The decorated top of the anchor. If it is not known or should be reset to
+     *     the top, pass -1.
+     */
+    private View layoutAnchor(RecyclerView.Recycler recycler, int anchorPosition, int anchorTop) {
+        View anchor = recycler.getViewForPosition(anchorPosition);
+        RecyclerView.LayoutParams params = getParams(anchor);
+        measureChildWithMargins(anchor, 0, 0);
+        int left = getPaddingLeft() + params.leftMargin;
+        int top = (anchorTop == -1) ? params.topMargin : anchorTop;
+        int right = left + getDecoratedMeasuredWidth(anchor);
+        int bottom = top + getDecoratedMeasuredHeight(anchor);
+        layoutDecorated(anchor, left, top, right, bottom);
+        addView(anchor);
+        return anchor;
+    }
+
+    /**
+     * Lays out the next row in the specified direction next to the specified adjacent row.
+     *
+     * @param recycler The recycler from which a new view can be created.
+     * @param adjacentRow The View of the adjacent row which will be used to position the new one.
+     * @param layoutDirection The side of the adjacent row that the new row will be laid out on.
+     * @return The new row that was laid out.
+     */
+    private View layoutNextRow(RecyclerView.Recycler recycler, View adjacentRow,
+            @LayoutDirection int layoutDirection) {
+        int adjacentRowPosition = getPosition(adjacentRow);
+        int newRowPosition = adjacentRowPosition;
+        if (layoutDirection == BEFORE) {
+            newRowPosition = adjacentRowPosition - 1;
+        } else if (layoutDirection == AFTER) {
+            newRowPosition = adjacentRowPosition + 1;
+        }
+
+        // Because we detach all rows in onLayoutChildren, this will often just return a view from
+        // the scrap heap.
+        View newRow = recycler.getViewForPosition(newRowPosition);
+
+        measureChildWithMargins(newRow, 0, 0);
+        RecyclerView.LayoutParams newRowParams =
+                (RecyclerView.LayoutParams) newRow.getLayoutParams();
+        RecyclerView.LayoutParams adjacentRowParams =
+                (RecyclerView.LayoutParams) adjacentRow.getLayoutParams();
+        int left = getPaddingLeft() + newRowParams.leftMargin;
+        int right = left + getDecoratedMeasuredWidth(newRow);
+        int top;
+        int bottom;
+        if (layoutDirection == BEFORE) {
+            bottom = adjacentRow.getTop() - adjacentRowParams.topMargin - newRowParams.bottomMargin;
+            top = bottom - getDecoratedMeasuredHeight(newRow);
+        } else {
+            top = getDecoratedBottom(adjacentRow) + adjacentRowParams.bottomMargin
+                    + newRowParams.topMargin;
+            bottom = top + getDecoratedMeasuredHeight(newRow);
+        }
+        layoutDecorated(newRow, left, top, right, bottom);
+
+        if (layoutDirection == BEFORE) {
+            addView(newRow, 0);
+        } else {
+            addView(newRow);
+        }
+
+        return newRow;
+    }
+
+    /** @return Whether another row should be laid out in the specified direction. */
+    private boolean shouldLayoutNextRow(
+            RecyclerView.State state, View adjacentRow, @LayoutDirection int layoutDirection) {
+        int adjacentRowPosition = getPosition(adjacentRow);
+
+        if (layoutDirection == BEFORE) {
+            if (adjacentRowPosition == 0) {
+                // We already laid out the first row.
+                return false;
+            }
+        } else if (layoutDirection == AFTER) {
+            if (adjacentRowPosition >= state.getItemCount() - 1) {
+                // We already laid out the last row.
+                return false;
+            }
+        }
+
+        // If we are scrolling layout views until the target position.
+        if (mSmoothScroller != null) {
+            if (layoutDirection == BEFORE
+                    && adjacentRowPosition >= mSmoothScroller.getTargetPosition()) {
+                return true;
+            } else if (layoutDirection == AFTER
+                    && adjacentRowPosition <= mSmoothScroller.getTargetPosition()) {
+                return true;
+            }
+        }
+
+        View focusedRow = getFocusedChild();
+        if (focusedRow != null) {
+            int focusedRowPosition = getPosition(focusedRow);
+            if (layoutDirection == BEFORE && adjacentRowPosition
+                    >= focusedRowPosition - NUM_EXTRA_ROWS_TO_LAYOUT_PAST_FOCUS) {
+                return true;
+            } else if (layoutDirection == AFTER && adjacentRowPosition
+                    <= focusedRowPosition + NUM_EXTRA_ROWS_TO_LAYOUT_PAST_FOCUS) {
+                return true;
+            }
+        }
+
+        RecyclerView.LayoutParams params = getParams(adjacentRow);
+        int adjacentRowTop = getDecoratedTop(adjacentRow) - params.topMargin;
+        int adjacentRowBottom = getDecoratedBottom(adjacentRow) - params.bottomMargin;
+        if (layoutDirection == BEFORE && adjacentRowTop < getPaddingTop() - getHeight()) {
+            // View is more than 1 page past the top of the screen and also past where the user has
+            // scrolled to. We want to keep one page past the top to make the scroll up calculation
+            // easier and scrolling smoother.
+            return false;
+        } else if (layoutDirection == AFTER
+                && adjacentRowBottom > getHeight() - getPaddingBottom()) {
+            // View is off of the bottom and also past where the user has scrolled to.
+            return false;
+        }
+
+        return true;
+    }
+
+    /** Remove and recycle views that are no longer needed. */
+    private void recycleChildrenFromStart(RecyclerView.Recycler recycler) {
+        // Start laying out children one page before the top of the viewport.
+        int childrenStart = getPaddingTop() - getHeight();
+
+        int focusedChildPosition = Integer.MAX_VALUE;
+        View focusedChild = getFocusedChild();
+        if (focusedChild != null) {
+            focusedChildPosition = getPosition(focusedChild);
+        }
+
+        // Count the number of views that should be removed.
+        int detachedCount = 0;
+        int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            final View child = getChildAt(i);
+            int childEnd = getDecoratedBottom(child);
+            int childPosition = getPosition(child);
+
+            if (childEnd >= childrenStart || childPosition >= focusedChildPosition - 1) {
+                break;
+            }
+
+            detachedCount++;
+        }
+
+        // Remove the number of views counted above. Done by removing the first child n times.
+        while (--detachedCount >= 0) {
+            final View child = getChildAt(0);
+            removeAndRecycleView(child, recycler);
+        }
+    }
+
+    /** Remove and recycle views that are no longer needed. */
+    private void recycleChildrenFromEnd(RecyclerView.Recycler recycler) {
+        // Layout views until the end of the viewport.
+        int childrenEnd = getHeight();
+
+        int focusedChildPosition = Integer.MIN_VALUE + 1;
+        View focusedChild = getFocusedChild();
+        if (focusedChild != null) {
+            focusedChildPosition = getPosition(focusedChild);
+        }
+
+        // Count the number of views that should be removed.
+        int firstDetachedPos = 0;
+        int detachedCount = 0;
+        int childCount = getChildCount();
+        for (int i = childCount - 1; i >= 0; i--) {
+            final View child = getChildAt(i);
+            int childStart = getDecoratedTop(child);
+            int childPosition = getPosition(child);
+
+            if (childStart <= childrenEnd || childPosition <= focusedChildPosition - 1) {
+                break;
+            }
+
+            firstDetachedPos = i;
+            detachedCount++;
+        }
+
+        while (--detachedCount >= 0) {
+            final View child = getChildAt(firstDetachedPos);
+            removeAndRecycleView(child, recycler);
+        }
+    }
+
+    /**
+     * Offset rows to do fancy animations. If offset rows was not enabled with
+     * {@link #setOffsetRows}, this will do nothing.
+     *
+     * @see #offsetRowsIndividually
+     * @see #offsetRowsByPage
+     * @see #setOffsetRows
+     */
+    public void offsetRows() {
+        if (!mOffsetRows) {
+            return;
+        }
+
+        if (mRowOffsetMode == ROW_OFFSET_MODE_PAGE) {
+            offsetRowsByPage();
+        } else if (mRowOffsetMode == ROW_OFFSET_MODE_INDIVIDUAL) {
+            offsetRowsIndividually();
+        }
+    }
+
+    /**
+     * Offset the single row that is scrolling off the screen such that by the time the next row
+     * reaches the top, it will have accelerated completely off of the screen.
+     */
+    private void offsetRowsIndividually() {
+        if (getChildCount() == 0) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, ":: offsetRowsIndividually getChildCount=0");
+            }
+            return;
+        }
+
+        // Identify the dangling row. It will be the first row that is at the top of the
+        // list or above.
+        int danglingChildIndex = -1;
+        for (int i = getChildCount() - 1; i >= 0; i--) {
+            View child = getChildAt(i);
+            if (getDecoratedTop(child) - getParams(child).topMargin <= getPaddingTop()) {
+                danglingChildIndex = i;
+                break;
+            }
+        }
+
+        mAnchorPageBreakPosition = danglingChildIndex;
+
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG, ":: offsetRowsIndividually danglingChildIndex: " + danglingChildIndex);
+        }
+
+        // Calculate the total amount that the view will need to scroll in order to go completely
+        // off screen.
+        RecyclerView rv = (RecyclerView) getChildAt(0).getParent();
+        int[] locs = new int[2];
+        rv.getLocationInWindow(locs);
+        int listTopInWindow = locs[1] + rv.getPaddingTop();
+        int maxDanglingViewTranslation;
+
+        int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View child = getChildAt(i);
+            RecyclerView.LayoutParams params = getParams(child);
+
+            maxDanglingViewTranslation = listTopInWindow;
+            // If the child has a negative margin, we'll actually need to translate the view a
+            // little but further to get it completely off screen.
+            if (params.topMargin < 0) {
+                maxDanglingViewTranslation -= params.topMargin;
+            }
+            if (params.bottomMargin < 0) {
+                maxDanglingViewTranslation -= params.bottomMargin;
+            }
+
+            if (i < danglingChildIndex) {
+                child.setAlpha(0f);
+            } else if (i > danglingChildIndex) {
+                child.setAlpha(1f);
+                setCardFlyingEffectOffset(child, 0);
+            } else {
+                int totalScrollDistance =
+                        getDecoratedMeasuredHeight(child) + params.topMargin + params.bottomMargin;
+
+                int distanceLeftInScroll =
+                        getDecoratedBottom(child) + params.bottomMargin - getPaddingTop();
+                float percentageIntoScroll = 1 - distanceLeftInScroll / (float) totalScrollDistance;
+                float interpolatedPercentage =
+                        mDanglingRowInterpolator.getInterpolation(percentageIntoScroll);
+
+                child.setAlpha(1f);
+                setCardFlyingEffectOffset(child, -(maxDanglingViewTranslation
+                        * interpolatedPercentage));
+            }
+        }
+    }
+
+    /**
+     * When the list scrolls, the entire page of rows will offset in one contiguous block. This
+     * significantly reduces the amount of extra motion at the top of the screen.
+     */
+    private void offsetRowsByPage() {
+        View anchorView = findViewByPosition(mAnchorPageBreakPosition);
+        if (anchorView == null) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, ":: offsetRowsByPage anchorView null");
+            }
+            return;
+        }
+        int anchorViewTop = getDecoratedTop(anchorView) - getParams(anchorView).topMargin;
+
+        View upperPageBreakView = findViewByPosition(mUpperPageBreakPosition);
+        int upperViewTop =
+                getDecoratedTop(upperPageBreakView) - getParams(upperPageBreakView).topMargin;
+
+        int scrollDistance = upperViewTop - anchorViewTop;
+
+        int distanceLeft = anchorViewTop - getPaddingTop();
+        float scrollPercentage =
+                (Math.abs(scrollDistance) - distanceLeft) / (float) Math.abs(scrollDistance);
+
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG, String.format(":: offsetRowsByPage scrollDistance:%s, distanceLeft:%s, "
+                            + "scrollPercentage:%s",
+                    scrollDistance, distanceLeft, scrollPercentage));
+        }
+
+        // Calculate the total amount that the view will need to scroll in order to go completely
+        // off screen.
+        RecyclerView rv = (RecyclerView) getChildAt(0).getParent();
+        int[] locs = new int[2];
+        rv.getLocationInWindow(locs);
+        int listTopInWindow = locs[1] + rv.getPaddingTop();
+
+        int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View child = getChildAt(i);
+            int position = getPosition(child);
+            if (position < mUpperPageBreakPosition) {
+                child.setAlpha(0f);
+                setCardFlyingEffectOffset(child, -listTopInWindow);
+            } else if (position < mAnchorPageBreakPosition) {
+                // If the child has a negative margin, we need to offset the row by a little bit
+                // extra so that it moves completely off screen.
+                RecyclerView.LayoutParams params = getParams(child);
+                int extraTranslation = 0;
+                if (params.topMargin < 0) {
+                    extraTranslation -= params.topMargin;
+                }
+                if (params.bottomMargin < 0) {
+                    extraTranslation -= params.bottomMargin;
+                }
+                int translation = (int) ((listTopInWindow + extraTranslation)
+                        * mDanglingRowInterpolator.getInterpolation(scrollPercentage));
+                child.setAlpha(1f);
+                setCardFlyingEffectOffset(child, -translation);
+            } else {
+                child.setAlpha(1f);
+                setCardFlyingEffectOffset(child, 0);
+            }
+        }
+    }
+
+    /**
+     * Apply an offset to this view. This offset is applied post-layout so it doesn't affect when
+     * views are recycled
+     *
+     * @param child The view to apply this to
+     * @param verticalOffset The offset for this child.
+     */
+    private void setCardFlyingEffectOffset(View child, float verticalOffset) {
+        // Ideally instead of doing all this, we could use View.setTranslationY(). However, the
+        // default RecyclerView.ItemAnimator also uses this method which causes layout issues.
+        // See: http://b/25977087
+        TranslateAnimation anim = mFlyOffscreenAnimations.get(child);
+        if (anim == null) {
+            anim = new TranslateAnimation();
+            anim.setFillEnabled(true);
+            anim.setFillAfter(true);
+            anim.setDuration(0);
+            mFlyOffscreenAnimations.put(child, anim);
+        } else if (anim.verticalOffset == verticalOffset) {
+            return;
+        }
+
+        anim.reset();
+        anim.verticalOffset = verticalOffset;
+        anim.setStartTime(Animation.START_ON_FIRST_FRAME);
+        child.setAnimation(anim);
+        anim.startNow();
+    }
+
+    /**
+     * Update the page break positions based on the position of the views on screen. This should be
+     * called whenever view move or change such as during a scroll or layout.
+     */
+    private void updatePageBreakPositions() {
+        if (getChildCount() == 0) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, ":: updatePageBreakPosition getChildCount: 0");
+            }
+            return;
+        }
+
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG, String.format(":: #BEFORE updatePageBreakPositions "
+                            + "mAnchorPageBreakPosition:%s, mUpperPageBreakPosition:%s, "
+                            + "mLowerPageBreakPosition:%s",
+                            mAnchorPageBreakPosition, mUpperPageBreakPosition,
+                            mLowerPageBreakPosition));
+        }
+
+        mAnchorPageBreakPosition = getPosition(getFirstFullyVisibleChild());
+
+        if (mAnchorPageBreakPosition == -1) {
+            Log.w(TAG, "Unable to update anchor positions. There is no anchor position.");
+            return;
+        }
+
+        View anchorPageBreakView = findViewByPosition(mAnchorPageBreakPosition);
+        if (anchorPageBreakView == null) {
+            return;
+        }
+        int topMargin = getParams(anchorPageBreakView).topMargin;
+        int anchorTop = getDecoratedTop(anchorPageBreakView) - topMargin;
+        View upperPageBreakView = findViewByPosition(mUpperPageBreakPosition);
+        int upperPageBreakTop = upperPageBreakView == null
+                ? Integer.MIN_VALUE
+                : getDecoratedTop(upperPageBreakView) - getParams(upperPageBreakView).topMargin;
+
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG, String.format(":: #MID updatePageBreakPositions topMargin:%s, anchorTop:%s"
+                            + " mAnchorPageBreakPosition:%s, mUpperPageBreakPosition:%s,"
+                            + " mLowerPageBreakPosition:%s",
+                            topMargin,
+                            anchorTop,
+                            mAnchorPageBreakPosition,
+                            mUpperPageBreakPosition,
+                            mLowerPageBreakPosition));
+        }
+
+        if (anchorTop < getPaddingTop()) {
+            // The anchor has moved above the viewport. We are now on the next page. Shift the page
+            // break positions and calculate a new lower one.
+            mUpperPageBreakPosition = mAnchorPageBreakPosition;
+            mAnchorPageBreakPosition = mLowerPageBreakPosition;
+            mLowerPageBreakPosition = calculateNextPageBreakPosition(mAnchorPageBreakPosition);
+        } else if (mAnchorPageBreakPosition > 0 && upperPageBreakTop >= getPaddingTop()) {
+            // The anchor has moved below the viewport. We are now on the previous page. Shift
+            // the page break positions and calculate a new upper one.
+            mLowerPageBreakPosition = mAnchorPageBreakPosition;
+            mAnchorPageBreakPosition = mUpperPageBreakPosition;
+            mUpperPageBreakPosition = calculatePreviousPageBreakPosition(mAnchorPageBreakPosition);
+        } else {
+            mUpperPageBreakPosition = calculatePreviousPageBreakPosition(mAnchorPageBreakPosition);
+            mLowerPageBreakPosition = calculateNextPageBreakPosition(mAnchorPageBreakPosition);
+        }
+
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG, String.format(":: #AFTER updatePageBreakPositions"
+                            + " mAnchorPageBreakPosition:%s, mUpperPageBreakPosition:%s,"
+                            + " mLowerPageBreakPosition:%s",
+                            mAnchorPageBreakPosition, mUpperPageBreakPosition,
+                    mLowerPageBreakPosition));
+        }
+    }
+
+    /**
+     * @return The page break position of the page before the anchor page break position. However,
+     *     if it reaches the end of the laid out children or position 0, it will just return that.
+     */
+    @VisibleForTesting
+    int calculatePreviousPageBreakPosition(int position) {
+        if (position == -1) {
+            return -1;
+        }
+        View referenceView = findViewByPosition(position);
+        int referenceViewTop = getDecoratedTop(referenceView) - getParams(referenceView).topMargin;
+
+        int previousPagePosition = position;
+        while (previousPagePosition > 0) {
+            previousPagePosition--;
+            View child = findViewByPosition(previousPagePosition);
+            if (child == null) {
+                // View has not been laid out yet.
+                return previousPagePosition + 1;
+            }
+
+            int childTop = getDecoratedTop(child) - getParams(child).topMargin;
+            if (childTop < referenceViewTop - getHeight()) {
+                return previousPagePosition + 1;
+            }
+        }
+        // Beginning of the list.
+        return 0;
+    }
+
+    /**
+     * @return The page break position of the next page after the anchor page break position.
+     *     However, if it reaches the end of the laid out children or end of the list, it will just
+     *     return that.
+     */
+    @VisibleForTesting
+    int calculateNextPageBreakPosition(int position) {
+        if (position == -1) {
+            return -1;
+        }
+
+        View referenceView = findViewByPosition(position);
+        if (referenceView == null) {
+            return position;
+        }
+        int referenceViewTop = getDecoratedTop(referenceView) - getParams(referenceView).topMargin;
+
+        int nextPagePosition = position;
+
+        // Search for the first child item after the referenceView that didn't fully fit on to the
+        // screen. The next page should start from the item before this child, so that users have
+        // a visual anchoring point of the page change.
+        while (nextPagePosition < getItemCount() - 1) {
+            nextPagePosition++;
+            View child = findViewByPosition(nextPagePosition);
+            if (child == null) {
+                // The next view has not been laid out yet.
+                return nextPagePosition - 1;
+            }
+
+            int childTop = getDecoratedTop(child) - getParams(child).topMargin;
+            if (childTop > referenceViewTop + getHeight()) {
+                // If choosing the previous child causes the view to snap back to the referenceView
+                // position, then skip that and go directly to the child. This avoids the case
+                // where a tall card in the layout causes the view to constantly snap back to
+                // the top when scrolled.
+                return nextPagePosition - 1 == position ? nextPagePosition : nextPagePosition - 1;
+            }
+        }
+        // End of the list.
+        return nextPagePosition;
+    }
+
+    /**
+     * In this style, the focus will scroll down to the middle of the screen and lock there so that
+     * moving in either direction will move the entire list by 1.
+     */
+    private boolean onRequestChildFocusMarioStyle(RecyclerView parent, View child) {
+        int focusedPosition = getPosition(child);
+        if (focusedPosition == mLastChildPositionToRequestFocus) {
+            return true;
+        }
+        mLastChildPositionToRequestFocus = focusedPosition;
+
+        int availableHeight = getAvailableHeight();
+        int focusedChildTop = getDecoratedTop(child);
+        int focusedChildBottom = getDecoratedBottom(child);
+
+        int childIndex = parent.indexOfChild(child);
+        // Iterate through children starting at the focused child to find the child above it to
+        // smooth scroll to such that the focused child will be as close to the middle of the screen
+        // as possible.
+        for (int i = childIndex; i >= 0; i--) {
+            View childAtI = getChildAt(i);
+            if (childAtI == null) {
+                Log.e(TAG, "Child is null at index " + i);
+                continue;
+            }
+            // We haven't found a view that is more than half of the recycler view height above it
+            // but we've reached the top so we can't go any further.
+            if (i == 0) {
+                parent.smoothScrollToPosition(getPosition(childAtI));
+                break;
+            }
+
+            // Because we want to scroll to the first view that is less than half of the screen
+            // away from the focused view, we "look ahead" one view. When the look ahead view
+            // is more than availableHeight / 2 away, the current child at i is the one we want to
+            // scroll to. However, sometimes, that view can be null (ie, if the view is in
+            // transition). In that case, just skip that view.
+
+            View childBefore = getChildAt(i - 1);
+            if (childBefore == null) {
+                continue;
+            }
+            int distanceToChildBeforeFromTop = focusedChildTop - getDecoratedTop(childBefore);
+            int distanceToChildBeforeFromBottom = focusedChildBottom - getDecoratedTop(childBefore);
+
+            if (distanceToChildBeforeFromTop > availableHeight / 2
+                    || distanceToChildBeforeFromBottom > availableHeight) {
+                parent.smoothScrollToPosition(getPosition(childAtI));
+                break;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * We don't actually know the size of every single view, only what is currently laid out. This
+     * makes it difficult to do accurate scrollbar calculations. However, lists in the car often
+     * consist of views with identical heights. Because of that, we can use a single sample view to
+     * do our calculations for. The main exceptions are in the first items of a list (hero card,
+     * last call card, etc) so if the first view is at position 0, we pick the next one.
+     *
+     * @return The decorated measured height of the sample view plus its margins.
+     */
+    private int getSampleViewHeight() {
+        if (mSampleViewHeight != -1) {
+            return mSampleViewHeight;
+        }
+        int sampleViewIndex = getFirstFullyVisibleChildIndex();
+        View sampleView = getChildAt(sampleViewIndex);
+        if (getPosition(sampleView) == 0 && sampleViewIndex < getChildCount() - 1) {
+            sampleView = getChildAt(++sampleViewIndex);
+        }
+        RecyclerView.LayoutParams params = getParams(sampleView);
+        int height = getDecoratedMeasuredHeight(sampleView) + params.topMargin
+                + params.bottomMargin;
+        if (height == 0) {
+            // This can happen if the view isn't measured yet.
+            Log.w(
+                    TAG,
+                    "The sample view has a height of 0. Returning a dummy value for now "
+                            + "that won't be cached.");
+            height = mContext.getResources().getDimensionPixelSize(R.dimen.car_sample_row_height);
+        } else {
+            mSampleViewHeight = height;
+        }
+        return height;
+    }
+
+    /** @return The height of the RecyclerView excluding padding. */
+    private int getAvailableHeight() {
+        return getHeight() - getPaddingTop() - getPaddingBottom();
+    }
+
+    /**
+     * @return {@link RecyclerView.LayoutParams} for the given view or null if it isn't a child of
+     *     {@link RecyclerView}.
+     */
+    private static RecyclerView.LayoutParams getParams(View view) {
+        return (RecyclerView.LayoutParams) view.getLayoutParams();
+    }
+
+    /**
+     * Custom {@link LinearSmoothScroller} that has: a) Custom control over the speed of scrolls. b)
+     * Scrolling snaps to start. All of our scrolling logic depends on that. c) Keeps track of some
+     * state of the current scroll so that can aid in things like the scrollbar calculations.
+     */
+    private final class CarSmoothScroller extends LinearSmoothScroller {
+        /** This value (150) was hand tuned by UX for what felt right. * */
+        private static final float MILLISECONDS_PER_INCH = 150f;
+        /** This value (0.45) was hand tuned by UX for what felt right. * */
+        private static final float DECELERATION_TIME_DIVISOR = 0.45f;
+
+        /** This value (1.8) was hand tuned by UX for what felt right. * */
+        private final Interpolator mInterpolator = new DecelerateInterpolator(1.8f);
+
+        private final int mTargetPosition;
+
+        CarSmoothScroller(Context context, int targetPosition) {
+            super(context);
+            mTargetPosition = targetPosition;
+        }
+
+        @Override
+        public PointF computeScrollVectorForPosition(int i) {
+            if (getChildCount() == 0) {
+                return null;
+            }
+            final int firstChildPos = getPosition(getChildAt(getFirstFullyVisibleChildIndex()));
+            final int direction = (mTargetPosition < firstChildPos) ? -1 : 1;
+            return new PointF(0, direction);
+        }
+
+        @Override
+        protected int getVerticalSnapPreference() {
+            // This is key for most of the scrolling logic that guarantees that scrolling
+            // will settle with a view aligned to the top.
+            return LinearSmoothScroller.SNAP_TO_START;
+        }
+
+        @Override
+        protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
+            int dy = calculateDyToMakeVisible(targetView, SNAP_TO_START);
+            if (dy == 0) {
+                if (Log.isLoggable(TAG, Log.DEBUG)) {
+                    Log.d(TAG, "Scroll distance is 0");
+                }
+                return;
+            }
+
+            final int time = calculateTimeForDeceleration(dy);
+            if (time > 0) {
+                action.update(0, -dy, time, mInterpolator);
+            }
+        }
+
+        @Override
+        protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
+            return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
+        }
+
+        @Override
+        protected int calculateTimeForDeceleration(int dx) {
+            return (int) Math.ceil(calculateTimeForScrolling(dx) / DECELERATION_TIME_DIVISOR);
+        }
+
+        @Override
+        public int getTargetPosition() {
+            return mTargetPosition;
+        }
+    }
+
+    /**
+     * Animation that translates a view by the specified amount. Used for card flying off the screen
+     * effect.
+     */
+    private static class TranslateAnimation extends Animation {
+        public float verticalOffset;
+
+        @Override
+        protected void applyTransformation(float interpolatedTime, Transformation t) {
+            super.applyTransformation(interpolatedTime, t);
+            t.getMatrix().setTranslate(0, verticalOffset);
+        }
+    }
+}
diff --git a/car/src/main/java/android/support/car/widget/CarRecyclerView.java b/car/src/main/java/android/support/car/widget/CarRecyclerView.java
new file mode 100644
index 0000000..edc3241
--- /dev/null
+++ b/car/src/main/java/android/support/car/widget/CarRecyclerView.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2017 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 android.support.car.widget;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.annotation.NonNull;
+import android.support.v7.widget.RecyclerView;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+
+/**
+ * Custom {@link RecyclerView} that helps {@link CarLayoutManager} properly fling and paginate.
+ *
+ * <p>It also has the ability to fade children as they scroll off screen that can be set with {@link
+ * #setFadeLastItem(boolean)}.
+ */
+public class CarRecyclerView extends RecyclerView {
+    private static final String PARCEL_CLASS = "android.os.Parcel";
+    private static final String SAVED_STATE_CLASS =
+            "android.support.v7.widget.RecyclerView.SavedState";
+    private boolean mFadeLastItem;
+    private Constructor<?> mSavedStateConstructor;
+    /**
+     * If the user releases the list with a velocity of 0, {@link #fling(int, int)} will not be
+     * called. However, we want to make sure that the list still snaps to the next page when this
+     * happens.
+     */
+    private boolean mWasFlingCalledForGesture;
+
+    public CarRecyclerView(Context context) {
+        this(context, null);
+    }
+
+    public CarRecyclerView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public CarRecyclerView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        setFocusableInTouchMode(false);
+        setFocusable(false);
+    }
+
+    @Override
+    protected void onRestoreInstanceState(Parcelable state) {
+        if (state.getClass().getClassLoader() != getClass().getClassLoader()) {
+            if (mSavedStateConstructor == null) {
+                mSavedStateConstructor = getSavedStateConstructor();
+            }
+            // Class loader mismatch, recreate from parcel.
+            Parcel obtain = Parcel.obtain();
+            state.writeToParcel(obtain, 0);
+            try {
+                Parcelable newState = (Parcelable) mSavedStateConstructor.newInstance(obtain);
+                super.onRestoreInstanceState(newState);
+            } catch (InstantiationException
+                    | IllegalAccessException
+                    | IllegalArgumentException
+                    | InvocationTargetException e) {
+                // Fail loudy here.
+                throw new RuntimeException(e);
+            }
+        } else {
+            super.onRestoreInstanceState(state);
+        }
+    }
+
+    @Override
+    public boolean fling(int velocityX, int velocityY) {
+        mWasFlingCalledForGesture = true;
+        return ((CarLayoutManager) getLayoutManager()).settleScrollForFling(this, velocityY);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent e) {
+        // We want the parent to handle all touch events. There's a lot going on there,
+        // and there is no reason to overwrite that functionality. If we do, bad things will happen.
+        final boolean ret = super.onTouchEvent(e);
+
+        int action = e.getActionMasked();
+        if (action == MotionEvent.ACTION_UP) {
+            if (!mWasFlingCalledForGesture) {
+                ((CarLayoutManager) getLayoutManager()).settleScrollForFling(this, 0);
+            }
+            mWasFlingCalledForGesture = false;
+        }
+
+        return ret;
+    }
+
+    @Override
+    public boolean drawChild(@NonNull Canvas canvas, @NonNull View child, long drawingTime) {
+        if (mFadeLastItem) {
+            float onScreen = 1f;
+            if ((child.getTop() < getBottom() && child.getBottom() > getBottom())) {
+                onScreen = ((float) (getBottom() - child.getTop())) / (float) child.getHeight();
+            } else if ((child.getTop() < getTop() && child.getBottom() > getTop())) {
+                onScreen = ((float) (child.getBottom() - getTop())) / (float) child.getHeight();
+            }
+            float alpha = 1 - (1 - onScreen) * (1 - onScreen);
+            fadeChild(child, alpha);
+        }
+
+        return super.drawChild(canvas, child, drawingTime);
+    }
+
+    public void setFadeLastItem(boolean fadeLastItem) {
+        mFadeLastItem = fadeLastItem;
+    }
+
+    /**
+     * Scrolls the contents of this {@link CarRecyclerView} up one page. A page is defined as the
+     * number of items that fit completely on the screen.
+     */
+    public void pageUp() {
+        CarLayoutManager lm = (CarLayoutManager) getLayoutManager();
+        int pageUpPosition = lm.getPageUpPosition();
+        if (pageUpPosition == -1) {
+            return;
+        }
+
+        smoothScrollToPosition(pageUpPosition);
+    }
+
+    /**
+     * Scrolls the contents of this {@link CarRecyclerView} down one page. A page is defined as the
+     * number of items that fit completely on the screen.
+     */
+    public void pageDown() {
+        CarLayoutManager lm = (CarLayoutManager) getLayoutManager();
+        int pageDownPosition = lm.getPageDownPosition();
+        if (pageDownPosition == -1) {
+            return;
+        }
+
+        smoothScrollToPosition(pageDownPosition);
+    }
+
+    /** Sets {@link #mSavedStateConstructor} to private SavedState constructor. */
+    private Constructor<?> getSavedStateConstructor() {
+        Class<?> savedStateClass = null;
+        // Find package private subclass RecyclerView$SavedState.
+        for (Class<?> c : RecyclerView.class.getDeclaredClasses()) {
+            if (c.getCanonicalName().equals(SAVED_STATE_CLASS)) {
+                savedStateClass = c;
+                break;
+            }
+        }
+        if (savedStateClass == null) {
+            throw new RuntimeException("RecyclerView$SavedState not found!");
+        }
+        // Find constructor that takes a {@link Parcel}.
+        for (Constructor<?> c : savedStateClass.getDeclaredConstructors()) {
+            Class<?>[] parameterTypes = c.getParameterTypes();
+            if (parameterTypes.length == 1
+                    && parameterTypes[0].getCanonicalName().equals(PARCEL_CLASS)) {
+                mSavedStateConstructor = c;
+                mSavedStateConstructor.setAccessible(true);
+                break;
+            }
+        }
+        if (mSavedStateConstructor == null) {
+            throw new RuntimeException("RecyclerView$SavedState constructor not found!");
+        }
+        return mSavedStateConstructor;
+    }
+
+    /**
+     * Fades child by alpha. If child is a {@link ViewGroup} then it will recursively fade its
+     * children instead.
+     */
+    private void fadeChild(@NonNull View child, float alpha) {
+        if (child instanceof ViewGroup) {
+            ViewGroup vg = (ViewGroup) child;
+            for (int i = 0; i < vg.getChildCount(); i++) {
+                fadeChild(vg.getChildAt(i), alpha);
+            }
+        } else {
+            child.setAlpha(alpha);
+        }
+    }
+}
diff --git a/car/src/main/java/android/support/car/widget/ColumnCardView.java b/car/src/main/java/android/support/car/widget/ColumnCardView.java
new file mode 100644
index 0000000..06f8553
--- /dev/null
+++ b/car/src/main/java/android/support/car/widget/ColumnCardView.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2017 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 android.support.car.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.support.car.R;
+import android.support.car.utils.ColumnCalculator;
+import android.support.v7.widget.CardView;
+import android.util.AttributeSet;
+import android.util.Log;
+
+/**
+ * A {@link CardView} whose width can be specified by the number of columns that it will span.
+ *
+ * <p>The {@code ColumnCardView} works similarly to a regular {@link CardView}, except that
+ * its {@code layout_width} attribute is always ignored. Instead, its width is automatically
+ * calculated based on a specified {@code columnSpan} attribute. Alternatively, a user can call
+ * {@link #setColumnSpan(int)}. If no column span is given, the {@code ColumnCardView} will have
+ * a default span value that it uses.
+ *
+ * <pre>
+ * &lt;android.support.car.widget.ColumnCardView
+ *     android:layout_width="wrap_content"
+ *     android:layout_height="wrap_content"
+ *     app:columnSpan="4" /&gt;
+ * </pre>
+ *
+ * @see ColumnCalculator
+ */
+public final class ColumnCardView extends CardView {
+    private static final String TAG = "ColumnCardView";
+
+    private ColumnCalculator mColumnCalculator;
+    private int mColumnSpan;
+
+    public ColumnCardView(Context context) {
+        super(context);
+        init(context, null, 0 /* defStyleAttrs */);
+    }
+
+    public ColumnCardView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(context, attrs, 0 /* defStyleAttrs */);
+    }
+
+    public ColumnCardView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        init(context, attrs, defStyleAttr);
+    }
+
+    private void init(Context context, AttributeSet attrs, int defStyleAttrs) {
+        mColumnCalculator = ColumnCalculator.getInstance(context);
+
+        int defaultColumnSpan = getResources().getInteger(
+                R.integer.column_card_default_column_span);
+
+        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ColumnCardView,
+                defStyleAttrs, 0 /* defStyleRes */);
+        mColumnSpan = ta.getInteger(R.styleable.ColumnCardView_columnSpan, defaultColumnSpan);
+        ta.recycle();
+
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, "Column span: " + mColumnSpan);
+        }
+    }
+
+    @Override
+    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        // Override any specified width so that the width is one that is calculated based on
+        // column and gutter span.
+        int width = mColumnCalculator.getSizeForColumnSpan(mColumnSpan);
+        super.onMeasure(
+                MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
+                heightMeasureSpec);
+    }
+
+    /**
+     * Sets the number of columns that this {@code ColumnCardView} will span. The given span is
+     * ignored if it is less than 0 or greater than the number of columns that fit on screen.
+     *
+     * @param columnSpan The number of columns this {@code ColumnCardView} will span across.
+     */
+    public void setColumnSpan(int columnSpan) {
+        if (columnSpan <= 0 || columnSpan > mColumnCalculator.getNumOfColumns()) {
+            return;
+        }
+
+        mColumnSpan = columnSpan;
+        requestLayout();
+    }
+
+    /**
+     * Returns the currently number of columns that this {@code ColumnCardView} spans.
+     *
+     * @return The number of columns this {@code ColumnCardView} spans across.
+     */
+    public int getColumnSpan() {
+        return mColumnSpan;
+    }
+}
diff --git a/car/src/main/java/android/support/car/widget/DayNightStyle.java b/car/src/main/java/android/support/car/widget/DayNightStyle.java
new file mode 100644
index 0000000..ff5a1b3
--- /dev/null
+++ b/car/src/main/java/android/support/car/widget/DayNightStyle.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2017 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 android.support.car.widget;
+
+import android.support.annotation.IntDef;
+
+/**
+ * Specifies how the system UI should respond to day/night mode events.
+ *
+ * <p>By default, the Android Auto system UI assumes the app content background is light during the
+ * day and dark during the night. The system UI updates the foreground color (such as status bar
+ * icon colors) to be dark during day mode and light during night mode. By setting the
+ * DayNightStyle, the app can specify how the system should respond to a day/night mode event. For
+ * example, if the app has a dark content background for both day and night time, the app can tell
+ * the system to use {@link #FORCE_NIGHT} style so the foreground color is locked to light color for
+ * both cases.
+ *
+ * <p>Note: Not all system UI elements can be customized with a DayNightStyle.
+ */
+@IntDef({
+        DayNightStyle.AUTO,
+        DayNightStyle.AUTO_INVERSE,
+        DayNightStyle.FORCE_NIGHT,
+        DayNightStyle.FORCE_DAY,
+})
+public @interface DayNightStyle {
+    /**
+     * Sets the foreground color to be automatically changed based on day/night mode, assuming the
+     * app content background is light during the day and dark during the night.
+     *
+     * <p>This is the default behavior.
+     */
+    int AUTO = 0;
+
+    /**
+     * Sets the foreground color to be automatically changed based on day/night mode, assuming the
+     * app content background is dark during the day and light during the night.
+     */
+    int AUTO_INVERSE = 1;
+
+    /**
+     * Sets the foreground color to be locked to the night version, which assumes the app content
+     * background is always dark during both day and night.
+     */
+    int FORCE_NIGHT = 2;
+
+    /**
+     * Sets the foreground color to be locked to the day version, which assumes the app content
+     * background is always light during both day and night.
+     */
+    int FORCE_DAY = 3;
+}
diff --git a/car/src/main/java/android/support/car/widget/PagedListView.java b/car/src/main/java/android/support/car/widget/PagedListView.java
new file mode 100644
index 0000000..4652700
--- /dev/null
+++ b/car/src/main/java/android/support/car/widget/PagedListView.java
@@ -0,0 +1,839 @@
+/*
+ * Copyright (C) 2017 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 android.support.car.widget;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.os.Handler;
+import android.support.annotation.IdRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
+import android.support.annotation.UiThread;
+import android.support.car.R;
+import android.support.v7.widget.RecyclerView;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.FrameLayout;
+
+/**
+ * Custom {@link android.support.v7.widget.RecyclerView} that displays a list of items that
+ * resembles a {@link android.widget.ListView} but also has page up and page down arrows on the
+ * right side.
+ */
+public class PagedListView extends FrameLayout {
+    /** Default maximum number of clicks allowed on a list */
+    public static final int DEFAULT_MAX_CLICKS = 6;
+
+    /**
+     * The amount of time after settling to wait before autoscrolling to the next page when the user
+     * holds down a pagination button.
+     */
+    protected static final int PAGINATION_HOLD_DELAY_MS = 400;
+
+    private static final String TAG = "PagedListView";
+    private static final int INVALID_RESOURCE_ID = -1;
+
+    protected final CarRecyclerView mRecyclerView;
+    protected final CarLayoutManager mLayoutManager;
+    protected final Handler mHandler = new Handler();
+    private final boolean mScrollBarEnabled;
+    private final PagedScrollBarView mScrollBarView;
+
+    private int mRowsPerPage = -1;
+    protected RecyclerView.Adapter<? extends RecyclerView.ViewHolder> mAdapter;
+
+    /** Maximum number of pages to show. Values < 0 show all pages. */
+    private int mMaxPages = -1;
+
+    protected OnScrollListener mOnScrollListener;
+
+    /** Number of visible rows per page */
+    private int mDefaultMaxPages = DEFAULT_MAX_CLICKS;
+
+    /** Used to check if there are more items added to the list. */
+    private int mLastItemCount = 0;
+
+    private boolean mNeedsFocus;
+
+    /**
+     * Interface for a {@link android.support.v7.widget.RecyclerView.Adapter} to cap the number of
+     * items.
+     *
+     * <p>NOTE: it is still up to the adapter to use maxItems in {@link
+     * android.support.v7.widget.RecyclerView.Adapter#getItemCount()}.
+     *
+     * <p>the recommended way would be with:
+     *
+     * <pre>{@code
+     * {@literal@}Override
+     * public int getItemCount() {
+     *   return Math.min(super.getItemCount(), mMaxItems);
+     * }
+     * }</pre>
+     */
+    public interface ItemCap {
+        /**
+         * A value to pass to {@link #setMaxItems(int)} that indicates there should be no limit.
+         */
+        int UNLIMITED = -1;
+
+        /**
+         * Sets the maximum number of items available in the adapter. A value less than '0' means
+         * the list should not be capped.
+         */
+        void setMaxItems(int maxItems);
+    }
+
+    /**
+     * Interface for a {@link android.support.v7.widget.RecyclerView.Adapter} to set the position
+     * offset for the adapter to load the data.
+     *
+     * <p>For example in the adapter, if the positionOffset is 20, then for position 0 it will show
+     * the item in position 20 instead, for position 1 it will show the item in position 21 instead
+     * and so on.
+     */
+    // TODO(b/28003781): ItemPositionOffset and ItemCap interfaces should be merged once
+    // we enable AlphaJump outside drawer.
+    public interface ItemPositionOffset {
+        /** Sets the position offset for the adapter. */
+        void setPositionOffset(int positionOffset);
+    }
+
+    public PagedListView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0 /*defStyleAttrs*/, 0 /*defStyleRes*/);
+    }
+
+    public PagedListView(Context context, AttributeSet attrs, int defStyleAttrs) {
+        this(context, attrs, defStyleAttrs, 0 /*defStyleRes*/);
+    }
+
+    public PagedListView(Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes) {
+        this(context, attrs, defStyleAttrs, defStyleRes, 0);
+    }
+
+    public PagedListView(
+            Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes, int layoutId) {
+        super(context, attrs, defStyleAttrs, defStyleRes);
+        if (layoutId == 0) {
+            layoutId = R.layout.car_paged_recycler_view;
+        }
+        LayoutInflater.from(context).inflate(layoutId, this /*root*/, true /*attachToRoot*/);
+
+        TypedArray a = context.obtainStyledAttributes(
+                attrs, R.styleable.PagedListView, defStyleAttrs, defStyleRes);
+        mRecyclerView = (CarRecyclerView) findViewById(R.id.recycler_view);
+        boolean fadeLastItem = a.getBoolean(R.styleable.PagedListView_fadeLastItem, false);
+        mRecyclerView.setFadeLastItem(fadeLastItem);
+        boolean offsetRows = a.getBoolean(R.styleable.PagedListView_offsetRows, false);
+
+        mMaxPages = getDefaultMaxPages();
+
+        mLayoutManager = new CarLayoutManager(context);
+        mLayoutManager.setOffsetRows(offsetRows);
+        mRecyclerView.setLayoutManager(mLayoutManager);
+        mRecyclerView.setOnScrollListener(mRecyclerViewOnScrollListener);
+        mRecyclerView.getRecycledViewPool().setMaxRecycledViews(0, 12);
+        mRecyclerView.setItemAnimator(new CarItemAnimator(mLayoutManager));
+
+        boolean offsetScrollBar = a.getBoolean(R.styleable.PagedListView_offsetScrollBar, false);
+        if (offsetScrollBar) {
+            MarginLayoutParams params = (MarginLayoutParams) mRecyclerView.getLayoutParams();
+            params.setMarginStart(getResources().getDimensionPixelSize(
+                    R.dimen.car_screen_margin_size));
+            params.setMarginEnd(
+                    a.getDimensionPixelSize(R.styleable.PagedListView_listEndMargin, 0));
+            mRecyclerView.setLayoutParams(params);
+        }
+
+        if (a.getBoolean(R.styleable.PagedListView_showPagedListViewDivider, true)) {
+            int dividerStartMargin = a.getDimensionPixelSize(
+                    R.styleable.PagedListView_dividerStartMargin, 0);
+            int dividerStartId = a.getResourceId(
+                    R.styleable.PagedListView_alignDividerStartTo, INVALID_RESOURCE_ID);
+            int dividerEndId = a.getResourceId(
+                    R.styleable.PagedListView_alignDividerEndTo, INVALID_RESOURCE_ID);
+
+            mRecyclerView.addItemDecoration(new DividerDecoration(context, dividerStartMargin,
+                    dividerStartId, dividerEndId));
+        }
+
+        // Set this to true so that this view consumes clicks events and views underneath
+        // don't receive this click event. Without this it's possible to click places in the
+        // view that don't capture the event, and as a result, elements visually hidden consume
+        // the event.
+        setClickable(true);
+
+        // Set focusable false explicitly to handle the behavior change in Android O where
+        // clickable view becomes focusable by default.
+        setFocusable(false);
+
+        mScrollBarEnabled = a.getBoolean(R.styleable.PagedListView_scrollBarEnabled, true);
+        mScrollBarView = (PagedScrollBarView) findViewById(R.id.paged_scroll_view);
+        mScrollBarView.setPaginationListener(
+                new PagedScrollBarView.PaginationListener() {
+                    @Override
+                    public void onPaginate(int direction) {
+                        if (direction == PagedScrollBarView.PaginationListener.PAGE_UP) {
+                            mRecyclerView.pageUp();
+                            if (mOnScrollListener != null) {
+                                mOnScrollListener.onScrollUpButtonClicked();
+                            }
+                        } else if (direction == PagedScrollBarView.PaginationListener.PAGE_DOWN) {
+                            mRecyclerView.pageDown();
+                            if (mOnScrollListener != null) {
+                                mOnScrollListener.onScrollDownButtonClicked();
+                            }
+                        } else {
+                            Log.e(TAG, "Unknown pagination direction (" + direction + ")");
+                        }
+                    }
+                });
+
+        mScrollBarView.setVisibility(mScrollBarEnabled ? VISIBLE : GONE);
+
+        // Modify the layout the Scroll Bar is not visible.
+        if (!mScrollBarEnabled) {
+            MarginLayoutParams params = (MarginLayoutParams) mRecyclerView.getLayoutParams();
+            params.setMarginStart(0);
+            mRecyclerView.setLayoutParams(params);
+        }
+
+        setDayNightStyle(DayNightStyle.AUTO);
+        a.recycle();
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        mHandler.removeCallbacks(mUpdatePaginationRunnable);
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent e) {
+        if (e.getAction() == MotionEvent.ACTION_DOWN) {
+            // The user has interacted with the list using touch. All movements will now paginate
+            // the list.
+            mLayoutManager.setRowOffsetMode(CarLayoutManager.ROW_OFFSET_MODE_PAGE);
+        }
+        return super.onInterceptTouchEvent(e);
+    }
+
+    @Override
+    public void requestChildFocus(View child, View focused) {
+        super.requestChildFocus(child, focused);
+        // The user has interacted with the list using the controller. Movements through the list
+        // will now be one row at a time.
+        mLayoutManager.setRowOffsetMode(CarLayoutManager.ROW_OFFSET_MODE_INDIVIDUAL);
+    }
+
+    /**
+     * Returns the position of the given View in the list.
+     *
+     * @param v The View to check for.
+     * @return The position or -1 if the given View is {@code null} or not in the list.
+     */
+    public int positionOf(@Nullable View v) {
+        if (v == null || v.getParent() != mRecyclerView) {
+            return -1;
+        }
+        return mLayoutManager.getPosition(v);
+    }
+
+    private void scroll(int direction) {
+        View focusedView = mRecyclerView.getFocusedChild();
+        if (focusedView != null) {
+            int position = mLayoutManager.getPosition(focusedView);
+            int newPosition =
+                    Math.max(Math.min(position + direction, mLayoutManager.getItemCount() - 1), 0);
+            if (newPosition != position) {
+                // newPosition/position are adapter positions.
+                // Convert to layout position by subtracting adapter position of view at layout
+                // position 0.
+                View childAt = mRecyclerView.getChildAt(
+                        newPosition - mLayoutManager.getPosition(mLayoutManager.getChildAt(0)));
+                if (childAt != null) {
+                    childAt.requestFocus();
+                }
+            }
+        }
+    }
+
+    private boolean canScroll(int direction) {
+        View focusedView = mRecyclerView.getFocusedChild();
+        if (focusedView != null) {
+            int position = mLayoutManager.getPosition(focusedView);
+            int newPosition =
+                    Math.max(Math.min(position + direction, mLayoutManager.getItemCount() - 1), 0);
+            if (newPosition != position) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @NonNull
+    public CarRecyclerView getRecyclerView() {
+        return mRecyclerView;
+    }
+
+    /**
+     * Scrolls to the given position in the PagedListView.
+     *
+     * @param position The position in the list to scroll to.
+     */
+    public void scrollToPosition(int position) {
+        mLayoutManager.scrollToPosition(position);
+
+        // Sometimes #scrollToPosition doesn't change the scroll state so we need to make sure
+        // the pagination arrows actually get updated. See b/http://b/15801119
+        mHandler.post(mUpdatePaginationRunnable);
+    }
+
+    /**
+     * Sets the adapter for the list.
+     *
+     * <p>It <em>must</em> implement {@link ItemCap}, otherwise, will throw an {@link
+     * IllegalArgumentException}.
+     */
+    public void setAdapter(
+            @NonNull RecyclerView.Adapter<? extends RecyclerView.ViewHolder> adapter) {
+        if (!(adapter instanceof ItemCap)) {
+            throw new IllegalArgumentException("ERROR: adapter ["
+                    + adapter.getClass().getCanonicalName() + "] MUST implement ItemCap");
+        }
+
+        mAdapter = adapter;
+        mRecyclerView.setAdapter(adapter);
+        updateMaxItems();
+    }
+
+    /** @hide */
+    @RestrictTo(LIBRARY_GROUP)
+    @NonNull
+    public CarLayoutManager getLayoutManager() {
+        return mLayoutManager;
+    }
+
+    @Nullable
+    @SuppressWarnings("unchecked")
+    public RecyclerView.Adapter<? extends RecyclerView.ViewHolder> getAdapter() {
+        return mRecyclerView.getAdapter();
+    }
+
+    /**
+     * Sets the maximum number of the pages that can be shown in the PagedListView. The size of a
+     * page is  defined as the number of items that fit completely on the screen at once.
+     *
+     * @param maxPages The maximum number of pages that fit on the screen. Should be positive.
+     */
+    public void setMaxPages(int maxPages) {
+        if (maxPages < 0) {
+            return;
+        }
+        mMaxPages = maxPages;
+        updateMaxItems();
+    }
+
+    /**
+     * Returns the maximum number of pages allowed in the PagedListView. This number is set by
+     * {@link #setMaxPages(int)}. If that method has not been called, then this value should match
+     * the default value.
+     *
+     * @return The maximum number of pages to be shown.
+     */
+    public int getMaxPages() {
+        return mMaxPages;
+    }
+
+    /**
+     * Gets the number of rows per page. Default value of mRowsPerPage is -1. If the first child of
+     * CarLayoutManager is null or the height of the first child is 0, it will return 1.
+     */
+    public int getRowsPerPage() {
+        return mRowsPerPage;
+    }
+
+    /** Resets the maximum number of pages to be shown to be the default. */
+    public void resetMaxPages() {
+        mMaxPages = getDefaultMaxPages();
+        updateMaxItems();
+    }
+
+    /**
+     * @return The position of first visible child in the list. -1 will be returned if there is no
+     *     child.
+     */
+    public int getFirstFullyVisibleChildPosition() {
+        return mLayoutManager.getFirstFullyVisibleChildPosition();
+    }
+
+    /**
+     * @return The position of last visible child in the list. -1 will be returned if there is no
+     *     child.
+     */
+    public int getLastFullyVisibleChildPosition() {
+        return mLayoutManager.getLastFullyVisibleChildPosition();
+    }
+
+    /**
+     * Adds an {@link android.support.v7.widget.RecyclerView.ItemDecoration} to this PagedListView.
+     *
+     * @param decor The decoration to add.
+     * @see RecyclerView#addItemDecoration(RecyclerView.ItemDecoration)
+     */
+    public void addItemDecoration(@NonNull RecyclerView.ItemDecoration decor) {
+        mRecyclerView.addItemDecoration(decor);
+    }
+
+    /**
+     * Removes the given {@link android.support.v7.widget.RecyclerView.ItemDecoration} from this
+     * PagedListView.
+     *
+     * <p>The decoration will function the same as the item decoration for a {@link RecyclerView}.
+     *
+     * @param decor The decoration to remove.
+     * @see RecyclerView#removeItemDecoration(RecyclerView.ItemDecoration)
+     */
+    public void removeItemDecoration(@NonNull RecyclerView.ItemDecoration decor) {
+        mRecyclerView.removeItemDecoration(decor);
+    }
+
+    /**
+     * Adds an {@link android.support.v7.widget.RecyclerView.OnItemTouchListener} to this
+     * PagedListView.
+     *
+     * <p>The listener will function the same as the listener for a regular {@link RecyclerView}.
+     *
+     * @param touchListener The touch listener to add.
+     * @see RecyclerView#addOnItemTouchListener(RecyclerView.OnItemTouchListener)
+     */
+    public void addOnItemTouchListener(@NonNull RecyclerView.OnItemTouchListener touchListener) {
+        mRecyclerView.addOnItemTouchListener(touchListener);
+    }
+
+    /**
+     * Removes the given {@link android.support.v7.widget.RecyclerView.OnItemTouchListener} from
+     * the PagedListView.
+     *
+     * @param touchListener The touch listener to remove.
+     * @see RecyclerView#removeOnItemTouchListener(RecyclerView.OnItemTouchListener)
+     */
+    public void removeOnItemTouchListener(@NonNull RecyclerView.OnItemTouchListener touchListener) {
+        mRecyclerView.removeOnItemTouchListener(touchListener);
+    }
+    /**
+     * Sets how this {@link PagedListView} responds to day/night configuration changes. By
+     * default, the PagedListView is darker in the day and lighter at night.
+     *
+     * @param dayNightStyle A value from {@link DayNightStyle}.
+     * @see DayNightStyle
+     */
+    public void setDayNightStyle(@DayNightStyle int dayNightStyle) {
+        // Update the scrollbar
+        mScrollBarView.setDayNightStyle(dayNightStyle);
+
+        int decorCount = mRecyclerView.getItemDecorationCount();
+        for (int i = 0; i < decorCount; i++) {
+            RecyclerView.ItemDecoration decor = mRecyclerView.getItemDecorationAt(i);
+            if (decor instanceof DividerDecoration) {
+                ((DividerDecoration) decor).updateDividerColor();
+            }
+        }
+    }
+
+    /**
+     * Returns the {@link android.support.v7.widget.RecyclerView.ViewHolder} that corresponds to the
+     * last child in the PagedListView that is fully visible.
+     *
+     * @return The corresponding ViewHolder or {@code null} if none exists.
+     */
+    @Nullable
+    public RecyclerView.ViewHolder getLastViewHolder() {
+        View lastFullyVisible = mLayoutManager.getLastFullyVisibleChild();
+        if (lastFullyVisible == null) {
+            return null;
+        }
+        int lastFullyVisibleAdapterPosition = mLayoutManager.getPosition(lastFullyVisible);
+        RecyclerView.ViewHolder lastViewHolder = getRecyclerView()
+                .findViewHolderForAdapterPosition(lastFullyVisibleAdapterPosition + 1);
+        // We want to get the very last ViewHolder in the list, even if it's only fully visible
+        // If it doesn't exist, return the last fully visible ViewHolder.
+        if (lastViewHolder == null) {
+            lastViewHolder = getRecyclerView()
+                    .findViewHolderForAdapterPosition(lastFullyVisibleAdapterPosition);
+        }
+        return lastViewHolder;
+    }
+
+    /**
+     * Sets the {@link OnScrollListener} that will be notified of scroll events within the
+     * PagedListView.
+     *
+     * @param listener The scroll listener to set.
+     */
+    public void setOnScrollListener(OnScrollListener listener) {
+        mOnScrollListener = listener;
+        mLayoutManager.setOnScrollListener(mOnScrollListener);
+    }
+
+    /** Returns the page the given position is on, starting with page 0. */
+    public int getPage(int position) {
+        if (mRowsPerPage == -1) {
+            return -1;
+        }
+        if (mRowsPerPage == 0) {
+            return 0;
+        }
+        return position / mRowsPerPage;
+    }
+
+    /**
+     * Sets the default number of pages that this PagedListView is limited to.
+     *
+     * @param newDefault The default number of pages. Should be positive.
+     */
+    public void setDefaultMaxPages(int newDefault) {
+        if (newDefault < 0) {
+            return;
+        }
+        mDefaultMaxPages = newDefault;
+    }
+
+    /** Returns the default number of pages the list should have */
+    protected int getDefaultMaxPages() {
+        // assume list shown in response to a click, so, reduce number of clicks by one
+        return mDefaultMaxPages - 1;
+    }
+
+    @Override
+    public void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        // if a late item is added to the top of the layout after the layout is stabilized, causing
+        // the former top item to be pushed to the 2nd page, the focus will still be on the former
+        // top item. Since our car layout manager tries to scroll the viewport so that the focused
+        // item is visible, the view port will be on the 2nd page. That means the newly added item
+        // will not be visible, on the first page.
+
+        // what we want to do is: if the formerly focused item is the first one in the list, any
+        // item added above it will make the focus to move to the new first item.
+        // if the focus is not on the formerly first item, then we don't need to do anything. Let
+        // the layout manager do the job and scroll the viewport so the currently focused item
+        // is visible.
+
+        // we need to calculate whether we want to request focus here, before the super call,
+        // because after the super call, the first born might be changed.
+        View focusedChild = mLayoutManager.getFocusedChild();
+        View firstBorn = mLayoutManager.getChildAt(0);
+
+        super.onLayout(changed, left, top, right, bottom);
+
+        if (mAdapter != null) {
+            int itemCount = mAdapter.getItemCount();
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, String.format(
+                        "onLayout hasFocus: %s, mLastItemCount: %s, itemCount: %s, "
+                                + "focusedChild: %s, firstBorn: %s, isInTouchMode: %s, "
+                                + "mNeedsFocus: %s",
+                        hasFocus(),
+                        mLastItemCount,
+                        itemCount,
+                        focusedChild,
+                        firstBorn,
+                        isInTouchMode(),
+                        mNeedsFocus));
+            }
+            updateMaxItems();
+            // This is a workaround for missing focus because isInTouchMode() is not always
+            // returning the right value.
+            // This is okay for the Engine release since focus is always showing.
+            // However, in Tala and Fender, we want to show focus only when the user uses
+            // hardware controllers, so we need to revisit this logic. b/22990605.
+            if (mNeedsFocus && itemCount > 0) {
+                if (focusedChild == null) {
+                    requestFocus();
+                }
+                mNeedsFocus = false;
+            }
+            if (itemCount > mLastItemCount && focusedChild == firstBorn) {
+                requestFocus();
+            }
+            mLastItemCount = itemCount;
+        }
+        // We need to update the scroll buttons after layout has happened.
+        // Determining if a scrollbar is necessary requires looking at the layout of the child
+        // views. Therefore, this determination can only be done after layout has happened.
+        // Note: don't animate here to prevent b/26849677
+        updatePaginationButtons(false /*animate*/);
+    }
+
+    /**
+     * Returns the View at the given position within the list.
+     *
+     * @param position A position within the list.
+     * @return The View or {@code null} if no View exists at the given position.
+     */
+    @Nullable
+    public View findViewByPosition(int position) {
+        return mLayoutManager.findViewByPosition(position);
+    }
+
+    /**
+     * Determines if scrollbar should be visible or not and shows/hides it accordingly. If this is
+     * being called as a result of adapter changes, it should be called after the new layout has
+     * been calculated because the method of determining scrollbar visibility uses the current
+     * layout. If this is called after an adapter change but before the new layout, the visibility
+     * determination may not be correct.
+     *
+     * @param animate {@code true} if the scrollbar should animate to its new position.
+     *                {@code false} if no animation is used
+     */
+    protected void updatePaginationButtons(boolean animate) {
+        if (!mScrollBarEnabled) {
+            // Don't change the visibility of the ScrollBar unless it's enabled.
+            return;
+        }
+
+        if ((mLayoutManager.isAtTop() && mLayoutManager.isAtBottom())
+                || mLayoutManager.getItemCount() == 0) {
+            mScrollBarView.setVisibility(View.INVISIBLE);
+        } else {
+            mScrollBarView.setVisibility(View.VISIBLE);
+        }
+        mScrollBarView.setUpEnabled(shouldEnablePageUpButton());
+        mScrollBarView.setDownEnabled(shouldEnablePageDownButton());
+
+        mScrollBarView.setParameters(
+                mRecyclerView.computeVerticalScrollRange(),
+                mRecyclerView.computeVerticalScrollOffset(),
+                mRecyclerView.computeVerticalScrollExtent(),
+                animate);
+        invalidate();
+    }
+
+    protected boolean shouldEnablePageUpButton() {
+        return !mLayoutManager.isAtTop();
+    }
+
+    protected boolean shouldEnablePageDownButton() {
+        return !mLayoutManager.isAtBottom();
+    }
+
+    @UiThread
+    protected void updateMaxItems() {
+        if (mAdapter == null) {
+            return;
+        }
+
+        final int originalCount = mAdapter.getItemCount();
+        updateRowsPerPage();
+        ((ItemCap) mAdapter).setMaxItems(calculateMaxItemCount());
+        final int newCount = mAdapter.getItemCount();
+        if (newCount == originalCount) {
+            return;
+        }
+
+        if (newCount < originalCount) {
+            mAdapter.notifyItemRangeRemoved(newCount, originalCount - newCount);
+        } else {
+            mAdapter.notifyItemRangeInserted(originalCount, newCount - originalCount);
+        }
+    }
+
+    protected int calculateMaxItemCount() {
+        final View firstChild = mLayoutManager.getChildAt(0);
+        if (firstChild == null || firstChild.getHeight() == 0) {
+            return -1;
+        } else {
+            return (mMaxPages < 0) ? -1 : mRowsPerPage * mMaxPages;
+        }
+    }
+
+    /**
+     * Updates the rows number per current page, which is used for calculating how many items we
+     * want to show.
+     */
+    protected void updateRowsPerPage() {
+        final View firstChild = mLayoutManager.getChildAt(0);
+        if (firstChild == null || firstChild.getHeight() == 0) {
+            mRowsPerPage = 1;
+        } else {
+            mRowsPerPage = Math.max(1, (getHeight() - getPaddingTop()) / firstChild.getHeight());
+        }
+    }
+
+    private final RecyclerView.OnScrollListener mRecyclerViewOnScrollListener =
+            new RecyclerView.OnScrollListener() {
+                @Override
+                public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+                    if (mOnScrollListener != null) {
+                        mOnScrollListener.onScrolled(recyclerView, dx, dy);
+                        if (!mLayoutManager.isAtTop() && mLayoutManager.isAtBottom()) {
+                            mOnScrollListener.onReachBottom();
+                        } else if (mLayoutManager.isAtTop() || !mLayoutManager.isAtBottom()) {
+                            mOnScrollListener.onLeaveBottom();
+                        }
+                    }
+                    updatePaginationButtons(false);
+                }
+
+                @Override
+                public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
+                    if (mOnScrollListener != null) {
+                        mOnScrollListener.onScrollStateChanged(recyclerView, newState);
+                    }
+                    if (newState == RecyclerView.SCROLL_STATE_IDLE) {
+                        mHandler.postDelayed(mPaginationRunnable, PAGINATION_HOLD_DELAY_MS);
+                    }
+                }
+            };
+
+    protected final Runnable mPaginationRunnable =
+            new Runnable() {
+                @Override
+                public void run() {
+                    boolean upPressed = mScrollBarView.isUpPressed();
+                    boolean downPressed = mScrollBarView.isDownPressed();
+                    if (upPressed && downPressed) {
+                        return;
+                    }
+                    if (upPressed) {
+                        mRecyclerView.pageUp();
+                    } else if (downPressed) {
+                        mRecyclerView.pageDown();
+                    }
+                }
+            };
+
+    private final Runnable mUpdatePaginationRunnable =
+            new Runnable() {
+                @Override
+                public void run() {
+                    updatePaginationButtons(true /*animate*/);
+                }
+            };
+
+    /** Used to listen for {@code PagedListView} scroll events. */
+    public abstract static class OnScrollListener {
+        /** Called when menu reaches the bottom */
+        public void onReachBottom() {}
+        /** Called when menu leaves the bottom */
+        public void onLeaveBottom() {}
+        /** Called when scroll up button is clicked */
+        public void onScrollUpButtonClicked() {}
+        /** Called when scroll down button is clicked */
+        public void onScrollDownButtonClicked() {}
+        /** Called when scrolling to the previous page via up gesture */
+        public void onGestureUp() {}
+        /** Called when scrolling to the next page via down gesture */
+        public void onGestureDown() {}
+
+        /**
+         * Called when RecyclerView.OnScrollListener#onScrolled is called. See
+         * RecyclerView.OnScrollListener
+         */
+        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {}
+
+        /** See RecyclerView.OnScrollListener */
+        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {}
+
+        /** Called when the view scrolls up a page */
+        public void onPageUp() {}
+
+        /** Called when the view scrolls down a page */
+        public void onPageDown() {}
+    }
+
+    /**
+     * A {@link android.support.v7.widget.RecyclerView.ItemDecoration} that will draw a dividing
+     * line between each item in the RecyclerView that it is added to.
+     */
+    public static class DividerDecoration extends RecyclerView.ItemDecoration {
+        private final Context mContext;
+        private final Paint mPaint;
+        private final int mDividerHeight;
+        private final int mDividerStartMargin;
+        @IdRes private final int mDividerStartId;
+        @IdRes private final int mDvidierEndId;
+
+        /**
+         * @param dividerStartMargin The start offset of the dividing line. This offset will be
+         *     relative to {@code dividerStartId} if that value is given.
+         * @param dividerStartId A child view id whose starting edge will be used as the starting
+         *     edge of the dividing line. If this value is {@link #INVALID_RESOURCE_ID}, the top
+         *     container of each child view will be used.
+         * @param dividerEndId A child view id whose ending edge will be used as the starting edge
+         *     of the dividing lin.e If this value is {@link #INVALID_RESOURCE_ID}, then the top
+         *     container view of each child will be used.
+         */
+        private DividerDecoration(Context context, int dividerStartMargin,
+                @IdRes int dividerStartId, @IdRes int dividerEndId) {
+            mContext = context;
+            mDividerStartMargin = dividerStartMargin;
+            mDividerStartId = dividerStartId;
+            mDvidierEndId = dividerEndId;
+
+            Resources res = context.getResources();
+            mPaint = new Paint();
+            mPaint.setColor(res.getColor(R.color.car_list_divider));
+            mDividerHeight = res.getDimensionPixelSize(R.dimen.car_divider_height);
+        }
+
+        /** Updates the list divider color which may have changed due to a day night transition. */
+        public void updateDividerColor() {
+            mPaint.setColor(mContext.getResources().getColor(R.color.car_list_divider));
+        }
+
+        @Override
+        public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
+            for (int i = 0, childCount = parent.getChildCount(); i < childCount; i++) {
+                View container = parent.getChildAt(i);
+                View startChild =
+                        mDividerStartId != INVALID_RESOURCE_ID
+                                ? container.findViewById(mDividerStartId)
+                                : container;
+
+                View endChild =
+                        mDvidierEndId != INVALID_RESOURCE_ID
+                                ? container.findViewById(mDvidierEndId)
+                                : container;
+
+                if (startChild == null || endChild == null) {
+                    continue;
+                }
+
+                int left = mDividerStartMargin + startChild.getLeft();
+                int right = endChild.getRight();
+                int bottom = container.getBottom();
+                int top = bottom - mDividerHeight;
+
+                // Draw a divider line between each item. No need to draw the line for the last
+                // item.
+                if (i != childCount - 1) {
+                    c.drawRect(left, top, right, bottom, mPaint);
+                }
+            }
+        }
+    }
+}
diff --git a/car/src/main/java/android/support/car/widget/PagedScrollBarView.java b/car/src/main/java/android/support/car/widget/PagedScrollBarView.java
new file mode 100644
index 0000000..125b354
--- /dev/null
+++ b/car/src/main/java/android/support/car/widget/PagedScrollBarView.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright (C) 2017 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 android.support.car.widget;
+
+import android.content.Context;
+import android.graphics.PorterDuff;
+import android.support.car.R;
+import android.support.v4.content.ContextCompat;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+
+/** A custom view to provide list scroll behaviour -- up/down buttons and scroll indicator. */
+public class PagedScrollBarView extends FrameLayout
+        implements View.OnClickListener, View.OnLongClickListener {
+    private static final float BUTTON_DISABLED_ALPHA = 0.2f;
+
+    @DayNightStyle private int mDayNightStyle;
+
+    /** Listener for when the list should paginate. */
+    public interface PaginationListener {
+        int PAGE_UP = 0;
+        int PAGE_DOWN = 1;
+
+        /** Called when the linked view should be paged in the given direction */
+        void onPaginate(int direction);
+    }
+
+    private final ImageView mUpButton;
+    private final ImageView mDownButton;
+    private final ImageView mScrollThumb;
+    /** The "filler" view between the up and down buttons */
+    private final View mFiller;
+
+    private final Interpolator mPaginationInterpolator = new AccelerateDecelerateInterpolator();
+    private final int mMinThumbLength;
+    private final int mMaxThumbLength;
+    private PaginationListener mPaginationListener;
+
+    public PagedScrollBarView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0 /*defStyleAttrs*/, 0 /*defStyleRes*/);
+    }
+
+    public PagedScrollBarView(Context context, AttributeSet attrs, int defStyleAttrs) {
+        this(context, attrs, defStyleAttrs, 0 /*defStyleRes*/);
+    }
+
+    public PagedScrollBarView(
+            Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes) {
+        super(context, attrs, defStyleAttrs, defStyleRes);
+
+        LayoutInflater inflater =
+                (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        inflater.inflate(R.layout.car_paged_scrollbar_buttons, this /* root */,
+                true /* attachToRoot */);
+
+        mUpButton = (ImageView) findViewById(R.id.page_up);
+        mUpButton.setOnClickListener(this);
+        mUpButton.setOnLongClickListener(this);
+        mDownButton = (ImageView) findViewById(R.id.page_down);
+        mDownButton.setOnClickListener(this);
+        mDownButton.setOnLongClickListener(this);
+
+        mScrollThumb = (ImageView) findViewById(R.id.scrollbar_thumb);
+        mFiller = findViewById(R.id.filler);
+
+        mMinThumbLength = getResources().getDimensionPixelSize(R.dimen.min_thumb_height);
+        mMaxThumbLength = getResources().getDimensionPixelSize(R.dimen.max_thumb_height);
+    }
+
+    @Override
+    public void onClick(View v) {
+        dispatchPageClick(v);
+    }
+
+    @Override
+    public boolean onLongClick(View v) {
+        dispatchPageClick(v);
+        return true;
+    }
+
+    /**
+     * Sets the listener that will be notified when the up and down buttons have been pressed.
+     *
+     * @param listener The listener to set.
+     */
+    public void setPaginationListener(PaginationListener listener) {
+        mPaginationListener = listener;
+    }
+
+    /** Returns {@code true} if the "up" button is pressed */
+    public boolean isUpPressed() {
+        return mUpButton.isPressed();
+    }
+
+    /** Returns {@code true} if the "down" button is pressed */
+    public boolean isDownPressed() {
+        return mDownButton.isPressed();
+    }
+
+    /** Sets the range, offset and extent of the scroll bar. See {@link View}. */
+    public void setParameters(int range, int offset, int extent, boolean animate) {
+        // This method is where we take the computed parameters from the CarLayoutManager and
+        // render it within the specified constraints ({@link #mMaxThumbLength} and
+        // {@link #mMinThumbLength}).
+        final int size = mFiller.getHeight() - mFiller.getPaddingTop() - mFiller.getPaddingBottom();
+
+        int thumbLength = extent * size / range;
+        thumbLength = Math.max(Math.min(thumbLength, mMaxThumbLength), mMinThumbLength);
+
+        int thumbOffset = size - thumbLength;
+        if (isDownEnabled()) {
+            // We need to adjust the offset so that it fits into the possible space inside the
+            // filler with regarding to the constraints set by mMaxThumbLength and mMinThumbLength.
+            thumbOffset = (size - thumbLength) * offset / range;
+        }
+
+        // Sets the size of the thumb and request a redraw if needed.
+        final ViewGroup.LayoutParams lp = mScrollThumb.getLayoutParams();
+        if (lp.height != thumbLength) {
+            lp.height = thumbLength;
+            mScrollThumb.requestLayout();
+        }
+
+        moveY(mScrollThumb, thumbOffset, animate);
+    }
+
+    /**
+     * Sets how this {@link PagedScrollBarView} responds to day/night configuration changes. By
+     * default, the PagedScrollBarView is darker in the day and lighter at night.
+     *
+     * @param dayNightStyle A value from {@link DayNightStyle}.
+     * @see DayNightStyle
+     */
+    public void setDayNightStyle(@DayNightStyle int dayNightStyle) {
+        mDayNightStyle = dayNightStyle;
+        reloadColors();
+    }
+
+    /**
+     * Sets whether or not the up button on the scroll bar is clickable.
+     *
+     * @param enabled {@code true} if the up button is enabled.
+     */
+    public void setUpEnabled(boolean enabled) {
+        mUpButton.setEnabled(enabled);
+        mUpButton.setAlpha(enabled ? 1f : BUTTON_DISABLED_ALPHA);
+    }
+
+    /**
+     * Sets whether or not the down button on the scroll bar is clickable.
+     *
+     * @param enabled {@code true} if the down button is enabled.
+     */
+    public void setDownEnabled(boolean enabled) {
+        mDownButton.setEnabled(enabled);
+        mDownButton.setAlpha(enabled ? 1f : BUTTON_DISABLED_ALPHA);
+    }
+
+    /**
+     * Returns whether or not the down button on the scroll bar is clickable.
+     *
+     * @return {@code true} if the down button is enabled. {@code false} otherwise.
+     */
+    public boolean isDownEnabled() {
+        return mDownButton.isEnabled();
+    }
+
+    /** Reload the colors for the current {@link DayNightStyle}. */
+    private void reloadColors() {
+        int tint;
+        int thumbBackground;
+        int upDownBackgroundResId;
+
+        switch (mDayNightStyle) {
+            case DayNightStyle.AUTO:
+                tint = ContextCompat.getColor(getContext(), R.color.car_tint);
+                thumbBackground = ContextCompat.getColor(getContext(),
+                        R.color.car_scrollbar_thumb);
+                upDownBackgroundResId = R.drawable.car_pagination_background;
+                break;
+            case DayNightStyle.AUTO_INVERSE:
+                tint = ContextCompat.getColor(getContext(), R.color.car_tint_inverse);
+                thumbBackground = ContextCompat.getColor(getContext(),
+                        R.color.car_scrollbar_thumb_inverse);
+                upDownBackgroundResId = R.drawable.car_pagination_background_inverse;
+                break;
+            case DayNightStyle.FORCE_NIGHT:
+                tint = ContextCompat.getColor(getContext(), R.color.car_tint_light);
+                thumbBackground = ContextCompat.getColor(getContext(),
+                        R.color.car_scrollbar_thumb_light);
+                upDownBackgroundResId = R.drawable.car_pagination_background_night;
+                break;
+            case DayNightStyle.FORCE_DAY:
+                tint = ContextCompat.getColor(getContext(), R.color.car_tint_dark);
+                thumbBackground = ContextCompat.getColor(getContext(),
+                        R.color.car_scrollbar_thumb_dark);
+                upDownBackgroundResId = R.drawable.car_pagination_background_day;
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown DayNightStyle: " + mDayNightStyle);
+        }
+
+        mScrollThumb.setBackgroundColor(thumbBackground);
+
+        mUpButton.setColorFilter(tint, PorterDuff.Mode.SRC_IN);
+        mUpButton.setBackgroundResource(upDownBackgroundResId);
+
+        mDownButton.setColorFilter(tint, PorterDuff.Mode.SRC_IN);
+        mDownButton.setBackgroundResource(upDownBackgroundResId);
+    }
+
+    private void dispatchPageClick(View v) {
+        final PaginationListener listener = mPaginationListener;
+        if (listener == null) {
+            return;
+        }
+
+        int direction = v.getId() == R.id.page_up
+                ? PaginationListener.PAGE_UP
+                : PaginationListener.PAGE_DOWN;
+        listener.onPaginate(direction);
+    }
+
+    /** Moves the given view to the specified 'y' position. */
+    private void moveY(final View view, float newPosition, boolean animate) {
+        final int duration = animate ? 200 : 0;
+        view.animate()
+                .y(newPosition)
+                .setDuration(duration)
+                .setInterpolator(mPaginationInterpolator)
+                .start();
+    }
+}
diff --git a/car/tests/AndroidManifest.xml b/car/tests/AndroidManifest.xml
new file mode 100644
index 0000000..6c03ac3
--- /dev/null
+++ b/car/tests/AndroidManifest.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 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.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.support.car.widget.test">
+    <uses-sdk android:targetSdkVersion="${target-sdk-version}"/>
+
+    <application android:supportsRtl="true">
+        <activity android:name="android.support.car.widget.ColumnCardViewTestActivity"/>
+    </application>
+</manifest>
\ No newline at end of file
diff --git a/car/tests/NO_DOCS b/car/tests/NO_DOCS
new file mode 100644
index 0000000..bd77b1a
--- /dev/null
+++ b/car/tests/NO_DOCS
@@ -0,0 +1,17 @@
+# Copyright (C) 2017 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.
+
+Having this file, named NO_DOCS, in a directory will prevent
+Android javadocs from being generated for java files under
+the directory. This is especially useful for test projects.
\ No newline at end of file
diff --git a/car/tests/res/layout/activity_column_card_view.xml b/car/tests/res/layout/activity_column_card_view.xml
new file mode 100644
index 0000000..ad9c5e1
--- /dev/null
+++ b/car/tests/res/layout/activity_column_card_view.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 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.
+  -->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:car="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <android.support.car.widget.ColumnCardView
+        android:id="@+id/default_width_column_card"
+        android:layout_gravity="center"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content" />
+
+    <android.support.car.widget.ColumnCardView
+        car:columnSpan="2"
+        android:id="@+id/span_2_column_card"
+        android:layout_gravity="center"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content" />
+</LinearLayout>
\ No newline at end of file
diff --git a/car/tests/src/android/support/car/widget/ColumnCardViewTest.java b/car/tests/src/android/support/car/widget/ColumnCardViewTest.java
new file mode 100644
index 0000000..cb61caf
--- /dev/null
+++ b/car/tests/src/android/support/car/widget/ColumnCardViewTest.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2017 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 android.support.car.widget;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import android.support.car.test.R;
+import android.support.car.utils.ColumnCalculator;
+import android.support.test.annotation.UiThreadTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.ViewTreeObserver;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Instrumentation unit tests for {@link ColumnCardView}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class ColumnCardViewTest {
+    @Rule
+    public ActivityTestRule<ColumnCardViewTestActivity> mActivityRule =
+            new ActivityTestRule<>(ColumnCardViewTestActivity.class);
+
+    private ColumnCalculator mCalculator;
+    private ColumnCardViewTestActivity mActivity;
+
+    @Before
+    public void setUp() {
+        mActivity = mActivityRule.getActivity();
+        mCalculator = ColumnCalculator.getInstance(mActivity);
+    }
+
+    @Test
+    public void defaultCardWidthMatchesCalculation() {
+        ColumnCardView card = mActivity.findViewById(R.id.default_width_column_card);
+
+        assertEquals(mCalculator.getSizeForColumnSpan(mActivity.getResources().getInteger(
+                R.integer.column_card_default_column_span)),
+                card.getWidth());
+    }
+
+    @Test
+    public void customXmlColumnSpanMatchesCalculation() {
+        ColumnCardView card = mActivity.findViewById(R.id.span_2_column_card);
+
+        assertEquals(mCalculator.getSizeForColumnSpan(2), card.getWidth());
+    }
+
+    @UiThreadTest
+    @Test
+    public void settingColumnSpanMatchesCalculation() {
+        final int columnSpan = 4;
+        final ColumnCardView card = mActivity.findViewById(R.id.span_2_column_card);
+        assertNotEquals(columnSpan, card.getColumnSpan());
+
+        card.setColumnSpan(columnSpan);
+        // When card finishes layout, verify its updated width.
+        card.getViewTreeObserver().addOnGlobalLayoutListener(
+                new ViewTreeObserver.OnGlobalLayoutListener() {
+                    @Override
+                    public void onGlobalLayout() {
+                        assertEquals(mCalculator.getSizeForColumnSpan(columnSpan), card.getWidth());
+                    }
+                });
+    }
+
+    @UiThreadTest
+    @Test
+    public void nonPositiveColumnSpanIsIgnored() {
+        final ColumnCardView card = mActivity.findViewById(R.id.default_width_column_card);
+        final int original = card.getColumnSpan();
+
+        card.setColumnSpan(0);
+        // When card finishes layout, verify its width remains unchanged.
+        card.getViewTreeObserver().addOnGlobalLayoutListener(
+                new ViewTreeObserver.OnGlobalLayoutListener() {
+                    @Override
+                    public void onGlobalLayout() {
+                        assertEquals(mCalculator.getSizeForColumnSpan(original), card.getWidth());
+                    }
+                });
+    }
+}
diff --git a/car/tests/src/android/support/car/widget/ColumnCardViewTestActivity.java b/car/tests/src/android/support/car/widget/ColumnCardViewTestActivity.java
new file mode 100644
index 0000000..693e4a1
--- /dev/null
+++ b/car/tests/src/android/support/car/widget/ColumnCardViewTestActivity.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2017 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 android.support.car.widget;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.support.car.test.R;
+
+public class ColumnCardViewTestActivity extends Activity {
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_column_card_view);
+    }
+}
diff --git a/compat/api/current.txt b/compat/api/current.txt
index 96a94cb..5a87c03 100644
--- a/compat/api/current.txt
+++ b/compat/api/current.txt
@@ -678,6 +678,7 @@
     ctor public ShortcutInfoCompat.Builder(android.content.Context, java.lang.String);
     method public android.support.v4.content.pm.ShortcutInfoCompat build();
     method public android.support.v4.content.pm.ShortcutInfoCompat.Builder setActivity(android.content.ComponentName);
+    method public android.support.v4.content.pm.ShortcutInfoCompat.Builder setAlwaysBadged();
     method public android.support.v4.content.pm.ShortcutInfoCompat.Builder setDisabledMessage(java.lang.CharSequence);
     method public android.support.v4.content.pm.ShortcutInfoCompat.Builder setIcon(android.support.v4.graphics.drawable.IconCompat);
     method public android.support.v4.content.pm.ShortcutInfoCompat.Builder setIntent(android.content.Intent);
diff --git a/compat/build.gradle b/compat/build.gradle
index 1e5d847..54b108d 100644
--- a/compat/build.gradle
+++ b/compat/build.gradle
@@ -11,7 +11,9 @@
     androidTestImplementation libs.espresso_core,    { exclude module: 'support-annotations' }
     androidTestImplementation libs.mockito_core,     { exclude group: 'net.bytebuddy' } // DexMaker has it"s own MockMaker
     androidTestImplementation libs.dexmaker_mockito, { exclude group: 'net.bytebuddy' } // DexMaker has it"s own MockMaker
-    androidTestImplementation project(':support-testutils')
+    androidTestImplementation project(':support-testutils'), {
+        exclude group: 'com.android.support', module: 'support-compat'
+    }
 }
 
 android {
diff --git a/compat/src/main/java/android/support/v4/content/pm/ShortcutInfoCompat.java b/compat/src/main/java/android/support/v4/content/pm/ShortcutInfoCompat.java
index 3ae7470..63585e1 100644
--- a/compat/src/main/java/android/support/v4/content/pm/ShortcutInfoCompat.java
+++ b/compat/src/main/java/android/support/v4/content/pm/ShortcutInfoCompat.java
@@ -18,17 +18,20 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.PackageManager;
 import android.content.pm.ShortcutInfo;
+import android.graphics.drawable.Drawable;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.annotation.RequiresApi;
+import android.support.annotation.VisibleForTesting;
 import android.support.v4.graphics.drawable.IconCompat;
 import android.text.TextUtils;
 
 import java.util.Arrays;
 
 /**
- * Helper for accessing features in {@link android.content.pm.ShortcutInfo}.
+ * Helper for accessing features in {@link ShortcutInfo}.
  */
 public class ShortcutInfoCompat {
 
@@ -43,6 +46,7 @@
     private CharSequence mDisabledMessage;
 
     private IconCompat mIcon;
+    private boolean mIsAlwaysBadged;
 
     private ShortcutInfoCompat() { }
 
@@ -69,11 +73,26 @@
         return builder.build();
     }
 
+    @VisibleForTesting
     Intent addToIntent(Intent outIntent) {
         outIntent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, mIntents[mIntents.length - 1])
                 .putExtra(Intent.EXTRA_SHORTCUT_NAME, mLabel.toString());
         if (mIcon != null) {
-            mIcon.addToShortcutIntent(outIntent);
+            Drawable badge = null;
+            if (mIsAlwaysBadged) {
+                PackageManager pm = mContext.getPackageManager();
+                if (mActivity != null) {
+                    try {
+                        badge = pm.getActivityIcon(mActivity);
+                    } catch (PackageManager.NameNotFoundException e) {
+                        // Ignore
+                    }
+                }
+                if (badge == null) {
+                    badge = mContext.getApplicationInfo().loadIcon(pm);
+                }
+            }
+            mIcon.addToShortcutIntent(outIntent, badge);
         }
         return outIntent;
     }
@@ -250,7 +269,7 @@
          * on the launcher.
          *
          * @see ShortcutInfo#getActivity()
-         * @see android.content.pm.ShortcutInfo.Builder#setActivity(ComponentName)
+         * @see ShortcutInfo.Builder#setActivity(ComponentName)
          */
         @NonNull
         public Builder setActivity(@NonNull ComponentName activity) {
@@ -259,6 +278,23 @@
         }
 
         /**
+         * Badges the icon before passing it over to the Launcher.
+         * <p>
+         * Launcher automatically badges {@link ShortcutInfo}, so only the legacy shortcut icon,
+         * {@link Intent.ShortcutIconResource} is badged. This field is ignored when using
+         * {@link ShortcutInfo} on API 25 and above.
+         * <p>
+         * If the shortcut is associated with an activity, the activity icon is used as the badge,
+         * otherwise application icon is used.
+         *
+         * @see #setActivity(ComponentName)
+         */
+        public Builder setAlwaysBadged() {
+            mInfo.mIsAlwaysBadged = true;
+            return this;
+        }
+
+        /**
          * Creates a {@link ShortcutInfoCompat} instance.
          */
         @NonNull
diff --git a/compat/src/main/java/android/support/v4/graphics/drawable/IconCompat.java b/compat/src/main/java/android/support/v4/graphics/drawable/IconCompat.java
index a2ad67f..359c96b 100644
--- a/compat/src/main/java/android/support/v4/graphics/drawable/IconCompat.java
+++ b/compat/src/main/java/android/support/v4/graphics/drawable/IconCompat.java
@@ -18,6 +18,7 @@
 
 import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
 
+import android.app.ActivityManager;
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.Bitmap;
@@ -27,13 +28,17 @@
 import android.graphics.Matrix;
 import android.graphics.Paint;
 import android.graphics.Shader;
+import android.graphics.drawable.Drawable;
 import android.graphics.drawable.Icon;
 import android.net.Uri;
 import android.os.Build;
 import android.support.annotation.DrawableRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
 import android.support.annotation.RequiresApi;
 import android.support.annotation.RestrictTo;
 import android.support.annotation.VisibleForTesting;
+import android.support.v4.content.ContextCompat;
 
 /**
  * Helper for accessing features in {@link android.graphics.drawable.Icon}.
@@ -187,7 +192,8 @@
                 if (Build.VERSION.SDK_INT >= 26) {
                     return Icon.createWithAdaptiveBitmap((Bitmap) mObj1);
                 } else {
-                    return Icon.createWithBitmap(createLegacyIconFromAdaptiveIcon((Bitmap) mObj1));
+                    return Icon.createWithBitmap(
+                            createLegacyIconFromAdaptiveIcon((Bitmap) mObj1, false));
                 }
             case TYPE_RESOURCE:
                 return Icon.createWithResource((Context) mObj1, mInt1);
@@ -201,34 +207,74 @@
     }
 
     /**
+     * Use {@link #addToShortcutIntent(Intent, Drawable)} instead
      * @hide
      */
     @RestrictTo(LIBRARY_GROUP)
-    public void addToShortcutIntent(Intent outIntent) {
+    @Deprecated
+    public void addToShortcutIntent(@NonNull Intent outIntent) {
+        addToShortcutIntent(outIntent, null);
+    }
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public void addToShortcutIntent(@NonNull Intent outIntent, @Nullable Drawable badge) {
+        Bitmap icon;
         switch (mType) {
             case TYPE_BITMAP:
-                outIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON, (Bitmap) mObj1);
+                icon = (Bitmap) mObj1;
+                if (badge != null) {
+                    // Do not modify the original icon when applying a badge
+                    icon = icon.copy(icon.getConfig(), true);
+                }
                 break;
             case TYPE_ADAPTIVE_BITMAP:
-                outIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON,
-                        createLegacyIconFromAdaptiveIcon((Bitmap) mObj1));
+                icon = createLegacyIconFromAdaptiveIcon((Bitmap) mObj1, true);
                 break;
             case TYPE_RESOURCE:
-                outIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
-                        Intent.ShortcutIconResource.fromContext((Context) mObj1, mInt1));
+                if (badge == null) {
+                    outIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
+                            Intent.ShortcutIconResource.fromContext((Context) mObj1, mInt1));
+                    return;
+                } else {
+                    Context context = (Context) mObj1;
+                    Drawable dr = ContextCompat.getDrawable(context, mInt1);
+                    if (dr.getIntrinsicWidth() <= 0 || dr.getIntrinsicHeight() <= 0) {
+                        int size = ((ActivityManager) context.getSystemService(
+                                Context.ACTIVITY_SERVICE)).getLauncherLargeIconSize();
+                        icon = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
+                    } else {
+                        icon = Bitmap.createBitmap(dr.getIntrinsicWidth(), dr.getIntrinsicHeight(),
+                                Bitmap.Config.ARGB_8888);
+                    }
+                    dr.setBounds(0, 0, icon.getWidth(), icon.getHeight());
+                    dr.draw(new Canvas(icon));
+                }
                 break;
             default:
                 throw new IllegalArgumentException("Icon type not supported for intent shortcuts");
         }
+        if (badge != null) {
+            // Badge the icon
+            int w = icon.getWidth();
+            int h = icon.getHeight();
+            badge.setBounds(w / 2, h / 2, w, h);
+            badge.draw(new Canvas(icon));
+        }
+        outIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON, icon);
     }
 
     /**
      * Converts a bitmap following the adaptive icon guide lines, into a bitmap following the
      * shortcut icon guide lines.
      * The returned bitmap will always have same width and height and clipped to a circle.
+     *
+     * @param addShadow set to {@code true} only for legacy shortcuts and {@code false} otherwise
      */
     @VisibleForTesting
-    static Bitmap createLegacyIconFromAdaptiveIcon(Bitmap adaptiveIconBitmap) {
+    static Bitmap createLegacyIconFromAdaptiveIcon(Bitmap adaptiveIconBitmap, boolean addShadow) {
         int size = (int) (DEFAULT_VIEW_PORT_SCALE * Math.min(adaptiveIconBitmap.getWidth(),
                 adaptiveIconBitmap.getHeight()));
 
@@ -239,16 +285,18 @@
         float center = size * 0.5f;
         float radius = center * ICON_DIAMETER_FACTOR;
 
-        // Draw key shadow
-        float blur = BLUR_FACTOR * size;
-        paint.setColor(Color.TRANSPARENT);
-        paint.setShadowLayer(blur, 0, KEY_SHADOW_OFFSET_FACTOR * size, KEY_SHADOW_ALPHA << 24);
-        canvas.drawCircle(center, center, radius, paint);
+        if (addShadow) {
+            // Draw key shadow
+            float blur = BLUR_FACTOR * size;
+            paint.setColor(Color.TRANSPARENT);
+            paint.setShadowLayer(blur, 0, KEY_SHADOW_OFFSET_FACTOR * size, KEY_SHADOW_ALPHA << 24);
+            canvas.drawCircle(center, center, radius, paint);
 
-        // Draw ambient shadow
-        paint.setShadowLayer(blur, 0, 0, AMBIENT_SHADOW_ALPHA << 24);
-        canvas.drawCircle(center, center, radius, paint);
-        paint.clearShadowLayer();
+            // Draw ambient shadow
+            paint.setShadowLayer(blur, 0, 0, AMBIENT_SHADOW_ALPHA << 24);
+            canvas.drawCircle(center, center, radius, paint);
+            paint.clearShadowLayer();
+        }
 
         // Draw the clipped icon
         paint.setColor(Color.BLACK);
diff --git a/compat/tests/AndroidManifest.xml b/compat/tests/AndroidManifest.xml
index 4988845..ed6727f 100644
--- a/compat/tests/AndroidManifest.xml
+++ b/compat/tests/AndroidManifest.xml
@@ -37,7 +37,8 @@
 
         <activity android:name="android.support.v4.view.ViewCompatActivity"/>
 
-        <activity android:name="android.support.v4.app.TestSupportActivity"/>
+        <activity android:name="android.support.v4.app.TestSupportActivity"
+                  android:icon="@drawable/test_drawable_blue"/>
 
         <provider android:name="android.support.v4.provider.MockFontProvider"
                   android:authorities="android.support.provider.fonts.font"
diff --git a/compat/tests/java/android/support/v4/content/pm/ShortcutInfoCompatTest.java b/compat/tests/java/android/support/v4/content/pm/ShortcutInfoCompatTest.java
new file mode 100644
index 0000000..c1a5832
--- /dev/null
+++ b/compat/tests/java/android/support/v4/content/pm/ShortcutInfoCompatTest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2017 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 android.support.v4.content.pm;
+
+import static android.support.v4.graphics.drawable.IconCompatTest.verifyBadgeBitmap;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.support.compat.test.R;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.app.TestSupportActivity;
+import android.support.v4.content.ContextCompat;
+import android.support.v4.graphics.drawable.IconCompat;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ShortcutInfoCompatTest {
+
+    private Intent mAction;
+
+    private Context mContext;
+    private ShortcutInfoCompat.Builder mBuilder;
+
+    @Before
+    public void setup() {
+        mContext = spy(new ContextWrapper(InstrumentationRegistry.getContext()));
+        mAction = new Intent(Intent.ACTION_VIEW).setPackage(mContext.getPackageName());
+
+        mBuilder = new ShortcutInfoCompat.Builder(mContext, "test-shortcut")
+                .setIntent(mAction)
+                .setShortLabel("Test shortcut")
+                .setIcon(IconCompat.createWithResource(mContext, R.drawable.test_drawable_red));
+    }
+
+    @Test
+    public void testAddToIntent_noBadge() {
+        Intent intent = new Intent();
+        mBuilder.setActivity(new ComponentName(mContext, TestSupportActivity.class))
+                .build()
+                .addToIntent(intent);
+
+        assertEquals(mAction, intent.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT));
+        assertNotNull(intent.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE));
+        assertNull(intent.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON));
+    }
+
+    @Test
+    public void testAddToIntent_badgeActivity() {
+        Intent intent = new Intent();
+        mBuilder.setActivity(new ComponentName(mContext, TestSupportActivity.class))
+                .setAlwaysBadged()
+                .build()
+                .addToIntent(intent);
+
+        assertEquals(mAction, intent.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT));
+        assertNull(intent.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE));
+
+        verifyBadgeBitmap(intent, ContextCompat.getColor(mContext, R.color.test_red),
+                ContextCompat.getColor(mContext, R.color.test_blue));
+    }
+
+    @Test
+    public void testAddToIntent_badgeApplication() {
+        ApplicationInfo appInfo = spy(mContext.getApplicationInfo());
+        doReturn(ContextCompat.getDrawable(mContext, R.drawable.test_drawable_green))
+                .when(appInfo).loadIcon(any(PackageManager.class));
+        doReturn(appInfo).when(mContext).getApplicationInfo();
+
+        Intent intent = new Intent();
+        mBuilder.setAlwaysBadged()
+                .build()
+                .addToIntent(intent);
+
+        assertEquals(mAction, intent.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT));
+        assertNull(intent.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE));
+
+        verifyBadgeBitmap(intent, ContextCompat.getColor(mContext, R.color.test_red),
+                ContextCompat.getColor(mContext, R.color.test_green));
+    }
+}
diff --git a/compat/tests/java/android/support/v4/content/pm/ShortcutManagerCompatTest.java b/compat/tests/java/android/support/v4/content/pm/ShortcutManagerCompatTest.java
index 3a48a6bd..7853f02 100644
--- a/compat/tests/java/android/support/v4/content/pm/ShortcutManagerCompatTest.java
+++ b/compat/tests/java/android/support/v4/content/pm/ShortcutManagerCompatTest.java
@@ -20,6 +20,7 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.nullable;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.doReturn;
@@ -112,7 +113,7 @@
         ShortcutManager mockShortcutManager = mock(ShortcutManager.class);
         doReturn(mockShortcutManager).when(mContext).getSystemService(eq(Context.SHORTCUT_SERVICE));
         when(mockShortcutManager.requestPinShortcut(
-                any(ShortcutInfo.class), any(IntentSender.class))).thenReturn(true);
+                any(ShortcutInfo.class), nullable(IntentSender.class))).thenReturn(true);
 
         assertTrue(ShortcutManagerCompat.requestPinShortcut(mContext, mInfoCompat, null));
         ArgumentCaptor<ShortcutInfo> captor = ArgumentCaptor.forClass(ShortcutInfo.class);
diff --git a/compat/tests/java/android/support/v4/graphics/drawable/IconCompatTest.java b/compat/tests/java/android/support/v4/graphics/drawable/IconCompatTest.java
index d87ddac..c83ba7e 100644
--- a/compat/tests/java/android/support/v4/graphics/drawable/IconCompatTest.java
+++ b/compat/tests/java/android/support/v4/graphics/drawable/IconCompatTest.java
@@ -17,6 +17,9 @@
 package android.support.v4.graphics.drawable;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
 import android.annotation.TargetApi;
@@ -34,6 +37,7 @@
 import android.support.test.filters.SdkSuppress;
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.content.ContextCompat;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -45,7 +49,7 @@
 @SmallTest
 public class IconCompatTest {
 
-    private void verifyClippedCircle(Bitmap bitmap, int fillColor, int size) {
+    private static void verifyClippedCircle(Bitmap bitmap, int fillColor, int size) {
         assertEquals(size, bitmap.getHeight());
         assertEquals(bitmap.getWidth(), bitmap.getHeight());
         assertEquals(fillColor, bitmap.getPixel(size / 2, size / 2));
@@ -53,14 +57,28 @@
         assertEquals(Color.TRANSPARENT, bitmap.getPixel(0, 0));
         assertEquals(Color.TRANSPARENT, bitmap.getPixel(0, size - 1));
         assertEquals(Color.TRANSPARENT, bitmap.getPixel(size - 1, 0));
+
+        // The badge is a full rectangle located at the bottom right corner. Check a single pixel
+        // in that region to verify that badging was properly applied.
         assertEquals(Color.TRANSPARENT, bitmap.getPixel(size - 1, size - 1));
     }
 
+    public static void verifyBadgeBitmap(Intent intent, int bgColor, int badgeColor) {
+        Bitmap bitmap = intent.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON);
+        int w = bitmap.getWidth();
+        int h = bitmap.getHeight();
+
+        assertEquals(bgColor, bitmap.getPixel(2, 2));
+        assertEquals(bgColor, bitmap.getPixel(w - 2, 2));
+        assertEquals(bgColor, bitmap.getPixel(2, h - 2));
+        assertEquals(badgeColor, bitmap.getPixel(w - 2, h - 2));
+    }
+
     @Test
     public void testClipAdaptiveIcon() throws Throwable {
         Bitmap source = Bitmap.createBitmap(200, 150, Bitmap.Config.ARGB_8888);
         source.eraseColor(Color.RED);
-        Bitmap result = IconCompat.createLegacyIconFromAdaptiveIcon(source);
+        Bitmap result = IconCompat.createLegacyIconFromAdaptiveIcon(source, false);
         verifyClippedCircle(result, Color.RED, 100);
     }
 
@@ -69,11 +87,46 @@
         Bitmap bitmap = Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888);
         bitmap.eraseColor(Color.RED);
         Intent intent = new Intent();
-        IconCompat.createWithBitmap(bitmap).addToShortcutIntent(intent);
+        IconCompat.createWithBitmap(bitmap).addToShortcutIntent(intent, null);
         assertEquals(bitmap, intent.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON));
     }
 
     @Test
+    public void testAddBitmapToShortcutIntent_badged() {
+        Context context = InstrumentationRegistry.getContext();
+        Bitmap bitmap = Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888);
+        bitmap.eraseColor(Color.RED);
+        Intent intent = new Intent();
+
+        Drawable badge = ContextCompat.getDrawable(context, R.drawable.test_drawable_blue);
+        IconCompat.createWithBitmap(bitmap).addToShortcutIntent(intent, badge);
+        assertNotSame(bitmap, intent.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON));
+
+        verifyBadgeBitmap(intent, Color.RED, ContextCompat.getColor(context, R.color.test_blue));
+    }
+
+    @Test
+    public void testAddResourceToShortcutIntent_badged() {
+        Context context = InstrumentationRegistry.getContext();
+        Intent intent = new Intent();
+
+        // No badge
+        IconCompat.createWithResource(context, R.drawable.test_drawable_green)
+                .addToShortcutIntent(intent, null);
+        assertNotNull(intent.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE));
+        assertNull(intent.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON));
+
+        intent = new Intent();
+        Drawable badge = ContextCompat.getDrawable(context, R.drawable.test_drawable_red);
+        IconCompat.createWithResource(context, R.drawable.test_drawable_blue)
+                .addToShortcutIntent(intent, badge);
+
+        assertNull(intent.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE));
+        verifyBadgeBitmap(intent, ContextCompat.getColor(context, R.color.test_blue),
+                ContextCompat.getColor(context, R.color.test_red));
+    }
+
+    @Test
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.M)
     @TargetApi(Build.VERSION_CODES.M)
     public void testCreateWithBitmap() {
@@ -90,7 +143,7 @@
         Bitmap bitmap = Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888);
         bitmap.eraseColor(Color.GREEN);
         Intent intent = new Intent();
-        IconCompat.createWithAdaptiveBitmap(bitmap).addToShortcutIntent(intent);
+        IconCompat.createWithAdaptiveBitmap(bitmap).addToShortcutIntent(intent, null);
 
         Bitmap clipped = intent.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON);
         verifyClippedCircle(clipped, Color.GREEN, clipped.getWidth());
diff --git a/core-ui/Android.mk b/core-ui/Android.mk
index 184d7be..47846a9 100644
--- a/core-ui/Android.mk
+++ b/core-ui/Android.mk
@@ -30,6 +30,7 @@
 LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
 LOCAL_SHARED_ANDROID_LIBRARIES := \
     android-support-compat \
+    android-support-core-utils \
     android-support-annotations
 LOCAL_JAR_EXCLUDE_FILES := none
 LOCAL_JAVA_LANGUAGE_VERSION := 1.7
diff --git a/core-ui/api/current.txt b/core-ui/api/current.txt
index 6ae4b1a..346ffc4 100644
--- a/core-ui/api/current.txt
+++ b/core-ui/api/current.txt
@@ -1,3 +1,97 @@
+package android.support.design.widget {
+
+  public class CoordinatorLayout extends android.view.ViewGroup {
+    ctor public CoordinatorLayout(android.content.Context);
+    ctor public CoordinatorLayout(android.content.Context, android.util.AttributeSet);
+    ctor public CoordinatorLayout(android.content.Context, android.util.AttributeSet, int);
+    method public void dispatchDependentViewsChanged(android.view.View);
+    method public boolean doViewsOverlap(android.view.View, android.view.View);
+    method protected android.support.design.widget.CoordinatorLayout.LayoutParams generateDefaultLayoutParams();
+    method public android.support.design.widget.CoordinatorLayout.LayoutParams generateLayoutParams(android.util.AttributeSet);
+    method protected android.support.design.widget.CoordinatorLayout.LayoutParams generateLayoutParams(android.view.ViewGroup.LayoutParams);
+    method public java.util.List<android.view.View> getDependencies(android.view.View);
+    method public java.util.List<android.view.View> getDependents(android.view.View);
+    method public android.graphics.drawable.Drawable getStatusBarBackground();
+    method public boolean isPointInChildBounds(android.view.View, int, int);
+    method public void onAttachedToWindow();
+    method public void onDetachedFromWindow();
+    method public void onDraw(android.graphics.Canvas);
+    method protected void onLayout(boolean, int, int, int, int);
+    method public void onLayoutChild(android.view.View, int);
+    method public void onMeasureChild(android.view.View, int, int, int, int);
+    method public void onNestedPreScroll(android.view.View, int, int, int[], int);
+    method public void onNestedScroll(android.view.View, int, int, int, int, int);
+    method public void onNestedScrollAccepted(android.view.View, android.view.View, int, int);
+    method public boolean onStartNestedScroll(android.view.View, android.view.View, int, int);
+    method public void onStopNestedScroll(android.view.View, int);
+    method public void setStatusBarBackground(android.graphics.drawable.Drawable);
+    method public void setStatusBarBackgroundColor(int);
+    method public void setStatusBarBackgroundResource(int);
+  }
+
+  public static abstract class CoordinatorLayout.Behavior<V extends android.view.View> {
+    ctor public CoordinatorLayout.Behavior();
+    ctor public CoordinatorLayout.Behavior(android.content.Context, android.util.AttributeSet);
+    method public boolean blocksInteractionBelow(android.support.design.widget.CoordinatorLayout, V);
+    method public boolean getInsetDodgeRect(android.support.design.widget.CoordinatorLayout, V, android.graphics.Rect);
+    method public int getScrimColor(android.support.design.widget.CoordinatorLayout, V);
+    method public float getScrimOpacity(android.support.design.widget.CoordinatorLayout, V);
+    method public static java.lang.Object getTag(android.view.View);
+    method public boolean layoutDependsOn(android.support.design.widget.CoordinatorLayout, V, android.view.View);
+    method public android.support.v4.view.WindowInsetsCompat onApplyWindowInsets(android.support.design.widget.CoordinatorLayout, V, android.support.v4.view.WindowInsetsCompat);
+    method public void onAttachedToLayoutParams(android.support.design.widget.CoordinatorLayout.LayoutParams);
+    method public boolean onDependentViewChanged(android.support.design.widget.CoordinatorLayout, V, android.view.View);
+    method public void onDependentViewRemoved(android.support.design.widget.CoordinatorLayout, V, android.view.View);
+    method public void onDetachedFromLayoutParams();
+    method public boolean onInterceptTouchEvent(android.support.design.widget.CoordinatorLayout, V, android.view.MotionEvent);
+    method public boolean onLayoutChild(android.support.design.widget.CoordinatorLayout, V, int);
+    method public boolean onMeasureChild(android.support.design.widget.CoordinatorLayout, V, int, int, int, int);
+    method public boolean onNestedFling(android.support.design.widget.CoordinatorLayout, V, android.view.View, float, float, boolean);
+    method public boolean onNestedPreFling(android.support.design.widget.CoordinatorLayout, V, android.view.View, float, float);
+    method public deprecated void onNestedPreScroll(android.support.design.widget.CoordinatorLayout, V, android.view.View, int, int, int[]);
+    method public void onNestedPreScroll(android.support.design.widget.CoordinatorLayout, V, android.view.View, int, int, int[], int);
+    method public deprecated void onNestedScroll(android.support.design.widget.CoordinatorLayout, V, android.view.View, int, int, int, int);
+    method public void onNestedScroll(android.support.design.widget.CoordinatorLayout, V, android.view.View, int, int, int, int, int);
+    method public deprecated void onNestedScrollAccepted(android.support.design.widget.CoordinatorLayout, V, android.view.View, android.view.View, int);
+    method public void onNestedScrollAccepted(android.support.design.widget.CoordinatorLayout, V, android.view.View, android.view.View, int, int);
+    method public boolean onRequestChildRectangleOnScreen(android.support.design.widget.CoordinatorLayout, V, android.graphics.Rect, boolean);
+    method public void onRestoreInstanceState(android.support.design.widget.CoordinatorLayout, V, android.os.Parcelable);
+    method public android.os.Parcelable onSaveInstanceState(android.support.design.widget.CoordinatorLayout, V);
+    method public deprecated boolean onStartNestedScroll(android.support.design.widget.CoordinatorLayout, V, android.view.View, android.view.View, int);
+    method public boolean onStartNestedScroll(android.support.design.widget.CoordinatorLayout, V, android.view.View, android.view.View, int, int);
+    method public deprecated void onStopNestedScroll(android.support.design.widget.CoordinatorLayout, V, android.view.View);
+    method public void onStopNestedScroll(android.support.design.widget.CoordinatorLayout, V, android.view.View, int);
+    method public boolean onTouchEvent(android.support.design.widget.CoordinatorLayout, V, android.view.MotionEvent);
+    method public static void setTag(android.view.View, java.lang.Object);
+  }
+
+  public static abstract class CoordinatorLayout.DefaultBehavior implements java.lang.annotation.Annotation {
+  }
+
+  public static class CoordinatorLayout.LayoutParams extends android.view.ViewGroup.MarginLayoutParams {
+    ctor public CoordinatorLayout.LayoutParams(int, int);
+    ctor public CoordinatorLayout.LayoutParams(android.support.design.widget.CoordinatorLayout.LayoutParams);
+    ctor public CoordinatorLayout.LayoutParams(android.view.ViewGroup.MarginLayoutParams);
+    ctor public CoordinatorLayout.LayoutParams(android.view.ViewGroup.LayoutParams);
+    method public int getAnchorId();
+    method public android.support.design.widget.CoordinatorLayout.Behavior getBehavior();
+    method public void setAnchorId(int);
+    method public void setBehavior(android.support.design.widget.CoordinatorLayout.Behavior);
+    field public int anchorGravity;
+    field public int dodgeInsetEdges;
+    field public int gravity;
+    field public int insetEdge;
+    field public int keyline;
+  }
+
+  protected static class CoordinatorLayout.SavedState extends android.support.v4.view.AbsSavedState {
+    ctor public CoordinatorLayout.SavedState(android.os.Parcel, java.lang.ClassLoader);
+    ctor public CoordinatorLayout.SavedState(android.os.Parcelable);
+    field public static final android.os.Parcelable.Creator<android.support.design.widget.CoordinatorLayout.SavedState> CREATOR;
+  }
+
+}
+
 package android.support.v4.app {
 
   public deprecated class ActionBarDrawerToggle implements android.support.v4.widget.DrawerLayout.DrawerListener {
diff --git a/core-ui/build.gradle b/core-ui/build.gradle
index 38a4fe2..f175b4c 100644
--- a/core-ui/build.gradle
+++ b/core-ui/build.gradle
@@ -3,12 +3,18 @@
 dependencies {
     api project(':support-annotations')
     api project(':support-compat')
+    api project(':support-core-utils')
 
     androidTestImplementation libs.test_runner,      { exclude module: 'support-annotations' }
     androidTestImplementation libs.espresso_core,    { exclude module: 'support-annotations' }
+    androidTestImplementation libs.espresso_contrib, { exclude group: 'com.android.support' }
     androidTestImplementation libs.mockito_core,     { exclude group: 'net.bytebuddy' } // DexMaker has it"s own MockMaker
     androidTestImplementation libs.dexmaker_mockito, { exclude group: 'net.bytebuddy' } // DexMaker has it"s own MockMaker
-    androidTestImplementation project(':support-testutils')
+    androidTestImplementation project(':support-testutils'), {
+        exclude group: 'com.android.support', module: 'support-core-ui'
+    }
+
+    testImplementation libs.junit
 }
 
 android {
@@ -16,6 +22,13 @@
         minSdkVersion 14
     }
 
+    sourceSets {
+        main.res.srcDirs = [
+                'res',
+                'res-public'
+        ]
+    }
+
     buildTypes.all {
         consumerProguardFiles 'proguard-rules.pro'
     }
diff --git a/core-ui/proguard-rules.pro b/core-ui/proguard-rules.pro
index 2ec1c65..cbf4e1f 100644
--- a/core-ui/proguard-rules.pro
+++ b/core-ui/proguard-rules.pro
@@ -12,5 +12,11 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Make sure we keep annotations for ViewPager's DecorView
+# CoordinatorLayout resolves the behaviors of its child components with reflection.
+-keep public class * extends android.support.design.widget.CoordinatorLayout$Behavior {
+    public <init>(android.content.Context, android.util.AttributeSet);
+    public <init>();
+}
+
+# Make sure we keep annotations for CoordinatorLayout's DefaultBehavior and ViewPager's DecorView
 -keepattributes *Annotation*
diff --git a/core-ui/res-public/values/public_attrs.xml b/core-ui/res-public/values/public_attrs.xml
new file mode 100644
index 0000000..505d55b
--- /dev/null
+++ b/core-ui/res-public/values/public_attrs.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+
+<!-- Definitions of attributes to be exposed as public -->
+<resources>
+    <public type="attr" name="keylines"/>
+    <public type="attr" name="layout_anchor"/>
+    <public type="attr" name="layout_anchorGravity"/>
+    <public type="attr" name="layout_behavior"/>
+    <public type="attr" name="layout_dodgeInsetEdges"/>
+    <public type="attr" name="layout_insetEdge"/>
+    <public type="attr" name="layout_keyline"/>
+    <public type="attr" name="statusBarBackground"/>
+</resources>
diff --git a/core-ui/res-public/values/public_styles.xml b/core-ui/res-public/values/public_styles.xml
new file mode 100644
index 0000000..f9b6bab
--- /dev/null
+++ b/core-ui/res-public/values/public_styles.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 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.
+  -->
+
+<!-- Definitions of styles to be exposed as public -->
+<resources>
+    <public type="style" name="Widget.Support.CoordinatorLayout"/>
+</resources>
diff --git a/core-ui/res/values/attrs.xml b/core-ui/res/values/attrs.xml
new file mode 100644
index 0000000..b535c45
--- /dev/null
+++ b/core-ui/res/values/attrs.xml
@@ -0,0 +1,121 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 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.
+  -->
+<resources>
+    <!-- Style to use for coordinator layouts. -->
+    <attr name="coordinatorLayoutStyle" format="reference" />
+
+    <declare-styleable name="CoordinatorLayout">
+        <!-- A reference to an array of integers representing the
+             locations of horizontal keylines in dp from the starting edge.
+             Child views can refer to these keylines for alignment using
+             layout_keyline="index" where index is a 0-based index into
+             this array. -->
+        <attr name="keylines" format="reference"/>
+        <!-- Drawable to display behind the status bar when the view is set to draw behind it. -->
+        <attr name="statusBarBackground" format="color|reference"/>
+    </declare-styleable>
+
+    <declare-styleable name="CoordinatorLayout_Layout">
+        <attr name="android:layout_gravity"/>
+        <!-- The class name of a Behavior class defining special runtime behavior
+             for this child view. -->
+        <attr name="layout_behavior" format="string"/>
+        <!-- The id of an anchor view that this view should position relative to. -->
+        <attr name="layout_anchor" format="reference"/>
+        <!-- The index of a keyline this view should position relative to.
+             android:layout_gravity will affect how the view aligns to the
+             specified keyline. -->
+        <attr name="layout_keyline" format="integer"/>
+
+        <!-- Specifies how an object should position relative to an anchor, on both the X and Y axes,
+             within its parent's bounds.  -->
+        <attr name="layout_anchorGravity">
+            <!-- Push object to the top of its container, not changing its size. -->
+            <flag name="top" value="0x30"/>
+            <!-- Push object to the bottom of its container, not changing its size. -->
+            <flag name="bottom" value="0x50"/>
+            <!-- Push object to the left of its container, not changing its size. -->
+            <flag name="left" value="0x03"/>
+            <!-- Push object to the right of its container, not changing its size. -->
+            <flag name="right" value="0x05"/>
+            <!-- Place object in the vertical center of its container, not changing its size. -->
+            <flag name="center_vertical" value="0x10"/>
+            <!-- Grow the vertical size of the object if needed so it completely fills its container. -->
+            <flag name="fill_vertical" value="0x70"/>
+            <!-- Place object in the horizontal center of its container, not changing its size. -->
+            <flag name="center_horizontal" value="0x01"/>
+            <!-- Grow the horizontal size of the object if needed so it completely fills its container. -->
+            <flag name="fill_horizontal" value="0x07"/>
+            <!-- Place the object in the center of its container in both the vertical and horizontal axis, not changing its size. -->
+            <flag name="center" value="0x11"/>
+            <!-- Grow the horizontal and vertical size of the object if needed so it completely fills its container. -->
+            <flag name="fill" value="0x77"/>
+            <!-- Additional option that can be set to have the top and/or bottom edges of
+                 the child clipped to its container's bounds.
+                 The clip will be based on the vertical gravity: a top gravity will clip the bottom
+                 edge, a bottom gravity will clip the top edge, and neither will clip both edges. -->
+            <flag name="clip_vertical" value="0x80"/>
+            <!-- Additional option that can be set to have the left and/or right edges of
+                 the child clipped to its container's bounds.
+                 The clip will be based on the horizontal gravity: a left gravity will clip the right
+                 edge, a right gravity will clip the left edge, and neither will clip both edges. -->
+            <flag name="clip_horizontal" value="0x08"/>
+            <!-- Push object to the beginning of its container, not changing its size. -->
+            <flag name="start" value="0x00800003"/>
+            <!-- Push object to the end of its container, not changing its size. -->
+            <flag name="end" value="0x00800005"/>
+        </attr>
+
+        <!-- Specifies how this view insets the CoordinatorLayout and make some other views
+             dodge it. -->
+        <attr name="layout_insetEdge" format="enum">
+            <!-- Don't inset. -->
+            <enum name="none" value="0x0"/>
+            <!-- Inset the top edge. -->
+            <enum name="top" value="0x30"/>
+            <!-- Inset the bottom edge. -->
+            <enum name="bottom" value="0x50"/>
+            <!-- Inset the left edge. -->
+            <enum name="left" value="0x03"/>
+            <!-- Inset the right edge. -->
+            <enum name="right" value="0x05"/>
+            <!-- Inset the start edge. -->
+            <enum name="start" value="0x00800003"/>
+            <!-- Inset the end edge. -->
+            <enum name="end" value="0x00800005"/>
+        </attr>
+        <!-- Specifies how this view dodges the inset edges of the CoordinatorLayout. -->
+        <attr name="layout_dodgeInsetEdges">
+            <!-- Don't dodge any edges -->
+            <flag name="none" value="0x0"/>
+            <!-- Dodge the top inset edge. -->
+            <flag name="top" value="0x30"/>
+            <!-- Dodge the bottom inset edge. -->
+            <flag name="bottom" value="0x50"/>
+            <!-- Dodge the left inset edge. -->
+            <flag name="left" value="0x03"/>
+            <!-- Dodge the right inset edge. -->
+            <flag name="right" value="0x05"/>
+            <!-- Dodge the start inset edge. -->
+            <flag name="start" value="0x00800003"/>
+            <!-- Dodge the end inset edge. -->
+            <flag name="end" value="0x00800005"/>
+            <!-- Dodge all the inset edges. -->
+            <flag name="all" value="0x77"/>
+        </attr>
+    </declare-styleable>
+</resources>
diff --git a/core-ui/res/values/styles.xml b/core-ui/res/values/styles.xml
new file mode 100644
index 0000000..07fdbc5
--- /dev/null
+++ b/core-ui/res/values/styles.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 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.
+  -->
+<resources xmlns:tools="http://schemas.android.com/tools">
+    <style name="Widget.Support.CoordinatorLayout" parent="android:Widget">
+        <item name="statusBarBackground">#000000</item>
+    </style>
+</resources>
diff --git a/design/src/android/support/design/widget/CoordinatorLayout.java b/core-ui/src/main/java/android/support/design/widget/CoordinatorLayout.java
similarity index 97%
rename from design/src/android/support/design/widget/CoordinatorLayout.java
rename to core-ui/src/main/java/android/support/design/widget/CoordinatorLayout.java
index d97d4e6..94de9b8 100644
--- a/design/src/android/support/design/widget/CoordinatorLayout.java
+++ b/core-ui/src/main/java/android/support/design/widget/CoordinatorLayout.java
@@ -41,7 +41,7 @@
 import android.support.annotation.Nullable;
 import android.support.annotation.RestrictTo;
 import android.support.annotation.VisibleForTesting;
-import android.support.design.R;
+import android.support.coreui.R;
 import android.support.v4.content.ContextCompat;
 import android.support.v4.graphics.drawable.DrawableCompat;
 import android.support.v4.math.MathUtils;
@@ -86,25 +86,25 @@
  *     <li>As a container for a specific interaction with one or more child views</li>
  * </ol>
  *
- * <p>By specifying {@link CoordinatorLayout.Behavior Behaviors} for child views of a
+ * <p>By specifying {@link Behavior Behaviors} for child views of a
  * CoordinatorLayout you can provide many different interactions within a single parent and those
  * views can also interact with one another. View classes can specify a default behavior when
  * used as a child of a CoordinatorLayout using the
- * {@link CoordinatorLayout.DefaultBehavior DefaultBehavior} annotation.</p>
+ * {@link DefaultBehavior} annotation.</p>
  *
  * <p>Behaviors may be used to implement a variety of interactions and additional layout
  * modifications ranging from sliding drawers and panels to swipe-dismissable elements and buttons
  * that stick to other elements as they move and animate.</p>
  *
  * <p>Children of a CoordinatorLayout may have an
- * {@link CoordinatorLayout.LayoutParams#setAnchorId(int) anchor}. This view id must correspond
+ * {@link LayoutParams#setAnchorId(int) anchor}. This view id must correspond
  * to an arbitrary descendant of the CoordinatorLayout, but it may not be the anchored child itself
  * or a descendant of the anchored child. This can be used to place floating views relative to
  * other arbitrary content panes.</p>
  *
- * <p>Children can specify {@link CoordinatorLayout.LayoutParams#insetEdge} to describe how the
+ * <p>Children can specify {@link LayoutParams#insetEdge} to describe how the
  * view insets the CoordinatorLayout. Any child views which are set to dodge the same inset edges by
- * {@link CoordinatorLayout.LayoutParams#dodgeInsetEdges} will be moved appropriately so that the
+ * {@link LayoutParams#dodgeInsetEdges} will be moved appropriately so that the
  * views do not overlap.</p>
  */
 public class CoordinatorLayout extends ViewGroup implements NestedScrollingParent2 {
@@ -197,16 +197,17 @@
     }
 
     public CoordinatorLayout(Context context, AttributeSet attrs) {
-        this(context, attrs, 0);
+        this(context, attrs, R.attr.coordinatorLayoutStyle);
     }
 
     public CoordinatorLayout(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
 
-        ThemeUtils.checkAppCompatTheme(context);
-
-        final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CoordinatorLayout,
-                defStyleAttr, R.style.Widget_Design_CoordinatorLayout);
+        final TypedArray a = (defStyleAttr == 0)
+                ? context.obtainStyledAttributes(attrs, R.styleable.CoordinatorLayout,
+                    0, R.style.Widget_Support_CoordinatorLayout)
+                : context.obtainStyledAttributes(attrs, R.styleable.CoordinatorLayout,
+                    defStyleAttr, 0);
         final int keylineArrayRes = a.getResourceId(R.styleable.CoordinatorLayout_keylines, 0);
         if (keylineArrayRes != 0) {
             final Resources res = context.getResources();
@@ -703,7 +704,7 @@
 
     /**
      * Called to measure each individual child view unless a
-     * {@link CoordinatorLayout.Behavior Behavior} is present. The Behavior may choose to delegate
+     * {@link Behavior Behavior} is present. The Behavior may choose to delegate
      * child measurement to this method.
      *
      * @param child the child to measure
@@ -834,7 +835,7 @@
 
     /**
      * Called to lay out each individual child view unless a
-     * {@link CoordinatorLayout.Behavior Behavior} is present. The Behavior may choose to
+     * {@link Behavior Behavior} is present. The Behavior may choose to
      * delegate child measurement to this method.
      *
      * @param child child view to lay out
@@ -899,7 +900,7 @@
      * Mark the last known child position rect for the given child view.
      * This will be used when checking if a child view's position has changed between frames.
      * The rect used here should be one returned by
-     * {@link #getChildRect(android.view.View, boolean, android.graphics.Rect)}, with translation
+     * {@link #getChildRect(View, boolean, Rect)}, with translation
      * disabled.
      *
      * @param child child view to set for
@@ -912,7 +913,7 @@
 
     /**
      * Get the last known child rect recorded by
-     * {@link #recordLastChildRect(android.view.View, android.graphics.Rect)}.
+     * {@link #recordLastChildRect(View, Rect)}.
      *
      * @param child child view to retrieve from
      * @param out rect to set to the outpur values
@@ -1469,9 +1470,9 @@
         if (dependents != null && !dependents.isEmpty()) {
             for (int i = 0; i < dependents.size(); i++) {
                 final View child = dependents.get(i);
-                CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams)
+                LayoutParams lp = (LayoutParams)
                         child.getLayoutParams();
-                CoordinatorLayout.Behavior b = lp.getBehavior();
+                Behavior b = lp.getBehavior();
                 if (b != null) {
                     b.onDependentViewChanged(this, child, view);
                 }
@@ -2079,7 +2080,7 @@
          * @param child the child view above the scrim
          * @return the desired scrim color in 0xAARRGGBB format. The default return value is
          *         {@link Color#BLACK}.
-         * @see #getScrimOpacity(CoordinatorLayout, android.view.View)
+         * @see #getScrimOpacity(CoordinatorLayout, View)
          */
         @ColorInt
         public int getScrimColor(CoordinatorLayout parent, V child) {
@@ -2109,11 +2110,11 @@
          * should be blocked.
          *
          * <p>The default implementation returns true if
-         * {@link #getScrimOpacity(CoordinatorLayout, android.view.View)} would return > 0.0f.</p>
+         * {@link #getScrimOpacity(CoordinatorLayout, View)} would return > 0.0f.</p>
          *
          * @param parent the parent view of the given child
          * @param child the child view to test
-         * @return true if {@link #getScrimOpacity(CoordinatorLayout, android.view.View)} would
+         * @return true if {@link #getScrimOpacity(CoordinatorLayout, View)} would
          *         return > 0.0f.
          */
         public boolean blocksInteractionBelow(CoordinatorLayout parent, V child) {
@@ -2140,7 +2141,7 @@
          * @return true if child's layout depends on the proposed dependency's layout,
          *         false otherwise
          *
-         * @see #onDependentViewChanged(CoordinatorLayout, android.view.View, android.view.View)
+         * @see #onDependentViewChanged(CoordinatorLayout, View, View)
          */
         public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) {
             return false;
@@ -2154,12 +2155,12 @@
          * the child view in response.</p>
          *
          * <p>A view's dependency is determined by
-         * {@link #layoutDependsOn(CoordinatorLayout, android.view.View, android.view.View)} or
+         * {@link #layoutDependsOn(CoordinatorLayout, View, View)} or
          * if {@code child} has set another view as it's anchor.</p>
          *
          * <p>Note that if a Behavior changes the layout of a child via this method, it should
          * also be able to reconstruct the correct position in
-         * {@link #onLayoutChild(CoordinatorLayout, android.view.View, int) onLayoutChild}.
+         * {@link #onLayoutChild(CoordinatorLayout, View, int) onLayoutChild}.
          * <code>onDependentViewChanged</code> will not be called during normal layout since
          * the layout of each child view will always happen in dependency order.</p>
          *
@@ -2182,7 +2183,7 @@
          * A Behavior may use this method to appropriately update the child view in response.</p>
          *
          * <p>A view's dependency is determined by
-         * {@link #layoutDependsOn(CoordinatorLayout, android.view.View, android.view.View)} or
+         * {@link #layoutDependsOn(CoordinatorLayout, View, View)} or
          * if {@code child} has set another view as it's anchor.</p>
          *
          * @param parent the parent view of the given child
@@ -2198,7 +2199,7 @@
          * <p>This method can be used to perform custom or modified measurement of a child view
          * in place of the default child measurement behavior. The Behavior's implementation
          * can delegate to the standard CoordinatorLayout measurement behavior by calling
-         * {@link CoordinatorLayout#onMeasureChild(android.view.View, int, int, int, int)
+         * {@link CoordinatorLayout#onMeasureChild(View, int, int, int, int)
          * parent.onMeasureChild}.</p>
          *
          * @param parent the parent CoordinatorLayout
@@ -2224,11 +2225,11 @@
          * <p>This method can be used to perform custom or modified layout of a child view
          * in place of the default child layout behavior. The Behavior's implementation can
          * delegate to the standard CoordinatorLayout measurement behavior by calling
-         * {@link CoordinatorLayout#onLayoutChild(android.view.View, int)
+         * {@link CoordinatorLayout#onLayoutChild(View, int)
          * parent.onLayoutChild}.</p>
          *
          * <p>If a Behavior implements
-         * {@link #onDependentViewChanged(CoordinatorLayout, android.view.View, android.view.View)}
+         * {@link #onDependentViewChanged(CoordinatorLayout, View, View)}
          * to change the position of a view in response to a dependent view changing, it
          * should also implement <code>onLayoutChild</code> in such a way that respects those
          * dependent views. <code>onLayoutChild</code> will always be called for a dependent view
@@ -2631,7 +2632,7 @@
          * @return Returns a Parcelable object containing the behavior's current dynamic
          *         state.
          *
-         * @see #onRestoreInstanceState(android.os.Parcelable)
+         * @see #onRestoreInstanceState(Parcelable)
          * @see View#onSaveInstanceState()
          */
         public Parcelable onSaveInstanceState(CoordinatorLayout parent, V child) {
@@ -2660,7 +2661,7 @@
     /**
      * Parameters describing the desired layout for a child of a {@link CoordinatorLayout}.
      */
-    public static class LayoutParams extends ViewGroup.MarginLayoutParams {
+    public static class LayoutParams extends MarginLayoutParams {
         /**
          * A {@link Behavior} that the child view should obey.
          */
@@ -2817,7 +2818,7 @@
          * a parent CoordinatorLayout.
          *
          * <p>Setting a new behavior will remove any currently associated
-         * {@link Behavior#setTag(android.view.View, Object) Behavior tag}.</p>
+         * {@link Behavior#setTag(View, Object) Behavior tag}.</p>
          *
          * @param behavior The behavior to set or null for no special behavior
          */
@@ -2868,7 +2869,7 @@
          * below the associated child since the touch behavior tracking was last
          * {@link #resetTouchBehaviorTracking() reset}.
          *
-         * @see #isBlockingInteractionBelow(CoordinatorLayout, android.view.View)
+         * @see #isBlockingInteractionBelow(CoordinatorLayout, View)
          */
         boolean didBlockInteraction() {
             if (mBehavior == null) {
@@ -2902,7 +2903,7 @@
          * Reset tracking of Behavior-specific touch interactions. This includes
          * interaction blocking.
          *
-         * @see #isBlockingInteractionBelow(CoordinatorLayout, android.view.View)
+         * @see #isBlockingInteractionBelow(CoordinatorLayout, View)
          * @see #didBlockInteraction()
          */
         void resetTouchBehaviorTracking() {
@@ -2963,7 +2964,7 @@
         /**
          * Invalidate the cached anchor view and direct child ancestor of that anchor.
          * The anchor will need to be
-         * {@link #findAnchorView(CoordinatorLayout, android.view.View) found} before
+         * {@link #findAnchorView(CoordinatorLayout, View) found} before
          * being used again.
          */
         void invalidateAnchor() {
@@ -3145,7 +3146,7 @@
 
     @Override
     public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) {
-        final CoordinatorLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
+        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
         final Behavior behavior = lp.getBehavior();
 
         if (behavior != null
@@ -3225,7 +3226,7 @@
 
         }
 
-        public static final Parcelable.Creator<SavedState> CREATOR =
+        public static final Creator<SavedState> CREATOR =
                 new ClassLoaderCreator<SavedState>() {
                     @Override
                     public SavedState createFromParcel(Parcel in, ClassLoader loader) {
diff --git a/design/src/android/support/design/widget/DirectedAcyclicGraph.java b/core-ui/src/main/java/android/support/design/widget/DirectedAcyclicGraph.java
similarity index 100%
rename from design/src/android/support/design/widget/DirectedAcyclicGraph.java
rename to core-ui/src/main/java/android/support/design/widget/DirectedAcyclicGraph.java
diff --git a/design/src/android/support/design/widget/ViewGroupUtils.java b/core-ui/src/main/java/android/support/design/widget/ViewGroupUtils.java
similarity index 96%
rename from design/src/android/support/design/widget/ViewGroupUtils.java
rename to core-ui/src/main/java/android/support/design/widget/ViewGroupUtils.java
index 0545516..5d8b5c7 100644
--- a/design/src/android/support/design/widget/ViewGroupUtils.java
+++ b/core-ui/src/main/java/android/support/design/widget/ViewGroupUtils.java
@@ -29,7 +29,7 @@
 
     /**
      * This is a port of the common
-     * {@link ViewGroup#offsetDescendantRectToMyCoords(android.view.View, android.graphics.Rect)}
+     * {@link ViewGroup#offsetDescendantRectToMyCoords(View, Rect)}
      * from the framework, but adapted to take transformations into account. The result
      * will be the bounding rect of the real transformed rect.
      *
diff --git a/design/jvm-tests/NO_DOCS b/core-ui/src/test/NO_DOCS
similarity index 100%
rename from design/jvm-tests/NO_DOCS
rename to core-ui/src/test/NO_DOCS
diff --git a/design/jvm-tests/src/android/support/design/widget/DirectedAcyclicGraphTest.java b/core-ui/src/test/java/android/support/design/widget/DirectedAcyclicGraphTest.java
similarity index 98%
rename from design/jvm-tests/src/android/support/design/widget/DirectedAcyclicGraphTest.java
rename to core-ui/src/test/java/android/support/design/widget/DirectedAcyclicGraphTest.java
index 4a5ffc5..ec7687d 100644
--- a/design/jvm-tests/src/android/support/design/widget/DirectedAcyclicGraphTest.java
+++ b/core-ui/src/test/java/android/support/design/widget/DirectedAcyclicGraphTest.java
@@ -22,7 +22,6 @@
 import static org.junit.Assert.assertTrue;
 
 import android.support.annotation.NonNull;
-import android.support.test.filters.SmallTest;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -32,7 +31,6 @@
 import java.util.List;
 
 @RunWith(JUnit4.class)
-@SmallTest
 public class DirectedAcyclicGraphTest {
 
     private DirectedAcyclicGraph<TestNode> mGraph;
diff --git a/core-utils/Android.mk b/core-utils/Android.mk
index a6855fc..6dda862 100644
--- a/core-utils/Android.mk
+++ b/core-utils/Android.mk
@@ -27,7 +27,9 @@
 LOCAL_MODULE := android-support-core-utils
 LOCAL_SDK_VERSION := $(SUPPORT_CURRENT_SDK_VERSION)
 LOCAL_SRC_FILES := \
-    $(call all-java-files-under,src/main/java)
+    $(call all-java-files-under,kitkat) \
+    $(call all-java-files-under,api21) \
+    $(call all-java-files-under,java)
 LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
 LOCAL_SHARED_ANDROID_LIBRARIES := \
     android-support-compat \
diff --git a/core-utils/src/main/java/android/support/v4/graphics/drawable/RoundedBitmapDrawable21.java b/core-utils/api21/android/support/v4/graphics/drawable/RoundedBitmapDrawable21.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/graphics/drawable/RoundedBitmapDrawable21.java
rename to core-utils/api21/android/support/v4/graphics/drawable/RoundedBitmapDrawable21.java
diff --git a/core-utils/build.gradle b/core-utils/build.gradle
index 55fe2bb..29fe86c 100644
--- a/core-utils/build.gradle
+++ b/core-utils/build.gradle
@@ -14,6 +14,14 @@
     defaultConfig {
         minSdkVersion 14
     }
+
+    sourceSets {
+        main.java.srcDirs = [
+                'kitkat',
+                'api21',
+                'java'
+        ]
+    }
 }
 
 supportLibrary {
diff --git a/core-utils/src/main/java/android/support/v4/app/AppLaunchChecker.java b/core-utils/java/android/support/v4/app/AppLaunchChecker.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/app/AppLaunchChecker.java
rename to core-utils/java/android/support/v4/app/AppLaunchChecker.java
diff --git a/core-utils/src/main/java/android/support/v4/app/FrameMetricsAggregator.java b/core-utils/java/android/support/v4/app/FrameMetricsAggregator.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/app/FrameMetricsAggregator.java
rename to core-utils/java/android/support/v4/app/FrameMetricsAggregator.java
diff --git a/core-utils/src/main/java/android/support/v4/app/NavUtils.java b/core-utils/java/android/support/v4/app/NavUtils.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/app/NavUtils.java
rename to core-utils/java/android/support/v4/app/NavUtils.java
diff --git a/core-utils/src/main/java/android/support/v4/app/TaskStackBuilder.java b/core-utils/java/android/support/v4/app/TaskStackBuilder.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/app/TaskStackBuilder.java
rename to core-utils/java/android/support/v4/app/TaskStackBuilder.java
diff --git a/core-utils/src/main/java/android/support/v4/app/package.html b/core-utils/java/android/support/v4/app/package.html
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/app/package.html
rename to core-utils/java/android/support/v4/app/package.html
diff --git a/core-utils/src/main/java/android/support/v4/content/AsyncTaskLoader.java b/core-utils/java/android/support/v4/content/AsyncTaskLoader.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/content/AsyncTaskLoader.java
rename to core-utils/java/android/support/v4/content/AsyncTaskLoader.java
diff --git a/core-utils/src/main/java/android/support/v4/content/CursorLoader.java b/core-utils/java/android/support/v4/content/CursorLoader.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/content/CursorLoader.java
rename to core-utils/java/android/support/v4/content/CursorLoader.java
diff --git a/core-utils/src/main/java/android/support/v4/content/FileProvider.java b/core-utils/java/android/support/v4/content/FileProvider.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/content/FileProvider.java
rename to core-utils/java/android/support/v4/content/FileProvider.java
diff --git a/core-utils/src/main/java/android/support/v4/content/Loader.java b/core-utils/java/android/support/v4/content/Loader.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/content/Loader.java
rename to core-utils/java/android/support/v4/content/Loader.java
diff --git a/core-utils/src/main/java/android/support/v4/content/LocalBroadcastManager.java b/core-utils/java/android/support/v4/content/LocalBroadcastManager.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/content/LocalBroadcastManager.java
rename to core-utils/java/android/support/v4/content/LocalBroadcastManager.java
diff --git a/core-utils/src/main/java/android/support/v4/content/MimeTypeFilter.java b/core-utils/java/android/support/v4/content/MimeTypeFilter.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/content/MimeTypeFilter.java
rename to core-utils/java/android/support/v4/content/MimeTypeFilter.java
diff --git a/core-utils/src/main/java/android/support/v4/content/ModernAsyncTask.java b/core-utils/java/android/support/v4/content/ModernAsyncTask.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/content/ModernAsyncTask.java
rename to core-utils/java/android/support/v4/content/ModernAsyncTask.java
diff --git a/core-utils/src/main/java/android/support/v4/content/PermissionChecker.java b/core-utils/java/android/support/v4/content/PermissionChecker.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/content/PermissionChecker.java
rename to core-utils/java/android/support/v4/content/PermissionChecker.java
diff --git a/core-utils/src/main/java/android/support/v4/content/WakefulBroadcastReceiver.java b/core-utils/java/android/support/v4/content/WakefulBroadcastReceiver.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/content/WakefulBroadcastReceiver.java
rename to core-utils/java/android/support/v4/content/WakefulBroadcastReceiver.java
diff --git a/core-utils/src/main/java/android/support/v4/content/package.html b/core-utils/java/android/support/v4/content/package.html
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/content/package.html
rename to core-utils/java/android/support/v4/content/package.html
diff --git a/core-utils/src/main/java/android/support/v4/graphics/ColorUtils.java b/core-utils/java/android/support/v4/graphics/ColorUtils.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/graphics/ColorUtils.java
rename to core-utils/java/android/support/v4/graphics/ColorUtils.java
diff --git a/core-utils/src/main/java/android/support/v4/graphics/drawable/RoundedBitmapDrawable.java b/core-utils/java/android/support/v4/graphics/drawable/RoundedBitmapDrawable.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/graphics/drawable/RoundedBitmapDrawable.java
rename to core-utils/java/android/support/v4/graphics/drawable/RoundedBitmapDrawable.java
diff --git a/core-utils/src/main/java/android/support/v4/graphics/drawable/RoundedBitmapDrawableFactory.java b/core-utils/java/android/support/v4/graphics/drawable/RoundedBitmapDrawableFactory.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/graphics/drawable/RoundedBitmapDrawableFactory.java
rename to core-utils/java/android/support/v4/graphics/drawable/RoundedBitmapDrawableFactory.java
diff --git a/core-utils/src/main/java/android/support/v4/math/MathUtils.java b/core-utils/java/android/support/v4/math/MathUtils.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/math/MathUtils.java
rename to core-utils/java/android/support/v4/math/MathUtils.java
diff --git a/core-utils/src/main/java/android/support/v4/print/PrintHelper.java b/core-utils/java/android/support/v4/print/PrintHelper.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/print/PrintHelper.java
rename to core-utils/java/android/support/v4/print/PrintHelper.java
diff --git a/core-utils/src/main/java/android/support/v4/provider/DocumentFile.java b/core-utils/java/android/support/v4/provider/DocumentFile.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/provider/DocumentFile.java
rename to core-utils/java/android/support/v4/provider/DocumentFile.java
diff --git a/core-utils/src/main/java/android/support/v4/provider/RawDocumentFile.java b/core-utils/java/android/support/v4/provider/RawDocumentFile.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/provider/RawDocumentFile.java
rename to core-utils/java/android/support/v4/provider/RawDocumentFile.java
diff --git a/core-utils/src/main/java/android/support/v4/provider/SingleDocumentFile.java b/core-utils/java/android/support/v4/provider/SingleDocumentFile.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/provider/SingleDocumentFile.java
rename to core-utils/java/android/support/v4/provider/SingleDocumentFile.java
diff --git a/core-utils/src/main/java/android/support/v4/provider/TreeDocumentFile.java b/core-utils/java/android/support/v4/provider/TreeDocumentFile.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/provider/TreeDocumentFile.java
rename to core-utils/java/android/support/v4/provider/TreeDocumentFile.java
diff --git a/core-utils/src/main/java/android/support/v4/provider/DocumentsContractApi19.java b/core-utils/kitkat/android/support/v4/provider/DocumentsContractApi19.java
similarity index 100%
rename from core-utils/src/main/java/android/support/v4/provider/DocumentsContractApi19.java
rename to core-utils/kitkat/android/support/v4/provider/DocumentsContractApi19.java
diff --git a/design/Android.mk b/design/Android.mk
index 08f8815..4a51f77 100644
--- a/design/Android.mk
+++ b/design/Android.mk
@@ -38,7 +38,11 @@
     android-support-transition \
     android-support-v7-appcompat \
     android-support-v7-recyclerview \
-    android-support-v4 \
+    android-support-compat \
+    android-support-media-compat \
+    android-support-core-utils \
+    android-support-core-ui \
+    android-support-fragment \
     android-support-annotations
 LOCAL_JAVA_LANGUAGE_VERSION := 1.7
 LOCAL_AAPT_FLAGS := \
diff --git a/design/api/27.0.0.ignore b/design/api/27.0.0.ignore
new file mode 100644
index 0000000..533cc49
--- /dev/null
+++ b/design/api/27.0.0.ignore
@@ -0,0 +1,5 @@
+197ce1d
+88bc57e
+9761c3e
+86b38bf
+c6abd5e
diff --git a/design/api/current.txt b/design/api/current.txt
index 602ee48..b15eca1 100644
--- a/design/api/current.txt
+++ b/design/api/current.txt
@@ -244,96 +244,6 @@
     field public static final int COLLAPSE_MODE_PIN = 1; // 0x1
   }
 
-  public class CoordinatorLayout extends android.view.ViewGroup {
-    ctor public CoordinatorLayout(android.content.Context);
-    ctor public CoordinatorLayout(android.content.Context, android.util.AttributeSet);
-    ctor public CoordinatorLayout(android.content.Context, android.util.AttributeSet, int);
-    method public void dispatchDependentViewsChanged(android.view.View);
-    method public boolean doViewsOverlap(android.view.View, android.view.View);
-    method protected android.support.design.widget.CoordinatorLayout.LayoutParams generateDefaultLayoutParams();
-    method public android.support.design.widget.CoordinatorLayout.LayoutParams generateLayoutParams(android.util.AttributeSet);
-    method protected android.support.design.widget.CoordinatorLayout.LayoutParams generateLayoutParams(android.view.ViewGroup.LayoutParams);
-    method public java.util.List<android.view.View> getDependencies(android.view.View);
-    method public java.util.List<android.view.View> getDependents(android.view.View);
-    method public android.graphics.drawable.Drawable getStatusBarBackground();
-    method public boolean isPointInChildBounds(android.view.View, int, int);
-    method public void onAttachedToWindow();
-    method public void onDetachedFromWindow();
-    method public void onDraw(android.graphics.Canvas);
-    method protected void onLayout(boolean, int, int, int, int);
-    method public void onLayoutChild(android.view.View, int);
-    method public void onMeasureChild(android.view.View, int, int, int, int);
-    method public void onNestedPreScroll(android.view.View, int, int, int[], int);
-    method public void onNestedScroll(android.view.View, int, int, int, int, int);
-    method public void onNestedScrollAccepted(android.view.View, android.view.View, int, int);
-    method public boolean onStartNestedScroll(android.view.View, android.view.View, int, int);
-    method public void onStopNestedScroll(android.view.View, int);
-    method public void setStatusBarBackground(android.graphics.drawable.Drawable);
-    method public void setStatusBarBackgroundColor(int);
-    method public void setStatusBarBackgroundResource(int);
-  }
-
-  public static abstract class CoordinatorLayout.Behavior<V extends android.view.View> {
-    ctor public CoordinatorLayout.Behavior();
-    ctor public CoordinatorLayout.Behavior(android.content.Context, android.util.AttributeSet);
-    method public boolean blocksInteractionBelow(android.support.design.widget.CoordinatorLayout, V);
-    method public boolean getInsetDodgeRect(android.support.design.widget.CoordinatorLayout, V, android.graphics.Rect);
-    method public int getScrimColor(android.support.design.widget.CoordinatorLayout, V);
-    method public float getScrimOpacity(android.support.design.widget.CoordinatorLayout, V);
-    method public static java.lang.Object getTag(android.view.View);
-    method public boolean layoutDependsOn(android.support.design.widget.CoordinatorLayout, V, android.view.View);
-    method public android.support.v4.view.WindowInsetsCompat onApplyWindowInsets(android.support.design.widget.CoordinatorLayout, V, android.support.v4.view.WindowInsetsCompat);
-    method public void onAttachedToLayoutParams(android.support.design.widget.CoordinatorLayout.LayoutParams);
-    method public boolean onDependentViewChanged(android.support.design.widget.CoordinatorLayout, V, android.view.View);
-    method public void onDependentViewRemoved(android.support.design.widget.CoordinatorLayout, V, android.view.View);
-    method public void onDetachedFromLayoutParams();
-    method public boolean onInterceptTouchEvent(android.support.design.widget.CoordinatorLayout, V, android.view.MotionEvent);
-    method public boolean onLayoutChild(android.support.design.widget.CoordinatorLayout, V, int);
-    method public boolean onMeasureChild(android.support.design.widget.CoordinatorLayout, V, int, int, int, int);
-    method public boolean onNestedFling(android.support.design.widget.CoordinatorLayout, V, android.view.View, float, float, boolean);
-    method public boolean onNestedPreFling(android.support.design.widget.CoordinatorLayout, V, android.view.View, float, float);
-    method public deprecated void onNestedPreScroll(android.support.design.widget.CoordinatorLayout, V, android.view.View, int, int, int[]);
-    method public void onNestedPreScroll(android.support.design.widget.CoordinatorLayout, V, android.view.View, int, int, int[], int);
-    method public deprecated void onNestedScroll(android.support.design.widget.CoordinatorLayout, V, android.view.View, int, int, int, int);
-    method public void onNestedScroll(android.support.design.widget.CoordinatorLayout, V, android.view.View, int, int, int, int, int);
-    method public deprecated void onNestedScrollAccepted(android.support.design.widget.CoordinatorLayout, V, android.view.View, android.view.View, int);
-    method public void onNestedScrollAccepted(android.support.design.widget.CoordinatorLayout, V, android.view.View, android.view.View, int, int);
-    method public boolean onRequestChildRectangleOnScreen(android.support.design.widget.CoordinatorLayout, V, android.graphics.Rect, boolean);
-    method public void onRestoreInstanceState(android.support.design.widget.CoordinatorLayout, V, android.os.Parcelable);
-    method public android.os.Parcelable onSaveInstanceState(android.support.design.widget.CoordinatorLayout, V);
-    method public deprecated boolean onStartNestedScroll(android.support.design.widget.CoordinatorLayout, V, android.view.View, android.view.View, int);
-    method public boolean onStartNestedScroll(android.support.design.widget.CoordinatorLayout, V, android.view.View, android.view.View, int, int);
-    method public deprecated void onStopNestedScroll(android.support.design.widget.CoordinatorLayout, V, android.view.View);
-    method public void onStopNestedScroll(android.support.design.widget.CoordinatorLayout, V, android.view.View, int);
-    method public boolean onTouchEvent(android.support.design.widget.CoordinatorLayout, V, android.view.MotionEvent);
-    method public static void setTag(android.view.View, java.lang.Object);
-  }
-
-  public static abstract class CoordinatorLayout.DefaultBehavior implements java.lang.annotation.Annotation {
-  }
-
-  public static class CoordinatorLayout.LayoutParams extends android.view.ViewGroup.MarginLayoutParams {
-    ctor public CoordinatorLayout.LayoutParams(int, int);
-    ctor public CoordinatorLayout.LayoutParams(android.support.design.widget.CoordinatorLayout.LayoutParams);
-    ctor public CoordinatorLayout.LayoutParams(android.view.ViewGroup.MarginLayoutParams);
-    ctor public CoordinatorLayout.LayoutParams(android.view.ViewGroup.LayoutParams);
-    method public int getAnchorId();
-    method public android.support.design.widget.CoordinatorLayout.Behavior getBehavior();
-    method public void setAnchorId(int);
-    method public void setBehavior(android.support.design.widget.CoordinatorLayout.Behavior);
-    field public int anchorGravity;
-    field public int dodgeInsetEdges;
-    field public int gravity;
-    field public int insetEdge;
-    field public int keyline;
-  }
-
-  protected static class CoordinatorLayout.SavedState extends android.support.v4.view.AbsSavedState {
-    ctor public CoordinatorLayout.SavedState(android.os.Parcel, java.lang.ClassLoader);
-    ctor public CoordinatorLayout.SavedState(android.os.Parcelable);
-    field public static final android.os.Parcelable.Creator<android.support.design.widget.CoordinatorLayout.SavedState> CREATOR;
-  }
-
   public class FloatingActionButton extends android.support.design.widget.VisibilityAwareImageButton {
     ctor public FloatingActionButton(android.content.Context);
     ctor public FloatingActionButton(android.content.Context, android.util.AttributeSet);
diff --git a/design/build.gradle b/design/build.gradle
index 4a82051..3be0d93 100644
--- a/design/build.gradle
+++ b/design/build.gradle
@@ -12,11 +12,6 @@
     androidTestImplementation libs.mockito_core,     { exclude group: 'net.bytebuddy' } // DexMaker has it"s own MockMaker
     androidTestImplementation libs.dexmaker_mockito, { exclude group: 'net.bytebuddy' } // DexMaker has it"s own MockMaker
     androidTestImplementation project(':support-testutils')
-
-    testImplementation libs.junit
-    testImplementation (libs.test_runner) {
-        exclude module: 'support-annotations'
-    }
 }
 
 android {
@@ -39,8 +34,6 @@
                 'res-public'
         ]
         main.resources.srcDir 'src'
-
-        test.java.srcDir 'jvm-tests/src'
     }
 
     buildTypes.all {
diff --git a/design/res-public/values/public_attrs.xml b/design/res-public/values/public_attrs.xml
index b443778..9afe981 100644
--- a/design/res-public/values/public_attrs.xml
+++ b/design/res-public/values/public_attrs.xml
@@ -51,19 +51,13 @@
     <public type="attr" name="itemIconTint"/>
     <public type="attr" name="itemTextAppearance"/>
     <public type="attr" name="itemTextColor"/>
-    <public type="attr" name="keylines"/>
-    <public type="attr" name="layout_anchor"/>
-    <public type="attr" name="layout_anchorGravity"/>
-    <public type="attr" name="layout_behavior"/>
     <public type="attr" name="layout_collapseMode"/>
     <public type="attr" name="layout_collapseParallaxMultiplier"/>
-    <public type="attr" name="layout_keyline"/>
     <public type="attr" name="layout_scrollFlags"/>
     <public type="attr" name="layout_scrollInterpolator"/>
     <public type="attr" name="menu"/>
     <public type="attr" name="pressedTranslationZ"/>
     <public type="attr" name="rippleColor"/>
-    <public type="attr" name="statusBarBackground"/>
     <public type="attr" name="statusBarScrim"/>
     <public type="attr" name="tabBackground"/>
     <public type="attr" name="tabContentStart"/>
diff --git a/design/res/values/attrs.xml b/design/res/values/attrs.xml
index b378849..6cdb22c 100644
--- a/design/res/values/attrs.xml
+++ b/design/res/values/attrs.xml
@@ -122,107 +122,6 @@
         <attr name="android:layout" />
     </declare-styleable>
 
-    <declare-styleable name="CoordinatorLayout">
-        <!-- A reference to an array of integers representing the
-             locations of horizontal keylines in dp from the starting edge.
-             Child views can refer to these keylines for alignment using
-             layout_keyline="index" where index is a 0-based index into
-             this array. -->
-        <attr name="keylines" format="reference"/>
-        <!-- Drawable to display behind the status bar when the view is set to draw behind it. -->
-        <attr name="statusBarBackground" format="reference"/>
-    </declare-styleable>
-
-    <declare-styleable name="CoordinatorLayout_Layout">
-        <attr name="android:layout_gravity"/>
-        <!-- The class name of a Behavior class defining special runtime behavior
-             for this child view. -->
-        <attr name="layout_behavior" format="string"/>
-        <!-- The id of an anchor view that this view should position relative to. -->
-        <attr name="layout_anchor" format="reference"/>
-        <!-- The index of a keyline this view should position relative to.
-             android:layout_gravity will affect how the view aligns to the
-             specified keyline. -->
-        <attr name="layout_keyline" format="integer"/>
-
-        <!-- Specifies how an object should position relative to an anchor, on both the X and Y axes,
-             within its parent's bounds.  -->
-        <attr name="layout_anchorGravity">
-            <!-- Push object to the top of its container, not changing its size. -->
-            <flag name="top" value="0x30"/>
-            <!-- Push object to the bottom of its container, not changing its size. -->
-            <flag name="bottom" value="0x50"/>
-            <!-- Push object to the left of its container, not changing its size. -->
-            <flag name="left" value="0x03"/>
-            <!-- Push object to the right of its container, not changing its size. -->
-            <flag name="right" value="0x05"/>
-            <!-- Place object in the vertical center of its container, not changing its size. -->
-            <flag name="center_vertical" value="0x10"/>
-            <!-- Grow the vertical size of the object if needed so it completely fills its container. -->
-            <flag name="fill_vertical" value="0x70"/>
-            <!-- Place object in the horizontal center of its container, not changing its size. -->
-            <flag name="center_horizontal" value="0x01"/>
-            <!-- Grow the horizontal size of the object if needed so it completely fills its container. -->
-            <flag name="fill_horizontal" value="0x07"/>
-            <!-- Place the object in the center of its container in both the vertical and horizontal axis, not changing its size. -->
-            <flag name="center" value="0x11"/>
-            <!-- Grow the horizontal and vertical size of the object if needed so it completely fills its container. -->
-            <flag name="fill" value="0x77"/>
-            <!-- Additional option that can be set to have the top and/or bottom edges of
-                 the child clipped to its container's bounds.
-                 The clip will be based on the vertical gravity: a top gravity will clip the bottom
-                 edge, a bottom gravity will clip the top edge, and neither will clip both edges. -->
-            <flag name="clip_vertical" value="0x80"/>
-            <!-- Additional option that can be set to have the left and/or right edges of
-                 the child clipped to its container's bounds.
-                 The clip will be based on the horizontal gravity: a left gravity will clip the right
-                 edge, a right gravity will clip the left edge, and neither will clip both edges. -->
-            <flag name="clip_horizontal" value="0x08"/>
-            <!-- Push object to the beginning of its container, not changing its size. -->
-            <flag name="start" value="0x00800003"/>
-            <!-- Push object to the end of its container, not changing its size. -->
-            <flag name="end" value="0x00800005"/>
-        </attr>
-
-        <!-- Specifies how this view insets the CoordinatorLayout and make some other views
-             dodge it. -->
-        <attr name="layout_insetEdge" format="enum">
-            <!-- Don't inset. -->
-            <enum name="none" value="0x0"/>
-            <!-- Inset the top edge. -->
-            <enum name="top" value="0x30"/>
-            <!-- Inset the bottom edge. -->
-            <enum name="bottom" value="0x50"/>
-            <!-- Inset the left edge. -->
-            <enum name="left" value="0x03"/>
-            <!-- Inset the right edge. -->
-            <enum name="right" value="0x03"/>
-            <!-- Inset the start edge. -->
-            <enum name="start" value="0x00800003"/>
-            <!-- Inset the end edge. -->
-            <enum name="end" value="0x00800005"/>
-        </attr>
-        <!-- Specifies how this view dodges the inset edges of the CoordinatorLayout. -->
-        <attr name="layout_dodgeInsetEdges">
-            <!-- Don't dodge any edges -->
-            <flag name="none" value="0x0"/>
-            <!-- Dodge the top inset edge. -->
-            <flag name="top" value="0x30"/>
-            <!-- Dodge the bottom inset edge. -->
-            <flag name="bottom" value="0x50"/>
-            <!-- Dodge the left inset edge. -->
-            <flag name="left" value="0x03"/>
-            <!-- Dodge the right inset edge. -->
-            <flag name="right" value="0x03"/>
-            <!-- Dodge the start inset edge. -->
-            <flag name="start" value="0x00800003"/>
-            <!-- Dodge the end inset edge. -->
-            <flag name="end" value="0x00800005"/>
-            <!-- Dodge all the inset edges. -->
-            <flag name="all" value="0x77"/>
-        </attr>
-    </declare-styleable>
-
     <declare-styleable name="TextInputLayout">
         <attr name="hintTextAppearance" format="reference"/>
         <!-- The hint to display in the floating label. -->
diff --git a/design/res/values/styles.xml b/design/res/values/styles.xml
index 93fb7eb..bbb200d 100644
--- a/design/res/values/styles.xml
+++ b/design/res/values/styles.xml
@@ -117,7 +117,7 @@
     <style name="Widget.Design.AppBarLayout" parent="Base.Widget.Design.AppBarLayout">
     </style>
 
-    <style name="Widget.Design.CoordinatorLayout" parent="android:Widget">
+    <style name="Widget.Design.CoordinatorLayout" parent="@style/Widget.Support.CoordinatorLayout">
         <item name="statusBarBackground">?attr/colorPrimaryDark</item>
     </style>
 
diff --git a/design/res/values/themes.xml b/design/res/values/themes.xml
index a7bd92d..aa4c876 100644
--- a/design/res/values/themes.xml
+++ b/design/res/values/themes.xml
@@ -30,10 +30,12 @@
 
     <style name="Theme.Design" parent="Theme.AppCompat">
         <item name="textColorError">?attr/colorError</item>
+        <item name="coordinatorLayoutStyle">@style/Widget.Design.CoordinatorLayout</item>
     </style>
 
     <style name="Theme.Design.Light" parent="Theme.AppCompat.Light">
         <item name="textColorError">?attr/colorError</item>
+        <item name="coordinatorLayoutStyle">@style/Widget.Design.CoordinatorLayout</item>
     </style>
 
     <style name="Theme.Design.NoActionBar">
diff --git a/design/tests/res/drawable-xxhdpi/ic_airplay_black_24dp.png b/design/tests/res/drawable-xxhdpi/ic_airplay_black_24dp.png
new file mode 100644
index 0000000..ddf2620
--- /dev/null
+++ b/design/tests/res/drawable-xxhdpi/ic_airplay_black_24dp.png
Binary files differ
diff --git a/design/tests/res/drawable-xxhdpi/ic_album_black_24dp.png b/design/tests/res/drawable-xxhdpi/ic_album_black_24dp.png
new file mode 100644
index 0000000..60f59f5
--- /dev/null
+++ b/design/tests/res/drawable-xxhdpi/ic_album_black_24dp.png
Binary files differ
diff --git a/design/tests/res/layout/design_appbar_dodge_left.xml b/design/tests/res/layout/design_appbar_dodge_left.xml
new file mode 100644
index 0000000..7f3ecb9
--- /dev/null
+++ b/design/tests/res/layout/design_appbar_dodge_left.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 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.
+  -->
+
+<android.support.design.widget.CoordinatorLayout
+    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:fitsSystemWindows="true">
+
+    <include layout="@layout/design_content_appbar_toolbar_collapse_pin" />
+
+    <android.support.design.widget.FloatingActionButton
+        android:id="@+id/fab"
+        android:layout_height="wrap_content"
+        android:layout_width="wrap_content"
+        android:layout_gravity="bottom|left"
+        android:src="@drawable/ic_album_black_24dp"
+        app:layout_insetEdge="left"
+        android:clickable="true" />
+
+    <android.support.design.widget.FloatingActionButton
+        android:id="@+id/fab2"
+        android:layout_height="wrap_content"
+        android:layout_width="wrap_content"
+        android:layout_gravity="bottom|left"
+        android:src="@drawable/ic_airplay_black_24dp"
+        app:layout_dodgeInsetEdges="left"
+        android:clickable="true" />
+
+</android.support.design.widget.CoordinatorLayout>
diff --git a/design/tests/res/layout/design_appbar_dodge_right.xml b/design/tests/res/layout/design_appbar_dodge_right.xml
new file mode 100644
index 0000000..10815c0
--- /dev/null
+++ b/design/tests/res/layout/design_appbar_dodge_right.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 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.
+  -->
+
+<android.support.design.widget.CoordinatorLayout
+    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:fitsSystemWindows="true">
+
+    <include layout="@layout/design_content_appbar_toolbar_collapse_pin" />
+
+    <android.support.design.widget.FloatingActionButton
+        android:id="@+id/fab"
+        android:layout_height="wrap_content"
+        android:layout_width="wrap_content"
+        android:layout_gravity="bottom|right"
+        android:src="@drawable/ic_album_black_24dp"
+        app:layout_insetEdge="right"
+        android:clickable="true" />
+
+    <android.support.design.widget.FloatingActionButton
+        android:id="@+id/fab2"
+        android:layout_height="wrap_content"
+        android:layout_width="wrap_content"
+        android:layout_gravity="bottom|right"
+        android:src="@drawable/ic_airplay_black_24dp"
+        app:layout_dodgeInsetEdges="right"
+        android:clickable="true" />
+
+</android.support.design.widget.CoordinatorLayout>
diff --git a/design/tests/res/values/strings.xml b/design/tests/res/values/strings.xml
index f456921..02763ec 100644
--- a/design/tests/res/values/strings.xml
+++ b/design/tests/res/values/strings.xml
@@ -42,6 +42,9 @@
     <string name="design_appbar_anchored_fab_margin_left">AppBar + anchored FAB with left margin</string>
     <string name="design_appbar_anchored_fab_margin_right">AppBar + anchored FAB with right margin</string>
 
+    <string name="design_appbar_dodge_left">AppBar + FABs with dodge on left</string>
+    <string name="design_appbar_dodge_right">AppBar + FABs with dodge on right</string>
+
     <string name="textinput_hint">Hint to the user</string>
 
 </resources>
\ No newline at end of file
diff --git a/design/tests/src/android/support/design/widget/AppBarWithCollapsingToolbarStateRestoreTest.java b/design/tests/src/android/support/design/widget/AppBarWithCollapsingToolbarStateRestoreTest.java
index b9a6518..e8a29af 100644
--- a/design/tests/src/android/support/design/widget/AppBarWithCollapsingToolbarStateRestoreTest.java
+++ b/design/tests/src/android/support/design/widget/AppBarWithCollapsingToolbarStateRestoreTest.java
@@ -24,8 +24,8 @@
 import static android.support.test.espresso.matcher.ViewMatchers.withId;
 
 import android.support.design.test.R;
-import android.support.design.testutils.ActivityUtils;
 import android.support.test.filters.LargeTest;
+import android.support.testutils.AppCompatActivityUtils;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -59,8 +59,8 @@
                 .check(matches(hasZ()))
                 .check(matches(isCollapsed()));
 
-        mActivity = ActivityUtils.recreateActivity(mActivityTestRule, mActivity);
-        ActivityUtils.waitForExecution(mActivityTestRule);
+        mActivity = AppCompatActivityUtils.recreateActivity(mActivityTestRule, mActivity);
+        AppCompatActivityUtils.waitForExecution(mActivityTestRule);
 
         // And check that the app bar still is restored correctly
         onView(withId(R.id.app_bar))
diff --git a/design/tests/src/android/support/design/widget/AppBarWithDodgingTest.java b/design/tests/src/android/support/design/widget/AppBarWithDodgingTest.java
new file mode 100644
index 0000000..ad337d5
--- /dev/null
+++ b/design/tests/src/android/support/design/widget/AppBarWithDodgingTest.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2017 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 android.support.design.widget;
+
+import static org.junit.Assert.assertTrue;
+
+import android.graphics.Rect;
+import android.support.design.test.R;
+import android.support.test.filters.SmallTest;
+
+import org.junit.Test;
+
+@SmallTest
+public class AppBarWithDodgingTest extends AppBarLayoutBaseTest {
+    @Test
+    public void testLeftDodge() throws Throwable {
+        configureContent(R.layout.design_appbar_dodge_left,
+                R.string.design_appbar_dodge_left);
+
+        final FloatingActionButton fab = mCoordinatorLayout.findViewById(R.id.fab);
+        final FloatingActionButton fab2 = mCoordinatorLayout.findViewById(R.id.fab2);
+
+        final int[] fabOnScreenXY = new int[2];
+        final int[] fab2OnScreenXY = new int[2];
+        fab.getLocationOnScreen(fabOnScreenXY);
+        fab2.getLocationOnScreen(fab2OnScreenXY);
+
+        final Rect fabRect = new Rect();
+        final Rect fab2Rect = new Rect();
+        fab.getContentRect(fabRect);
+        fab2.getContentRect(fab2Rect);
+
+        // Our second FAB is configured to "dodge" the first one - to be displayed to the
+        // right of it
+        int firstRight = fabOnScreenXY[0] + fabRect.right;
+        int secondLeft = fab2OnScreenXY[0] + fab2Rect.left;
+        assertTrue("Second button left edge at " + secondLeft
+                        + " should be dodging the first button right edge at " + firstRight,
+                secondLeft >= firstRight);
+    }
+
+    @Test
+    public void testRightDodge() throws Throwable {
+        configureContent(R.layout.design_appbar_dodge_right,
+                R.string.design_appbar_dodge_right);
+
+        final FloatingActionButton fab = mCoordinatorLayout.findViewById(R.id.fab);
+        final FloatingActionButton fab2 = mCoordinatorLayout.findViewById(R.id.fab2);
+
+        final int[] fabOnScreenXY = new int[2];
+        final int[] fab2OnScreenXY = new int[2];
+        fab.getLocationOnScreen(fabOnScreenXY);
+        fab2.getLocationOnScreen(fab2OnScreenXY);
+
+        final Rect fabRect = new Rect();
+        final Rect fab2Rect = new Rect();
+        fab.getContentRect(fabRect);
+        fab2.getContentRect(fab2Rect);
+
+        // Our second FAB is configured to "dodge" the first one - to be displayed to the
+        // left of it
+        int firstLeft = fabOnScreenXY[0] + fabRect.left;
+        int secondRight = fab2OnScreenXY[0] + fab2Rect.right;
+        assertTrue("Second button right edge at " + secondRight
+                        + " should be dodging the first button left edge at " + firstLeft,
+                secondRight <= firstLeft);
+    }
+}
diff --git a/design/tests/src/android/support/design/widget/BaseTestActivity.java b/design/tests/src/android/support/design/widget/BaseTestActivity.java
index 4662001..e1e44e2 100755
--- a/design/tests/src/android/support/design/widget/BaseTestActivity.java
+++ b/design/tests/src/android/support/design/widget/BaseTestActivity.java
@@ -18,7 +18,7 @@
 
 import android.os.Bundle;
 import android.support.annotation.LayoutRes;
-import android.support.design.testutils.RecreatedAppCompatActivity;
+import android.support.testutils.RecreatedAppCompatActivity;
 import android.view.WindowManager;
 
 abstract class BaseTestActivity extends RecreatedAppCompatActivity {
diff --git a/design/tests/src/android/support/design/widget/TextInputLayoutTest.java b/design/tests/src/android/support/design/widget/TextInputLayoutTest.java
index 52471a9..5969235 100755
--- a/design/tests/src/android/support/design/widget/TextInputLayoutTest.java
+++ b/design/tests/src/android/support/design/widget/TextInputLayoutTest.java
@@ -65,8 +65,6 @@
 import android.os.Build;
 import android.os.Parcelable;
 import android.support.design.test.R;
-import android.support.design.testutils.ActivityUtils;
-import android.support.design.testutils.RecreatedAppCompatActivity;
 import android.support.design.testutils.TestUtils;
 import android.support.design.testutils.ViewStructureImpl;
 import android.support.test.annotation.UiThreadTest;
@@ -75,6 +73,8 @@
 import android.support.test.filters.LargeTest;
 import android.support.test.filters.MediumTest;
 import android.support.test.filters.SdkSuppress;
+import android.support.testutils.AppCompatActivityUtils;
+import android.support.testutils.RecreatedAppCompatActivity;
 import android.support.v4.widget.TextViewCompat;
 import android.text.method.PasswordTransformationMethod;
 import android.text.method.TransformationMethod;
@@ -524,8 +524,8 @@
         onView(withId(R.id.textinput_password)).check(isPasswordToggledVisible(true));
 
         RecreatedAppCompatActivity activity = mActivityTestRule.getActivity();
-        activity = ActivityUtils.recreateActivity(mActivityTestRule, activity);
-        ActivityUtils.waitForExecution(mActivityTestRule);
+        AppCompatActivityUtils.recreateActivity(mActivityTestRule, activity);
+        AppCompatActivityUtils.waitForExecution(mActivityTestRule);
 
         // Check that the password is still toggled to be shown as plain text
         onView(withId(R.id.textinput_password)).check(isPasswordToggledVisible(true));
diff --git a/fragment/build.gradle b/fragment/build.gradle
index 3a86f38..c7ad39a 100644
--- a/fragment/build.gradle
+++ b/fragment/build.gradle
@@ -8,8 +8,11 @@
 
     androidTestImplementation libs.test_runner,      { exclude module: 'support-annotations' }
     androidTestImplementation libs.espresso_core,    { exclude module: 'support-annotations' }
-    androidTestImplementation libs.mockito_core,     { exclude group: 'net.bytebuddy' } // DexMaker has it"s own MockMaker
-    androidTestImplementation libs.dexmaker_mockito, { exclude group: 'net.bytebuddy' } // DexMaker has it"s own MockMaker
+    androidTestImplementation libs.mockito_core,     { exclude group: 'net.bytebuddy' } // DexMaker has its own MockMaker
+    androidTestImplementation libs.dexmaker_mockito, { exclude group: 'net.bytebuddy' } // DexMaker has its own MockMaker
+    androidTestImplementation project(':support-testutils'), {
+        exclude group: 'com.android.support', module: 'support-fragment'
+    }
 }
 
 android {
diff --git a/fragment/tests/java/android/support/v4/app/FragmentManagerNonConfigTest.java b/fragment/tests/java/android/support/v4/app/FragmentManagerNonConfigTest.java
index eeae2b4..dc62c01 100644
--- a/fragment/tests/java/android/support/v4/app/FragmentManagerNonConfigTest.java
+++ b/fragment/tests/java/android/support/v4/app/FragmentManagerNonConfigTest.java
@@ -21,6 +21,7 @@
 import android.support.test.filters.MediumTest;
 import android.support.test.rule.ActivityTestRule;
 import android.support.test.runner.AndroidJUnit4;
+import android.support.testutils.FragmentActivityUtils;
 import android.support.v4.app.test.NonConfigOnStopActivity;
 
 import org.junit.Rule;
@@ -41,7 +42,7 @@
      */
     @Test
     public void nonConfigStop() throws Throwable {
-        FragmentActivity activity = FragmentTestUtil.recreateActivity(mActivityRule,
+        FragmentActivity activity = FragmentActivityUtils.recreateActivity(mActivityRule,
                 mActivityRule.getActivity());
 
         // A fragment was added in onStop(), but we shouldn't see it here...
diff --git a/fragment/tests/java/android/support/v4/app/FragmentTestUtil.java b/fragment/tests/java/android/support/v4/app/FragmentTestUtil.java
index 1da1af6..604701f 100644
--- a/fragment/tests/java/android/support/v4/app/FragmentTestUtil.java
+++ b/fragment/tests/java/android/support/v4/app/FragmentTestUtil.java
@@ -16,7 +16,6 @@
 package android.support.v4.app;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
 
 import android.app.Activity;
 import android.app.Instrumentation;
@@ -28,15 +27,12 @@
 import android.support.test.InstrumentationRegistry;
 import android.support.test.rule.ActivityTestRule;
 import android.support.v4.app.test.FragmentTestActivity;
-import android.support.v4.app.test.RecreatedActivity;
 import android.util.Pair;
 import android.view.ViewGroup;
 import android.view.animation.Animation;
 
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
 
 public class FragmentTestUtil {
     private static final Runnable DO_NOTHING = new Runnable() {
@@ -247,32 +243,4 @@
             }
         }
     }
-
-    /**
-     * Restarts the RecreatedActivity and waits for the new activity to be resumed.
-     *
-     * @return The newly-restarted Activity
-     */
-    public static <T extends RecreatedActivity> T recreateActivity(
-            ActivityTestRule<? extends RecreatedActivity> rule, final T activity)
-            throws InterruptedException {
-        // Now switch the orientation
-        RecreatedActivity.sResumed = new CountDownLatch(1);
-        RecreatedActivity.sDestroyed = new CountDownLatch(1);
-
-        runOnUiThreadRethrow(rule, new Runnable() {
-            @Override
-            public void run() {
-                activity.recreate();
-            }
-        });
-        assertTrue(RecreatedActivity.sResumed.await(1, TimeUnit.SECONDS));
-        assertTrue(RecreatedActivity.sDestroyed.await(1, TimeUnit.SECONDS));
-        T newActivity = (T) RecreatedActivity.sActivity;
-
-        waitForExecution(rule);
-
-        RecreatedActivity.clearState();
-        return newActivity;
-    }
 }
diff --git a/fragment/tests/java/android/support/v4/app/LoaderTest.java b/fragment/tests/java/android/support/v4/app/LoaderTest.java
index b581fe7..523baf0 100644
--- a/fragment/tests/java/android/support/v4/app/LoaderTest.java
+++ b/fragment/tests/java/android/support/v4/app/LoaderTest.java
@@ -30,6 +30,7 @@
 import android.support.test.filters.MediumTest;
 import android.support.test.rule.ActivityTestRule;
 import android.support.test.runner.AndroidJUnit4;
+import android.support.testutils.FragmentActivityUtils;
 import android.support.v4.app.test.LoaderActivity;
 import android.support.v4.content.AsyncTaskLoader;
 import android.support.v4.content.Loader;
@@ -58,7 +59,7 @@
     public void testLeak() throws Throwable {
         // Restart the activity because mActivityRule keeps a strong reference to the
         // old activity.
-        LoaderActivity activity = FragmentTestUtil.recreateActivity(mActivityRule,
+        LoaderActivity activity = FragmentActivityUtils.recreateActivity(mActivityRule,
                 mActivityRule.getActivity());
 
         LoaderFragment fragment = new LoaderFragment();
@@ -80,7 +81,7 @@
 
         WeakReference<LoaderActivity> weakActivity = new WeakReference(LoaderActivity.sActivity);
 
-        activity = FragmentTestUtil.recreateActivity(mActivityRule, activity);
+        activity = FragmentActivityUtils.recreateActivity(mActivityRule, activity);
 
         // Wait for everything to settle. We have to make sure that the old Activity
         // is ready to be collected.
@@ -101,7 +102,7 @@
 
         assertEquals("Loaded!", activity.textView.getText().toString());
 
-        activity = FragmentTestUtil.recreateActivity(mActivityRule, activity);
+        activity = FragmentActivityUtils.recreateActivity(mActivityRule, activity);
 
         FragmentTestUtil.waitForExecution(mActivityRule);
 
diff --git a/fragment/tests/java/android/support/v4/app/test/LoaderActivity.java b/fragment/tests/java/android/support/v4/app/test/LoaderActivity.java
index 8a051f4..2990f0a 100644
--- a/fragment/tests/java/android/support/v4/app/test/LoaderActivity.java
+++ b/fragment/tests/java/android/support/v4/app/test/LoaderActivity.java
@@ -20,6 +20,7 @@
 import android.os.Bundle;
 import android.support.annotation.Nullable;
 import android.support.fragment.test.R;
+import android.support.testutils.RecreatedActivity;
 import android.support.v4.app.LoaderManager;
 import android.support.v4.content.AsyncTaskLoader;
 import android.support.v4.content.Loader;
diff --git a/fragment/tests/java/android/support/v4/app/test/NonConfigOnStopActivity.java b/fragment/tests/java/android/support/v4/app/test/NonConfigOnStopActivity.java
index fc03b50..9d71388 100644
--- a/fragment/tests/java/android/support/v4/app/test/NonConfigOnStopActivity.java
+++ b/fragment/tests/java/android/support/v4/app/test/NonConfigOnStopActivity.java
@@ -16,6 +16,7 @@
 
 package android.support.v4.app.test;
 
+import android.support.testutils.RecreatedActivity;
 import android.support.v4.app.Fragment;
 
 public class NonConfigOnStopActivity extends RecreatedActivity {
diff --git a/media-compat/tests/src/android/support/v4/media/session/MediaSessionCompatTest.java b/media-compat/tests/src/android/support/v4/media/session/MediaSessionCompatTest.java
index 2cda242..9911c11 100644
--- a/media-compat/tests/src/android/support/v4/media/session/MediaSessionCompatTest.java
+++ b/media-compat/tests/src/android/support/v4/media/session/MediaSessionCompatTest.java
@@ -502,6 +502,29 @@
     }
 
     /**
+     * Tests {@link MediaSessionCompat#setCallback} with {@code null}. No callback will be called
+     * once {@code setCallback(null)} is done.
+     */
+    @Test
+    @SmallTest
+    public void testSetCallbackWithNull() throws Exception {
+        MediaSessionCallback sessionCallback = new MediaSessionCallback();
+        mSession.setFlags(MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
+        mSession.setActive(true);
+        mSession.setCallback(sessionCallback, mHandler);
+
+        MediaControllerCompat controller = mSession.getController();
+        setPlaybackState(PlaybackStateCompat.STATE_PLAYING);
+
+        sessionCallback.reset(1);
+        mSession.setCallback(null, mHandler);
+
+        controller.getTransportControls().pause();
+        assertFalse(sessionCallback.await(WAIT_TIME_MS));
+        assertFalse("Callback shouldn't be called.", sessionCallback.mOnPauseCalled);
+    }
+
+    /**
      * Tests {@link MediaSessionCompat#setPlaybackToLocal} and
      * {@link MediaSessionCompat#setPlaybackToRemote}.
      */
@@ -716,22 +739,6 @@
         }
     }
 
-    @Test
-    @SmallTest
-    public void testSetNullCallback() throws Throwable {
-        getInstrumentation().runOnMainSync(new Runnable() {
-            @Override
-            public void run() {
-                try {
-                    MediaSessionCompat session = new MediaSessionCompat(getContext(), "TEST");
-                    session.setCallback(null);
-                } catch (Exception e) {
-                    fail("Fail with an exception: " + e);
-                }
-            }
-        });
-    }
-
     /**
      * Tests {@link MediaSessionCompat.QueueItem}.
      */
diff --git a/settings.gradle b/settings.gradle
index c281bb1..9c335c9 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -103,6 +103,9 @@
 include ':support-content'
 project(':support-content').projectDir = new File(rootDir, 'content')
 
+include ':car'
+project(':car').projectDir = new File(rootDir, 'car')
+
 /////////////////////////////
 //
 // Samples
diff --git a/testutils/build.gradle b/testutils/build.gradle
index f1be70d..d85b21a 100644
--- a/testutils/build.gradle
+++ b/testutils/build.gradle
@@ -17,6 +17,13 @@
 apply plugin: android.support.SupportAndroidLibraryPlugin
 
 dependencies {
+    api project(':support-fragment')
+    api project(':appcompat-v7')
+
+    compile libs.test_runner,      { exclude module: 'support-annotations' }
+    compile libs.espresso_core,    { exclude module: 'support-annotations' }
+    compile libs.mockito_core,     { exclude group: 'net.bytebuddy' } // DexMaker has its own MockMaker
+    compile libs.dexmaker_mockito, { exclude group: 'net.bytebuddy' } // DexMaker has its own MockMaker
     compile libs.junit
 }
 
diff --git a/design/tests/src/android/support/design/testutils/ActivityUtils.java b/testutils/src/main/java/android/support/testutils/AppCompatActivityUtils.java
similarity index 91%
rename from design/tests/src/android/support/design/testutils/ActivityUtils.java
rename to testutils/src/main/java/android/support/testutils/AppCompatActivityUtils.java
index 1ed6a3f..49ccc1b 100644
--- a/design/tests/src/android/support/design/testutils/ActivityUtils.java
+++ b/testutils/src/main/java/android/support/testutils/AppCompatActivityUtils.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package android.support.design.testutils;
+package android.support.testutils;
 
 import static org.junit.Assert.assertTrue;
 
@@ -24,15 +24,20 @@
 import java.util.concurrent.TimeUnit;
 
 /**
- * Utility methods for testing activities.
+ * Utility methods for testing AppCompat activities.
  */
-public class ActivityUtils {
+public class AppCompatActivityUtils {
     private static final Runnable DO_NOTHING = new Runnable() {
         @Override
         public void run() {
         }
     };
 
+    /**
+     * Waits for the execution of the provided activity test rule.
+     *
+     * @param rule Activity test rule to wait for.
+     */
     public static void waitForExecution(
             final ActivityTestRule<? extends RecreatedAppCompatActivity> rule) {
         // Wait for two cycles. When starting a postponed transition, it will post to
diff --git a/design/tests/src/android/support/design/testutils/ActivityUtils.java b/testutils/src/main/java/android/support/testutils/FragmentActivityUtils.java
similarity index 64%
copy from design/tests/src/android/support/design/testutils/ActivityUtils.java
copy to testutils/src/main/java/android/support/testutils/FragmentActivityUtils.java
index 1ed6a3f..7d12deb 100644
--- a/design/tests/src/android/support/design/testutils/ActivityUtils.java
+++ b/testutils/src/main/java/android/support/testutils/FragmentActivityUtils.java
@@ -13,28 +13,29 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package android.support.design.testutils;
+package android.support.testutils;
 
 import static org.junit.Assert.assertTrue;
 
+import android.app.Activity;
 import android.os.Looper;
 import android.support.test.rule.ActivityTestRule;
+import android.support.v4.app.FragmentActivity;
 
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
 /**
- * Utility methods for testing activities.
+ * Utility methods for testing fragment activities.
  */
-public class ActivityUtils {
+public class FragmentActivityUtils {
     private static final Runnable DO_NOTHING = new Runnable() {
         @Override
         public void run() {
         }
     };
 
-    public static void waitForExecution(
-            final ActivityTestRule<? extends RecreatedAppCompatActivity> rule) {
+    private static void waitForExecution(final ActivityTestRule<? extends FragmentActivity> rule) {
         // Wait for two cycles. When starting a postponed transition, it will post to
         // the UI thread and then the execution will be added onto the queue after that.
         // The two-cycle wait makes sure fragments have the opportunity to complete both
@@ -47,8 +48,8 @@
         }
     }
 
-    private static void runOnUiThreadRethrow(
-            ActivityTestRule<? extends RecreatedAppCompatActivity> rule, Runnable r) {
+    private static void runOnUiThreadRethrow(ActivityTestRule<? extends Activity> rule,
+            Runnable r) {
         if (Looper.getMainLooper() == Looper.myLooper()) {
             r.run();
         } else {
@@ -61,16 +62,16 @@
     }
 
     /**
-     * Restarts the RecreatedAppCompatActivity and waits for the new activity to be resumed.
+     * Restarts the RecreatedActivity and waits for the new activity to be resumed.
      *
-     * @return The newly-restarted RecreatedAppCompatActivity
+     * @return The newly-restarted Activity
      */
-    public static <T extends RecreatedAppCompatActivity> T recreateActivity(
-            ActivityTestRule<? extends RecreatedAppCompatActivity> rule, final T activity)
+    public static <T extends RecreatedActivity> T recreateActivity(
+            ActivityTestRule<? extends RecreatedActivity> rule, final T activity)
             throws InterruptedException {
         // Now switch the orientation
-        RecreatedAppCompatActivity.sResumed = new CountDownLatch(1);
-        RecreatedAppCompatActivity.sDestroyed = new CountDownLatch(1);
+        RecreatedActivity.sResumed = new CountDownLatch(1);
+        RecreatedActivity.sDestroyed = new CountDownLatch(1);
 
         runOnUiThreadRethrow(rule, new Runnable() {
             @Override
@@ -78,13 +79,13 @@
                 activity.recreate();
             }
         });
-        assertTrue(RecreatedAppCompatActivity.sResumed.await(1, TimeUnit.SECONDS));
-        assertTrue(RecreatedAppCompatActivity.sDestroyed.await(1, TimeUnit.SECONDS));
-        T newActivity = (T) RecreatedAppCompatActivity.sActivity;
+        assertTrue(RecreatedActivity.sResumed.await(1, TimeUnit.SECONDS));
+        assertTrue(RecreatedActivity.sDestroyed.await(1, TimeUnit.SECONDS));
+        T newActivity = (T) RecreatedActivity.sActivity;
 
         waitForExecution(rule);
 
-        RecreatedAppCompatActivity.clearState();
+        RecreatedActivity.clearState();
         return newActivity;
     }
 }
diff --git a/fragment/tests/java/android/support/v4/app/test/RecreatedActivity.java b/testutils/src/main/java/android/support/testutils/RecreatedActivity.java
similarity index 80%
rename from fragment/tests/java/android/support/v4/app/test/RecreatedActivity.java
rename to testutils/src/main/java/android/support/testutils/RecreatedActivity.java
index c298a88..aaea3a9 100644
--- a/fragment/tests/java/android/support/v4/app/test/RecreatedActivity.java
+++ b/testutils/src/main/java/android/support/testutils/RecreatedActivity.java
@@ -14,21 +14,27 @@
  * limitations under the License.
  */
 
-package android.support.v4.app.test;
+package android.support.testutils;
 
 import android.os.Bundle;
 import android.support.annotation.Nullable;
+import android.support.test.rule.ActivityTestRule;
 import android.support.v4.app.FragmentActivity;
 
 import java.util.concurrent.CountDownLatch;
 
+/**
+ * Extension of {@link FragmentActivity} that keeps track of when it is recreated.
+ * In order to use this class, have your activity extend it and call
+ * {@link FragmentActivityUtils#recreateActivity(ActivityTestRule, RecreatedActivity)} API.
+ */
 public class RecreatedActivity extends FragmentActivity {
     // These must be cleared after each test using clearState()
     public static RecreatedActivity sActivity;
     public static CountDownLatch sResumed;
     public static CountDownLatch sDestroyed;
 
-    public static void clearState() {
+    static void clearState() {
         sActivity = null;
         sResumed = null;
         sDestroyed = null;
diff --git a/design/tests/src/android/support/design/testutils/RecreatedAppCompatActivity.java b/testutils/src/main/java/android/support/testutils/RecreatedAppCompatActivity.java
similarity index 81%
rename from design/tests/src/android/support/design/testutils/RecreatedAppCompatActivity.java
rename to testutils/src/main/java/android/support/testutils/RecreatedAppCompatActivity.java
index 52ba059..d5645a3 100644
--- a/design/tests/src/android/support/design/testutils/RecreatedAppCompatActivity.java
+++ b/testutils/src/main/java/android/support/testutils/RecreatedAppCompatActivity.java
@@ -14,17 +14,20 @@
  * limitations under the License.
  */
 
-package android.support.design.testutils;
+package android.support.testutils;
 
 import android.os.Bundle;
 import android.support.annotation.Nullable;
+import android.support.test.rule.ActivityTestRule;
 import android.support.v7.app.AppCompatActivity;
 
 import java.util.concurrent.CountDownLatch;
 
 /**
- * Activity that keeps track of resume / destroy lifecycle events, as well as of the last
- * instance of itself.
+ * Extension of {@link AppCompatActivity} that keeps track of when it is recreated.
+ * In order to use this class, have your activity extend it and call
+ * {@link AppCompatActivityUtils#recreateActivity(ActivityTestRule, RecreatedAppCompatActivity)}
+ * API.
  */
 public class RecreatedAppCompatActivity extends AppCompatActivity {
     // These must be cleared after each test using clearState()
@@ -32,7 +35,7 @@
     public static CountDownLatch sResumed;
     public static CountDownLatch sDestroyed;
 
-    public static void clearState() {
+    static void clearState() {
         sActivity = null;
         sResumed = null;
         sDestroyed = null;
diff --git a/transition/Android.mk b/transition/Android.mk
index cbff183..8c76d6b 100644
--- a/transition/Android.mk
+++ b/transition/Android.mk
@@ -27,13 +27,7 @@
 LOCAL_MODULE := android-support-transition
 LOCAL_SDK_VERSION := $(SUPPORT_CURRENT_SDK_VERSION)
 LOCAL_SRC_FILES := \
-    $(call all-java-files-under,base) \
-    $(call all-java-files-under,api14) \
-    $(call all-java-files-under,api18) \
-    $(call all-java-files-under,api19) \
-    $(call all-java-files-under,api21) \
-    $(call all-java-files-under,api22) \
-    $(call all-java-files-under,src)
+    $(call all-java-files-under,src/main/java)
 LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
 LOCAL_SHARED_ANDROID_LIBRARIES := \
     android-support-annotations \
diff --git a/transition/build.gradle b/transition/build.gradle
index be366d6..43d60e8 100644
--- a/transition/build.gradle
+++ b/transition/build.gradle
@@ -19,15 +19,6 @@
     }
 
     sourceSets {
-        main.java.srcDirs = [
-                'base',
-                'api14',
-                'api18',
-                'api19',
-                'api21',
-                'api22',
-                'src'
-        ]
         main.res.srcDirs = [
                 'res',
                 'res-public'
diff --git a/transition/src/android/support/transition/AnimatorUtils.java b/transition/src/main/java/android/support/transition/AnimatorUtils.java
similarity index 100%
rename from transition/src/android/support/transition/AnimatorUtils.java
rename to transition/src/main/java/android/support/transition/AnimatorUtils.java
diff --git a/transition/api14/android/support/transition/AnimatorUtilsApi14.java b/transition/src/main/java/android/support/transition/AnimatorUtilsApi14.java
similarity index 100%
rename from transition/api14/android/support/transition/AnimatorUtilsApi14.java
rename to transition/src/main/java/android/support/transition/AnimatorUtilsApi14.java
diff --git a/transition/api19/android/support/transition/AnimatorUtilsApi19.java b/transition/src/main/java/android/support/transition/AnimatorUtilsApi19.java
similarity index 100%
rename from transition/api19/android/support/transition/AnimatorUtilsApi19.java
rename to transition/src/main/java/android/support/transition/AnimatorUtilsApi19.java
diff --git a/transition/base/android/support/transition/AnimatorUtilsImpl.java b/transition/src/main/java/android/support/transition/AnimatorUtilsImpl.java
similarity index 100%
rename from transition/base/android/support/transition/AnimatorUtilsImpl.java
rename to transition/src/main/java/android/support/transition/AnimatorUtilsImpl.java
diff --git a/transition/src/android/support/transition/ArcMotion.java b/transition/src/main/java/android/support/transition/ArcMotion.java
similarity index 100%
rename from transition/src/android/support/transition/ArcMotion.java
rename to transition/src/main/java/android/support/transition/ArcMotion.java
diff --git a/transition/src/android/support/transition/AutoTransition.java b/transition/src/main/java/android/support/transition/AutoTransition.java
similarity index 91%
rename from transition/src/android/support/transition/AutoTransition.java
rename to transition/src/main/java/android/support/transition/AutoTransition.java
index 02b49e2..bf39c3c 100644
--- a/transition/src/android/support/transition/AutoTransition.java
+++ b/transition/src/main/java/android/support/transition/AutoTransition.java
@@ -45,9 +45,9 @@
 
     private void init() {
         setOrdering(ORDERING_SEQUENTIAL);
-        addTransition(new Fade(Fade.OUT)).
-                addTransition(new ChangeBounds()).
-                addTransition(new Fade(Fade.IN));
+        addTransition(new Fade(Fade.OUT))
+                .addTransition(new ChangeBounds())
+                .addTransition(new Fade(Fade.IN));
     }
 
 }
diff --git a/transition/src/android/support/transition/ChangeBounds.java b/transition/src/main/java/android/support/transition/ChangeBounds.java
similarity index 100%
rename from transition/src/android/support/transition/ChangeBounds.java
rename to transition/src/main/java/android/support/transition/ChangeBounds.java
diff --git a/transition/src/android/support/transition/ChangeClipBounds.java b/transition/src/main/java/android/support/transition/ChangeClipBounds.java
similarity index 100%
rename from transition/src/android/support/transition/ChangeClipBounds.java
rename to transition/src/main/java/android/support/transition/ChangeClipBounds.java
diff --git a/transition/src/android/support/transition/ChangeImageTransform.java b/transition/src/main/java/android/support/transition/ChangeImageTransform.java
similarity index 100%
rename from transition/src/android/support/transition/ChangeImageTransform.java
rename to transition/src/main/java/android/support/transition/ChangeImageTransform.java
diff --git a/transition/src/android/support/transition/ChangeScroll.java b/transition/src/main/java/android/support/transition/ChangeScroll.java
similarity index 100%
rename from transition/src/android/support/transition/ChangeScroll.java
rename to transition/src/main/java/android/support/transition/ChangeScroll.java
diff --git a/transition/src/android/support/transition/ChangeTransform.java b/transition/src/main/java/android/support/transition/ChangeTransform.java
similarity index 100%
rename from transition/src/android/support/transition/ChangeTransform.java
rename to transition/src/main/java/android/support/transition/ChangeTransform.java
diff --git a/transition/src/android/support/transition/CircularPropagation.java b/transition/src/main/java/android/support/transition/CircularPropagation.java
similarity index 100%
rename from transition/src/android/support/transition/CircularPropagation.java
rename to transition/src/main/java/android/support/transition/CircularPropagation.java
diff --git a/transition/src/android/support/transition/Explode.java b/transition/src/main/java/android/support/transition/Explode.java
similarity index 100%
rename from transition/src/android/support/transition/Explode.java
rename to transition/src/main/java/android/support/transition/Explode.java
diff --git a/transition/src/android/support/transition/Fade.java b/transition/src/main/java/android/support/transition/Fade.java
similarity index 100%
rename from transition/src/android/support/transition/Fade.java
rename to transition/src/main/java/android/support/transition/Fade.java
diff --git a/transition/src/android/support/transition/FloatArrayEvaluator.java b/transition/src/main/java/android/support/transition/FloatArrayEvaluator.java
similarity index 100%
rename from transition/src/android/support/transition/FloatArrayEvaluator.java
rename to transition/src/main/java/android/support/transition/FloatArrayEvaluator.java
diff --git a/transition/src/android/support/transition/FragmentTransitionSupport.java b/transition/src/main/java/android/support/transition/FragmentTransitionSupport.java
similarity index 100%
rename from transition/src/android/support/transition/FragmentTransitionSupport.java
rename to transition/src/main/java/android/support/transition/FragmentTransitionSupport.java
diff --git a/transition/api14/android/support/transition/GhostViewApi14.java b/transition/src/main/java/android/support/transition/GhostViewApi14.java
similarity index 100%
rename from transition/api14/android/support/transition/GhostViewApi14.java
rename to transition/src/main/java/android/support/transition/GhostViewApi14.java
diff --git a/transition/api21/android/support/transition/GhostViewApi21.java b/transition/src/main/java/android/support/transition/GhostViewApi21.java
similarity index 100%
rename from transition/api21/android/support/transition/GhostViewApi21.java
rename to transition/src/main/java/android/support/transition/GhostViewApi21.java
diff --git a/transition/base/android/support/transition/GhostViewImpl.java b/transition/src/main/java/android/support/transition/GhostViewImpl.java
similarity index 100%
rename from transition/base/android/support/transition/GhostViewImpl.java
rename to transition/src/main/java/android/support/transition/GhostViewImpl.java
diff --git a/transition/src/android/support/transition/GhostViewUtils.java b/transition/src/main/java/android/support/transition/GhostViewUtils.java
similarity index 100%
rename from transition/src/android/support/transition/GhostViewUtils.java
rename to transition/src/main/java/android/support/transition/GhostViewUtils.java
diff --git a/transition/src/android/support/transition/ImageViewUtils.java b/transition/src/main/java/android/support/transition/ImageViewUtils.java
similarity index 100%
rename from transition/src/android/support/transition/ImageViewUtils.java
rename to transition/src/main/java/android/support/transition/ImageViewUtils.java
diff --git a/transition/api14/android/support/transition/ImageViewUtilsApi14.java b/transition/src/main/java/android/support/transition/ImageViewUtilsApi14.java
similarity index 100%
rename from transition/api14/android/support/transition/ImageViewUtilsApi14.java
rename to transition/src/main/java/android/support/transition/ImageViewUtilsApi14.java
diff --git a/transition/api21/android/support/transition/ImageViewUtilsApi21.java b/transition/src/main/java/android/support/transition/ImageViewUtilsApi21.java
similarity index 100%
rename from transition/api21/android/support/transition/ImageViewUtilsApi21.java
rename to transition/src/main/java/android/support/transition/ImageViewUtilsApi21.java
diff --git a/transition/base/android/support/transition/ImageViewUtilsImpl.java b/transition/src/main/java/android/support/transition/ImageViewUtilsImpl.java
similarity index 100%
rename from transition/base/android/support/transition/ImageViewUtilsImpl.java
rename to transition/src/main/java/android/support/transition/ImageViewUtilsImpl.java
diff --git a/transition/src/android/support/transition/MatrixUtils.java b/transition/src/main/java/android/support/transition/MatrixUtils.java
similarity index 100%
rename from transition/src/android/support/transition/MatrixUtils.java
rename to transition/src/main/java/android/support/transition/MatrixUtils.java
diff --git a/transition/src/android/support/transition/ObjectAnimatorUtils.java b/transition/src/main/java/android/support/transition/ObjectAnimatorUtils.java
similarity index 100%
rename from transition/src/android/support/transition/ObjectAnimatorUtils.java
rename to transition/src/main/java/android/support/transition/ObjectAnimatorUtils.java
diff --git a/transition/api14/android/support/transition/ObjectAnimatorUtilsApi14.java b/transition/src/main/java/android/support/transition/ObjectAnimatorUtilsApi14.java
similarity index 100%
rename from transition/api14/android/support/transition/ObjectAnimatorUtilsApi14.java
rename to transition/src/main/java/android/support/transition/ObjectAnimatorUtilsApi14.java
diff --git a/transition/api21/android/support/transition/ObjectAnimatorUtilsApi21.java b/transition/src/main/java/android/support/transition/ObjectAnimatorUtilsApi21.java
similarity index 100%
rename from transition/api21/android/support/transition/ObjectAnimatorUtilsApi21.java
rename to transition/src/main/java/android/support/transition/ObjectAnimatorUtilsApi21.java
diff --git a/transition/base/android/support/transition/ObjectAnimatorUtilsImpl.java b/transition/src/main/java/android/support/transition/ObjectAnimatorUtilsImpl.java
similarity index 100%
rename from transition/base/android/support/transition/ObjectAnimatorUtilsImpl.java
rename to transition/src/main/java/android/support/transition/ObjectAnimatorUtilsImpl.java
diff --git a/transition/src/android/support/transition/PathMotion.java b/transition/src/main/java/android/support/transition/PathMotion.java
similarity index 100%
rename from transition/src/android/support/transition/PathMotion.java
rename to transition/src/main/java/android/support/transition/PathMotion.java
diff --git a/transition/api14/android/support/transition/PathProperty.java b/transition/src/main/java/android/support/transition/PathProperty.java
similarity index 100%
rename from transition/api14/android/support/transition/PathProperty.java
rename to transition/src/main/java/android/support/transition/PathProperty.java
diff --git a/transition/src/android/support/transition/PatternPathMotion.java b/transition/src/main/java/android/support/transition/PatternPathMotion.java
similarity index 100%
rename from transition/src/android/support/transition/PatternPathMotion.java
rename to transition/src/main/java/android/support/transition/PatternPathMotion.java
diff --git a/transition/src/android/support/transition/PropertyValuesHolderUtils.java b/transition/src/main/java/android/support/transition/PropertyValuesHolderUtils.java
similarity index 100%
rename from transition/src/android/support/transition/PropertyValuesHolderUtils.java
rename to transition/src/main/java/android/support/transition/PropertyValuesHolderUtils.java
diff --git a/transition/api14/android/support/transition/PropertyValuesHolderUtilsApi14.java b/transition/src/main/java/android/support/transition/PropertyValuesHolderUtilsApi14.java
similarity index 100%
rename from transition/api14/android/support/transition/PropertyValuesHolderUtilsApi14.java
rename to transition/src/main/java/android/support/transition/PropertyValuesHolderUtilsApi14.java
diff --git a/transition/api21/android/support/transition/PropertyValuesHolderUtilsApi21.java b/transition/src/main/java/android/support/transition/PropertyValuesHolderUtilsApi21.java
similarity index 100%
rename from transition/api21/android/support/transition/PropertyValuesHolderUtilsApi21.java
rename to transition/src/main/java/android/support/transition/PropertyValuesHolderUtilsApi21.java
diff --git a/transition/base/android/support/transition/PropertyValuesHolderUtilsImpl.java b/transition/src/main/java/android/support/transition/PropertyValuesHolderUtilsImpl.java
similarity index 100%
rename from transition/base/android/support/transition/PropertyValuesHolderUtilsImpl.java
rename to transition/src/main/java/android/support/transition/PropertyValuesHolderUtilsImpl.java
diff --git a/transition/src/android/support/transition/RectEvaluator.java b/transition/src/main/java/android/support/transition/RectEvaluator.java
similarity index 100%
rename from transition/src/android/support/transition/RectEvaluator.java
rename to transition/src/main/java/android/support/transition/RectEvaluator.java
diff --git a/transition/src/android/support/transition/Scene.java b/transition/src/main/java/android/support/transition/Scene.java
similarity index 100%
rename from transition/src/android/support/transition/Scene.java
rename to transition/src/main/java/android/support/transition/Scene.java
diff --git a/transition/src/android/support/transition/SidePropagation.java b/transition/src/main/java/android/support/transition/SidePropagation.java
similarity index 100%
rename from transition/src/android/support/transition/SidePropagation.java
rename to transition/src/main/java/android/support/transition/SidePropagation.java
diff --git a/transition/src/android/support/transition/Slide.java b/transition/src/main/java/android/support/transition/Slide.java
similarity index 100%
rename from transition/src/android/support/transition/Slide.java
rename to transition/src/main/java/android/support/transition/Slide.java
diff --git a/transition/src/android/support/transition/Styleable.java b/transition/src/main/java/android/support/transition/Styleable.java
similarity index 100%
rename from transition/src/android/support/transition/Styleable.java
rename to transition/src/main/java/android/support/transition/Styleable.java
diff --git a/transition/src/android/support/transition/Transition.java b/transition/src/main/java/android/support/transition/Transition.java
similarity index 100%
rename from transition/src/android/support/transition/Transition.java
rename to transition/src/main/java/android/support/transition/Transition.java
diff --git a/transition/src/android/support/transition/TransitionInflater.java b/transition/src/main/java/android/support/transition/TransitionInflater.java
similarity index 100%
rename from transition/src/android/support/transition/TransitionInflater.java
rename to transition/src/main/java/android/support/transition/TransitionInflater.java
diff --git a/transition/src/android/support/transition/TransitionListenerAdapter.java b/transition/src/main/java/android/support/transition/TransitionListenerAdapter.java
similarity index 100%
rename from transition/src/android/support/transition/TransitionListenerAdapter.java
rename to transition/src/main/java/android/support/transition/TransitionListenerAdapter.java
diff --git a/transition/src/android/support/transition/TransitionManager.java b/transition/src/main/java/android/support/transition/TransitionManager.java
similarity index 100%
rename from transition/src/android/support/transition/TransitionManager.java
rename to transition/src/main/java/android/support/transition/TransitionManager.java
diff --git a/transition/src/android/support/transition/TransitionPropagation.java b/transition/src/main/java/android/support/transition/TransitionPropagation.java
similarity index 100%
rename from transition/src/android/support/transition/TransitionPropagation.java
rename to transition/src/main/java/android/support/transition/TransitionPropagation.java
diff --git a/transition/src/android/support/transition/TransitionSet.java b/transition/src/main/java/android/support/transition/TransitionSet.java
similarity index 100%
rename from transition/src/android/support/transition/TransitionSet.java
rename to transition/src/main/java/android/support/transition/TransitionSet.java
diff --git a/transition/src/android/support/transition/TransitionUtils.java b/transition/src/main/java/android/support/transition/TransitionUtils.java
similarity index 100%
rename from transition/src/android/support/transition/TransitionUtils.java
rename to transition/src/main/java/android/support/transition/TransitionUtils.java
diff --git a/transition/src/android/support/transition/TransitionValues.java b/transition/src/main/java/android/support/transition/TransitionValues.java
similarity index 100%
rename from transition/src/android/support/transition/TransitionValues.java
rename to transition/src/main/java/android/support/transition/TransitionValues.java
diff --git a/transition/src/android/support/transition/TransitionValuesMaps.java b/transition/src/main/java/android/support/transition/TransitionValuesMaps.java
similarity index 100%
rename from transition/src/android/support/transition/TransitionValuesMaps.java
rename to transition/src/main/java/android/support/transition/TransitionValuesMaps.java
diff --git a/transition/src/android/support/transition/TranslationAnimationCreator.java b/transition/src/main/java/android/support/transition/TranslationAnimationCreator.java
similarity index 100%
rename from transition/src/android/support/transition/TranslationAnimationCreator.java
rename to transition/src/main/java/android/support/transition/TranslationAnimationCreator.java
diff --git a/transition/api14/android/support/transition/ViewGroupOverlayApi14.java b/transition/src/main/java/android/support/transition/ViewGroupOverlayApi14.java
similarity index 100%
rename from transition/api14/android/support/transition/ViewGroupOverlayApi14.java
rename to transition/src/main/java/android/support/transition/ViewGroupOverlayApi14.java
diff --git a/transition/api18/android/support/transition/ViewGroupOverlayApi18.java b/transition/src/main/java/android/support/transition/ViewGroupOverlayApi18.java
similarity index 100%
rename from transition/api18/android/support/transition/ViewGroupOverlayApi18.java
rename to transition/src/main/java/android/support/transition/ViewGroupOverlayApi18.java
diff --git a/transition/base/android/support/transition/ViewGroupOverlayImpl.java b/transition/src/main/java/android/support/transition/ViewGroupOverlayImpl.java
similarity index 100%
rename from transition/base/android/support/transition/ViewGroupOverlayImpl.java
rename to transition/src/main/java/android/support/transition/ViewGroupOverlayImpl.java
diff --git a/transition/src/android/support/transition/ViewGroupUtils.java b/transition/src/main/java/android/support/transition/ViewGroupUtils.java
similarity index 100%
rename from transition/src/android/support/transition/ViewGroupUtils.java
rename to transition/src/main/java/android/support/transition/ViewGroupUtils.java
diff --git a/transition/api14/android/support/transition/ViewGroupUtilsApi14.java b/transition/src/main/java/android/support/transition/ViewGroupUtilsApi14.java
similarity index 100%
rename from transition/api14/android/support/transition/ViewGroupUtilsApi14.java
rename to transition/src/main/java/android/support/transition/ViewGroupUtilsApi14.java
diff --git a/transition/api18/android/support/transition/ViewGroupUtilsApi18.java b/transition/src/main/java/android/support/transition/ViewGroupUtilsApi18.java
similarity index 100%
rename from transition/api18/android/support/transition/ViewGroupUtilsApi18.java
rename to transition/src/main/java/android/support/transition/ViewGroupUtilsApi18.java
diff --git a/transition/base/android/support/transition/ViewGroupUtilsImpl.java b/transition/src/main/java/android/support/transition/ViewGroupUtilsImpl.java
similarity index 100%
rename from transition/base/android/support/transition/ViewGroupUtilsImpl.java
rename to transition/src/main/java/android/support/transition/ViewGroupUtilsImpl.java
diff --git a/transition/api14/android/support/transition/ViewOverlayApi14.java b/transition/src/main/java/android/support/transition/ViewOverlayApi14.java
similarity index 100%
rename from transition/api14/android/support/transition/ViewOverlayApi14.java
rename to transition/src/main/java/android/support/transition/ViewOverlayApi14.java
diff --git a/transition/api18/android/support/transition/ViewOverlayApi18.java b/transition/src/main/java/android/support/transition/ViewOverlayApi18.java
similarity index 100%
rename from transition/api18/android/support/transition/ViewOverlayApi18.java
rename to transition/src/main/java/android/support/transition/ViewOverlayApi18.java
diff --git a/transition/base/android/support/transition/ViewOverlayImpl.java b/transition/src/main/java/android/support/transition/ViewOverlayImpl.java
similarity index 100%
rename from transition/base/android/support/transition/ViewOverlayImpl.java
rename to transition/src/main/java/android/support/transition/ViewOverlayImpl.java
diff --git a/transition/src/android/support/transition/ViewUtils.java b/transition/src/main/java/android/support/transition/ViewUtils.java
similarity index 100%
rename from transition/src/android/support/transition/ViewUtils.java
rename to transition/src/main/java/android/support/transition/ViewUtils.java
diff --git a/transition/api14/android/support/transition/ViewUtilsApi14.java b/transition/src/main/java/android/support/transition/ViewUtilsApi14.java
similarity index 100%
rename from transition/api14/android/support/transition/ViewUtilsApi14.java
rename to transition/src/main/java/android/support/transition/ViewUtilsApi14.java
diff --git a/transition/api18/android/support/transition/ViewUtilsApi18.java b/transition/src/main/java/android/support/transition/ViewUtilsApi18.java
similarity index 100%
rename from transition/api18/android/support/transition/ViewUtilsApi18.java
rename to transition/src/main/java/android/support/transition/ViewUtilsApi18.java
diff --git a/transition/api19/android/support/transition/ViewUtilsApi19.java b/transition/src/main/java/android/support/transition/ViewUtilsApi19.java
similarity index 100%
rename from transition/api19/android/support/transition/ViewUtilsApi19.java
rename to transition/src/main/java/android/support/transition/ViewUtilsApi19.java
diff --git a/transition/api21/android/support/transition/ViewUtilsApi21.java b/transition/src/main/java/android/support/transition/ViewUtilsApi21.java
similarity index 100%
rename from transition/api21/android/support/transition/ViewUtilsApi21.java
rename to transition/src/main/java/android/support/transition/ViewUtilsApi21.java
diff --git a/transition/api22/android/support/transition/ViewUtilsApi22.java b/transition/src/main/java/android/support/transition/ViewUtilsApi22.java
similarity index 100%
rename from transition/api22/android/support/transition/ViewUtilsApi22.java
rename to transition/src/main/java/android/support/transition/ViewUtilsApi22.java
diff --git a/transition/base/android/support/transition/ViewUtilsImpl.java b/transition/src/main/java/android/support/transition/ViewUtilsImpl.java
similarity index 100%
rename from transition/base/android/support/transition/ViewUtilsImpl.java
rename to transition/src/main/java/android/support/transition/ViewUtilsImpl.java
diff --git a/transition/src/android/support/transition/Visibility.java b/transition/src/main/java/android/support/transition/Visibility.java
similarity index 100%
rename from transition/src/android/support/transition/Visibility.java
rename to transition/src/main/java/android/support/transition/Visibility.java
diff --git a/transition/src/android/support/transition/VisibilityPropagation.java b/transition/src/main/java/android/support/transition/VisibilityPropagation.java
similarity index 100%
rename from transition/src/android/support/transition/VisibilityPropagation.java
rename to transition/src/main/java/android/support/transition/VisibilityPropagation.java
diff --git a/transition/api14/android/support/transition/WindowIdApi14.java b/transition/src/main/java/android/support/transition/WindowIdApi14.java
similarity index 100%
rename from transition/api14/android/support/transition/WindowIdApi14.java
rename to transition/src/main/java/android/support/transition/WindowIdApi14.java
diff --git a/transition/api18/android/support/transition/WindowIdApi18.java b/transition/src/main/java/android/support/transition/WindowIdApi18.java
similarity index 100%
rename from transition/api18/android/support/transition/WindowIdApi18.java
rename to transition/src/main/java/android/support/transition/WindowIdApi18.java
diff --git a/transition/base/android/support/transition/WindowIdImpl.java b/transition/src/main/java/android/support/transition/WindowIdImpl.java
similarity index 100%
rename from transition/base/android/support/transition/WindowIdImpl.java
rename to transition/src/main/java/android/support/transition/WindowIdImpl.java
diff --git a/transition/src/android/support/transition/package.html b/transition/src/main/java/android/support/transition/package.html
similarity index 100%
rename from transition/src/android/support/transition/package.html
rename to transition/src/main/java/android/support/transition/package.html
diff --git a/tv-provider/api/current.txt b/tv-provider/api/current.txt
index 42cad9f..80421e9 100644
--- a/tv-provider/api/current.txt
+++ b/tv-provider/api/current.txt
@@ -531,6 +531,7 @@
     method public int getWatchNextType();
     method public android.content.ContentValues toContentValues();
     method public java.lang.String toString();
+    field public static final int WATCH_NEXT_TYPE_UNKNOWN = -1; // 0xffffffff
   }
 
   public static final class WatchNextProgram.Builder {
diff --git a/tv-provider/lint-baseline.xml b/tv-provider/lint-baseline.xml
index 9814796..4387a5a 100644
--- a/tv-provider/lint-baseline.xml
+++ b/tv-provider/lint-baseline.xml
@@ -1,92 +1,4 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="4" by="lint 3.0.0-beta6">
-
-    <issue
-        id="WrongConstant"
-        message="Must be one of: PreviewProgramColumns.TYPE_MOVIE, PreviewProgramColumns.TYPE_TV_SERIES, PreviewProgramColumns.TYPE_TV_SEASON, PreviewProgramColumns.TYPE_TV_EPISODE, PreviewProgramColumns.TYPE_CLIP, PreviewProgramColumns.TYPE_EVENT, PreviewProgramColumns.TYPE_CHANNEL, PreviewProgramColumns.TYPE_TRACK, PreviewProgramColumns.TYPE_ALBUM, PreviewProgramColumns.TYPE_ARTIST, PreviewProgramColumns.TYPE_PLAYLIST, PreviewProgramColumns.TYPE_STATION, PreviewProgramColumns.TYPE_GAME"
-        errorLine1="        return i == null ? INVALID_INT_VALUE : i;"
-        errorLine2="                           ~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/android/support/media/tv/BasePreviewProgram.java"
-            line="130"
-            column="28"/>
-    </issue>
-
-    <issue
-        id="WrongConstant"
-        message="Must be one of: PreviewProgramColumns.ASPECT_RATIO_16_9, PreviewProgramColumns.ASPECT_RATIO_3_2, PreviewProgramColumns.ASPECT_RATIO_4_3, PreviewProgramColumns.ASPECT_RATIO_1_1, PreviewProgramColumns.ASPECT_RATIO_2_3, PreviewProgramColumns.ASPECT_RATIO_MOVIE_POSTER"
-        errorLine1="        return i == null ? INVALID_INT_VALUE : i;"
-        errorLine2="                           ~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/android/support/media/tv/BasePreviewProgram.java"
-            line="140"
-            column="28"/>
-    </issue>
-
-    <issue
-        id="WrongConstant"
-        message="Must be one of: PreviewProgramColumns.ASPECT_RATIO_16_9, PreviewProgramColumns.ASPECT_RATIO_3_2, PreviewProgramColumns.ASPECT_RATIO_4_3, PreviewProgramColumns.ASPECT_RATIO_1_1, PreviewProgramColumns.ASPECT_RATIO_2_3, PreviewProgramColumns.ASPECT_RATIO_MOVIE_POSTER"
-        errorLine1="        return i == null ? INVALID_INT_VALUE : i;"
-        errorLine2="                           ~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/android/support/media/tv/BasePreviewProgram.java"
-            line="150"
-            column="28"/>
-    </issue>
-
-    <issue
-        id="WrongConstant"
-        message="Must be one of: PreviewProgramColumns.AVAILABILITY_AVAILABLE, PreviewProgramColumns.AVAILABILITY_FREE_WITH_SUBSCRIPTION, PreviewProgramColumns.AVAILABILITY_PAID_CONTENT, PreviewProgramColumns.AVAILABILITY_PURCHASED, PreviewProgramColumns.AVAILABILITY_FREE"
-        errorLine1="        return i == null ? INVALID_INT_VALUE : i;"
-        errorLine2="                           ~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/android/support/media/tv/BasePreviewProgram.java"
-            line="168"
-            column="28"/>
-    </issue>
-
-    <issue
-        id="WrongConstant"
-        message="Must be one of: PreviewProgramColumns.INTERACTION_TYPE_VIEWS, PreviewProgramColumns.INTERACTION_TYPE_LISTENS, PreviewProgramColumns.INTERACTION_TYPE_FOLLOWERS, PreviewProgramColumns.INTERACTION_TYPE_FANS, PreviewProgramColumns.INTERACTION_TYPE_LIKES, PreviewProgramColumns.INTERACTION_TYPE_THUMBS, PreviewProgramColumns.INTERACTION_TYPE_VIEWERS"
-        errorLine1="        return i == null ? INVALID_INT_VALUE : i;"
-        errorLine2="                           ~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/android/support/media/tv/BasePreviewProgram.java"
-            line="219"
-            column="28"/>
-    </issue>
-
-    <issue
-        id="WrongConstant"
-        message="Must be one of: ProgramColumns.REVIEW_RATING_STYLE_STARS, ProgramColumns.REVIEW_RATING_STYLE_THUMBS_UP_DOWN, ProgramColumns.REVIEW_RATING_STYLE_PERCENTAGE"
-        errorLine1="        return i == null ? INVALID_INT_VALUE : i;"
-        errorLine2="                           ~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/android/support/media/tv/BaseProgram.java"
-            line="257"
-            column="28"/>
-    </issue>
-
-    <issue
-        id="WrongConstant"
-        message="Must be one of: Genres.FAMILY_KIDS, Genres.SPORTS, Genres.SHOPPING, Genres.MOVIES, Genres.COMEDY, Genres.TRAVEL, Genres.DRAMA, Genres.EDUCATION, Genres.ANIMAL_WILDLIFE, Genres.NEWS, Genres.GAMING, Genres.ARTS, Genres.ENTERTAINMENT, Genres.LIFE_STYLE, Genres.MUSIC, Genres.PREMIER, Genres.TECH_SCIENCE"
-        errorLine1="            mValues.put(Programs.COLUMN_BROADCAST_GENRE, Programs.Genres.encode(genres));"
-        errorLine2="                                                                                ~~~~~~">
-        <location
-            file="src/main/java/android/support/media/tv/Program.java"
-            line="286"
-            column="81"/>
-    </issue>
-
-    <issue
-        id="WrongConstant"
-        message="Must be one of: WatchNextPrograms.WATCH_NEXT_TYPE_CONTINUE, WatchNextPrograms.WATCH_NEXT_TYPE_NEXT, WatchNextPrograms.WATCH_NEXT_TYPE_NEW, WatchNextPrograms.WATCH_NEXT_TYPE_WATCHLIST"
-        errorLine1="        return i == null ? INVALID_INT_VALUE : i;"
-        errorLine2="                           ~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/android/support/media/tv/WatchNextProgram.java"
-            line="99"
-            column="28"/>
-    </issue>
+<issues format="4" by="lint 3.0.0-beta7">
 
 </issues>
diff --git a/tv-provider/src/main/java/android/support/media/tv/BasePreviewProgram.java b/tv-provider/src/main/java/android/support/media/tv/BasePreviewProgram.java
index 1423d9d..39c3014 100644
--- a/tv-provider/src/main/java/android/support/media/tv/BasePreviewProgram.java
+++ b/tv-provider/src/main/java/android/support/media/tv/BasePreviewProgram.java
@@ -23,14 +23,13 @@
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.Build;
+import android.support.annotation.IntDef;
 import android.support.annotation.RestrictTo;
 import android.support.media.tv.TvContractCompat.PreviewProgramColumns;
-import android.support.media.tv.TvContractCompat.PreviewProgramColumns.AspectRatio;
-import android.support.media.tv.TvContractCompat.PreviewProgramColumns.Availability;
-import android.support.media.tv.TvContractCompat.PreviewProgramColumns.InteractionType;
-import android.support.media.tv.TvContractCompat.PreviewProgramColumns.Type;
 import android.support.media.tv.TvContractCompat.PreviewPrograms;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.net.URISyntaxException;
 import java.text.SimpleDateFormat;
 import java.util.Date;
@@ -55,6 +54,89 @@
     private static final int IS_LIVE = 1;
     private static final int IS_BROWSABLE = 1;
 
+    /** @hide */
+    @IntDef({
+            TYPE_UNKNOWN,
+            PreviewProgramColumns.TYPE_MOVIE,
+            PreviewProgramColumns.TYPE_TV_SERIES,
+            PreviewProgramColumns.TYPE_TV_SEASON,
+            PreviewProgramColumns.TYPE_TV_EPISODE,
+            PreviewProgramColumns.TYPE_CLIP,
+            PreviewProgramColumns.TYPE_EVENT,
+            PreviewProgramColumns.TYPE_CHANNEL,
+            PreviewProgramColumns.TYPE_TRACK,
+            PreviewProgramColumns.TYPE_ALBUM,
+            PreviewProgramColumns.TYPE_ARTIST,
+            PreviewProgramColumns.TYPE_PLAYLIST,
+            PreviewProgramColumns.TYPE_STATION,
+            PreviewProgramColumns.TYPE_GAME
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @RestrictTo(LIBRARY_GROUP)
+    public @interface Type {}
+
+    /**
+     * The unknown program type.
+     */
+    private static final int TYPE_UNKNOWN = -1;
+
+    /** @hide */
+    @IntDef({
+            ASPECT_RATIO_UNKNOWN,
+            PreviewProgramColumns.ASPECT_RATIO_16_9,
+            PreviewProgramColumns.ASPECT_RATIO_3_2,
+            PreviewProgramColumns.ASPECT_RATIO_4_3,
+            PreviewProgramColumns.ASPECT_RATIO_1_1,
+            PreviewProgramColumns.ASPECT_RATIO_2_3,
+            PreviewProgramColumns.ASPECT_RATIO_MOVIE_POSTER
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @RestrictTo(LIBRARY_GROUP)
+    public @interface AspectRatio {}
+
+    /**
+     * The aspect ratio for unknown aspect ratios.
+     */
+    private static final int ASPECT_RATIO_UNKNOWN = -1;
+
+    /** @hide */
+    @IntDef({
+            AVAILABILITY_UNKNOWN,
+            PreviewProgramColumns.AVAILABILITY_AVAILABLE,
+            PreviewProgramColumns.AVAILABILITY_FREE_WITH_SUBSCRIPTION,
+            PreviewProgramColumns.AVAILABILITY_PAID_CONTENT,
+            PreviewProgramColumns.AVAILABILITY_PURCHASED,
+            PreviewProgramColumns.AVAILABILITY_FREE
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @RestrictTo(LIBRARY_GROUP)
+    public @interface Availability {}
+
+    /**
+     * The unknown availability.
+     */
+    private static final int AVAILABILITY_UNKNOWN = -1;
+
+    /** @hide */
+    @IntDef({
+            INTERACTION_TYPE_UNKNOWN,
+            PreviewProgramColumns.INTERACTION_TYPE_VIEWS,
+            PreviewProgramColumns.INTERACTION_TYPE_LISTENS,
+            PreviewProgramColumns.INTERACTION_TYPE_FOLLOWERS,
+            PreviewProgramColumns.INTERACTION_TYPE_FANS,
+            PreviewProgramColumns.INTERACTION_TYPE_LIKES,
+            PreviewProgramColumns.INTERACTION_TYPE_THUMBS,
+            PreviewProgramColumns.INTERACTION_TYPE_VIEWERS,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @RestrictTo(LIBRARY_GROUP)
+    public @interface InteractionType {}
+
+    /**
+     * The unknown interaction type.
+     */
+    private static final int INTERACTION_TYPE_UNKNOWN = -1;
+
     BasePreviewProgram(Builder builder) {
         super(builder);
     }
@@ -127,7 +209,7 @@
      */
     public @Type int getType() {
         Integer i = mValues.getAsInteger(PreviewPrograms.COLUMN_TYPE);
-        return i == null ? INVALID_INT_VALUE : i;
+        return i == null ? TYPE_UNKNOWN : i;
     }
 
     /**
@@ -137,7 +219,7 @@
      */
     public @AspectRatio int getPosterArtAspectRatio() {
         Integer i = mValues.getAsInteger(PreviewPrograms.COLUMN_POSTER_ART_ASPECT_RATIO);
-        return i == null ? INVALID_INT_VALUE : i;
+        return i == null ? ASPECT_RATIO_UNKNOWN : i;
     }
 
     /**
@@ -147,7 +229,7 @@
      */
     public @AspectRatio int getThumbnailAspectRatio() {
         Integer i = mValues.getAsInteger(PreviewPrograms.COLUMN_THUMBNAIL_ASPECT_RATIO);
-        return i == null ? INVALID_INT_VALUE : i;
+        return i == null ? ASPECT_RATIO_UNKNOWN : i;
     }
 
     /**
@@ -165,7 +247,7 @@
      */
     public @Availability int getAvailability() {
         Integer i = mValues.getAsInteger(PreviewPrograms.COLUMN_AVAILABILITY);
-        return i == null ? INVALID_INT_VALUE : i;
+        return i == null ? AVAILABILITY_UNKNOWN : i;
     }
 
     /**
@@ -216,7 +298,7 @@
      */
     public @InteractionType int getInteractionType() {
         Integer i = mValues.getAsInteger(PreviewPrograms.COLUMN_INTERACTION_TYPE);
-        return i == null ? INVALID_INT_VALUE : i;
+        return i == null ? INTERACTION_TYPE_UNKNOWN : i;
     }
 
     /**
diff --git a/tv-provider/src/main/java/android/support/media/tv/BaseProgram.java b/tv-provider/src/main/java/android/support/media/tv/BaseProgram.java
index e4ce9d1..23b5cf9 100644
--- a/tv-provider/src/main/java/android/support/media/tv/BaseProgram.java
+++ b/tv-provider/src/main/java/android/support/media/tv/BaseProgram.java
@@ -22,13 +22,16 @@
 import android.media.tv.TvContentRating;
 import android.net.Uri;
 import android.os.Build;
+import android.support.annotation.IntDef;
 import android.support.annotation.RestrictTo;
 import android.support.media.tv.TvContractCompat.BaseTvColumns;
 import android.support.media.tv.TvContractCompat.ProgramColumns;
-import android.support.media.tv.TvContractCompat.ProgramColumns.ReviewRatingStyle;
 import android.support.media.tv.TvContractCompat.Programs;
 import android.support.media.tv.TvContractCompat.Programs.Genres.Genre;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
 /**
  * Base class for derived classes that want to have common fields for programs defined in
  * {@link TvContractCompat}.
@@ -46,6 +49,22 @@
     private static final int IS_SEARCHABLE = 1;
 
     /** @hide */
+    @IntDef({
+            REVIEW_RATING_STYLE_UNKNOWN,
+            ProgramColumns.REVIEW_RATING_STYLE_STARS,
+            ProgramColumns.REVIEW_RATING_STYLE_THUMBS_UP_DOWN,
+            ProgramColumns.REVIEW_RATING_STYLE_PERCENTAGE,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @RestrictTo(LIBRARY_GROUP)
+    @interface ReviewRatingStyle {}
+
+    /**
+     * The unknown review rating style.
+     */
+    private static final int REVIEW_RATING_STYLE_UNKNOWN = -1;
+
+    /** @hide */
     @RestrictTo(LIBRARY_GROUP)
     protected ContentValues mValues;
 
@@ -254,7 +273,7 @@
      */
     public @ReviewRatingStyle int getReviewRatingStyle() {
         Integer i = mValues.getAsInteger(Programs.COLUMN_REVIEW_RATING_STYLE);
-        return i == null ? INVALID_INT_VALUE : i;
+        return i == null ? REVIEW_RATING_STYLE_UNKNOWN : i;
     }
 
     /**
diff --git a/tv-provider/src/main/java/android/support/media/tv/Program.java b/tv-provider/src/main/java/android/support/media/tv/Program.java
index 4e3bd7a..233f1ba 100644
--- a/tv-provider/src/main/java/android/support/media/tv/Program.java
+++ b/tv-provider/src/main/java/android/support/media/tv/Program.java
@@ -25,6 +25,7 @@
 import android.support.annotation.NonNull;
 import android.support.annotation.RestrictTo;
 import android.support.media.tv.TvContractCompat.Programs;
+import android.support.media.tv.TvContractCompat.Programs.Genres.Genre;
 
 /**
  * A convenience class to access {@link TvContractCompat.Programs} entries in the system content
@@ -282,7 +283,7 @@
          * @return This Builder object to allow for chaining of calls to builder methods.
          * @see Programs#COLUMN_BROADCAST_GENRE
          */
-        public Builder setBroadcastGenres(String[] genres) {
+        public Builder setBroadcastGenres(@Genre String[] genres) {
             mValues.put(Programs.COLUMN_BROADCAST_GENRE, Programs.Genres.encode(genres));
             return this;
         }
diff --git a/tv-provider/src/main/java/android/support/media/tv/TvContractCompat.java b/tv-provider/src/main/java/android/support/media/tv/TvContractCompat.java
index 5a46e79..de4fd04 100644
--- a/tv-provider/src/main/java/android/support/media/tv/TvContractCompat.java
+++ b/tv-provider/src/main/java/android/support/media/tv/TvContractCompat.java
@@ -30,7 +30,6 @@
 import android.os.Build;
 import android.os.Bundle;
 import android.provider.BaseColumns;
-import android.support.annotation.IntDef;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.annotation.RequiresApi;
@@ -606,16 +605,6 @@
      */
     @RestrictTo(LIBRARY_GROUP)
     interface ProgramColumns {
-        /** @hide */
-        @IntDef({
-                REVIEW_RATING_STYLE_STARS,
-                REVIEW_RATING_STYLE_THUMBS_UP_DOWN,
-                REVIEW_RATING_STYLE_PERCENTAGE,
-        })
-        @Retention(RetentionPolicy.SOURCE)
-        @RestrictTo(LIBRARY_GROUP)
-        @interface ReviewRatingStyle {}
-
         /**
          * The review rating style for five star rating.
          *
@@ -934,27 +923,6 @@
      */
     @RestrictTo(LIBRARY_GROUP)
     public interface PreviewProgramColumns {
-
-        /** @hide */
-        @IntDef({
-                TYPE_MOVIE,
-                TYPE_TV_SERIES,
-                TYPE_TV_SEASON,
-                TYPE_TV_EPISODE,
-                TYPE_CLIP,
-                TYPE_EVENT,
-                TYPE_CHANNEL,
-                TYPE_TRACK,
-                TYPE_ALBUM,
-                TYPE_ARTIST,
-                TYPE_PLAYLIST,
-                TYPE_STATION,
-                TYPE_GAME
-        })
-        @Retention(RetentionPolicy.SOURCE)
-        @RestrictTo(LIBRARY_GROUP)
-        public @interface Type {}
-
         /**
          * The program type for movie.
          *
@@ -1046,19 +1014,6 @@
          */
         int TYPE_GAME = 12;
 
-        /** @hide */
-        @IntDef({
-                ASPECT_RATIO_16_9,
-                ASPECT_RATIO_3_2,
-                ASPECT_RATIO_4_3,
-                ASPECT_RATIO_1_1,
-                ASPECT_RATIO_2_3,
-                ASPECT_RATIO_MOVIE_POSTER,
-        })
-        @Retention(RetentionPolicy.SOURCE)
-        @RestrictTo(LIBRARY_GROUP)
-        public @interface AspectRatio {}
-
         /**
          * The aspect ratio for 16:9.
          *
@@ -1107,18 +1062,6 @@
          */
         int ASPECT_RATIO_MOVIE_POSTER = 5;
 
-        /** @hide */
-        @IntDef({
-                AVAILABILITY_AVAILABLE,
-                AVAILABILITY_FREE_WITH_SUBSCRIPTION,
-                AVAILABILITY_PAID_CONTENT,
-                AVAILABILITY_PURCHASED,
-                AVAILABILITY_FREE,
-        })
-        @Retention(RetentionPolicy.SOURCE)
-        @RestrictTo(LIBRARY_GROUP)
-        public @interface Availability {}
-
         /**
          * The availability for "available to this user".
          *
@@ -1155,20 +1098,6 @@
          */
         int AVAILABILITY_FREE = 4;
 
-        /** @hide */
-        @IntDef({
-                INTERACTION_TYPE_VIEWS,
-                INTERACTION_TYPE_LISTENS,
-                INTERACTION_TYPE_FOLLOWERS,
-                INTERACTION_TYPE_FANS,
-                INTERACTION_TYPE_LIKES,
-                INTERACTION_TYPE_THUMBS,
-                INTERACTION_TYPE_VIEWERS,
-        })
-        @Retention(RetentionPolicy.SOURCE)
-        @RestrictTo(LIBRARY_GROUP)
-        public @interface InteractionType {}
-
         /**
          * The interaction type for "views".
          *
@@ -2895,17 +2824,6 @@
         /** The MIME type of a single preview TV program. */
         public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/watch_next_program";
 
-        /** @hide */
-        @IntDef({
-                WATCH_NEXT_TYPE_CONTINUE,
-                WATCH_NEXT_TYPE_NEXT,
-                WATCH_NEXT_TYPE_NEW,
-                WATCH_NEXT_TYPE_WATCHLIST,
-        })
-        @Retention(RetentionPolicy.SOURCE)
-        @RestrictTo(LIBRARY_GROUP)
-        public @interface WatchNextType {}
-
         /**
          * The watch next type for CONTINUE. Use this type when the user has already watched more
          * than 1 minute of this content.
diff --git a/tv-provider/src/main/java/android/support/media/tv/WatchNextProgram.java b/tv-provider/src/main/java/android/support/media/tv/WatchNextProgram.java
index f466584..c192745 100644
--- a/tv-provider/src/main/java/android/support/media/tv/WatchNextProgram.java
+++ b/tv-provider/src/main/java/android/support/media/tv/WatchNextProgram.java
@@ -22,12 +22,15 @@
 import android.database.Cursor;
 import android.media.tv.TvContentRating;  // For javadoc gen of super class
 import android.os.Build;
+import android.support.annotation.IntDef;
 import android.support.annotation.RestrictTo;
 import android.support.media.tv.TvContractCompat.PreviewPrograms;  // For javadoc gen of super class
 import android.support.media.tv.TvContractCompat.Programs;  // For javadoc gen of super class
 import android.support.media.tv.TvContractCompat.Programs.Genres;  // For javadoc gen of super class
 import android.support.media.tv.TvContractCompat.WatchNextPrograms;
-import android.support.media.tv.TvContractCompat.WatchNextPrograms.WatchNextType;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 
 /**
  * A convenience class to access {@link WatchNextPrograms} entries in the system content
@@ -87,16 +90,34 @@
     private static final long INVALID_LONG_VALUE = -1;
     private static final int INVALID_INT_VALUE = -1;
 
+    /** @hide */
+    @IntDef({
+            WATCH_NEXT_TYPE_UNKNOWN,
+            WatchNextPrograms.WATCH_NEXT_TYPE_CONTINUE,
+            WatchNextPrograms.WATCH_NEXT_TYPE_NEXT,
+            WatchNextPrograms.WATCH_NEXT_TYPE_NEW,
+            WatchNextPrograms.WATCH_NEXT_TYPE_WATCHLIST,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @RestrictTo(LIBRARY_GROUP)
+    public @interface WatchNextType {}
+
+    /**
+     * The unknown watch next type. Use this type when the actual type is not known.
+     */
+    public static final int WATCH_NEXT_TYPE_UNKNOWN = -1;
+
     private WatchNextProgram(Builder builder) {
         super(builder);
     }
 
     /**
-     * @return The value of {@link WatchNextPrograms#COLUMN_WATCH_NEXT_TYPE} for the program.
+     * @return The value of {@link WatchNextPrograms#COLUMN_WATCH_NEXT_TYPE} for the program,
+     * or {@link #WATCH_NEXT_TYPE_UNKNOWN} if it's unknown.
      */
     public @WatchNextType int getWatchNextType() {
         Integer i = mValues.getAsInteger(WatchNextPrograms.COLUMN_WATCH_NEXT_TYPE);
-        return i == null ? INVALID_INT_VALUE : i;
+        return i == null ? WATCH_NEXT_TYPE_UNKNOWN : i;
     }
 
     /**
diff --git a/v4/Android.mk b/v4/Android.mk
index a9c9145..84fd5c3 100644
--- a/v4/Android.mk
+++ b/v4/Android.mk
@@ -27,7 +27,7 @@
 LOCAL_SDK_VERSION := $(SUPPORT_CURRENT_SDK_VERSION)
 # Some projects expect to inherit android-support-annotations from
 # android-support-v4, so we need to keep it static until they can be fixed.
-LOCAL_STATIC_JAVA_LIBRARIES := \
+LOCAL_STATIC_ANDROID_LIBRARIES := \
     android-support-compat \
     android-support-media-compat \
     android-support-core-utils \
diff --git a/v7/appcompat/build.gradle b/v7/appcompat/build.gradle
index 2d57ac4..5efe0da 100644
--- a/v7/appcompat/build.gradle
+++ b/v7/appcompat/build.gradle
@@ -11,7 +11,9 @@
     androidTestImplementation libs.espresso_core,    { exclude module: 'support-annotations' }
     androidTestImplementation libs.mockito_core,     { exclude group: 'net.bytebuddy' } // DexMaker has it"s own MockMaker
     androidTestImplementation libs.dexmaker_mockito, { exclude group: 'net.bytebuddy' } // DexMaker has it"s own MockMaker
-    androidTestImplementation project(':support-testutils')
+    androidTestImplementation project(':support-testutils'), {
+        exclude group: 'com.android.support', module: 'appcompat-v7'
+    }
 }
 
 android {
diff --git a/v7/appcompat/src/main/java/android/support/v7/view/menu/CascadingMenuPopup.java b/v7/appcompat/src/main/java/android/support/v7/view/menu/CascadingMenuPopup.java
index 73499cf..564bbfc 100644
--- a/v7/appcompat/src/main/java/android/support/v7/view/menu/CascadingMenuPopup.java
+++ b/v7/appcompat/src/main/java/android/support/v7/view/menu/CascadingMenuPopup.java
@@ -404,14 +404,14 @@
             final boolean showOnRight = nextMenuPosition == HORIZ_POSITION_RIGHT;
             mLastPosition = nextMenuPosition;
 
-            final int parentOffsetLeft;
-            final int parentOffsetTop;
+            final int parentOffsetX;
+            final int parentOffsetY;
             if (Build.VERSION.SDK_INT >= 26) {
                 // Anchor the submenu directly to the parent menu item view. This allows for
                 // accurate submenu positioning when the parent menu is being moved.
                 popupWindow.setAnchorView(parentView);
-                parentOffsetLeft = 0;
-                parentOffsetTop = 0;
+                parentOffsetX = 0;
+                parentOffsetY = 0;
             } else {
                 // Framework does not allow anchoring to a view in another popup window. Use the
                 // same top-level anchor as the parent menu is using, with appropriate offsets.
@@ -428,10 +428,19 @@
                 final int[] parentViewScreenLocation = new int[2];
                 parentView.getLocationOnScreen(parentViewScreenLocation);
 
+                // For Gravity.LEFT case, the baseline is just the left border of the view. So we
+                // can use the X of the location directly. But for Gravity.RIGHT case, the baseline
+                // is the right border. So we need add view's width with the location to make the
+                // baseline as the right border correctly.
+                if ((mDropDownGravity & (Gravity.RIGHT | Gravity.LEFT)) == Gravity.RIGHT) {
+                    anchorScreenLocation[0] += mAnchorView.getWidth();
+                    parentViewScreenLocation[0] += parentView.getWidth();
+                }
+
                 // If used as horizontal/vertical offsets, these values would position the submenu
                 // at the exact same position as the parent item.
-                parentOffsetLeft = parentViewScreenLocation[0] - anchorScreenLocation[0];
-                parentOffsetTop = parentViewScreenLocation[1] - anchorScreenLocation[1];
+                parentOffsetX = parentViewScreenLocation[0] - anchorScreenLocation[0];
+                parentOffsetY = parentViewScreenLocation[1] - anchorScreenLocation[1];
             }
 
             // Adjust the horizontal offset to display the submenu to the right or to the left
@@ -441,22 +450,22 @@
             final int x;
             if ((mDropDownGravity & Gravity.RIGHT) == Gravity.RIGHT) {
                 if (showOnRight) {
-                    x = parentOffsetLeft + menuWidth;
+                    x = parentOffsetX + menuWidth;
                 } else {
-                    x = parentOffsetLeft - parentView.getWidth();
+                    x = parentOffsetX - parentView.getWidth();
                 }
             } else {
                 if (showOnRight) {
-                    x = parentOffsetLeft + parentView.getWidth();
+                    x = parentOffsetX + parentView.getWidth();
                 } else {
-                    x = parentOffsetLeft - menuWidth;
+                    x = parentOffsetX - menuWidth;
                 }
             }
             popupWindow.setHorizontalOffset(x);
 
             // Vertically align with the parent item.
             popupWindow.setOverlapAnchor(true);
-            popupWindow.setVerticalOffset(parentOffsetTop);
+            popupWindow.setVerticalOffset(parentOffsetY);
         } else {
             if (mHasXOffset) {
                 popupWindow.setHorizontalOffset(mXOffset);
diff --git a/v7/appcompat/tests/src/android/support/v7/app/NightModeTestCase.java b/v7/appcompat/tests/src/android/support/v7/app/NightModeTestCase.java
index 2981ad4..8ae5b24 100644
--- a/v7/appcompat/tests/src/android/support/v7/app/NightModeTestCase.java
+++ b/v7/appcompat/tests/src/android/support/v7/app/NightModeTestCase.java
@@ -23,13 +23,12 @@
 import static android.support.v7.app.NightModeActivity.TOP_ACTIVITY;
 import static android.support.v7.testutils.TestUtilsMatchers.isBackground;
 
-import static org.junit.Assert.assertFalse;
-
 import android.app.Instrumentation;
 import android.support.test.InstrumentationRegistry;
 import android.support.test.filters.LargeTest;
 import android.support.test.rule.ActivityTestRule;
 import android.support.test.runner.AndroidJUnit4;
+import android.support.testutils.AppCompatActivityUtils;
 import android.support.v4.content.ContextCompat;
 import android.support.v7.appcompat.test.R;
 
@@ -100,27 +99,26 @@
         TwilightManager.setInstance(twilightManager);
 
         final NightModeActivity activity = mActivityTestRule.getActivity();
-        final AppCompatDelegateImplV14 delegate = (AppCompatDelegateImplV14) activity.getDelegate();
 
         // Verify that we're currently in day mode
         onView(withId(R.id.text_night_mode)).check(matches(withText(STRING_DAY)));
 
         // Now set MODE_NIGHT_AUTO so that we will change to night mode automatically
-        setLocalNightModeAndWaitForRecreate(activity, AppCompatDelegate.MODE_NIGHT_AUTO);
-
-        // Assert that the original Activity has not been destroyed yet
-        assertFalse(activity.isDestroyed());
+        final NightModeActivity newActivity =
+                setLocalNightModeAndWaitForRecreate(activity, AppCompatDelegate.MODE_NIGHT_AUTO);
+        final AppCompatDelegateImplV14 newDelegate =
+                (AppCompatDelegateImplV14) newActivity.getDelegate();
 
         // Now update the fake twilight manager to be in night and trigger a fake 'time' change
         mActivityTestRule.runOnUiThread(new Runnable() {
             @Override
             public void run() {
                 twilightManager.setIsNight(true);
-                delegate.getAutoNightModeManager().dispatchTimeChanged();
+                newDelegate.getAutoNightModeManager().dispatchTimeChanged();
             }
         });
 
-        // Now wait for the recreate
+        // At this point recreate has been completed
         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
 
         // Now check that the text has changed, signifying that night resources are being used
@@ -133,28 +131,30 @@
         final FakeTwilightManager twilightManager = new FakeTwilightManager();
         TwilightManager.setInstance(twilightManager);
 
-        final NightModeActivity activity = mActivityTestRule.getActivity();
+        NightModeActivity activity = mActivityTestRule.getActivity();
 
         // Set MODE_NIGHT_AUTO so that we will change to night mode automatically
-        setLocalNightModeAndWaitForRecreate(activity, AppCompatDelegate.MODE_NIGHT_AUTO);
+        activity = setLocalNightModeAndWaitForRecreate(activity, AppCompatDelegate.MODE_NIGHT_AUTO);
         // Verify that we're currently in day mode
         onView(withId(R.id.text_night_mode)).check(matches(withText(STRING_DAY)));
 
+        final NightModeActivity toTest = activity;
+
         mActivityTestRule.runOnUiThread(new Runnable() {
             @Override
             public void run() {
                 final Instrumentation instrumentation =
                         InstrumentationRegistry.getInstrumentation();
                 // Now fool the Activity into thinking that it has gone into the background
-                instrumentation.callActivityOnPause(activity);
-                instrumentation.callActivityOnStop(activity);
+                instrumentation.callActivityOnPause(toTest);
+                instrumentation.callActivityOnStop(toTest);
 
                 // Now update the twilight manager while the Activity is in the 'background'
                 twilightManager.setIsNight(true);
 
                 // Now tell the Activity that it has gone into the foreground again
-                instrumentation.callActivityOnStart(activity);
-                instrumentation.callActivityOnResume(activity);
+                instrumentation.callActivityOnStart(toTest);
+                instrumentation.callActivityOnResume(toTest);
             }
         });
 
@@ -179,7 +179,8 @@
         }
     }
 
-    private void setLocalNightModeAndWaitForRecreate(final AppCompatActivity activity,
+    private NightModeActivity setLocalNightModeAndWaitForRecreate(
+            final NightModeActivity activity,
             @AppCompatDelegate.NightMode final int nightMode) throws Throwable {
         final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
         mActivityTestRule.runOnUiThread(new Runnable() {
@@ -188,6 +189,11 @@
                 activity.getDelegate().setLocalNightMode(nightMode);
             }
         });
+        final NightModeActivity result =
+                AppCompatActivityUtils.recreateActivity(mActivityTestRule, activity);
+        AppCompatActivityUtils.waitForExecution(mActivityTestRule);
+
         instrumentation.waitForIdleSync();
+        return result;
     }
 }
diff --git a/v7/appcompat/tests/src/android/support/v7/testutils/BaseTestActivity.java b/v7/appcompat/tests/src/android/support/v7/testutils/BaseTestActivity.java
index 8ed22ad..e4dbf26 100644
--- a/v7/appcompat/tests/src/android/support/v7/testutils/BaseTestActivity.java
+++ b/v7/appcompat/tests/src/android/support/v7/testutils/BaseTestActivity.java
@@ -19,7 +19,7 @@
 import android.os.Bundle;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
-import android.support.v7.app.AppCompatActivity;
+import android.support.testutils.RecreatedAppCompatActivity;
 import android.support.v7.app.AppCompatCallback;
 import android.support.v7.appcompat.test.R;
 import android.support.v7.view.ActionMode;
@@ -28,7 +28,7 @@
 import android.view.MenuItem;
 import android.view.WindowManager;
 
-public abstract class BaseTestActivity extends AppCompatActivity {
+public abstract class BaseTestActivity extends RecreatedAppCompatActivity {
 
     private Menu mMenu;
 
diff --git a/v7/preference/api/current.txt b/v7/preference/api/current.txt
index 04c7329..1b2a746 100644
--- a/v7/preference/api/current.txt
+++ b/v7/preference/api/current.txt
@@ -275,6 +275,7 @@
     method protected void dispatchRestoreInstanceState(android.os.Bundle);
     method protected void dispatchSaveInstanceState(android.os.Bundle);
     method public android.support.v7.preference.Preference findPreference(java.lang.CharSequence);
+    method public int getInitialExpandedChildrenCount();
     method public android.support.v7.preference.Preference getPreference(int);
     method public int getPreferenceCount();
     method protected boolean isOnSameScreenAsChildren();
@@ -282,6 +283,7 @@
     method protected boolean onPrepareAddPreference(android.support.v7.preference.Preference);
     method public void removeAll();
     method public boolean removePreference(android.support.v7.preference.Preference);
+    method public void setInitialExpandedChildrenCount(int);
     method public void setOrderingAsAdded(boolean);
   }
 
diff --git a/v7/preference/res/drawable/ic_arrow_down_24dp.xml b/v7/preference/res/drawable/ic_arrow_down_24dp.xml
new file mode 100644
index 0000000..7c5866d
--- /dev/null
+++ b/v7/preference/res/drawable/ic_arrow_down_24dp.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0"
+        android:tint="?android:attr/colorAccent">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M7.41,7.84L12,12.42l4.59,-4.58L18,9.25l-6,6 -6,-6z"/>
+</vector>
diff --git a/v7/preference/res/layout-v7/expand_button.xml b/v7/preference/res/layout-v7/expand_button.xml
new file mode 100644
index 0000000..35faae8
--- /dev/null
+++ b/v7/preference/res/layout-v7/expand_button.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2017 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.
+  -->
+
+<!-- Based off frameworks/base/core/res/res/layout/preference_material.xml -->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:minHeight="?android:attr/listPreferredItemHeight"
+    android:gravity="center_vertical"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+    android:background="?android:attr/selectableItemBackground"
+    android:clipToPadding="false">
+
+    <LinearLayout
+        android:id="@android:id/icon_frame"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:gravity="start|center_vertical"
+        android:orientation="horizontal"
+        android:layout_marginStart="-4dp"
+        android:minWidth="60dp"
+        android:paddingEnd="12dp"
+        android:paddingTop="4dp"
+        android:paddingBottom="4dp">
+        <android.support.v7.internal.widget.PreferenceImageView
+            android:id="@android:id/icon"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:maxWidth="48dp"
+            android:maxHeight="48dp"/>
+    </LinearLayout>
+
+    <RelativeLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_weight="1"
+        android:paddingTop="16dp"
+        android:paddingBottom="16dp">
+
+        <TextView
+            android:id="@android:id/title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textAppearance="?android:attr/textAppearanceListItem"
+            android:ellipsize="marquee"/>
+
+        <TextView
+            android:id="@android:id/summary"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_below="@android:id/title"
+            android:layout_alignStart="@android:id/title"
+            android:textAppearance="?android:attr/textAppearanceListItemSecondary"
+            android:textColor="?android:attr/textColorSecondary"
+            android:ellipsize="marquee"
+            android:singleLine="true"/>
+
+    </RelativeLayout>
+
+</LinearLayout>
diff --git a/v7/preference/res/values/attrs.xml b/v7/preference/res/values/attrs.xml
index f204d45..8ab8de1 100644
--- a/v7/preference/res/values/attrs.xml
+++ b/v7/preference/res/values/attrs.xml
@@ -91,6 +91,15 @@
              default to alphabetic for those without the order attribute. -->
         <attr name="orderingFromXml" format="boolean" />
         <attr name="android:orderingFromXml" />
+        <!-- The maximal number of children that are shown when the preference group is launched
+             where the rest of the children will be hidden. If some children are hidden an expand
+             button will be provided to show all the hidden children.
+             Any child in any level of the hierarchy that is also a preference group (e.g.
+             preference category) will not be counted towards the limit. But instead the children of
+             such group will be counted.
+             By default, all children will be shown, so the default value of this attribute is equal
+             to Integer.MAX_VALUE. -->
+        <attr name="initialExpandedChildrenCount" format="integer" />
     </declare-styleable>
 
     <!-- Base attributes available to Preference. -->
diff --git a/v7/preference/res/values/strings.xml b/v7/preference/res/values/strings.xml
index 3414e44..1788f13 100644
--- a/v7/preference/res/values/strings.xml
+++ b/v7/preference/res/values/strings.xml
@@ -1,5 +1,9 @@
 <?xml version="1.0" encoding="utf-8"?>
-<resources>
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="v7_preference_on">ON</string>
     <string name="v7_preference_off">OFF</string>
+    <!-- Title for the preference expand button [CHAR LIMIT=30] -->
+    <string name="expand_button_title">Advanced</string>
+    <!-- Summary for the preference expand button. This is used to format preference summaries as a list. [CHAR_LIMIT=NONE] -->
+    <string name="summary_collapsed_preference_list"><xliff:g id="current_items">%1$s</xliff:g>, <xliff:g id="added_items">%2$s</xliff:g></string>
 </resources>
diff --git a/v7/preference/src/main/java/android/support/v7/preference/CollapsiblePreferenceGroupController.java b/v7/preference/src/main/java/android/support/v7/preference/CollapsiblePreferenceGroupController.java
new file mode 100644
index 0000000..e15ca18
--- /dev/null
+++ b/v7/preference/src/main/java/android/support/v7/preference/CollapsiblePreferenceGroupController.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2017 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 android.support.v7.preference;
+
+import android.content.Context;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A controller to handle advanced children display logic with collapsible functionality.
+ */
+final class CollapsiblePreferenceGroupController
+        implements PreferenceGroup.PreferenceInstanceStateCallback {
+
+    private final PreferenceGroupAdapter mPreferenceGroupAdapter;
+    private int mMaxPreferenceToShow;
+    private final Context mContext;
+
+    CollapsiblePreferenceGroupController(PreferenceGroup preferenceGroup,
+            PreferenceGroupAdapter preferenceGroupAdapter) {
+        mPreferenceGroupAdapter = preferenceGroupAdapter;
+        mMaxPreferenceToShow = preferenceGroup.getInitialExpandedChildrenCount();
+        mContext = preferenceGroup.getContext();
+        preferenceGroup.setPreferenceInstanceStateCallback(this);
+    }
+
+    /**
+     * Creates the visible portion of the flattened preferences.
+     *
+     * @param flattenedPreferenceList the flattened children of the preference group
+     * @return the visible portion of the flattened preferences
+     */
+    public List<Preference> createVisiblePreferencesList(List<Preference> flattenedPreferenceList) {
+        int visiblePreferenceCount = 0;
+        final List<Preference> visiblePreferenceList =
+                new ArrayList<>(flattenedPreferenceList.size());
+        // Copy only the visible preferences to the active list up to the maximum specified
+        for (final Preference preference : flattenedPreferenceList) {
+            if (preference.isVisible()) {
+                if (visiblePreferenceCount < mMaxPreferenceToShow) {
+                    visiblePreferenceList.add(preference);
+                }
+                // Do no count PreferenceGroup as expanded preference because the list of its child
+                // is already contained in the flattenedPreferenceList
+                if (!(preference instanceof PreferenceGroup)) {
+                    visiblePreferenceCount++;
+                }
+            }
+        }
+        // If there are any visible preferences being hidden, add an expand button to show the rest
+        // of the preferences. Clicking the expand button will show all the visible preferences and
+        // reset mMaxPreferenceToShow
+        if (showLimitedChildren() && visiblePreferenceCount > mMaxPreferenceToShow) {
+            final ExpandButton expandButton  = createExpandButton(visiblePreferenceList,
+                    flattenedPreferenceList);
+            visiblePreferenceList.add(expandButton);
+        }
+        return visiblePreferenceList;
+    }
+
+    /**
+     * Called when a preference has changed its visibility.
+     *
+     * @param preference The preference whose visibility has changed.
+     * @return {@code true} if view update has been handled by this controller.
+     */
+    public boolean onPreferenceVisibilityChange(Preference preference) {
+        if (showLimitedChildren()) {
+            // We only want to show up to the max number of preferences. Preference visibility
+            // change can result in the expand button being added/removed, as well as expand button
+            // summary change. Rebulid the data to ensure the correct data is shown.
+            mPreferenceGroupAdapter.onPreferenceHierarchyChange(preference);
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public Parcelable saveInstanceState(Parcelable state) {
+        final SavedState myState = new SavedState(state);
+        myState.mMaxPreferenceToShow = mMaxPreferenceToShow;
+        return myState;
+    }
+
+    @Override
+    public Parcelable restoreInstanceState(Parcelable state) {
+        if (state == null || !state.getClass().equals(SavedState.class)) {
+            // Didn't save state for us in saveInstanceState
+            return state;
+        }
+        SavedState myState = (SavedState) state;
+        final int restoredMaxToShow = myState.mMaxPreferenceToShow;
+        if (mMaxPreferenceToShow != restoredMaxToShow) {
+            mMaxPreferenceToShow = restoredMaxToShow;
+            mPreferenceGroupAdapter.onPreferenceHierarchyChange(null);
+        }
+        return myState.getSuperState();
+    }
+
+    private ExpandButton createExpandButton(List<Preference> visiblePreferenceList,
+            List<Preference> flattenedPreferenceList) {
+        final ExpandButton preference = new ExpandButton(mContext, visiblePreferenceList,
+                flattenedPreferenceList);
+        preference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+            @Override
+            public boolean onPreferenceClick(Preference preference) {
+                mMaxPreferenceToShow = Integer.MAX_VALUE;
+                mPreferenceGroupAdapter.onPreferenceHierarchyChange(preference);
+                return true;
+            }
+        });
+        return preference;
+    }
+
+    private boolean showLimitedChildren() {
+        return mMaxPreferenceToShow != Integer.MAX_VALUE;
+    }
+
+    /**
+     * A {@link Preference} that provides capability to expand the collapsed items in the
+     * {@link PreferenceGroup}.
+     */
+    static class ExpandButton extends Preference {
+        ExpandButton(Context context, List<Preference> visiblePreferenceList,
+                List<Preference> flattenedPreferenceList) {
+            super(context);
+            initLayout();
+            setSummary(visiblePreferenceList, flattenedPreferenceList);
+        }
+
+        private void initLayout() {
+            setLayoutResource(R.layout.expand_button);
+            setIcon(R.drawable.ic_arrow_down_24dp);
+            setTitle(R.string.expand_button_title);
+            // Sets a high order so that the expand button will be placed at the bottom of the group
+            setOrder(999);
+        }
+
+        /*
+         * The summary of this will be the list of title for collapsed preferences. Iterate through
+         * the preferences not in the visible list and add its title to the summary text.
+         */
+        private void setSummary(List<Preference> visiblePreferenceList,
+                List<Preference> flattenedPreferenceList) {
+            final Preference lastVisiblePreference =
+                    visiblePreferenceList.get(visiblePreferenceList.size() - 1);
+            final int collapsedIndex = flattenedPreferenceList.indexOf(lastVisiblePreference) + 1;
+            CharSequence summary = null;
+            for (int i = collapsedIndex; i < flattenedPreferenceList.size(); i++) {
+                final Preference preference = flattenedPreferenceList.get(i);
+                if (preference instanceof PreferenceGroup) {
+                    continue;
+                }
+                final CharSequence title = preference.getTitle();
+                if (!TextUtils.isEmpty(title)) {
+                    if (summary == null) {
+                        summary = title;
+                    } else {
+                        summary = getContext().getString(
+                                R.string.summary_collapsed_preference_list, summary, title);
+                    }
+                }
+            }
+            setSummary(summary);
+        }
+
+        @Override
+        public void onBindViewHolder(PreferenceViewHolder holder) {
+            super.onBindViewHolder(holder);
+            holder.setDividerAllowedAbove(false);
+        }
+    }
+
+    /**
+     * A class for managing the instance state of a {@link PreferenceGroup}.
+     */
+    static class SavedState extends Preference.BaseSavedState {
+        int mMaxPreferenceToShow;
+
+        SavedState(Parcel source) {
+            super(source);
+            mMaxPreferenceToShow = source.readInt();
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            super.writeToParcel(dest, flags);
+            dest.writeInt(mMaxPreferenceToShow);
+        }
+
+        SavedState(Parcelable superState) {
+            super(superState);
+        }
+
+        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];
+                    }
+                };
+    }
+}
diff --git a/v7/preference/src/main/java/android/support/v7/preference/PreferenceGroup.java b/v7/preference/src/main/java/android/support/v7/preference/PreferenceGroup.java
index d285ee6..a951e70 100644
--- a/v7/preference/src/main/java/android/support/v7/preference/PreferenceGroup.java
+++ b/v7/preference/src/main/java/android/support/v7/preference/PreferenceGroup.java
@@ -22,7 +22,9 @@
 import android.content.res.TypedArray;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.Parcelable;
 import android.support.annotation.RestrictTo;
+import android.support.annotation.VisibleForTesting;
 import android.support.v4.content.res.TypedArrayUtils;
 import android.support.v4.util.SimpleArrayMap;
 import android.text.TextUtils;
@@ -45,6 +47,7 @@
  * </div>
  *
  * @attr name android:orderingFromXml
+ * @attr name initialExpandedChildrenCount
  */
 public abstract class PreferenceGroup extends Preference {
     /**
@@ -60,6 +63,9 @@
 
     private boolean mAttachedToHierarchy = false;
 
+    private int mInitialExpandedChildrenCount = Integer.MAX_VALUE;
+    private PreferenceInstanceStateCallback mPreferenceInstanceStateCallback;
+
     private final SimpleArrayMap<String, Long> mIdRecycleCache = new SimpleArrayMap<>();
     private final Handler mHandler = new Handler();
     private final Runnable mClearRecycleCacheRunnable = new Runnable() {
@@ -83,6 +89,11 @@
                 TypedArrayUtils.getBoolean(a, R.styleable.PreferenceGroup_orderingFromXml,
                         R.styleable.PreferenceGroup_orderingFromXml, true);
 
+        if (a.hasValue(R.styleable.PreferenceGroup_initialExpandedChildrenCount)) {
+            mInitialExpandedChildrenCount = TypedArrayUtils.getInt(
+                    a, R.styleable.PreferenceGroup_initialExpandedChildrenCount,
+                            R.styleable.PreferenceGroup_initialExpandedChildrenCount, -1);
+        }
         a.recycle();
     }
 
@@ -120,6 +131,35 @@
     }
 
     /**
+     * Sets the maximal number of children that are shown when the preference group is launched
+     * where the rest of the children will be hidden.
+     * If some children are hidden an expand button will be provided to show all the hidden
+     * children. Any child in any level of the hierarchy that is also a preference group (e.g.
+     * preference category) will not be counted towards the limit. But instead the children of such
+     * group will be counted.
+     * By default, all children will be shown, so the default value of this attribute is equal to
+     * Integer.MAX_VALUE.
+     *
+     * @param expandedCount the number of children that is initially shown.
+     *
+     * @attr ref R.styleable#PreferenceGroup_initialExpandedChildrenCount
+     */
+    public void setInitialExpandedChildrenCount(int expandedCount) {
+        mInitialExpandedChildrenCount = expandedCount;
+    }
+
+    /**
+     * Gets the maximal number of children that is initially shown.
+     *
+     * @return the maximal number of children that is initially shown.
+     *
+     * @attr ref R.styleable#PreferenceGroup_initialExpandedChildrenCount
+     */
+    public int getInitialExpandedChildrenCount() {
+        return mInitialExpandedChildrenCount;
+    }
+
+    /**
      * Called by the inflater to add an item to this group.
      */
     public void addItemFromInflater(Preference preference) {
@@ -400,6 +440,44 @@
         }
     }
 
+    @Override
+    protected Parcelable onSaveInstanceState() {
+        final Parcelable superState = super.onSaveInstanceState();
+        if (mPreferenceInstanceStateCallback != null) {
+            return mPreferenceInstanceStateCallback.saveInstanceState(superState);
+        }
+        return superState;
+    }
+
+    @Override
+    protected void onRestoreInstanceState(Parcelable state) {
+        if (mPreferenceInstanceStateCallback != null) {
+            state = mPreferenceInstanceStateCallback.restoreInstanceState(state);
+        }
+        super.onRestoreInstanceState(state);
+    }
+
+    /**
+     * Sets the instance state callback.
+     *
+     * @param callback The callback.
+     * @see #onSaveInstanceState()
+     * @see #onRestoreInstanceState()
+     */
+    final void setPreferenceInstanceStateCallback(PreferenceInstanceStateCallback callback) {
+        mPreferenceInstanceStateCallback = callback;
+    }
+
+    /**
+     * Gets the instance state callback.
+     *
+     * @return the instance state callback.
+     */
+    @VisibleForTesting
+    final PreferenceInstanceStateCallback getPreferenceInstanceStateCallback() {
+        return mPreferenceInstanceStateCallback;
+    }
+
     /**
      * Interface for PreferenceGroup Adapters to implement so that
      * {@link android.support.v14.preference.PreferenceFragment#scrollToPreference(String)} and
@@ -426,4 +504,29 @@
          */
         int getPreferenceAdapterPosition(Preference preference);
     }
+
+    /**
+     * Interface for callback to implement so that they can save and restore the preference group's
+     * instance state.
+     */
+    interface PreferenceInstanceStateCallback {
+
+        /**
+         * Save the internal state that can later be used to create a new instance with that
+         * same state.
+         *
+         * @param state The Parcelable to save the current dynamic state.
+         */
+        Parcelable saveInstanceState(Parcelable state);
+
+        /**
+         * Restore the previously saved state from the given parcelable.
+         *
+         * @param state The Parcelable that holds the previously saved state.
+         * @return the super state if data has been saved in the state in {@link saveInstanceState}
+         *         or state otherwise
+         */
+        Parcelable restoreInstanceState(Parcelable state);
+    }
+
 }
diff --git a/v7/preference/src/main/java/android/support/v7/preference/PreferenceGroupAdapter.java b/v7/preference/src/main/java/android/support/v7/preference/PreferenceGroupAdapter.java
index d1c630f..00a0c5b 100644
--- a/v7/preference/src/main/java/android/support/v7/preference/PreferenceGroupAdapter.java
+++ b/v7/preference/src/main/java/android/support/v7/preference/PreferenceGroupAdapter.java
@@ -22,6 +22,7 @@
 import android.graphics.drawable.Drawable;
 import android.os.Handler;
 import android.support.annotation.RestrictTo;
+import android.support.annotation.VisibleForTesting;
 import android.support.v4.content.ContextCompat;
 import android.support.v4.view.ViewCompat;
 import android.support.v7.util.DiffUtil;
@@ -73,7 +74,9 @@
 
     private PreferenceLayout mTempPreferenceLayout = new PreferenceLayout();
 
-    private Handler mHandler = new Handler();
+    private Handler mHandler;
+
+    private CollapsiblePreferenceGroupController mPreferenceGroupController;
 
     private Runnable mSyncRunnable = new Runnable() {
         @Override
@@ -117,7 +120,14 @@
     }
 
     public PreferenceGroupAdapter(PreferenceGroup preferenceGroup) {
+        this(preferenceGroup, new Handler());
+    }
+
+    private PreferenceGroupAdapter(PreferenceGroup preferenceGroup, Handler handler) {
         mPreferenceGroup = preferenceGroup;
+        mHandler = handler;
+        mPreferenceGroupController =
+                new CollapsiblePreferenceGroupController(preferenceGroup, this);
         // If this group gets or loses any children, let us know
         mPreferenceGroup.setOnPreferenceChangeInternalListener(this);
 
@@ -134,6 +144,12 @@
         syncMyPreferences();
     }
 
+    @VisibleForTesting
+    static PreferenceGroupAdapter createInstanceWithCustomHandler(PreferenceGroup preferenceGroup,
+            Handler handler) {
+        return new PreferenceGroupAdapter(preferenceGroup, handler);
+    }
+
     private void syncMyPreferences() {
         for (final Preference preference : mPreferenceListInternal) {
             // Clear out the listeners in anticipation of some items being removed. This listener
@@ -143,13 +159,8 @@
         final List<Preference> fullPreferenceList = new ArrayList<>(mPreferenceListInternal.size());
         flattenPreferenceGroup(fullPreferenceList, mPreferenceGroup);
 
-        final List<Preference> visiblePreferenceList = new ArrayList<>(fullPreferenceList.size());
-        // Copy only the visible preferences to the active list
-        for (final Preference preference : fullPreferenceList) {
-            if (preference.isVisible()) {
-                visiblePreferenceList.add(preference);
-            }
-        }
+        final List<Preference> visiblePreferenceList =
+                mPreferenceGroupController.createVisiblePreferencesList(fullPreferenceList);
 
         final List<Preference> oldVisibleList = mPreferenceList;
         mPreferenceList = visiblePreferenceList;
@@ -277,6 +288,9 @@
         if (!mPreferenceListInternal.contains(preference)) {
             return;
         }
+        if (mPreferenceGroupController.onPreferenceVisibilityChange(preference)) {
+            return;
+        }
         if (preference.isVisible()) {
             // The preference has become visible, we need to add it in the correct location.
 
diff --git a/v7/preference/tests/src/android/support/v7/preference/PreferenceGroupInitialExpandedChildrenCountTest.java b/v7/preference/tests/src/android/support/v7/preference/PreferenceGroupInitialExpandedChildrenCountTest.java
new file mode 100644
index 0000000..4f53b9a
--- /dev/null
+++ b/v7/preference/tests/src/android/support/v7/preference/PreferenceGroupInitialExpandedChildrenCountTest.java
@@ -0,0 +1,383 @@
+/*
+ * Copyright (C) 2017 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 android.support.v7.preference;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertNotNull;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyLong;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Parcelable;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.annotation.UiThreadTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class PreferenceGroupInitialExpandedChildrenCountTest {
+
+    private static final int INITIAL_EXPANDED_COUNT = 5;
+    private static final int TOTAL_PREFERENCE = 10;
+    private static final String PREFERENCE_TITLE_PREFIX = "Preference_";
+
+    private Context mContext;
+    private PreferenceManager mPreferenceManager;
+    private PreferenceScreen mScreen;
+    private Handler mHandler;
+    private List<Preference> mPreferenceList;
+
+    @Before
+    @UiThreadTest
+    public void setup() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mContext = InstrumentationRegistry.getTargetContext();
+        mPreferenceManager = new PreferenceManager(mContext);
+        mScreen = mPreferenceManager.createPreferenceScreen(mContext);
+
+        // Add 10 preferences to the screen and to the cache
+        mPreferenceList = new ArrayList<>();
+        createTestPreferences(mScreen, mPreferenceList, TOTAL_PREFERENCE);
+
+        // Execute the handler task immediately
+        mHandler = spy(new Handler());
+        doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(InvocationOnMock invocation) throws Throwable {
+                Object[] args = invocation.getArguments();
+                Message message = (Message) args[0];
+                mHandler.dispatchMessage(message);
+                return null;
+            }
+        }).when(mHandler).sendMessageDelayed(any(Message.class), anyLong());
+    }
+
+    /**
+     * Verifies that when PreferenceGroupAdapter is created, the PreferenceInstanceStateCallback
+     * is set on the PreferenceGroup.
+     */
+    @Test
+    @UiThreadTest
+    public void createPreferenceGroupAdapter_setPreferenceInstanceStateCallback() {
+        PreferenceGroupAdapter preferenceGroupAdapter = new PreferenceGroupAdapter(mScreen);
+        assertNotNull(mScreen.getPreferenceInstanceStateCallback());
+    }
+
+    /**
+     * Verifies that PreferenceGroupAdapter is showing the preferences on the screen correctly with
+     * and without the collapsed child count set.
+     */
+    @Test
+    @UiThreadTest
+    public void createPreferenceGroupAdapter_displayTopLevelPreferences() {
+        // No limit, should display all 10 preferences
+        PreferenceGroupAdapter preferenceGroupAdapter = new PreferenceGroupAdapter(mScreen);
+        assertPreferencesAreExpanded(preferenceGroupAdapter);
+
+        // Limit > child count, should display all 10 preferences
+        mScreen.setInitialExpandedChildrenCount(TOTAL_PREFERENCE + 4);
+        preferenceGroupAdapter = new PreferenceGroupAdapter(mScreen);
+        assertPreferencesAreExpanded(preferenceGroupAdapter);
+
+        // Limit = child count, should display all 10 preferences
+        mScreen.setInitialExpandedChildrenCount(TOTAL_PREFERENCE);
+        preferenceGroupAdapter = new PreferenceGroupAdapter(mScreen);
+        assertPreferencesAreExpanded(preferenceGroupAdapter);
+
+        // Limit < child count, should display up to the limit + expand button
+        mScreen.setInitialExpandedChildrenCount(INITIAL_EXPANDED_COUNT);
+        preferenceGroupAdapter = new PreferenceGroupAdapter(mScreen);
+        assertPreferencesAreCollapsed(preferenceGroupAdapter);
+        for (int i = 0; i < INITIAL_EXPANDED_COUNT; i++) {
+            assertEquals(mPreferenceList.get(i), preferenceGroupAdapter.getItem(i));
+        }
+        assertEquals(CollapsiblePreferenceGroupController.ExpandButton.class,
+                preferenceGroupAdapter.getItem(INITIAL_EXPANDED_COUNT).getClass());
+    }
+
+    /**
+     * Verifies that PreferenceGroupAdapter is showing nested preferences on the screen correctly
+     * with and without the collapsed child count set.
+     */
+    @Test
+    @UiThreadTest
+    public void createPreferenceGroupAdapter_displayNestedPreferences() {
+        final PreferenceScreen screen = mPreferenceManager.createPreferenceScreen(mContext);
+        final List<Preference> preferenceList = new ArrayList<>();
+
+        // Add 2 preferences and 2 categories to screen
+        createTestPreferences(screen, preferenceList, 2);
+        createTestPreferencesCategory(screen, preferenceList, 4);
+        createTestPreferencesCategory(screen, preferenceList, 4);
+
+        // No limit, should display all 10 preferences + 2 categories
+        PreferenceGroupAdapter preferenceGroupAdapter = new PreferenceGroupAdapter(screen);
+        assertEquals(TOTAL_PREFERENCE + 2, preferenceGroupAdapter.getItemCount());
+
+        // Limit > child count, should display all 10 preferences + 2 categories
+        screen.setInitialExpandedChildrenCount(TOTAL_PREFERENCE + 4);
+        preferenceGroupAdapter = new PreferenceGroupAdapter(screen);
+        assertEquals(TOTAL_PREFERENCE + 2, preferenceGroupAdapter.getItemCount());
+
+        // Limit = child count, should display all 10 preferences + 2 categories
+        screen.setInitialExpandedChildrenCount(TOTAL_PREFERENCE);
+        preferenceGroupAdapter = new PreferenceGroupAdapter(screen);
+        assertEquals(TOTAL_PREFERENCE + 2, preferenceGroupAdapter.getItemCount());
+
+        // Limit < child count, should display 2 preferences and the first 3 preference in the
+        // category + expand button
+        screen.setInitialExpandedChildrenCount(INITIAL_EXPANDED_COUNT);
+        preferenceGroupAdapter = new PreferenceGroupAdapter(screen);
+        assertEquals(INITIAL_EXPANDED_COUNT + 2, preferenceGroupAdapter.getItemCount());
+        for (int i = 0; i <= INITIAL_EXPANDED_COUNT; i++) {
+            assertEquals(preferenceList.get(i), preferenceGroupAdapter.getItem(i));
+        }
+        assertEquals(CollapsiblePreferenceGroupController.ExpandButton.class,
+                preferenceGroupAdapter.getItem(INITIAL_EXPANDED_COUNT + 1).getClass());
+    }
+
+    /**
+     * Verifies that correct summary is set for the expand button.
+     */
+    @Test
+    @UiThreadTest
+    public void createPreferenceGroupAdapter_setExpandButtonSummary() {
+        mScreen.setInitialExpandedChildrenCount(INITIAL_EXPANDED_COUNT);
+        PreferenceGroupAdapter preferenceGroupAdapter = new PreferenceGroupAdapter(mScreen);
+        // Preference 5 to Preference 9 are collapsed
+        CharSequence summary = mPreferenceList.get(INITIAL_EXPANDED_COUNT).getTitle();
+        for (int i = INITIAL_EXPANDED_COUNT + 1; i < TOTAL_PREFERENCE; i++) {
+            summary = mContext.getString(R.string.summary_collapsed_preference_list,
+                    summary, mPreferenceList.get(i).getTitle());
+        }
+        final Preference expandButton = preferenceGroupAdapter.getItem(INITIAL_EXPANDED_COUNT);
+        assertEquals(summary, expandButton.getSummary());
+    }
+
+    /**
+     * Verifies that clicking the expand button will show all preferences.
+     */
+    @Test
+    @UiThreadTest
+    public void clickExpandButton_shouldShowAllPreferences() {
+        mScreen.setInitialExpandedChildrenCount(INITIAL_EXPANDED_COUNT);
+
+        // First showing 5 preference with expand button
+        PreferenceGroupAdapter preferenceGroupAdapter =
+                PreferenceGroupAdapter.createInstanceWithCustomHandler(mScreen, mHandler);
+        assertPreferencesAreCollapsed(preferenceGroupAdapter);
+
+        // Click the expand button, should review all preferences
+        final Preference expandButton = preferenceGroupAdapter.getItem(INITIAL_EXPANDED_COUNT);
+        expandButton.performClick();
+        assertPreferencesAreExpanded(preferenceGroupAdapter);
+    }
+
+    /**
+     * Verifies that when preference visibility changes, it will sync the preferences only if some
+     * preferences are collapsed.
+     */
+    @Test
+    @UiThreadTest
+    public void onPreferenceVisibilityChange_shouldSyncPreferencesIfCollapsed() {
+        // No limit set, should not sync preference
+        PreferenceGroupAdapter preferenceGroupAdapter =
+                PreferenceGroupAdapter.createInstanceWithCustomHandler(mScreen, mHandler);
+        preferenceGroupAdapter.onPreferenceVisibilityChange(mPreferenceList.get(3));
+        verify(mHandler, never()).sendMessageDelayed(any(Message.class), anyLong());
+
+        // Has limit set, should sync preference
+        mScreen.setInitialExpandedChildrenCount(INITIAL_EXPANDED_COUNT);
+        preferenceGroupAdapter =
+                PreferenceGroupAdapter.createInstanceWithCustomHandler(mScreen, mHandler);
+        preferenceGroupAdapter.onPreferenceVisibilityChange(mPreferenceList.get(3));
+        verify(mHandler).sendMessageDelayed(any(Message.class), anyLong());
+
+        // Preferences expanded already, should not sync preference
+        final Preference expandButton = preferenceGroupAdapter.getItem(INITIAL_EXPANDED_COUNT);
+        expandButton.performClick();
+        reset(mHandler);
+        preferenceGroupAdapter.onPreferenceVisibilityChange(mPreferenceList.get(3));
+        verify(mHandler, never()).sendMessageDelayed(any(Message.class), anyLong());
+    }
+
+    /**
+     * Verifies that the correct maximum number of preferences to show is being saved in the
+     * instance state.
+     */
+    @Test
+    @UiThreadTest
+    public void saveInstanceState_shouldSaveMaxNumberOfChildrenToShow() {
+        // No limit set, should save max value
+        PreferenceGroupAdapter preferenceGroupAdapter = new PreferenceGroupAdapter(mScreen);
+        Parcelable state = mScreen.onSaveInstanceState();
+        assertEquals(CollapsiblePreferenceGroupController.SavedState.class, state.getClass());
+        assertEquals(Integer.MAX_VALUE,
+                ((CollapsiblePreferenceGroupController.SavedState) state).mMaxPreferenceToShow);
+
+        // Has limit set, should save limit
+        mScreen.setInitialExpandedChildrenCount(INITIAL_EXPANDED_COUNT);
+        preferenceGroupAdapter = new PreferenceGroupAdapter(mScreen);
+        state = mScreen.onSaveInstanceState();
+        assertEquals(CollapsiblePreferenceGroupController.SavedState.class, state.getClass());
+        assertEquals(INITIAL_EXPANDED_COUNT,
+                ((CollapsiblePreferenceGroupController.SavedState) state).mMaxPreferenceToShow);
+
+        // Preferences expanded already, should save max value
+        final Preference expandButton = preferenceGroupAdapter.getItem(INITIAL_EXPANDED_COUNT);
+        expandButton.performClick();
+        state = mScreen.onSaveInstanceState();
+        assertEquals(CollapsiblePreferenceGroupController.SavedState.class, state.getClass());
+        assertEquals(Integer.MAX_VALUE,
+                ((CollapsiblePreferenceGroupController.SavedState) state).mMaxPreferenceToShow);
+    }
+
+    /**
+     * Verifies that if we restore to the same number of preferences to show, it will not update
+     * anything.
+     */
+    @Test
+    @UiThreadTest
+    public void restoreInstanceState_noChange_shouldDoNothing() {
+        Parcelable baseState = Preference.BaseSavedState.EMPTY_STATE;
+        // Initialized as expanded, restore with no saved data, should remain expanded
+        PreferenceGroupAdapter preferenceGroupAdapter =
+                PreferenceGroupAdapter.createInstanceWithCustomHandler(mScreen, mHandler);
+        mScreen.onRestoreInstanceState(baseState);
+        assertPreferencesAreExpanded(preferenceGroupAdapter);
+        verify(mHandler, never()).sendMessageDelayed(any(Message.class), anyLong());
+
+        // Initialized as collapsed, restore with no saved data, should remain collapsed
+        mScreen.setInitialExpandedChildrenCount(INITIAL_EXPANDED_COUNT);
+        preferenceGroupAdapter =
+                PreferenceGroupAdapter.createInstanceWithCustomHandler(mScreen, mHandler);
+        mScreen.onRestoreInstanceState(baseState);
+        assertPreferencesAreCollapsed(preferenceGroupAdapter);
+        verify(mHandler, never()).sendMessageDelayed(any(Message.class), anyLong());
+
+        CollapsiblePreferenceGroupController.SavedState state =
+                new CollapsiblePreferenceGroupController.SavedState(baseState);
+        // Initialized as expanded, restore as expanded, should remain expanded
+        state.mMaxPreferenceToShow = Integer.MAX_VALUE;
+        mScreen.setInitialExpandedChildrenCount(Integer.MAX_VALUE);
+        preferenceGroupAdapter =
+                PreferenceGroupAdapter.createInstanceWithCustomHandler(mScreen, mHandler);
+        mScreen.onRestoreInstanceState(state);
+        assertPreferencesAreExpanded(preferenceGroupAdapter);
+        verify(mHandler, never()).sendMessageDelayed(any(Message.class), anyLong());
+
+        // Initialized as collapsed, restore as collapsed, should remain collapsed
+        state.mMaxPreferenceToShow = INITIAL_EXPANDED_COUNT;
+        mScreen.setInitialExpandedChildrenCount(INITIAL_EXPANDED_COUNT);
+        preferenceGroupAdapter =
+                PreferenceGroupAdapter.createInstanceWithCustomHandler(mScreen, mHandler);
+        mScreen.onRestoreInstanceState(state);
+        assertPreferencesAreCollapsed(preferenceGroupAdapter);
+        verify(mHandler, never()).sendMessageDelayed(any(Message.class), anyLong());
+    }
+
+    /**
+     * Verifies that if the children is collapsed previously, they should be collapsed after the
+     * state is being restored.
+     */
+    @Test
+    @UiThreadTest
+    public void restoreHierarchyState_previouslyCollapsed_shouldRestoreToCollapsedState() {
+        CollapsiblePreferenceGroupController.SavedState state =
+                new CollapsiblePreferenceGroupController.SavedState(
+                        Preference.BaseSavedState.EMPTY_STATE);
+        // Initialized as expanded, restore as collapsed, should collapse
+        state.mMaxPreferenceToShow = INITIAL_EXPANDED_COUNT;
+        mScreen.setInitialExpandedChildrenCount(Integer.MAX_VALUE);
+        PreferenceGroupAdapter preferenceGroupAdapter =
+                PreferenceGroupAdapter.createInstanceWithCustomHandler(mScreen, mHandler);
+        mScreen.onRestoreInstanceState(state);
+        verify(mHandler).sendMessageDelayed(any(Message.class), anyLong());
+        assertPreferencesAreCollapsed(preferenceGroupAdapter);
+    }
+
+    /**
+     * Verifies that if the children is expanded previously, they should be expanded after the
+     * state is being restored.
+     */
+    @Test
+    @UiThreadTest
+    public void restoreHierarchyState_previouslyExpanded_shouldRestoreToExpandedState() {
+        CollapsiblePreferenceGroupController.SavedState state =
+                new CollapsiblePreferenceGroupController.SavedState(
+                        Preference.BaseSavedState.EMPTY_STATE);
+        // Initialized as collapsed, restore as expanded, should expand
+        state.mMaxPreferenceToShow = Integer.MAX_VALUE;
+        mScreen.setInitialExpandedChildrenCount(INITIAL_EXPANDED_COUNT);
+        PreferenceGroupAdapter preferenceGroupAdapter =
+                PreferenceGroupAdapter.createInstanceWithCustomHandler(mScreen, mHandler);
+        mScreen.onRestoreInstanceState(state);
+        verify(mHandler).sendMessageDelayed(any(Message.class), anyLong());
+        assertPreferencesAreExpanded(preferenceGroupAdapter);
+    }
+
+    // assert that the preferences are all expanded
+    private void assertPreferencesAreExpanded(PreferenceGroupAdapter adapter) {
+        assertEquals(TOTAL_PREFERENCE, adapter.getItemCount());
+    }
+
+    // assert that the preferences exceeding the limit are collapsed
+    private void assertPreferencesAreCollapsed(PreferenceGroupAdapter adapter) {
+        // list shows preferences up to the limit and the expand button
+        assertEquals(INITIAL_EXPANDED_COUNT + 1, adapter.getItemCount());
+    }
+
+    // create the number of preference in the corresponding preference group and add it to the cache
+    private void createTestPreferences(PreferenceGroup preferenceGroup,
+            List<Preference> preferenceList, int numPreference) {
+        for (int i = 0; i < numPreference; i++) {
+            final Preference preference = new Preference(mContext);
+            preference.setTitle(PREFERENCE_TITLE_PREFIX + i);
+            preferenceGroup.addPreference(preference);
+            preferenceList.add(preference);
+        }
+    }
+
+    // add a preference category and add the number of preference to it and the cache
+    private void createTestPreferencesCategory(PreferenceGroup preferenceGroup,
+            List<Preference> preferenceList, int numPreference) {
+        PreferenceCategory category = new PreferenceCategory(mContext);
+        preferenceGroup.addPreference(category);
+        preferenceList.add(category);
+        createTestPreferences(category, preferenceList, numPreference);
+    }
+
+}
diff --git a/v7/recyclerview/api/current.txt b/v7/recyclerview/api/current.txt
index 9b4500a..aa79213 100644
--- a/v7/recyclerview/api/current.txt
+++ b/v7/recyclerview/api/current.txt
@@ -55,6 +55,13 @@
     method public void dispatchUpdatesTo(android.support.v7.util.ListUpdateCallback);
   }
 
+  public static abstract class DiffUtil.ItemCallback<T> {
+    ctor public DiffUtil.ItemCallback();
+    method public abstract boolean areContentsTheSame(T, T);
+    method public abstract boolean areItemsTheSame(T, T);
+    method public java.lang.Object getChangePayload(T, T);
+  }
+
   public abstract interface ListUpdateCallback {
     method public abstract void onChanged(int, int, java.lang.Object);
     method public abstract void onInserted(int, int);
@@ -305,6 +312,7 @@
     method public android.support.v7.widget.RecyclerView.ViewHolder getChildViewHolder(android.view.View);
     method public android.support.v7.widget.RecyclerViewAccessibilityDelegate getCompatAccessibilityDelegate();
     method public void getDecoratedBoundsWithMargins(android.view.View, android.graphics.Rect);
+    method public android.support.v7.widget.RecyclerView.EdgeEffectFactory getEdgeEffectFactory();
     method public android.support.v7.widget.RecyclerView.ItemAnimator getItemAnimator();
     method public android.support.v7.widget.RecyclerView.ItemDecoration getItemDecorationAt(int);
     method public int getItemDecorationCount();
@@ -339,6 +347,7 @@
     method public void setAccessibilityDelegateCompat(android.support.v7.widget.RecyclerViewAccessibilityDelegate);
     method public void setAdapter(android.support.v7.widget.RecyclerView.Adapter);
     method public void setChildDrawingOrderCallback(android.support.v7.widget.RecyclerView.ChildDrawingOrderCallback);
+    method public void setEdgeEffectFactory(android.support.v7.widget.RecyclerView.EdgeEffectFactory);
     method public void setHasFixedSize(boolean);
     method public void setItemAnimator(android.support.v7.widget.RecyclerView.ItemAnimator);
     method public void setItemViewCacheSize(int);
@@ -417,6 +426,18 @@
     method public abstract int onGetChildDrawingOrder(int, int);
   }
 
+  public static class RecyclerView.EdgeEffectFactory {
+    ctor public RecyclerView.EdgeEffectFactory();
+    method protected android.widget.EdgeEffect createEdgeEffect(android.support.v7.widget.RecyclerView, int);
+    field public static final int DIRECTION_BOTTOM = 3; // 0x3
+    field public static final int DIRECTION_LEFT = 0; // 0x0
+    field public static final int DIRECTION_RIGHT = 2; // 0x2
+    field public static final int DIRECTION_TOP = 1; // 0x1
+  }
+
+  public static abstract class RecyclerView.EdgeEffectFactory.EdgeDirection implements java.lang.annotation.Annotation {
+  }
+
   public static abstract class RecyclerView.ItemAnimator {
     ctor public RecyclerView.ItemAnimator();
     method public abstract boolean animateAppearance(android.support.v7.widget.RecyclerView.ViewHolder, android.support.v7.widget.RecyclerView.ItemAnimator.ItemHolderInfo, android.support.v7.widget.RecyclerView.ItemAnimator.ItemHolderInfo);
diff --git a/v7/recyclerview/src/main/java/android/support/v7/util/DiffUtil.java b/v7/recyclerview/src/main/java/android/support/v7/util/DiffUtil.java
index 6302666..ebc33f3 100644
--- a/v7/recyclerview/src/main/java/android/support/v7/util/DiffUtil.java
+++ b/v7/recyclerview/src/main/java/android/support/v7/util/DiffUtil.java
@@ -16,6 +16,7 @@
 
 package android.support.v7.util;
 
+import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.annotation.VisibleForTesting;
 import android.support.v7.widget.RecyclerView;
@@ -348,6 +349,72 @@
     }
 
     /**
+     * Callback for calculating the diff between two non-null items in a list.
+     * <p>
+     * {@link Callback} serves two roles - list indexing, and item diffing. ItemCallback handles
+     * just the second of these, which allows separation of code that indexes into an array or List
+     * from the presentation-layer and content specific diffing code.
+     *
+     * @param <T> Type of items to compare.
+     */
+    public abstract static class ItemCallback<T> {
+        /**
+         * Called to check whether two objects represent the same item.
+         * <p>
+         * For example, if your items have unique ids, this method should check their id equality.
+         *
+         * @param oldItem The item in the old list.
+         * @param newItem The item in the new list.
+         * @return True if the two items represent the same object or false if they are different.
+         *
+         * @see Callback#areItemsTheSame(int, int)
+         */
+        public abstract boolean areItemsTheSame(@NonNull T oldItem, @NonNull T newItem);
+
+        /**
+         * Called to check whether two items have the same data.
+         * <p>
+         * This information is used to detect if the contents of an item have changed.
+         * <p>
+         * This method to check equality instead of {@link Object#equals(Object)} so that you can
+         * change its behavior depending on your UI.
+         * <p>
+         * For example, if you are using DiffUtil with a
+         * {@link android.support.v7.widget.RecyclerView.Adapter RecyclerView.Adapter}, you should
+         * return whether the items' visual representations are the same.
+         * <p>
+         * This method is called only if {@link #areItemsTheSame(T, T)} returns {@code true} for
+         * these items.
+         *
+         * @param oldItem The item in the old list.
+         * @param newItem The item in the new list.
+         * @return True if the contents of the items are the same or false if they are different.
+         *
+         * @see Callback#areContentsTheSame(int, int)
+         */
+        public abstract boolean areContentsTheSame(@NonNull T oldItem, @NonNull T newItem);
+
+        /**
+         * When {@link #areItemsTheSame(T, T)} returns {@code true} for two items and
+         * {@link #areContentsTheSame(T, T)} returns false for them, this method is called to
+         * get a payload about the change.
+         * <p>
+         * For example, if you are using DiffUtil with {@link RecyclerView}, you can return the
+         * particular field that changed in the item and your
+         * {@link android.support.v7.widget.RecyclerView.ItemAnimator ItemAnimator} can use that
+         * information to run the correct animation.
+         * <p>
+         * Default implementation returns {@code null}.
+         *
+         * @see Callback#getChangePayload(int, int)
+         */
+        @SuppressWarnings({"WeakerAccess", "unused"})
+        public Object getChangePayload(@NonNull T oldItem, @NonNull T newItem) {
+            return null;
+        }
+    }
+
+    /**
      * Snakes represent a match between two lists. It is optionally prefixed or postfixed with an
      * add or remove operation. See the Myers' paper for details.
      */
diff --git a/v7/recyclerview/src/main/java/android/support/v7/widget/RecyclerView.java b/v7/recyclerview/src/main/java/android/support/v7/widget/RecyclerView.java
index dea8546..7009733 100644
--- a/v7/recyclerview/src/main/java/android/support/v7/widget/RecyclerView.java
+++ b/v7/recyclerview/src/main/java/android/support/v7/widget/RecyclerView.java
@@ -44,6 +44,7 @@
 import android.support.annotation.RestrictTo;
 import android.support.annotation.VisibleForTesting;
 import android.support.v4.os.TraceCompat;
+import android.support.v4.util.Preconditions;
 import android.support.v4.view.AbsSavedState;
 import android.support.v4.view.InputDeviceCompat;
 import android.support.v4.view.MotionEventCompat;
@@ -417,6 +418,8 @@
      */
     private int mDispatchScrollCounter = 0;
 
+    @NonNull
+    private EdgeEffectFactory mEdgeEffectFactory = new EdgeEffectFactory();
     private EdgeEffect mLeftGlow, mTopGlow, mRightGlow, mBottomGlow;
 
     ItemAnimator mItemAnimator = new DefaultItemAnimator();
@@ -2306,7 +2309,7 @@
         if (mLeftGlow != null) {
             return;
         }
-        mLeftGlow = new EdgeEffect(getContext());
+        mLeftGlow = mEdgeEffectFactory.createEdgeEffect(this, EdgeEffectFactory.DIRECTION_LEFT);
         if (mClipToPadding) {
             mLeftGlow.setSize(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(),
                     getMeasuredWidth() - getPaddingLeft() - getPaddingRight());
@@ -2319,7 +2322,7 @@
         if (mRightGlow != null) {
             return;
         }
-        mRightGlow = new EdgeEffect(getContext());
+        mRightGlow = mEdgeEffectFactory.createEdgeEffect(this, EdgeEffectFactory.DIRECTION_RIGHT);
         if (mClipToPadding) {
             mRightGlow.setSize(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(),
                     getMeasuredWidth() - getPaddingLeft() - getPaddingRight());
@@ -2332,7 +2335,7 @@
         if (mTopGlow != null) {
             return;
         }
-        mTopGlow = new EdgeEffect(getContext());
+        mTopGlow = mEdgeEffectFactory.createEdgeEffect(this, EdgeEffectFactory.DIRECTION_TOP);
         if (mClipToPadding) {
             mTopGlow.setSize(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
                     getMeasuredHeight() - getPaddingTop() - getPaddingBottom());
@@ -2346,7 +2349,7 @@
         if (mBottomGlow != null) {
             return;
         }
-        mBottomGlow = new EdgeEffect(getContext());
+        mBottomGlow = mEdgeEffectFactory.createEdgeEffect(this, EdgeEffectFactory.DIRECTION_BOTTOM);
         if (mClipToPadding) {
             mBottomGlow.setSize(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
                     getMeasuredHeight() - getPaddingTop() - getPaddingBottom());
@@ -2360,6 +2363,32 @@
     }
 
     /**
+     * Set a {@link EdgeEffectFactory} for this {@link RecyclerView}.
+     * <p>
+     * When a new {@link EdgeEffectFactory} is set, any existing over-scroll effects are cleared
+     * and new effects are created as needed using
+     * {@link EdgeEffectFactory#createEdgeEffect(RecyclerView, int)}
+     *
+     * @param edgeEffectFactory The {@link EdgeEffectFactory} instance.
+     */
+    public void setEdgeEffectFactory(@NonNull EdgeEffectFactory edgeEffectFactory) {
+        Preconditions.checkNotNull(edgeEffectFactory);
+        mEdgeEffectFactory = edgeEffectFactory;
+        invalidateGlows();
+    }
+
+    /**
+     * Retrieves the previously set {@link EdgeEffectFactory} or the default factory if nothing
+     * was set.
+     *
+     * @return The previously set {@link EdgeEffectFactory}
+     * @see #setEdgeEffectFactory(EdgeEffectFactory)
+     */
+    public EdgeEffectFactory getEdgeEffectFactory() {
+        return mEdgeEffectFactory;
+    }
+
+    /**
      * Since RecyclerView is a collection ViewGroup that includes virtual children (items that are
      * in the Adapter but not visible in the UI), it employs a more involved focus search strategy
      * that differs from other ViewGroups.
@@ -5130,6 +5159,46 @@
     }
 
     /**
+     * EdgeEffectFactory lets you customize the over-scroll edge effect for RecyclerViews.
+     *
+     * @see RecyclerView#setEdgeEffectFactory(EdgeEffectFactory)
+     */
+    public static class EdgeEffectFactory {
+
+        @Retention(RetentionPolicy.SOURCE)
+        @IntDef({DIRECTION_LEFT, DIRECTION_TOP, DIRECTION_RIGHT, DIRECTION_BOTTOM})
+        public @interface EdgeDirection {}
+
+        /**
+         * Direction constant for the left edge
+         */
+        public static final int DIRECTION_LEFT = 0;
+
+        /**
+         * Direction constant for the top edge
+         */
+        public static final int DIRECTION_TOP = 1;
+
+        /**
+         * Direction constant for the right edge
+         */
+        public static final int DIRECTION_RIGHT = 2;
+
+        /**
+         * Direction constant for the bottom edge
+         */
+        public static final int DIRECTION_BOTTOM = 3;
+
+        /**
+         * Create a new EdgeEffect for the provided direction.
+         */
+        protected @NonNull EdgeEffect createEdgeEffect(RecyclerView view,
+                @EdgeDirection int direction) {
+            return new EdgeEffect(view.getContext());
+        }
+    }
+
+    /**
      * RecycledViewPool lets you share Views between multiple RecyclerViews.
      * <p>
      * If you want to recycle views across RecyclerViews, create an instance of RecycledViewPool
diff --git a/v7/recyclerview/tests/src/android/support/v7/util/TouchUtils.java b/v7/recyclerview/tests/src/android/support/v7/util/TouchUtils.java
index 1a64e3c..418d33f 100644
--- a/v7/recyclerview/tests/src/android/support/v7/util/TouchUtils.java
+++ b/v7/recyclerview/tests/src/android/support/v7/util/TouchUtils.java
@@ -154,6 +154,18 @@
         inst.waitForIdleSync();
     }
 
+    public static void scrollView(int axis, int axisValue, int inputDevice, View v) {
+        MotionEvent.PointerProperties[] pointerProperties = { new MotionEvent.PointerProperties() };
+        MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
+        coords.setAxisValue(axis, axisValue);
+        MotionEvent.PointerCoords[] pointerCoords = { coords };
+        MotionEvent e = MotionEvent.obtain(
+                0, System.currentTimeMillis(), MotionEvent.ACTION_SCROLL,
+                1, pointerProperties, pointerCoords, 0, 0, 1, 1, 0, 0, inputDevice, 0);
+        v.onGenericMotionEvent(e);
+        e.recycle();
+    }
+
     public static void dragViewToTop(Instrumentation inst, View v) {
         dragViewToTop(inst, v, calculateStepsForDistance(v.getTop()));
     }
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/CustomEdgeEffectTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/CustomEdgeEffectTest.java
new file mode 100644
index 0000000..0644416
--- /dev/null
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/CustomEdgeEffectTest.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2017 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 android.support.v7.widget;
+
+
+import static android.support.v7.widget.RecyclerView.EdgeEffectFactory;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.view.InputDeviceCompat;
+import android.support.v7.util.TouchUtils;
+import android.view.MotionEvent;
+import android.view.ViewGroup;
+import android.widget.EdgeEffect;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests custom edge effect are properly applied when scrolling.
+ */
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class CustomEdgeEffectTest extends BaseRecyclerViewInstrumentationTest {
+
+    private static final int NUM_ITEMS = 10;
+
+    private LinearLayoutManager mLayoutManager;
+    private RecyclerView mRecyclerView;
+
+    @Before
+    public void setup() throws Throwable {
+        mLayoutManager = new LinearLayoutManager(getActivity());
+        mLayoutManager.ensureLayoutState();
+
+        mRecyclerView = new RecyclerView(getActivity());
+        mRecyclerView.setLayoutManager(mLayoutManager);
+        mRecyclerView.setAdapter(new TestAdapter(NUM_ITEMS) {
+
+            @Override
+            public TestViewHolder onCreateViewHolder(ViewGroup parent,
+                    int viewType) {
+                TestViewHolder holder = super.onCreateViewHolder(parent, viewType);
+                holder.itemView.setMinimumHeight(mRecyclerView.getMeasuredHeight() * 2 / NUM_ITEMS);
+                return holder;
+            }
+        });
+        setRecyclerView(mRecyclerView);
+        getInstrumentation().waitForIdleSync();
+        assertThat("Test sanity", mRecyclerView.getChildCount() > 0, is(true));
+    }
+
+    @Test
+    public void testEdgeEffectDirections() throws Throwable {
+        TestEdgeEffectFactory factory = new TestEdgeEffectFactory();
+        mRecyclerView.setEdgeEffectFactory(factory);
+        scrollToPosition(0);
+        waitForIdleScroll(mRecyclerView);
+        scrollViewBy(3);
+        assertNull(factory.mBottom);
+        assertNotNull(factory.mTop);
+        assertTrue(factory.mTop.mPullDistance > 0);
+
+        scrollToPosition(NUM_ITEMS - 1);
+        waitForIdleScroll(mRecyclerView);
+        scrollViewBy(-3);
+
+        assertNotNull(factory.mBottom);
+        assertTrue(factory.mBottom.mPullDistance > 0);
+    }
+
+    @Test
+    public void testEdgeEffectReplaced() throws Throwable {
+        TestEdgeEffectFactory factory1 = new TestEdgeEffectFactory();
+        mRecyclerView.setEdgeEffectFactory(factory1);
+        scrollToPosition(0);
+        waitForIdleScroll(mRecyclerView);
+
+        scrollViewBy(3);
+        assertNotNull(factory1.mTop);
+        float oldPullDistance = factory1.mTop.mPullDistance;
+
+        waitForIdleScroll(mRecyclerView);
+        TestEdgeEffectFactory factory2 = new TestEdgeEffectFactory();
+        mRecyclerView.setEdgeEffectFactory(factory2);
+        scrollViewBy(30);
+        assertNotNull(factory2.mTop);
+
+        assertTrue(factory2.mTop.mPullDistance > oldPullDistance);
+        assertEquals(oldPullDistance, factory1.mTop.mPullDistance, 0.1f);
+    }
+
+    private void scrollViewBy(final int value) throws Throwable {
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                TouchUtils.scrollView(MotionEvent.AXIS_VSCROLL, value,
+                        InputDeviceCompat.SOURCE_CLASS_POINTER, mRecyclerView);
+            }
+        });
+    }
+
+    private class TestEdgeEffectFactory extends EdgeEffectFactory {
+
+        TestEdgeEffect mTop, mBottom;
+
+        @NonNull
+        @Override
+        protected EdgeEffect createEdgeEffect(RecyclerView view, int direction) {
+            TestEdgeEffect effect = new TestEdgeEffect(view.getContext());
+            if (direction == EdgeEffectFactory.DIRECTION_TOP) {
+                mTop = effect;
+            } else if (direction == EdgeEffectFactory.DIRECTION_BOTTOM) {
+                mBottom = effect;
+            }
+            return effect;
+        }
+    }
+
+    private class TestEdgeEffect extends EdgeEffect {
+
+        private float mPullDistance;
+
+        TestEdgeEffect(Context context) {
+            super(context);
+        }
+
+        @Override
+        public void onPull(float deltaDistance, float displacement) {
+            onPull(deltaDistance);
+        }
+
+        @Override
+        public void onPull(float deltaDistance) {
+            mPullDistance = deltaDistance;
+        }
+    }
+}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewOnGenericMotionEventTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewOnGenericMotionEventTest.java
index aee15dd..2f80156 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewOnGenericMotionEventTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewOnGenericMotionEventTest.java
@@ -24,8 +24,8 @@
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
 import android.support.v4.view.InputDeviceCompat;
-import android.support.v4.view.MotionEventCompat;
 import android.support.v4.view.ViewConfigurationCompat;
+import android.support.v7.util.TouchUtils;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewConfiguration;
@@ -60,9 +60,8 @@
         MockLayoutManager layoutManager = new MockLayoutManager(true, true);
         mRecyclerView.setLayoutManager(layoutManager);
         layout();
-        MotionEvent e = obtainScrollMotionEvent(
-                MotionEventCompat.AXIS_SCROLL, 2, InputDeviceCompat.SOURCE_ROTARY_ENCODER);
-        mRecyclerView.onGenericMotionEvent(e);
+        TouchUtils.scrollView(
+                MotionEvent.AXIS_SCROLL, 2, InputDeviceCompat.SOURCE_ROTARY_ENCODER, mRecyclerView);
         assertTotalScroll(0, (int) (-2f * getScaledVerticalScrollFactor()));
     }
 
@@ -73,9 +72,8 @@
         MockLayoutManager layoutManager = new MockLayoutManager(true, false);
         mRecyclerView.setLayoutManager(layoutManager);
         layout();
-        MotionEvent e = obtainScrollMotionEvent(
-                MotionEventCompat.AXIS_SCROLL, 2, InputDeviceCompat.SOURCE_ROTARY_ENCODER);
-        mRecyclerView.onGenericMotionEvent(e);
+        TouchUtils.scrollView(
+                MotionEvent.AXIS_SCROLL, 2, InputDeviceCompat.SOURCE_ROTARY_ENCODER, mRecyclerView);
         assertTotalScroll((int) (2f * getScaledHorizontalScrollFactor()), 0);
     }
 
@@ -84,9 +82,8 @@
         MockLayoutManager layoutManager = new MockLayoutManager(true, true);
         mRecyclerView.setLayoutManager(layoutManager);
         layout();
-        MotionEvent e = obtainScrollMotionEvent(
-                MotionEventCompat.AXIS_VSCROLL, 2, InputDeviceCompat.SOURCE_CLASS_POINTER);
-        mRecyclerView.onGenericMotionEvent(e);
+        TouchUtils.scrollView(
+                MotionEvent.AXIS_VSCROLL, 2, InputDeviceCompat.SOURCE_CLASS_POINTER, mRecyclerView);
         assertTotalScroll(0, (int) (-2f * getScaledVerticalScrollFactor()));
     }
 
@@ -95,9 +92,8 @@
         MockLayoutManager layoutManager = new MockLayoutManager(true, true);
         mRecyclerView.setLayoutManager(layoutManager);
         layout();
-        MotionEvent e = obtainScrollMotionEvent(
-                MotionEventCompat.AXIS_HSCROLL, 2, InputDeviceCompat.SOURCE_CLASS_POINTER);
-        mRecyclerView.onGenericMotionEvent(e);
+        TouchUtils.scrollView(
+                MotionEvent.AXIS_HSCROLL, 2, InputDeviceCompat.SOURCE_CLASS_POINTER, mRecyclerView);
         assertTotalScroll((int) (2f * getScaledHorizontalScrollFactor()), 0);
     }