Merge "Cancel pending hover tooltip when updating tooltip text." into oc-mr1-support-27.0-dev
am: 35802d2ddd  -s ours

Change-Id: I0bc8b93d647ee875be3e148868e45490447dbb7f
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/init.gradle b/buildSrc/init.gradle
index 6b613d0..ccbd35a 100644
--- a/buildSrc/init.gradle
+++ b/buildSrc/init.gradle
@@ -150,8 +150,7 @@
         // Only modify Android projects.
         if (project.name.equals('doclava')
                 || project.name.equals('jdiff')
-                || project.name.equals('noto-emoji-compat')
-                || project.name.equals('support-media-compat-test-lib')) {
+                || project.name.equals('noto-emoji-compat')) {
             // disable tests and return
             project.tasks.whenTaskAdded { task ->
                 if (task instanceof org.gradle.api.tasks.testing.Test) {
diff --git a/buildSrc/src/main/java/android/support/LibraryVersions.java b/buildSrc/src/main/java/android/support/LibraryVersions.java
index 64f1840..27a52bd 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.1");
+    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/OWNERS b/car/OWNERS
new file mode 100644
index 0000000..d226975
--- /dev/null
+++ b/car/OWNERS
@@ -0,0 +1 @@
+ajchen@google.com
\ No newline at end of file
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..c1be3a3
--- /dev/null
+++ b/car/build.gradle
@@ -0,0 +1,39 @@
+import android.support.LibraryGroups
+
+plugins {
+    id("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.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
+}
+
+android {
+    defaultConfig {
+        minSdkVersion 24
+    }
+
+    sourceSets {
+        main.res.srcDirs 'res', 'res-public'
+    }
+}
+
+supportLibrary {
+    name = "Android Car Support UI"
+    publish = false
+    mavenGroup = LibraryGroups.SUPPORT
+    inceptionYear = "2017"
+    description = "Android Car Support UI"
+    java8Library = true
+    legacySourceLocation = true
+}
diff --git a/media-compat-test-client/lint-baseline.xml b/car/lint-baseline.xml
similarity index 100%
copy from media-compat-test-client/lint-baseline.xml
copy to car/lint-baseline.xml
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/anim/fade_in_trans_left.xml b/car/res/anim/fade_in_trans_left.xml
new file mode 100644
index 0000000..2d6bab5
--- /dev/null
+++ b/car/res/anim/fade_in_trans_left.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.
+-->
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+     android:duration="@android:integer/config_shortAnimTime">
+    <translate
+        android:interpolator="@android:interpolator/decelerate_quint"
+        android:fromXDelta="-10%p"
+        android:toXDelta="0" />
+
+    <alpha
+        android:fromAlpha="0.2"
+        android:toAlpha="1"
+        android:interpolator="@android:interpolator/decelerate_quint" />
+</set>
diff --git a/car/res/anim/fade_in_trans_left_layout_anim.xml b/car/res/anim/fade_in_trans_left_layout_anim.xml
new file mode 100644
index 0000000..e7660db
--- /dev/null
+++ b/car/res/anim/fade_in_trans_left_layout_anim.xml
@@ -0,0 +1,20 @@
+<?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.
+-->
+<layoutAnimation
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:animation="@anim/fade_in_trans_left"
+    android:delay="0%"
+    android:animationOrder="normal" />
diff --git a/car/res/anim/fade_in_trans_right.xml b/car/res/anim/fade_in_trans_right.xml
new file mode 100644
index 0000000..5cbeb59
--- /dev/null
+++ b/car/res/anim/fade_in_trans_right.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.
+-->
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+     android:duration="@android:integer/config_shortAnimTime">
+    <translate
+        android:interpolator="@android:interpolator/decelerate_quint"
+        android:fromXDelta="10%p"
+        android:toXDelta="0" />
+
+    <alpha
+        android:fromAlpha="0.2"
+        android:toAlpha="1"
+        android:interpolator="@android:interpolator/decelerate_quint" />
+</set>
diff --git a/car/res/anim/fade_in_trans_right_layout_anim.xml b/car/res/anim/fade_in_trans_right_layout_anim.xml
new file mode 100644
index 0000000..b76de23
--- /dev/null
+++ b/car/res/anim/fade_in_trans_right_layout_anim.xml
@@ -0,0 +1,20 @@
+<?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.
+-->
+<layoutAnimation
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:animation="@anim/fade_in_trans_right"
+    android:delay="0%"
+    android:animationOrder="normal" />
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_button_background.xml b/car/res/drawable/car_button_background.xml
new file mode 100644
index 0000000..3b139d9
--- /dev/null
+++ b/car/res/drawable/car_button_background.xml
@@ -0,0 +1,31 @@
+<?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.
+-->
+<!-- Default background styles for car buttons when enabled/disabled. -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_enabled="false">
+        <shape android:shape="rectangle">
+            <corners android:radius="@dimen/car_button_radius" />
+            <solid android:color="@color/car_grey_300"/>
+        </shape>
+    </item>
+    <item>
+        <shape android:shape="rectangle">
+            <corners android:radius="@dimen/car_button_radius" />
+            <solid android:color="@color/car_highlight"/>
+        </shape>
+    </item>
+</selector>
diff --git a/car/res/drawable/car_button_text_color.xml b/car/res/drawable/car_button_text_color.xml
new file mode 100644
index 0000000..b14ec68
--- /dev/null
+++ b/car/res/drawable/car_button_text_color.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.
+-->
+<!-- Default text colors for car buttons when enabled/disabled. -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_enabled="false" android:color="@color/car_grey_700" />
+    <item android:color="@color/car_action1"/>
+</selector>
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..812acb4
--- /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/car_app_bar_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..9136aae
--- /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_double_line_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..2818eef
--- /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_single_line_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_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..47a82ff
--- /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_margin"
+        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..88f05e3
--- /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/car_app_bar_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-h1752dp/dimens.xml b/car/res/values-h1752dp/dimens.xml
new file mode 100644
index 0000000..93aa85f
--- /dev/null
+++ b/car/res/values-h1752dp/dimens.xml
@@ -0,0 +1,38 @@
+<?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>
+    <!-- Type Sizings -->
+    <dimen name="car_title_size">32sp</dimen>
+    <dimen name="car_title2_size">40sp</dimen>
+    <dimen name="car_headline1_size">56sp</dimen>
+    <dimen name="car_headline2_size">50sp</dimen>
+    <dimen name="car_body1_size">40sp</dimen>
+    <dimen name="car_body2_size">32sp</dimen>
+    <dimen name="car_action1_size">32sp</dimen>
+
+    <!-- Car Component Dimensions -->
+    <!-- Application Bar Height -->
+    <dimen name="car_app_bar_height">112dp</dimen>
+
+    <dimen name="car_touch_target">96dp</dimen>
+
+    <!-- Icon dimensions -->
+    <dimen name="car_primary_icon_size">56dp</dimen>
+    <dimen name="car_secondary_icon_size">36dp</dimen>
+
+    <!-- Line heights -->
+    <dimen name="car_double_line_list_item_height">128dp</dimen>
+</resources>
diff --git a/car/res/values-h684dp/dimens.xml b/car/res/values-h684dp/dimens.xml
new file mode 100644
index 0000000..72b04f2
--- /dev/null
+++ b/car/res/values-h684dp/dimens.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.
+-->
+<resources>
+    <!-- Car Component Dimensions -->
+    <dimen name="car_app_bar_height">96dp</dimen>
+
+    <!-- List and Drawer Dimensions -->
+    <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>
+
+    <!-- Line heights -->
+    <dimen name="car_double_line_list_item_height">116dp</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..ea46dcf
--- /dev/null
+++ b/car/res/values-w1280dp/dimens.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.
+-->
+<resources>
+    <dimen name="car_screen_margin_size">148dp</dimen>
+    <dimen name="car_scroll_bar_button_size">76dp</dimen>
+    
+    <dimen name="car_keyline_1">32dp</dimen>
+    <dimen name="car_keyline_2">108dp</dimen>
+    <dimen name="car_keyline_3">128dp</dimen>
+    <dimen name="car_keyline_4">168dp</dimen>
+    <dimen name="car_keyline_1_neg">32dp</dimen>
+    <dimen name="car_keyline_2_neg">108dp</dimen>
+    <dimen name="car_keyline_3_neg">128dp</dimen>
+</resources>
diff --git a/car/res/values-w1920dp/dimens.xml b/car/res/values-w1920dp/dimens.xml
new file mode 100644
index 0000000..9914613
--- /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_4">184dp</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-w690dp/dimens.xml b/car/res/values-w690dp/dimens.xml
new file mode 100644
index 0000000..edf6c59
--- /dev/null
+++ b/car/res/values-w690dp/dimens.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>
+    <dimen name="car_keyline_1">24dp</dimen>
+    <dimen name="car_keyline_2">96dp</dimen>
+    <dimen name="car_keyline_3">112dp</dimen>
+    <dimen name="car_keyline_4">148dp</dimen>
+    <dimen name="car_keyline_1_neg">-24dp</dimen>
+    <dimen name="car_keyline_2_neg">-96dp</dimen>
+    <dimen name="car_keyline_3_neg">-112dp</dimen>
+</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..8b4d992
--- /dev/null
+++ b/car/res/values-w840dp/dimens.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>
+    <dimen name="car_keyline_1">32dp</dimen>
+    <dimen name="car_keyline_2">108dp</dimen>
+    <dimen name="car_keyline_3">128dp</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..0ba8f55
--- /dev/null
+++ b/car/res/values/attrs.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.
+-->
+<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" />
+        <!-- An optional spacing between items in the list -->
+        <attr name="itemSpacing" format="dimension" />
+        <!-- The icon to be used for the up button of the scroll bar. -->
+        <attr name="upButtonIcon" format="reference" />
+        <!-- The icon to be used for the down button of the scroll bar.  -->
+        <attr name="downButtonIcon" format="reference" />
+    </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..00c6cf9
--- /dev/null
+++ b/car/res/values/colors.xml
@@ -0,0 +1,160 @@
+<?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_200">#ffeeeeee</color>
+    <color name="car_grey_300">#ffe0e0e0</color>
+    <color name="car_grey_400">#ffbdbdbd</color>
+    <color name="car_grey_500">#ff9e9e9e</color>
+    <color name="car_grey_600">#ff757575</color>
+    <color name="car_grey_650">#ff6B6B6B</color>
+    <color name="car_grey_700">#ff616161</color>
+    <color name="car_grey_800">#ff424242</color>
+    <color name="car_grey_900">#ff212121</color>
+    <color name="car_grey_1000">#cc000000</color>
+    <color name="car_white_1000">#1effffff</color>
+    <color name="car_blue_grey_800">#ff37474F</color>
+    <color name="car_blue_grey_900">#ff263238</color>
+    <color name="car_dark_blue_grey_600">#ff1d272d</color>
+    <color name="car_dark_blue_grey_700">#ff172026</color>
+    <color name="car_dark_blue_grey_800">#ff11181d</color>
+    <color name="car_dark_blue_grey_900">#ff0c1013</color>
+    <color name="car_dark_blue_grey_1000">#ff090c0f</color>
+    <color name="car_light_blue_300">#ff4fc3f7</color>
+    <color name="car_light_blue_500">#ff03A9F4</color>
+    <color name="car_light_blue_600">#ff039be5</color>
+    <color name="car_light_blue_700">#ff0288d1</color>
+    <color name="car_light_blue_800">#ff0277bd</color>
+    <color name="car_light_blue_900">#ff01579b</color>
+    <color name="car_blue_300">#ff91a7ff</color>
+    <color name="car_blue_500">#ff5677fc</color>
+    <color name="car_green_500">#ff0f9d58</color>
+    <color name="car_green_700">#ff0b8043</color>
+    <color name="car_yellow_500">#fff4b400</color>
+    <color name="car_yellow_800">#ffee8100</color>
+    <color name="car_red_400">#ffe06055</color>
+    <color name="car_red_500">#ffdb4437</color>
+    <color name="car_red_500a">#ffd50000</color>
+    <color name="car_red_700">#ffc53929</color>
+    <color name="car_teal_200">#ff80cbc4</color>
+    <color name="car_teal_700">#ff00796b</color>
+    <color name="car_indigo_800">#ff283593</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_headline1_light">@color/car_grey_100</color>
+    <color name="car_headline1_dark">@color/car_grey_800</color>
+    <color name="car_headline1">@color/car_headline1_dark</color>
+
+    <color name="car_headline2_light">@color/car_grey_100</color>
+    <color name="car_headline2_dark">@color/car_grey_900</color>
+    <color name="car_headline2">@color/car_headline2_dark</color>
+
+    <color name="car_headline3_light">@android:color/white</color>
+    <color name="car_headline3_dark">@color/car_grey_900</color>
+    <color name="car_headline3">@color/car_headline3_dark</color>
+
+    <color name="car_headline4_light">@android:color/white</color>
+    <color name="car_headline4_dark">@android:color/black</color>
+    <color name="car_headline4">@color/car_headline4_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_300</color>
+    <color name="car_body2_dark">@color/car_grey_650</color>
+    <color name="car_body2">@color/car_body2_dark</color>
+
+    <color name="car_body3_light">@android:color/white</color>
+    <color name="car_body3_dark">@android:color/black</color>
+    <color name="car_body3">@color/car_body3_dark</color>
+
+    <color name="car_body4_light">@android:color/white</color>
+    <color name="car_body4_dark">@android:color/black</color>
+    <color name="car_body4">@color/car_body4_dark</color>
+
+    <color name="car_action1_light">@color/car_grey_900</color>
+    <color name="car_action1_dark">@color/car_grey_50</color>
+    <color name="car_action1">@color/car_action1_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_dark_blue_grey_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">#8F000000</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>
+
+    <!-- Misc colors -->
+    <color name="car_highlight_light">@color/car_teal_700</color>
+    <color name="car_highlight_dark">@color/car_teal_200</color>
+    <color name="car_highlight">@color/car_highlight_light</color>
+</resources>
diff --git a/car/res/values/dimens.xml b/car/res/values/dimens.xml
new file mode 100644
index 0000000..c42437d
--- /dev/null
+++ b/car/res/values/dimens.xml
@@ -0,0 +1,150 @@
+<?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">48dp</dimen>
+    <dimen name="car_keyline_2">108dp</dimen>
+    <dimen name="car_keyline_3">152dp</dimen>
+    <dimen name="car_keyline_4">182dp</dimen>
+    <dimen name="car_keyline_1_neg">-48dp</dimen>
+    <dimen name="car_keyline_2_neg">-108dp</dimen>
+    <dimen name="car_keyline_3_neg">-152dp</dimen>
+
+    <!-- Type Sizings -->
+    <dimen name="car_title_size">26sp</dimen>
+    <dimen name="car_title2_size">32sp</dimen>
+    <dimen name="car_headline1_size">45sp</dimen>
+    <dimen name="car_headline2_size">36sp</dimen>
+    <dimen name="car_headline3_size">24sp</dimen>
+    <dimen name="car_headline4_size">20sp</dimen>
+    <dimen name="car_body1_size">32sp</dimen>
+    <dimen name="car_body2_size">26sp</dimen>
+    <dimen name="car_body3_size">16sp</dimen>
+    <dimen name="car_body4_size">14sp</dimen>
+    <dimen name="car_body5_size">18sp</dimen>
+    <dimen name="car_action1_size">26sp</dimen>
+
+    <!-- Paddings -->
+    <dimen name="car_padding_1">4dp</dimen>
+    <dimen name="car_padding_2">10dp</dimen>
+    <dimen name="car_padding_3">16dp</dimen>
+    <dimen name="car_padding_4">28dp</dimen>
+    <dimen name="car_padding_5">32dp</dimen>
+
+    <!-- Radii -->
+    <dimen name="car_radius_1">4dp</dimen>
+    <dimen name="car_radius_2">8dp</dimen>
+    <dimen name="car_radius_3">16dp</dimen>
+    <dimen name="car_radius_5">100dp</dimen>
+
+    <!-- Margin -->
+    <dimen name="car_margin">112dp</dimen>
+
+    <!-- Car Component Dimensions -->
+    <!-- Application Bar Height -->
+    <dimen name="car_app_bar_height">80dp</dimen>
+
+    <!-- The height of the bar that contains an applications action buttons. -->
+    <dimen name="car_action_bar_height">128dp</dimen>
+
+    <!-- Minimum touch target size. -->
+    <dimen name="car_touch_target">76dp</dimen>
+
+    <!-- Button Dimensions -->
+    <dimen name="car_button_height">64dp</dimen>
+    <dimen name="car_button_min_width">158dp</dimen>
+    <dimen name="car_button_horizontal_padding">@dimen/car_padding_4</dimen>
+    <dimen name="car_button_radius">@dimen/car_radius_1</dimen>
+
+    <!-- Icon dimensions -->
+    <dimen name="car_primary_icon_size">44dp</dimen>
+    <dimen name="car_secondary_icon_size">24dp</dimen>
+
+    <!-- Line heights -->
+    <dimen name="car_single_line_list_item_height">76dp</dimen>
+    <dimen name="car_double_line_list_item_height">96dp</dimen>
+
+    <!-- List and Drawer Dimensions -->
+    <!-- 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 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 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..61e089b
--- /dev/null
+++ b/car/res/values/styles.xml
@@ -0,0 +1,132 @@
+<?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 the main headline text. The color of this text changes based on the
+         day/night mode. -->
+    <style name="CarHeadline1" >
+        <item name="android:textStyle">normal</item>
+        <item name="android:textSize">@dimen/car_headline1_size</item>
+        <item name="android:textColor">@color/car_headline1</item>
+    </style>
+
+    <!-- The styling for a sub-headline text. The color of this text changes based on the
+         day/night mode. -->
+    <style name="CarHeadline2" >
+        <item name="android:textStyle">normal</item>
+        <item name="android:textSize">@dimen/car_headline2_size</item>
+        <item name="android:textColor">@color/car_headline2</item>
+    </style>
+
+    <!-- The styling for a smaller alternate headline text. The color of this text changes based on
+         the day/night mode. -->
+    <style name="CarHeadline3" >
+        <item name="android:textStyle">normal</item>
+        <item name="android:textSize">@dimen/car_headline3_size</item>
+        <item name="android:textColor">@color/car_headline3</item>
+    </style>
+
+    <!-- The styling for the smallest headline text. The color of this text changes based on the
+         day/night mode. -->
+    <style name="CarHeadline4" >
+        <item name="android:textStyle">normal</item>
+        <item name="android:textSize">@dimen/car_headline4_size</item>
+        <item name="android:textColor">@color/car_headline4</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>
+
+    <!-- A smaller styling for body text. The color of this text changes based on the day/night
+         mode. -->
+    <style name="CarBody3" >
+        <item name="android:textStyle">normal</item>
+        <item name="android:textSize">@dimen/car_body3_size</item>
+        <item name="android:textColor">@color/car_body3</item>
+    </style>
+
+    <!-- The smallest styling for body text. The color of this text changes based on the day/night
+         mode. -->
+    <style name="CarBody4" >
+        <item name="android:textStyle">normal</item>
+        <item name="android:textSize">@dimen/car_body4_size</item>
+        <item name="android:textColor">@color/car_body4</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>
+
+    <!-- The styles for the regular and borderless buttons -->
+    <style name="CarButton" parent="android:Widget.Material.Button">
+        <item name="android:layout_height">@dimen/car_button_height</item>
+        <item name="android:minWidth">@dimen/car_button_min_width</item>
+        <item name="android:paddingStart">@dimen/car_button_horizontal_padding</item>
+        <item name="android:paddingEnd">@dimen/car_button_horizontal_padding</item>
+        <item name="android:textStyle">normal</item>
+        <item name="android:textSize">@dimen/car_action1_size</item>
+        <item name="android:textColor">@drawable/car_button_text_color</item>
+        <item name="android:textAllCaps">true</item>
+        <item name="android:background">@drawable/car_button_background</item>
+    </style>
+
+    <style name="CarBorderlessButton" parent="android:Widget.Material.Button.Borderless">
+        <item name="android:layout_height">@dimen/car_button_height</item>
+        <item name="android:minWidth">@dimen/car_button_min_width</item>
+        <item name="android:paddingStart">@dimen/car_button_horizontal_padding</item>
+        <item name="android:paddingEnd">@dimen/car_button_horizontal_padding</item>
+        <item name="android:textStyle">normal</item>
+        <item name="android:textSize">@dimen/car_action1_size</item>
+        <item name="android:textColor">@drawable/car_button_text_color</item>
+        <item name="android:textAllCaps">true</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..f46c652
--- /dev/null
+++ b/car/src/main/java/android/support/car/drawer/CarDrawerActivity.java
@@ -0,0 +1,152 @@
+/*
+ * 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.annotation.Nullable;
+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#pushAdapter(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 {
+    private 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);
+    }
+
+    /**
+     * Returns the {@link CarDrawerController} that is responsible for handling events relating
+     * to the drawer in this Activity.
+     *
+     * @return The {@link CarDrawerController} linked to this Activity. This value will be
+     * {@code null} if this method is called before {@code onCreate()} has been called.
+     */
+    @Nullable
+    protected CarDrawerController getDrawerController() {
+        return mDrawerController;
+    }
+
+    @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..7b23714
--- /dev/null
+++ b/car/src/main/java/android/support/car/drawer/CarDrawerController.java
@@ -0,0 +1,335 @@
+/*
+ * 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.AnimRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+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.RecyclerView;
+import android.support.v7.widget.Toolbar;
+import android.view.Gravity;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.animation.AnimationUtils;
+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 {
+    /** An animation for when a user navigates into a submenu. */
+    @AnimRes
+    private static final int DRILL_DOWN_ANIM = R.anim.fade_in_trans_right_layout_anim;
+
+    /** An animation for when a user navigates up (when the back button is pressed). */
+    @AnimRes
+    private static final int NAVIGATE_UP_ANIM = R.anim.fade_in_trans_left_layout_anim;
+
+    /** 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 popped 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();
+        mDrawerToggle = drawerToggle;
+        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);
+
+        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.
+     *
+     * @param rootAdapter The adapter that will act as the root. If this value is {@code null}, then
+     *                    this method will do nothing.
+     */
+    public void setRootAdapter(@Nullable CarDrawerAdapter rootAdapter) {
+        if (rootAdapter == null) {
+            return;
+        }
+
+        // The root adapter is always the last item in the stack.
+        if (mAdapterStack.size() > 0) {
+            mAdapterStack.set(0, rootAdapter);
+        } else {
+            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 pushAdapter(CarDrawerAdapter adapter) {
+        mAdapterStack.peek().setTitleChangeListener(null);
+        mAdapterStack.push(adapter);
+        setDisplayAdapter(adapter);
+        runLayoutAnimation(DRILL_DOWN_ANIM);
+    }
+
+    /** 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 given adapter as the one displaying the current contents of the drawer.
+     *
+     * <p>The drawer's title will also be derived from the given adapter.
+     */
+    private void setDisplayAdapter(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);
+    }
+
+    /**
+     * 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();
+        setDisplayAdapter(mAdapterStack.peek());
+        runLayoutAnimation(NAVIGATE_UP_ANIM);
+        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();
+        }
+        setDisplayAdapter(mAdapterStack.peek());
+        runLayoutAnimation(NAVIGATE_UP_ANIM);
+    }
+
+    /**
+     * Runs the given layout animation on the PagedListView. Running this animation will also
+     * refresh the contents of the list.
+     */
+    private void runLayoutAnimation(@AnimRes int animation) {
+        RecyclerView recyclerView = mDrawerList.getRecyclerView();
+        recyclerView.setLayoutAnimation(AnimationUtils.loadLayoutAnimation(mContext, animation));
+        recyclerView.getAdapter().notifyDataSetChanged();
+        recyclerView.scheduleLayoutAnimation();
+    }
+}
diff --git a/media-compat-test-lib/build.gradle b/car/src/main/java/android/support/car/drawer/DrawerItemClickListener.java
similarity index 64%
copy from media-compat-test-lib/build.gradle
copy to car/src/main/java/android/support/car/drawer/DrawerItemClickListener.java
index 26594e5..d707dbd 100644
--- a/media-compat-test-lib/build.gradle
+++ b/car/src/main/java/android/support/car/drawer/DrawerItemClickListener.java
@@ -14,4 +14,16 @@
  * limitations under the License.
  */
 
-apply plugin: 'java'
+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..fa5dd43
--- /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_margin);
+        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..ef22c48
--- /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 PagedLayoutManager mLayoutManager;
+
+    public CarItemAnimator(PagedLayoutManager 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/CarRecyclerView.java b/car/src/main/java/android/support/car/widget/CarRecyclerView.java
new file mode 100644
index 0000000..bb9cb71
--- /dev/null
+++ b/car/src/main/java/android/support/car/widget/CarRecyclerView.java
@@ -0,0 +1,142 @@
+/*
+ * 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.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;
+
+/**
+ * Custom {@link RecyclerView} that helps {@link PagedLayoutManager} 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 boolean mFadeLastItem;
+    /**
+     * 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
+    public boolean fling(int velocityX, int velocityY) {
+        mWasFlingCalledForGesture = true;
+        return ((PagedLayoutManager) 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) {
+                ((PagedLayoutManager) 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() {
+        PagedLayoutManager lm = (PagedLayoutManager) 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() {
+        PagedLayoutManager lm = (PagedLayoutManager) getLayoutManager();
+        int pageDownPosition = lm.getPageDownPosition();
+        if (pageDownPosition == -1) {
+            return;
+        }
+
+        smoothScrollToPosition(pageDownPosition);
+    }
+
+    /**
+     * 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/PagedLayoutManager.java b/car/src/main/java/android/support/car/widget/PagedLayoutManager.java
new file mode 100644
index 0000000..c4f469a
--- /dev/null
+++ b/car/src/main/java/android/support/car/widget/PagedLayoutManager.java
@@ -0,0 +1,1687 @@
+/*
+ * Copyright 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.os.Parcel;
+import android.os.Parcelable;
+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
+ *       PagedLayoutManager, 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 PagedLayoutManager extends RecyclerView.LayoutManager {
+    private static final String TAG = "PagedLayoutManager";
+
+    /**
+     * 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 PagedLayoutManager(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;
+    }
+
+    @Override
+    public Parcelable onSaveInstanceState() {
+        SavedState savedState = new SavedState();
+        savedState.mFirstChildPosition = getFirstFullyVisibleChildPosition();
+        return savedState;
+    }
+
+    @Override
+    public void onRestoreInstanceState(Parcelable state) {
+        if (state instanceof SavedState) {
+            scrollToPosition(((SavedState) state).mFirstChildPosition);
+        }
+    }
+
+    /** The state that will be saved across configuration changes. */
+    static class SavedState implements Parcelable {
+        /** The position of the first visible child view in the list. */
+        int mFirstChildPosition;
+
+        SavedState() {}
+
+        private SavedState(Parcel in) {
+            mFirstChildPosition = in.readInt();
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeInt(mFirstChildPosition);
+        }
+
+        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];
+                    }
+                };
+    }
+
+    /**
+     * 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/PagedListView.java b/car/src/main/java/android/support/car/widget/PagedListView.java
new file mode 100644
index 0000000..67a6247
--- /dev/null
+++ b/car/src/main/java/android/support/car/widget/PagedListView.java
@@ -0,0 +1,996 @@
+/*
+ * 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.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.os.Parcel;
+import android.os.Parcelable;
+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.util.SparseArray;
+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
+ * left side.
+ */
+public class PagedListView extends FrameLayout {
+    /** Default maximum number of clicks allowed on a list */
+    public static final int DEFAULT_MAX_CLICKS = 6;
+
+    /**
+     * Value to pass to {@link #setMaxPages(int)} to indicate there is no restriction on the
+     * maximum number of pages to show.
+     */
+    public static final int UNLIMITED_PAGES = -1;
+
+    /**
+     * 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 PagedLayoutManager 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. */
+    private int mMaxPages;
+
+    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.
+     */
+    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 PagedLayoutManager(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_margin));
+            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));
+        }
+
+        int itemSpacing = a.getDimensionPixelSize(R.styleable.PagedListView_itemSpacing, 0);
+        if (itemSpacing > 0) {
+            mRecyclerView.addItemDecoration(new ItemSpacingDecoration(itemSpacing));
+        }
+
+        // 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 + ")");
+                        }
+                    }
+                });
+
+        Drawable upButtonIcon = a.getDrawable(R.styleable.PagedListView_upButtonIcon);
+        if (upButtonIcon != null) {
+            setUpButtonIcon(upButtonIcon);
+        }
+
+        Drawable downButtonIcon = a.getDrawable(R.styleable.PagedListView_downButtonIcon);
+        if (downButtonIcon != null) {
+            setDownButtonIcon(downButtonIcon);
+        }
+
+        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(PagedLayoutManager.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(PagedLayoutManager.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);
+    }
+
+    @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 icon to be used for the up button. */
+    public void setUpButtonIcon(Drawable icon) {
+        mScrollBarView.setUpButtonIcon(icon);
+    }
+
+    /** Sets the icon to be used for the down button. */
+    public void setDownButtonIcon(Drawable icon) {
+        mScrollBarView.setDownButtonIcon(icon);
+    }
+
+    /**
+     * Sets the adapter for the list.
+     *
+     * <p>The given Adapter can implement {@link ItemCap} if it wishes to control the behavior of
+     * a max number of items. Otherwise, methods in the PagedListView to limit the content, such as
+     * {@link #setMaxPages(int)}, will do nothing.
+     */
+    public void setAdapter(
+            @NonNull RecyclerView.Adapter<? extends RecyclerView.ViewHolder> adapter) {
+        mAdapter = adapter;
+        mRecyclerView.setAdapter(adapter);
+        updateMaxItems();
+    }
+
+    /** @hide */
+    @RestrictTo(LIBRARY_GROUP)
+    @NonNull
+    public PagedLayoutManager 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.
+     *
+     * <p>Passing {@link #UNLIMITED_PAGES} will remove any restrictions on a maximum number
+     * of pages.
+     *
+     * <p>Note that for any restriction on maximum pages to work, the adapter passed to this
+     * PagedListView needs to implement {@link ItemCap}.
+     *
+     * @param maxPages The maximum number of pages that fit on the screen. Should be positive or
+     * {@link #UNLIMITED_PAGES}.
+     */
+    public void setMaxPages(int maxPages) {
+        mMaxPages = Math.max(UNLIMITED_PAGES, 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 or {@link #UNLIMITED_PAGES} if there is
+     * no limit.
+     */
+    public int getMaxPages() {
+        return mMaxPages;
+    }
+
+    /**
+     * Gets the number of rows per page. Default value of mRowsPerPage is -1. If the first child of
+     * PagedLayoutManager 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);
+    }
+
+    /**
+     * Sets spacing between each item in the list. The spacing will not be added before the first
+     * item and after the last.
+     *
+     * @param itemSpacing the spacing between each item.
+     */
+    public void setItemSpacing(int itemSpacing) {
+        ItemSpacingDecoration existing = null;
+        for (int i = 0, count = mRecyclerView.getItemDecorationCount(); i < count; i++) {
+            RecyclerView.ItemDecoration itemDecoration = mRecyclerView.getItemDecorationAt(i);
+            if (itemDecoration instanceof ItemSpacingDecoration) {
+                existing = (ItemSpacingDecoration) itemDecoration;
+                break;
+            }
+        }
+
+        if (itemSpacing == 0 && existing != null) {
+            mRecyclerView.removeItemDecoration(existing);
+        } else if (existing == null) {
+            mRecyclerView.addItemDecoration(new ItemSpacingDecoration(itemSpacing));
+        } else {
+            existing.setItemSpacing(itemSpacing);
+        }
+        mRecyclerView.invalidateItemDecorations();
+    }
+
+    /**
+     * 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;
+        resetMaxPages();
+    }
+
+    /** 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;
+        }
+
+        // Ensure mRowsPerPage regardless of if the adapter implements ItemCap.
+        updateRowsPerPage();
+
+        // If the adapter does not implement ItemCap, then the max items on it cannot be updated.
+        if (!(mAdapter instanceof ItemCap)) {
+            return;
+        }
+
+        final int originalCount = mAdapter.getItemCount();
+        ((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());
+        }
+    }
+
+    @Override
+    protected Parcelable onSaveInstanceState() {
+        SavedState savedState = new SavedState(super.onSaveInstanceState());
+        savedState.mLayoutManagerState = mLayoutManager.onSaveInstanceState();
+        return savedState;
+    }
+
+    @Override
+    protected void onRestoreInstanceState(Parcelable state) {
+        SavedState savedState = (SavedState) state;
+        mLayoutManager.onRestoreInstanceState(savedState.mLayoutManagerState);
+        super.onRestoreInstanceState(savedState.getSuperState());
+    }
+
+    @Override
+    protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
+        // There is the possibility of multiple PagedListViews on a page. This means that the ids
+        // of the child Views of PagedListView are no longer unique, and onSaveInstanceState()
+        // cannot be used. As a result, PagedListViews needs to manually dispatch the instance
+        // states. Call dispatchFreezeSelfOnly() so that no child views have onSaveInstanceState()
+        // called by the system.
+        dispatchFreezeSelfOnly(container);
+    }
+
+    @Override
+    protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
+        // Prevent onRestoreInstanceState() from being called on child Views. Instead, PagedListView
+        // will manually handle passing the state. See the comment in dispatchSaveInstanceState()
+        // for more information.
+        dispatchThawSelfOnly(container);
+    }
+
+    /** The state that will be saved across configuration changes. */
+    private static class SavedState extends BaseSavedState {
+        /** The state of the {@link #mLayoutManager} of this PagedListView. */
+        Parcelable mLayoutManagerState;
+
+        SavedState(Parcelable superState) {
+            super(superState);
+        }
+
+        private SavedState(Parcel in) {
+            super(in);
+            mLayoutManagerState =
+                    in.readParcelable(PagedLayoutManager.SavedState.class.getClassLoader());
+        }
+
+        @Override
+        public void writeToParcel(Parcel out, int flags) {
+            super.writeToParcel(out, flags);
+            out.writeParcelable(mLayoutManagerState, flags);
+        }
+
+        public static final ClassLoaderCreator<SavedState> CREATOR =
+                new ClassLoaderCreator<SavedState>() {
+                    @Override
+                    public SavedState createFromParcel(Parcel source, ClassLoader loader) {
+                        return new SavedState(source);
+                    }
+
+                    @Override
+                    public SavedState createFromParcel(Parcel source) {
+                        return createFromParcel(source, null /* loader */);
+                    }
+
+                    @Override
+                    public SavedState[] newArray(int size) {
+                        return new SavedState[size];
+                    }
+                };
+    }
+
+    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 add spacing
+     * between each item in the RecyclerView that it is added to.
+     */
+    private static class ItemSpacingDecoration extends RecyclerView.ItemDecoration {
+
+        private int mHalfItemSpacing;
+
+        private ItemSpacingDecoration(int itemSpacing) {
+            mHalfItemSpacing = itemSpacing / 2;
+        }
+
+        @Override
+        public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
+                RecyclerView.State state) {
+            super.getItemOffsets(outRect, view, parent, state);
+            // Skip top offset for first item and bottom offset for last.
+            int position = parent.getChildAdapterPosition(view);
+            if (position > 0) {
+                outRect.top = mHalfItemSpacing;
+            }
+            if (position < state.getItemCount() - 1) {
+                outRect.bottom = mHalfItemSpacing;
+            }
+        }
+
+        /**
+         * @param itemSpacing sets spacing between each item.
+         */
+        public void setItemSpacing(int itemSpacing) {
+            mHalfItemSpacing = itemSpacing / 2;
+        }
+    }
+
+    /**
+     * 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.
+     */
+    private 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 mDividerEndId;
+
+        /**
+         * @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;
+            mDividerEndId = 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) {
+            // Draw a divider line between each item. No need to draw the line for the last item.
+            for (int i = 0, childCount = parent.getChildCount(); i < childCount - 1; i++) {
+                View container = parent.getChildAt(i);
+                View nextContainer = parent.getChildAt(i + 1);
+                int spacing = nextContainer.getTop() - container.getBottom();
+
+                View startChild =
+                        mDividerStartId != INVALID_RESOURCE_ID
+                                ? container.findViewById(mDividerStartId)
+                                : container;
+
+                View endChild =
+                        mDividerEndId != INVALID_RESOURCE_ID
+                                ? container.findViewById(mDividerEndId)
+                                : container;
+
+                if (startChild == null || endChild == null) {
+                    continue;
+                }
+
+                int left = mDividerStartMargin + startChild.getLeft();
+                int right = endChild.getRight();
+                int bottom = container.getBottom() + spacing / 2 + mDividerHeight / 2;
+                int top = bottom - mDividerHeight;
+
+                c.drawRect(left, top, right, bottom, mPaint);
+            }
+        }
+
+        @Override
+        public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
+                RecyclerView.State state) {
+            super.getItemOffsets(outRect, view, parent, state);
+            // Skip top offset for first item and bottom offset for last.
+            int position = parent.getChildAdapterPosition(view);
+            if (position > 0) {
+                outRect.top = mDividerHeight / 2;
+            }
+            if (position < state.getItemCount() - 1) {
+                outRect.bottom = mDividerHeight / 2;
+            }
+        }
+    }
+}
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..1c46b5d
--- /dev/null
+++ b/car/src/main/java/android/support/car/widget/PagedScrollBarView.java
@@ -0,0 +1,264 @@
+/*
+ * 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.graphics.drawable.Drawable;
+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 icon to be used for the up button. */
+    public void setUpButtonIcon(Drawable icon) {
+        mUpButton.setImageDrawable(icon);
+    }
+
+    /** Sets the icon to be used for the down button. */
+    public void setDownButtonIcon(Drawable icon) {
+        mDownButton.setImageDrawable(icon);
+    }
+
+    /**
+     * 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 PagedLayoutManager 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..949e85a
--- /dev/null
+++ b/car/tests/AndroidManifest.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.
+  -->
+<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"/>
+        <activity android:name="android.support.car.widget.PagedListViewSavedStateActivity"/>
+        <activity android:name="android.support.car.widget.PagedListViewTestActivity"/>
+    </application>
+</manifest>
diff --git a/media-compat-test-service/tests/NO_DOCS b/car/tests/NO_DOCS
similarity index 99%
rename from media-compat-test-service/tests/NO_DOCS
rename to car/tests/NO_DOCS
index 4dad694..bd77b1a 100644
--- a/media-compat-test-service/tests/NO_DOCS
+++ b/car/tests/NO_DOCS
@@ -14,4 +14,4 @@
 
 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.
+the directory. This is especially useful for test projects.
\ No newline at end of file
diff --git a/car/tests/res/drawable/ic_thumb_down.xml b/car/tests/res/drawable/ic_thumb_down.xml
new file mode 100644
index 0000000..25fccdb
--- /dev/null
+++ b/car/tests/res/drawable/ic_thumb_down.xml
@@ -0,0 +1,25 @@
+<?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="48dp"
+        android:height="48dp"
+        android:viewportWidth="48.0"
+        android:viewportHeight="48.0">
+    <path
+        android:pathData="M30,6L12,6c-1.66,0 -3.08,1.01 -3.68,2.44l-6.03,14.1C2.11,23 2,23.49 2,24v3.83l0.02,0.02L2,28c0,2.21 1.79,4 4,4h12.63l-1.91,9.14c-0.04,0.2 -0.07,0.41 -0.07,0.63 0,0.83 0.34,1.58 0.88,2.12L19.66,46l13.17,-13.17C33.55,32.1 34,31.1 34,30L34,10c0,-2.21 -1.79,-4 -4,-4zM38,6v24h8L46,6h-8z"
+        android:fillColor="#000000"/>
+</vector>
diff --git a/car/tests/res/drawable/ic_thumb_up.xml b/car/tests/res/drawable/ic_thumb_up.xml
new file mode 100644
index 0000000..9f02cf3
--- /dev/null
+++ b/car/tests/res/drawable/ic_thumb_up.xml
@@ -0,0 +1,25 @@
+<?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="48dp"
+        android:height="48dp"
+        android:viewportWidth="48.0"
+        android:viewportHeight="48.0">
+    <path
+        android:pathData="M2,42h8L10,18L2,18v24zM46,20c0,-2.21 -1.79,-4 -4,-4L29.37,16l1.91,-9.14c0.04,-0.2 0.07,-0.41 0.07,-0.63 0,-0.83 -0.34,-1.58 -0.88,-2.12L28.34,2 15.17,15.17C14.45,15.9 14,16.9 14,18v20c0,2.21 1.79,4 4,4h18c1.66,0 3.08,-1.01 3.68,-2.44l6.03,-14.1c0.18,-0.46 0.29,-0.95 0.29,-1.46v-3.83l-0.02,-0.02L46,20z"
+        android:fillColor="#000000"/>
+</vector>
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/res/layout/activity_paged_list_view.xml b/car/tests/res/layout/activity_paged_list_view.xml
new file mode 100644
index 0000000..d14eb96
--- /dev/null
+++ b/car/tests/res/layout/activity_paged_list_view.xml
@@ -0,0 +1,30 @@
+<?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/frame_layout"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <android.support.car.widget.PagedListView
+        android:id="@+id/paged_list_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        app:showPagedListViewDivider="false"
+        app:offsetScrollBar="true"/>
+</FrameLayout>
diff --git a/car/tests/res/layout/activity_two_paged_list_view.xml b/car/tests/res/layout/activity_two_paged_list_view.xml
new file mode 100644
index 0000000..588071f
--- /dev/null
+++ b/car/tests/res/layout/activity_two_paged_list_view.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.
+  -->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="horizontal">
+
+    <android.support.car.widget.PagedListView
+        android:id="@+id/paged_list_view_1"
+        android:layout_weight="1"
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        app:showPagedListViewDivider="false"
+        app:offsetScrollBar="true"/>
+
+    <android.support.car.widget.PagedListView
+        android:id="@+id/paged_list_view_2"
+        android:layout_weight="1"
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        app:showPagedListViewDivider="false"
+        app:offsetScrollBar="true"/>
+
+</LinearLayout>
diff --git a/car/tests/res/layout/paged_list_item_column_card.xml b/car/tests/res/layout/paged_list_item_column_card.xml
new file mode 100644
index 0000000..2bc5cd0
--- /dev/null
+++ b/car/tests/res/layout/paged_list_item_column_card.xml
@@ -0,0 +1,35 @@
+<?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="wrap_content"
+    android:orientation="vertical">
+
+    <android.support.car.widget.ColumnCardView
+        android:id="@+id/column_card"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content">
+
+        <TextView
+            android:id="@+id/text_view"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"/>
+
+    </android.support.car.widget.ColumnCardView>
+
+</FrameLayout>
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/media-compat-test-lib/build.gradle b/car/tests/src/android/support/car/widget/ColumnCardViewTestActivity.java
similarity index 62%
copy from media-compat-test-lib/build.gradle
copy to car/tests/src/android/support/car/widget/ColumnCardViewTestActivity.java
index 26594e5..693e4a1 100644
--- a/media-compat-test-lib/build.gradle
+++ b/car/tests/src/android/support/car/widget/ColumnCardViewTestActivity.java
@@ -14,4 +14,16 @@
  * limitations under the License.
  */
 
-apply plugin: 'java'
+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/car/tests/src/android/support/car/widget/PagedListViewSavedStateActivity.java b/car/tests/src/android/support/car/widget/PagedListViewSavedStateActivity.java
new file mode 100644
index 0000000..8cb976c
--- /dev/null
+++ b/car/tests/src/android/support/car/widget/PagedListViewSavedStateActivity.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 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;
+
+/**
+ * Test Activity for testing the saving of state for the {@link PagedListView}. It will inflate
+ * a layout that has two PagedListViews next to each other.
+ */
+public class PagedListViewSavedStateActivity extends Activity {
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_two_paged_list_view);
+    }
+}
diff --git a/car/tests/src/android/support/car/widget/PagedListViewSavedStateTest.java b/car/tests/src/android/support/car/widget/PagedListViewSavedStateTest.java
new file mode 100644
index 0000000..9b871b3
--- /dev/null
+++ b/car/tests/src/android/support/car/widget/PagedListViewSavedStateTest.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright 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.test.espresso.Espresso.onView;
+import static android.support.test.espresso.action.ViewActions.click;
+import static android.support.test.espresso.matcher.ViewMatchers.isDescendantOfA;
+import static android.support.test.espresso.matcher.ViewMatchers.withId;
+
+import static org.hamcrest.Matchers.allOf;
+import static org.junit.Assert.assertEquals;
+
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.support.car.test.R;
+import android.support.test.espresso.IdlingRegistry;
+import android.support.test.espresso.IdlingResource;
+import android.support.test.filters.SmallTest;
+import android.support.test.filters.Suppress;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import org.hamcrest.Matcher;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+/** Unit tests for the ability of the {@link PagedListView} to save state. */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class PagedListViewSavedStateTest {
+    /**
+     * Used by {@link TestAdapter} to calculate ViewHolder height so N items appear in one page of
+     * {@link PagedListView}. If you need to test behavior under multiple pages, set number of items
+     * to ITEMS_PER_PAGE * desired_pages.
+     *
+     * <p>Actual value does not matter.
+     */
+    private static final int ITEMS_PER_PAGE = 5;
+
+    /**
+     * The total number of items to display in a list. This value just needs to be large enough
+     * to ensure the scroll bar shows.
+     */
+    private static final int TOTAL_ITEMS_IN_LIST = 100;
+
+    private static final int NUM_OF_PAGES = TOTAL_ITEMS_IN_LIST / ITEMS_PER_PAGE;
+
+    @Rule
+    public ActivityTestRule<PagedListViewSavedStateActivity> mActivityRule =
+            new ActivityTestRule<>(PagedListViewSavedStateActivity.class);
+
+    private PagedListViewSavedStateActivity mActivity;
+    private PagedListView mPagedListView1;
+    private PagedListView mPagedListView2;
+
+    @Before
+    public void setUp() {
+        mActivity = mActivityRule.getActivity();
+        mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
+
+        mPagedListView1 = mActivity.findViewById(R.id.paged_list_view_1);
+        mPagedListView2 = mActivity.findViewById(R.id.paged_list_view_2);
+
+        setUpPagedListView(mPagedListView1);
+        setUpPagedListView(mPagedListView2);
+    }
+
+    private boolean isAutoDevice() {
+        PackageManager packageManager = mActivityRule.getActivity().getPackageManager();
+        return packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
+    }
+
+    private void setUpPagedListView(PagedListView pagedListView) {
+        try {
+            mActivityRule.runOnUiThread(() -> {
+                pagedListView.setMaxPages(PagedListView.ItemCap.UNLIMITED);
+                pagedListView.setAdapter(new TestAdapter(TOTAL_ITEMS_IN_LIST,
+                        pagedListView.getMeasuredHeight()));
+            });
+        } catch (Throwable throwable) {
+            throwable.printStackTrace();
+            throw new RuntimeException(throwable);
+        }
+    }
+
+    @After
+    public void tearDown() {
+        for (IdlingResource idlingResource : IdlingRegistry.getInstance().getResources()) {
+            IdlingRegistry.getInstance().unregister(idlingResource);
+        }
+    }
+
+    @Suppress
+    @Test
+    public void testPagePositionRememberedOnRotation() {
+        if (!isAutoDevice()) {
+            return;
+        }
+
+        Random random = new Random();
+        IdlingRegistry.getInstance().register(new PagedListViewScrollingIdlingResource(
+                mPagedListView1, mPagedListView2));
+
+        // Add 1 to this random number to ensure it is a value between 1 and NUM_OF_PAGES.
+        int numOfClicks = random.nextInt(NUM_OF_PAGES) + 1;
+        clickPageDownButton(onPagedListView1(), numOfClicks);
+        int topPositionOfPagedListView1 =
+                mPagedListView1.getLayoutManager().getFirstFullyVisibleChildPosition();
+
+        numOfClicks = random.nextInt(NUM_OF_PAGES) + 1;
+        clickPageDownButton(onPagedListView2(), numOfClicks);
+        int topPositionOfPagedListView2 =
+                mPagedListView2.getLayoutManager().getFirstFullyVisibleChildPosition();
+
+        // Perform a configuration change by rotating the screen.
+        mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+        mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
+
+        // Check that the positions are the same after the change.
+        assertEquals(topPositionOfPagedListView1,
+                mPagedListView1.getLayoutManager().getFirstFullyVisibleChildPosition());
+        assertEquals(topPositionOfPagedListView2,
+                mPagedListView2.getLayoutManager().getFirstFullyVisibleChildPosition());
+    }
+
+    /** Clicks the page down button on the given PagedListView for the given number of times. */
+    private void clickPageDownButton(Matcher<View> pagedListView, int times) {
+        for (int i = 0; i < times; i++) {
+            onView(allOf(withId(R.id.page_down), pagedListView)).perform(click());
+        }
+    }
+
+
+    /** Convenience method for checking that a View is on the first PagedListView. */
+    private Matcher<View> onPagedListView1() {
+        return isDescendantOfA(withId(R.id.paged_list_view_1));
+    }
+
+    /** Convenience method for checking that a View is on the second PagedListView. */
+    private Matcher<View> onPagedListView2() {
+        return isDescendantOfA(withId(R.id.paged_list_view_2));
+    }
+
+    private static String getItemText(int index) {
+        return "Data " + index;
+    }
+
+    /** An Adapter that ensures that there is {@link #ITEMS_PER_PAGE} displayed. */
+    private class TestAdapter extends RecyclerView.Adapter<TestViewHolder>
+            implements PagedListView.ItemCap {
+        private List<String> mData;
+        private int mParentHeight;
+
+        TestAdapter(int itemCount, int parentHeight) {
+            mData = new ArrayList<>();
+            for (int i = 0; i < itemCount; i++) {
+                mData.add(getItemText(i));
+            }
+            mParentHeight = parentHeight;
+        }
+
+        @Override
+        public TestViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+            LayoutInflater inflater = LayoutInflater.from(parent.getContext());
+            return new TestViewHolder(inflater, parent);
+        }
+
+        @Override
+        public void onBindViewHolder(TestViewHolder holder, int position) {
+            // Calculate height for an item so one page fits ITEMS_PER_PAGE items.
+            int height = (int) Math.floor(mParentHeight / ITEMS_PER_PAGE);
+            holder.itemView.setMinimumHeight(height);
+            holder.setText(mData.get(position));
+        }
+
+        @Override
+        public int getItemCount() {
+            return mData.size();
+        }
+
+        @Override
+        public void setMaxItems(int maxItems) {
+            // No-op
+        }
+    }
+
+    /** A ViewHolder that holds a View with a TextView. */
+    private class TestViewHolder extends RecyclerView.ViewHolder {
+        private TextView mTextView;
+
+        TestViewHolder(LayoutInflater inflater, ViewGroup parent) {
+            super(inflater.inflate(R.layout.paged_list_item_column_card, parent, false));
+            mTextView = itemView.findViewById(R.id.text_view);
+        }
+
+        public void setText(String text) {
+            mTextView.setText(text);
+        }
+    }
+
+    // Registering IdlingResource in @Before method does not work - espresso doesn't actually wait
+    // for ViewAction to finish. So each method that  clicks on button will need to register their
+    // own IdlingResource.
+    private class PagedListViewScrollingIdlingResource implements IdlingResource {
+        private boolean mIsIdle = true;
+        private ResourceCallback mResourceCallback;
+
+        PagedListViewScrollingIdlingResource(PagedListView pagedListView1,
+                PagedListView pagedListView2) {
+            // Ensure the IdlingResource waits for both RecyclerViews to finish their movement.
+            pagedListView1.getRecyclerView().addOnScrollListener(mOnScrollListener);
+            pagedListView2.getRecyclerView().addOnScrollListener(mOnScrollListener);
+        }
+
+        @Override
+        public String getName() {
+            return PagedListViewScrollingIdlingResource.class.getName();
+        }
+
+        @Override
+        public boolean isIdleNow() {
+            return mIsIdle;
+        }
+
+        @Override
+        public void registerIdleTransitionCallback(ResourceCallback callback) {
+            mResourceCallback = callback;
+        }
+
+        private final RecyclerView.OnScrollListener mOnScrollListener =
+                new RecyclerView.OnScrollListener() {
+                    @Override
+                    public void onScrollStateChanged(
+                            RecyclerView recyclerView, int newState) {
+                        super.onScrollStateChanged(recyclerView, newState);
+
+                        // Treat dragging as idle, or Espresso will block itself when
+                        // swiping.
+                        mIsIdle = (newState == RecyclerView.SCROLL_STATE_IDLE
+                                || newState == RecyclerView.SCROLL_STATE_DRAGGING);
+
+                        if (mIsIdle && mResourceCallback != null) {
+                            mResourceCallback.onTransitionToIdle();
+                        }
+                    }
+
+                    @Override
+                    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {}
+                };
+    }
+}
diff --git a/car/tests/src/android/support/car/widget/PagedListViewTest.java b/car/tests/src/android/support/car/widget/PagedListViewTest.java
new file mode 100644
index 0000000..0d07fbd
--- /dev/null
+++ b/car/tests/src/android/support/car/widget/PagedListViewTest.java
@@ -0,0 +1,497 @@
+/*
+ * 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.test.espresso.Espresso.onView;
+import static android.support.test.espresso.action.ViewActions.click;
+import static android.support.test.espresso.action.ViewActions.swipeDown;
+import static android.support.test.espresso.action.ViewActions.swipeUp;
+import static android.support.test.espresso.assertion.ViewAssertions.matches;
+import static android.support.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition;
+import static android.support.test.espresso.contrib.RecyclerViewActions.scrollToPosition;
+import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static android.support.test.espresso.matcher.ViewMatchers.isEnabled;
+import static android.support.test.espresso.matcher.ViewMatchers.withId;
+import static android.support.test.espresso.matcher.ViewMatchers.withText;
+
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.junit.Assert.assertThat;
+
+import android.content.pm.PackageManager;
+import android.graphics.drawable.Drawable;
+import android.support.car.test.R;
+import android.support.test.annotation.UiThreadTest;
+import android.support.test.espresso.Espresso;
+import android.support.test.espresso.IdlingResource;
+import android.support.test.espresso.matcher.ViewMatchers;
+import android.support.test.filters.SmallTest;
+import android.support.test.filters.Suppress;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** Unit tests for {@link PagedListView}. */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class PagedListViewTest {
+
+    /**
+     * Used by {@link TestAdapter} to calculate ViewHolder height so N items appear in one page of
+     * {@link PagedListView}. If you need to test behavior under multiple pages, set number of items
+     * to ITEMS_PER_PAGE * desired_pages.
+     * Actual value does not matter.
+     */
+    private static final int ITEMS_PER_PAGE = 5;
+
+    @Rule
+    public ActivityTestRule<PagedListViewTestActivity> mActivityRule =
+            new ActivityTestRule<>(PagedListViewTestActivity.class);
+
+    private PagedListViewTestActivity mActivity;
+    private PagedListView mPagedListView;
+
+    @Before
+    public void setUp() {
+        mActivity = mActivityRule.getActivity();
+        mPagedListView = mActivity.findViewById(R.id.paged_list_view);
+
+        // Using deprecated Espresso methods instead of calling it on the IdlingRegistry because
+        // the latter does not seem to work as reliably. Specifically, on the latter, it does
+        // not always register and unregister.
+        Espresso.registerIdlingResources(new PagedListViewScrollingIdlingResource(mPagedListView));
+    }
+
+    @After
+    public void tearDown() {
+        for (IdlingResource idlingResource : Espresso.getIdlingResources()) {
+            Espresso.unregisterIdlingResources(idlingResource);
+        }
+    }
+
+    private boolean isAutoDevice() {
+        PackageManager packageManager = mActivityRule.getActivity().getPackageManager();
+        return packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
+    }
+
+    private void setUpPagedListView(int itemCount) {
+        setUpPagedListView(itemCount, PagedListView.ItemCap.UNLIMITED);
+    }
+
+    private void setUpPagedListView(int itemCount, int maxPages) {
+        try {
+            mActivityRule.runOnUiThread(() -> {
+                mPagedListView.setMaxPages(maxPages);
+                mPagedListView.setAdapter(
+                        new TestAdapter(itemCount, mPagedListView.getMeasuredHeight()));
+            });
+        } catch (Throwable throwable) {
+            throwable.printStackTrace();
+            throw new RuntimeException(throwable);
+        }
+    }
+
+    /** Initializes {@link #mPagedListView} with an adapter that does not implement ItemCap. */
+    public void setUpNonItemCapPagedListView(int itemCount, int maxPages) {
+        try {
+            mActivityRule.runOnUiThread(() -> {
+                mPagedListView.setMaxPages(maxPages);
+                mPagedListView.setAdapter(
+                        new NoItemCapAdapter(itemCount, mPagedListView.getMeasuredHeight()));
+            });
+        } catch (Throwable throwable) {
+            throwable.printStackTrace();
+            throw new RuntimeException(throwable);
+        }
+    }
+
+    @Test
+    public void scrollBarIsInvisibleIfItemsDoNotFillOnePage() {
+        setUpPagedListView(1 /* itemCount */);
+
+        onView(withId(R.id.paged_scroll_view)).check(matches(not(isDisplayed())));
+    }
+
+    @Test
+    public void pageUpDownButtonIsDisabledOnListEnds() throws Throwable {
+        final int itemCount = ITEMS_PER_PAGE * 3;
+        setUpPagedListView(itemCount);
+        // Initially page_up button is disabled.
+        onView(withId(R.id.page_up)).check(matches(not(isEnabled())));
+
+        // Moving to middle of list enables page_up button.
+        onView(withId(R.id.recycler_view)).perform(scrollToPosition(itemCount / 2));
+        onView(withId(R.id.page_up)).check(matches(isEnabled()));
+
+        // Moving to page end, page_down button is disabled.
+        onView(withId(R.id.recycler_view)).perform(scrollToPosition(itemCount));
+        onView(withId(R.id.page_down)).check(matches(not(isEnabled())));
+    }
+
+    @Test
+    public void testMaxPageGetterSetterDefaultValue() {
+        final int maxPages = 2;
+        final int defaultMaxPages = 3;
+
+        // setMaxPages
+        setUpPagedListView(ITEMS_PER_PAGE, maxPages);
+        assertThat(mPagedListView.getMaxPages(), is(equalTo(maxPages)));
+
+        // resetMaxPages
+        mPagedListView.resetMaxPages();
+        // Max pages is equal to max clicks - 1
+        assertThat(mPagedListView.getMaxPages(), is(equalTo(PagedListView.DEFAULT_MAX_CLICKS - 1)));
+
+        // setDefaultMaxPages
+        mPagedListView.setDefaultMaxPages(defaultMaxPages);
+        mPagedListView.resetMaxPages();
+        assertThat(mPagedListView.getMaxPages(), is(equalTo(defaultMaxPages - 1)));
+    }
+
+    @Test
+    public void setMaxPagesLimitsNumberOfClicks() {
+        if (!isAutoDevice()) {
+            return;
+        }
+
+        setUpPagedListView(ITEMS_PER_PAGE * 3 /* itemCount */, 2 /* maxPages */);
+
+        onView(withId(R.id.page_down)).perform(click());
+        onView(withId(R.id.page_down)).check(matches(not(isEnabled())));
+    }
+
+    @Test
+    public void testMaxPagesDoesNothingIfAdapterDoesNotImplementItemCap() {
+        if (!isAutoDevice()) {
+            return;
+        }
+
+        int numOfPages = 20;
+        int maxPages = 2;
+
+        setUpNonItemCapPagedListView(ITEMS_PER_PAGE * numOfPages, maxPages);
+
+        // There should be no limit on the scroll even though a max number of pages was set.
+        for (int i = 0; i < maxPages; i++) {
+            onView(withId(R.id.page_down)).perform(click());
+        }
+        onView(withId(R.id.page_down)).check(matches(isEnabled()));
+
+        // Next scroll all the way to bottom and check this is possible.
+        for (int i = 0; i < numOfPages - maxPages; i++) {
+            onView(withId(R.id.page_down)).perform(click());
+        }
+        onView(withId(R.id.page_down)).check(matches(not(isEnabled())));
+    }
+
+    @Suppress
+    @Test
+    public void resetMaxPagesToDefaultUnlimitedExtendsList() throws Throwable {
+        if (!isAutoDevice()) {
+            return;
+        }
+
+        final int itemCount = ITEMS_PER_PAGE * 4;
+        setUpPagedListView(itemCount, 2 /* maxPages */);
+
+        // Move to next page - should reach end of list.
+        onView(withId(R.id.page_down)).perform(click()).check(matches(not(isEnabled())));
+
+        // After resetting max pages (default unlimited), we scroll to the known total number of
+        // items.
+        mActivityRule.runOnUiThread(() -> mPagedListView.resetMaxPages());
+        onView(withId(R.id.recycler_view)).perform(scrollToPosition(itemCount - 1));
+
+        // Verify the last item that would've been hidden due to max pages is now shown.
+        onView(allOf(withId(R.id.text_view), withText(itemText(itemCount - 1))))
+                .check(matches(isDisplayed()));
+    }
+
+    @Test
+    public void scrollbarKeepsItemSnappedToTopOfList() {
+        if (!isAutoDevice()) {
+            return;
+        }
+
+        // 2.5 so last page is not full
+        setUpPagedListView((int) (ITEMS_PER_PAGE * 2.5 /* itemCount */));
+
+        // Going down one page and first item is snapped to top
+        onView(withId(R.id.page_down)).perform(click());
+        verifyItemSnappedToListTop();
+
+        // Go down another page and we reach the last page.
+        onView(withId(R.id.page_down)).perform(click()).check(matches(not(isEnabled())));
+        verifyItemSnappedToListTop();
+    }
+
+    @Suppress
+    @Test
+    public void swipeUpKeepsItemSnappedToTopOfList() {
+        setUpPagedListView(ITEMS_PER_PAGE * 2 /* itemCount */);
+
+        onView(withId(R.id.recycler_view)).perform(actionOnItemAtPosition(1, swipeUp()));
+
+        verifyItemSnappedToListTop();
+    }
+
+    @Suppress
+    @Test
+    public void swipeDownKeepsItemSnappedToTopOfList() throws Throwable {
+        setUpPagedListView(ITEMS_PER_PAGE * 2 /* itemCount */);
+
+        // Go down one page, then swipe down (going up).
+        onView(withId(R.id.recycler_view)).perform(scrollToPosition(ITEMS_PER_PAGE));
+        onView(withId(R.id.recycler_view))
+                .perform(actionOnItemAtPosition(ITEMS_PER_PAGE, swipeDown()));
+
+        verifyItemSnappedToListTop();
+    }
+
+    @Test
+    public void pageUpAndDownMoveSameDistance() {
+        if (!isAutoDevice()) {
+            return;
+        }
+
+        setUpPagedListView(ITEMS_PER_PAGE * 10);
+
+        // Move down one page so there will be sufficient pages for up and downs.
+        onView(withId(R.id.page_down)).perform(click());
+        final int topPosition = mPagedListView.getFirstFullyVisibleChildPosition();
+
+        for (int i = 0; i < 3; i++) {
+            onView(withId(R.id.page_down)).perform(click());
+            onView(withId(R.id.page_up)).perform(click());
+        }
+
+        assertThat(mPagedListView.getFirstFullyVisibleChildPosition(), is(equalTo(topPosition)));
+    }
+
+    @Suppress
+    @Test
+    public void setItemSpacing() throws Throwable {
+        final int itemCount = 3;
+        setUpPagedListView(itemCount /* itemCount */);
+
+        // Initial spacing is 0.
+        final View[] views = new View[itemCount];
+        mActivityRule.runOnUiThread(() -> {
+            for (int i = 0; i < itemCount; i++) {
+                views[i] = mPagedListView.findViewByPosition(i);
+            }
+        });
+        for (int i = 0; i < itemCount - 1; i++) {
+            assertThat(views[i + 1].getTop() - views[i].getBottom(), is(equalTo(0)));
+        }
+
+        // Setting item spacing causes layout change.
+        // Implicitly wait for layout by making two calls in UI thread.
+        final int itemSpacing = 10;
+        mActivityRule.runOnUiThread(() -> {
+            mPagedListView.setItemSpacing(itemSpacing);
+        });
+        mActivityRule.runOnUiThread(() -> {
+            for (int i = 0; i < itemCount; i++) {
+                views[i] = mPagedListView.findViewByPosition(i);
+            }
+        });
+        for (int i = 0; i < itemCount - 1; i++) {
+            assertThat(views[i + 1].getTop() - views[i].getBottom(), is(equalTo(itemSpacing)));
+        }
+
+        // Re-setting spacing back to 0 also works.
+        mActivityRule.runOnUiThread(() -> {
+            mPagedListView.setItemSpacing(0);
+        });
+        mActivityRule.runOnUiThread(() -> {
+            for (int i = 0; i < itemCount; i++) {
+                views[i] = mPagedListView.findViewByPosition(i);
+            }
+        });
+        for (int i = 0; i < itemCount - 1; i++) {
+            assertThat(views[i + 1].getTop() - views[i].getBottom(), is(equalTo(0)));
+        }
+    }
+
+    @Test
+    @UiThreadTest
+    public void testSetScrollBarButtonIcons() throws Throwable {
+        // Set up a pagedListView with a large item count to ensure the scroll bar buttons are
+        // always showing.
+        setUpPagedListView(100 /* itemCount */);
+
+        Drawable upDrawable = mActivity.getDrawable(R.drawable.ic_thumb_up);
+        mPagedListView.setUpButtonIcon(upDrawable);
+
+        ImageView upButton = mPagedListView.findViewById(R.id.page_up);
+        ViewMatchers.assertThat(upButton.getDrawable().getConstantState(),
+                is(equalTo(upDrawable.getConstantState())));
+
+        Drawable downDrawable = mActivity.getDrawable(R.drawable.ic_thumb_down);
+        mPagedListView.setDownButtonIcon(downDrawable);
+
+        ImageView downButton = mPagedListView.findViewById(R.id.page_down);
+        ViewMatchers.assertThat(downButton.getDrawable().getConstantState(),
+                is(equalTo(downDrawable.getConstantState())));
+    }
+
+    private static String itemText(int index) {
+        return "Data " + index;
+    }
+
+    private void verifyItemSnappedToListTop() {
+        int firstVisiblePosition = mPagedListView.getFirstFullyVisibleChildPosition();
+        if (firstVisiblePosition > 1) {
+            int lastInPreviousPagePosition = firstVisiblePosition - 1;
+            onView(withText(itemText(lastInPreviousPagePosition)))
+                    .check(matches(not(isDisplayed())));
+        }
+    }
+
+    /** A base adapter that will handle inflating the test view and binding data to it. */
+    private abstract class BaseTestAdapter extends RecyclerView.Adapter<TestViewHolder> {
+        protected List<String> mData;
+        protected int mParentHeight;
+
+        BaseTestAdapter(int itemCount, int parentHeight) {
+            mData = new ArrayList<>();
+            for (int i = 0; i < itemCount; i++) {
+                mData.add(itemText(i));
+            }
+            mParentHeight = parentHeight;
+        }
+
+        @Override
+        public TestViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+            LayoutInflater inflater = LayoutInflater.from(parent.getContext());
+            return new TestViewHolder(inflater, parent);
+        }
+
+        @Override
+        public void onBindViewHolder(TestViewHolder holder, int position) {
+            // Calculate height for an item so one page fits ITEMS_PER_PAGE items.
+            int height = (int) Math.floor(mParentHeight / ITEMS_PER_PAGE);
+            holder.itemView.setMinimumHeight(height);
+            holder.bind(mData.get(position));
+        }
+    }
+
+    private class TestAdapter extends BaseTestAdapter implements PagedListView.ItemCap {
+        private int mMaxItems;
+
+        TestAdapter(int itemCount, int parentHeight) {
+            super(itemCount, parentHeight);
+        }
+
+        @Override
+        public void setMaxItems(int maxItems) {
+            mMaxItems = maxItems;
+        }
+
+        @Override
+        public int getItemCount() {
+            return mMaxItems > 0 ? Math.min(mData.size(), mMaxItems) : mData.size();
+        }
+    }
+
+    /**
+     * A variant of a {@link BaseTestAdapter} that does not implement {@link PagedListView.ItemCap}.
+     */
+    private class NoItemCapAdapter extends BaseTestAdapter {
+        NoItemCapAdapter(int itemCount, int parentHeight) {
+            super(itemCount, parentHeight);
+        }
+
+        @Override
+        public int getItemCount() {
+            return mData.size();
+        }
+    }
+
+    private class TestViewHolder extends RecyclerView.ViewHolder {
+        private TextView mTextView;
+
+        TestViewHolder(LayoutInflater inflater, ViewGroup parent) {
+            super(inflater.inflate(R.layout.paged_list_item_column_card, parent, false));
+            mTextView = itemView.findViewById(R.id.text_view);
+        }
+
+        public void bind(String text) {
+            mTextView.setText(text);
+        }
+    }
+
+    private class PagedListViewScrollingIdlingResource implements IdlingResource {
+
+        private boolean mIdle = true;
+        private ResourceCallback mResourceCallback;
+
+        PagedListViewScrollingIdlingResource(PagedListView pagedListView) {
+            pagedListView.getRecyclerView().addOnScrollListener(
+                    new RecyclerView.OnScrollListener() {
+                        @Override
+                        public void onScrollStateChanged(
+                                RecyclerView recyclerView, int newState) {
+                            super.onScrollStateChanged(recyclerView, newState);
+                            mIdle = (newState == RecyclerView.SCROLL_STATE_IDLE
+                                    // Treat dragging as idle, or Espresso will block itself when
+                                    // swiping.
+                                    || newState == RecyclerView.SCROLL_STATE_DRAGGING);
+                            if (mIdle && mResourceCallback != null) {
+                                mResourceCallback.onTransitionToIdle();
+                            }
+                        }
+
+                        @Override
+                        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+                        }
+                    });
+        }
+
+        @Override
+        public String getName() {
+            return PagedListViewScrollingIdlingResource.class.getName();
+        }
+
+        @Override
+        public boolean isIdleNow() {
+            return mIdle;
+        }
+
+        @Override
+        public void registerIdleTransitionCallback(ResourceCallback callback) {
+            mResourceCallback = callback;
+        }
+    }
+}
diff --git a/car/tests/src/android/support/car/widget/PagedListViewTestActivity.java b/car/tests/src/android/support/car/widget/PagedListViewTestActivity.java
new file mode 100644
index 0000000..6371374
--- /dev/null
+++ b/car/tests/src/android/support/car/widget/PagedListViewTestActivity.java
@@ -0,0 +1,33 @@
+/*
+ * 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;
+
+/**
+ * Simple test activity for {@link PagedListView} class.
+ *
+ */
+public class PagedListViewTestActivity extends Activity {
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_paged_list_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 82d503c..b8ea13b 100644
--- a/compat/build.gradle
+++ b/compat/build.gradle
@@ -16,7 +16,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/src/main/java/android/support/v4/view/ViewCompat.java b/compat/src/main/java/android/support/v4/view/ViewCompat.java
index 34a198a..204a121 100644
--- a/compat/src/main/java/android/support/v4/view/ViewCompat.java
+++ b/compat/src/main/java/android/support/v4/view/ViewCompat.java
@@ -1356,7 +1356,7 @@
                 // after applying the tint
                 Drawable background = view.getBackground();
                 boolean hasTint = (view.getBackgroundTintList() != null)
-                        && (view.getBackgroundTintMode() != null);
+                        || (view.getBackgroundTintMode() != null);
                 if ((background != null) && hasTint) {
                     if (background.isStateful()) {
                         background.setState(view.getDrawableState());
@@ -1375,7 +1375,7 @@
                 // after applying the tint
                 Drawable background = view.getBackground();
                 boolean hasTint = (view.getBackgroundTintList() != null)
-                        && (view.getBackgroundTintMode() != null);
+                        || (view.getBackgroundTintMode() != null);
                 if ((background != null) && hasTint) {
                     if (background.isStateful()) {
                         background.setState(view.getDrawableState());
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/app/ActivityCompatTest.java b/compat/tests/java/android/support/v4/app/ActivityCompatTest.java
index 3b4f1b5..35889fb 100644
--- a/compat/tests/java/android/support/v4/app/ActivityCompatTest.java
+++ b/compat/tests/java/android/support/v4/app/ActivityCompatTest.java
@@ -25,6 +25,7 @@
 
 import android.Manifest;
 import android.app.Activity;
+import android.support.test.filters.SdkSuppress;
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
 import android.support.v4.BaseInstrumentationTestCase;
@@ -40,6 +41,7 @@
         super(TestSupportActivity.class);
     }
 
+    @SdkSuppress(minSdkVersion = 24)
     @SmallTest
     @Test
     public void testPermissionDelegate() {
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/content/OWNERS b/content/OWNERS
new file mode 100644
index 0000000..779e918
--- /dev/null
+++ b/content/OWNERS
@@ -0,0 +1 @@
+smckay@google.com
\ No newline at end of file
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 cd70447..098440d 100644
--- a/core-ui/build.gradle
+++ b/core-ui/build.gradle
@@ -8,12 +8,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 {
@@ -21,6 +27,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..c45810e 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;
@@ -56,6 +56,8 @@
 import android.support.v4.view.ViewCompat.NestedScrollType;
 import android.support.v4.view.ViewCompat.ScrollAxis;
 import android.support.v4.view.WindowInsetsCompat;
+import android.support.v4.widget.DirectedAcyclicGraph;
+import android.support.v4.widget.ViewGroupUtils;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.util.Log;
@@ -86,25 +88,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 +199,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();
@@ -609,8 +612,8 @@
             }
             Constructor<Behavior> c = constructors.get(fullName);
             if (c == null) {
-                final Class<Behavior> clazz = (Class<Behavior>) Class.forName(fullName, true,
-                        context.getClassLoader());
+                final Class<Behavior> clazz = (Class<Behavior>) context.getClassLoader()
+                        .loadClass(fullName);
                 c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
                 c.setAccessible(true);
                 constructors.put(fullName, c);
@@ -703,7 +706,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 +837,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 +902,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 +915,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 +1472,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 +2082,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 +2112,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 +2143,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 +2157,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 +2185,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 +2201,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 +2227,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 +2634,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 +2663,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 +2820,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 +2871,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 +2905,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 +2966,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 +3148,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 +3228,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/v4/widget/DirectedAcyclicGraph.java
similarity index 85%
rename from design/src/android/support/design/widget/DirectedAcyclicGraph.java
rename to core-ui/src/main/java/android/support/v4/widget/DirectedAcyclicGraph.java
index 85a32cd..83c62c0 100644
--- a/design/src/android/support/design/widget/DirectedAcyclicGraph.java
+++ b/core-ui/src/main/java/android/support/v4/widget/DirectedAcyclicGraph.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright 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.
@@ -14,10 +14,13 @@
  * limitations under the License.
  */
 
-package android.support.design.widget;
+package android.support.v4.widget;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY;
 
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
 import android.support.v4.util.Pools;
 import android.support.v4.util.SimpleArrayMap;
 
@@ -27,8 +30,13 @@
 
 /**
  * A class which represents a simple directed acyclic graph.
+ *
+ * @param <T> Class for the data objects of this graph.
+ *
+ * @hide
  */
-final class DirectedAcyclicGraph<T> {
+@RestrictTo(LIBRARY)
+public final class DirectedAcyclicGraph<T> {
     private final Pools.Pool<ArrayList<T>> mListPool = new Pools.SimplePool<>(10);
     private final SimpleArrayMap<T, ArrayList<T>> mGraph = new SimpleArrayMap<>();
 
@@ -42,7 +50,7 @@
      *
      * @param node the node to add
      */
-    void addNode(@NonNull T node) {
+    public void addNode(@NonNull T node) {
         if (!mGraph.containsKey(node)) {
             mGraph.put(node, null);
         }
@@ -51,7 +59,7 @@
     /**
      * Returns true if the node is already present in the graph, false otherwise.
      */
-    boolean contains(@NonNull T node) {
+    public boolean contains(@NonNull T node) {
         return mGraph.containsKey(node);
     }
 
@@ -64,7 +72,7 @@
      * @param node the parent node
      * @param incomingEdge the node which has is an incoming edge to {@code node}
      */
-    void addEdge(@NonNull T node, @NonNull T incomingEdge) {
+    public void addEdge(@NonNull T node, @NonNull T incomingEdge) {
         if (!mGraph.containsKey(node) || !mGraph.containsKey(incomingEdge)) {
             throw new IllegalArgumentException("All nodes must be present in the graph before"
                     + " being added as an edge");
@@ -86,7 +94,7 @@
      * @return a list containing any incoming edges, or null if there are none.
      */
     @Nullable
-    List getIncomingEdges(@NonNull T node) {
+    public List getIncomingEdges(@NonNull T node) {
         return mGraph.get(node);
     }
 
@@ -97,7 +105,7 @@
      * @return a list containing any outgoing edges, or null if there are none.
      */
     @Nullable
-    List<T> getOutgoingEdges(@NonNull T node) {
+    public List<T> getOutgoingEdges(@NonNull T node) {
         ArrayList<T> result = null;
         for (int i = 0, size = mGraph.size(); i < size; i++) {
             ArrayList<T> edges = mGraph.valueAt(i);
@@ -111,7 +119,14 @@
         return result;
     }
 
-    boolean hasOutgoingEdges(@NonNull T node) {
+    /**
+     * Checks whether we have any outgoing edges for the given node (i.e. nodes which have
+     * an incoming edge from the given node).
+     *
+     * @return <code>true</code> if the node has any outgoing edges, <code>false</code>
+     * otherwise.
+     */
+    public boolean hasOutgoingEdges(@NonNull T node) {
         for (int i = 0, size = mGraph.size(); i < size; i++) {
             ArrayList<T> edges = mGraph.valueAt(i);
             if (edges != null && edges.contains(node)) {
@@ -124,7 +139,7 @@
     /**
      * Clears the internal graph, and releases resources to pools.
      */
-    void clear() {
+    public void clear() {
         for (int i = 0, size = mGraph.size(); i < size; i++) {
             ArrayList<T> edges = mGraph.valueAt(i);
             if (edges != null) {
@@ -143,7 +158,7 @@
      * of the graph. The node at the end of the list will have no dependencies on other nodes.</p>
      */
     @NonNull
-    ArrayList<T> getSortedList() {
+    public ArrayList<T> getSortedList() {
         mSortResult.clear();
         mSortTmpMarked.clear();
 
@@ -198,4 +213,4 @@
         list.clear();
         mListPool.release(list);
     }
-}
\ No newline at end of file
+}
diff --git a/design/src/android/support/design/widget/ViewGroupUtils.java b/core-ui/src/main/java/android/support/v4/widget/ViewGroupUtils.java
similarity index 86%
rename from design/src/android/support/design/widget/ViewGroupUtils.java
rename to core-ui/src/main/java/android/support/v4/widget/ViewGroupUtils.java
index 0545516..986b4c2 100644
--- a/design/src/android/support/design/widget/ViewGroupUtils.java
+++ b/core-ui/src/main/java/android/support/v4/widget/ViewGroupUtils.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright 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.
@@ -14,22 +14,29 @@
  * limitations under the License.
  */
 
-package android.support.design.widget;
+package android.support.v4.widget;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY;
 
 import android.graphics.Matrix;
 import android.graphics.Rect;
 import android.graphics.RectF;
+import android.support.annotation.RestrictTo;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewParent;
 
-class ViewGroupUtils {
+/**
+ * @hide
+ */
+@RestrictTo(LIBRARY)
+public class ViewGroupUtils {
     private static final ThreadLocal<Matrix> sMatrix = new ThreadLocal<>();
     private static final ThreadLocal<RectF> sRectF = new ThreadLocal<>();
 
     /**
      * 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.
      *
@@ -65,7 +72,7 @@
      * @param descendant descendant view to reference
      * @param out rect to set to the bounds of the descendant view
      */
-    static void getDescendantRect(ViewGroup parent, View descendant, Rect out) {
+    public static void getDescendantRect(ViewGroup parent, View descendant, Rect out) {
         out.set(0, 0, descendant.getWidth(), descendant.getHeight());
         offsetDescendantRect(parent, descendant, out);
     }
diff --git a/design/jvm-tests/src/android/support/design/widget/DirectedAcyclicGraphTest.java b/core-ui/tests/java/android/support/v4/widget/DirectedAcyclicGraphTest.java
similarity index 97%
rename from design/jvm-tests/src/android/support/design/widget/DirectedAcyclicGraphTest.java
rename to core-ui/tests/java/android/support/v4/widget/DirectedAcyclicGraphTest.java
index 4a5ffc5..8355fcc 100644
--- a/design/jvm-tests/src/android/support/design/widget/DirectedAcyclicGraphTest.java
+++ b/core-ui/tests/java/android/support/v4/widget/DirectedAcyclicGraphTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package android.support.design.widget;
+package android.support.v4.widget;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -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 b384a37..64c9ff8 100644
--- a/core-utils/build.gradle
+++ b/core-utils/build.gradle
@@ -19,6 +19,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 3af966d..06a5a55 100644
--- a/design/build.gradle
+++ b/design/build.gradle
@@ -17,11 +17,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 {
@@ -44,8 +39,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/src/android/support/design/widget/CollapsingToolbarLayout.java b/design/src/android/support/design/widget/CollapsingToolbarLayout.java
index 0051de9..8c9b7d4 100644
--- a/design/src/android/support/design/widget/CollapsingToolbarLayout.java
+++ b/design/src/android/support/design/widget/CollapsingToolbarLayout.java
@@ -44,6 +44,7 @@
 import android.support.v4.view.GravityCompat;
 import android.support.v4.view.ViewCompat;
 import android.support.v4.view.WindowInsetsCompat;
+import android.support.v4.widget.ViewGroupUtils;
 import android.support.v7.widget.Toolbar;
 import android.text.TextUtils;
 import android.util.AttributeSet;
diff --git a/design/src/android/support/design/widget/FloatingActionButton.java b/design/src/android/support/design/widget/FloatingActionButton.java
index b938836..a53096e 100644
--- a/design/src/android/support/design/widget/FloatingActionButton.java
+++ b/design/src/android/support/design/widget/FloatingActionButton.java
@@ -36,6 +36,7 @@
 import android.support.design.R;
 import android.support.design.widget.FloatingActionButtonImpl.InternalVisibilityChangedListener;
 import android.support.v4.view.ViewCompat;
+import android.support.v4.widget.ViewGroupUtils;
 import android.support.v7.widget.AppCompatImageHelper;
 import android.util.AttributeSet;
 import android.util.Log;
diff --git a/design/src/android/support/design/widget/TextInputLayout.java b/design/src/android/support/design/widget/TextInputLayout.java
index c9e8010..82c3a2a 100644
--- a/design/src/android/support/design/widget/TextInputLayout.java
+++ b/design/src/android/support/design/widget/TextInputLayout.java
@@ -49,6 +49,7 @@
 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
 import android.support.v4.widget.Space;
 import android.support.v4.widget.TextViewCompat;
+import android.support.v4.widget.ViewGroupUtils;
 import android.support.v7.content.res.AppCompatResources;
 import android.support.v7.widget.AppCompatDrawableManager;
 import android.support.v7.widget.AppCompatTextView;
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 dfb6ca9..73977c2 100644
--- a/fragment/build.gradle
+++ b/fragment/build.gradle
@@ -13,8 +13,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/HangingFragmentTest.java b/fragment/tests/java/android/support/v4/app/HangingFragmentTest.java
index e124b67..bf8726f 100644
--- a/fragment/tests/java/android/support/v4/app/HangingFragmentTest.java
+++ b/fragment/tests/java/android/support/v4/app/HangingFragmentTest.java
@@ -19,6 +19,7 @@
 import android.support.test.filters.SmallTest;
 import android.support.test.rule.ActivityTestRule;
 import android.support.test.runner.AndroidJUnit4;
+import android.support.testutils.FragmentActivityUtils;
 import android.support.v4.app.test.HangingFragmentActivity;
 
 import org.junit.Assert;
@@ -37,7 +38,7 @@
 
     @Test
     public void testNoCrash() throws InterruptedException {
-        HangingFragmentActivity newActivity = FragmentTestUtil.recreateActivity(
+        HangingFragmentActivity newActivity = FragmentActivityUtils.recreateActivity(
                 mActivityRule, mActivityRule.getActivity());
         Assert.assertNotNull(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/HangingFragmentActivity.java b/fragment/tests/java/android/support/v4/app/test/HangingFragmentActivity.java
index 9fab4df..80b9aa5 100644
--- a/fragment/tests/java/android/support/v4/app/test/HangingFragmentActivity.java
+++ b/fragment/tests/java/android/support/v4/app/test/HangingFragmentActivity.java
@@ -19,6 +19,7 @@
 import android.os.Bundle;
 import android.support.annotation.Nullable;
 import android.support.fragment.test.R;
+import android.support.testutils.RecreatedActivity;
 
 public class HangingFragmentActivity extends RecreatedActivity {
 
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/v17/leanback/Android.mk b/leanback/Android.mk
similarity index 100%
rename from v17/leanback/Android.mk
rename to leanback/Android.mk
diff --git a/v17/leanback/AndroidManifest.xml b/leanback/AndroidManifest.xml
similarity index 100%
rename from v17/leanback/AndroidManifest.xml
rename to leanback/AndroidManifest.xml
diff --git a/v17/leanback/OWNERS b/leanback/OWNERS
similarity index 100%
rename from v17/leanback/OWNERS
rename to leanback/OWNERS
diff --git a/v17/leanback/api/26.0.0.ignore b/leanback/api/26.0.0.ignore
similarity index 100%
rename from v17/leanback/api/26.0.0.ignore
rename to leanback/api/26.0.0.ignore
diff --git a/v17/leanback/api/26.0.0.txt b/leanback/api/26.0.0.txt
similarity index 100%
rename from v17/leanback/api/26.0.0.txt
rename to leanback/api/26.0.0.txt
diff --git a/v17/leanback/api/26.1.0.ignore b/leanback/api/26.1.0.ignore
similarity index 100%
rename from v17/leanback/api/26.1.0.ignore
rename to leanback/api/26.1.0.ignore
diff --git a/v17/leanback/api/26.1.0.txt b/leanback/api/26.1.0.txt
similarity index 100%
rename from v17/leanback/api/26.1.0.txt
rename to leanback/api/26.1.0.txt
diff --git a/v17/leanback/api/27.0.0.txt b/leanback/api/27.0.0.txt
similarity index 100%
rename from v17/leanback/api/27.0.0.txt
rename to leanback/api/27.0.0.txt
diff --git a/v17/leanback/api/current.txt b/leanback/api/current.txt
similarity index 97%
rename from v17/leanback/api/current.txt
rename to leanback/api/current.txt
index 4ee4d94..4a5067c 100644
--- a/v17/leanback/api/current.txt
+++ b/leanback/api/current.txt
@@ -20,7 +20,7 @@
     method public void setThemeDrawableResourceId(int);
   }
 
-  public class BaseFragment extends android.support.v17.leanback.app.BrandedFragment {
+  public deprecated class BaseFragment extends android.support.v17.leanback.app.BrandedFragment {
     method protected java.lang.Object createEntranceTransition();
     method public final android.support.v17.leanback.app.ProgressBarManager getProgressBarManager();
     method protected void onEntranceTransitionEnd();
@@ -31,7 +31,7 @@
     method public void startEntranceTransition();
   }
 
-   abstract class BaseRowFragment extends android.app.Fragment {
+   abstract deprecated class BaseRowFragment extends android.app.Fragment {
     method public final android.support.v17.leanback.widget.ObjectAdapter getAdapter();
     method public final android.support.v17.leanback.widget.ItemBridgeAdapter getBridgeAdapter();
     method public final android.support.v17.leanback.widget.PresenterSelector getPresenterSelector();
@@ -74,7 +74,7 @@
     method public void startEntranceTransition();
   }
 
-  public class BrandedFragment extends android.app.Fragment {
+  public deprecated class BrandedFragment extends android.app.Fragment {
     ctor public BrandedFragment();
     method public android.graphics.drawable.Drawable getBadgeDrawable();
     method public int getSearchAffordanceColor();
@@ -116,7 +116,7 @@
     method public void showTitle(int);
   }
 
-  public class BrowseFragment extends android.support.v17.leanback.app.BaseFragment {
+  public deprecated class BrowseFragment extends android.support.v17.leanback.app.BaseFragment {
     ctor public BrowseFragment();
     method public static android.os.Bundle createArgs(android.os.Bundle, java.lang.String, int);
     method public void enableMainFragmentScaling(boolean);
@@ -153,29 +153,29 @@
     field public static final int HEADERS_HIDDEN = 2; // 0x2
   }
 
-  public static class BrowseFragment.BrowseTransitionListener {
+  public static deprecated class BrowseFragment.BrowseTransitionListener {
     ctor public BrowseFragment.BrowseTransitionListener();
     method public void onHeadersTransitionStart(boolean);
     method public void onHeadersTransitionStop(boolean);
   }
 
-  public static abstract class BrowseFragment.FragmentFactory<T extends android.app.Fragment> {
+  public static abstract deprecated class BrowseFragment.FragmentFactory<T extends android.app.Fragment> {
     ctor public BrowseFragment.FragmentFactory();
     method public abstract T createFragment(java.lang.Object);
   }
 
-  public static abstract interface BrowseFragment.FragmentHost {
+  public static abstract deprecated interface BrowseFragment.FragmentHost {
     method public abstract void notifyDataReady(android.support.v17.leanback.app.BrowseFragment.MainFragmentAdapter);
     method public abstract void notifyViewCreated(android.support.v17.leanback.app.BrowseFragment.MainFragmentAdapter);
     method public abstract void showTitleView(boolean);
   }
 
-  public static class BrowseFragment.ListRowFragmentFactory extends android.support.v17.leanback.app.BrowseFragment.FragmentFactory {
+  public static deprecated class BrowseFragment.ListRowFragmentFactory extends android.support.v17.leanback.app.BrowseFragment.FragmentFactory {
     ctor public BrowseFragment.ListRowFragmentFactory();
     method public android.support.v17.leanback.app.RowsFragment createFragment(java.lang.Object);
   }
 
-  public static class BrowseFragment.MainFragmentAdapter<T extends android.app.Fragment> {
+  public static deprecated class BrowseFragment.MainFragmentAdapter<T extends android.app.Fragment> {
     ctor public BrowseFragment.MainFragmentAdapter(T);
     method public final T getFragment();
     method public final android.support.v17.leanback.app.BrowseFragment.FragmentHost getFragmentHost();
@@ -190,17 +190,17 @@
     method public void setScalingEnabled(boolean);
   }
 
-  public static abstract interface BrowseFragment.MainFragmentAdapterProvider {
+  public static abstract deprecated interface BrowseFragment.MainFragmentAdapterProvider {
     method public abstract android.support.v17.leanback.app.BrowseFragment.MainFragmentAdapter getMainFragmentAdapter();
   }
 
-  public static final class BrowseFragment.MainFragmentAdapterRegistry {
+  public static final deprecated class BrowseFragment.MainFragmentAdapterRegistry {
     ctor public BrowseFragment.MainFragmentAdapterRegistry();
     method public android.app.Fragment createFragment(java.lang.Object);
     method public void registerFragment(java.lang.Class, android.support.v17.leanback.app.BrowseFragment.FragmentFactory);
   }
 
-  public static class BrowseFragment.MainFragmentRowsAdapter<T extends android.app.Fragment> {
+  public static deprecated class BrowseFragment.MainFragmentRowsAdapter<T extends android.app.Fragment> {
     ctor public BrowseFragment.MainFragmentRowsAdapter(T);
     method public android.support.v17.leanback.widget.RowPresenter.ViewHolder findRowViewHolderByPosition(int);
     method public final T getFragment();
@@ -212,7 +212,7 @@
     method public void setSelectedPosition(int, boolean);
   }
 
-  public static abstract interface BrowseFragment.MainFragmentRowsAdapterProvider {
+  public static abstract deprecated interface BrowseFragment.MainFragmentRowsAdapterProvider {
     method public abstract android.support.v17.leanback.app.BrowseFragment.MainFragmentRowsAdapter getMainFragmentRowsAdapter();
   }
 
@@ -316,7 +316,7 @@
     method public abstract android.support.v17.leanback.app.BrowseSupportFragment.MainFragmentRowsAdapter getMainFragmentRowsAdapter();
   }
 
-  public class DetailsFragment extends android.support.v17.leanback.app.BaseFragment {
+  public deprecated class DetailsFragment extends android.support.v17.leanback.app.BaseFragment {
     ctor public DetailsFragment();
     method public android.support.v17.leanback.widget.ObjectAdapter getAdapter();
     method public android.support.v17.leanback.widget.BaseOnItemViewClickedListener getOnItemViewClickedListener();
@@ -334,7 +334,7 @@
     method protected void setupPresenter(android.support.v17.leanback.widget.Presenter);
   }
 
-  public class DetailsFragmentBackgroundController {
+  public deprecated class DetailsFragmentBackgroundController {
     ctor public DetailsFragmentBackgroundController(android.support.v17.leanback.app.DetailsFragment);
     method public boolean canNavigateToVideoFragment();
     method public void enableParallax();
@@ -396,7 +396,7 @@
     method public final void switchToVideo();
   }
 
-  public class ErrorFragment extends android.support.v17.leanback.app.BrandedFragment {
+  public deprecated class ErrorFragment extends android.support.v17.leanback.app.BrandedFragment {
     ctor public ErrorFragment();
     method public android.graphics.drawable.Drawable getBackgroundDrawable();
     method public android.view.View.OnClickListener getButtonClickListener();
@@ -428,7 +428,7 @@
     method public void setMessage(java.lang.CharSequence);
   }
 
-  public class GuidedStepFragment extends android.app.Fragment {
+  public deprecated class GuidedStepFragment extends android.app.Fragment {
     ctor public GuidedStepFragment();
     method public static int add(android.app.FragmentManager, android.support.v17.leanback.app.GuidedStepFragment);
     method public static int add(android.app.FragmentManager, android.support.v17.leanback.app.GuidedStepFragment, int);
@@ -478,6 +478,7 @@
     method public void openInEditMode(android.support.v17.leanback.widget.GuidedAction);
     method public void popBackStackToGuidedStepFragment(java.lang.Class, int);
     method public void setActions(java.util.List<android.support.v17.leanback.widget.GuidedAction>);
+    method public void setActionsDiffCallback(android.support.v17.leanback.widget.DiffCallback<android.support.v17.leanback.widget.GuidedAction>);
     method public void setButtonActions(java.util.List<android.support.v17.leanback.widget.GuidedAction>);
     method public void setSelectedActionPosition(int);
     method public void setSelectedButtonActionPosition(int);
@@ -539,6 +540,7 @@
     method public void openInEditMode(android.support.v17.leanback.widget.GuidedAction);
     method public void popBackStackToGuidedStepSupportFragment(java.lang.Class, int);
     method public void setActions(java.util.List<android.support.v17.leanback.widget.GuidedAction>);
+    method public void setActionsDiffCallback(android.support.v17.leanback.widget.DiffCallback<android.support.v17.leanback.widget.GuidedAction>);
     method public void setButtonActions(java.util.List<android.support.v17.leanback.widget.GuidedAction>);
     method public void setSelectedActionPosition(int);
     method public void setSelectedButtonActionPosition(int);
@@ -550,18 +552,18 @@
     field public static final int UI_STYLE_REPLACE = 0; // 0x0
   }
 
-  public class HeadersFragment extends android.support.v17.leanback.app.BaseRowFragment {
+  public deprecated class HeadersFragment extends android.support.v17.leanback.app.BaseRowFragment {
     ctor public HeadersFragment();
     method public boolean isScrolling();
     method public void setOnHeaderClickedListener(android.support.v17.leanback.app.HeadersFragment.OnHeaderClickedListener);
     method public void setOnHeaderViewSelectedListener(android.support.v17.leanback.app.HeadersFragment.OnHeaderViewSelectedListener);
   }
 
-  public static abstract interface HeadersFragment.OnHeaderClickedListener {
+  public static abstract deprecated interface HeadersFragment.OnHeaderClickedListener {
     method public abstract void onHeaderClicked(android.support.v17.leanback.widget.RowHeaderPresenter.ViewHolder, android.support.v17.leanback.widget.Row);
   }
 
-  public static abstract interface HeadersFragment.OnHeaderViewSelectedListener {
+  public static abstract deprecated interface HeadersFragment.OnHeaderViewSelectedListener {
     method public abstract void onHeaderSelected(android.support.v17.leanback.widget.RowHeaderPresenter.ViewHolder, android.support.v17.leanback.widget.Row);
   }
 
@@ -580,7 +582,7 @@
     method public abstract void onHeaderSelected(android.support.v17.leanback.widget.RowHeaderPresenter.ViewHolder, android.support.v17.leanback.widget.Row);
   }
 
-  public abstract class OnboardingFragment extends android.app.Fragment {
+  public abstract deprecated class OnboardingFragment extends android.app.Fragment {
     ctor public OnboardingFragment();
     method public final int getArrowBackgroundColor();
     method public final int getArrowColor();
@@ -658,7 +660,7 @@
     method protected final void startEnterAnimation(boolean);
   }
 
-  public class PlaybackFragment extends android.app.Fragment {
+  public deprecated class PlaybackFragment extends android.app.Fragment {
     ctor public PlaybackFragment();
     method public deprecated void fadeOut();
     method public android.support.v17.leanback.widget.ObjectAdapter getAdapter();
@@ -693,7 +695,7 @@
     field public static final int BG_NONE = 0; // 0x0
   }
 
-  public class PlaybackFragmentGlueHost extends android.support.v17.leanback.media.PlaybackGlueHost implements android.support.v17.leanback.widget.PlaybackSeekUi {
+  public deprecated class PlaybackFragmentGlueHost extends android.support.v17.leanback.media.PlaybackGlueHost implements android.support.v17.leanback.widget.PlaybackSeekUi {
     ctor public PlaybackFragmentGlueHost(android.support.v17.leanback.app.PlaybackFragment);
     method public void fadeOut();
     method public void setPlaybackSeekUiClient(android.support.v17.leanback.widget.PlaybackSeekUi.Client);
@@ -752,7 +754,7 @@
     method public void show();
   }
 
-  public class RowsFragment extends android.support.v17.leanback.app.BaseRowFragment implements android.support.v17.leanback.app.BrowseFragment.MainFragmentAdapterProvider android.support.v17.leanback.app.BrowseFragment.MainFragmentRowsAdapterProvider {
+  public deprecated class RowsFragment extends android.support.v17.leanback.app.BaseRowFragment implements android.support.v17.leanback.app.BrowseFragment.MainFragmentAdapterProvider android.support.v17.leanback.app.BrowseFragment.MainFragmentRowsAdapterProvider {
     ctor public RowsFragment();
     method public deprecated void enableRowScaling(boolean);
     method protected android.support.v17.leanback.widget.VerticalGridView findGridViewFromRoot(android.view.View);
@@ -774,7 +776,7 @@
     ctor public RowsFragment.MainFragmentAdapter(android.support.v17.leanback.app.RowsFragment);
   }
 
-  public static class RowsFragment.MainFragmentRowsAdapter extends android.support.v17.leanback.app.BrowseFragment.MainFragmentRowsAdapter {
+  public static deprecated class RowsFragment.MainFragmentRowsAdapter extends android.support.v17.leanback.app.BrowseFragment.MainFragmentRowsAdapter {
     ctor public RowsFragment.MainFragmentRowsAdapter(android.support.v17.leanback.app.RowsFragment);
   }
 
@@ -804,7 +806,7 @@
     ctor public RowsSupportFragment.MainFragmentRowsAdapter(android.support.v17.leanback.app.RowsSupportFragment);
   }
 
-  public class SearchFragment extends android.app.Fragment {
+  public deprecated class SearchFragment extends android.app.Fragment {
     ctor public SearchFragment();
     method public static android.os.Bundle createArgs(android.os.Bundle, java.lang.String);
     method public static android.os.Bundle createArgs(android.os.Bundle, java.lang.String, java.lang.String);
@@ -864,7 +866,7 @@
     method public abstract boolean onQueryTextSubmit(java.lang.String);
   }
 
-  public class VerticalGridFragment extends android.support.v17.leanback.app.BaseFragment {
+  public deprecated class VerticalGridFragment extends android.support.v17.leanback.app.BaseFragment {
     ctor public VerticalGridFragment();
     method public android.support.v17.leanback.widget.ObjectAdapter getAdapter();
     method public android.support.v17.leanback.widget.VerticalGridPresenter getGridPresenter();
@@ -888,13 +890,13 @@
     method public void setSelectedPosition(int);
   }
 
-  public class VideoFragment extends android.support.v17.leanback.app.PlaybackFragment {
+  public deprecated class VideoFragment extends android.support.v17.leanback.app.PlaybackFragment {
     ctor public VideoFragment();
     method public android.view.SurfaceView getSurfaceView();
     method public void setSurfaceHolderCallback(android.view.SurfaceHolder.Callback);
   }
 
-  public class VideoFragmentGlueHost extends android.support.v17.leanback.app.PlaybackFragmentGlueHost implements android.support.v17.leanback.media.SurfaceHolderGlueHost {
+  public deprecated class VideoFragmentGlueHost extends android.support.v17.leanback.app.PlaybackFragmentGlueHost implements android.support.v17.leanback.media.SurfaceHolderGlueHost {
     ctor public VideoFragmentGlueHost(android.support.v17.leanback.app.VideoFragment);
     method public void setSurfaceHolderCallback(android.view.SurfaceHolder.Callback);
   }
@@ -1913,6 +1915,13 @@
     method public B title(int);
   }
 
+  public class GuidedActionDiffCallback extends android.support.v17.leanback.widget.DiffCallback {
+    ctor public GuidedActionDiffCallback();
+    method public boolean areContentsTheSame(android.support.v17.leanback.widget.GuidedAction, android.support.v17.leanback.widget.GuidedAction);
+    method public boolean areItemsTheSame(android.support.v17.leanback.widget.GuidedAction, android.support.v17.leanback.widget.GuidedAction);
+    method public static final android.support.v17.leanback.widget.GuidedActionDiffCallback getInstance();
+  }
+
   public class GuidedActionEditText extends android.widget.EditText implements android.support.v17.leanback.widget.ImeKeyMonitor {
     ctor public GuidedActionEditText(android.content.Context);
     ctor public GuidedActionEditText(android.content.Context, android.util.AttributeSet);
diff --git a/v17/leanback/api21/android/support/v17/leanback/transition/FadeAndShortSlide.java b/leanback/api21/android/support/v17/leanback/transition/FadeAndShortSlide.java
similarity index 100%
rename from v17/leanback/api21/android/support/v17/leanback/transition/FadeAndShortSlide.java
rename to leanback/api21/android/support/v17/leanback/transition/FadeAndShortSlide.java
diff --git a/v17/leanback/api21/android/support/v17/leanback/transition/SlideNoPropagation.java b/leanback/api21/android/support/v17/leanback/transition/SlideNoPropagation.java
similarity index 100%
rename from v17/leanback/api21/android/support/v17/leanback/transition/SlideNoPropagation.java
rename to leanback/api21/android/support/v17/leanback/transition/SlideNoPropagation.java
diff --git a/v17/leanback/api21/android/support/v17/leanback/transition/TransitionHelperApi21.java b/leanback/api21/android/support/v17/leanback/transition/TransitionHelperApi21.java
similarity index 100%
rename from v17/leanback/api21/android/support/v17/leanback/transition/TransitionHelperApi21.java
rename to leanback/api21/android/support/v17/leanback/transition/TransitionHelperApi21.java
diff --git a/v17/leanback/api21/android/support/v17/leanback/transition/TranslationAnimationCreator.java b/leanback/api21/android/support/v17/leanback/transition/TranslationAnimationCreator.java
similarity index 100%
rename from v17/leanback/api21/android/support/v17/leanback/transition/TranslationAnimationCreator.java
rename to leanback/api21/android/support/v17/leanback/transition/TranslationAnimationCreator.java
diff --git a/v17/leanback/api21/android/support/v17/leanback/widget/RoundedRectHelperApi21.java b/leanback/api21/android/support/v17/leanback/widget/RoundedRectHelperApi21.java
similarity index 100%
rename from v17/leanback/api21/android/support/v17/leanback/widget/RoundedRectHelperApi21.java
rename to leanback/api21/android/support/v17/leanback/widget/RoundedRectHelperApi21.java
diff --git a/v17/leanback/api21/android/support/v17/leanback/widget/ShadowHelperApi21.java b/leanback/api21/android/support/v17/leanback/widget/ShadowHelperApi21.java
similarity index 100%
rename from v17/leanback/api21/android/support/v17/leanback/widget/ShadowHelperApi21.java
rename to leanback/api21/android/support/v17/leanback/widget/ShadowHelperApi21.java
diff --git a/v17/leanback/build.gradle b/leanback/build.gradle
similarity index 100%
rename from v17/leanback/build.gradle
rename to leanback/build.gradle
diff --git a/v17/leanback/common/android/support/v17/leanback/transition/TransitionEpicenterCallback.java b/leanback/common/android/support/v17/leanback/transition/TransitionEpicenterCallback.java
similarity index 100%
rename from v17/leanback/common/android/support/v17/leanback/transition/TransitionEpicenterCallback.java
rename to leanback/common/android/support/v17/leanback/transition/TransitionEpicenterCallback.java
diff --git a/v17/leanback/common/android/support/v17/leanback/transition/TransitionListener.java b/leanback/common/android/support/v17/leanback/transition/TransitionListener.java
similarity index 100%
rename from v17/leanback/common/android/support/v17/leanback/transition/TransitionListener.java
rename to leanback/common/android/support/v17/leanback/transition/TransitionListener.java
diff --git a/v17/leanback/generatef.py b/leanback/generatef.py
similarity index 85%
rename from v17/leanback/generatef.py
rename to leanback/generatef.py
index 04e303a..6364f09 100755
--- a/v17/leanback/generatef.py
+++ b/leanback/generatef.py
@@ -45,8 +45,8 @@
         content = content + line
     file.close()
     # add deprecated tag to fragment class and inner classes/interfaces
-    # content = re.sub(r'\*\/\n(@.*\n|)(public |abstract public |abstract |)class', '* @deprecated use {@link ' + w + 'SupportFragment}\n */\n@Deprecated\n\\1\\2class', content)
-    # content = re.sub(r'\*\/\n    public (static class|interface|final static class|abstract static class)', '* @deprecated use {@link ' + w + 'SupportFragment}\n     */\n    @Deprecated\n    public \\1', content)
+    content = re.sub(r'\*\/\n(@.*\n|)(public |abstract public |abstract |)class', '* @deprecated use {@link ' + w + 'SupportFragment}\n */\n@Deprecated\n\\1\\2class', content)
+    content = re.sub(r'\*\/\n    public (static class|interface|final static class|abstract static class)', '* @deprecated use {@link ' + w + 'SupportFragment}\n     */\n    @Deprecated\n    public \\1', content)
     outfile = open('src/android/support/v17/leanback/app/{}Fragment.java'.format(w), 'w')
     outfile.write(content)
     outfile.close()
@@ -64,7 +64,7 @@
     content = content + line
 file.close()
 # add deprecated tag to class
-# content = re.sub(r'\*\/\npublic class', '* @deprecated use {@link VideoSupportFragmentGlueHost}\n */\n@Deprecated\npublic class', content)
+content = re.sub(r'\*\/\npublic class', '* @deprecated use {@link VideoSupportFragmentGlueHost}\n */\n@Deprecated\npublic class', content)
 outfile = open('src/android/support/v17/leanback/app/VideoFragmentGlueHost.java', 'w')
 outfile.write(content)
 outfile.close()
@@ -82,7 +82,7 @@
     content = content + line
 file.close()
 # add deprecated tag to class
-# content = re.sub(r'\*\/\npublic class', '* @deprecated use {@link PlaybackSupportFragmentGlueHost}\n */\n@Deprecated\npublic class', content)
+content = re.sub(r'\*\/\npublic class', '* @deprecated use {@link PlaybackSupportFragmentGlueHost}\n */\n@Deprecated\npublic class', content)
 outfile = open('src/android/support/v17/leanback/app/PlaybackFragmentGlueHost.java', 'w')
 outfile.write(content)
 outfile.close()
@@ -102,7 +102,7 @@
     content = content + line
 file.close()
 # add deprecated tag to class
-# content = re.sub(r'\*\/\npublic class', '* @deprecated use {@link DetailsSupportFragmentBackgroundController}\n */\n@Deprecated\npublic class', content)
+content = re.sub(r'\*\/\npublic class', '* @deprecated use {@link DetailsSupportFragmentBackgroundController}\n */\n@Deprecated\npublic class', content)
 outfile = open('src/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java', 'w')
 outfile.write(content)
 outfile.close()
diff --git a/v17/leanback/jbmr2/android/support/v17/leanback/widget/ShadowHelperJbmr2.java b/leanback/jbmr2/android/support/v17/leanback/widget/ShadowHelperJbmr2.java
similarity index 100%
rename from v17/leanback/jbmr2/android/support/v17/leanback/widget/ShadowHelperJbmr2.java
rename to leanback/jbmr2/android/support/v17/leanback/widget/ShadowHelperJbmr2.java
diff --git a/v17/leanback/kitkat/android/support/v17/leanback/transition/LeanbackTransitionHelperKitKat.java b/leanback/kitkat/android/support/v17/leanback/transition/LeanbackTransitionHelperKitKat.java
similarity index 100%
rename from v17/leanback/kitkat/android/support/v17/leanback/transition/LeanbackTransitionHelperKitKat.java
rename to leanback/kitkat/android/support/v17/leanback/transition/LeanbackTransitionHelperKitKat.java
diff --git a/v17/leanback/kitkat/android/support/v17/leanback/transition/Scale.java b/leanback/kitkat/android/support/v17/leanback/transition/Scale.java
similarity index 100%
rename from v17/leanback/kitkat/android/support/v17/leanback/transition/Scale.java
rename to leanback/kitkat/android/support/v17/leanback/transition/Scale.java
diff --git a/v17/leanback/kitkat/android/support/v17/leanback/transition/SlideKitkat.java b/leanback/kitkat/android/support/v17/leanback/transition/SlideKitkat.java
similarity index 100%
rename from v17/leanback/kitkat/android/support/v17/leanback/transition/SlideKitkat.java
rename to leanback/kitkat/android/support/v17/leanback/transition/SlideKitkat.java
diff --git a/v17/leanback/kitkat/android/support/v17/leanback/transition/TransitionHelperKitkat.java b/leanback/kitkat/android/support/v17/leanback/transition/TransitionHelperKitkat.java
similarity index 100%
rename from v17/leanback/kitkat/android/support/v17/leanback/transition/TransitionHelperKitkat.java
rename to leanback/kitkat/android/support/v17/leanback/transition/TransitionHelperKitkat.java
diff --git a/v17/leanback/lint-baseline.xml b/leanback/lint-baseline.xml
similarity index 100%
rename from v17/leanback/lint-baseline.xml
rename to leanback/lint-baseline.xml
diff --git a/v17/leanback/res/anim/lb_decelerator_2.xml b/leanback/res/anim/lb_decelerator_2.xml
similarity index 100%
rename from v17/leanback/res/anim/lb_decelerator_2.xml
rename to leanback/res/anim/lb_decelerator_2.xml
diff --git a/v17/leanback/res/anim/lb_decelerator_4.xml b/leanback/res/anim/lb_decelerator_4.xml
similarity index 100%
rename from v17/leanback/res/anim/lb_decelerator_4.xml
rename to leanback/res/anim/lb_decelerator_4.xml
diff --git a/v17/leanback/res/animator-v21/lb_onboarding_description_enter.xml b/leanback/res/animator-v21/lb_onboarding_description_enter.xml
similarity index 100%
rename from v17/leanback/res/animator-v21/lb_onboarding_description_enter.xml
rename to leanback/res/animator-v21/lb_onboarding_description_enter.xml
diff --git a/v17/leanback/res/animator-v21/lb_onboarding_logo_enter.xml b/leanback/res/animator-v21/lb_onboarding_logo_enter.xml
similarity index 100%
rename from v17/leanback/res/animator-v21/lb_onboarding_logo_enter.xml
rename to leanback/res/animator-v21/lb_onboarding_logo_enter.xml
diff --git a/v17/leanback/res/animator-v21/lb_onboarding_logo_exit.xml b/leanback/res/animator-v21/lb_onboarding_logo_exit.xml
similarity index 100%
rename from v17/leanback/res/animator-v21/lb_onboarding_logo_exit.xml
rename to leanback/res/animator-v21/lb_onboarding_logo_exit.xml
diff --git a/v17/leanback/res/animator-v21/lb_onboarding_page_indicator_enter.xml b/leanback/res/animator-v21/lb_onboarding_page_indicator_enter.xml
similarity index 100%
rename from v17/leanback/res/animator-v21/lb_onboarding_page_indicator_enter.xml
rename to leanback/res/animator-v21/lb_onboarding_page_indicator_enter.xml
diff --git a/v17/leanback/res/animator-v21/lb_onboarding_title_enter.xml b/leanback/res/animator-v21/lb_onboarding_title_enter.xml
similarity index 100%
rename from v17/leanback/res/animator-v21/lb_onboarding_title_enter.xml
rename to leanback/res/animator-v21/lb_onboarding_title_enter.xml
diff --git a/v17/leanback/res/animator-v21/lb_playback_bg_fade_in.xml b/leanback/res/animator-v21/lb_playback_bg_fade_in.xml
similarity index 100%
rename from v17/leanback/res/animator-v21/lb_playback_bg_fade_in.xml
rename to leanback/res/animator-v21/lb_playback_bg_fade_in.xml
diff --git a/v17/leanback/res/animator-v21/lb_playback_bg_fade_out.xml b/leanback/res/animator-v21/lb_playback_bg_fade_out.xml
similarity index 100%
rename from v17/leanback/res/animator-v21/lb_playback_bg_fade_out.xml
rename to leanback/res/animator-v21/lb_playback_bg_fade_out.xml
diff --git a/v17/leanback/res/animator-v21/lb_playback_description_fade_out.xml b/leanback/res/animator-v21/lb_playback_description_fade_out.xml
similarity index 100%
rename from v17/leanback/res/animator-v21/lb_playback_description_fade_out.xml
rename to leanback/res/animator-v21/lb_playback_description_fade_out.xml
diff --git a/v17/leanback/res/animator/lb_guidedactions_item_pressed.xml b/leanback/res/animator/lb_guidedactions_item_pressed.xml
similarity index 100%
rename from v17/leanback/res/animator/lb_guidedactions_item_pressed.xml
rename to leanback/res/animator/lb_guidedactions_item_pressed.xml
diff --git a/v17/leanback/res/animator/lb_guidedactions_item_unpressed.xml b/leanback/res/animator/lb_guidedactions_item_unpressed.xml
similarity index 100%
rename from v17/leanback/res/animator/lb_guidedactions_item_unpressed.xml
rename to leanback/res/animator/lb_guidedactions_item_unpressed.xml
diff --git a/v17/leanback/res/animator/lb_guidedstep_slide_down.xml b/leanback/res/animator/lb_guidedstep_slide_down.xml
similarity index 100%
rename from v17/leanback/res/animator/lb_guidedstep_slide_down.xml
rename to leanback/res/animator/lb_guidedstep_slide_down.xml
diff --git a/v17/leanback/res/animator/lb_guidedstep_slide_up.xml b/leanback/res/animator/lb_guidedstep_slide_up.xml
similarity index 100%
rename from v17/leanback/res/animator/lb_guidedstep_slide_up.xml
rename to leanback/res/animator/lb_guidedstep_slide_up.xml
diff --git a/v17/leanback/res/animator/lb_onboarding_description_enter.xml b/leanback/res/animator/lb_onboarding_description_enter.xml
similarity index 100%
rename from v17/leanback/res/animator/lb_onboarding_description_enter.xml
rename to leanback/res/animator/lb_onboarding_description_enter.xml
diff --git a/v17/leanback/res/animator/lb_onboarding_logo_enter.xml b/leanback/res/animator/lb_onboarding_logo_enter.xml
similarity index 100%
rename from v17/leanback/res/animator/lb_onboarding_logo_enter.xml
rename to leanback/res/animator/lb_onboarding_logo_enter.xml
diff --git a/v17/leanback/res/animator/lb_onboarding_logo_exit.xml b/leanback/res/animator/lb_onboarding_logo_exit.xml
similarity index 100%
rename from v17/leanback/res/animator/lb_onboarding_logo_exit.xml
rename to leanback/res/animator/lb_onboarding_logo_exit.xml
diff --git a/v17/leanback/res/animator/lb_onboarding_page_indicator_enter.xml b/leanback/res/animator/lb_onboarding_page_indicator_enter.xml
similarity index 100%
rename from v17/leanback/res/animator/lb_onboarding_page_indicator_enter.xml
rename to leanback/res/animator/lb_onboarding_page_indicator_enter.xml
diff --git a/v17/leanback/res/animator/lb_onboarding_page_indicator_fade_in.xml b/leanback/res/animator/lb_onboarding_page_indicator_fade_in.xml
similarity index 100%
rename from v17/leanback/res/animator/lb_onboarding_page_indicator_fade_in.xml
rename to leanback/res/animator/lb_onboarding_page_indicator_fade_in.xml
diff --git a/v17/leanback/res/animator/lb_onboarding_page_indicator_fade_out.xml b/leanback/res/animator/lb_onboarding_page_indicator_fade_out.xml
similarity index 100%
rename from v17/leanback/res/animator/lb_onboarding_page_indicator_fade_out.xml
rename to leanback/res/animator/lb_onboarding_page_indicator_fade_out.xml
diff --git a/v17/leanback/res/animator/lb_onboarding_start_button_fade_in.xml b/leanback/res/animator/lb_onboarding_start_button_fade_in.xml
similarity index 100%
rename from v17/leanback/res/animator/lb_onboarding_start_button_fade_in.xml
rename to leanback/res/animator/lb_onboarding_start_button_fade_in.xml
diff --git a/v17/leanback/res/animator/lb_onboarding_start_button_fade_out.xml b/leanback/res/animator/lb_onboarding_start_button_fade_out.xml
similarity index 100%
rename from v17/leanback/res/animator/lb_onboarding_start_button_fade_out.xml
rename to leanback/res/animator/lb_onboarding_start_button_fade_out.xml
diff --git a/v17/leanback/res/animator/lb_onboarding_title_enter.xml b/leanback/res/animator/lb_onboarding_title_enter.xml
similarity index 100%
rename from v17/leanback/res/animator/lb_onboarding_title_enter.xml
rename to leanback/res/animator/lb_onboarding_title_enter.xml
diff --git a/v17/leanback/res/animator/lb_playback_bg_fade_in.xml b/leanback/res/animator/lb_playback_bg_fade_in.xml
similarity index 100%
rename from v17/leanback/res/animator/lb_playback_bg_fade_in.xml
rename to leanback/res/animator/lb_playback_bg_fade_in.xml
diff --git a/v17/leanback/res/animator/lb_playback_bg_fade_out.xml b/leanback/res/animator/lb_playback_bg_fade_out.xml
similarity index 100%
rename from v17/leanback/res/animator/lb_playback_bg_fade_out.xml
rename to leanback/res/animator/lb_playback_bg_fade_out.xml
diff --git a/v17/leanback/res/animator/lb_playback_controls_fade_in.xml b/leanback/res/animator/lb_playback_controls_fade_in.xml
similarity index 100%
rename from v17/leanback/res/animator/lb_playback_controls_fade_in.xml
rename to leanback/res/animator/lb_playback_controls_fade_in.xml
diff --git a/v17/leanback/res/animator/lb_playback_controls_fade_out.xml b/leanback/res/animator/lb_playback_controls_fade_out.xml
similarity index 100%
rename from v17/leanback/res/animator/lb_playback_controls_fade_out.xml
rename to leanback/res/animator/lb_playback_controls_fade_out.xml
diff --git a/v17/leanback/res/animator/lb_playback_description_fade_in.xml b/leanback/res/animator/lb_playback_description_fade_in.xml
similarity index 100%
rename from v17/leanback/res/animator/lb_playback_description_fade_in.xml
rename to leanback/res/animator/lb_playback_description_fade_in.xml
diff --git a/v17/leanback/res/animator/lb_playback_description_fade_out.xml b/leanback/res/animator/lb_playback_description_fade_out.xml
similarity index 100%
rename from v17/leanback/res/animator/lb_playback_description_fade_out.xml
rename to leanback/res/animator/lb_playback_description_fade_out.xml
diff --git a/v17/leanback/res/animator/lb_playback_rows_fade_in.xml b/leanback/res/animator/lb_playback_rows_fade_in.xml
similarity index 100%
rename from v17/leanback/res/animator/lb_playback_rows_fade_in.xml
rename to leanback/res/animator/lb_playback_rows_fade_in.xml
diff --git a/v17/leanback/res/animator/lb_playback_rows_fade_out.xml b/leanback/res/animator/lb_playback_rows_fade_out.xml
similarity index 100%
rename from v17/leanback/res/animator/lb_playback_rows_fade_out.xml
rename to leanback/res/animator/lb_playback_rows_fade_out.xml
diff --git a/v17/leanback/res/drawable-hdpi/lb_action_bg_focused.9.png b/leanback/res/drawable-hdpi/lb_action_bg_focused.9.png
similarity index 100%
rename from v17/leanback/res/drawable-hdpi/lb_action_bg_focused.9.png
rename to leanback/res/drawable-hdpi/lb_action_bg_focused.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-hdpi/lb_ic_actions_right_arrow.png b/leanback/res/drawable-hdpi/lb_ic_actions_right_arrow.png
similarity index 100%
rename from v17/leanback/res/drawable-hdpi/lb_ic_actions_right_arrow.png
rename to leanback/res/drawable-hdpi/lb_ic_actions_right_arrow.png
Binary files differ
diff --git a/v17/leanback/res/drawable-hdpi/lb_ic_in_app_search.png b/leanback/res/drawable-hdpi/lb_ic_in_app_search.png
similarity index 100%
rename from v17/leanback/res/drawable-hdpi/lb_ic_in_app_search.png
rename to leanback/res/drawable-hdpi/lb_ic_in_app_search.png
Binary files differ
diff --git a/v17/leanback/res/drawable-hdpi/lb_ic_sad_cloud.png b/leanback/res/drawable-hdpi/lb_ic_sad_cloud.png
similarity index 100%
rename from v17/leanback/res/drawable-hdpi/lb_ic_sad_cloud.png
rename to leanback/res/drawable-hdpi/lb_ic_sad_cloud.png
Binary files differ
diff --git a/v17/leanback/res/drawable-hdpi/lb_ic_search_mic.png b/leanback/res/drawable-hdpi/lb_ic_search_mic.png
similarity index 100%
rename from v17/leanback/res/drawable-hdpi/lb_ic_search_mic.png
rename to leanback/res/drawable-hdpi/lb_ic_search_mic.png
Binary files differ
diff --git a/v17/leanback/res/drawable-hdpi/lb_ic_search_mic_out.png b/leanback/res/drawable-hdpi/lb_ic_search_mic_out.png
similarity index 100%
rename from v17/leanback/res/drawable-hdpi/lb_ic_search_mic_out.png
rename to leanback/res/drawable-hdpi/lb_ic_search_mic_out.png
Binary files differ
diff --git a/v17/leanback/res/drawable-hdpi/lb_in_app_search_bg.9.png b/leanback/res/drawable-hdpi/lb_in_app_search_bg.9.png
similarity index 100%
rename from v17/leanback/res/drawable-hdpi/lb_in_app_search_bg.9.png
rename to leanback/res/drawable-hdpi/lb_in_app_search_bg.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-hdpi/lb_in_app_search_shadow_focused.9.png b/leanback/res/drawable-hdpi/lb_in_app_search_shadow_focused.9.png
similarity index 100%
rename from v17/leanback/res/drawable-hdpi/lb_in_app_search_shadow_focused.9.png
rename to leanback/res/drawable-hdpi/lb_in_app_search_shadow_focused.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-hdpi/lb_in_app_search_shadow_normal.9.png b/leanback/res/drawable-hdpi/lb_in_app_search_shadow_normal.9.png
similarity index 100%
rename from v17/leanback/res/drawable-hdpi/lb_in_app_search_shadow_normal.9.png
rename to leanback/res/drawable-hdpi/lb_in_app_search_shadow_normal.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-mdpi/lb_action_bg_focused.9.png b/leanback/res/drawable-mdpi/lb_action_bg_focused.9.png
similarity index 100%
rename from v17/leanback/res/drawable-mdpi/lb_action_bg_focused.9.png
rename to leanback/res/drawable-mdpi/lb_action_bg_focused.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-mdpi/lb_ic_actions_right_arrow.png b/leanback/res/drawable-mdpi/lb_ic_actions_right_arrow.png
similarity index 100%
rename from v17/leanback/res/drawable-mdpi/lb_ic_actions_right_arrow.png
rename to leanback/res/drawable-mdpi/lb_ic_actions_right_arrow.png
Binary files differ
diff --git a/v17/leanback/res/drawable-mdpi/lb_ic_in_app_search.png b/leanback/res/drawable-mdpi/lb_ic_in_app_search.png
similarity index 100%
rename from v17/leanback/res/drawable-mdpi/lb_ic_in_app_search.png
rename to leanback/res/drawable-mdpi/lb_ic_in_app_search.png
Binary files differ
diff --git a/v17/leanback/res/drawable-mdpi/lb_ic_sad_cloud.png b/leanback/res/drawable-mdpi/lb_ic_sad_cloud.png
similarity index 100%
rename from v17/leanback/res/drawable-mdpi/lb_ic_sad_cloud.png
rename to leanback/res/drawable-mdpi/lb_ic_sad_cloud.png
Binary files differ
diff --git a/v17/leanback/res/drawable-mdpi/lb_ic_search_mic.png b/leanback/res/drawable-mdpi/lb_ic_search_mic.png
similarity index 100%
rename from v17/leanback/res/drawable-mdpi/lb_ic_search_mic.png
rename to leanback/res/drawable-mdpi/lb_ic_search_mic.png
Binary files differ
diff --git a/v17/leanback/res/drawable-mdpi/lb_ic_search_mic_out.png b/leanback/res/drawable-mdpi/lb_ic_search_mic_out.png
similarity index 100%
rename from v17/leanback/res/drawable-mdpi/lb_ic_search_mic_out.png
rename to leanback/res/drawable-mdpi/lb_ic_search_mic_out.png
Binary files differ
diff --git a/v17/leanback/res/drawable-mdpi/lb_in_app_search_bg.9.png b/leanback/res/drawable-mdpi/lb_in_app_search_bg.9.png
similarity index 100%
rename from v17/leanback/res/drawable-mdpi/lb_in_app_search_bg.9.png
rename to leanback/res/drawable-mdpi/lb_in_app_search_bg.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-mdpi/lb_in_app_search_shadow_focused.9.png b/leanback/res/drawable-mdpi/lb_in_app_search_shadow_focused.9.png
similarity index 100%
rename from v17/leanback/res/drawable-mdpi/lb_in_app_search_shadow_focused.9.png
rename to leanback/res/drawable-mdpi/lb_in_app_search_shadow_focused.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-mdpi/lb_in_app_search_shadow_normal.9.png b/leanback/res/drawable-mdpi/lb_in_app_search_shadow_normal.9.png
similarity index 100%
rename from v17/leanback/res/drawable-mdpi/lb_in_app_search_shadow_normal.9.png
rename to leanback/res/drawable-mdpi/lb_in_app_search_shadow_normal.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-v21/lb_action_bg.xml b/leanback/res/drawable-v21/lb_action_bg.xml
similarity index 100%
rename from v17/leanback/res/drawable-v21/lb_action_bg.xml
rename to leanback/res/drawable-v21/lb_action_bg.xml
diff --git a/v17/leanback/res/drawable-v21/lb_card_foreground.xml b/leanback/res/drawable-v21/lb_card_foreground.xml
similarity index 100%
rename from v17/leanback/res/drawable-v21/lb_card_foreground.xml
rename to leanback/res/drawable-v21/lb_card_foreground.xml
diff --git a/v17/leanback/res/drawable-v21/lb_control_button_primary.xml b/leanback/res/drawable-v21/lb_control_button_primary.xml
similarity index 100%
rename from v17/leanback/res/drawable-v21/lb_control_button_primary.xml
rename to leanback/res/drawable-v21/lb_control_button_primary.xml
diff --git a/v17/leanback/res/drawable-v21/lb_control_button_secondary.xml b/leanback/res/drawable-v21/lb_control_button_secondary.xml
similarity index 100%
rename from v17/leanback/res/drawable-v21/lb_control_button_secondary.xml
rename to leanback/res/drawable-v21/lb_control_button_secondary.xml
diff --git a/v17/leanback/res/drawable-v21/lb_selectable_item_rounded_rect.xml b/leanback/res/drawable-v21/lb_selectable_item_rounded_rect.xml
similarity index 100%
rename from v17/leanback/res/drawable-v21/lb_selectable_item_rounded_rect.xml
rename to leanback/res/drawable-v21/lb_selectable_item_rounded_rect.xml
diff --git a/v17/leanback/res/drawable-xhdpi/lb_action_bg_focused.9.png b/leanback/res/drawable-xhdpi/lb_action_bg_focused.9.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_action_bg_focused.9.png
rename to leanback/res/drawable-xhdpi/lb_action_bg_focused.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_card_shadow_focused.9.png b/leanback/res/drawable-xhdpi/lb_card_shadow_focused.9.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_card_shadow_focused.9.png
rename to leanback/res/drawable-xhdpi/lb_card_shadow_focused.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_card_shadow_normal.9.png b/leanback/res/drawable-xhdpi/lb_card_shadow_normal.9.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_card_shadow_normal.9.png
rename to leanback/res/drawable-xhdpi/lb_card_shadow_normal.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_actions_right_arrow.png b/leanback/res/drawable-xhdpi/lb_ic_actions_right_arrow.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_actions_right_arrow.png
rename to leanback/res/drawable-xhdpi/lb_ic_actions_right_arrow.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_cc.png b/leanback/res/drawable-xhdpi/lb_ic_cc.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_cc.png
rename to leanback/res/drawable-xhdpi/lb_ic_cc.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_fast_forward.png b/leanback/res/drawable-xhdpi/lb_ic_fast_forward.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_fast_forward.png
rename to leanback/res/drawable-xhdpi/lb_ic_fast_forward.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_fast_rewind.png b/leanback/res/drawable-xhdpi/lb_ic_fast_rewind.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_fast_rewind.png
rename to leanback/res/drawable-xhdpi/lb_ic_fast_rewind.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_guidedactions_item_chevron.png b/leanback/res/drawable-xhdpi/lb_ic_guidedactions_item_chevron.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_guidedactions_item_chevron.png
rename to leanback/res/drawable-xhdpi/lb_ic_guidedactions_item_chevron.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_hq.png b/leanback/res/drawable-xhdpi/lb_ic_hq.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_hq.png
rename to leanback/res/drawable-xhdpi/lb_ic_hq.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_in_app_search.png b/leanback/res/drawable-xhdpi/lb_ic_in_app_search.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_in_app_search.png
rename to leanback/res/drawable-xhdpi/lb_ic_in_app_search.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_loop.png b/leanback/res/drawable-xhdpi/lb_ic_loop.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_loop.png
rename to leanback/res/drawable-xhdpi/lb_ic_loop.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_loop_one.png b/leanback/res/drawable-xhdpi/lb_ic_loop_one.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_loop_one.png
rename to leanback/res/drawable-xhdpi/lb_ic_loop_one.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_more.png b/leanback/res/drawable-xhdpi/lb_ic_more.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_more.png
rename to leanback/res/drawable-xhdpi/lb_ic_more.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_nav_arrow.png b/leanback/res/drawable-xhdpi/lb_ic_nav_arrow.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_nav_arrow.png
rename to leanback/res/drawable-xhdpi/lb_ic_nav_arrow.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_pause.png b/leanback/res/drawable-xhdpi/lb_ic_pause.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_pause.png
rename to leanback/res/drawable-xhdpi/lb_ic_pause.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_pip.png b/leanback/res/drawable-xhdpi/lb_ic_pip.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_pip.png
rename to leanback/res/drawable-xhdpi/lb_ic_pip.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_play.png b/leanback/res/drawable-xhdpi/lb_ic_play.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_play.png
rename to leanback/res/drawable-xhdpi/lb_ic_play.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_play_fit.png b/leanback/res/drawable-xhdpi/lb_ic_play_fit.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_play_fit.png
rename to leanback/res/drawable-xhdpi/lb_ic_play_fit.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_playback_loop.png b/leanback/res/drawable-xhdpi/lb_ic_playback_loop.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_playback_loop.png
rename to leanback/res/drawable-xhdpi/lb_ic_playback_loop.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_replay.png b/leanback/res/drawable-xhdpi/lb_ic_replay.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_replay.png
rename to leanback/res/drawable-xhdpi/lb_ic_replay.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_sad_cloud.png b/leanback/res/drawable-xhdpi/lb_ic_sad_cloud.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_sad_cloud.png
rename to leanback/res/drawable-xhdpi/lb_ic_sad_cloud.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_search_mic.png b/leanback/res/drawable-xhdpi/lb_ic_search_mic.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_search_mic.png
rename to leanback/res/drawable-xhdpi/lb_ic_search_mic.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_search_mic_out.png b/leanback/res/drawable-xhdpi/lb_ic_search_mic_out.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_search_mic_out.png
rename to leanback/res/drawable-xhdpi/lb_ic_search_mic_out.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_shuffle.png b/leanback/res/drawable-xhdpi/lb_ic_shuffle.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_shuffle.png
rename to leanback/res/drawable-xhdpi/lb_ic_shuffle.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_skip_next.png b/leanback/res/drawable-xhdpi/lb_ic_skip_next.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_skip_next.png
rename to leanback/res/drawable-xhdpi/lb_ic_skip_next.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_skip_previous.png b/leanback/res/drawable-xhdpi/lb_ic_skip_previous.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_skip_previous.png
rename to leanback/res/drawable-xhdpi/lb_ic_skip_previous.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_stop.png b/leanback/res/drawable-xhdpi/lb_ic_stop.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_stop.png
rename to leanback/res/drawable-xhdpi/lb_ic_stop.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_thumb_down.png b/leanback/res/drawable-xhdpi/lb_ic_thumb_down.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_thumb_down.png
rename to leanback/res/drawable-xhdpi/lb_ic_thumb_down.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_thumb_down_outline.png b/leanback/res/drawable-xhdpi/lb_ic_thumb_down_outline.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_thumb_down_outline.png
rename to leanback/res/drawable-xhdpi/lb_ic_thumb_down_outline.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_thumb_up.png b/leanback/res/drawable-xhdpi/lb_ic_thumb_up.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_thumb_up.png
rename to leanback/res/drawable-xhdpi/lb_ic_thumb_up.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_thumb_up_outline.png b/leanback/res/drawable-xhdpi/lb_ic_thumb_up_outline.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_ic_thumb_up_outline.png
rename to leanback/res/drawable-xhdpi/lb_ic_thumb_up_outline.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_in_app_search_bg.9.png b/leanback/res/drawable-xhdpi/lb_in_app_search_bg.9.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_in_app_search_bg.9.png
rename to leanback/res/drawable-xhdpi/lb_in_app_search_bg.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_in_app_search_shadow_focused.9.png b/leanback/res/drawable-xhdpi/lb_in_app_search_shadow_focused.9.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_in_app_search_shadow_focused.9.png
rename to leanback/res/drawable-xhdpi/lb_in_app_search_shadow_focused.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_in_app_search_shadow_normal.9.png b/leanback/res/drawable-xhdpi/lb_in_app_search_shadow_normal.9.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_in_app_search_shadow_normal.9.png
rename to leanback/res/drawable-xhdpi/lb_in_app_search_shadow_normal.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_text_dot_one.png b/leanback/res/drawable-xhdpi/lb_text_dot_one.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_text_dot_one.png
rename to leanback/res/drawable-xhdpi/lb_text_dot_one.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_text_dot_one_small.png b/leanback/res/drawable-xhdpi/lb_text_dot_one_small.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_text_dot_one_small.png
rename to leanback/res/drawable-xhdpi/lb_text_dot_one_small.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_text_dot_two.png b/leanback/res/drawable-xhdpi/lb_text_dot_two.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_text_dot_two.png
rename to leanback/res/drawable-xhdpi/lb_text_dot_two.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_text_dot_two_small.png b/leanback/res/drawable-xhdpi/lb_text_dot_two_small.png
similarity index 100%
rename from v17/leanback/res/drawable-xhdpi/lb_text_dot_two_small.png
rename to leanback/res/drawable-xhdpi/lb_text_dot_two_small.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xxhdpi/lb_action_bg_focused.9.png b/leanback/res/drawable-xxhdpi/lb_action_bg_focused.9.png
similarity index 100%
rename from v17/leanback/res/drawable-xxhdpi/lb_action_bg_focused.9.png
rename to leanback/res/drawable-xxhdpi/lb_action_bg_focused.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xxhdpi/lb_ic_actions_right_arrow.png b/leanback/res/drawable-xxhdpi/lb_ic_actions_right_arrow.png
similarity index 100%
rename from v17/leanback/res/drawable-xxhdpi/lb_ic_actions_right_arrow.png
rename to leanback/res/drawable-xxhdpi/lb_ic_actions_right_arrow.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xxhdpi/lb_ic_in_app_search.png b/leanback/res/drawable-xxhdpi/lb_ic_in_app_search.png
similarity index 100%
rename from v17/leanback/res/drawable-xxhdpi/lb_ic_in_app_search.png
rename to leanback/res/drawable-xxhdpi/lb_ic_in_app_search.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xxhdpi/lb_ic_sad_cloud.png b/leanback/res/drawable-xxhdpi/lb_ic_sad_cloud.png
similarity index 100%
rename from v17/leanback/res/drawable-xxhdpi/lb_ic_sad_cloud.png
rename to leanback/res/drawable-xxhdpi/lb_ic_sad_cloud.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xxhdpi/lb_ic_search_mic.png b/leanback/res/drawable-xxhdpi/lb_ic_search_mic.png
similarity index 100%
rename from v17/leanback/res/drawable-xxhdpi/lb_ic_search_mic.png
rename to leanback/res/drawable-xxhdpi/lb_ic_search_mic.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xxhdpi/lb_ic_search_mic_out.png b/leanback/res/drawable-xxhdpi/lb_ic_search_mic_out.png
similarity index 100%
rename from v17/leanback/res/drawable-xxhdpi/lb_ic_search_mic_out.png
rename to leanback/res/drawable-xxhdpi/lb_ic_search_mic_out.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xxhdpi/lb_in_app_search_bg.9.png b/leanback/res/drawable-xxhdpi/lb_in_app_search_bg.9.png
similarity index 100%
rename from v17/leanback/res/drawable-xxhdpi/lb_in_app_search_bg.9.png
rename to leanback/res/drawable-xxhdpi/lb_in_app_search_bg.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xxhdpi/lb_in_app_search_shadow_focused.9.png b/leanback/res/drawable-xxhdpi/lb_in_app_search_shadow_focused.9.png
similarity index 100%
rename from v17/leanback/res/drawable-xxhdpi/lb_in_app_search_shadow_focused.9.png
rename to leanback/res/drawable-xxhdpi/lb_in_app_search_shadow_focused.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xxhdpi/lb_in_app_search_shadow_normal.9.png b/leanback/res/drawable-xxhdpi/lb_in_app_search_shadow_normal.9.png
similarity index 100%
rename from v17/leanback/res/drawable-xxhdpi/lb_in_app_search_shadow_normal.9.png
rename to leanback/res/drawable-xxhdpi/lb_in_app_search_shadow_normal.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable/lb_background.xml b/leanback/res/drawable/lb_background.xml
similarity index 100%
rename from v17/leanback/res/drawable/lb_background.xml
rename to leanback/res/drawable/lb_background.xml
diff --git a/v17/leanback/res/drawable/lb_card_foreground.xml b/leanback/res/drawable/lb_card_foreground.xml
similarity index 100%
rename from v17/leanback/res/drawable/lb_card_foreground.xml
rename to leanback/res/drawable/lb_card_foreground.xml
diff --git a/v17/leanback/res/drawable/lb_control_button_primary.xml b/leanback/res/drawable/lb_control_button_primary.xml
similarity index 100%
rename from v17/leanback/res/drawable/lb_control_button_primary.xml
rename to leanback/res/drawable/lb_control_button_primary.xml
diff --git a/v17/leanback/res/drawable/lb_control_button_secondary.xml b/leanback/res/drawable/lb_control_button_secondary.xml
similarity index 100%
rename from v17/leanback/res/drawable/lb_control_button_secondary.xml
rename to leanback/res/drawable/lb_control_button_secondary.xml
diff --git a/v17/leanback/res/drawable/lb_headers_right_fading.xml b/leanback/res/drawable/lb_headers_right_fading.xml
similarity index 100%
rename from v17/leanback/res/drawable/lb_headers_right_fading.xml
rename to leanback/res/drawable/lb_headers_right_fading.xml
diff --git a/v17/leanback/res/drawable/lb_onboarding_start_button_background.xml b/leanback/res/drawable/lb_onboarding_start_button_background.xml
similarity index 100%
rename from v17/leanback/res/drawable/lb_onboarding_start_button_background.xml
rename to leanback/res/drawable/lb_onboarding_start_button_background.xml
diff --git a/v17/leanback/res/drawable/lb_playback_now_playing_bar.xml b/leanback/res/drawable/lb_playback_now_playing_bar.xml
similarity index 100%
rename from v17/leanback/res/drawable/lb_playback_now_playing_bar.xml
rename to leanback/res/drawable/lb_playback_now_playing_bar.xml
diff --git a/v17/leanback/res/drawable/lb_playback_progress_bar.xml b/leanback/res/drawable/lb_playback_progress_bar.xml
similarity index 100%
rename from v17/leanback/res/drawable/lb_playback_progress_bar.xml
rename to leanback/res/drawable/lb_playback_progress_bar.xml
diff --git a/v17/leanback/res/drawable/lb_search_orb.xml b/leanback/res/drawable/lb_search_orb.xml
similarity index 100%
rename from v17/leanback/res/drawable/lb_search_orb.xml
rename to leanback/res/drawable/lb_search_orb.xml
diff --git a/v17/leanback/res/drawable/lb_speech_orb.xml b/leanback/res/drawable/lb_speech_orb.xml
similarity index 100%
rename from v17/leanback/res/drawable/lb_speech_orb.xml
rename to leanback/res/drawable/lb_speech_orb.xml
diff --git a/v17/leanback/res/layout/lb_action_1_line.xml b/leanback/res/layout/lb_action_1_line.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_action_1_line.xml
rename to leanback/res/layout/lb_action_1_line.xml
diff --git a/v17/leanback/res/layout/lb_action_2_lines.xml b/leanback/res/layout/lb_action_2_lines.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_action_2_lines.xml
rename to leanback/res/layout/lb_action_2_lines.xml
diff --git a/v17/leanback/res/layout/lb_background_window.xml b/leanback/res/layout/lb_background_window.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_background_window.xml
rename to leanback/res/layout/lb_background_window.xml
diff --git a/v17/leanback/res/layout/lb_browse_fragment.xml b/leanback/res/layout/lb_browse_fragment.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_browse_fragment.xml
rename to leanback/res/layout/lb_browse_fragment.xml
diff --git a/v17/leanback/res/layout/lb_browse_title.xml b/leanback/res/layout/lb_browse_title.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_browse_title.xml
rename to leanback/res/layout/lb_browse_title.xml
diff --git a/v17/leanback/res/layout/lb_control_bar.xml b/leanback/res/layout/lb_control_bar.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_control_bar.xml
rename to leanback/res/layout/lb_control_bar.xml
diff --git a/v17/leanback/res/layout/lb_control_button_primary.xml b/leanback/res/layout/lb_control_button_primary.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_control_button_primary.xml
rename to leanback/res/layout/lb_control_button_primary.xml
diff --git a/v17/leanback/res/layout/lb_control_button_secondary.xml b/leanback/res/layout/lb_control_button_secondary.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_control_button_secondary.xml
rename to leanback/res/layout/lb_control_button_secondary.xml
diff --git a/v17/leanback/res/layout/lb_details_description.xml b/leanback/res/layout/lb_details_description.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_details_description.xml
rename to leanback/res/layout/lb_details_description.xml
diff --git a/v17/leanback/res/layout/lb_details_fragment.xml b/leanback/res/layout/lb_details_fragment.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_details_fragment.xml
rename to leanback/res/layout/lb_details_fragment.xml
diff --git a/v17/leanback/res/layout/lb_details_overview.xml b/leanback/res/layout/lb_details_overview.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_details_overview.xml
rename to leanback/res/layout/lb_details_overview.xml
diff --git a/v17/leanback/res/layout/lb_divider.xml b/leanback/res/layout/lb_divider.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_divider.xml
rename to leanback/res/layout/lb_divider.xml
diff --git a/v17/leanback/res/layout/lb_error_fragment.xml b/leanback/res/layout/lb_error_fragment.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_error_fragment.xml
rename to leanback/res/layout/lb_error_fragment.xml
diff --git a/v17/leanback/res/layout/lb_fullwidth_details_overview.xml b/leanback/res/layout/lb_fullwidth_details_overview.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_fullwidth_details_overview.xml
rename to leanback/res/layout/lb_fullwidth_details_overview.xml
diff --git a/v17/leanback/res/layout/lb_fullwidth_details_overview_logo.xml b/leanback/res/layout/lb_fullwidth_details_overview_logo.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_fullwidth_details_overview_logo.xml
rename to leanback/res/layout/lb_fullwidth_details_overview_logo.xml
diff --git a/v17/leanback/res/layout/lb_guidance.xml b/leanback/res/layout/lb_guidance.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_guidance.xml
rename to leanback/res/layout/lb_guidance.xml
diff --git a/v17/leanback/res/layout/lb_guidedactions.xml b/leanback/res/layout/lb_guidedactions.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_guidedactions.xml
rename to leanback/res/layout/lb_guidedactions.xml
diff --git a/v17/leanback/res/layout/lb_guidedactions_datepicker_item.xml b/leanback/res/layout/lb_guidedactions_datepicker_item.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_guidedactions_datepicker_item.xml
rename to leanback/res/layout/lb_guidedactions_datepicker_item.xml
diff --git a/v17/leanback/res/layout/lb_guidedactions_item.xml b/leanback/res/layout/lb_guidedactions_item.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_guidedactions_item.xml
rename to leanback/res/layout/lb_guidedactions_item.xml
diff --git a/v17/leanback/res/layout/lb_guidedbuttonactions.xml b/leanback/res/layout/lb_guidedbuttonactions.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_guidedbuttonactions.xml
rename to leanback/res/layout/lb_guidedbuttonactions.xml
diff --git a/v17/leanback/res/layout/lb_guidedstep_background.xml b/leanback/res/layout/lb_guidedstep_background.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_guidedstep_background.xml
rename to leanback/res/layout/lb_guidedstep_background.xml
diff --git a/v17/leanback/res/layout/lb_guidedstep_fragment.xml b/leanback/res/layout/lb_guidedstep_fragment.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_guidedstep_fragment.xml
rename to leanback/res/layout/lb_guidedstep_fragment.xml
diff --git a/v17/leanback/res/layout/lb_header.xml b/leanback/res/layout/lb_header.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_header.xml
rename to leanback/res/layout/lb_header.xml
diff --git a/v17/leanback/res/layout/lb_headers_fragment.xml b/leanback/res/layout/lb_headers_fragment.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_headers_fragment.xml
rename to leanback/res/layout/lb_headers_fragment.xml
diff --git a/v17/leanback/res/layout/lb_image_card_view.xml b/leanback/res/layout/lb_image_card_view.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_image_card_view.xml
rename to leanback/res/layout/lb_image_card_view.xml
diff --git a/v17/leanback/res/layout/lb_image_card_view_themed_badge_left.xml b/leanback/res/layout/lb_image_card_view_themed_badge_left.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_image_card_view_themed_badge_left.xml
rename to leanback/res/layout/lb_image_card_view_themed_badge_left.xml
diff --git a/v17/leanback/res/layout/lb_image_card_view_themed_badge_right.xml b/leanback/res/layout/lb_image_card_view_themed_badge_right.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_image_card_view_themed_badge_right.xml
rename to leanback/res/layout/lb_image_card_view_themed_badge_right.xml
diff --git a/v17/leanback/res/layout/lb_image_card_view_themed_content.xml b/leanback/res/layout/lb_image_card_view_themed_content.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_image_card_view_themed_content.xml
rename to leanback/res/layout/lb_image_card_view_themed_content.xml
diff --git a/v17/leanback/res/layout/lb_image_card_view_themed_title.xml b/leanback/res/layout/lb_image_card_view_themed_title.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_image_card_view_themed_title.xml
rename to leanback/res/layout/lb_image_card_view_themed_title.xml
diff --git a/v17/leanback/res/layout/lb_list_row.xml b/leanback/res/layout/lb_list_row.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_list_row.xml
rename to leanback/res/layout/lb_list_row.xml
diff --git a/v17/leanback/res/layout/lb_list_row_hovercard.xml b/leanback/res/layout/lb_list_row_hovercard.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_list_row_hovercard.xml
rename to leanback/res/layout/lb_list_row_hovercard.xml
diff --git a/v17/leanback/res/layout/lb_media_item_number_view_flipper.xml b/leanback/res/layout/lb_media_item_number_view_flipper.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_media_item_number_view_flipper.xml
rename to leanback/res/layout/lb_media_item_number_view_flipper.xml
diff --git a/v17/leanback/res/layout/lb_media_list_header.xml b/leanback/res/layout/lb_media_list_header.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_media_list_header.xml
rename to leanback/res/layout/lb_media_list_header.xml
diff --git a/v17/leanback/res/layout/lb_onboarding_fragment.xml b/leanback/res/layout/lb_onboarding_fragment.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_onboarding_fragment.xml
rename to leanback/res/layout/lb_onboarding_fragment.xml
diff --git a/v17/leanback/res/layout/lb_picker.xml b/leanback/res/layout/lb_picker.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_picker.xml
rename to leanback/res/layout/lb_picker.xml
diff --git a/v17/leanback/res/layout/lb_picker_column.xml b/leanback/res/layout/lb_picker_column.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_picker_column.xml
rename to leanback/res/layout/lb_picker_column.xml
diff --git a/v17/leanback/res/layout/lb_picker_item.xml b/leanback/res/layout/lb_picker_item.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_picker_item.xml
rename to leanback/res/layout/lb_picker_item.xml
diff --git a/v17/leanback/res/layout/lb_picker_separator.xml b/leanback/res/layout/lb_picker_separator.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_picker_separator.xml
rename to leanback/res/layout/lb_picker_separator.xml
diff --git a/v17/leanback/res/layout/lb_playback_controls.xml b/leanback/res/layout/lb_playback_controls.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_playback_controls.xml
rename to leanback/res/layout/lb_playback_controls.xml
diff --git a/v17/leanback/res/layout/lb_playback_controls_row.xml b/leanback/res/layout/lb_playback_controls_row.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_playback_controls_row.xml
rename to leanback/res/layout/lb_playback_controls_row.xml
diff --git a/v17/leanback/res/layout/lb_playback_fragment.xml b/leanback/res/layout/lb_playback_fragment.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_playback_fragment.xml
rename to leanback/res/layout/lb_playback_fragment.xml
diff --git a/v17/leanback/res/layout/lb_playback_now_playing_bars.xml b/leanback/res/layout/lb_playback_now_playing_bars.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_playback_now_playing_bars.xml
rename to leanback/res/layout/lb_playback_now_playing_bars.xml
diff --git a/v17/leanback/res/layout/lb_playback_transport_controls.xml b/leanback/res/layout/lb_playback_transport_controls.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_playback_transport_controls.xml
rename to leanback/res/layout/lb_playback_transport_controls.xml
diff --git a/v17/leanback/res/layout/lb_playback_transport_controls_row.xml b/leanback/res/layout/lb_playback_transport_controls_row.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_playback_transport_controls_row.xml
rename to leanback/res/layout/lb_playback_transport_controls_row.xml
diff --git a/v17/leanback/res/layout/lb_row_container.xml b/leanback/res/layout/lb_row_container.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_row_container.xml
rename to leanback/res/layout/lb_row_container.xml
diff --git a/v17/leanback/res/layout/lb_row_header.xml b/leanback/res/layout/lb_row_header.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_row_header.xml
rename to leanback/res/layout/lb_row_header.xml
diff --git a/v17/leanback/res/layout/lb_row_media_item.xml b/leanback/res/layout/lb_row_media_item.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_row_media_item.xml
rename to leanback/res/layout/lb_row_media_item.xml
diff --git a/v17/leanback/res/layout/lb_row_media_item_action.xml b/leanback/res/layout/lb_row_media_item_action.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_row_media_item_action.xml
rename to leanback/res/layout/lb_row_media_item_action.xml
diff --git a/v17/leanback/res/layout/lb_rows_fragment.xml b/leanback/res/layout/lb_rows_fragment.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_rows_fragment.xml
rename to leanback/res/layout/lb_rows_fragment.xml
diff --git a/v17/leanback/res/layout/lb_search_bar.xml b/leanback/res/layout/lb_search_bar.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_search_bar.xml
rename to leanback/res/layout/lb_search_bar.xml
diff --git a/v17/leanback/res/layout/lb_search_fragment.xml b/leanback/res/layout/lb_search_fragment.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_search_fragment.xml
rename to leanback/res/layout/lb_search_fragment.xml
diff --git a/v17/leanback/res/layout/lb_search_orb.xml b/leanback/res/layout/lb_search_orb.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_search_orb.xml
rename to leanback/res/layout/lb_search_orb.xml
diff --git a/v17/leanback/res/layout/lb_section_header.xml b/leanback/res/layout/lb_section_header.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_section_header.xml
rename to leanback/res/layout/lb_section_header.xml
diff --git a/v17/leanback/res/layout/lb_shadow.xml b/leanback/res/layout/lb_shadow.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_shadow.xml
rename to leanback/res/layout/lb_shadow.xml
diff --git a/v17/leanback/res/layout/lb_speech_orb.xml b/leanback/res/layout/lb_speech_orb.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_speech_orb.xml
rename to leanback/res/layout/lb_speech_orb.xml
diff --git a/v17/leanback/res/layout/lb_title_view.xml b/leanback/res/layout/lb_title_view.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_title_view.xml
rename to leanback/res/layout/lb_title_view.xml
diff --git a/v17/leanback/res/layout/lb_vertical_grid.xml b/leanback/res/layout/lb_vertical_grid.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_vertical_grid.xml
rename to leanback/res/layout/lb_vertical_grid.xml
diff --git a/v17/leanback/res/layout/lb_vertical_grid_fragment.xml b/leanback/res/layout/lb_vertical_grid_fragment.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_vertical_grid_fragment.xml
rename to leanback/res/layout/lb_vertical_grid_fragment.xml
diff --git a/v17/leanback/res/layout/lb_video_surface.xml b/leanback/res/layout/lb_video_surface.xml
similarity index 100%
rename from v17/leanback/res/layout/lb_video_surface.xml
rename to leanback/res/layout/lb_video_surface.xml
diff --git a/v17/leanback/res/layout/video_surface_fragment.xml b/leanback/res/layout/video_surface_fragment.xml
similarity index 100%
rename from v17/leanback/res/layout/video_surface_fragment.xml
rename to leanback/res/layout/video_surface_fragment.xml
diff --git a/v17/leanback/res/raw/lb_voice_failure.ogg b/leanback/res/raw/lb_voice_failure.ogg
similarity index 100%
rename from v17/leanback/res/raw/lb_voice_failure.ogg
rename to leanback/res/raw/lb_voice_failure.ogg
Binary files differ
diff --git a/v17/leanback/res/raw/lb_voice_no_input.ogg b/leanback/res/raw/lb_voice_no_input.ogg
similarity index 100%
rename from v17/leanback/res/raw/lb_voice_no_input.ogg
rename to leanback/res/raw/lb_voice_no_input.ogg
Binary files differ
diff --git a/v17/leanback/res/raw/lb_voice_open.ogg b/leanback/res/raw/lb_voice_open.ogg
similarity index 100%
rename from v17/leanback/res/raw/lb_voice_open.ogg
rename to leanback/res/raw/lb_voice_open.ogg
Binary files differ
diff --git a/v17/leanback/res/raw/lb_voice_success.ogg b/leanback/res/raw/lb_voice_success.ogg
similarity index 100%
rename from v17/leanback/res/raw/lb_voice_success.ogg
rename to leanback/res/raw/lb_voice_success.ogg
Binary files differ
diff --git a/v17/leanback/res/transition-v19/lb_browse_headers_in.xml b/leanback/res/transition-v19/lb_browse_headers_in.xml
similarity index 100%
rename from v17/leanback/res/transition-v19/lb_browse_headers_in.xml
rename to leanback/res/transition-v19/lb_browse_headers_in.xml
diff --git a/v17/leanback/res/transition-v19/lb_browse_headers_out.xml b/leanback/res/transition-v19/lb_browse_headers_out.xml
similarity index 100%
rename from v17/leanback/res/transition-v19/lb_browse_headers_out.xml
rename to leanback/res/transition-v19/lb_browse_headers_out.xml
diff --git a/v17/leanback/res/transition-v21/lb_browse_enter_transition.xml b/leanback/res/transition-v21/lb_browse_enter_transition.xml
similarity index 100%
rename from v17/leanback/res/transition-v21/lb_browse_enter_transition.xml
rename to leanback/res/transition-v21/lb_browse_enter_transition.xml
diff --git a/v17/leanback/res/transition-v21/lb_browse_entrance_transition.xml b/leanback/res/transition-v21/lb_browse_entrance_transition.xml
similarity index 100%
rename from v17/leanback/res/transition-v21/lb_browse_entrance_transition.xml
rename to leanback/res/transition-v21/lb_browse_entrance_transition.xml
diff --git a/v17/leanback/res/transition-v21/lb_browse_headers_in.xml b/leanback/res/transition-v21/lb_browse_headers_in.xml
similarity index 100%
rename from v17/leanback/res/transition-v21/lb_browse_headers_in.xml
rename to leanback/res/transition-v21/lb_browse_headers_in.xml
diff --git a/v17/leanback/res/transition-v21/lb_browse_headers_out.xml b/leanback/res/transition-v21/lb_browse_headers_out.xml
similarity index 100%
rename from v17/leanback/res/transition-v21/lb_browse_headers_out.xml
rename to leanback/res/transition-v21/lb_browse_headers_out.xml
diff --git a/v17/leanback/res/transition-v21/lb_browse_return_transition.xml b/leanback/res/transition-v21/lb_browse_return_transition.xml
similarity index 100%
rename from v17/leanback/res/transition-v21/lb_browse_return_transition.xml
rename to leanback/res/transition-v21/lb_browse_return_transition.xml
diff --git a/v17/leanback/res/transition-v21/lb_details_enter_transition.xml b/leanback/res/transition-v21/lb_details_enter_transition.xml
similarity index 100%
rename from v17/leanback/res/transition-v21/lb_details_enter_transition.xml
rename to leanback/res/transition-v21/lb_details_enter_transition.xml
diff --git a/v17/leanback/res/transition-v21/lb_details_return_transition.xml b/leanback/res/transition-v21/lb_details_return_transition.xml
similarity index 100%
rename from v17/leanback/res/transition-v21/lb_details_return_transition.xml
rename to leanback/res/transition-v21/lb_details_return_transition.xml
diff --git a/v17/leanback/res/transition-v21/lb_enter_transition.xml b/leanback/res/transition-v21/lb_enter_transition.xml
similarity index 100%
rename from v17/leanback/res/transition-v21/lb_enter_transition.xml
rename to leanback/res/transition-v21/lb_enter_transition.xml
diff --git a/v17/leanback/res/transition-v21/lb_guidedstep_activity_enter.xml b/leanback/res/transition-v21/lb_guidedstep_activity_enter.xml
similarity index 100%
rename from v17/leanback/res/transition-v21/lb_guidedstep_activity_enter.xml
rename to leanback/res/transition-v21/lb_guidedstep_activity_enter.xml
diff --git a/v17/leanback/res/transition-v21/lb_guidedstep_activity_enter_bottom.xml b/leanback/res/transition-v21/lb_guidedstep_activity_enter_bottom.xml
similarity index 100%
rename from v17/leanback/res/transition-v21/lb_guidedstep_activity_enter_bottom.xml
rename to leanback/res/transition-v21/lb_guidedstep_activity_enter_bottom.xml
diff --git a/v17/leanback/res/transition-v21/lb_return_transition.xml b/leanback/res/transition-v21/lb_return_transition.xml
similarity index 100%
rename from v17/leanback/res/transition-v21/lb_return_transition.xml
rename to leanback/res/transition-v21/lb_return_transition.xml
diff --git a/v17/leanback/res/transition-v21/lb_shared_element_enter_transition.xml b/leanback/res/transition-v21/lb_shared_element_enter_transition.xml
similarity index 100%
rename from v17/leanback/res/transition-v21/lb_shared_element_enter_transition.xml
rename to leanback/res/transition-v21/lb_shared_element_enter_transition.xml
diff --git a/v17/leanback/res/transition-v21/lb_shared_element_return_transition.xml b/leanback/res/transition-v21/lb_shared_element_return_transition.xml
similarity index 100%
rename from v17/leanback/res/transition-v21/lb_shared_element_return_transition.xml
rename to leanback/res/transition-v21/lb_shared_element_return_transition.xml
diff --git a/v17/leanback/res/transition-v21/lb_title_in.xml b/leanback/res/transition-v21/lb_title_in.xml
similarity index 100%
rename from v17/leanback/res/transition-v21/lb_title_in.xml
rename to leanback/res/transition-v21/lb_title_in.xml
diff --git a/v17/leanback/res/transition-v21/lb_title_out.xml b/leanback/res/transition-v21/lb_title_out.xml
similarity index 100%
rename from v17/leanback/res/transition-v21/lb_title_out.xml
rename to leanback/res/transition-v21/lb_title_out.xml
diff --git a/v17/leanback/res/transition-v21/lb_vertical_grid_enter_transition.xml b/leanback/res/transition-v21/lb_vertical_grid_enter_transition.xml
similarity index 100%
rename from v17/leanback/res/transition-v21/lb_vertical_grid_enter_transition.xml
rename to leanback/res/transition-v21/lb_vertical_grid_enter_transition.xml
diff --git a/v17/leanback/res/transition-v21/lb_vertical_grid_entrance_transition.xml b/leanback/res/transition-v21/lb_vertical_grid_entrance_transition.xml
similarity index 100%
rename from v17/leanback/res/transition-v21/lb_vertical_grid_entrance_transition.xml
rename to leanback/res/transition-v21/lb_vertical_grid_entrance_transition.xml
diff --git a/v17/leanback/res/transition-v21/lb_vertical_grid_return_transition.xml b/leanback/res/transition-v21/lb_vertical_grid_return_transition.xml
similarity index 100%
rename from v17/leanback/res/transition-v21/lb_vertical_grid_return_transition.xml
rename to leanback/res/transition-v21/lb_vertical_grid_return_transition.xml
diff --git a/v17/leanback/res/values-af/strings.xml b/leanback/res/values-af/strings.xml
similarity index 100%
rename from v17/leanback/res/values-af/strings.xml
rename to leanback/res/values-af/strings.xml
diff --git a/v17/leanback/res/values-am/strings.xml b/leanback/res/values-am/strings.xml
similarity index 100%
rename from v17/leanback/res/values-am/strings.xml
rename to leanback/res/values-am/strings.xml
diff --git a/v17/leanback/res/values-ar/strings.xml b/leanback/res/values-ar/strings.xml
similarity index 100%
rename from v17/leanback/res/values-ar/strings.xml
rename to leanback/res/values-ar/strings.xml
diff --git a/v17/leanback/res/values-az/strings.xml b/leanback/res/values-az/strings.xml
similarity index 100%
rename from v17/leanback/res/values-az/strings.xml
rename to leanback/res/values-az/strings.xml
diff --git a/v17/leanback/res/values-b+sr+Latn/strings.xml b/leanback/res/values-b+sr+Latn/strings.xml
similarity index 100%
rename from v17/leanback/res/values-b+sr+Latn/strings.xml
rename to leanback/res/values-b+sr+Latn/strings.xml
diff --git a/v17/leanback/res/values-be/strings.xml b/leanback/res/values-be/strings.xml
similarity index 100%
rename from v17/leanback/res/values-be/strings.xml
rename to leanback/res/values-be/strings.xml
diff --git a/v17/leanback/res/values-bg/strings.xml b/leanback/res/values-bg/strings.xml
similarity index 100%
rename from v17/leanback/res/values-bg/strings.xml
rename to leanback/res/values-bg/strings.xml
diff --git a/v17/leanback/res/values-bn/strings.xml b/leanback/res/values-bn/strings.xml
similarity index 100%
rename from v17/leanback/res/values-bn/strings.xml
rename to leanback/res/values-bn/strings.xml
diff --git a/v17/leanback/res/values-bs/strings.xml b/leanback/res/values-bs/strings.xml
similarity index 100%
rename from v17/leanback/res/values-bs/strings.xml
rename to leanback/res/values-bs/strings.xml
diff --git a/v17/leanback/res/values-ca/strings.xml b/leanback/res/values-ca/strings.xml
similarity index 100%
rename from v17/leanback/res/values-ca/strings.xml
rename to leanback/res/values-ca/strings.xml
diff --git a/v17/leanback/res/values-cs/strings.xml b/leanback/res/values-cs/strings.xml
similarity index 100%
rename from v17/leanback/res/values-cs/strings.xml
rename to leanback/res/values-cs/strings.xml
diff --git a/v17/leanback/res/values-da/strings.xml b/leanback/res/values-da/strings.xml
similarity index 100%
rename from v17/leanback/res/values-da/strings.xml
rename to leanback/res/values-da/strings.xml
diff --git a/v17/leanback/res/values-de/strings.xml b/leanback/res/values-de/strings.xml
similarity index 100%
rename from v17/leanback/res/values-de/strings.xml
rename to leanback/res/values-de/strings.xml
diff --git a/v17/leanback/res/values-el/strings.xml b/leanback/res/values-el/strings.xml
similarity index 100%
rename from v17/leanback/res/values-el/strings.xml
rename to leanback/res/values-el/strings.xml
diff --git a/v17/leanback/res/values-en-rAU/strings.xml b/leanback/res/values-en-rAU/strings.xml
similarity index 100%
rename from v17/leanback/res/values-en-rAU/strings.xml
rename to leanback/res/values-en-rAU/strings.xml
diff --git a/v17/leanback/res/values-en-rCA/strings.xml b/leanback/res/values-en-rCA/strings.xml
similarity index 100%
rename from v17/leanback/res/values-en-rCA/strings.xml
rename to leanback/res/values-en-rCA/strings.xml
diff --git a/v17/leanback/res/values-en-rGB/strings.xml b/leanback/res/values-en-rGB/strings.xml
similarity index 100%
rename from v17/leanback/res/values-en-rGB/strings.xml
rename to leanback/res/values-en-rGB/strings.xml
diff --git a/v17/leanback/res/values-en-rIN/strings.xml b/leanback/res/values-en-rIN/strings.xml
similarity index 100%
rename from v17/leanback/res/values-en-rIN/strings.xml
rename to leanback/res/values-en-rIN/strings.xml
diff --git a/v17/leanback/res/values-en-rXC/strings.xml b/leanback/res/values-en-rXC/strings.xml
similarity index 100%
rename from v17/leanback/res/values-en-rXC/strings.xml
rename to leanback/res/values-en-rXC/strings.xml
diff --git a/v17/leanback/res/values-es-rUS/strings.xml b/leanback/res/values-es-rUS/strings.xml
similarity index 100%
rename from v17/leanback/res/values-es-rUS/strings.xml
rename to leanback/res/values-es-rUS/strings.xml
diff --git a/v17/leanback/res/values-es/strings.xml b/leanback/res/values-es/strings.xml
similarity index 100%
rename from v17/leanback/res/values-es/strings.xml
rename to leanback/res/values-es/strings.xml
diff --git a/v17/leanback/res/values-et/strings.xml b/leanback/res/values-et/strings.xml
similarity index 100%
rename from v17/leanback/res/values-et/strings.xml
rename to leanback/res/values-et/strings.xml
diff --git a/v17/leanback/res/values-eu/strings.xml b/leanback/res/values-eu/strings.xml
similarity index 100%
rename from v17/leanback/res/values-eu/strings.xml
rename to leanback/res/values-eu/strings.xml
diff --git a/v17/leanback/res/values-fa/strings.xml b/leanback/res/values-fa/strings.xml
similarity index 100%
rename from v17/leanback/res/values-fa/strings.xml
rename to leanback/res/values-fa/strings.xml
diff --git a/v17/leanback/res/values-fi/strings.xml b/leanback/res/values-fi/strings.xml
similarity index 100%
rename from v17/leanback/res/values-fi/strings.xml
rename to leanback/res/values-fi/strings.xml
diff --git a/v17/leanback/res/values-fr-rCA/strings.xml b/leanback/res/values-fr-rCA/strings.xml
similarity index 100%
rename from v17/leanback/res/values-fr-rCA/strings.xml
rename to leanback/res/values-fr-rCA/strings.xml
diff --git a/v17/leanback/res/values-fr/strings.xml b/leanback/res/values-fr/strings.xml
similarity index 100%
rename from v17/leanback/res/values-fr/strings.xml
rename to leanback/res/values-fr/strings.xml
diff --git a/v17/leanback/res/values-gl/strings.xml b/leanback/res/values-gl/strings.xml
similarity index 100%
rename from v17/leanback/res/values-gl/strings.xml
rename to leanback/res/values-gl/strings.xml
diff --git a/v17/leanback/res/values-gu/strings.xml b/leanback/res/values-gu/strings.xml
similarity index 100%
rename from v17/leanback/res/values-gu/strings.xml
rename to leanback/res/values-gu/strings.xml
diff --git a/v17/leanback/res/values-hi/strings.xml b/leanback/res/values-hi/strings.xml
similarity index 100%
rename from v17/leanback/res/values-hi/strings.xml
rename to leanback/res/values-hi/strings.xml
diff --git a/v17/leanback/res/values-hr/strings.xml b/leanback/res/values-hr/strings.xml
similarity index 100%
rename from v17/leanback/res/values-hr/strings.xml
rename to leanback/res/values-hr/strings.xml
diff --git a/v17/leanback/res/values-hu/strings.xml b/leanback/res/values-hu/strings.xml
similarity index 100%
rename from v17/leanback/res/values-hu/strings.xml
rename to leanback/res/values-hu/strings.xml
diff --git a/v17/leanback/res/values-hy/strings.xml b/leanback/res/values-hy/strings.xml
similarity index 100%
rename from v17/leanback/res/values-hy/strings.xml
rename to leanback/res/values-hy/strings.xml
diff --git a/v17/leanback/res/values-in/strings.xml b/leanback/res/values-in/strings.xml
similarity index 100%
rename from v17/leanback/res/values-in/strings.xml
rename to leanback/res/values-in/strings.xml
diff --git a/v17/leanback/res/values-is/strings.xml b/leanback/res/values-is/strings.xml
similarity index 100%
rename from v17/leanback/res/values-is/strings.xml
rename to leanback/res/values-is/strings.xml
diff --git a/v17/leanback/res/values-it/strings.xml b/leanback/res/values-it/strings.xml
similarity index 100%
rename from v17/leanback/res/values-it/strings.xml
rename to leanback/res/values-it/strings.xml
diff --git a/v17/leanback/res/values-iw/strings.xml b/leanback/res/values-iw/strings.xml
similarity index 100%
rename from v17/leanback/res/values-iw/strings.xml
rename to leanback/res/values-iw/strings.xml
diff --git a/v17/leanback/res/values-ja/strings.xml b/leanback/res/values-ja/strings.xml
similarity index 100%
rename from v17/leanback/res/values-ja/strings.xml
rename to leanback/res/values-ja/strings.xml
diff --git a/v17/leanback/res/values-ka/strings.xml b/leanback/res/values-ka/strings.xml
similarity index 100%
rename from v17/leanback/res/values-ka/strings.xml
rename to leanback/res/values-ka/strings.xml
diff --git a/v17/leanback/res/values-kk/strings.xml b/leanback/res/values-kk/strings.xml
similarity index 100%
rename from v17/leanback/res/values-kk/strings.xml
rename to leanback/res/values-kk/strings.xml
diff --git a/v17/leanback/res/values-km/strings.xml b/leanback/res/values-km/strings.xml
similarity index 100%
rename from v17/leanback/res/values-km/strings.xml
rename to leanback/res/values-km/strings.xml
diff --git a/v17/leanback/res/values-kn/strings.xml b/leanback/res/values-kn/strings.xml
similarity index 100%
rename from v17/leanback/res/values-kn/strings.xml
rename to leanback/res/values-kn/strings.xml
diff --git a/v17/leanback/res/values-ko/strings.xml b/leanback/res/values-ko/strings.xml
similarity index 100%
rename from v17/leanback/res/values-ko/strings.xml
rename to leanback/res/values-ko/strings.xml
diff --git a/v17/leanback/res/values-ky/strings.xml b/leanback/res/values-ky/strings.xml
similarity index 100%
rename from v17/leanback/res/values-ky/strings.xml
rename to leanback/res/values-ky/strings.xml
diff --git a/v17/leanback/res/values-ldrtl/dimens.xml b/leanback/res/values-ldrtl/dimens.xml
similarity index 100%
rename from v17/leanback/res/values-ldrtl/dimens.xml
rename to leanback/res/values-ldrtl/dimens.xml
diff --git a/v17/leanback/res/values-ldrtl/integers.xml b/leanback/res/values-ldrtl/integers.xml
similarity index 100%
rename from v17/leanback/res/values-ldrtl/integers.xml
rename to leanback/res/values-ldrtl/integers.xml
diff --git a/v17/leanback/res/values-lo/strings.xml b/leanback/res/values-lo/strings.xml
similarity index 100%
rename from v17/leanback/res/values-lo/strings.xml
rename to leanback/res/values-lo/strings.xml
diff --git a/v17/leanback/res/values-lt/strings.xml b/leanback/res/values-lt/strings.xml
similarity index 100%
rename from v17/leanback/res/values-lt/strings.xml
rename to leanback/res/values-lt/strings.xml
diff --git a/v17/leanback/res/values-lv/strings.xml b/leanback/res/values-lv/strings.xml
similarity index 100%
rename from v17/leanback/res/values-lv/strings.xml
rename to leanback/res/values-lv/strings.xml
diff --git a/v17/leanback/res/values-mk/strings.xml b/leanback/res/values-mk/strings.xml
similarity index 100%
rename from v17/leanback/res/values-mk/strings.xml
rename to leanback/res/values-mk/strings.xml
diff --git a/v17/leanback/res/values-ml/strings.xml b/leanback/res/values-ml/strings.xml
similarity index 100%
rename from v17/leanback/res/values-ml/strings.xml
rename to leanback/res/values-ml/strings.xml
diff --git a/v17/leanback/res/values-mn/strings.xml b/leanback/res/values-mn/strings.xml
similarity index 100%
rename from v17/leanback/res/values-mn/strings.xml
rename to leanback/res/values-mn/strings.xml
diff --git a/v17/leanback/res/values-mr/strings.xml b/leanback/res/values-mr/strings.xml
similarity index 100%
rename from v17/leanback/res/values-mr/strings.xml
rename to leanback/res/values-mr/strings.xml
diff --git a/v17/leanback/res/values-ms/strings.xml b/leanback/res/values-ms/strings.xml
similarity index 100%
rename from v17/leanback/res/values-ms/strings.xml
rename to leanback/res/values-ms/strings.xml
diff --git a/v17/leanback/res/values-my/strings.xml b/leanback/res/values-my/strings.xml
similarity index 100%
rename from v17/leanback/res/values-my/strings.xml
rename to leanback/res/values-my/strings.xml
diff --git a/v17/leanback/res/values-nb/strings.xml b/leanback/res/values-nb/strings.xml
similarity index 100%
rename from v17/leanback/res/values-nb/strings.xml
rename to leanback/res/values-nb/strings.xml
diff --git a/v17/leanback/res/values-ne/strings.xml b/leanback/res/values-ne/strings.xml
similarity index 100%
rename from v17/leanback/res/values-ne/strings.xml
rename to leanback/res/values-ne/strings.xml
diff --git a/v17/leanback/res/values-nl/strings.xml b/leanback/res/values-nl/strings.xml
similarity index 100%
rename from v17/leanback/res/values-nl/strings.xml
rename to leanback/res/values-nl/strings.xml
diff --git a/v17/leanback/res/values-pa/strings.xml b/leanback/res/values-pa/strings.xml
similarity index 100%
rename from v17/leanback/res/values-pa/strings.xml
rename to leanback/res/values-pa/strings.xml
diff --git a/v17/leanback/res/values-pl/strings.xml b/leanback/res/values-pl/strings.xml
similarity index 100%
rename from v17/leanback/res/values-pl/strings.xml
rename to leanback/res/values-pl/strings.xml
diff --git a/v17/leanback/res/values-pt-rBR/strings.xml b/leanback/res/values-pt-rBR/strings.xml
similarity index 100%
rename from v17/leanback/res/values-pt-rBR/strings.xml
rename to leanback/res/values-pt-rBR/strings.xml
diff --git a/v17/leanback/res/values-pt-rPT/strings.xml b/leanback/res/values-pt-rPT/strings.xml
similarity index 100%
rename from v17/leanback/res/values-pt-rPT/strings.xml
rename to leanback/res/values-pt-rPT/strings.xml
diff --git a/v17/leanback/res/values-pt/strings.xml b/leanback/res/values-pt/strings.xml
similarity index 100%
rename from v17/leanback/res/values-pt/strings.xml
rename to leanback/res/values-pt/strings.xml
diff --git a/v17/leanback/res/values-ro/strings.xml b/leanback/res/values-ro/strings.xml
similarity index 100%
rename from v17/leanback/res/values-ro/strings.xml
rename to leanback/res/values-ro/strings.xml
diff --git a/v17/leanback/res/values-ru/strings.xml b/leanback/res/values-ru/strings.xml
similarity index 100%
rename from v17/leanback/res/values-ru/strings.xml
rename to leanback/res/values-ru/strings.xml
diff --git a/v17/leanback/res/values-si/strings.xml b/leanback/res/values-si/strings.xml
similarity index 100%
rename from v17/leanback/res/values-si/strings.xml
rename to leanback/res/values-si/strings.xml
diff --git a/v17/leanback/res/values-sk/strings.xml b/leanback/res/values-sk/strings.xml
similarity index 100%
rename from v17/leanback/res/values-sk/strings.xml
rename to leanback/res/values-sk/strings.xml
diff --git a/v17/leanback/res/values-sl/strings.xml b/leanback/res/values-sl/strings.xml
similarity index 100%
rename from v17/leanback/res/values-sl/strings.xml
rename to leanback/res/values-sl/strings.xml
diff --git a/v17/leanback/res/values-sq/strings.xml b/leanback/res/values-sq/strings.xml
similarity index 100%
rename from v17/leanback/res/values-sq/strings.xml
rename to leanback/res/values-sq/strings.xml
diff --git a/v17/leanback/res/values-sr/strings.xml b/leanback/res/values-sr/strings.xml
similarity index 100%
rename from v17/leanback/res/values-sr/strings.xml
rename to leanback/res/values-sr/strings.xml
diff --git a/v17/leanback/res/values-sv/strings.xml b/leanback/res/values-sv/strings.xml
similarity index 100%
rename from v17/leanback/res/values-sv/strings.xml
rename to leanback/res/values-sv/strings.xml
diff --git a/v17/leanback/res/values-sw/strings.xml b/leanback/res/values-sw/strings.xml
similarity index 100%
rename from v17/leanback/res/values-sw/strings.xml
rename to leanback/res/values-sw/strings.xml
diff --git a/v17/leanback/res/values-ta/strings.xml b/leanback/res/values-ta/strings.xml
similarity index 100%
rename from v17/leanback/res/values-ta/strings.xml
rename to leanback/res/values-ta/strings.xml
diff --git a/v17/leanback/res/values-te/strings.xml b/leanback/res/values-te/strings.xml
similarity index 100%
rename from v17/leanback/res/values-te/strings.xml
rename to leanback/res/values-te/strings.xml
diff --git a/v17/leanback/res/values-th/strings.xml b/leanback/res/values-th/strings.xml
similarity index 100%
rename from v17/leanback/res/values-th/strings.xml
rename to leanback/res/values-th/strings.xml
diff --git a/v17/leanback/res/values-tl/strings.xml b/leanback/res/values-tl/strings.xml
similarity index 100%
rename from v17/leanback/res/values-tl/strings.xml
rename to leanback/res/values-tl/strings.xml
diff --git a/v17/leanback/res/values-tr/strings.xml b/leanback/res/values-tr/strings.xml
similarity index 100%
rename from v17/leanback/res/values-tr/strings.xml
rename to leanback/res/values-tr/strings.xml
diff --git a/v17/leanback/res/values-uk/strings.xml b/leanback/res/values-uk/strings.xml
similarity index 100%
rename from v17/leanback/res/values-uk/strings.xml
rename to leanback/res/values-uk/strings.xml
diff --git a/v17/leanback/res/values-ur/strings.xml b/leanback/res/values-ur/strings.xml
similarity index 100%
rename from v17/leanback/res/values-ur/strings.xml
rename to leanback/res/values-ur/strings.xml
diff --git a/v17/leanback/res/values-uz/strings.xml b/leanback/res/values-uz/strings.xml
similarity index 100%
rename from v17/leanback/res/values-uz/strings.xml
rename to leanback/res/values-uz/strings.xml
diff --git a/v17/leanback/res/values-v18/themes.xml b/leanback/res/values-v18/themes.xml
similarity index 100%
rename from v17/leanback/res/values-v18/themes.xml
rename to leanback/res/values-v18/themes.xml
diff --git a/v17/leanback/res/values-v19/themes.xml b/leanback/res/values-v19/themes.xml
similarity index 100%
rename from v17/leanback/res/values-v19/themes.xml
rename to leanback/res/values-v19/themes.xml
diff --git a/v17/leanback/res/values-v21/styles.xml b/leanback/res/values-v21/styles.xml
similarity index 100%
rename from v17/leanback/res/values-v21/styles.xml
rename to leanback/res/values-v21/styles.xml
diff --git a/v17/leanback/res/values-v21/themes.xml b/leanback/res/values-v21/themes.xml
similarity index 100%
rename from v17/leanback/res/values-v21/themes.xml
rename to leanback/res/values-v21/themes.xml
diff --git a/v17/leanback/res/values-v22/integers.xml b/leanback/res/values-v22/integers.xml
similarity index 100%
rename from v17/leanback/res/values-v22/integers.xml
rename to leanback/res/values-v22/integers.xml
diff --git a/v17/leanback/res/values-vi/strings.xml b/leanback/res/values-vi/strings.xml
similarity index 100%
rename from v17/leanback/res/values-vi/strings.xml
rename to leanback/res/values-vi/strings.xml
diff --git a/v17/leanback/res/values-zh-rCN/strings.xml b/leanback/res/values-zh-rCN/strings.xml
similarity index 100%
rename from v17/leanback/res/values-zh-rCN/strings.xml
rename to leanback/res/values-zh-rCN/strings.xml
diff --git a/v17/leanback/res/values-zh-rHK/strings.xml b/leanback/res/values-zh-rHK/strings.xml
similarity index 100%
rename from v17/leanback/res/values-zh-rHK/strings.xml
rename to leanback/res/values-zh-rHK/strings.xml
diff --git a/v17/leanback/res/values-zh-rTW/strings.xml b/leanback/res/values-zh-rTW/strings.xml
similarity index 100%
rename from v17/leanback/res/values-zh-rTW/strings.xml
rename to leanback/res/values-zh-rTW/strings.xml
diff --git a/v17/leanback/res/values-zu/strings.xml b/leanback/res/values-zu/strings.xml
similarity index 100%
rename from v17/leanback/res/values-zu/strings.xml
rename to leanback/res/values-zu/strings.xml
diff --git a/v17/leanback/res/values/attrs.xml b/leanback/res/values/attrs.xml
similarity index 100%
rename from v17/leanback/res/values/attrs.xml
rename to leanback/res/values/attrs.xml
diff --git a/v17/leanback/res/values/colors.xml b/leanback/res/values/colors.xml
similarity index 100%
rename from v17/leanback/res/values/colors.xml
rename to leanback/res/values/colors.xml
diff --git a/v17/leanback/res/values/dimens.xml b/leanback/res/values/dimens.xml
similarity index 100%
rename from v17/leanback/res/values/dimens.xml
rename to leanback/res/values/dimens.xml
diff --git a/v17/leanback/res/values/ids.xml b/leanback/res/values/ids.xml
similarity index 100%
rename from v17/leanback/res/values/ids.xml
rename to leanback/res/values/ids.xml
diff --git a/v17/leanback/res/values/integers.xml b/leanback/res/values/integers.xml
similarity index 100%
rename from v17/leanback/res/values/integers.xml
rename to leanback/res/values/integers.xml
diff --git a/v17/leanback/res/values/strings.xml b/leanback/res/values/strings.xml
similarity index 100%
rename from v17/leanback/res/values/strings.xml
rename to leanback/res/values/strings.xml
diff --git a/v17/leanback/res/values/styles.xml b/leanback/res/values/styles.xml
similarity index 100%
rename from v17/leanback/res/values/styles.xml
rename to leanback/res/values/styles.xml
diff --git a/v17/leanback/res/values/themes.xml b/leanback/res/values/themes.xml
similarity index 100%
rename from v17/leanback/res/values/themes.xml
rename to leanback/res/values/themes.xml
diff --git a/v17/leanback/src/android/support/v17/leanback/animation/LogAccelerateInterpolator.java b/leanback/src/android/support/v17/leanback/animation/LogAccelerateInterpolator.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/animation/LogAccelerateInterpolator.java
rename to leanback/src/android/support/v17/leanback/animation/LogAccelerateInterpolator.java
diff --git a/v17/leanback/src/android/support/v17/leanback/animation/LogDecelerateInterpolator.java b/leanback/src/android/support/v17/leanback/animation/LogDecelerateInterpolator.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/animation/LogDecelerateInterpolator.java
rename to leanback/src/android/support/v17/leanback/animation/LogDecelerateInterpolator.java
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BackgroundFragment.java b/leanback/src/android/support/v17/leanback/app/BackgroundFragment.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/app/BackgroundFragment.java
rename to leanback/src/android/support/v17/leanback/app/BackgroundFragment.java
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BackgroundManager.java b/leanback/src/android/support/v17/leanback/app/BackgroundManager.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/app/BackgroundManager.java
rename to leanback/src/android/support/v17/leanback/app/BackgroundManager.java
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BaseFragment.java b/leanback/src/android/support/v17/leanback/app/BaseFragment.java
similarity index 99%
rename from v17/leanback/src/android/support/v17/leanback/app/BaseFragment.java
rename to leanback/src/android/support/v17/leanback/app/BaseFragment.java
index bdb213f..ea46011 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/BaseFragment.java
+++ b/leanback/src/android/support/v17/leanback/app/BaseFragment.java
@@ -31,7 +31,9 @@
 
 /**
  * Base class for leanback Fragments. This class is not intended to be subclassed by apps.
+ * @deprecated use {@link BaseSupportFragment}
  */
+@Deprecated
 @SuppressWarnings("FragmentNotInstantiable")
 public class BaseFragment extends BrandedFragment {
 
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BaseRowFragment.java b/leanback/src/android/support/v17/leanback/app/BaseRowFragment.java
similarity index 96%
rename from v17/leanback/src/android/support/v17/leanback/app/BaseRowFragment.java
rename to leanback/src/android/support/v17/leanback/app/BaseRowFragment.java
index 2d79f3e..97a5b84 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/BaseRowFragment.java
+++ b/leanback/src/android/support/v17/leanback/app/BaseRowFragment.java
@@ -34,7 +34,9 @@
 
 /**
  * An internal base class for a fragment containing a list of rows.
+ * @deprecated use {@link BaseRowSupportFragment}
  */
+@Deprecated
 abstract class BaseRowFragment extends Fragment {
     private static final String CURRENT_SELECTED_POSITION = "currentSelectedPosition";
     private ObjectAdapter mAdapter;
@@ -164,8 +166,10 @@
      * Set the presenter selector used to create and bind views.
      */
     public final void setPresenterSelector(PresenterSelector presenterSelector) {
-        mPresenterSelector = presenterSelector;
-        updateAdapter();
+        if (mPresenterSelector != presenterSelector) {
+            mPresenterSelector = presenterSelector;
+            updateAdapter();
+        }
     }
 
     /**
@@ -180,8 +184,10 @@
      * @param rowsAdapter Adapter that represents list of rows.
      */
     public final void setAdapter(ObjectAdapter rowsAdapter) {
-        mAdapter = rowsAdapter;
-        updateAdapter();
+        if (mAdapter != rowsAdapter) {
+            mAdapter = rowsAdapter;
+            updateAdapter();
+        }
     }
 
     /**
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BaseRowSupportFragment.java b/leanback/src/android/support/v17/leanback/app/BaseRowSupportFragment.java
similarity index 97%
rename from v17/leanback/src/android/support/v17/leanback/app/BaseRowSupportFragment.java
rename to leanback/src/android/support/v17/leanback/app/BaseRowSupportFragment.java
index dba78da..6a477ab 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/BaseRowSupportFragment.java
+++ b/leanback/src/android/support/v17/leanback/app/BaseRowSupportFragment.java
@@ -161,8 +161,10 @@
      * Set the presenter selector used to create and bind views.
      */
     public final void setPresenterSelector(PresenterSelector presenterSelector) {
-        mPresenterSelector = presenterSelector;
-        updateAdapter();
+        if (mPresenterSelector != presenterSelector) {
+            mPresenterSelector = presenterSelector;
+            updateAdapter();
+        }
     }
 
     /**
@@ -177,8 +179,10 @@
      * @param rowsAdapter Adapter that represents list of rows.
      */
     public final void setAdapter(ObjectAdapter rowsAdapter) {
-        mAdapter = rowsAdapter;
-        updateAdapter();
+        if (mAdapter != rowsAdapter) {
+            mAdapter = rowsAdapter;
+            updateAdapter();
+        }
     }
 
     /**
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BaseSupportFragment.java b/leanback/src/android/support/v17/leanback/app/BaseSupportFragment.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/app/BaseSupportFragment.java
rename to leanback/src/android/support/v17/leanback/app/BaseSupportFragment.java
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BrandedFragment.java b/leanback/src/android/support/v17/leanback/app/BrandedFragment.java
similarity index 99%
rename from v17/leanback/src/android/support/v17/leanback/app/BrandedFragment.java
rename to leanback/src/android/support/v17/leanback/app/BrandedFragment.java
index 1f6ad29..415c13e 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/BrandedFragment.java
+++ b/leanback/src/android/support/v17/leanback/app/BrandedFragment.java
@@ -33,7 +33,9 @@
 /**
  * Fragment class for managing search and branding using a view that implements
  * {@link TitleViewAdapter.Provider}.
+ * @deprecated use {@link BrandedSupportFragment}
  */
+@Deprecated
 public class BrandedFragment extends Fragment {
 
     // BUNDLE attribute for title is showing
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BrandedSupportFragment.java b/leanback/src/android/support/v17/leanback/app/BrandedSupportFragment.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/app/BrandedSupportFragment.java
rename to leanback/src/android/support/v17/leanback/app/BrandedSupportFragment.java
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BrowseFragment.java b/leanback/src/android/support/v17/leanback/app/BrowseFragment.java
similarity index 93%
rename from v17/leanback/src/android/support/v17/leanback/app/BrowseFragment.java
rename to leanback/src/android/support/v17/leanback/app/BrowseFragment.java
index f377389..c561ea9 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/BrowseFragment.java
+++ b/leanback/src/android/support/v17/leanback/app/BrowseFragment.java
@@ -81,7 +81,9 @@
  * The recommended theme to use with a BrowseFragment is
  * {@link android.support.v17.leanback.R.style#Theme_Leanback_Browse}.
  * </p>
+ * @deprecated use {@link BrowseSupportFragment}
  */
+@Deprecated
 public class BrowseFragment extends BaseFragment {
 
     // BUNDLE attribute for saving header show/hide status when backstack is used:
@@ -203,7 +205,9 @@
 
     /**
      * Listener for transitions between browse headers and rows.
+     * @deprecated use {@link BrowseSupportFragment}
      */
+    @Deprecated
     public static class BrowseTransitionListener {
         /**
          * Callback when headers transition starts.
@@ -267,7 +271,9 @@
     /**
      * Possible set of actions that {@link BrowseFragment} exposes to clients. Custom
      * fragments can interact with {@link BrowseFragment} using this interface.
+     * @deprecated use {@link BrowseSupportFragment}
      */
+    @Deprecated
     public interface FragmentHost {
         /**
          * Fragments are required to invoke this callback once their view is created
@@ -376,7 +382,9 @@
      * and provide that through {@link MainFragmentAdapterRegistry}.
      * {@link MainFragmentAdapter} implementation can supply any fragment and override
      * just those interactions that makes sense.
+     * @deprecated use {@link BrowseSupportFragment}
      */
+    @Deprecated
     public static class MainFragmentAdapter<T extends Fragment> {
         private boolean mScalingEnabled;
         private final T mFragment;
@@ -466,7 +474,9 @@
      * Interface to be implemented by all fragments for providing an instance of
      * {@link MainFragmentAdapter}. Both {@link RowsFragment} and custom fragment provided
      * against {@link PageRow} will need to implement this interface.
+     * @deprecated use {@link BrowseSupportFragment}
      */
+    @Deprecated
     public interface MainFragmentAdapterProvider {
         /**
          * Returns an instance of {@link MainFragmentAdapter} that {@link BrowseFragment}
@@ -478,7 +488,9 @@
     /**
      * Interface to be implemented by {@link RowsFragment} and its subclasses for providing
      * an instance of {@link MainFragmentRowsAdapter}.
+     * @deprecated use {@link BrowseSupportFragment}
      */
+    @Deprecated
     public interface MainFragmentRowsAdapterProvider {
         /**
          * Returns an instance of {@link MainFragmentRowsAdapter} that {@link BrowseFragment}
@@ -491,7 +503,9 @@
      * This is used to pass information to {@link RowsFragment} or its subclasses.
      * {@link BrowseFragment} uses this interface to pass row based interaction events to
      * the target fragment.
+     * @deprecated use {@link BrowseSupportFragment}
      */
+    @Deprecated
     public static class MainFragmentRowsAdapter<T extends Fragment> {
         private final T mFragment;
 
@@ -570,14 +584,27 @@
         }
 
         boolean oldIsPageRow = mIsPageRow;
+        Object oldPageRow = mPageRow;
         mIsPageRow = mCanShowHeaders && item instanceof PageRow;
+        mPageRow = mIsPageRow ? item : null;
         boolean swap;
 
         if (mMainFragment == null) {
             swap = true;
         } else {
             if (oldIsPageRow) {
-                swap = true;
+                if (mIsPageRow) {
+                    if (oldPageRow == null) {
+                        // fragment is restored, page row object not yet set, so just set the
+                        // mPageRow object and there is no need to replace the fragment
+                        swap = false;
+                    } else {
+                        // swap if page row object changes
+                        swap = oldPageRow != mPageRow;
+                    }
+                } else {
+                    swap = true;
+                }
             } else {
                 swap = mIsPageRow;
             }
@@ -590,37 +617,45 @@
                         "Fragment must implement MainFragmentAdapterProvider");
             }
 
-            mMainFragmentAdapter = ((MainFragmentAdapterProvider)mMainFragment)
-                    .getMainFragmentAdapter();
-            mMainFragmentAdapter.setFragmentHost(new FragmentHostImpl());
-            if (!mIsPageRow) {
-                if (mMainFragment instanceof MainFragmentRowsAdapterProvider) {
-                    mMainFragmentRowsAdapter = ((MainFragmentRowsAdapterProvider)mMainFragment)
-                            .getMainFragmentRowsAdapter();
-                } else {
-                    mMainFragmentRowsAdapter = null;
-                }
-                mIsPageRow = mMainFragmentRowsAdapter == null;
-            } else {
-                mMainFragmentRowsAdapter = null;
-            }
+            setMainFragmentAdapter();
         }
 
         return swap;
     }
 
+    void setMainFragmentAdapter() {
+        mMainFragmentAdapter = ((MainFragmentAdapterProvider) mMainFragment)
+                .getMainFragmentAdapter();
+        mMainFragmentAdapter.setFragmentHost(new FragmentHostImpl());
+        if (!mIsPageRow) {
+            if (mMainFragment instanceof MainFragmentRowsAdapterProvider) {
+                setMainFragmentRowsAdapter(((MainFragmentRowsAdapterProvider) mMainFragment)
+                        .getMainFragmentRowsAdapter());
+            } else {
+                setMainFragmentRowsAdapter(null);
+            }
+            mIsPageRow = mMainFragmentRowsAdapter == null;
+        } else {
+            setMainFragmentRowsAdapter(null);
+        }
+    }
+
     /**
      * Factory class responsible for creating fragment given the current item. {@link ListRow}
      * should return {@link RowsFragment} or its subclass whereas {@link PageRow}
      * can return any fragment class.
+     * @deprecated use {@link BrowseSupportFragment}
      */
+    @Deprecated
     public abstract static class FragmentFactory<T extends Fragment> {
         public abstract T createFragment(Object row);
     }
 
     /**
      * FragmentFactory implementation for {@link ListRow}.
+     * @deprecated use {@link BrowseSupportFragment}
      */
+    @Deprecated
     public static class ListRowFragmentFactory extends FragmentFactory<RowsFragment> {
         @Override
         public RowsFragment createFragment(Object row) {
@@ -634,7 +669,9 @@
      * handling {@link ListRow}. Developers can override that and also if they want to
      * use custom fragment, they can register a custom {@link FragmentFactory}
      * against {@link PageRow}.
+     * @deprecated use {@link BrowseSupportFragment}
      */
+    @Deprecated
     public final static class MainFragmentAdapterRegistry {
         private final Map<Class, FragmentFactory> mItemToFragmentFactoryMapping = new HashMap<>();
         private final static FragmentFactory sDefaultFragmentFactory = new ListRowFragmentFactory();
@@ -678,11 +715,11 @@
     MainFragmentAdapter mMainFragmentAdapter;
     Fragment mMainFragment;
     HeadersFragment mHeadersFragment;
-    private MainFragmentRowsAdapter mMainFragmentRowsAdapter;
+    MainFragmentRowsAdapter mMainFragmentRowsAdapter;
+    ListRowDataAdapter mMainFragmentListRowDataAdapter;
 
     private ObjectAdapter mAdapter;
     private PresenterSelector mAdapterPresenter;
-    private PresenterSelector mWrappingPresenterSelector;
 
     private int mHeadersState = HEADERS_ENABLED;
     private int mBrandColor = Color.TRANSPARENT;
@@ -702,6 +739,7 @@
     private int mSelectedPosition = -1;
     private float mScaleFactor;
     boolean mIsPageRow;
+    Object mPageRow;
 
     private PresenterSelector mHeaderPresenterSelector;
     private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable();
@@ -767,7 +805,11 @@
      * Wrapping app provided PresenterSelector to support InvisibleRowPresenter for SectionRow
      * DividerRow and PageRow.
      */
-    private void createAndSetWrapperPresenter() {
+    private void updateWrapperPresenter() {
+        if (mAdapter == null) {
+            mAdapterPresenter = null;
+            return;
+        }
         final PresenterSelector adapterPresenter = mAdapter.getPresenterSelector();
         if (adapterPresenter == null) {
             throw new IllegalArgumentException("Adapter.getPresenterSelector() is null");
@@ -812,17 +854,49 @@
      */
     public void setAdapter(ObjectAdapter adapter) {
         mAdapter = adapter;
-        createAndSetWrapperPresenter();
+        updateWrapperPresenter();
         if (getView() == null) {
             return;
         }
-        replaceMainFragment(mSelectedPosition);
 
-        if (adapter != null) {
-            if (mMainFragmentRowsAdapter != null) {
-                mMainFragmentRowsAdapter.setAdapter(new ListRowDataAdapter(adapter));
-            }
-            mHeadersFragment.setAdapter(adapter);
+        updateMainFragmentRowsAdapter();
+        mHeadersFragment.setAdapter(mAdapter);
+    }
+
+    void setMainFragmentRowsAdapter(MainFragmentRowsAdapter mainFragmentRowsAdapter) {
+        if (mainFragmentRowsAdapter == mMainFragmentRowsAdapter) {
+            return;
+        }
+        // first clear previous mMainFragmentRowsAdapter and set a new mMainFragmentRowsAdapter
+        if (mMainFragmentRowsAdapter != null) {
+            // RowsFragment cannot change click/select listeners after view created.
+            // The main fragment and adapter should be GCed as long as there is no reference from
+            // BrowseFragment to it.
+            mMainFragmentRowsAdapter.setAdapter(null);
+        }
+        mMainFragmentRowsAdapter = mainFragmentRowsAdapter;
+        if (mMainFragmentRowsAdapter != null) {
+            mMainFragmentRowsAdapter.setOnItemViewSelectedListener(
+                    new MainFragmentItemViewSelectedListener(mMainFragmentRowsAdapter));
+            mMainFragmentRowsAdapter.setOnItemViewClickedListener(mOnItemViewClickedListener);
+        }
+        // second update mMainFragmentListRowDataAdapter set on mMainFragmentRowsAdapter
+        updateMainFragmentRowsAdapter();
+    }
+
+    /**
+     * Update mMainFragmentListRowDataAdapter and set it on mMainFragmentRowsAdapter.
+     * It also clears old mMainFragmentListRowDataAdapter.
+     */
+    void updateMainFragmentRowsAdapter() {
+        if (mMainFragmentListRowDataAdapter != null) {
+            mMainFragmentListRowDataAdapter.detach();
+            mMainFragmentListRowDataAdapter = null;
+        }
+        if (mMainFragmentRowsAdapter != null) {
+            mMainFragmentListRowDataAdapter = mAdapter == null
+                    ? null : new ListRowDataAdapter(mAdapter);
+            mMainFragmentRowsAdapter.setAdapter(mMainFragmentListRowDataAdapter);
         }
     }
 
@@ -1143,7 +1217,8 @@
 
     @Override
     public void onDestroyView() {
-        mMainFragmentRowsAdapter = null;
+        setMainFragmentRowsAdapter(null);
+        mPageRow = null;
         mMainFragmentAdapter = null;
         mMainFragment = null;
         mHeadersFragment = null;
@@ -1197,26 +1272,17 @@
             mHeadersFragment = (HeadersFragment) getChildFragmentManager()
                     .findFragmentById(R.id.browse_headers_dock);
             mMainFragment = getChildFragmentManager().findFragmentById(R.id.scale_frame);
-            mMainFragmentAdapter = ((MainFragmentAdapterProvider)mMainFragment)
-                    .getMainFragmentAdapter();
-            mMainFragmentAdapter.setFragmentHost(new FragmentHostImpl());
 
             mIsPageRow = savedInstanceState != null
                     && savedInstanceState.getBoolean(IS_PAGE_ROW, false);
+            // mPageRow object is unable to restore, if its null and mIsPageRow is true, this is
+            // the case for restoring, later if setSelection() triggers a createMainFragment(),
+            // should not create fragment.
 
             mSelectedPosition = savedInstanceState != null
                     ? savedInstanceState.getInt(CURRENT_SELECTED_POSITION, 0) : 0;
 
-            if (!mIsPageRow) {
-                if (mMainFragment instanceof MainFragmentRowsAdapterProvider) {
-                    mMainFragmentRowsAdapter = ((MainFragmentRowsAdapterProvider) mMainFragment)
-                            .getMainFragmentRowsAdapter();
-                } else {
-                    mMainFragmentRowsAdapter = null;
-                }
-            } else {
-                mMainFragmentRowsAdapter = null;
-            }
+            setMainFragmentAdapter();
         }
 
         mHeadersFragment.setHeadersGone(!mCanShowHeaders);
@@ -1241,8 +1307,6 @@
         mScaleFrameLayout.setPivotX(0);
         mScaleFrameLayout.setPivotY(mContainerListAlignTop);
 
-        setupMainFragment();
-
         if (mBrandColorSet) {
             mHeadersFragment.setBackgroundColor(mBrandColor);
         }
@@ -1269,17 +1333,6 @@
         return root;
     }
 
-    private void setupMainFragment() {
-        if (mMainFragmentRowsAdapter != null) {
-            if (mAdapter != null) {
-                mMainFragmentRowsAdapter.setAdapter(new ListRowDataAdapter(mAdapter));
-            }
-            mMainFragmentRowsAdapter.setOnItemViewSelectedListener(
-                    new MainFragmentItemViewSelectedListener(mMainFragmentRowsAdapter));
-            mMainFragmentRowsAdapter.setOnItemViewClickedListener(mOnItemViewClickedListener);
-        }
-    }
-
     void createHeadersTransition() {
         mHeadersTransition = TransitionHelper.loadTransition(FragmentUtil.getContext(BrowseFragment.this),
                 mShowingHeaders
@@ -1469,10 +1522,10 @@
     };
 
     void onRowSelected(int position) {
-        if (position != mSelectedPosition) {
-            mSetSelectionRunnable.post(
-                    position, SetSelectionRunnable.TYPE_INTERNAL_SYNC, true);
-        }
+        // even position is same, it could be data changed, always post selection runnable
+        // to possibly swap main fragment.
+        mSetSelectionRunnable.post(
+                position, SetSelectionRunnable.TYPE_INTERNAL_SYNC, true);
     }
 
     void setSelection(int position, boolean smooth) {
@@ -1499,7 +1552,6 @@
         if (createMainFragment(mAdapter, position)) {
             swapToMainFragment();
             expandMainFragment(!(mCanShowHeaders && mShowingHeaders));
-            setupMainFragment();
         }
     }
 
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BrowseSupportFragment.java b/leanback/src/android/support/v17/leanback/app/BrowseSupportFragment.java
similarity index 94%
rename from v17/leanback/src/android/support/v17/leanback/app/BrowseSupportFragment.java
rename to leanback/src/android/support/v17/leanback/app/BrowseSupportFragment.java
index 03b3c8a..c28064c 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/BrowseSupportFragment.java
+++ b/leanback/src/android/support/v17/leanback/app/BrowseSupportFragment.java
@@ -567,14 +567,27 @@
         }
 
         boolean oldIsPageRow = mIsPageRow;
+        Object oldPageRow = mPageRow;
         mIsPageRow = mCanShowHeaders && item instanceof PageRow;
+        mPageRow = mIsPageRow ? item : null;
         boolean swap;
 
         if (mMainFragment == null) {
             swap = true;
         } else {
             if (oldIsPageRow) {
-                swap = true;
+                if (mIsPageRow) {
+                    if (oldPageRow == null) {
+                        // fragment is restored, page row object not yet set, so just set the
+                        // mPageRow object and there is no need to replace the fragment
+                        swap = false;
+                    } else {
+                        // swap if page row object changes
+                        swap = oldPageRow != mPageRow;
+                    }
+                } else {
+                    swap = true;
+                }
             } else {
                 swap = mIsPageRow;
             }
@@ -587,25 +600,29 @@
                         "Fragment must implement MainFragmentAdapterProvider");
             }
 
-            mMainFragmentAdapter = ((MainFragmentAdapterProvider)mMainFragment)
-                    .getMainFragmentAdapter();
-            mMainFragmentAdapter.setFragmentHost(new FragmentHostImpl());
-            if (!mIsPageRow) {
-                if (mMainFragment instanceof MainFragmentRowsAdapterProvider) {
-                    mMainFragmentRowsAdapter = ((MainFragmentRowsAdapterProvider)mMainFragment)
-                            .getMainFragmentRowsAdapter();
-                } else {
-                    mMainFragmentRowsAdapter = null;
-                }
-                mIsPageRow = mMainFragmentRowsAdapter == null;
-            } else {
-                mMainFragmentRowsAdapter = null;
-            }
+            setMainFragmentAdapter();
         }
 
         return swap;
     }
 
+    void setMainFragmentAdapter() {
+        mMainFragmentAdapter = ((MainFragmentAdapterProvider) mMainFragment)
+                .getMainFragmentAdapter();
+        mMainFragmentAdapter.setFragmentHost(new FragmentHostImpl());
+        if (!mIsPageRow) {
+            if (mMainFragment instanceof MainFragmentRowsAdapterProvider) {
+                setMainFragmentRowsAdapter(((MainFragmentRowsAdapterProvider) mMainFragment)
+                        .getMainFragmentRowsAdapter());
+            } else {
+                setMainFragmentRowsAdapter(null);
+            }
+            mIsPageRow = mMainFragmentRowsAdapter == null;
+        } else {
+            setMainFragmentRowsAdapter(null);
+        }
+    }
+
     /**
      * Factory class responsible for creating fragment given the current item. {@link ListRow}
      * should return {@link RowsSupportFragment} or its subclass whereas {@link PageRow}
@@ -675,11 +692,11 @@
     MainFragmentAdapter mMainFragmentAdapter;
     Fragment mMainFragment;
     HeadersSupportFragment mHeadersSupportFragment;
-    private MainFragmentRowsAdapter mMainFragmentRowsAdapter;
+    MainFragmentRowsAdapter mMainFragmentRowsAdapter;
+    ListRowDataAdapter mMainFragmentListRowDataAdapter;
 
     private ObjectAdapter mAdapter;
     private PresenterSelector mAdapterPresenter;
-    private PresenterSelector mWrappingPresenterSelector;
 
     private int mHeadersState = HEADERS_ENABLED;
     private int mBrandColor = Color.TRANSPARENT;
@@ -699,6 +716,7 @@
     private int mSelectedPosition = -1;
     private float mScaleFactor;
     boolean mIsPageRow;
+    Object mPageRow;
 
     private PresenterSelector mHeaderPresenterSelector;
     private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable();
@@ -764,7 +782,11 @@
      * Wrapping app provided PresenterSelector to support InvisibleRowPresenter for SectionRow
      * DividerRow and PageRow.
      */
-    private void createAndSetWrapperPresenter() {
+    private void updateWrapperPresenter() {
+        if (mAdapter == null) {
+            mAdapterPresenter = null;
+            return;
+        }
         final PresenterSelector adapterPresenter = mAdapter.getPresenterSelector();
         if (adapterPresenter == null) {
             throw new IllegalArgumentException("Adapter.getPresenterSelector() is null");
@@ -809,17 +831,49 @@
      */
     public void setAdapter(ObjectAdapter adapter) {
         mAdapter = adapter;
-        createAndSetWrapperPresenter();
+        updateWrapperPresenter();
         if (getView() == null) {
             return;
         }
-        replaceMainFragment(mSelectedPosition);
 
-        if (adapter != null) {
-            if (mMainFragmentRowsAdapter != null) {
-                mMainFragmentRowsAdapter.setAdapter(new ListRowDataAdapter(adapter));
-            }
-            mHeadersSupportFragment.setAdapter(adapter);
+        updateMainFragmentRowsAdapter();
+        mHeadersSupportFragment.setAdapter(mAdapter);
+    }
+
+    void setMainFragmentRowsAdapter(MainFragmentRowsAdapter mainFragmentRowsAdapter) {
+        if (mainFragmentRowsAdapter == mMainFragmentRowsAdapter) {
+            return;
+        }
+        // first clear previous mMainFragmentRowsAdapter and set a new mMainFragmentRowsAdapter
+        if (mMainFragmentRowsAdapter != null) {
+            // RowsFragment cannot change click/select listeners after view created.
+            // The main fragment and adapter should be GCed as long as there is no reference from
+            // BrowseSupportFragment to it.
+            mMainFragmentRowsAdapter.setAdapter(null);
+        }
+        mMainFragmentRowsAdapter = mainFragmentRowsAdapter;
+        if (mMainFragmentRowsAdapter != null) {
+            mMainFragmentRowsAdapter.setOnItemViewSelectedListener(
+                    new MainFragmentItemViewSelectedListener(mMainFragmentRowsAdapter));
+            mMainFragmentRowsAdapter.setOnItemViewClickedListener(mOnItemViewClickedListener);
+        }
+        // second update mMainFragmentListRowDataAdapter set on mMainFragmentRowsAdapter
+        updateMainFragmentRowsAdapter();
+    }
+
+    /**
+     * Update mMainFragmentListRowDataAdapter and set it on mMainFragmentRowsAdapter.
+     * It also clears old mMainFragmentListRowDataAdapter.
+     */
+    void updateMainFragmentRowsAdapter() {
+        if (mMainFragmentListRowDataAdapter != null) {
+            mMainFragmentListRowDataAdapter.detach();
+            mMainFragmentListRowDataAdapter = null;
+        }
+        if (mMainFragmentRowsAdapter != null) {
+            mMainFragmentListRowDataAdapter = mAdapter == null
+                    ? null : new ListRowDataAdapter(mAdapter);
+            mMainFragmentRowsAdapter.setAdapter(mMainFragmentListRowDataAdapter);
         }
     }
 
@@ -1140,7 +1194,8 @@
 
     @Override
     public void onDestroyView() {
-        mMainFragmentRowsAdapter = null;
+        setMainFragmentRowsAdapter(null);
+        mPageRow = null;
         mMainFragmentAdapter = null;
         mMainFragment = null;
         mHeadersSupportFragment = null;
@@ -1194,26 +1249,17 @@
             mHeadersSupportFragment = (HeadersSupportFragment) getChildFragmentManager()
                     .findFragmentById(R.id.browse_headers_dock);
             mMainFragment = getChildFragmentManager().findFragmentById(R.id.scale_frame);
-            mMainFragmentAdapter = ((MainFragmentAdapterProvider)mMainFragment)
-                    .getMainFragmentAdapter();
-            mMainFragmentAdapter.setFragmentHost(new FragmentHostImpl());
 
             mIsPageRow = savedInstanceState != null
                     && savedInstanceState.getBoolean(IS_PAGE_ROW, false);
+            // mPageRow object is unable to restore, if its null and mIsPageRow is true, this is
+            // the case for restoring, later if setSelection() triggers a createMainFragment(),
+            // should not create fragment.
 
             mSelectedPosition = savedInstanceState != null
                     ? savedInstanceState.getInt(CURRENT_SELECTED_POSITION, 0) : 0;
 
-            if (!mIsPageRow) {
-                if (mMainFragment instanceof MainFragmentRowsAdapterProvider) {
-                    mMainFragmentRowsAdapter = ((MainFragmentRowsAdapterProvider) mMainFragment)
-                            .getMainFragmentRowsAdapter();
-                } else {
-                    mMainFragmentRowsAdapter = null;
-                }
-            } else {
-                mMainFragmentRowsAdapter = null;
-            }
+            setMainFragmentAdapter();
         }
 
         mHeadersSupportFragment.setHeadersGone(!mCanShowHeaders);
@@ -1238,8 +1284,6 @@
         mScaleFrameLayout.setPivotX(0);
         mScaleFrameLayout.setPivotY(mContainerListAlignTop);
 
-        setupMainFragment();
-
         if (mBrandColorSet) {
             mHeadersSupportFragment.setBackgroundColor(mBrandColor);
         }
@@ -1266,17 +1310,6 @@
         return root;
     }
 
-    private void setupMainFragment() {
-        if (mMainFragmentRowsAdapter != null) {
-            if (mAdapter != null) {
-                mMainFragmentRowsAdapter.setAdapter(new ListRowDataAdapter(mAdapter));
-            }
-            mMainFragmentRowsAdapter.setOnItemViewSelectedListener(
-                    new MainFragmentItemViewSelectedListener(mMainFragmentRowsAdapter));
-            mMainFragmentRowsAdapter.setOnItemViewClickedListener(mOnItemViewClickedListener);
-        }
-    }
-
     void createHeadersTransition() {
         mHeadersTransition = TransitionHelper.loadTransition(getContext(),
                 mShowingHeaders
@@ -1466,10 +1499,10 @@
     };
 
     void onRowSelected(int position) {
-        if (position != mSelectedPosition) {
-            mSetSelectionRunnable.post(
-                    position, SetSelectionRunnable.TYPE_INTERNAL_SYNC, true);
-        }
+        // even position is same, it could be data changed, always post selection runnable
+        // to possibly swap main fragment.
+        mSetSelectionRunnable.post(
+                position, SetSelectionRunnable.TYPE_INTERNAL_SYNC, true);
     }
 
     void setSelection(int position, boolean smooth) {
@@ -1496,7 +1529,6 @@
         if (createMainFragment(mAdapter, position)) {
             swapToMainFragment();
             expandMainFragment(!(mCanShowHeaders && mShowingHeaders));
-            setupMainFragment();
         }
     }
 
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsBackgroundVideoHelper.java b/leanback/src/android/support/v17/leanback/app/DetailsBackgroundVideoHelper.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/app/DetailsBackgroundVideoHelper.java
rename to leanback/src/android/support/v17/leanback/app/DetailsBackgroundVideoHelper.java
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java b/leanback/src/android/support/v17/leanback/app/DetailsFragment.java
similarity index 99%
rename from v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java
rename to leanback/src/android/support/v17/leanback/app/DetailsFragment.java
index 3655963..18934f4 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java
+++ b/leanback/src/android/support/v17/leanback/app/DetailsFragment.java
@@ -91,7 +91,9 @@
  * DetailsFragment can use {@link DetailsFragmentBackgroundController} to add a parallax drawable
  * background and embedded video playing fragment.
  * </p>
+ * @deprecated use {@link DetailsSupportFragment}
  */
+@Deprecated
 public class DetailsFragment extends BaseFragment {
     static final String TAG = "DetailsFragment";
     static boolean DEBUG = false;
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java b/leanback/src/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java
similarity index 99%
rename from v17/leanback/src/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java
rename to leanback/src/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java
index 223b8ef..25ed723 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java
+++ b/leanback/src/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java
@@ -107,7 +107,9 @@
  * {@link #onCreateGlueHost()}.
  * </p>
  *
+ * @deprecated use {@link DetailsSupportFragmentBackgroundController}
  */
+@Deprecated
 public class DetailsFragmentBackgroundController {
 
     final DetailsFragment mFragment;
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragment.java b/leanback/src/android/support/v17/leanback/app/DetailsSupportFragment.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragment.java
rename to leanback/src/android/support/v17/leanback/app/DetailsSupportFragment.java
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragmentBackgroundController.java b/leanback/src/android/support/v17/leanback/app/DetailsSupportFragmentBackgroundController.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragmentBackgroundController.java
rename to leanback/src/android/support/v17/leanback/app/DetailsSupportFragmentBackgroundController.java
diff --git a/v17/leanback/src/android/support/v17/leanback/app/ErrorFragment.java b/leanback/src/android/support/v17/leanback/app/ErrorFragment.java
similarity index 98%
rename from v17/leanback/src/android/support/v17/leanback/app/ErrorFragment.java
rename to leanback/src/android/support/v17/leanback/app/ErrorFragment.java
index 2896d0f..eda0de1 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/ErrorFragment.java
+++ b/leanback/src/android/support/v17/leanback/app/ErrorFragment.java
@@ -32,7 +32,9 @@
 
 /**
  * A fragment for displaying an error indication.
+ * @deprecated use {@link ErrorSupportFragment}
  */
+@Deprecated
 public class ErrorFragment extends BrandedFragment {
 
     private ViewGroup mErrorFrame;
diff --git a/v17/leanback/src/android/support/v17/leanback/app/ErrorSupportFragment.java b/leanback/src/android/support/v17/leanback/app/ErrorSupportFragment.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/app/ErrorSupportFragment.java
rename to leanback/src/android/support/v17/leanback/app/ErrorSupportFragment.java
diff --git a/v17/leanback/src/android/support/v17/leanback/app/FragmentUtil.java b/leanback/src/android/support/v17/leanback/app/FragmentUtil.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/app/FragmentUtil.java
rename to leanback/src/android/support/v17/leanback/app/FragmentUtil.java
diff --git a/v17/leanback/src/android/support/v17/leanback/app/GuidedStepFragment.java b/leanback/src/android/support/v17/leanback/app/GuidedStepFragment.java
similarity index 98%
rename from v17/leanback/src/android/support/v17/leanback/app/GuidedStepFragment.java
rename to leanback/src/android/support/v17/leanback/app/GuidedStepFragment.java
index 2b7f2d0..9be350d 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/GuidedStepFragment.java
+++ b/leanback/src/android/support/v17/leanback/app/GuidedStepFragment.java
@@ -27,6 +27,7 @@
 import android.support.annotation.RestrictTo;
 import android.support.v17.leanback.R;
 import android.support.v17.leanback.transition.TransitionHelper;
+import android.support.v17.leanback.widget.DiffCallback;
 import android.support.v17.leanback.widget.GuidanceStylist;
 import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
 import android.support.v17.leanback.widget.GuidedAction;
@@ -140,7 +141,9 @@
  * @see GuidanceStylist.Guidance
  * @see GuidedAction
  * @see GuidedActionsStylist
+ * @deprecated use {@link GuidedStepSupportFragment}
  */
+@Deprecated
 public class GuidedStepFragment extends Fragment implements GuidedActionAdapter.FocusListener {
 
     private static final String TAG_LEAN_BACK_ACTIONS_FRAGMENT = "leanBackGuidedStepFragment";
@@ -806,6 +809,8 @@
 
     /**
      * Sets the list of GuidedActions that the user may take in this fragment.
+     * Uses DiffCallback set by {@link #setActionsDiffCallback(DiffCallback)}.
+     *
      * @param actions The list of GuidedActions for this fragment.
      */
     public void setActions(List<GuidedAction> actions) {
@@ -816,6 +821,18 @@
     }
 
     /**
+     * Sets the RecyclerView DiffCallback used when {@link #setActions(List)} is called. By default
+     * GuidedStepFragment uses
+     * {@link android.support.v17.leanback.widget.GuidedActionDiffCallback}.
+     * Sets it to null if app wants to refresh the whole list.
+     *
+     * @param diffCallback DiffCallback used in {@link #setActions(List)}.
+     */
+    public void setActionsDiffCallback(DiffCallback<GuidedAction> diffCallback) {
+        mAdapter.setDiffCallback(diffCallback);
+    }
+
+    /**
      * Notify an action has changed and update its UI.
      * @param position Position of the GuidedAction in array.
      */
diff --git a/v17/leanback/src/android/support/v17/leanback/app/GuidedStepRootLayout.java b/leanback/src/android/support/v17/leanback/app/GuidedStepRootLayout.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/app/GuidedStepRootLayout.java
rename to leanback/src/android/support/v17/leanback/app/GuidedStepRootLayout.java
diff --git a/v17/leanback/src/android/support/v17/leanback/app/GuidedStepSupportFragment.java b/leanback/src/android/support/v17/leanback/app/GuidedStepSupportFragment.java
similarity index 98%
rename from v17/leanback/src/android/support/v17/leanback/app/GuidedStepSupportFragment.java
rename to leanback/src/android/support/v17/leanback/app/GuidedStepSupportFragment.java
index aeb2d33..e276d07 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/GuidedStepSupportFragment.java
+++ b/leanback/src/android/support/v17/leanback/app/GuidedStepSupportFragment.java
@@ -24,6 +24,7 @@
 import android.support.annotation.RestrictTo;
 import android.support.v17.leanback.R;
 import android.support.v17.leanback.transition.TransitionHelper;
+import android.support.v17.leanback.widget.DiffCallback;
 import android.support.v17.leanback.widget.GuidanceStylist;
 import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
 import android.support.v17.leanback.widget.GuidedAction;
@@ -803,6 +804,8 @@
 
     /**
      * Sets the list of GuidedActions that the user may take in this fragment.
+     * Uses DiffCallback set by {@link #setActionsDiffCallback(DiffCallback)}.
+     *
      * @param actions The list of GuidedActions for this fragment.
      */
     public void setActions(List<GuidedAction> actions) {
@@ -813,6 +816,18 @@
     }
 
     /**
+     * Sets the RecyclerView DiffCallback used when {@link #setActions(List)} is called. By default
+     * GuidedStepSupportFragment uses
+     * {@link android.support.v17.leanback.widget.GuidedActionDiffCallback}.
+     * Sets it to null if app wants to refresh the whole list.
+     *
+     * @param diffCallback DiffCallback used in {@link #setActions(List)}.
+     */
+    public void setActionsDiffCallback(DiffCallback<GuidedAction> diffCallback) {
+        mAdapter.setDiffCallback(diffCallback);
+    }
+
+    /**
      * Notify an action has changed and update its UI.
      * @param position Position of the GuidedAction in array.
      */
diff --git a/v17/leanback/src/android/support/v17/leanback/app/HeadersFragment.java b/leanback/src/android/support/v17/leanback/app/HeadersFragment.java
similarity index 98%
rename from v17/leanback/src/android/support/v17/leanback/app/HeadersFragment.java
rename to leanback/src/android/support/v17/leanback/app/HeadersFragment.java
index dd037d2..08780a5 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/HeadersFragment.java
+++ b/leanback/src/android/support/v17/leanback/app/HeadersFragment.java
@@ -52,12 +52,16 @@
  * </ul>
  * Use {@link #setPresenterSelector(PresenterSelector)} in subclass constructor to customize
  * Presenters. App may override {@link BrowseFragment#onCreateHeadersFragment()}.
+ * @deprecated use {@link HeadersSupportFragment}
  */
+@Deprecated
 public class HeadersFragment extends BaseRowFragment {
 
     /**
      * Interface definition for a callback to be invoked when a header item is clicked.
+     * @deprecated use {@link HeadersSupportFragment}
      */
+    @Deprecated
     public interface OnHeaderClickedListener {
         /**
          * Called when a header item has been clicked.
@@ -70,7 +74,9 @@
 
     /**
      * Interface definition for a callback to be invoked when a header item is selected.
+     * @deprecated use {@link HeadersSupportFragment}
      */
+    @Deprecated
     public interface OnHeaderViewSelectedListener {
         /**
          * Called when a header item has been selected.
diff --git a/v17/leanback/src/android/support/v17/leanback/app/HeadersSupportFragment.java b/leanback/src/android/support/v17/leanback/app/HeadersSupportFragment.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/app/HeadersSupportFragment.java
rename to leanback/src/android/support/v17/leanback/app/HeadersSupportFragment.java
diff --git a/v17/leanback/src/android/support/v17/leanback/app/ListRowDataAdapter.java b/leanback/src/android/support/v17/leanback/app/ListRowDataAdapter.java
similarity index 93%
rename from v17/leanback/src/android/support/v17/leanback/app/ListRowDataAdapter.java
rename to leanback/src/android/support/v17/leanback/app/ListRowDataAdapter.java
index f9af12f..03d948b 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/ListRowDataAdapter.java
+++ b/leanback/src/android/support/v17/leanback/app/ListRowDataAdapter.java
@@ -13,6 +13,7 @@
  * thinks there are items even though they're invisible. This class takes care of filtering out
  * the invisible rows at the end. In case the data inside the adapter changes, it adjusts the
  * bounds to reflect the latest data.
+ * {@link #detach()} must be called to release DataObserver from Adapter.
  */
 class ListRowDataAdapter extends ObjectAdapter {
     public static final int ON_ITEM_RANGE_CHANGED = 2;
@@ -22,6 +23,7 @@
 
     private final ObjectAdapter mAdapter;
     int mLastVisibleRowIndex;
+    final DataObserver mDataObserver;
 
     public ListRowDataAdapter(ObjectAdapter adapter) {
         super(adapter.getPresenterSelector());
@@ -34,10 +36,20 @@
         // operation. To handle this case, we use QueueBasedDataObserver which forces
         // recyclerview to do a full data refresh after each update operation.
         if (adapter.isImmediateNotifySupported()) {
-            mAdapter.registerObserver(new SimpleDataObserver());
+            mDataObserver = new SimpleDataObserver();
         } else {
-            mAdapter.registerObserver(new QueueBasedDataObserver());
+            mDataObserver = new QueueBasedDataObserver();
         }
+        attach();
+    }
+
+    void detach() {
+        mAdapter.unregisterObserver(mDataObserver);
+    }
+
+    void attach() {
+        initialize();
+        mAdapter.registerObserver(mDataObserver);
     }
 
     void initialize() {
diff --git a/v17/leanback/src/android/support/v17/leanback/app/OnboardingFragment.java b/leanback/src/android/support/v17/leanback/app/OnboardingFragment.java
similarity index 99%
rename from v17/leanback/src/android/support/v17/leanback/app/OnboardingFragment.java
rename to leanback/src/android/support/v17/leanback/app/OnboardingFragment.java
index b69d5a7..f352c41 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/OnboardingFragment.java
+++ b/leanback/src/android/support/v17/leanback/app/OnboardingFragment.java
@@ -154,7 +154,9 @@
  * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingPageIndicatorStyle
  * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingStartButtonStyle
  * @attr ref R.styleable#LeanbackOnboardingTheme_onboardingLogoStyle
+ * @deprecated use {@link OnboardingSupportFragment}
  */
+@Deprecated
 abstract public class OnboardingFragment extends Fragment {
     private static final String TAG = "OnboardingF";
     private static final boolean DEBUG = false;
diff --git a/v17/leanback/src/android/support/v17/leanback/app/OnboardingSupportFragment.java b/leanback/src/android/support/v17/leanback/app/OnboardingSupportFragment.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/app/OnboardingSupportFragment.java
rename to leanback/src/android/support/v17/leanback/app/OnboardingSupportFragment.java
diff --git a/v17/leanback/src/android/support/v17/leanback/app/PermissionHelper.java b/leanback/src/android/support/v17/leanback/app/PermissionHelper.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/app/PermissionHelper.java
rename to leanback/src/android/support/v17/leanback/app/PermissionHelper.java
diff --git a/v17/leanback/src/android/support/v17/leanback/app/PlaybackFragment.java b/leanback/src/android/support/v17/leanback/app/PlaybackFragment.java
similarity index 99%
rename from v17/leanback/src/android/support/v17/leanback/app/PlaybackFragment.java
rename to leanback/src/android/support/v17/leanback/app/PlaybackFragment.java
index 33e787c..e2e6be4 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/PlaybackFragment.java
+++ b/leanback/src/android/support/v17/leanback/app/PlaybackFragment.java
@@ -81,7 +81,9 @@
  * {@link #setControlsOverlayAutoHideEnabled(boolean)} upon play/pause. The auto hiding timer will
  * be cancelled upon {@link #tickle()} triggered by input event.
  * </p>
+ * @deprecated use {@link PlaybackSupportFragment}
  */
+@Deprecated
 public class PlaybackFragment extends Fragment {
     static final String BUNDLE_CONTROL_VISIBLE_ON_CREATEVIEW = "controlvisible_oncreateview";
 
@@ -181,7 +183,9 @@
      * Listener allowing the application to receive notification of fade in and/or fade out
      * completion events.
      * @hide
+     * @deprecated use {@link PlaybackSupportFragment}
      */
+    @Deprecated
     public static class OnFadeCompleteListener {
         public void onFadeInComplete() {
         }
diff --git a/v17/leanback/src/android/support/v17/leanback/app/PlaybackFragmentGlueHost.java b/leanback/src/android/support/v17/leanback/app/PlaybackFragmentGlueHost.java
similarity index 98%
rename from v17/leanback/src/android/support/v17/leanback/app/PlaybackFragmentGlueHost.java
rename to leanback/src/android/support/v17/leanback/app/PlaybackFragmentGlueHost.java
index 4a9d10f..9e342fd 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/PlaybackFragmentGlueHost.java
+++ b/leanback/src/android/support/v17/leanback/app/PlaybackFragmentGlueHost.java
@@ -30,7 +30,9 @@
 /**
  * {@link PlaybackGlueHost} implementation
  * the interaction between this class and {@link PlaybackFragment}.
+ * @deprecated use {@link PlaybackSupportFragmentGlueHost}
  */
+@Deprecated
 public class PlaybackFragmentGlueHost extends PlaybackGlueHost implements PlaybackSeekUi {
     private final PlaybackFragment mFragment;
 
diff --git a/v17/leanback/src/android/support/v17/leanback/app/PlaybackSupportFragment.java b/leanback/src/android/support/v17/leanback/app/PlaybackSupportFragment.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/app/PlaybackSupportFragment.java
rename to leanback/src/android/support/v17/leanback/app/PlaybackSupportFragment.java
diff --git a/v17/leanback/src/android/support/v17/leanback/app/PlaybackSupportFragmentGlueHost.java b/leanback/src/android/support/v17/leanback/app/PlaybackSupportFragmentGlueHost.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/app/PlaybackSupportFragmentGlueHost.java
rename to leanback/src/android/support/v17/leanback/app/PlaybackSupportFragmentGlueHost.java
diff --git a/v17/leanback/src/android/support/v17/leanback/app/ProgressBarManager.java b/leanback/src/android/support/v17/leanback/app/ProgressBarManager.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/app/ProgressBarManager.java
rename to leanback/src/android/support/v17/leanback/app/ProgressBarManager.java
diff --git a/v17/leanback/src/android/support/v17/leanback/app/RowsFragment.java b/leanback/src/android/support/v17/leanback/app/RowsFragment.java
similarity index 99%
rename from v17/leanback/src/android/support/v17/leanback/app/RowsFragment.java
rename to leanback/src/android/support/v17/leanback/app/RowsFragment.java
index a008ad6..aa346bd 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/RowsFragment.java
+++ b/leanback/src/android/support/v17/leanback/app/RowsFragment.java
@@ -53,7 +53,9 @@
  * of rows in a vertical list. The Adapter's {@link PresenterSelector} must maintain subclasses
  * of {@link RowPresenter}.
  * </p>
+ * @deprecated use {@link RowsSupportFragment}
  */
+@Deprecated
 public class RowsFragment extends BaseRowFragment implements
         BrowseFragment.MainFragmentRowsAdapterProvider,
         BrowseFragment.MainFragmentAdapterProvider {
@@ -634,7 +636,9 @@
      * The adapter that RowsFragment implements
      * BrowseFragment.MainFragmentRowsAdapter.
      * @see #getMainFragmentRowsAdapter().
+     * @deprecated use {@link RowsSupportFragment}
      */
+    @Deprecated
     public static class MainFragmentRowsAdapter
             extends BrowseFragment.MainFragmentRowsAdapter<RowsFragment> {
 
diff --git a/v17/leanback/src/android/support/v17/leanback/app/RowsSupportFragment.java b/leanback/src/android/support/v17/leanback/app/RowsSupportFragment.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/app/RowsSupportFragment.java
rename to leanback/src/android/support/v17/leanback/app/RowsSupportFragment.java
diff --git a/v17/leanback/src/android/support/v17/leanback/app/SearchFragment.java b/leanback/src/android/support/v17/leanback/app/SearchFragment.java
similarity index 99%
rename from v17/leanback/src/android/support/v17/leanback/app/SearchFragment.java
rename to leanback/src/android/support/v17/leanback/app/SearchFragment.java
index 2154ff2..00f2cca 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/SearchFragment.java
+++ b/leanback/src/android/support/v17/leanback/app/SearchFragment.java
@@ -66,7 +66,9 @@
  * not when fragment is restored from an instance state.  Activity may manually
  * call {@link #startRecognition()}, typically in onNewIntent().
  * </p>
+ * @deprecated use {@link SearchSupportFragment}
  */
+@Deprecated
 public class SearchFragment extends Fragment {
     static final String TAG = SearchFragment.class.getSimpleName();
     static final boolean DEBUG = false;
diff --git a/v17/leanback/src/android/support/v17/leanback/app/SearchSupportFragment.java b/leanback/src/android/support/v17/leanback/app/SearchSupportFragment.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/app/SearchSupportFragment.java
rename to leanback/src/android/support/v17/leanback/app/SearchSupportFragment.java
diff --git a/v17/leanback/src/android/support/v17/leanback/app/VerticalGridFragment.java b/leanback/src/android/support/v17/leanback/app/VerticalGridFragment.java
similarity index 98%
rename from v17/leanback/src/android/support/v17/leanback/app/VerticalGridFragment.java
rename to leanback/src/android/support/v17/leanback/app/VerticalGridFragment.java
index 5bc52ff..bff3dba 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/VerticalGridFragment.java
+++ b/leanback/src/android/support/v17/leanback/app/VerticalGridFragment.java
@@ -39,7 +39,9 @@
  *
  * <p>Renders a vertical grid of objects given a {@link VerticalGridPresenter} and
  * an {@link ObjectAdapter}.
+ * @deprecated use {@link VerticalGridSupportFragment}
  */
+@Deprecated
 public class VerticalGridFragment extends BaseFragment {
     static final String TAG = "VerticalGF";
     static boolean DEBUG = false;
diff --git a/v17/leanback/src/android/support/v17/leanback/app/VerticalGridSupportFragment.java b/leanback/src/android/support/v17/leanback/app/VerticalGridSupportFragment.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/app/VerticalGridSupportFragment.java
rename to leanback/src/android/support/v17/leanback/app/VerticalGridSupportFragment.java
diff --git a/v17/leanback/src/android/support/v17/leanback/app/VideoFragment.java b/leanback/src/android/support/v17/leanback/app/VideoFragment.java
similarity index 98%
rename from v17/leanback/src/android/support/v17/leanback/app/VideoFragment.java
rename to leanback/src/android/support/v17/leanback/app/VideoFragment.java
index 1b2b8d0..e4d75f3 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/VideoFragment.java
+++ b/leanback/src/android/support/v17/leanback/app/VideoFragment.java
@@ -27,7 +27,9 @@
 /**
  * Subclass of {@link PlaybackFragment} that is responsible for providing a {@link SurfaceView}
  * and rendering video.
+ * @deprecated use {@link VideoSupportFragment}
  */
+@Deprecated
 public class VideoFragment extends PlaybackFragment {
     static final int SURFACE_NOT_CREATED = 0;
     static final int SURFACE_CREATED = 1;
diff --git a/v17/leanback/src/android/support/v17/leanback/app/VideoFragmentGlueHost.java b/leanback/src/android/support/v17/leanback/app/VideoFragmentGlueHost.java
similarity index 96%
rename from v17/leanback/src/android/support/v17/leanback/app/VideoFragmentGlueHost.java
rename to leanback/src/android/support/v17/leanback/app/VideoFragmentGlueHost.java
index d123676..546e581 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/VideoFragmentGlueHost.java
+++ b/leanback/src/android/support/v17/leanback/app/VideoFragmentGlueHost.java
@@ -24,7 +24,9 @@
 /**
  * {@link PlaybackGlueHost} implementation
  * the interaction between {@link PlaybackGlue} and {@link VideoFragment}.
+ * @deprecated use {@link VideoSupportFragmentGlueHost}
  */
+@Deprecated
 public class VideoFragmentGlueHost extends PlaybackFragmentGlueHost
         implements SurfaceHolderGlueHost {
     private final VideoFragment mFragment;
diff --git a/v17/leanback/src/android/support/v17/leanback/app/VideoSupportFragment.java b/leanback/src/android/support/v17/leanback/app/VideoSupportFragment.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/app/VideoSupportFragment.java
rename to leanback/src/android/support/v17/leanback/app/VideoSupportFragment.java
diff --git a/v17/leanback/src/android/support/v17/leanback/app/VideoSupportFragmentGlueHost.java b/leanback/src/android/support/v17/leanback/app/VideoSupportFragmentGlueHost.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/app/VideoSupportFragmentGlueHost.java
rename to leanback/src/android/support/v17/leanback/app/VideoSupportFragmentGlueHost.java
diff --git a/v17/leanback/src/android/support/v17/leanback/app/package-info.java b/leanback/src/android/support/v17/leanback/app/package-info.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/app/package-info.java
rename to leanback/src/android/support/v17/leanback/app/package-info.java
diff --git a/v17/leanback/src/android/support/v17/leanback/database/CursorMapper.java b/leanback/src/android/support/v17/leanback/database/CursorMapper.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/database/CursorMapper.java
rename to leanback/src/android/support/v17/leanback/database/CursorMapper.java
diff --git a/v17/leanback/src/android/support/v17/leanback/graphics/BoundsRule.java b/leanback/src/android/support/v17/leanback/graphics/BoundsRule.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/graphics/BoundsRule.java
rename to leanback/src/android/support/v17/leanback/graphics/BoundsRule.java
diff --git a/v17/leanback/src/android/support/v17/leanback/graphics/ColorFilterCache.java b/leanback/src/android/support/v17/leanback/graphics/ColorFilterCache.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/graphics/ColorFilterCache.java
rename to leanback/src/android/support/v17/leanback/graphics/ColorFilterCache.java
diff --git a/v17/leanback/src/android/support/v17/leanback/graphics/ColorFilterDimmer.java b/leanback/src/android/support/v17/leanback/graphics/ColorFilterDimmer.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/graphics/ColorFilterDimmer.java
rename to leanback/src/android/support/v17/leanback/graphics/ColorFilterDimmer.java
diff --git a/v17/leanback/src/android/support/v17/leanback/graphics/ColorOverlayDimmer.java b/leanback/src/android/support/v17/leanback/graphics/ColorOverlayDimmer.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/graphics/ColorOverlayDimmer.java
rename to leanback/src/android/support/v17/leanback/graphics/ColorOverlayDimmer.java
diff --git a/v17/leanback/src/android/support/v17/leanback/graphics/CompositeDrawable.java b/leanback/src/android/support/v17/leanback/graphics/CompositeDrawable.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/graphics/CompositeDrawable.java
rename to leanback/src/android/support/v17/leanback/graphics/CompositeDrawable.java
diff --git a/v17/leanback/src/android/support/v17/leanback/graphics/FitWidthBitmapDrawable.java b/leanback/src/android/support/v17/leanback/graphics/FitWidthBitmapDrawable.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/graphics/FitWidthBitmapDrawable.java
rename to leanback/src/android/support/v17/leanback/graphics/FitWidthBitmapDrawable.java
diff --git a/v17/leanback/src/android/support/v17/leanback/media/MediaControllerAdapter.java b/leanback/src/android/support/v17/leanback/media/MediaControllerAdapter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/media/MediaControllerAdapter.java
rename to leanback/src/android/support/v17/leanback/media/MediaControllerAdapter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/media/MediaControllerGlue.java b/leanback/src/android/support/v17/leanback/media/MediaControllerGlue.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/media/MediaControllerGlue.java
rename to leanback/src/android/support/v17/leanback/media/MediaControllerGlue.java
diff --git a/v17/leanback/src/android/support/v17/leanback/media/MediaPlayerAdapter.java b/leanback/src/android/support/v17/leanback/media/MediaPlayerAdapter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/media/MediaPlayerAdapter.java
rename to leanback/src/android/support/v17/leanback/media/MediaPlayerAdapter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/media/MediaPlayerGlue.java b/leanback/src/android/support/v17/leanback/media/MediaPlayerGlue.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/media/MediaPlayerGlue.java
rename to leanback/src/android/support/v17/leanback/media/MediaPlayerGlue.java
diff --git a/v17/leanback/src/android/support/v17/leanback/media/PlaybackBannerControlGlue.java b/leanback/src/android/support/v17/leanback/media/PlaybackBannerControlGlue.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/media/PlaybackBannerControlGlue.java
rename to leanback/src/android/support/v17/leanback/media/PlaybackBannerControlGlue.java
diff --git a/v17/leanback/src/android/support/v17/leanback/media/PlaybackBaseControlGlue.java b/leanback/src/android/support/v17/leanback/media/PlaybackBaseControlGlue.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/media/PlaybackBaseControlGlue.java
rename to leanback/src/android/support/v17/leanback/media/PlaybackBaseControlGlue.java
diff --git a/v17/leanback/src/android/support/v17/leanback/media/PlaybackControlGlue.java b/leanback/src/android/support/v17/leanback/media/PlaybackControlGlue.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/media/PlaybackControlGlue.java
rename to leanback/src/android/support/v17/leanback/media/PlaybackControlGlue.java
diff --git a/v17/leanback/src/android/support/v17/leanback/media/PlaybackGlue.java b/leanback/src/android/support/v17/leanback/media/PlaybackGlue.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/media/PlaybackGlue.java
rename to leanback/src/android/support/v17/leanback/media/PlaybackGlue.java
diff --git a/v17/leanback/src/android/support/v17/leanback/media/PlaybackGlueHost.java b/leanback/src/android/support/v17/leanback/media/PlaybackGlueHost.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/media/PlaybackGlueHost.java
rename to leanback/src/android/support/v17/leanback/media/PlaybackGlueHost.java
diff --git a/v17/leanback/src/android/support/v17/leanback/media/PlaybackTransportControlGlue.java b/leanback/src/android/support/v17/leanback/media/PlaybackTransportControlGlue.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/media/PlaybackTransportControlGlue.java
rename to leanback/src/android/support/v17/leanback/media/PlaybackTransportControlGlue.java
diff --git a/v17/leanback/src/android/support/v17/leanback/media/PlayerAdapter.java b/leanback/src/android/support/v17/leanback/media/PlayerAdapter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/media/PlayerAdapter.java
rename to leanback/src/android/support/v17/leanback/media/PlayerAdapter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/media/SurfaceHolderGlueHost.java b/leanback/src/android/support/v17/leanback/media/SurfaceHolderGlueHost.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/media/SurfaceHolderGlueHost.java
rename to leanback/src/android/support/v17/leanback/media/SurfaceHolderGlueHost.java
diff --git a/v17/leanback/src/android/support/v17/leanback/package-info.java b/leanback/src/android/support/v17/leanback/package-info.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/package-info.java
rename to leanback/src/android/support/v17/leanback/package-info.java
diff --git a/v17/leanback/src/android/support/v17/leanback/system/Settings.java b/leanback/src/android/support/v17/leanback/system/Settings.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/system/Settings.java
rename to leanback/src/android/support/v17/leanback/system/Settings.java
diff --git a/v17/leanback/src/android/support/v17/leanback/transition/LeanbackTransitionHelper.java b/leanback/src/android/support/v17/leanback/transition/LeanbackTransitionHelper.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/transition/LeanbackTransitionHelper.java
rename to leanback/src/android/support/v17/leanback/transition/LeanbackTransitionHelper.java
diff --git a/v17/leanback/src/android/support/v17/leanback/transition/ParallaxTransition.java b/leanback/src/android/support/v17/leanback/transition/ParallaxTransition.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/transition/ParallaxTransition.java
rename to leanback/src/android/support/v17/leanback/transition/ParallaxTransition.java
diff --git a/v17/leanback/src/android/support/v17/leanback/transition/TransitionHelper.java b/leanback/src/android/support/v17/leanback/transition/TransitionHelper.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/transition/TransitionHelper.java
rename to leanback/src/android/support/v17/leanback/transition/TransitionHelper.java
diff --git a/v17/leanback/src/android/support/v17/leanback/util/MathUtil.java b/leanback/src/android/support/v17/leanback/util/MathUtil.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/util/MathUtil.java
rename to leanback/src/android/support/v17/leanback/util/MathUtil.java
diff --git a/v17/leanback/src/android/support/v17/leanback/util/StateMachine.java b/leanback/src/android/support/v17/leanback/util/StateMachine.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/util/StateMachine.java
rename to leanback/src/android/support/v17/leanback/util/StateMachine.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/AbstractDetailsDescriptionPresenter.java b/leanback/src/android/support/v17/leanback/widget/AbstractDetailsDescriptionPresenter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/AbstractDetailsDescriptionPresenter.java
rename to leanback/src/android/support/v17/leanback/widget/AbstractDetailsDescriptionPresenter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/AbstractMediaItemPresenter.java b/leanback/src/android/support/v17/leanback/widget/AbstractMediaItemPresenter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/AbstractMediaItemPresenter.java
rename to leanback/src/android/support/v17/leanback/widget/AbstractMediaItemPresenter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/AbstractMediaListHeaderPresenter.java b/leanback/src/android/support/v17/leanback/widget/AbstractMediaListHeaderPresenter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/AbstractMediaListHeaderPresenter.java
rename to leanback/src/android/support/v17/leanback/widget/AbstractMediaListHeaderPresenter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/Action.java b/leanback/src/android/support/v17/leanback/widget/Action.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/Action.java
rename to leanback/src/android/support/v17/leanback/widget/Action.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ActionPresenterSelector.java b/leanback/src/android/support/v17/leanback/widget/ActionPresenterSelector.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ActionPresenterSelector.java
rename to leanback/src/android/support/v17/leanback/widget/ActionPresenterSelector.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ArrayObjectAdapter.java b/leanback/src/android/support/v17/leanback/widget/ArrayObjectAdapter.java
similarity index 87%
rename from v17/leanback/src/android/support/v17/leanback/widget/ArrayObjectAdapter.java
rename to leanback/src/android/support/v17/leanback/widget/ArrayObjectAdapter.java
index 00bc073..2dcf51f 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/ArrayObjectAdapter.java
+++ b/leanback/src/android/support/v17/leanback/widget/ArrayObjectAdapter.java
@@ -225,6 +225,8 @@
         return true;
     }
 
+    ListUpdateCallback mListUpdateCallback;
+
     /**
      * Set a new item list to adapter. The DiffUtil will compute the difference and dispatch it to
      * specified position.
@@ -280,39 +282,43 @@
         mItems.addAll(itemList);
 
         // dispatch diff result
-        diffResult.dispatchUpdatesTo(new ListUpdateCallback() {
+        if (mListUpdateCallback == null) {
+            mListUpdateCallback = new ListUpdateCallback() {
 
-            @Override
-            public void onInserted(int position, int count) {
-                if (DEBUG) {
-                    Log.d(TAG, "onInserted");
+                @Override
+                public void onInserted(int position, int count) {
+                    if (DEBUG) {
+                        Log.d(TAG, "onInserted");
+                    }
+                    notifyItemRangeInserted(position, count);
                 }
-                notifyItemRangeInserted(position, count);
-            }
 
-            @Override
-            public void onRemoved(int position, int count) {
-                if (DEBUG) {
-                    Log.d(TAG, "onRemoved");
+                @Override
+                public void onRemoved(int position, int count) {
+                    if (DEBUG) {
+                        Log.d(TAG, "onRemoved");
+                    }
+                    notifyItemRangeRemoved(position, count);
                 }
-                notifyItemRangeRemoved(position, count);
-            }
 
-            @Override
-            public void onMoved(int fromPosition, int toPosition) {
-                if (DEBUG) {
-                    Log.d(TAG, "onMoved");
+                @Override
+                public void onMoved(int fromPosition, int toPosition) {
+                    if (DEBUG) {
+                        Log.d(TAG, "onMoved");
+                    }
+                    notifyItemMoved(fromPosition, toPosition);
                 }
-                notifyItemMoved(fromPosition, toPosition);
-            }
 
-            @Override
-            public void onChanged(int position, int count, Object payload) {
-                if (DEBUG) {
-                    Log.d(TAG, "onChanged");
+                @Override
+                public void onChanged(int position, int count, Object payload) {
+                    if (DEBUG) {
+                        Log.d(TAG, "onChanged");
+                    }
+                    notifyItemRangeChanged(position, count, payload);
                 }
-                notifyItemRangeChanged(position, count, payload);
-            }
-        });
+            };
+        }
+        diffResult.dispatchUpdatesTo(mListUpdateCallback);
+        mOldItems.clear();
     }
 }
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/BackgroundHelper.java b/leanback/src/android/support/v17/leanback/widget/BackgroundHelper.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/BackgroundHelper.java
rename to leanback/src/android/support/v17/leanback/widget/BackgroundHelper.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/BaseCardView.java b/leanback/src/android/support/v17/leanback/widget/BaseCardView.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/BaseCardView.java
rename to leanback/src/android/support/v17/leanback/widget/BaseCardView.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/BaseGridView.java b/leanback/src/android/support/v17/leanback/widget/BaseGridView.java
similarity index 99%
rename from v17/leanback/src/android/support/v17/leanback/widget/BaseGridView.java
rename to leanback/src/android/support/v17/leanback/widget/BaseGridView.java
index f4e01c0..2ebec47 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/BaseGridView.java
+++ b/leanback/src/android/support/v17/leanback/widget/BaseGridView.java
@@ -1134,7 +1134,7 @@
     @Override
     public void scrollToPosition(int position) {
         // dont abort the animateOut() animation, just record the position
-        if (mLayoutManager.mIsSlidingChildViews) {
+        if (mLayoutManager.isSlidingChildViews()) {
             mLayoutManager.setSelectionWithSub(position, 0, 0);
             return;
         }
@@ -1144,7 +1144,7 @@
     @Override
     public void smoothScrollToPosition(int position) {
         // dont abort the animateOut() animation, just record the position
-        if (mLayoutManager.mIsSlidingChildViews) {
+        if (mLayoutManager.isSlidingChildViews()) {
             mLayoutManager.setSelectionWithSub(position, 0, 0);
             return;
         }
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/BaseOnItemViewClickedListener.java b/leanback/src/android/support/v17/leanback/widget/BaseOnItemViewClickedListener.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/BaseOnItemViewClickedListener.java
rename to leanback/src/android/support/v17/leanback/widget/BaseOnItemViewClickedListener.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/BaseOnItemViewSelectedListener.java b/leanback/src/android/support/v17/leanback/widget/BaseOnItemViewSelectedListener.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/BaseOnItemViewSelectedListener.java
rename to leanback/src/android/support/v17/leanback/widget/BaseOnItemViewSelectedListener.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/BrowseFrameLayout.java b/leanback/src/android/support/v17/leanback/widget/BrowseFrameLayout.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/BrowseFrameLayout.java
rename to leanback/src/android/support/v17/leanback/widget/BrowseFrameLayout.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/BrowseRowsFrameLayout.java b/leanback/src/android/support/v17/leanback/widget/BrowseRowsFrameLayout.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/BrowseRowsFrameLayout.java
rename to leanback/src/android/support/v17/leanback/widget/BrowseRowsFrameLayout.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/CheckableImageView.java b/leanback/src/android/support/v17/leanback/widget/CheckableImageView.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/CheckableImageView.java
rename to leanback/src/android/support/v17/leanback/widget/CheckableImageView.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ClassPresenterSelector.java b/leanback/src/android/support/v17/leanback/widget/ClassPresenterSelector.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ClassPresenterSelector.java
rename to leanback/src/android/support/v17/leanback/widget/ClassPresenterSelector.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ControlBar.java b/leanback/src/android/support/v17/leanback/widget/ControlBar.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ControlBar.java
rename to leanback/src/android/support/v17/leanback/widget/ControlBar.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ControlBarPresenter.java b/leanback/src/android/support/v17/leanback/widget/ControlBarPresenter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ControlBarPresenter.java
rename to leanback/src/android/support/v17/leanback/widget/ControlBarPresenter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ControlButtonPresenterSelector.java b/leanback/src/android/support/v17/leanback/widget/ControlButtonPresenterSelector.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ControlButtonPresenterSelector.java
rename to leanback/src/android/support/v17/leanback/widget/ControlButtonPresenterSelector.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/CursorObjectAdapter.java b/leanback/src/android/support/v17/leanback/widget/CursorObjectAdapter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/CursorObjectAdapter.java
rename to leanback/src/android/support/v17/leanback/widget/CursorObjectAdapter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/DetailsOverviewLogoPresenter.java b/leanback/src/android/support/v17/leanback/widget/DetailsOverviewLogoPresenter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/DetailsOverviewLogoPresenter.java
rename to leanback/src/android/support/v17/leanback/widget/DetailsOverviewLogoPresenter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/DetailsOverviewRow.java b/leanback/src/android/support/v17/leanback/widget/DetailsOverviewRow.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/DetailsOverviewRow.java
rename to leanback/src/android/support/v17/leanback/widget/DetailsOverviewRow.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/DetailsOverviewRowPresenter.java b/leanback/src/android/support/v17/leanback/widget/DetailsOverviewRowPresenter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/DetailsOverviewRowPresenter.java
rename to leanback/src/android/support/v17/leanback/widget/DetailsOverviewRowPresenter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/DetailsOverviewSharedElementHelper.java b/leanback/src/android/support/v17/leanback/widget/DetailsOverviewSharedElementHelper.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/DetailsOverviewSharedElementHelper.java
rename to leanback/src/android/support/v17/leanback/widget/DetailsOverviewSharedElementHelper.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/DetailsParallax.java b/leanback/src/android/support/v17/leanback/widget/DetailsParallax.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/DetailsParallax.java
rename to leanback/src/android/support/v17/leanback/widget/DetailsParallax.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/DetailsParallaxDrawable.java b/leanback/src/android/support/v17/leanback/widget/DetailsParallaxDrawable.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/DetailsParallaxDrawable.java
rename to leanback/src/android/support/v17/leanback/widget/DetailsParallaxDrawable.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/DiffCallback.java b/leanback/src/android/support/v17/leanback/widget/DiffCallback.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/DiffCallback.java
rename to leanback/src/android/support/v17/leanback/widget/DiffCallback.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/DividerPresenter.java b/leanback/src/android/support/v17/leanback/widget/DividerPresenter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/DividerPresenter.java
rename to leanback/src/android/support/v17/leanback/widget/DividerPresenter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/DividerRow.java b/leanback/src/android/support/v17/leanback/widget/DividerRow.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/DividerRow.java
rename to leanback/src/android/support/v17/leanback/widget/DividerRow.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/FacetProvider.java b/leanback/src/android/support/v17/leanback/widget/FacetProvider.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/FacetProvider.java
rename to leanback/src/android/support/v17/leanback/widget/FacetProvider.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/FacetProviderAdapter.java b/leanback/src/android/support/v17/leanback/widget/FacetProviderAdapter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/FacetProviderAdapter.java
rename to leanback/src/android/support/v17/leanback/widget/FacetProviderAdapter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/FocusHighlight.java b/leanback/src/android/support/v17/leanback/widget/FocusHighlight.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/FocusHighlight.java
rename to leanback/src/android/support/v17/leanback/widget/FocusHighlight.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/FocusHighlightHandler.java b/leanback/src/android/support/v17/leanback/widget/FocusHighlightHandler.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/FocusHighlightHandler.java
rename to leanback/src/android/support/v17/leanback/widget/FocusHighlightHandler.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/FocusHighlightHelper.java b/leanback/src/android/support/v17/leanback/widget/FocusHighlightHelper.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/FocusHighlightHelper.java
rename to leanback/src/android/support/v17/leanback/widget/FocusHighlightHelper.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ForegroundHelper.java b/leanback/src/android/support/v17/leanback/widget/ForegroundHelper.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ForegroundHelper.java
rename to leanback/src/android/support/v17/leanback/widget/ForegroundHelper.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/FragmentAnimationProvider.java b/leanback/src/android/support/v17/leanback/widget/FragmentAnimationProvider.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/FragmentAnimationProvider.java
rename to leanback/src/android/support/v17/leanback/widget/FragmentAnimationProvider.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/FullWidthDetailsOverviewRowPresenter.java b/leanback/src/android/support/v17/leanback/widget/FullWidthDetailsOverviewRowPresenter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/FullWidthDetailsOverviewRowPresenter.java
rename to leanback/src/android/support/v17/leanback/widget/FullWidthDetailsOverviewRowPresenter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/FullWidthDetailsOverviewSharedElementHelper.java b/leanback/src/android/support/v17/leanback/widget/FullWidthDetailsOverviewSharedElementHelper.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/FullWidthDetailsOverviewSharedElementHelper.java
rename to leanback/src/android/support/v17/leanback/widget/FullWidthDetailsOverviewSharedElementHelper.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/Grid.java b/leanback/src/android/support/v17/leanback/widget/Grid.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/Grid.java
rename to leanback/src/android/support/v17/leanback/widget/Grid.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java b/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java
similarity index 92%
rename from v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java
rename to leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java
index af37f77..d7020e9 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java
+++ b/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java
@@ -217,9 +217,9 @@
                 mFocusPosition = getTargetPosition();
             }
             if (hasFocus()) {
-                mInSelection = true;
+                mFlag |= PF_IN_SELECTION;
                 targetView.requestFocus();
-                mInSelection = false;
+                mFlag &= ~PF_IN_SELECTION;
             }
             dispatchChildSelected();
             dispatchChildSelectedAndPositioned();
@@ -320,9 +320,9 @@
                 }
             }
             if (newSelected != null && hasFocus()) {
-                mInSelection = true;
+                mFlag |= PF_IN_SELECTION;
                 newSelected.requestFocus();
-                mInSelection = false;
+                mFlag &= ~PF_IN_SELECTION;
             }
         }
 
@@ -355,7 +355,8 @@
             if (mPendingMoves == 0) {
                 return null;
             }
-            int direction = (mReverseFlowPrimary ? mPendingMoves > 0 : mPendingMoves < 0)
+            int direction = ((mFlag & PF_REVERSE_FLOW_PRIMARY) != 0
+                    ? mPendingMoves > 0 : mPendingMoves < 0)
                     ? -1 : 1;
             if (mOrientation == HORIZONTAL) {
                 return new PointF(direction, 0);
@@ -386,10 +387,6 @@
     // effect smooth scrolling too over to bind an item view then drag the item view back.
     final static int MIN_MS_SMOOTH_SCROLL_MAIN_SCREEN = 30;
 
-    // Represents whether child views are temporarily sliding out
-    boolean mIsSlidingChildViews;
-    boolean mLayoutEatenInSliding;
-
     String getTag() {
         return TAG + ":" + mBaseGridView.getId();
     }
@@ -444,15 +441,101 @@
 
     private static final Rect sTempRect = new Rect();
 
-    boolean mInLayout;
-    private boolean mInScroll;
-    boolean mInFastRelayout;
+    // 2 bits mask is for 3 STAGEs: 0, PF_STAGE_LAYOUT or PF_STAGE_SCROLL.
+    static final int PF_STAGE_MASK = 0x3;
+    static final int PF_STAGE_LAYOUT = 0x1;
+    static final int PF_STAGE_SCROLL = 0x2;
+
+    // Flag for "in fast relayout", determined by layoutInit() result.
+    static final int PF_FAST_RELAYOUT = 1 << 2;
+
+    // Flag for the selected item being updated in fast relayout.
+    static final int PF_FAST_RELAYOUT_UPDATED_SELECTED_POSITION = 1 << 3;
     /**
      * During full layout pass, when GridView had focus: onLayoutChildren will
      * skip non-focusable child and adjust mFocusPosition.
      */
-    boolean mInLayoutSearchFocus;
-    boolean mInSelection = false;
+    static final int PF_IN_LAYOUT_SEARCH_FOCUS = 1 << 4;
+
+    // flag to prevent reentry if it's already processing selection request.
+    static final int PF_IN_SELECTION = 1 << 5;
+
+    // Represents whether child views are temporarily sliding out
+    static final int PF_SLIDING = 1 << 6;
+    static final int PF_LAYOUT_EATEN_IN_SLIDING = 1 << 7;
+
+    /**
+     * Force a full layout under certain situations.  E.g. Rows change, jump to invisible child.
+     */
+    static final int PF_FORCE_FULL_LAYOUT = 1 << 8;
+
+    /**
+     * True if layout is enabled.
+     */
+    static final int PF_LAYOUT_ENABLED = 1 << 9;
+
+    /**
+     * Flag controlling whether the current/next layout should
+     * be updating the secondary size of rows.
+     */
+    static final int PF_ROW_SECONDARY_SIZE_REFRESH = 1 << 10;
+
+    /**
+     *  Allow DPAD key to navigate out at the front of the View (where position = 0),
+     *  default is false.
+     */
+    static final int PF_FOCUS_OUT_FRONT = 1 << 11;
+
+    /**
+     * Allow DPAD key to navigate out at the end of the view, default is false.
+     */
+    static final int PF_FOCUS_OUT_END = 1 << 12;
+
+    static final int PF_FOCUS_OUT_MASKS = PF_FOCUS_OUT_FRONT | PF_FOCUS_OUT_END;
+
+    /**
+     *  Allow DPAD key to navigate out of second axis.
+     *  default is true.
+     */
+    static final int PF_FOCUS_OUT_SIDE_START = 1 << 13;
+
+    /**
+     * Allow DPAD key to navigate out of second axis.
+     */
+    static final int PF_FOCUS_OUT_SIDE_END = 1 << 14;
+
+    static final int PF_FOCUS_OUT_SIDE_MASKS = PF_FOCUS_OUT_SIDE_START | PF_FOCUS_OUT_SIDE_END;
+
+    /**
+     * True if focus search is disabled.
+     */
+    static final int PF_FOCUS_SEARCH_DISABLED = 1 << 15;
+
+    /**
+     * True if prune child,  might be disabled during transition.
+     */
+    static final int PF_PRUNE_CHILD = 1 << 16;
+
+    /**
+     * True if scroll content,  might be disabled during transition.
+     */
+    static final int PF_SCROLL_ENABLED = 1 << 17;
+
+    /**
+     * Set to true for RTL layout in horizontal orientation
+     */
+    static final int PF_REVERSE_FLOW_PRIMARY = 1 << 18;
+
+    /**
+     * Set to true for RTL layout in vertical orientation
+     */
+    static final int PF_REVERSE_FLOW_SECONDARY = 1 << 19;
+
+    static final int PF_REVERSE_FLOW_MASK = PF_REVERSE_FLOW_PRIMARY | PF_REVERSE_FLOW_SECONDARY;
+
+    int mFlag = PF_LAYOUT_ENABLED
+            | PF_FOCUS_OUT_SIDE_START | PF_FOCUS_OUT_SIDE_END
+            | PF_PRUNE_CHILD | PF_SCROLL_ENABLED;
 
     private OnChildSelectedListener mChildSelectedListener = null;
 
@@ -493,16 +576,6 @@
     private int mPrimaryScrollExtra;
 
     /**
-     * Force a full layout under certain situations.  E.g. Rows change, jump to invisible child.
-     */
-    private boolean mForceFullLayout;
-
-    /**
-     * True if layout is enabled.
-     */
-    private boolean mLayoutEnabled = true;
-
-    /**
      * override child visibility
      */
     @Visibility
@@ -535,12 +608,6 @@
     private int[] mRowSizeSecondary;
 
     /**
-     * Flag controlling whether the current/next layout should
-     * be updating the secondary size of rows.
-     */
-    private boolean mRowSecondarySizeRefresh;
-
-    /**
      * The maximum measured size of the view.
      */
     private int mMaxSizeSecondary;
@@ -605,58 +672,11 @@
     private int mExtraLayoutSpace;
 
     /**
-     *  Allow DPAD key to navigate out at the front of the View (where position = 0),
-     *  default is false.
-     */
-    private boolean mFocusOutFront;
-
-    /**
-     * Allow DPAD key to navigate out at the end of the view, default is false.
-     */
-    private boolean mFocusOutEnd;
-
-    /**
-     *  Allow DPAD key to navigate out of second axis.
-     *  default is true.
-     */
-    private boolean mFocusOutSideStart = true;
-
-    /**
-     * Allow DPAD key to navigate out of second axis.
-     */
-    private boolean mFocusOutSideEnd = true;
-
-    /**
-     * True if focus search is disabled.
-     */
-    private boolean mFocusSearchDisabled;
-
-    /**
-     * True if prune child,  might be disabled during transition.
-     */
-    private boolean mPruneChild = true;
-
-    /**
-     * True if scroll content,  might be disabled during transition.
-     */
-    private boolean mScrollEnabled = true;
-
-    /**
      * Temporary variable: an int array of length=2.
      */
     static int[] sTwoInts = new int[2];
 
     /**
-     * Set to true for RTL layout in horizontal orientation
-     */
-    boolean mReverseFlowPrimary = false;
-
-    /**
-     * Set to true for RTL layout in vertical orientation
-     */
-    private boolean mReverseFlowSecondary = false;
-
-    /**
      * Temporaries used for measuring.
      */
     private int[] mMeasuredDimension = new int[2];
@@ -685,24 +705,21 @@
         mOrientationHelper = OrientationHelper.createOrientationHelper(this, mOrientation);
         mWindowAlignment.setOrientation(orientation);
         mItemAlignment.setOrientation(orientation);
-        mForceFullLayout = true;
+        mFlag |= PF_FORCE_FULL_LAYOUT;
     }
 
     public void onRtlPropertiesChanged(int layoutDirection) {
-        boolean reversePrimary, reverseSecondary;
+        final int flags;
         if (mOrientation == HORIZONTAL) {
-            reversePrimary = layoutDirection == View.LAYOUT_DIRECTION_RTL;
-            reverseSecondary = false;
+            flags = layoutDirection == View.LAYOUT_DIRECTION_RTL ? PF_REVERSE_FLOW_PRIMARY : 0;
         } else {
-            reverseSecondary = layoutDirection == View.LAYOUT_DIRECTION_RTL;
-            reversePrimary = false;
+            flags = layoutDirection == View.LAYOUT_DIRECTION_RTL ? PF_REVERSE_FLOW_SECONDARY : 0;
         }
-        if (mReverseFlowPrimary == reversePrimary && mReverseFlowSecondary == reverseSecondary) {
+        if ((mFlag & PF_REVERSE_FLOW_MASK) == flags) {
             return;
         }
-        mReverseFlowPrimary = reversePrimary;
-        mReverseFlowSecondary = reverseSecondary;
-        mForceFullLayout = true;
+        mFlag = (mFlag & ~PF_REVERSE_FLOW_MASK) | flags;
+        mFlag |= PF_FORCE_FULL_LAYOUT;
         mWindowAlignment.horizontal.setReversedFlow(layoutDirection == View.LAYOUT_DIRECTION_RTL);
     }
 
@@ -775,13 +792,15 @@
     }
 
     public void setFocusOutAllowed(boolean throughFront, boolean throughEnd) {
-        mFocusOutFront = throughFront;
-        mFocusOutEnd = throughEnd;
+        mFlag = (mFlag & ~PF_FOCUS_OUT_MASKS)
+                | (throughFront ? PF_FOCUS_OUT_FRONT : 0)
+                | (throughEnd ? PF_FOCUS_OUT_END : 0);
     }
 
     public void setFocusOutSideAllowed(boolean throughStart, boolean throughEnd) {
-        mFocusOutSideStart = throughStart;
-        mFocusOutSideEnd = throughEnd;
+        mFlag = (mFlag & ~PF_FOCUS_OUT_SIDE_MASKS)
+                | (throughStart ? PF_FOCUS_OUT_SIDE_START : 0)
+                | (throughEnd ? PF_FOCUS_OUT_SIDE_END : 0);
     }
 
     public void setNumRows(int numRows) {
@@ -971,7 +990,7 @@
         // layout warning.
         // If not in layout, we may be scrolling in which case the child layout request will be
         // eaten by recyclerview.  Post a requestLayout.
-        if (!mInLayout && !mBaseGridView.isLayoutRequested()) {
+        if ((mFlag & PF_STAGE_MASK) != PF_STAGE_LAYOUT && !mBaseGridView.isLayoutRequested()) {
             int childCount = getChildCount();
             for (int i = 0; i < childCount; i++) {
                 if (getChildAt(i).isLayoutRequested()) {
@@ -1177,19 +1196,19 @@
             mSubFocusPosition = 0;
         }
         if (!mState.didStructureChange() && mGrid != null && mGrid.getFirstVisibleIndex() >= 0
-                && !mForceFullLayout && mGrid.getNumRows() == mNumRows) {
+                && (mFlag & PF_FORCE_FULL_LAYOUT) == 0 && mGrid.getNumRows() == mNumRows) {
             updateScrollController();
             updateSecondaryScrollLimits();
             mGrid.setSpacing(mSpacingPrimary);
             return true;
         } else {
-            mForceFullLayout = false;
+            mFlag &= ~PF_FORCE_FULL_LAYOUT;
 
             if (mGrid == null || mNumRows != mGrid.getNumRows()
-                    || mReverseFlowPrimary != mGrid.isReversedFlow()) {
+                    || ((mFlag & PF_REVERSE_FLOW_PRIMARY) != 0) != mGrid.isReversedFlow()) {
                 mGrid = Grid.createGrid(mNumRows);
                 mGrid.setProvider(mGridProvider);
-                mGrid.setReversedFlow(mReverseFlowPrimary);
+                mGrid.setReversedFlow((mFlag & PF_REVERSE_FLOW_PRIMARY) != 0);
             }
             initScrollController();
             updateSecondaryScrollLimits();
@@ -1216,7 +1235,7 @@
         int start = 0;
         // Iterate from left to right, which is a different index traversal
         // in RTL flow
-        if (mReverseFlowSecondary) {
+        if ((mFlag & PF_REVERSE_FLOW_PRIMARY) != 0) {
             for (int i = mNumRows-1; i > rowIndex; i--) {
                 start += getRowSizeSecondary(i) + mSpacingSecondary;
             }
@@ -1229,7 +1248,7 @@
     }
 
     private int getSizeSecondary() {
-        int rightmostIndex = mReverseFlowSecondary ? 0 : mNumRows - 1;
+        int rightmostIndex = (mFlag & PF_REVERSE_FLOW_SECONDARY) != 0 ? 0 : mNumRows - 1;
         return getRowStartSecondary(rightmostIndex) + getRowSizeSecondary(rightmostIndex);
     }
 
@@ -1366,8 +1385,9 @@
      * Checks if we need to update row secondary sizes.
      */
     private void updateRowSecondarySizeRefresh() {
-        mRowSecondarySizeRefresh = processRowSizeSecondary(false);
-        if (mRowSecondarySizeRefresh) {
+        mFlag = (mFlag & ~PF_ROW_SECONDARY_SIZE_REFRESH)
+                | (processRowSizeSecondary(false) ? PF_ROW_SECONDARY_SIZE_REFRESH : 0);
+        if ((mFlag & PF_ROW_SECONDARY_SIZE_REFRESH) != 0) {
             if (DEBUG) Log.v(getTag(), "mRowSecondarySizeRefresh now set");
             forceRequestLayout();
         }
@@ -1599,7 +1619,7 @@
                     mPendingMoveSmoothScroller.consumePendingMovesBeforeLayout();
                 }
                 int subindex = getSubPositionByView(v, v.findFocus());
-                if (!mInLayout) {
+                if ((mFlag & PF_STAGE_MASK) != PF_STAGE_LAYOUT) {
                     // when we are appending item during scroll pass and the item's position
                     // matches the mFocusPosition,  we should signal a childSelected event.
                     // However if we are still running PendingMoveSmoothScroller,  we defer and
@@ -1610,20 +1630,20 @@
                             && mPendingMoveSmoothScroller == null) {
                         dispatchChildSelected();
                     }
-                } else if (!mInFastRelayout) {
+                } else if ((mFlag & PF_FAST_RELAYOUT) == 0) {
                     // fastRelayout will dispatch event at end of onLayoutChildren().
                     // For full layout, two situations here:
                     // 1. mInLayoutSearchFocus is false, dispatchChildSelected() at mFocusPosition.
                     // 2. mInLayoutSearchFocus is true:  dispatchChildSelected() on first child
                     //    equal to or after mFocusPosition that can take focus.
-                    if (!mInLayoutSearchFocus && index == mFocusPosition
+                    if ((mFlag & PF_IN_LAYOUT_SEARCH_FOCUS) == 0 && index == mFocusPosition
                             && subindex == mSubFocusPosition) {
                         dispatchChildSelected();
-                    } else if (mInLayoutSearchFocus && index >= mFocusPosition
+                    } else if ((mFlag & PF_IN_LAYOUT_SEARCH_FOCUS) != 0 && index >= mFocusPosition
                             && v.hasFocusable()) {
                         mFocusPosition = index;
                         mSubFocusPosition = subindex;
-                        mInLayoutSearchFocus = false;
+                        mFlag &= ~PF_IN_LAYOUT_SEARCH_FOCUS;
                         dispatchChildSelected();
                     }
                 }
@@ -1663,7 +1683,7 @@
             if (!mState.isPreLayout()) {
                 updateScrollLimits();
             }
-            if (!mInLayout && mPendingMoveSmoothScroller != null) {
+            if ((mFlag & PF_STAGE_MASK) != PF_STAGE_LAYOUT && mPendingMoveSmoothScroller != null) {
                 mPendingMoveSmoothScroller.consumePendingMovesAfterLayout();
             }
             if (mChildLaidOutListener != null) {
@@ -1677,7 +1697,7 @@
         public void removeItem(int index) {
             if (TRACE) TraceCompat.beginSection("removeItem");
             View v = findViewByPosition(index - mPositionDeltaInPreLayout);
-            if (mInLayout) {
+            if ((mFlag & PF_STAGE_MASK) == PF_STAGE_LAYOUT) {
                 detachAndScrapView(v, mRecycler);
             } else {
                 removeAndRecycleView(v, mRecycler);
@@ -1688,7 +1708,7 @@
         @Override
         public int getEdge(int index) {
             View v = findViewByPosition(index - mPositionDeltaInPreLayout);
-            return mReverseFlowPrimary ? getViewMax(v) : getViewMin(v);
+            return (mFlag & PF_REVERSE_FLOW_PRIMARY) != 0 ? getViewMax(v) : getViewMin(v);
         }
 
         @Override
@@ -1705,7 +1725,7 @@
             sizeSecondary = Math.min(sizeSecondary, mFixedRowSizeSecondary);
         }
         final int verticalGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
-        final int horizontalGravity = (mReverseFlowPrimary || mReverseFlowSecondary)
+        final int horizontalGravity = (mFlag & PF_REVERSE_FLOW_MASK) != 0
                 ? Gravity.getAbsoluteGravity(mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK,
                 View.LAYOUT_DIRECTION_RTL)
                 : mGravity & Gravity.HORIZONTAL_GRAVITY_MASK;
@@ -1781,16 +1801,16 @@
     }
 
     private void removeInvisibleViewsAtEnd() {
-        if (mPruneChild && !mIsSlidingChildViews) {
-            mGrid.removeInvisibleItemsAtEnd(mFocusPosition,
-                    mReverseFlowPrimary ? -mExtraLayoutSpace : mSizePrimary + mExtraLayoutSpace);
+        if ((mFlag & (PF_PRUNE_CHILD | PF_SLIDING)) == PF_PRUNE_CHILD) {
+            mGrid.removeInvisibleItemsAtEnd(mFocusPosition, (mFlag & PF_REVERSE_FLOW_PRIMARY) != 0
+                    ? -mExtraLayoutSpace : mSizePrimary + mExtraLayoutSpace);
         }
     }
 
     private void removeInvisibleViewsAtFront() {
-        if (mPruneChild && !mIsSlidingChildViews) {
-            mGrid.removeInvisibleItemsAtFront(mFocusPosition,
-                    mReverseFlowPrimary ? mSizePrimary + mExtraLayoutSpace: -mExtraLayoutSpace);
+        if ((mFlag & (PF_PRUNE_CHILD | PF_SLIDING)) == PF_PRUNE_CHILD) {
+            mGrid.removeInvisibleItemsAtFront(mFocusPosition, (mFlag & PF_REVERSE_FLOW_PRIMARY) != 0
+                    ? mSizePrimary + mExtraLayoutSpace : -mExtraLayoutSpace);
         }
     }
 
@@ -1799,16 +1819,16 @@
     }
 
     void slideIn() {
-        if (mIsSlidingChildViews) {
-            mIsSlidingChildViews = false;
+        if ((mFlag & PF_SLIDING) != 0) {
+            mFlag &= ~PF_SLIDING;
             if (mFocusPosition >= 0) {
                 scrollToSelection(mFocusPosition, mSubFocusPosition, true, mPrimaryScrollExtra);
             } else {
-                mLayoutEatenInSliding = false;
+                mFlag &= ~PF_LAYOUT_EATEN_IN_SLIDING;
                 requestLayout();
             }
-            if (mLayoutEatenInSliding) {
-                mLayoutEatenInSliding = false;
+            if ((mFlag & PF_LAYOUT_EATEN_IN_SLIDING) != 0) {
+                mFlag &= ~PF_LAYOUT_EATEN_IN_SLIDING;
                 if (mBaseGridView.getScrollState() != SCROLL_STATE_IDLE || isSmoothScrolling()) {
                     mBaseGridView.addOnScrollListener(new RecyclerView.OnScrollListener() {
                         @Override
@@ -1838,7 +1858,7 @@
                 }
             }
         } else {
-            if (mReverseFlowPrimary) {
+            if ((mFlag & PF_REVERSE_FLOW_PRIMARY) != 0) {
                 distance = getWidth();
                 if (getChildCount() > 0) {
                     int start = getChildAt(0).getRight();
@@ -1861,14 +1881,18 @@
         return distance;
     }
 
+    boolean isSlidingChildViews() {
+        return (mFlag & PF_SLIDING) != 0;
+    }
+
     /**
      * Temporarily slide out child and block layout and scroll requests.
      */
     void slideOut() {
-        if (mIsSlidingChildViews) {
+        if ((mFlag & PF_SLIDING) != 0) {
             return;
         }
-        mIsSlidingChildViews = true;
+        mFlag |= PF_SLIDING;
         if (getChildCount() == 0) {
             return;
         }
@@ -1886,13 +1910,13 @@
     }
 
     private void appendVisibleItems() {
-        mGrid.appendVisibleItems(mReverseFlowPrimary
+        mGrid.appendVisibleItems((mFlag & PF_REVERSE_FLOW_PRIMARY) != 0
                 ? -mExtraLayoutSpace - mExtraLayoutSpaceInPreLayout
                 : mSizePrimary + mExtraLayoutSpace + mExtraLayoutSpaceInPreLayout);
     }
 
     private void prependVisibleItems() {
-        mGrid.prependVisibleItems(mReverseFlowPrimary
+        mGrid.prependVisibleItems((mFlag & PF_REVERSE_FLOW_PRIMARY) != 0
                 ? mSizePrimary + mExtraLayoutSpace + mExtraLayoutSpaceInPreLayout
                 : -mExtraLayoutSpace - mExtraLayoutSpaceInPreLayout);
     }
@@ -1907,6 +1931,7 @@
         final int childCount = getChildCount();
         int position = mGrid.getFirstVisibleIndex();
         int index = 0;
+        mFlag &= ~PF_FAST_RELAYOUT_UPDATED_SELECTED_POSITION;
         for (; index < childCount; index++, position++) {
             View view = getChildAt(index);
             // We don't hit fastRelayout() if State.didStructure() is true, but prelayout may add
@@ -1932,6 +1957,7 @@
 
             LayoutParams lp = (LayoutParams) view.getLayoutParams();
             if (lp.viewNeedsUpdate()) {
+                mFlag |= PF_FAST_RELAYOUT_UPDATED_SELECTED_POSITION;
                 detachAndScrapView(view, mRecycler);
                 view = getViewForPosition(position);
                 addView(view, index);
@@ -1960,7 +1986,7 @@
                 detachAndScrapView(v, mRecycler);
             }
             mGrid.invalidateItemsAfter(position);
-            if (mPruneChild) {
+            if ((mFlag & PF_PRUNE_CHILD) != 0) {
                 // in regular prune child mode, we just append items up to edge limit
                 appendVisibleItems();
                 if (mFocusPosition >= 0 && mFocusPosition <= savedLastPos) {
@@ -2108,7 +2134,7 @@
             Log.v(getTag(), "layoutChildren start numRows " + mNumRows
                     + " inPreLayout " + state.isPreLayout()
                     + " didStructureChange " + state.didStructureChange()
-                    + " mForceFullLayout " + mForceFullLayout);
+                    + " mForceFullLayout " + ((mFlag & PF_FORCE_FULL_LAYOUT) != 0));
             Log.v(getTag(), "width " + getWidth() + " height " + getHeight());
         }
 
@@ -2121,20 +2147,20 @@
             return;
         }
 
-        if (mIsSlidingChildViews) {
+        if ((mFlag & PF_SLIDING) != 0) {
             // if there is already children, delay the layout process until slideIn(), if it's
             // first time layout children: scroll them offscreen at end of onLayoutChildren()
             if (getChildCount() > 0) {
-                mLayoutEatenInSliding = true;
+                mFlag |= PF_LAYOUT_EATEN_IN_SLIDING;
                 return;
             }
         }
-        if (!mLayoutEnabled) {
+        if ((mFlag & PF_LAYOUT_ENABLED) == 0) {
             discardLayoutInfo();
             removeAndRecycleAllViews(recycler);
             return;
         }
-        mInLayout = true;
+        mFlag = (mFlag & ~PF_STAGE_MASK) | PF_STAGE_LAYOUT;
 
         saveContext(recycler, state);
         if (state.isPreLayout()) {
@@ -2172,7 +2198,7 @@
                 appendVisibleItems();
                 prependVisibleItems();
             }
-            mInLayout = false;
+            mFlag &= ~PF_STAGE_MASK;
             leaveContext();
             if (DEBUG) Log.v(getTag(), "layoutChildren end");
             return;
@@ -2206,13 +2232,16 @@
             deltaSecondary = state.getRemainingScrollHorizontal();
             deltaPrimary = state.getRemainingScrollVertical();
         }
-        if (mInFastRelayout = layoutInit()) {
+        if (layoutInit()) {
+            mFlag |= PF_FAST_RELAYOUT;
             // If grid view is empty, we will start from mFocusPosition
             mGrid.setStart(mFocusPosition);
             fastRelayout();
         } else {
+            mFlag &= ~PF_FAST_RELAYOUT;
             // layoutInit() has detached all views, so start from scratch
-            mInLayoutSearchFocus = hadFocus;
+            mFlag = (mFlag & ~PF_IN_LAYOUT_SEARCH_FOCUS)
+                    | (hadFocus ? PF_IN_LAYOUT_SEARCH_FOCUS : 0);
             int startFromPosition, endPos;
             if (scrollToFocus && (firstVisibleIndex < 0 || mFocusPosition > lastVisibleIndex
                     || mFocusPosition < firstVisibleIndex)) {
@@ -2270,27 +2299,30 @@
             Log.d(getTag(), sw.toString());
         }
 
-        if (mRowSecondarySizeRefresh) {
-            mRowSecondarySizeRefresh = false;
+        if ((mFlag & PF_ROW_SECONDARY_SIZE_REFRESH) != 0) {
+            mFlag &= ~PF_ROW_SECONDARY_SIZE_REFRESH;
         } else {
             updateRowSecondarySizeRefresh();
         }
 
-        // For fastRelayout, only dispatch event when focus position changes.
-        if (mInFastRelayout && (mFocusPosition != savedFocusPos || mSubFocusPosition
-                != savedSubFocusPos || findViewByPosition(mFocusPosition) != savedFocusView)) {
+        // For fastRelayout, only dispatch event when focus position changes or selected item
+        // being updated.
+        if ((mFlag & PF_FAST_RELAYOUT) != 0 && (mFocusPosition != savedFocusPos || mSubFocusPosition
+                != savedSubFocusPos || findViewByPosition(mFocusPosition) != savedFocusView
+                || (mFlag & PF_FAST_RELAYOUT_UPDATED_SELECTED_POSITION) != 0)) {
             dispatchChildSelected();
-        } else if (!mInFastRelayout && mInLayoutSearchFocus) {
+        } else if ((mFlag & (PF_FAST_RELAYOUT | PF_IN_LAYOUT_SEARCH_FOCUS))
+                == PF_IN_LAYOUT_SEARCH_FOCUS) {
             // For full layout we dispatchChildSelected() in createItem() unless searched all
             // children and found none is focusable then dispatchChildSelected() here.
             dispatchChildSelected();
         }
         dispatchChildSelectedAndPositioned();
-        if (mIsSlidingChildViews) {
+        if ((mFlag & PF_SLIDING) != 0) {
             scrollDirectionPrimary(getSlideOutDistance());
         }
 
-        mInLayout = false;
+        mFlag &= ~PF_STAGE_MASK;
         leaveContext();
         if (DEBUG) Log.v(getTag(), "layoutChildren end");
     }
@@ -2324,11 +2356,11 @@
     @Override
     public int scrollHorizontallyBy(int dx, Recycler recycler, RecyclerView.State state) {
         if (DEBUG) Log.v(getTag(), "scrollHorizontallyBy " + dx);
-        if (!mLayoutEnabled || !hasDoneFirstLayout()) {
+        if ((mFlag & PF_LAYOUT_ENABLED) == 0 || !hasDoneFirstLayout()) {
             return 0;
         }
         saveContext(recycler, state);
-        mInScroll = true;
+        mFlag = (mFlag & ~PF_STAGE_MASK) | PF_STAGE_SCROLL;
         int result;
         if (mOrientation == HORIZONTAL) {
             result = scrollDirectionPrimary(dx);
@@ -2336,17 +2368,17 @@
             result = scrollDirectionSecondary(dx);
         }
         leaveContext();
-        mInScroll = false;
+        mFlag &= ~PF_STAGE_MASK;
         return result;
     }
 
     @Override
     public int scrollVerticallyBy(int dy, Recycler recycler, RecyclerView.State state) {
         if (DEBUG) Log.v(getTag(), "scrollVerticallyBy " + dy);
-        if (!mLayoutEnabled || !hasDoneFirstLayout()) {
+        if ((mFlag & PF_LAYOUT_ENABLED) == 0 || !hasDoneFirstLayout()) {
             return 0;
         }
-        mInScroll = true;
+        mFlag = (mFlag & ~PF_STAGE_MASK) | PF_STAGE_SCROLL;
         saveContext(recycler, state);
         int result;
         if (mOrientation == VERTICAL) {
@@ -2355,7 +2387,7 @@
             result = scrollDirectionSecondary(dy);
         }
         leaveContext();
-        mInScroll = false;
+        mFlag &= ~PF_STAGE_MASK;
         return result;
     }
 
@@ -2367,7 +2399,7 @@
         // 2. During onLayoutChildren(), it may compensate the remaining scroll delta,
         //    we should honor the request regardless if it goes over minScroll / maxScroll.
         //    (see b/64931938 testScrollAndRemove and testScrollAndRemoveSample1)
-        if (!mIsSlidingChildViews && !mInLayout) {
+        if ((mFlag & PF_SLIDING) == 0 && (mFlag & PF_STAGE_MASK) != PF_STAGE_LAYOUT) {
             if (da > 0) {
                 if (!mWindowAlignment.mainAxis().isMaxUnknown()) {
                     int maxScroll = mWindowAlignment.mainAxis().getMaxScroll();
@@ -2389,7 +2421,7 @@
             return 0;
         }
         offsetChildrenPrimary(-da);
-        if (mInLayout) {
+        if ((mFlag & PF_STAGE_MASK) == PF_STAGE_LAYOUT) {
             updateScrollLimits();
             if (TRACE) TraceCompat.endSection();
             return da;
@@ -2398,7 +2430,7 @@
         int childCount = getChildCount();
         boolean updated;
 
-        if (mReverseFlowPrimary ? da > 0 : da < 0) {
+        if ((mFlag & PF_REVERSE_FLOW_PRIMARY) != 0 ? da > 0 : da < 0) {
             prependVisibleItems();
         } else {
             appendVisibleItems();
@@ -2407,7 +2439,7 @@
         childCount = getChildCount();
 
         if (TRACE) TraceCompat.beginSection("remove");
-        if (mReverseFlowPrimary ? da > 0 : da < 0) {
+        if ((mFlag & PF_REVERSE_FLOW_PRIMARY) != 0 ? da > 0 : da < 0) {
             removeInvisibleViewsAtEnd();
         } else {
             removeInvisibleViewsAtFront();
@@ -2476,7 +2508,7 @@
         }
         int highVisiblePos, lowVisiblePos;
         int highMaxPos, lowMinPos;
-        if (!mReverseFlowPrimary) {
+        if ((mFlag & PF_REVERSE_FLOW_PRIMARY) == 0) {
             highVisiblePos = mGrid.getLastVisibleIndex();
             highMaxPos = mState.getItemCount() - 1;
             lowVisiblePos = mGrid.getFirstVisibleIndex();
@@ -2614,14 +2646,14 @@
         // scrollToView() is based on Adapter position. Only call scrollToView() when item
         // is still valid.
         if (view != null && getAdapterPositionByView(view) == position) {
-            mInSelection = true;
+            mFlag |= PF_IN_SELECTION;
             scrollToView(view, smooth);
-            mInSelection = false;
+            mFlag &= ~PF_IN_SELECTION;
         } else {
             mFocusPosition = position;
             mSubFocusPosition = subposition;
             mFocusPositionOffset = Integer.MIN_VALUE;
-            if (!mLayoutEnabled || mIsSlidingChildViews) {
+            if ((mFlag & PF_LAYOUT_ENABLED) == 0 || (mFlag & PF_SLIDING) != 0) {
                 return;
             }
             if (smooth) {
@@ -2637,7 +2669,7 @@
                     mSubFocusPosition = 0;
                 }
             } else {
-                mForceFullLayout = true;
+                mFlag |= PF_FORCE_FULL_LAYOUT;
                 requestLayout();
             }
         }
@@ -2654,7 +2686,8 @@
                 final int firstChildPos = getPosition(getChildAt(0));
                 // TODO We should be able to deduce direction from bounds of current and target
                 // focus, rather than making assumptions about positions and directionality
-                final boolean isStart = mReverseFlowPrimary ? targetPosition > firstChildPos
+                final boolean isStart = (mFlag & PF_REVERSE_FLOW_PRIMARY) != 0
+                        ? targetPosition > firstChildPos
                         : targetPosition < firstChildPos;
                 final int direction = isStart ? -1 : 1;
                 if (mOrientation == HORIZONTAL) {
@@ -2693,6 +2726,40 @@
         }
     }
 
+    // Observer is registered on Adapter to invalidate saved instance state
+    final RecyclerView.AdapterDataObserver mObServer = new RecyclerView.AdapterDataObserver() {
+        @Override
+        public void onChanged() {
+            mChildrenStates.clear();
+        }
+
+        @Override
+        public void onItemRangeChanged(int positionStart, int itemCount) {
+            if (DEBUG) {
+                Log.v(getTag(), "onItemRangeChanged positionStart "
+                        + positionStart + " itemCount " + itemCount);
+            }
+            for (int i = positionStart, end = positionStart + itemCount; i < end; i++) {
+                mChildrenStates.remove(i);
+            }
+        }
+
+        @Override
+        public void onItemRangeInserted(int positionStart, int itemCount) {
+            mChildrenStates.clear();
+        }
+
+        @Override
+        public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
+            mChildrenStates.clear();
+        }
+
+        @Override
+        public void onItemRangeRemoved(int positionStart, int itemCount) {
+            mChildrenStates.clear();
+        }
+    };
+
     @Override
     public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) {
         if (DEBUG) Log.v(getTag(), "onItemsAdded positionStart "
@@ -2704,14 +2771,12 @@
                 mFocusPositionOffset += itemCount;
             }
         }
-        mChildrenStates.clear();
     }
 
     @Override
     public void onItemsChanged(RecyclerView recyclerView) {
         if (DEBUG) Log.v(getTag(), "onItemsChanged");
         mFocusPositionOffset = 0;
-        mChildrenStates.clear();
     }
 
     @Override
@@ -2732,7 +2797,6 @@
                 }
             }
         }
-        mChildrenStates.clear();
     }
 
     @Override
@@ -2753,28 +2817,18 @@
                 mFocusPositionOffset += itemCount;
             }
         }
-        mChildrenStates.clear();
-    }
-
-    @Override
-    public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount) {
-        if (DEBUG) Log.v(getTag(), "onItemsUpdated positionStart "
-                + positionStart + " itemCount " + itemCount);
-        for (int i = positionStart, end = positionStart + itemCount; i < end; i++) {
-            mChildrenStates.remove(i);
-        }
     }
 
     @Override
     public boolean onRequestChildFocus(RecyclerView parent, View child, View focused) {
-        if (mFocusSearchDisabled) {
+        if ((mFlag & PF_FOCUS_SEARCH_DISABLED) != 0) {
             return true;
         }
         if (getAdapterPositionByView(child) == NO_POSITION) {
             // This is could be the last view in DISAPPEARING animation.
             return true;
         }
-        if (!mInLayout && !mInSelection && !mInScroll) {
+        if ((mFlag & (PF_STAGE_MASK | PF_IN_SELECTION)) == 0) {
             scrollToView(child, focused, true);
         }
         return true;
@@ -2844,7 +2898,7 @@
      */
     private void scrollToView(View view, View childView, boolean smooth, int extraDelta,
             int extraDeltaSecondary) {
-        if (mIsSlidingChildViews) {
+        if ((mFlag & PF_SLIDING) != 0) {
             return;
         }
         int newFocusPosition = getAdapterPositionByView(view);
@@ -2853,7 +2907,7 @@
             mFocusPosition = newFocusPosition;
             mSubFocusPosition = newSubFocusPosition;
             mFocusPositionOffset = 0;
-            if (!mInLayout) {
+            if ((mFlag & PF_STAGE_MASK) != PF_STAGE_LAYOUT) {
                 dispatchChildSelected();
             }
             if (mBaseGridView.isChildrenDrawingOrderEnabledInternal()) {
@@ -2868,7 +2922,7 @@
             // by setSelection())
             view.requestFocus();
         }
-        if (!mScrollEnabled && smooth) {
+        if ((mFlag & PF_SCROLL_ENABLED) == 0 && smooth) {
             return;
         }
         if (getScrollPosition(view, childView, sTwoInts)
@@ -2986,7 +3040,7 @@
     }
 
     private void scrollGrid(int scrollPrimary, int scrollSecondary, boolean smooth) {
-        if (mInLayout) {
+        if ((mFlag & PF_STAGE_MASK) == PF_STAGE_LAYOUT) {
             scrollDirectionPrimary(scrollPrimary);
             scrollDirectionSecondary(scrollSecondary);
         } else {
@@ -3009,22 +3063,23 @@
     }
 
     public void setPruneChild(boolean pruneChild) {
-        if (mPruneChild != pruneChild) {
-            mPruneChild = pruneChild;
-            if (mPruneChild) {
+        if (((mFlag & PF_PRUNE_CHILD) != 0) != pruneChild) {
+            mFlag = (mFlag & ~PF_PRUNE_CHILD) | (pruneChild ? PF_PRUNE_CHILD : 0);
+            if (pruneChild) {
                 requestLayout();
             }
         }
     }
 
     public boolean getPruneChild() {
-        return mPruneChild;
+        return (mFlag & PF_PRUNE_CHILD) != 0;
     }
 
     public void setScrollEnabled(boolean scrollEnabled) {
-        if (mScrollEnabled != scrollEnabled) {
-            mScrollEnabled = scrollEnabled;
-            if (mScrollEnabled && mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_ALIGNED
+        if (((mFlag & PF_SCROLL_ENABLED) != 0) != scrollEnabled) {
+            mFlag = (mFlag & ~PF_SCROLL_ENABLED) | (scrollEnabled ? PF_SCROLL_ENABLED : 0);
+            if (((mFlag & PF_SCROLL_ENABLED) != 0)
+                    && mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_ALIGNED
                     && mFocusPosition != NO_POSITION) {
                 scrollToSelection(mFocusPosition, mSubFocusPosition,
                         true, mPrimaryScrollExtra);
@@ -3033,7 +3088,7 @@
     }
 
     public boolean isScrollEnabled() {
-        return mScrollEnabled;
+        return (mFlag & PF_SCROLL_ENABLED) != 0;
     }
 
     private int findImmediateChildIndex(View view) {
@@ -3067,16 +3122,16 @@
     }
 
     void setFocusSearchDisabled(boolean disabled) {
-        mFocusSearchDisabled = disabled;
+        mFlag = (mFlag & ~PF_FOCUS_SEARCH_DISABLED) | (disabled ? PF_FOCUS_SEARCH_DISABLED : 0);
     }
 
     boolean isFocusSearchDisabled() {
-        return mFocusSearchDisabled;
+        return (mFlag & PF_FOCUS_SEARCH_DISABLED) != 0;
     }
 
     @Override
     public View onInterceptFocusSearch(View focused, int direction) {
-        if (mFocusSearchDisabled) {
+        if ((mFlag & PF_FOCUS_SEARCH_DISABLED) != 0) {
             return focused;
         }
 
@@ -3111,27 +3166,27 @@
         int movement = getMovement(direction);
         final boolean isScroll = mBaseGridView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE;
         if (movement == NEXT_ITEM) {
-            if (isScroll || !mFocusOutEnd) {
+            if (isScroll || (mFlag & PF_FOCUS_OUT_END) == 0) {
                 result = focused;
             }
-            if (mScrollEnabled && !hasCreatedLastItem()) {
+            if ((mFlag & PF_SCROLL_ENABLED) != 0 && !hasCreatedLastItem()) {
                 processPendingMovement(true);
                 result = focused;
             }
         } else if (movement == PREV_ITEM) {
-            if (isScroll || !mFocusOutFront) {
+            if (isScroll || (mFlag & PF_FOCUS_OUT_FRONT) == 0) {
                 result = focused;
             }
-            if (mScrollEnabled && !hasCreatedFirstItem()) {
+            if ((mFlag & PF_SCROLL_ENABLED) != 0 && !hasCreatedFirstItem()) {
                 processPendingMovement(false);
                 result = focused;
             }
         } else if (movement == NEXT_ROW) {
-            if (isScroll || !mFocusOutSideEnd) {
+            if (isScroll || (mFlag & PF_FOCUS_OUT_SIDE_END) == 0) {
                 result = focused;
             }
         } else if (movement == PREV_ROW) {
-            if (isScroll || !mFocusOutSideStart) {
+            if (isScroll || (mFlag & PF_FOCUS_OUT_SIDE_START) == 0) {
                 result = focused;
             }
         }
@@ -3170,7 +3225,7 @@
     @Override
     public boolean onAddFocusables(RecyclerView recyclerView,
             ArrayList<View> views, int direction, int focusableMode) {
-        if (mFocusSearchDisabled) {
+        if ((mFlag & PF_FOCUS_SEARCH_DISABLED) != 0) {
             return true;
         }
         // If this viewgroup or one of its children currently has focus then we
@@ -3402,10 +3457,10 @@
         if (mOrientation == HORIZONTAL) {
             switch(direction) {
                 case View.FOCUS_LEFT:
-                    movement = (!mReverseFlowPrimary) ? PREV_ITEM : NEXT_ITEM;
+                    movement = (mFlag & PF_REVERSE_FLOW_PRIMARY) == 0 ? PREV_ITEM : NEXT_ITEM;
                     break;
                 case View.FOCUS_RIGHT:
-                    movement = (!mReverseFlowPrimary) ? NEXT_ITEM : PREV_ITEM;
+                    movement = (mFlag & PF_REVERSE_FLOW_PRIMARY) == 0 ? NEXT_ITEM : PREV_ITEM;
                     break;
                 case View.FOCUS_UP:
                     movement = PREV_ROW;
@@ -3417,10 +3472,10 @@
         } else if (mOrientation == VERTICAL) {
             switch(direction) {
                 case View.FOCUS_LEFT:
-                    movement = (!mReverseFlowSecondary) ? PREV_ROW : NEXT_ROW;
+                    movement = (mFlag & PF_REVERSE_FLOW_SECONDARY) == 0 ? PREV_ROW : NEXT_ROW;
                     break;
                 case View.FOCUS_RIGHT:
-                    movement = (!mReverseFlowSecondary) ? NEXT_ROW : PREV_ROW;
+                    movement = (mFlag & PF_REVERSE_FLOW_SECONDARY) == 0 ? NEXT_ROW : PREV_ROW;
                     break;
                 case View.FOCUS_UP:
                     movement = PREV_ITEM;
@@ -3460,24 +3515,28 @@
             mFocusPosition = NO_POSITION;
             mFocusPositionOffset = 0;
             mChildrenStates.clear();
+            oldAdapter.unregisterAdapterDataObserver(mObServer);
         }
         if (newAdapter instanceof FacetProviderAdapter) {
             mFacetProviderAdapter = (FacetProviderAdapter) newAdapter;
         } else {
             mFacetProviderAdapter = null;
         }
+        if (newAdapter != null) {
+            newAdapter.registerAdapterDataObserver(mObServer);
+        }
         super.onAdapterChanged(oldAdapter, newAdapter);
     }
 
     private void discardLayoutInfo() {
         mGrid = null;
         mRowSizeSecondary = null;
-        mRowSecondarySizeRefresh = false;
+        mFlag &= ~PF_ROW_SECONDARY_SIZE_REFRESH;
     }
 
     public void setLayoutEnabled(boolean layoutEnabled) {
-        if (mLayoutEnabled != layoutEnabled) {
-            mLayoutEnabled = layoutEnabled;
+        if (((mFlag & PF_LAYOUT_ENABLED) != 0) != layoutEnabled) {
+            mFlag = (mFlag & ~PF_LAYOUT_ENABLED) | (layoutEnabled ? PF_LAYOUT_ENABLED : 0);
             requestLayout();
         }
     }
@@ -3567,7 +3626,7 @@
         mFocusPosition = loadingState.index;
         mFocusPositionOffset = 0;
         mChildrenStates.loadFromBundle(loadingState.childStates);
-        mForceFullLayout = true;
+        mFlag |= PF_FORCE_FULL_LAYOUT;
         requestLayout();
         if (DEBUG) Log.v(getTag(), "onRestoreInstanceState mFocusPosition " + mFocusPosition);
     }
@@ -3674,9 +3733,9 @@
         if (newSelected != null) {
             if (preventScroll) {
                 if (hasFocus()) {
-                    mInSelection = true;
+                    mFlag |= PF_IN_SELECTION;
                     newSelected.requestFocus();
-                    mInSelection = false;
+                    mFlag &= ~PF_IN_SELECTION;
                 }
                 mFocusPosition = focusPosition;
                 mSubFocusPosition = 0;
@@ -3692,11 +3751,11 @@
             AccessibilityNodeInfoCompat info) {
         saveContext(recycler, state);
         int count = state.getItemCount();
-        if (mScrollEnabled && count > 1 && !isItemFullyVisible(0)) {
+        if ((mFlag & PF_SCROLL_ENABLED) != 0 && count > 1 && !isItemFullyVisible(0)) {
             info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD);
             info.setScrollable(true);
         }
-        if (mScrollEnabled && count > 1 && !isItemFullyVisible(count - 1)) {
+        if ((mFlag & PF_SCROLL_ENABLED) != 0 && count > 1 && !isItemFullyVisible(count - 1)) {
             info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD);
             info.setScrollable(true);
         }
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/GuidanceStylingRelativeLayout.java b/leanback/src/android/support/v17/leanback/widget/GuidanceStylingRelativeLayout.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/GuidanceStylingRelativeLayout.java
rename to leanback/src/android/support/v17/leanback/widget/GuidanceStylingRelativeLayout.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/GuidanceStylist.java b/leanback/src/android/support/v17/leanback/widget/GuidanceStylist.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/GuidanceStylist.java
rename to leanback/src/android/support/v17/leanback/widget/GuidanceStylist.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/GuidedAction.java b/leanback/src/android/support/v17/leanback/widget/GuidedAction.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/GuidedAction.java
rename to leanback/src/android/support/v17/leanback/widget/GuidedAction.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/GuidedActionAdapter.java b/leanback/src/android/support/v17/leanback/widget/GuidedActionAdapter.java
similarity index 87%
rename from v17/leanback/src/android/support/v17/leanback/widget/GuidedActionAdapter.java
rename to leanback/src/android/support/v17/leanback/widget/GuidedActionAdapter.java
index 5b755f5..51b29e2 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/GuidedActionAdapter.java
+++ b/leanback/src/android/support/v17/leanback/widget/GuidedActionAdapter.java
@@ -15,7 +15,9 @@
 
 import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
 
+import android.support.annotation.Nullable;
 import android.support.annotation.RestrictTo;
+import android.support.v7.util.DiffUtil;
 import android.support.v7.widget.RecyclerView;
 import android.support.v7.widget.RecyclerView.ViewHolder;
 import android.util.Log;
@@ -103,6 +105,7 @@
     private ClickListener mClickListener;
     final GuidedActionsStylist mStylist;
     GuidedActionAdapterGroup mGroup;
+    DiffCallback<GuidedAction> mDiffCallback;
 
     private final View.OnClickListener mOnClickListener = new View.OnClickListener() {
         @Override
@@ -145,20 +148,78 @@
         mActionOnFocusListener = new ActionOnFocusListener(focusListener);
         mActionEditListener = new ActionEditListener();
         mIsSubAdapter = isSubAdapter;
+        if (!isSubAdapter) {
+            mDiffCallback = GuidedActionDiffCallback.getInstance();
+        }
     }
 
     /**
-     * Sets the list of actions managed by this adapter.
+     * Change DiffCallback used in {@link #setActions(List)}. Set to null for firing a
+     * general {@link #notifyDataSetChanged()}.
+     *
+     * @param diffCallback
+     */
+    public void setDiffCallback(DiffCallback<GuidedAction> diffCallback) {
+        mDiffCallback = diffCallback;
+    }
+
+    /**
+     * Sets the list of actions managed by this adapter. Use {@link #setDiffCallback(DiffCallback)}
+     * to change DiffCallback.
      * @param actions The list of actions to be managed.
      */
-    public void setActions(List<GuidedAction> actions) {
+    public void setActions(final List<GuidedAction> actions) {
         if (!mIsSubAdapter) {
             mStylist.collapseAction(false);
         }
         mActionOnFocusListener.unFocus();
-        mActions.clear();
-        mActions.addAll(actions);
-        notifyDataSetChanged();
+        if (mDiffCallback != null) {
+            // temporary variable used for DiffCallback
+            final List<GuidedAction> oldActions = new ArrayList();
+            oldActions.addAll(mActions);
+
+            // update items.
+            mActions.clear();
+            mActions.addAll(actions);
+
+            DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffUtil.Callback() {
+                @Override
+                public int getOldListSize() {
+                    return oldActions.size();
+                }
+
+                @Override
+                public int getNewListSize() {
+                    return mActions.size();
+                }
+
+                @Override
+                public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
+                    return mDiffCallback.areItemsTheSame(oldActions.get(oldItemPosition),
+                            mActions.get(newItemPosition));
+                }
+
+                @Override
+                public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
+                    return mDiffCallback.areContentsTheSame(oldActions.get(oldItemPosition),
+                            mActions.get(newItemPosition));
+                }
+
+                @Nullable
+                @Override
+                public Object getChangePayload(int oldItemPosition, int newItemPosition) {
+                    return mDiffCallback.getChangePayload(oldActions.get(oldItemPosition),
+                            mActions.get(newItemPosition));
+                }
+            });
+
+            // dispatch diff result
+            diffResult.dispatchUpdatesTo(this);
+        } else {
+            mActions.clear();
+            mActions.addAll(actions);
+            notifyDataSetChanged();
+        }
     }
 
     /**
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/GuidedActionAdapterGroup.java b/leanback/src/android/support/v17/leanback/widget/GuidedActionAdapterGroup.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/GuidedActionAdapterGroup.java
rename to leanback/src/android/support/v17/leanback/widget/GuidedActionAdapterGroup.java
diff --git a/leanback/src/android/support/v17/leanback/widget/GuidedActionDiffCallback.java b/leanback/src/android/support/v17/leanback/widget/GuidedActionDiffCallback.java
new file mode 100644
index 0000000..d4d4d77
--- /dev/null
+++ b/leanback/src/android/support/v17/leanback/widget/GuidedActionDiffCallback.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 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.v17.leanback.widget;
+
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+
+/**
+ * DiffCallback used for GuidedActions, see {@link
+ * android.support.v17.leanback.app.GuidedStepSupportFragment#setActionsDiffCallback(DiffCallback)}.
+ */
+public class GuidedActionDiffCallback extends DiffCallback<GuidedAction> {
+
+    static final GuidedActionDiffCallback sInstance = new GuidedActionDiffCallback();
+
+    /**
+     * Returns the singleton GuidedActionDiffCallback.
+     * @return The singleton GuidedActionDiffCallback.
+     */
+    public static final GuidedActionDiffCallback getInstance() {
+        return sInstance;
+    }
+
+    @Override
+    public boolean areItemsTheSame(@NonNull GuidedAction oldItem, @NonNull GuidedAction newItem) {
+        if (oldItem == null) {
+            return newItem == null;
+        } else if (newItem == null) {
+            return false;
+        }
+        return oldItem.getId() == newItem.getId();
+    }
+
+    @Override
+    public boolean areContentsTheSame(@NonNull GuidedAction oldItem,
+            @NonNull GuidedAction newItem) {
+        if (oldItem == null) {
+            return newItem == null;
+        } else if (newItem == null) {
+            return false;
+        }
+        return oldItem.getCheckSetId() == newItem.getCheckSetId()
+                && oldItem.mActionFlags == newItem.mActionFlags
+                && TextUtils.equals(oldItem.getTitle(), newItem.getTitle())
+                && TextUtils.equals(oldItem.getDescription(), newItem.getDescription())
+                && oldItem.getInputType() == newItem.getInputType()
+                && TextUtils.equals(oldItem.getEditTitle(), newItem.getEditTitle())
+                && TextUtils.equals(oldItem.getEditDescription(), newItem.getEditDescription())
+                && oldItem.getEditInputType() == newItem.getEditInputType()
+                && oldItem.getDescriptionEditInputType() == newItem.getDescriptionEditInputType();
+    }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/GuidedActionEditText.java b/leanback/src/android/support/v17/leanback/widget/GuidedActionEditText.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/GuidedActionEditText.java
rename to leanback/src/android/support/v17/leanback/widget/GuidedActionEditText.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/GuidedActionItemContainer.java b/leanback/src/android/support/v17/leanback/widget/GuidedActionItemContainer.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/GuidedActionItemContainer.java
rename to leanback/src/android/support/v17/leanback/widget/GuidedActionItemContainer.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/GuidedActionsRelativeLayout.java b/leanback/src/android/support/v17/leanback/widget/GuidedActionsRelativeLayout.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/GuidedActionsRelativeLayout.java
rename to leanback/src/android/support/v17/leanback/widget/GuidedActionsRelativeLayout.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/GuidedActionsStylist.java b/leanback/src/android/support/v17/leanback/widget/GuidedActionsStylist.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/GuidedActionsStylist.java
rename to leanback/src/android/support/v17/leanback/widget/GuidedActionsStylist.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/GuidedDatePickerAction.java b/leanback/src/android/support/v17/leanback/widget/GuidedDatePickerAction.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/GuidedDatePickerAction.java
rename to leanback/src/android/support/v17/leanback/widget/GuidedDatePickerAction.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/HeaderItem.java b/leanback/src/android/support/v17/leanback/widget/HeaderItem.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/HeaderItem.java
rename to leanback/src/android/support/v17/leanback/widget/HeaderItem.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/HorizontalGridView.java b/leanback/src/android/support/v17/leanback/widget/HorizontalGridView.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/HorizontalGridView.java
rename to leanback/src/android/support/v17/leanback/widget/HorizontalGridView.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/HorizontalHoverCardSwitcher.java b/leanback/src/android/support/v17/leanback/widget/HorizontalHoverCardSwitcher.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/HorizontalHoverCardSwitcher.java
rename to leanback/src/android/support/v17/leanback/widget/HorizontalHoverCardSwitcher.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ImageCardView.java b/leanback/src/android/support/v17/leanback/widget/ImageCardView.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ImageCardView.java
rename to leanback/src/android/support/v17/leanback/widget/ImageCardView.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ImeKeyMonitor.java b/leanback/src/android/support/v17/leanback/widget/ImeKeyMonitor.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ImeKeyMonitor.java
rename to leanback/src/android/support/v17/leanback/widget/ImeKeyMonitor.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/InvisibleRowPresenter.java b/leanback/src/android/support/v17/leanback/widget/InvisibleRowPresenter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/InvisibleRowPresenter.java
rename to leanback/src/android/support/v17/leanback/widget/InvisibleRowPresenter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ItemAlignment.java b/leanback/src/android/support/v17/leanback/widget/ItemAlignment.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ItemAlignment.java
rename to leanback/src/android/support/v17/leanback/widget/ItemAlignment.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ItemAlignmentFacet.java b/leanback/src/android/support/v17/leanback/widget/ItemAlignmentFacet.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ItemAlignmentFacet.java
rename to leanback/src/android/support/v17/leanback/widget/ItemAlignmentFacet.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ItemAlignmentFacetHelper.java b/leanback/src/android/support/v17/leanback/widget/ItemAlignmentFacetHelper.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ItemAlignmentFacetHelper.java
rename to leanback/src/android/support/v17/leanback/widget/ItemAlignmentFacetHelper.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ItemBridgeAdapter.java b/leanback/src/android/support/v17/leanback/widget/ItemBridgeAdapter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ItemBridgeAdapter.java
rename to leanback/src/android/support/v17/leanback/widget/ItemBridgeAdapter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ItemBridgeAdapterShadowOverlayWrapper.java b/leanback/src/android/support/v17/leanback/widget/ItemBridgeAdapterShadowOverlayWrapper.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ItemBridgeAdapterShadowOverlayWrapper.java
rename to leanback/src/android/support/v17/leanback/widget/ItemBridgeAdapterShadowOverlayWrapper.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ListRow.java b/leanback/src/android/support/v17/leanback/widget/ListRow.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ListRow.java
rename to leanback/src/android/support/v17/leanback/widget/ListRow.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ListRowHoverCardView.java b/leanback/src/android/support/v17/leanback/widget/ListRowHoverCardView.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ListRowHoverCardView.java
rename to leanback/src/android/support/v17/leanback/widget/ListRowHoverCardView.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ListRowPresenter.java b/leanback/src/android/support/v17/leanback/widget/ListRowPresenter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ListRowPresenter.java
rename to leanback/src/android/support/v17/leanback/widget/ListRowPresenter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ListRowView.java b/leanback/src/android/support/v17/leanback/widget/ListRowView.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ListRowView.java
rename to leanback/src/android/support/v17/leanback/widget/ListRowView.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/MediaItemActionPresenter.java b/leanback/src/android/support/v17/leanback/widget/MediaItemActionPresenter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/MediaItemActionPresenter.java
rename to leanback/src/android/support/v17/leanback/widget/MediaItemActionPresenter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/MediaNowPlayingView.java b/leanback/src/android/support/v17/leanback/widget/MediaNowPlayingView.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/MediaNowPlayingView.java
rename to leanback/src/android/support/v17/leanback/widget/MediaNowPlayingView.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/MediaRowFocusView.java b/leanback/src/android/support/v17/leanback/widget/MediaRowFocusView.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/MediaRowFocusView.java
rename to leanback/src/android/support/v17/leanback/widget/MediaRowFocusView.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/MultiActionsProvider.java b/leanback/src/android/support/v17/leanback/widget/MultiActionsProvider.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/MultiActionsProvider.java
rename to leanback/src/android/support/v17/leanback/widget/MultiActionsProvider.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/NonOverlappingFrameLayout.java b/leanback/src/android/support/v17/leanback/widget/NonOverlappingFrameLayout.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/NonOverlappingFrameLayout.java
rename to leanback/src/android/support/v17/leanback/widget/NonOverlappingFrameLayout.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/NonOverlappingLinearLayout.java b/leanback/src/android/support/v17/leanback/widget/NonOverlappingLinearLayout.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/NonOverlappingLinearLayout.java
rename to leanback/src/android/support/v17/leanback/widget/NonOverlappingLinearLayout.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/NonOverlappingLinearLayoutWithForeground.java b/leanback/src/android/support/v17/leanback/widget/NonOverlappingLinearLayoutWithForeground.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/NonOverlappingLinearLayoutWithForeground.java
rename to leanback/src/android/support/v17/leanback/widget/NonOverlappingLinearLayoutWithForeground.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/NonOverlappingRelativeLayout.java b/leanback/src/android/support/v17/leanback/widget/NonOverlappingRelativeLayout.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/NonOverlappingRelativeLayout.java
rename to leanback/src/android/support/v17/leanback/widget/NonOverlappingRelativeLayout.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/NonOverlappingView.java b/leanback/src/android/support/v17/leanback/widget/NonOverlappingView.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/NonOverlappingView.java
rename to leanback/src/android/support/v17/leanback/widget/NonOverlappingView.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ObjectAdapter.java b/leanback/src/android/support/v17/leanback/widget/ObjectAdapter.java
similarity index 96%
rename from v17/leanback/src/android/support/v17/leanback/widget/ObjectAdapter.java
rename to leanback/src/android/support/v17/leanback/widget/ObjectAdapter.java
index 535f81b..d411f9e 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/ObjectAdapter.java
+++ b/leanback/src/android/support/v17/leanback/widget/ObjectAdapter.java
@@ -13,7 +13,10 @@
  */
 package android.support.v17.leanback.widget;
 
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
 import android.database.Observable;
+import android.support.annotation.RestrictTo;
 
 /**
  * Base class adapter to be used in leanback activities.  Provides access to a data model and is
@@ -132,6 +135,10 @@
                 mObservers.get(i).onItemMoved(positionStart, toPosition);
             }
         }
+
+        boolean hasObserver() {
+            return mObservers.size() > 0;
+        }
     }
 
     private final DataObservable mObservable = new DataObservable();
@@ -207,6 +214,14 @@
     }
 
     /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public final boolean hasObserver() {
+        return mObservable.hasObserver();
+    }
+
+    /**
      * Unregisters all DataObservers for this ObjectAdapter.
      */
     public final void unregisterAllObservers() {
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/OnActionClickedListener.java b/leanback/src/android/support/v17/leanback/widget/OnActionClickedListener.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/OnActionClickedListener.java
rename to leanback/src/android/support/v17/leanback/widget/OnActionClickedListener.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/OnChildLaidOutListener.java b/leanback/src/android/support/v17/leanback/widget/OnChildLaidOutListener.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/OnChildLaidOutListener.java
rename to leanback/src/android/support/v17/leanback/widget/OnChildLaidOutListener.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/OnChildSelectedListener.java b/leanback/src/android/support/v17/leanback/widget/OnChildSelectedListener.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/OnChildSelectedListener.java
rename to leanback/src/android/support/v17/leanback/widget/OnChildSelectedListener.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/OnChildViewHolderSelectedListener.java b/leanback/src/android/support/v17/leanback/widget/OnChildViewHolderSelectedListener.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/OnChildViewHolderSelectedListener.java
rename to leanback/src/android/support/v17/leanback/widget/OnChildViewHolderSelectedListener.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/OnItemViewClickedListener.java b/leanback/src/android/support/v17/leanback/widget/OnItemViewClickedListener.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/OnItemViewClickedListener.java
rename to leanback/src/android/support/v17/leanback/widget/OnItemViewClickedListener.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/OnItemViewSelectedListener.java b/leanback/src/android/support/v17/leanback/widget/OnItemViewSelectedListener.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/OnItemViewSelectedListener.java
rename to leanback/src/android/support/v17/leanback/widget/OnItemViewSelectedListener.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/PageRow.java b/leanback/src/android/support/v17/leanback/widget/PageRow.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/PageRow.java
rename to leanback/src/android/support/v17/leanback/widget/PageRow.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/PagingIndicator.java b/leanback/src/android/support/v17/leanback/widget/PagingIndicator.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/PagingIndicator.java
rename to leanback/src/android/support/v17/leanback/widget/PagingIndicator.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/Parallax.java b/leanback/src/android/support/v17/leanback/widget/Parallax.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/Parallax.java
rename to leanback/src/android/support/v17/leanback/widget/Parallax.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ParallaxEffect.java b/leanback/src/android/support/v17/leanback/widget/ParallaxEffect.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ParallaxEffect.java
rename to leanback/src/android/support/v17/leanback/widget/ParallaxEffect.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ParallaxTarget.java b/leanback/src/android/support/v17/leanback/widget/ParallaxTarget.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ParallaxTarget.java
rename to leanback/src/android/support/v17/leanback/widget/ParallaxTarget.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/PersistentFocusWrapper.java b/leanback/src/android/support/v17/leanback/widget/PersistentFocusWrapper.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/PersistentFocusWrapper.java
rename to leanback/src/android/support/v17/leanback/widget/PersistentFocusWrapper.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/PlaybackControlsPresenter.java b/leanback/src/android/support/v17/leanback/widget/PlaybackControlsPresenter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/PlaybackControlsPresenter.java
rename to leanback/src/android/support/v17/leanback/widget/PlaybackControlsPresenter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/PlaybackControlsRow.java b/leanback/src/android/support/v17/leanback/widget/PlaybackControlsRow.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/PlaybackControlsRow.java
rename to leanback/src/android/support/v17/leanback/widget/PlaybackControlsRow.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/PlaybackControlsRowPresenter.java b/leanback/src/android/support/v17/leanback/widget/PlaybackControlsRowPresenter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/PlaybackControlsRowPresenter.java
rename to leanback/src/android/support/v17/leanback/widget/PlaybackControlsRowPresenter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/PlaybackControlsRowView.java b/leanback/src/android/support/v17/leanback/widget/PlaybackControlsRowView.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/PlaybackControlsRowView.java
rename to leanback/src/android/support/v17/leanback/widget/PlaybackControlsRowView.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/PlaybackRowPresenter.java b/leanback/src/android/support/v17/leanback/widget/PlaybackRowPresenter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/PlaybackRowPresenter.java
rename to leanback/src/android/support/v17/leanback/widget/PlaybackRowPresenter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/PlaybackSeekDataProvider.java b/leanback/src/android/support/v17/leanback/widget/PlaybackSeekDataProvider.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/PlaybackSeekDataProvider.java
rename to leanback/src/android/support/v17/leanback/widget/PlaybackSeekDataProvider.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/PlaybackSeekUi.java b/leanback/src/android/support/v17/leanback/widget/PlaybackSeekUi.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/PlaybackSeekUi.java
rename to leanback/src/android/support/v17/leanback/widget/PlaybackSeekUi.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/PlaybackTransportRowPresenter.java b/leanback/src/android/support/v17/leanback/widget/PlaybackTransportRowPresenter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/PlaybackTransportRowPresenter.java
rename to leanback/src/android/support/v17/leanback/widget/PlaybackTransportRowPresenter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/PlaybackTransportRowView.java b/leanback/src/android/support/v17/leanback/widget/PlaybackTransportRowView.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/PlaybackTransportRowView.java
rename to leanback/src/android/support/v17/leanback/widget/PlaybackTransportRowView.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/Presenter.java b/leanback/src/android/support/v17/leanback/widget/Presenter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/Presenter.java
rename to leanback/src/android/support/v17/leanback/widget/Presenter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/PresenterSelector.java b/leanback/src/android/support/v17/leanback/widget/PresenterSelector.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/PresenterSelector.java
rename to leanback/src/android/support/v17/leanback/widget/PresenterSelector.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/PresenterSwitcher.java b/leanback/src/android/support/v17/leanback/widget/PresenterSwitcher.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/PresenterSwitcher.java
rename to leanback/src/android/support/v17/leanback/widget/PresenterSwitcher.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/RecyclerViewParallax.java b/leanback/src/android/support/v17/leanback/widget/RecyclerViewParallax.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/RecyclerViewParallax.java
rename to leanback/src/android/support/v17/leanback/widget/RecyclerViewParallax.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ResizingTextView.java b/leanback/src/android/support/v17/leanback/widget/ResizingTextView.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ResizingTextView.java
rename to leanback/src/android/support/v17/leanback/widget/ResizingTextView.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/RoundedRectHelper.java b/leanback/src/android/support/v17/leanback/widget/RoundedRectHelper.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/RoundedRectHelper.java
rename to leanback/src/android/support/v17/leanback/widget/RoundedRectHelper.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/Row.java b/leanback/src/android/support/v17/leanback/widget/Row.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/Row.java
rename to leanback/src/android/support/v17/leanback/widget/Row.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/RowContainerView.java b/leanback/src/android/support/v17/leanback/widget/RowContainerView.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/RowContainerView.java
rename to leanback/src/android/support/v17/leanback/widget/RowContainerView.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/RowHeaderPresenter.java b/leanback/src/android/support/v17/leanback/widget/RowHeaderPresenter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/RowHeaderPresenter.java
rename to leanback/src/android/support/v17/leanback/widget/RowHeaderPresenter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/RowHeaderView.java b/leanback/src/android/support/v17/leanback/widget/RowHeaderView.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/RowHeaderView.java
rename to leanback/src/android/support/v17/leanback/widget/RowHeaderView.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/RowPresenter.java b/leanback/src/android/support/v17/leanback/widget/RowPresenter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/RowPresenter.java
rename to leanback/src/android/support/v17/leanback/widget/RowPresenter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ScaleFrameLayout.java b/leanback/src/android/support/v17/leanback/widget/ScaleFrameLayout.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ScaleFrameLayout.java
rename to leanback/src/android/support/v17/leanback/widget/ScaleFrameLayout.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/SearchBar.java b/leanback/src/android/support/v17/leanback/widget/SearchBar.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/SearchBar.java
rename to leanback/src/android/support/v17/leanback/widget/SearchBar.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/SearchEditText.java b/leanback/src/android/support/v17/leanback/widget/SearchEditText.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/SearchEditText.java
rename to leanback/src/android/support/v17/leanback/widget/SearchEditText.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/SearchOrbView.java b/leanback/src/android/support/v17/leanback/widget/SearchOrbView.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/SearchOrbView.java
rename to leanback/src/android/support/v17/leanback/widget/SearchOrbView.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/SectionRow.java b/leanback/src/android/support/v17/leanback/widget/SectionRow.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/SectionRow.java
rename to leanback/src/android/support/v17/leanback/widget/SectionRow.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/SeekBar.java b/leanback/src/android/support/v17/leanback/widget/SeekBar.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/SeekBar.java
rename to leanback/src/android/support/v17/leanback/widget/SeekBar.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ShadowHelper.java b/leanback/src/android/support/v17/leanback/widget/ShadowHelper.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ShadowHelper.java
rename to leanback/src/android/support/v17/leanback/widget/ShadowHelper.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ShadowOverlayContainer.java b/leanback/src/android/support/v17/leanback/widget/ShadowOverlayContainer.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ShadowOverlayContainer.java
rename to leanback/src/android/support/v17/leanback/widget/ShadowOverlayContainer.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ShadowOverlayHelper.java b/leanback/src/android/support/v17/leanback/widget/ShadowOverlayHelper.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ShadowOverlayHelper.java
rename to leanback/src/android/support/v17/leanback/widget/ShadowOverlayHelper.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/SinglePresenterSelector.java b/leanback/src/android/support/v17/leanback/widget/SinglePresenterSelector.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/SinglePresenterSelector.java
rename to leanback/src/android/support/v17/leanback/widget/SinglePresenterSelector.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/SingleRow.java b/leanback/src/android/support/v17/leanback/widget/SingleRow.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/SingleRow.java
rename to leanback/src/android/support/v17/leanback/widget/SingleRow.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/SparseArrayObjectAdapter.java b/leanback/src/android/support/v17/leanback/widget/SparseArrayObjectAdapter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/SparseArrayObjectAdapter.java
rename to leanback/src/android/support/v17/leanback/widget/SparseArrayObjectAdapter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/SpeechOrbView.java b/leanback/src/android/support/v17/leanback/widget/SpeechOrbView.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/SpeechOrbView.java
rename to leanback/src/android/support/v17/leanback/widget/SpeechOrbView.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/SpeechRecognitionCallback.java b/leanback/src/android/support/v17/leanback/widget/SpeechRecognitionCallback.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/SpeechRecognitionCallback.java
rename to leanback/src/android/support/v17/leanback/widget/SpeechRecognitionCallback.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/StaggeredGrid.java b/leanback/src/android/support/v17/leanback/widget/StaggeredGrid.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/StaggeredGrid.java
rename to leanback/src/android/support/v17/leanback/widget/StaggeredGrid.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/StaggeredGridDefault.java b/leanback/src/android/support/v17/leanback/widget/StaggeredGridDefault.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/StaggeredGridDefault.java
rename to leanback/src/android/support/v17/leanback/widget/StaggeredGridDefault.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/StaticShadowHelper.java b/leanback/src/android/support/v17/leanback/widget/StaticShadowHelper.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/StaticShadowHelper.java
rename to leanback/src/android/support/v17/leanback/widget/StaticShadowHelper.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/StreamingTextView.java b/leanback/src/android/support/v17/leanback/widget/StreamingTextView.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/StreamingTextView.java
rename to leanback/src/android/support/v17/leanback/widget/StreamingTextView.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ThumbsBar.java b/leanback/src/android/support/v17/leanback/widget/ThumbsBar.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ThumbsBar.java
rename to leanback/src/android/support/v17/leanback/widget/ThumbsBar.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/TitleHelper.java b/leanback/src/android/support/v17/leanback/widget/TitleHelper.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/TitleHelper.java
rename to leanback/src/android/support/v17/leanback/widget/TitleHelper.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/TitleView.java b/leanback/src/android/support/v17/leanback/widget/TitleView.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/TitleView.java
rename to leanback/src/android/support/v17/leanback/widget/TitleView.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/TitleViewAdapter.java b/leanback/src/android/support/v17/leanback/widget/TitleViewAdapter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/TitleViewAdapter.java
rename to leanback/src/android/support/v17/leanback/widget/TitleViewAdapter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/Util.java b/leanback/src/android/support/v17/leanback/widget/Util.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/Util.java
rename to leanback/src/android/support/v17/leanback/widget/Util.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/VerticalGridPresenter.java b/leanback/src/android/support/v17/leanback/widget/VerticalGridPresenter.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/VerticalGridPresenter.java
rename to leanback/src/android/support/v17/leanback/widget/VerticalGridPresenter.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/VerticalGridView.java b/leanback/src/android/support/v17/leanback/widget/VerticalGridView.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/VerticalGridView.java
rename to leanback/src/android/support/v17/leanback/widget/VerticalGridView.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/VideoSurfaceView.java b/leanback/src/android/support/v17/leanback/widget/VideoSurfaceView.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/VideoSurfaceView.java
rename to leanback/src/android/support/v17/leanback/widget/VideoSurfaceView.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ViewHolderTask.java b/leanback/src/android/support/v17/leanback/widget/ViewHolderTask.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ViewHolderTask.java
rename to leanback/src/android/support/v17/leanback/widget/ViewHolderTask.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ViewsStateBundle.java b/leanback/src/android/support/v17/leanback/widget/ViewsStateBundle.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/ViewsStateBundle.java
rename to leanback/src/android/support/v17/leanback/widget/ViewsStateBundle.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/Visibility.java b/leanback/src/android/support/v17/leanback/widget/Visibility.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/Visibility.java
rename to leanback/src/android/support/v17/leanback/widget/Visibility.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/WindowAlignment.java b/leanback/src/android/support/v17/leanback/widget/WindowAlignment.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/WindowAlignment.java
rename to leanback/src/android/support/v17/leanback/widget/WindowAlignment.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/package-info.java b/leanback/src/android/support/v17/leanback/widget/package-info.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/package-info.java
rename to leanback/src/android/support/v17/leanback/widget/package-info.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/picker/DatePicker.java b/leanback/src/android/support/v17/leanback/widget/picker/DatePicker.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/picker/DatePicker.java
rename to leanback/src/android/support/v17/leanback/widget/picker/DatePicker.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/picker/Picker.java b/leanback/src/android/support/v17/leanback/widget/picker/Picker.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/picker/Picker.java
rename to leanback/src/android/support/v17/leanback/widget/picker/Picker.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/picker/PickerColumn.java b/leanback/src/android/support/v17/leanback/widget/picker/PickerColumn.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/picker/PickerColumn.java
rename to leanback/src/android/support/v17/leanback/widget/picker/PickerColumn.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/picker/PickerUtility.java b/leanback/src/android/support/v17/leanback/widget/picker/PickerUtility.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/picker/PickerUtility.java
rename to leanback/src/android/support/v17/leanback/widget/picker/PickerUtility.java
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/picker/TimePicker.java b/leanback/src/android/support/v17/leanback/widget/picker/TimePicker.java
similarity index 100%
rename from v17/leanback/src/android/support/v17/leanback/widget/picker/TimePicker.java
rename to leanback/src/android/support/v17/leanback/widget/picker/TimePicker.java
diff --git a/v17/leanback/tests/AndroidManifest.xml b/leanback/tests/AndroidManifest.xml
similarity index 100%
rename from v17/leanback/tests/AndroidManifest.xml
rename to leanback/tests/AndroidManifest.xml
diff --git a/v17/leanback/tests/NO_DOCS b/leanback/tests/NO_DOCS
similarity index 100%
rename from v17/leanback/tests/NO_DOCS
rename to leanback/tests/NO_DOCS
diff --git a/leanback/tests/generatev4.py b/leanback/tests/generatev4.py
new file mode 100755
index 0000000..d7d14a8
--- /dev/null
+++ b/leanback/tests/generatev4.py
@@ -0,0 +1,168 @@
+#!/usr/bin/python
+
+# Copyright (C) 2015 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.
+
+import os
+import sys
+
+print "Generate v4 fragment related code for leanback"
+
+####### generate XXXTestFragment classes #######
+
+files = ['BrowseTest', 'GuidedStepTest', 'PlaybackTest', 'DetailsTest']
+
+cls = ['BrowseTest', 'Background', 'Base', 'BaseRow', 'Browse', 'Details', 'Error', 'Headers',
+      'PlaybackOverlay', 'Rows', 'Search', 'VerticalGrid', 'Branded',
+      'GuidedStepTest', 'GuidedStep', 'RowsTest', 'PlaybackTest', 'Playback', 'Video',
+      'DetailsTest']
+
+for w in files:
+    print "copy {}SupportFragment to {}Fragment".format(w, w)
+
+    file = open('java/android/support/v17/leanback/app/{}SupportFragment.java'.format(w), 'r')
+    outfile = open('java/android/support/v17/leanback/app/{}Fragment.java'.format(w), 'w')
+
+    outfile.write("// CHECKSTYLE:OFF Generated code\n")
+    outfile.write("/* This file is auto-generated from {}SupportFragment.java.  DO NOT MODIFY. */\n\n".format(w))
+
+    for line in file:
+        for w in cls:
+            line = line.replace('{}SupportFragment'.format(w), '{}Fragment'.format(w))
+        line = line.replace('android.support.v4.app.FragmentActivity', 'android.app.Activity')
+        line = line.replace('android.support.v4.app.Fragment', 'android.app.Fragment')
+        line = line.replace('FragmentActivity getActivity()', 'Activity getActivity()')
+        outfile.write(line)
+    file.close()
+    outfile.close()
+
+####### generate XXXFragmentTestBase classes #######
+
+testcls = ['GuidedStep', 'Single']
+
+for w in testcls:
+    print "copy {}SupportFrgamentTestBase to {}FragmentTestBase".format(w, w)
+
+    file = open('java/android/support/v17/leanback/app/{}SupportFragmentTestBase.java'.format(w), 'r')
+    outfile = open('java/android/support/v17/leanback/app/{}FragmentTestBase.java'.format(w), 'w')
+
+    outfile.write("// CHECKSTYLE:OFF Generated code\n")
+    outfile.write("/* This file is auto-generated from {}SupportFrgamentTestBase.java.  DO NOT MODIFY. */\n\n".format(w))
+
+    for line in file:
+        for w in cls:
+            line = line.replace('{}SupportFragment'.format(w), '{}Fragment'.format(w))
+        for w in testcls:
+            line = line.replace('{}SupportFragmentTestBase'.format(w), '{}FragmentTestBase'.format(w))
+            line = line.replace('{}SupportFragmentTestActivity'.format(w), '{}FragmentTestActivity'.format(w))
+            line = line.replace('{}TestSupportFragment'.format(w), '{}TestFragment'.format(w))
+        line = line.replace('android.support.v4.app.FragmentActivity', 'android.app.Activity')
+        line = line.replace('android.support.v4.app.Fragment', 'android.app.Fragment')
+        outfile.write(line)
+    file.close()
+    outfile.close()
+
+####### generate XXXFragmentTest classes #######
+
+testcls = ['Browse', 'GuidedStep', 'VerticalGrid', 'Playback', 'Video', 'Details', 'Rows', 'Headers']
+
+for w in testcls:
+    print "copy {}SupporFrgamentTest to {}tFragmentTest".format(w, w)
+
+    file = open('java/android/support/v17/leanback/app/{}SupportFragmentTest.java'.format(w), 'r')
+    outfile = open('java/android/support/v17/leanback/app/{}FragmentTest.java'.format(w), 'w')
+
+    outfile.write("// CHECKSTYLE:OFF Generated code\n")
+    outfile.write("/* This file is auto-generated from {}SupportFragmentTest.java.  DO NOT MODIFY. */\n\n".format(w))
+
+    for line in file:
+        for w in cls:
+            line = line.replace('{}SupportFragment'.format(w), '{}Fragment'.format(w))
+        for w in testcls:
+            line = line.replace('SingleSupportFragmentTestBase', 'SingleFragmentTestBase')
+            line = line.replace('SingleSupportFragmentTestActivity', 'SingleFragmentTestActivity')
+            line = line.replace('{}SupportFragmentTestBase'.format(w), '{}FragmentTestBase'.format(w))
+            line = line.replace('{}SupportFragmentTest'.format(w), '{}FragmentTest'.format(w))
+            line = line.replace('{}SupportFragmentTestActivity'.format(w), '{}FragmentTestActivity'.format(w))
+            line = line.replace('{}TestSupportFragment'.format(w), '{}TestFragment'.format(w))
+        line = line.replace('android.support.v4.app.FragmentActivity', 'android.app.Activity')
+        line = line.replace('android.support.v4.app.Fragment', 'android.app.Fragment')
+	line = line.replace('extends FragmentActivity', 'extends Activity')
+	line = line.replace('Activity.this.getSupportFragmentManager', 'Activity.this.getFragmentManager')
+	line = line.replace('tivity.getSupportFragmentManager', 'tivity.getFragmentManager')
+        outfile.write(line)
+    file.close()
+    outfile.close()
+
+
+####### generate XXXTestActivity classes #######
+testcls = ['Browse', 'GuidedStep', 'Single']
+
+for w in testcls:
+    print "copy {}SupportFragmentTestActivity to {}FragmentTestActivity".format(w, w)
+    file = open('java/android/support/v17/leanback/app/{}SupportFragmentTestActivity.java'.format(w), 'r')
+    outfile = open('java/android/support/v17/leanback/app/{}FragmentTestActivity.java'.format(w), 'w')
+    outfile.write("// CHECKSTYLE:OFF Generated code\n")
+    outfile.write("/* This file is auto-generated from {}SupportFragmentTestActivity.java.  DO NOT MODIFY. */\n\n".format(w))
+    for line in file:
+        line = line.replace('{}TestSupportFragment'.format(w), '{}TestFragment'.format(w))
+        line = line.replace('{}SupportFragmentTestActivity'.format(w), '{}FragmentTestActivity'.format(w))
+        line = line.replace('android.support.v4.app.FragmentActivity', 'android.app.Activity')
+        line = line.replace('android.support.v4.app.Fragment', 'android.app.Fragment')
+        line = line.replace('extends FragmentActivity', 'extends Activity')
+        line = line.replace('getSupportFragmentManager', 'getFragmentManager')
+        outfile.write(line)
+    file.close()
+    outfile.close()
+
+####### generate Float parallax test #######
+
+print "copy ParallaxIntEffectTest to ParallaxFloatEffectTest"
+file = open('java/android/support/v17/leanback/widget/ParallaxIntEffectTest.java', 'r')
+outfile = open('java/android/support/v17/leanback/widget/ParallaxFloatEffectTest.java', 'w')
+outfile.write("// CHECKSTYLE:OFF Generated code\n")
+outfile.write("/* This file is auto-generated from ParallaxIntEffectTest.java.  DO NOT MODIFY. */\n\n")
+for line in file:
+    line = line.replace('IntEffect', 'FloatEffect')
+    line = line.replace('IntParallax', 'FloatParallax')
+    line = line.replace('IntProperty', 'FloatProperty')
+    line = line.replace('intValue()', 'floatValue()')
+    line = line.replace('int screenMax', 'float screenMax')
+    line = line.replace('assertEquals((int)', 'assertFloatEquals((float)')
+    line = line.replace('(int)', '(float)')
+    line = line.replace('int[', 'float[')
+    line = line.replace('Integer', 'Float');
+    outfile.write(line)
+file.close()
+outfile.close()
+
+
+print "copy ParallaxIntTest to ParallaxFloatTest"
+file = open('java/android/support/v17/leanback/widget/ParallaxIntTest.java', 'r')
+outfile = open('java/android/support/v17/leanback/widget/ParallaxFloatTest.java', 'w')
+outfile.write("// CHECKSTYLE:OFF Generated code\n")
+outfile.write("/* This file is auto-generated from ParallaxIntTest.java.  DO NOT MODIFY. */\n\n")
+for line in file:
+    line = line.replace('ParallaxIntTest', 'ParallaxFloatTest')
+    line = line.replace('IntParallax', 'FloatParallax')
+    line = line.replace('IntProperty', 'FloatProperty')
+    line = line.replace('verifyIntProperties', 'verifyFloatProperties')
+    line = line.replace('intValue()', 'floatValue()')
+    line = line.replace('int screenMax', 'float screenMax')
+    line = line.replace('assertEquals((int)', 'assertFloatEquals((float)')
+    line = line.replace('(int)', '(float)')
+    outfile.write(line)
+file.close()
+outfile.close()
+
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/BackgroundManagerTest.java b/leanback/tests/java/android/support/v17/leanback/app/BackgroundManagerTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/BackgroundManagerTest.java
rename to leanback/tests/java/android/support/v17/leanback/app/BackgroundManagerTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/BrowseFragmentTest.java b/leanback/tests/java/android/support/v17/leanback/app/BrowseFragmentTest.java
similarity index 98%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/BrowseFragmentTest.java
rename to leanback/tests/java/android/support/v17/leanback/app/BrowseFragmentTest.java
index 06a1217..654bbe7 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/BrowseFragmentTest.java
+++ b/leanback/tests/java/android/support/v17/leanback/app/BrowseFragmentTest.java
@@ -1,3 +1,6 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from BrowseSupportFragmentTest.java.  DO NOT MODIFY. */
+
 /*
  * Copyright (C) 2015 The Android Open Source Project
  *
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/BrowseFragmentTestActivity.java b/leanback/tests/java/android/support/v17/leanback/app/BrowseFragmentTestActivity.java
similarity index 94%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/BrowseFragmentTestActivity.java
rename to leanback/tests/java/android/support/v17/leanback/app/BrowseFragmentTestActivity.java
index 605a9ca..3ce025d 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/BrowseFragmentTestActivity.java
+++ b/leanback/tests/java/android/support/v17/leanback/app/BrowseFragmentTestActivity.java
@@ -1,3 +1,6 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from BrowseSupportFragmentTestActivity.java.  DO NOT MODIFY. */
+
 /*
  * Copyright (C) 2015 The Android Open Source Project
  *
@@ -15,11 +18,11 @@
  */
 package android.support.v17.leanback.app;
 
-import android.app.Activity;
-import android.app.FragmentTransaction;
 import android.content.Intent;
 import android.os.Bundle;
 import android.support.v17.leanback.test.R;
+import android.app.Activity;
+import android.app.FragmentTransaction;
 
 public class BrowseFragmentTestActivity extends Activity {
 
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/BrowseSupportFragmentTest.java b/leanback/tests/java/android/support/v17/leanback/app/BrowseSupportFragmentTest.java
similarity index 98%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/BrowseSupportFragmentTest.java
rename to leanback/tests/java/android/support/v17/leanback/app/BrowseSupportFragmentTest.java
index f578874..51151ae 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/BrowseSupportFragmentTest.java
+++ b/leanback/tests/java/android/support/v17/leanback/app/BrowseSupportFragmentTest.java
@@ -1,6 +1,3 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from BrowseFragmentTest.java.  DO NOT MODIFY. */
-
 /*
  * Copyright (C) 2015 The Android Open Source Project
  *
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/BrowseSupportFragmentTestActivity.java b/leanback/tests/java/android/support/v17/leanback/app/BrowseSupportFragmentTestActivity.java
similarity index 94%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/BrowseSupportFragmentTestActivity.java
rename to leanback/tests/java/android/support/v17/leanback/app/BrowseSupportFragmentTestActivity.java
index 9df846f..313dc86 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/BrowseSupportFragmentTestActivity.java
+++ b/leanback/tests/java/android/support/v17/leanback/app/BrowseSupportFragmentTestActivity.java
@@ -1,6 +1,3 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from BrowseFragmentTestActivity.java.  DO NOT MODIFY. */
-
 /*
  * Copyright (C) 2015 The Android Open Source Project
  *
@@ -18,11 +15,11 @@
  */
 package android.support.v17.leanback.app;
 
-import android.support.v4.app.FragmentActivity;
-import android.support.v4.app.FragmentTransaction;
 import android.content.Intent;
 import android.os.Bundle;
 import android.support.v17.leanback.test.R;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.FragmentTransaction;
 
 public class BrowseSupportFragmentTestActivity extends FragmentActivity {
 
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/BrowseTestFragment.java b/leanback/tests/java/android/support/v17/leanback/app/BrowseTestFragment.java
similarity index 98%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/BrowseTestFragment.java
rename to leanback/tests/java/android/support/v17/leanback/app/BrowseTestFragment.java
index 4fe79f0..6a34c53 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/BrowseTestFragment.java
+++ b/leanback/tests/java/android/support/v17/leanback/app/BrowseTestFragment.java
@@ -1,3 +1,6 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from BrowseTestSupportFragment.java.  DO NOT MODIFY. */
+
 /*
  * Copyright (C) 2015 The Android Open Source Project
  *
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/BrowseTestSupportFragment.java b/leanback/tests/java/android/support/v17/leanback/app/BrowseTestSupportFragment.java
similarity index 98%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/BrowseTestSupportFragment.java
rename to leanback/tests/java/android/support/v17/leanback/app/BrowseTestSupportFragment.java
index 2acc530..373d7a3 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/BrowseTestSupportFragment.java
+++ b/leanback/tests/java/android/support/v17/leanback/app/BrowseTestSupportFragment.java
@@ -1,6 +1,3 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from BrowseTestFragment.java.  DO NOT MODIFY. */
-
 /*
  * Copyright (C) 2015 The Android Open Source Project
  *
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsFragmentTest.java b/leanback/tests/java/android/support/v17/leanback/app/DetailsFragmentTest.java
similarity index 99%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/DetailsFragmentTest.java
rename to leanback/tests/java/android/support/v17/leanback/app/DetailsFragmentTest.java
index 38d08c7..bf70fae 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsFragmentTest.java
+++ b/leanback/tests/java/android/support/v17/leanback/app/DetailsFragmentTest.java
@@ -1,3 +1,6 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from DetailsSupportFragmentTest.java.  DO NOT MODIFY. */
+
 /*
  * Copyright (C) 2016 The Android Open Source Project
  *
@@ -21,7 +24,6 @@
 import static org.junit.Assert.assertTrue;
 
 import android.animation.PropertyValuesHolder;
-import android.app.Fragment;
 import android.content.Intent;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
@@ -47,6 +49,7 @@
 import android.support.v17.leanback.widget.ParallaxTarget;
 import android.support.v17.leanback.widget.RecyclerViewParallax;
 import android.support.v17.leanback.widget.VerticalGridView;
+import android.app.Fragment;
 import android.view.KeyEvent;
 import android.view.View;
 
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsSupportFragmentTest.java b/leanback/tests/java/android/support/v17/leanback/app/DetailsSupportFragmentTest.java
similarity index 99%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/DetailsSupportFragmentTest.java
rename to leanback/tests/java/android/support/v17/leanback/app/DetailsSupportFragmentTest.java
index 04f20bc..0178d26 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsSupportFragmentTest.java
+++ b/leanback/tests/java/android/support/v17/leanback/app/DetailsSupportFragmentTest.java
@@ -1,6 +1,3 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from DetailsFragmentTest.java.  DO NOT MODIFY. */
-
 /*
  * Copyright (C) 2016 The Android Open Source Project
  *
@@ -24,7 +21,6 @@
 import static org.junit.Assert.assertTrue;
 
 import android.animation.PropertyValuesHolder;
-import android.support.v4.app.Fragment;
 import android.content.Intent;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
@@ -50,6 +46,7 @@
 import android.support.v17.leanback.widget.ParallaxTarget;
 import android.support.v17.leanback.widget.RecyclerViewParallax;
 import android.support.v17.leanback.widget.VerticalGridView;
+import android.support.v4.app.Fragment;
 import android.view.KeyEvent;
 import android.view.View;
 
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsTestFragment.java b/leanback/tests/java/android/support/v17/leanback/app/DetailsTestFragment.java
similarity index 97%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/DetailsTestFragment.java
rename to leanback/tests/java/android/support/v17/leanback/app/DetailsTestFragment.java
index 354e574..833b344 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsTestFragment.java
+++ b/leanback/tests/java/android/support/v17/leanback/app/DetailsTestFragment.java
@@ -1,3 +1,6 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from DetailsTestSupportFragment.java.  DO NOT MODIFY. */
+
 /*
  * Copyright (C) 2016 The Android Open Source Project
  *
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsTestSupportFragment.java b/leanback/tests/java/android/support/v17/leanback/app/DetailsTestSupportFragment.java
similarity index 97%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/DetailsTestSupportFragment.java
rename to leanback/tests/java/android/support/v17/leanback/app/DetailsTestSupportFragment.java
index 7d03a45..e0d60b4 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsTestSupportFragment.java
+++ b/leanback/tests/java/android/support/v17/leanback/app/DetailsTestSupportFragment.java
@@ -1,6 +1,3 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from DetailsTestFragment.java.  DO NOT MODIFY. */
-
 /*
  * Copyright (C) 2016 The Android Open Source Project
  *
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTest.java b/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTest.java
similarity index 90%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTest.java
rename to leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTest.java
index fa324bf..650391d 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTest.java
+++ b/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTest.java
@@ -1,3 +1,6 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from GuidedStepSupportFragmentTest.java.  DO NOT MODIFY. */
+
 /*
  * Copyright (C) 2016 The Android Open Source Project
  *
@@ -26,11 +29,15 @@
 import static org.mockito.Mockito.verify;
 
 import android.os.Bundle;
+import android.support.test.InstrumentationRegistry;
 import android.support.test.filters.LargeTest;
 import android.support.test.runner.AndroidJUnit4;
 import android.support.v17.leanback.testutils.PollingCheck;
 import android.support.v17.leanback.widget.GuidedAction;
 import android.support.v17.leanback.widget.GuidedActionsStylist;
+import android.support.v17.leanback.widget.VerticalGridView;
+import android.support.v7.widget.DefaultItemAnimator;
+import android.support.v7.widget.RecyclerView;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -448,4 +455,51 @@
         View firstView = first.getFragment().getActionItemView(0);
         assertTrue(firstView.hasFocus());
     }
+
+    @Test
+    public void recyclerViewDiffTest() throws Throwable {
+        final String firstFragmentName = generateMethodTestName("first");
+        final GuidedStepTestFragment.Provider first = mockProvider(firstFragmentName);
+        doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(InvocationOnMock invocation) {
+                List actions = (List) invocation.getArguments()[0];
+                actions.add(new GuidedAction.Builder().id(1000).title("action1").build());
+                actions.add(new GuidedAction.Builder().id(1001).title("action2").build());
+                return null;
+            }
+        }).when(first).onCreateActions(any(List.class), nullable(Bundle.class));
+
+        launchTestActivity(firstFragmentName, true);
+
+        final ArrayList<RecyclerView.ViewHolder> changeList = new ArrayList();
+        VerticalGridView rv = first.getFragment().mActionsStylist.getActionsGridView();
+        rv.setItemAnimator(new DefaultItemAnimator() {
+            @Override
+            public void onChangeStarting(RecyclerView.ViewHolder item, boolean oldItem) {
+                if (!oldItem) {
+                    changeList.add(item);
+                }
+                super.onChangeStarting(item, oldItem);
+            }
+        });
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                List actions = new ArrayList();
+                actions.add(new GuidedAction.Builder().id(1001).title("action2x").build());
+                actions.add(new GuidedAction.Builder().id(1000).title("action1x").build());
+                first.getFragment().setActions(actions);
+            }
+        });
+
+        // should causes two change animation.
+        PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return changeList.size() == 2;
+            }
+        });
+    }
 }
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTestActivity.java b/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTestActivity.java
similarity index 94%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTestActivity.java
rename to leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTestActivity.java
index 4dcf188..dd17fd3 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTestActivity.java
+++ b/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTestActivity.java
@@ -1,3 +1,6 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from GuidedStepSupportFragmentTestActivity.java.  DO NOT MODIFY. */
+
 /*
  * Copyright (C) 2016 The Android Open Source Project
  *
@@ -14,9 +17,9 @@
 
 package android.support.v17.leanback.app;
 
-import android.app.Activity;
 import android.content.Intent;
 import android.os.Bundle;
+import android.app.Activity;
 
 /**
  * @hide from javadoc
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTestBase.java b/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTestBase.java
similarity index 97%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTestBase.java
rename to leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTestBase.java
index 7059c9a..34ec694 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTestBase.java
+++ b/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTestBase.java
@@ -1,3 +1,6 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from GuidedStepSupportFrgamentTestBase.java.  DO NOT MODIFY. */
+
 /*
  * Copyright (C) 2016 The Android Open Source Project
  *
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTest.java b/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTest.java
similarity index 90%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTest.java
rename to leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTest.java
index b4d9b59..5f015a1 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTest.java
+++ b/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTest.java
@@ -1,6 +1,3 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from GuidedStepFragmentTest.java.  DO NOT MODIFY. */
-
 /*
  * Copyright (C) 2016 The Android Open Source Project
  *
@@ -29,11 +26,15 @@
 import static org.mockito.Mockito.verify;
 
 import android.os.Bundle;
+import android.support.test.InstrumentationRegistry;
 import android.support.test.filters.LargeTest;
 import android.support.test.runner.AndroidJUnit4;
 import android.support.v17.leanback.testutils.PollingCheck;
 import android.support.v17.leanback.widget.GuidedAction;
 import android.support.v17.leanback.widget.GuidedActionsStylist;
+import android.support.v17.leanback.widget.VerticalGridView;
+import android.support.v7.widget.DefaultItemAnimator;
+import android.support.v7.widget.RecyclerView;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -451,4 +452,51 @@
         View firstView = first.getFragment().getActionItemView(0);
         assertTrue(firstView.hasFocus());
     }
+
+    @Test
+    public void recyclerViewDiffTest() throws Throwable {
+        final String firstFragmentName = generateMethodTestName("first");
+        final GuidedStepTestSupportFragment.Provider first = mockProvider(firstFragmentName);
+        doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(InvocationOnMock invocation) {
+                List actions = (List) invocation.getArguments()[0];
+                actions.add(new GuidedAction.Builder().id(1000).title("action1").build());
+                actions.add(new GuidedAction.Builder().id(1001).title("action2").build());
+                return null;
+            }
+        }).when(first).onCreateActions(any(List.class), nullable(Bundle.class));
+
+        launchTestActivity(firstFragmentName, true);
+
+        final ArrayList<RecyclerView.ViewHolder> changeList = new ArrayList();
+        VerticalGridView rv = first.getFragment().mActionsStylist.getActionsGridView();
+        rv.setItemAnimator(new DefaultItemAnimator() {
+            @Override
+            public void onChangeStarting(RecyclerView.ViewHolder item, boolean oldItem) {
+                if (!oldItem) {
+                    changeList.add(item);
+                }
+                super.onChangeStarting(item, oldItem);
+            }
+        });
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                List actions = new ArrayList();
+                actions.add(new GuidedAction.Builder().id(1001).title("action2x").build());
+                actions.add(new GuidedAction.Builder().id(1000).title("action1x").build());
+                first.getFragment().setActions(actions);
+            }
+        });
+
+        // should causes two change animation.
+        PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return changeList.size() == 2;
+            }
+        });
+    }
 }
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTestActivity.java b/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTestActivity.java
similarity index 94%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTestActivity.java
rename to leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTestActivity.java
index fb877ed..bac2f49 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTestActivity.java
+++ b/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTestActivity.java
@@ -1,6 +1,3 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from GuidedStepFragmentTestActivity.java.  DO NOT MODIFY. */
-
 /*
  * Copyright (C) 2016 The Android Open Source Project
  *
@@ -17,9 +14,9 @@
 
 package android.support.v17.leanback.app;
 
-import android.support.v4.app.FragmentActivity;
 import android.content.Intent;
 import android.os.Bundle;
+import android.support.v4.app.FragmentActivity;
 
 /**
  * @hide from javadoc
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTestBase.java b/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTestBase.java
similarity index 97%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTestBase.java
rename to leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTestBase.java
index 17533fa..12e4d09 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTestBase.java
+++ b/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTestBase.java
@@ -1,6 +1,3 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from GuidedStepFrgamentTestBase.java.  DO NOT MODIFY. */
-
 /*
  * Copyright (C) 2016 The Android Open Source Project
  *
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepTestFragment.java b/leanback/tests/java/android/support/v17/leanback/app/GuidedStepTestFragment.java
similarity index 97%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepTestFragment.java
rename to leanback/tests/java/android/support/v17/leanback/app/GuidedStepTestFragment.java
index c530925..73e4083 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepTestFragment.java
+++ b/leanback/tests/java/android/support/v17/leanback/app/GuidedStepTestFragment.java
@@ -1,3 +1,6 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from GuidedStepTestSupportFragment.java.  DO NOT MODIFY. */
+
 /*
  * Copyright (C) 2016 The Android Open Source Project
  *
@@ -14,19 +17,17 @@
 
 package android.support.v17.leanback.app;
 
-import android.app.Activity;
-import android.app.FragmentManager;
 import android.os.Bundle;
-import android.view.ViewGroup;
-import android.view.View;
-import android.view.LayoutInflater;
-
-
 import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
 import android.support.v17.leanback.widget.GuidedAction;
+import android.app.Activity;
+import android.app.FragmentManager;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
 
-import java.util.List;
 import java.util.HashMap;
+import java.util.List;
 
 /**
  * @hide from javadoc
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepTestSupportFragment.java b/leanback/tests/java/android/support/v17/leanback/app/GuidedStepTestSupportFragment.java
similarity index 97%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepTestSupportFragment.java
rename to leanback/tests/java/android/support/v17/leanback/app/GuidedStepTestSupportFragment.java
index bafc2db..95491ce 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepTestSupportFragment.java
+++ b/leanback/tests/java/android/support/v17/leanback/app/GuidedStepTestSupportFragment.java
@@ -1,6 +1,3 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from GuidedStepTestFragment.java.  DO NOT MODIFY. */
-
 /*
  * Copyright (C) 2016 The Android Open Source Project
  *
@@ -17,19 +14,17 @@
 
 package android.support.v17.leanback.app;
 
-import android.support.v4.app.FragmentActivity;
-import android.support.v4.app.FragmentManager;
 import android.os.Bundle;
-import android.view.ViewGroup;
-import android.view.View;
-import android.view.LayoutInflater;
-
-
 import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
 import android.support.v17.leanback.widget.GuidedAction;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.FragmentManager;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
 
-import java.util.List;
 import java.util.HashMap;
+import java.util.List;
 
 /**
  * @hide from javadoc
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/HeadersFragmentTest.java b/leanback/tests/java/android/support/v17/leanback/app/HeadersFragmentTest.java
similarity index 97%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/HeadersFragmentTest.java
rename to leanback/tests/java/android/support/v17/leanback/app/HeadersFragmentTest.java
index e05237f..f23e38a 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/HeadersFragmentTest.java
+++ b/leanback/tests/java/android/support/v17/leanback/app/HeadersFragmentTest.java
@@ -1,3 +1,6 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from HeadersSupportFragmentTest.java.  DO NOT MODIFY. */
+
 /*
  * Copyright (C) 2016 The Android Open Source Project
  *
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/HeadersSupportFragmentTest.java b/leanback/tests/java/android/support/v17/leanback/app/HeadersSupportFragmentTest.java
similarity index 97%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/HeadersSupportFragmentTest.java
rename to leanback/tests/java/android/support/v17/leanback/app/HeadersSupportFragmentTest.java
index 7ec69b9..436a797 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/HeadersSupportFragmentTest.java
+++ b/leanback/tests/java/android/support/v17/leanback/app/HeadersSupportFragmentTest.java
@@ -1,6 +1,3 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from HeadersFragmentTest.java.  DO NOT MODIFY. */
-
 /*
  * Copyright (C) 2016 The Android Open Source Project
  *
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/ListRowDataAdapterTest.java b/leanback/tests/java/android/support/v17/leanback/app/ListRowDataAdapterTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/ListRowDataAdapterTest.java
rename to leanback/tests/java/android/support/v17/leanback/app/ListRowDataAdapterTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PhotoItem.java b/leanback/tests/java/android/support/v17/leanback/app/PhotoItem.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/PhotoItem.java
rename to leanback/tests/java/android/support/v17/leanback/app/PhotoItem.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackFragmentTest.java b/leanback/tests/java/android/support/v17/leanback/app/PlaybackFragmentTest.java
similarity index 99%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackFragmentTest.java
rename to leanback/tests/java/android/support/v17/leanback/app/PlaybackFragmentTest.java
index 6353ef9..a9101a7 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackFragmentTest.java
+++ b/leanback/tests/java/android/support/v17/leanback/app/PlaybackFragmentTest.java
@@ -1,3 +1,6 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from PlaybackSupportFragmentTest.java.  DO NOT MODIFY. */
+
 /*
  * Copyright (C) 2016 The Android Open Source Project
  *
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportFragmentTest.java b/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportFragmentTest.java
similarity index 99%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportFragmentTest.java
rename to leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportFragmentTest.java
index cbc8222..4aaeae8 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportFragmentTest.java
+++ b/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportFragmentTest.java
@@ -1,6 +1,3 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from PlaybackFragmentTest.java.  DO NOT MODIFY. */
-
 /*
  * Copyright (C) 2016 The Android Open Source Project
  *
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestFragment.java b/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestFragment.java
similarity index 98%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestFragment.java
rename to leanback/tests/java/android/support/v17/leanback/app/PlaybackTestFragment.java
index 027ea02..47b644c 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestFragment.java
+++ b/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestFragment.java
@@ -1,3 +1,6 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from PlaybackTestSupportFragment.java.  DO NOT MODIFY. */
+
 /*
  * Copyright (C) 2016 The Android Open Source Project
  *
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestSupportFragment.java b/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestSupportFragment.java
similarity index 98%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestSupportFragment.java
rename to leanback/tests/java/android/support/v17/leanback/app/PlaybackTestSupportFragment.java
index 273df26..dc93a1c 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestSupportFragment.java
+++ b/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestSupportFragment.java
@@ -1,6 +1,3 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from PlaybackTestFragment.java.  DO NOT MODIFY. */
-
 /*
  * Copyright (C) 2016 The Android Open Source Project
  *
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/ProgressBarManagerTest.java b/leanback/tests/java/android/support/v17/leanback/app/ProgressBarManagerTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/ProgressBarManagerTest.java
rename to leanback/tests/java/android/support/v17/leanback/app/ProgressBarManagerTest.java
diff --git a/leanback/tests/java/android/support/v17/leanback/app/RowsFragmentTest.java b/leanback/tests/java/android/support/v17/leanback/app/RowsFragmentTest.java
new file mode 100644
index 0000000..dc10a05
--- /dev/null
+++ b/leanback/tests/java/android/support/v17/leanback/app/RowsFragmentTest.java
@@ -0,0 +1,1354 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from RowsSupportFragmentTest.java.  DO NOT MODIFY. */
+
+/*
+ * Copyright (C) 2016 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.v17.leanback.app;
+
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertNotSame;
+import static junit.framework.Assert.assertNull;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import android.graphics.Rect;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.LargeTest;
+import android.support.test.filters.SdkSuppress;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v17.leanback.test.R;
+import android.support.v17.leanback.testutils.PollingCheck;
+import android.support.v17.leanback.widget.ArrayObjectAdapter;
+import android.support.v17.leanback.widget.HeaderItem;
+import android.support.v17.leanback.widget.HorizontalGridView;
+import android.support.v17.leanback.widget.ItemBridgeAdapter;
+import android.support.v17.leanback.widget.ListRow;
+import android.support.v17.leanback.widget.ListRowPresenter;
+import android.support.v17.leanback.widget.ObjectAdapter;
+import android.support.v17.leanback.widget.OnItemViewClickedListener;
+import android.support.v17.leanback.widget.PageRow;
+import android.support.v17.leanback.widget.Presenter;
+import android.support.v17.leanback.widget.Row;
+import android.support.v17.leanback.widget.RowPresenter;
+import android.support.v17.leanback.widget.SinglePresenterSelector;
+import android.support.v17.leanback.widget.VerticalGridView;
+import android.app.Fragment;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class RowsFragmentTest extends SingleFragmentTestBase {
+
+    static final StringPresenter sCardPresenter = new StringPresenter();
+
+    static void loadData(ArrayObjectAdapter adapter, int numRows, int repeatPerRow) {
+        for (int i = 0; i < numRows; ++i) {
+            ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(sCardPresenter);
+            int index = 0;
+            for (int j = 0; j < repeatPerRow; ++j) {
+                listRowAdapter.add("Hello world-" + (index++));
+                listRowAdapter.add("This is a test-" + (index++));
+                listRowAdapter.add("Android TV-" + (index++));
+                listRowAdapter.add("Leanback-" + (index++));
+                listRowAdapter.add("Hello world-" + (index++));
+                listRowAdapter.add("Android TV-" + (index++));
+                listRowAdapter.add("Leanback-" + (index++));
+                listRowAdapter.add("GuidedStepFragment-" + (index++));
+            }
+            HeaderItem header = new HeaderItem(i, "Row " + i);
+            adapter.add(new ListRow(header, listRowAdapter));
+        }
+    }
+
+    static Bundle saveActivityState(final SingleFragmentTestActivity activity) {
+        final Bundle[] savedState = new Bundle[1];
+        // save activity state
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                savedState[0] = activity.performSaveInstanceState();
+            }
+        });
+        return savedState[0];
+    }
+
+    static void waitForHeaderTransition(final F_Base fragment) {
+        // Wait header transition finishes
+        SystemClock.sleep(100);
+        PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return !fragment.isInHeadersTransition();
+            }
+        });
+    }
+
+    static void selectAndWaitFragmentAnimation(final F_Base fragment, final int row,
+            final int item) {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                fragment.setSelectedPosition(row, true,
+                        new ListRowPresenter.SelectItemViewHolderTask(item));
+            }
+        });
+        // Wait header transition finishes and scrolling stops
+        SystemClock.sleep(100);
+        PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return !fragment.isInHeadersTransition()
+                        && !fragment.getHeadersFragment().isScrolling();
+            }
+        });
+    }
+
+    public static class F_defaultAlignment extends RowsFragment {
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            ListRowPresenter lrp = new ListRowPresenter();
+            ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+            setAdapter(adapter);
+            loadData(adapter, 10, 1);
+        }
+    }
+
+    @Test
+    public void defaultAlignment() throws Throwable {
+        SingleFragmentTestActivity activity = launchAndWaitActivity(F_defaultAlignment.class, 1000);
+
+        final Rect rect = new Rect();
+
+        final VerticalGridView gridView = ((RowsFragment) activity.getTestFragment())
+                .getVerticalGridView();
+        View row0 = gridView.findViewHolderForAdapterPosition(0).itemView;
+        rect.set(0, 0, row0.getWidth(), row0.getHeight());
+        gridView.offsetDescendantRectToMyCoords(row0, rect);
+        assertEquals("First row is initially aligned to top of screen", 0, rect.top);
+
+        sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+        waitForScrollIdle(gridView);
+        View row1 = gridView.findViewHolderForAdapterPosition(1).itemView;
+        PollingCheck.waitFor(new PollingCheck.ViewStableOnScreen(row1));
+
+        rect.set(0, 0, row1.getWidth(), row1.getHeight());
+        gridView.offsetDescendantRectToMyCoords(row1, rect);
+        assertTrue("Second row should not be aligned to top of screen", rect.top > 0);
+    }
+
+    public static class F_selectBeforeSetAdapter extends RowsFragment {
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            setSelectedPosition(7, false);
+            new Handler().postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    getVerticalGridView().requestLayout();
+                }
+            }, 100);
+            new Handler().postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    ListRowPresenter lrp = new ListRowPresenter();
+                    ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+                    setAdapter(adapter);
+                    loadData(adapter, 10, 1);
+                }
+            }, 1000);
+        }
+    }
+
+    @Test
+    public void selectBeforeSetAdapter() throws InterruptedException {
+        SingleFragmentTestActivity activity =
+                launchAndWaitActivity(F_selectBeforeSetAdapter.class, 2000);
+
+        final VerticalGridView gridView = ((RowsFragment) activity.getTestFragment())
+                .getVerticalGridView();
+        assertEquals(7, gridView.getSelectedPosition());
+        assertNotNull(gridView.findViewHolderForAdapterPosition(7));
+    }
+
+    public static class F_selectBeforeAddData extends RowsFragment {
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            ListRowPresenter lrp = new ListRowPresenter();
+            final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+            setAdapter(adapter);
+            setSelectedPosition(7, false);
+            new Handler().postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    getVerticalGridView().requestLayout();
+                }
+            }, 100);
+            new Handler().postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    loadData(adapter, 10, 1);
+                }
+            }, 1000);
+        }
+    }
+
+    @Test
+    public void selectBeforeAddData() throws InterruptedException {
+        SingleFragmentTestActivity activity =
+                launchAndWaitActivity(F_selectBeforeAddData.class, 2000);
+
+        final VerticalGridView gridView = ((RowsFragment) activity.getTestFragment())
+                .getVerticalGridView();
+        assertEquals(7, gridView.getSelectedPosition());
+        assertNotNull(gridView.findViewHolderForAdapterPosition(7));
+    }
+
+    public static class F_selectAfterAddData extends RowsFragment {
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            ListRowPresenter lrp = new ListRowPresenter();
+            final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+            setAdapter(adapter);
+            loadData(adapter, 10, 1);
+            new Handler().postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    setSelectedPosition(7, false);
+                }
+            }, 1000);
+        }
+    }
+
+    @Test
+    public void selectAfterAddData() throws InterruptedException {
+        SingleFragmentTestActivity activity =
+                launchAndWaitActivity(F_selectAfterAddData.class, 2000);
+
+        final VerticalGridView gridView = ((RowsFragment) activity.getTestFragment())
+                .getVerticalGridView();
+        assertEquals(7, gridView.getSelectedPosition());
+        assertNotNull(gridView.findViewHolderForAdapterPosition(7));
+    }
+
+    static WeakReference<F_restoreSelection> sLastF_restoreSelection;
+
+    public static class F_restoreSelection extends RowsFragment {
+        public F_restoreSelection() {
+            sLastF_restoreSelection = new WeakReference<F_restoreSelection>(this);
+        }
+
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            ListRowPresenter lrp = new ListRowPresenter();
+            final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+            setAdapter(adapter);
+            loadData(adapter, 10, 1);
+            if (savedInstanceState == null) {
+                setSelectedPosition(7, false);
+            }
+        }
+    }
+
+    @Test
+    public void restoreSelection() {
+        final SingleFragmentTestActivity activity =
+                launchAndWaitActivity(F_restoreSelection.class, 1000);
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        activity.recreate();
+                    }
+                }
+        );
+        SystemClock.sleep(1000);
+
+        // mActivity is invalid after recreate(), a new Activity instance is created
+        // but we could get Fragment from static variable.
+        RowsFragment fragment = sLastF_restoreSelection.get();
+        final VerticalGridView gridView = fragment.getVerticalGridView();
+        assertEquals(7, gridView.getSelectedPosition());
+        assertNotNull(gridView.findViewHolderForAdapterPosition(7));
+
+    }
+
+    public static class F_ListRowWithOnClick extends RowsFragment {
+        Presenter.ViewHolder mLastClickedItemViewHolder;
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            setOnItemViewClickedListener(new OnItemViewClickedListener() {
+                @Override
+                public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
+                        RowPresenter.ViewHolder rowViewHolder, Row row) {
+                    mLastClickedItemViewHolder = itemViewHolder;
+                }
+            });
+            ListRowPresenter lrp = new ListRowPresenter();
+            ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+            setAdapter(adapter);
+            loadData(adapter, 10, 1);
+        }
+    }
+
+    @Test
+    public void prefetchChildItemsBeforeAttach() throws Throwable {
+        SingleFragmentTestActivity activity =
+                launchAndWaitActivity(F_ListRowWithOnClick.class, 1000);
+
+        F_ListRowWithOnClick fragment = (F_ListRowWithOnClick) activity.getTestFragment();
+        final VerticalGridView gridView = fragment.getVerticalGridView();
+        View lastRow = gridView.getChildAt(gridView.getChildCount() - 1);
+        final int lastRowPos = gridView.getChildAdapterPosition(lastRow);
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(
+                new Runnable() {
+                    public void run() {
+                        gridView.setSelectedPositionSmooth(lastRowPos);
+                    }
+                }
+        );
+        waitForScrollIdle(gridView);
+        ItemBridgeAdapter.ViewHolder prefetchedBridgeVh = (ItemBridgeAdapter.ViewHolder)
+                gridView.findViewHolderForAdapterPosition(lastRowPos + 1);
+        RowPresenter prefetchedRowPresenter = (RowPresenter) prefetchedBridgeVh.getPresenter();
+        final ListRowPresenter.ViewHolder prefetchedListRowVh = (ListRowPresenter.ViewHolder)
+                prefetchedRowPresenter.getRowViewHolder(prefetchedBridgeVh.getViewHolder());
+
+        fragment.mLastClickedItemViewHolder = null;
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(
+                new Runnable() {
+                    public void run() {
+                        prefetchedListRowVh.getItemViewHolder(0).view.performClick();
+                    }
+                }
+        );
+        assertSame(prefetchedListRowVh.getItemViewHolder(0), fragment.mLastClickedItemViewHolder);
+    }
+
+    @Test
+    public void changeHasStableIdToTrueAfterViewCreated() throws InterruptedException {
+        SingleFragmentTestActivity activity =
+                launchAndWaitActivity(RowsFragment.class, 2000);
+        final RowsFragment fragment = (RowsFragment) activity.getTestFragment();
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(
+                new Runnable() {
+                    public void run() {
+                        ObjectAdapter adapter = new ObjectAdapter() {
+                            @Override
+                            public int size() {
+                                return 0;
+                            }
+
+                            @Override
+                            public Object get(int position) {
+                                return null;
+                            }
+
+                            @Override
+                            public long getId(int position) {
+                                return 1;
+                            }
+                        };
+                        adapter.setHasStableIds(true);
+                        fragment.setAdapter(adapter);
+                    }
+                }
+        );
+    }
+
+    static class StableIdAdapter extends ObjectAdapter {
+        ArrayList<Integer> mList = new ArrayList();
+
+        @Override
+        public long getId(int position) {
+            return mList.get(position).longValue();
+        }
+
+        @Override
+        public Object get(int position) {
+            return mList.get(position);
+        }
+
+        @Override
+        public int size() {
+            return mList.size();
+        }
+    }
+
+    public static class F_rowNotifyItemRangeChange extends BrowseFragment {
+
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            ListRowPresenter lrp = new ListRowPresenter();
+            final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+            for (int i = 0; i < 2; i++) {
+                StableIdAdapter listRowAdapter = new StableIdAdapter();
+                listRowAdapter.setHasStableIds(true);
+                listRowAdapter.setPresenterSelector(
+                        new SinglePresenterSelector(sCardPresenter));
+                int index = 0;
+                listRowAdapter.mList.add(index++);
+                listRowAdapter.mList.add(index++);
+                listRowAdapter.mList.add(index++);
+                HeaderItem header = new HeaderItem(i, "Row " + i);
+                adapter.add(new ListRow(header, listRowAdapter));
+            }
+            setAdapter(adapter);
+            new Handler().postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    StableIdAdapter rowAdapter = (StableIdAdapter)
+                            ((ListRow) adapter.get(1)).getAdapter();
+                    rowAdapter.notifyItemRangeChanged(0, 3);
+                }
+            }, 500);
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
+    public void rowNotifyItemRangeChange() throws InterruptedException {
+        SingleFragmentTestActivity activity = launchAndWaitActivity(
+                RowsFragmentTest.F_rowNotifyItemRangeChange.class, 2000);
+
+        VerticalGridView verticalGridView = ((BrowseFragment) activity.getTestFragment())
+                .getRowsFragment().getVerticalGridView();
+        for (int i = 0; i < verticalGridView.getChildCount(); i++) {
+            HorizontalGridView horizontalGridView = verticalGridView.getChildAt(i)
+                    .findViewById(R.id.row_content);
+            for (int j = 0; j < horizontalGridView.getChildCount(); j++) {
+                assertEquals(horizontalGridView.getPaddingTop(),
+                        horizontalGridView.getChildAt(j).getTop());
+            }
+        }
+    }
+
+    public static class F_rowNotifyItemRangeChangeWithTransition extends BrowseFragment {
+
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            ListRowPresenter lrp = new ListRowPresenter();
+            prepareEntranceTransition();
+            final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+            for (int i = 0; i < 2; i++) {
+                StableIdAdapter listRowAdapter = new StableIdAdapter();
+                listRowAdapter.setHasStableIds(true);
+                listRowAdapter.setPresenterSelector(
+                        new SinglePresenterSelector(sCardPresenter));
+                int index = 0;
+                listRowAdapter.mList.add(index++);
+                listRowAdapter.mList.add(index++);
+                listRowAdapter.mList.add(index++);
+                HeaderItem header = new HeaderItem(i, "Row " + i);
+                adapter.add(new ListRow(header, listRowAdapter));
+            }
+            setAdapter(adapter);
+            new Handler().postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    StableIdAdapter rowAdapter = (StableIdAdapter)
+                            ((ListRow) adapter.get(1)).getAdapter();
+                    rowAdapter.notifyItemRangeChanged(0, 3);
+                }
+            }, 500);
+            new Handler().postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    startEntranceTransition();
+                }
+            }, 520);
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
+    public void rowNotifyItemRangeChangeWithTransition() throws InterruptedException {
+        SingleFragmentTestActivity activity = launchAndWaitActivity(
+                        RowsFragmentTest.F_rowNotifyItemRangeChangeWithTransition.class, 3000);
+
+        VerticalGridView verticalGridView = ((BrowseFragment) activity.getTestFragment())
+                .getRowsFragment().getVerticalGridView();
+        for (int i = 0; i < verticalGridView.getChildCount(); i++) {
+            HorizontalGridView horizontalGridView = verticalGridView.getChildAt(i)
+                    .findViewById(R.id.row_content);
+            for (int j = 0; j < horizontalGridView.getChildCount(); j++) {
+                assertEquals(horizontalGridView.getPaddingTop(),
+                        horizontalGridView.getChildAt(j).getTop());
+                assertEquals(0, horizontalGridView.getChildAt(j).getTranslationY(), 0.1f);
+            }
+        }
+    }
+
+    public static class F_Base extends BrowseFragment {
+
+        List<Long> mEntranceTransitionStartTS = new ArrayList();
+        List<Long> mEntranceTransitionEndTS = new ArrayList();
+
+        @Override
+        protected void onEntranceTransitionStart() {
+            super.onEntranceTransitionStart();
+            mEntranceTransitionStartTS.add(SystemClock.uptimeMillis());
+        }
+
+        @Override
+        protected void onEntranceTransitionEnd() {
+            super.onEntranceTransitionEnd();
+            mEntranceTransitionEndTS.add(SystemClock.uptimeMillis());
+        }
+
+        public void assertExecutedEntranceTransition() {
+            assertEquals(1, mEntranceTransitionStartTS.size());
+            assertEquals(1, mEntranceTransitionEndTS.size());
+            assertTrue(mEntranceTransitionEndTS.get(0) - mEntranceTransitionStartTS.get(0) > 100);
+        }
+
+        public void assertNoEntranceTransition() {
+            assertEquals(0, mEntranceTransitionStartTS.size());
+            assertEquals(0, mEntranceTransitionEndTS.size());
+        }
+
+        /**
+         * Util to wait PageFragment swapped.
+         */
+        Fragment waitPageFragment(final Class pageFragmentClass) {
+            PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+                @Override
+                public boolean canProceed() {
+                    return pageFragmentClass.isInstance(getMainFragment())
+                            && getMainFragment().getView() != null;
+                }
+            });
+            return getMainFragment();
+        }
+
+        /**
+         * Wait until a fragment for non-page Row is created. Does not apply to the case a
+         * RowsFragment is created on a PageRow.
+         */
+        RowsFragment waitRowsFragment() {
+            PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+                @Override
+                public boolean canProceed() {
+                    return mMainFragmentListRowDataAdapter != null
+                            && getMainFragment() instanceof RowsFragment
+                            && !(getMainFragment() instanceof SampleRowsFragment);
+                }
+            });
+            return (RowsFragment) getMainFragment();
+        }
+    }
+
+    static ObjectAdapter createListRowAdapter() {
+        StableIdAdapter listRowAdapter = new StableIdAdapter();
+        listRowAdapter.setHasStableIds(false);
+        listRowAdapter.setPresenterSelector(
+                new SinglePresenterSelector(sCardPresenter));
+        int index = 0;
+        listRowAdapter.mList.add(index++);
+        listRowAdapter.mList.add(index++);
+        listRowAdapter.mList.add(index++);
+        return listRowAdapter;
+    }
+
+    /**
+     * Create BrowseFragmentAdapter with 3 ListRows
+     */
+    static ArrayObjectAdapter createListRowsAdapter() {
+        ListRowPresenter lrp = new ListRowPresenter();
+        final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+        for (int i = 0; i < 3; i++) {
+            ObjectAdapter listRowAdapter = createListRowAdapter();
+            HeaderItem header = new HeaderItem(i, "Row " + i);
+            adapter.add(new ListRow(header, listRowAdapter));
+        }
+        return adapter;
+    }
+
+    /**
+     * A typical BrowseFragment with multiple rows that start entrance transition
+     */
+    public static class F_standard extends F_Base {
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            if (savedInstanceState == null) {
+                prepareEntranceTransition();
+            }
+            new Handler().postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    setAdapter(createListRowsAdapter());
+                    startEntranceTransition();
+                }
+            }, 100);
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
+    public void browseFragmentSetNullAdapter() throws InterruptedException {
+        final SingleFragmentTestActivity activity = launchAndWaitActivity(
+                RowsFragmentTest.F_standard.class, 2000);
+        final F_standard fragment = ((F_standard) activity.getTestFragment());
+        fragment.assertExecutedEntranceTransition();
+
+        final ObjectAdapter adapter1 = fragment.getAdapter();
+        ListRowDataAdapter wrappedAdapter = fragment.mMainFragmentListRowDataAdapter;
+        assertTrue(adapter1.hasObserver());
+        assertTrue(wrappedAdapter.hasObserver());
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                fragment.setAdapter(null);
+            }
+        });
+        // adapter should no longer has observer and there is no reference to adapter from
+        // BrowseFragment.
+        assertFalse(adapter1.hasObserver());
+        assertFalse(wrappedAdapter.hasObserver());
+        assertNull(fragment.getAdapter());
+        assertNull(fragment.mMainFragmentListRowDataAdapter);
+        // RowsFragment is still there
+        assertTrue(fragment.mMainFragment instanceof RowsFragment);
+        assertNotNull(fragment.mMainFragmentRowsAdapter);
+        assertNotNull(fragment.mMainFragmentAdapter);
+
+        // initialize to same adapter
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                fragment.setAdapter(adapter1);
+            }
+        });
+        assertTrue(adapter1.hasObserver());
+        assertNotSame(wrappedAdapter, fragment.mMainFragmentListRowDataAdapter);
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
+    public void browseFragmentChangeAdapter() throws InterruptedException {
+        final SingleFragmentTestActivity activity = launchAndWaitActivity(
+                RowsFragmentTest.F_standard.class, 2000);
+        final F_standard fragment = ((F_standard) activity.getTestFragment());
+        fragment.assertExecutedEntranceTransition();
+
+        final ObjectAdapter adapter1 = fragment.getAdapter();
+        ListRowDataAdapter wrappedAdapter = fragment.mMainFragmentListRowDataAdapter;
+        assertTrue(adapter1.hasObserver());
+        assertTrue(wrappedAdapter.hasObserver());
+        final ObjectAdapter adapter2 = createListRowsAdapter();
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                fragment.setAdapter(adapter2);
+            }
+        });
+        // adapter1 should no longer has observer and adapter2 will have observer
+        assertFalse(adapter1.hasObserver());
+        assertFalse(wrappedAdapter.hasObserver());
+        assertSame(adapter2, fragment.getAdapter());
+        assertNotSame(wrappedAdapter, fragment.mMainFragmentListRowDataAdapter);
+        assertTrue(adapter2.hasObserver());
+        assertTrue(fragment.mMainFragmentListRowDataAdapter.hasObserver());
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
+    public void browseFragmentChangeAdapterToPage() throws InterruptedException {
+        final SingleFragmentTestActivity activity = launchAndWaitActivity(
+                RowsFragmentTest.F_standard.class, 2000);
+        final F_standard fragment = ((F_standard) activity.getTestFragment());
+        fragment.assertExecutedEntranceTransition();
+
+        final ObjectAdapter adapter1 = fragment.getAdapter();
+        ListRowDataAdapter wrappedAdapter = fragment.mMainFragmentListRowDataAdapter;
+        assertTrue(adapter1.hasObserver());
+        assertTrue(wrappedAdapter.hasObserver());
+        final ObjectAdapter adapter2 = create2PageRow3ListRow();
+        fragment.getMainFragmentRegistry().registerFragment(MyPageRow.class,
+                new MyFragmentFactory());
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                fragment.setAdapter(adapter2);
+            }
+        });
+        fragment.waitPageFragment(SampleRowsFragment.class);
+        // adapter1 should no longer has observer and adapter2 will have observer
+        assertFalse(adapter1.hasObserver());
+        assertFalse(wrappedAdapter.hasObserver());
+        assertSame(adapter2, fragment.getAdapter());
+        assertNull(fragment.mMainFragmentListRowDataAdapter);
+        assertTrue(adapter2.hasObserver());
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
+    public void browseFragmentNotifyDataChangeListRowToPage() throws InterruptedException {
+        final SingleFragmentTestActivity activity = launchAndWaitActivity(
+                RowsFragmentTest.F_standard.class, 2000);
+        final F_standard fragment = ((F_standard) activity.getTestFragment());
+        fragment.assertExecutedEntranceTransition();
+
+        final ArrayObjectAdapter adapter1 = (ArrayObjectAdapter) fragment.getAdapter();
+        ListRowDataAdapter wrappedAdapter = fragment.mMainFragmentListRowDataAdapter;
+        assertTrue(adapter1.hasObserver());
+        assertTrue(wrappedAdapter.hasObserver());
+
+        fragment.getMainFragmentRegistry().registerFragment(MyPageRow.class,
+                new MyFragmentFactory());
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                adapter1.removeItems(0, 1);
+                adapter1.add(0, new MyPageRow(0));
+            }
+        });
+        fragment.waitPageFragment(SampleRowsFragment.class);
+        assertTrue(adapter1.hasObserver());
+        assertNull(fragment.mMainFragmentListRowDataAdapter);
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
+    public void browseFragmentNotifyItemChangeListRowToPage() throws InterruptedException {
+        final SingleFragmentTestActivity activity = launchAndWaitActivity(
+                RowsFragmentTest.F_standard.class, 2000);
+        final F_standard fragment = ((F_standard) activity.getTestFragment());
+        fragment.assertExecutedEntranceTransition();
+
+        final ArrayObjectAdapter adapter1 = (ArrayObjectAdapter) fragment.getAdapter();
+        ListRowDataAdapter wrappedAdapter = fragment.mMainFragmentListRowDataAdapter;
+        assertTrue(adapter1.hasObserver());
+        assertTrue(wrappedAdapter.hasObserver());
+
+        fragment.getMainFragmentRegistry().registerFragment(MyPageRow.class,
+                new MyFragmentFactory());
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                adapter1.replace(0, new MyPageRow(0));
+            }
+        });
+        fragment.waitPageFragment(SampleRowsFragment.class);
+        assertTrue(adapter1.hasObserver());
+        assertNull(fragment.mMainFragmentListRowDataAdapter);
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
+    public void browseFragmentNotifyDataChangeListRowToListRow() throws InterruptedException {
+        final SingleFragmentTestActivity activity = launchAndWaitActivity(
+                RowsFragmentTest.F_standard.class, 2000);
+        final F_standard fragment = ((F_standard) activity.getTestFragment());
+        fragment.assertExecutedEntranceTransition();
+
+        final ArrayObjectAdapter adapter1 = (ArrayObjectAdapter) fragment.getAdapter();
+        ListRowDataAdapter wrappedAdapter = fragment.mMainFragmentListRowDataAdapter;
+        assertTrue(adapter1.hasObserver());
+        assertTrue(wrappedAdapter.hasObserver());
+
+        fragment.getMainFragmentRegistry().registerFragment(MyPageRow.class,
+                new MyFragmentFactory());
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                ObjectAdapter listRowAdapter = createListRowAdapter();
+                HeaderItem header = new HeaderItem(0, "Row 0 changed");
+                adapter1.removeItems(0, 1);
+                adapter1.add(0, new ListRow(header, listRowAdapter));
+            }
+        });
+        assertTrue(adapter1.hasObserver());
+        assertTrue(wrappedAdapter.hasObserver());
+        assertSame(wrappedAdapter, fragment.mMainFragmentListRowDataAdapter);
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
+    public void browseFragmentNotifyItemChangeListRowToListRow() throws InterruptedException {
+        final SingleFragmentTestActivity activity = launchAndWaitActivity(
+                RowsFragmentTest.F_standard.class, 2000);
+        final F_standard fragment = ((F_standard) activity.getTestFragment());
+        fragment.assertExecutedEntranceTransition();
+
+        final ArrayObjectAdapter adapter1 = (ArrayObjectAdapter) fragment.getAdapter();
+        ListRowDataAdapter wrappedAdapter = fragment.mMainFragmentListRowDataAdapter;
+        assertTrue(adapter1.hasObserver());
+        assertTrue(wrappedAdapter.hasObserver());
+
+        fragment.getMainFragmentRegistry().registerFragment(MyPageRow.class,
+                new MyFragmentFactory());
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                ObjectAdapter listRowAdapter = createListRowAdapter();
+                HeaderItem header = new HeaderItem(0, "Row 0 changed");
+                adapter1.replace(0, new ListRow(header, listRowAdapter));
+            }
+        });
+        assertTrue(adapter1.hasObserver());
+        assertTrue(wrappedAdapter.hasObserver());
+        assertSame(wrappedAdapter, fragment.mMainFragmentListRowDataAdapter);
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
+    public void browseFragmentChangeAdapterPageToPage() throws InterruptedException {
+        final SingleFragmentTestActivity activity = launchAndWaitActivity(
+                RowsFragmentTest.F_2PageRow3ListRow.class, 2000);
+        final F_2PageRow3ListRow fragment = ((F_2PageRow3ListRow) activity.getTestFragment());
+        fragment.assertExecutedEntranceTransition();
+
+        final ObjectAdapter adapter1 = fragment.getAdapter();
+        assertNull(fragment.mMainFragmentListRowDataAdapter);
+        assertTrue(adapter1.hasObserver());
+        final ObjectAdapter adapter2 = create2PageRow3ListRow();
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                fragment.setAdapter(adapter2);
+            }
+        });
+        fragment.waitPageFragment(SampleRowsFragment.class);
+        // adapter1 should no longer has observer and adapter2 will have observer
+        assertFalse(adapter1.hasObserver());
+        assertSame(adapter2, fragment.getAdapter());
+        assertNull(fragment.mMainFragmentListRowDataAdapter);
+        assertTrue(adapter2.hasObserver());
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
+    public void browseFragmentNotifyChangePageToPage() throws InterruptedException {
+        final SingleFragmentTestActivity activity = launchAndWaitActivity(
+                RowsFragmentTest.F_2PageRow3ListRow.class, 2000);
+        final F_2PageRow3ListRow fragment = ((F_2PageRow3ListRow) activity.getTestFragment());
+        fragment.assertExecutedEntranceTransition();
+
+        final ArrayObjectAdapter adapter1 = (ArrayObjectAdapter) fragment.getAdapter();
+        assertNull(fragment.mMainFragmentListRowDataAdapter);
+        assertTrue(adapter1.hasObserver());
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                adapter1.removeItems(0, 1);
+                adapter1.add(0, new MyPageRow(1));
+            }
+        });
+        fragment.waitPageFragment(SampleFragment.class);
+        // adapter1 should no longer has observer and adapter2 will have observer
+        assertTrue(adapter1.hasObserver());
+        assertNull(fragment.mMainFragmentListRowDataAdapter);
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
+    public void browseFragmentNotifyItemChangePageToPage() throws InterruptedException {
+        final SingleFragmentTestActivity activity = launchAndWaitActivity(
+                RowsFragmentTest.F_2PageRow3ListRow.class, 2000);
+        final F_2PageRow3ListRow fragment = ((F_2PageRow3ListRow) activity.getTestFragment());
+        fragment.assertExecutedEntranceTransition();
+
+        final ArrayObjectAdapter adapter1 = (ArrayObjectAdapter) fragment.getAdapter();
+        assertNull(fragment.mMainFragmentListRowDataAdapter);
+        assertTrue(adapter1.hasObserver());
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                adapter1.replace(0, new MyPageRow(1));
+            }
+        });
+        fragment.waitPageFragment(SampleFragment.class);
+        // adapter1 should no longer has observer and adapter2 will have observer
+        assertTrue(adapter1.hasObserver());
+        assertNull(fragment.mMainFragmentListRowDataAdapter);
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
+    public void browseFragmentChangeAdapterPageToListRow() throws InterruptedException {
+        final SingleFragmentTestActivity activity = launchAndWaitActivity(
+                RowsFragmentTest.F_2PageRow3ListRow.class, 2000);
+        final F_2PageRow3ListRow fragment = ((F_2PageRow3ListRow) activity.getTestFragment());
+        fragment.assertExecutedEntranceTransition();
+
+        final ObjectAdapter adapter1 = fragment.getAdapter();
+        assertNull(fragment.mMainFragmentListRowDataAdapter);
+        assertTrue(adapter1.hasObserver());
+        final ObjectAdapter adapter2 = createListRowsAdapter();
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                fragment.setAdapter(adapter2);
+            }
+        });
+        fragment.waitRowsFragment();
+        // adapter1 should no longer has observer and adapter2 will have observer
+        assertFalse(adapter1.hasObserver());
+        assertSame(adapter2, fragment.getAdapter());
+        assertTrue(adapter2.hasObserver());
+        assertTrue(fragment.mMainFragmentListRowDataAdapter.hasObserver());
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
+    public void browseFragmentNotifyDataChangePageToListRow() throws InterruptedException {
+        final SingleFragmentTestActivity activity = launchAndWaitActivity(
+                RowsFragmentTest.F_2PageRow3ListRow.class, 2000);
+        final F_2PageRow3ListRow fragment = ((F_2PageRow3ListRow) activity.getTestFragment());
+        fragment.assertExecutedEntranceTransition();
+
+        final ArrayObjectAdapter adapter1 = (ArrayObjectAdapter) fragment.getAdapter();
+        assertNull(fragment.mMainFragmentListRowDataAdapter);
+        assertTrue(adapter1.hasObserver());
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                ObjectAdapter listRowAdapter = createListRowAdapter();
+                HeaderItem header = new HeaderItem(0, "Row 0 changed");
+                adapter1.removeItems(0, 1);
+                adapter1.add(0, new ListRow(header, listRowAdapter));
+            }
+        });
+        fragment.waitRowsFragment();
+        assertTrue(adapter1.hasObserver());
+        assertTrue(fragment.mMainFragmentListRowDataAdapter.hasObserver());
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
+    public void browseFragmentNotifyItemChangePageToListRow() throws InterruptedException {
+        final SingleFragmentTestActivity activity = launchAndWaitActivity(
+                RowsFragmentTest.F_2PageRow3ListRow.class, 2000);
+        final F_2PageRow3ListRow fragment = ((F_2PageRow3ListRow) activity.getTestFragment());
+        fragment.assertExecutedEntranceTransition();
+
+        final ArrayObjectAdapter adapter1 = (ArrayObjectAdapter) fragment.getAdapter();
+        assertNull(fragment.mMainFragmentListRowDataAdapter);
+        assertTrue(adapter1.hasObserver());
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                ObjectAdapter listRowAdapter = createListRowAdapter();
+                HeaderItem header = new HeaderItem(0, "Row 0 changed");
+                adapter1.replace(0, new ListRow(header, listRowAdapter));
+            }
+        });
+        fragment.waitRowsFragment();
+        assertTrue(adapter1.hasObserver());
+        assertTrue(fragment.mMainFragmentListRowDataAdapter.hasObserver());
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
+    public void browseFragmentRestore() throws InterruptedException {
+        final SingleFragmentTestActivity activity = launchAndWaitActivity(
+                RowsFragmentTest.F_standard.class, 2000);
+        final F_standard fragment = ((F_standard) activity.getTestFragment());
+        fragment.assertExecutedEntranceTransition();
+
+        // select item 2 on row 1
+        selectAndWaitFragmentAnimation(fragment, 1, 2);
+        // save activity to state
+        Bundle savedState = saveActivityState(activity);
+        activity.finish();
+
+        // recreate activity with saved state
+        SingleFragmentTestActivity activity2 = launchAndWaitActivity(
+                RowsFragmentTest.F_standard.class,
+                new Options().savedInstance(savedState), 2000);
+        final F_standard fragment2 = ((F_standard) activity2.getTestFragment());
+        // validate restored activity selected row and selected item
+        fragment2.assertNoEntranceTransition();
+        assertEquals(1, fragment2.getSelectedPosition());
+        assertEquals(2, ((ListRowPresenter.ViewHolder) fragment2.getSelectedRowViewHolder())
+                .getSelectedPosition());
+        activity2.finish();
+    }
+
+    public static class MyPageRow extends PageRow {
+        public int type;
+        public MyPageRow(int type) {
+            super(new HeaderItem(100 + type, "page type " + type));
+            this.type = type;
+        }
+    }
+
+    /**
+     * A RowsFragment that is a separate page in BrowseFragment.
+     */
+    public static class SampleRowsFragment extends RowsFragment {
+        public SampleRowsFragment() {
+            // simulates late data loading:
+            new Handler().postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    setAdapter(createListRowsAdapter());
+                    if (getMainFragmentAdapter() != null) {
+                        getMainFragmentAdapter().getFragmentHost()
+                                .notifyDataReady(getMainFragmentAdapter());
+                    }
+                }
+            }, 500);
+        }
+    }
+
+    /**
+     * A custom Fragment that is a separate page in BrowseFragment.
+     */
+    public static class SampleFragment extends Fragment implements
+            BrowseFragment.MainFragmentAdapterProvider {
+
+        public static class PageFragmentAdapterImpl extends
+                BrowseFragment.MainFragmentAdapter<SampleFragment> {
+
+            public PageFragmentAdapterImpl(SampleFragment fragment) {
+                super(fragment);
+                setScalingEnabled(true);
+            }
+
+            @Override
+            public void setEntranceTransitionState(boolean state) {
+                getFragment().setEntranceTransitionState(state);
+            }
+        }
+
+        final PageFragmentAdapterImpl mMainFragmentAdapter = new PageFragmentAdapterImpl(this);
+
+        void setEntranceTransitionState(boolean state) {
+            final View view = getView();
+            int visibility = state ? View.VISIBLE : View.INVISIBLE;
+            view.findViewById(R.id.tv1).setVisibility(visibility);
+            view.findViewById(R.id.tv2).setVisibility(visibility);
+            view.findViewById(R.id.tv3).setVisibility(visibility);
+        }
+
+        @Override
+        public View onCreateView(
+                final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+            View view = inflater.inflate(R.layout.page_fragment, container, false);
+            return view;
+        }
+
+        @Override
+        public void onViewCreated(View view, Bundle savedInstanceState) {
+            // static layout has view and data ready immediately
+            mMainFragmentAdapter.getFragmentHost().notifyViewCreated(mMainFragmentAdapter);
+            mMainFragmentAdapter.getFragmentHost().notifyDataReady(mMainFragmentAdapter);
+        }
+
+        @Override
+        public BrowseFragment.MainFragmentAdapter getMainFragmentAdapter() {
+            return mMainFragmentAdapter;
+        }
+    }
+
+    /**
+     * Create BrowseFragmentAdapter with 3 ListRows and 2 PageRows
+     */
+    private static ArrayObjectAdapter create3ListRow2PageRowAdapter() {
+        ListRowPresenter lrp = new ListRowPresenter();
+        final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+        for (int i = 0; i < 3; i++) {
+            StableIdAdapter listRowAdapter = new StableIdAdapter();
+            listRowAdapter.setHasStableIds(false);
+            listRowAdapter.setPresenterSelector(
+                    new SinglePresenterSelector(sCardPresenter));
+            int index = 0;
+            listRowAdapter.mList.add(index++);
+            listRowAdapter.mList.add(index++);
+            listRowAdapter.mList.add(index++);
+            HeaderItem header = new HeaderItem(i, "Row " + i);
+            adapter.add(new ListRow(header, listRowAdapter));
+        }
+        adapter.add(new MyPageRow(0));
+        adapter.add(new MyPageRow(1));
+        return adapter;
+    }
+
+    /**
+     * Create BrowseFragmentAdapter with 2 PageRows then 3 ListRow
+     */
+    private static ArrayObjectAdapter create2PageRow3ListRow() {
+        ListRowPresenter lrp = new ListRowPresenter();
+        final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+        adapter.add(new MyPageRow(0));
+        adapter.add(new MyPageRow(1));
+        for (int i = 0; i < 3; i++) {
+            StableIdAdapter listRowAdapter = new StableIdAdapter();
+            listRowAdapter.setHasStableIds(false);
+            listRowAdapter.setPresenterSelector(
+                    new SinglePresenterSelector(sCardPresenter));
+            int index = 0;
+            listRowAdapter.mList.add(index++);
+            listRowAdapter.mList.add(index++);
+            listRowAdapter.mList.add(index++);
+            HeaderItem header = new HeaderItem(i, "Row " + i);
+            adapter.add(new ListRow(header, listRowAdapter));
+        }
+        return adapter;
+    }
+
+    static class MyFragmentFactory extends BrowseFragment.FragmentFactory {
+        @Override
+        public Fragment createFragment(Object rowObj) {
+            MyPageRow row = (MyPageRow) rowObj;
+            if (row.type == 0) {
+                return new SampleRowsFragment();
+            } else if (row.type == 1) {
+                return new SampleFragment();
+            }
+            return null;
+        }
+    }
+
+    /**
+     * A BrowseFragment with three ListRows, one SampleRowsFragment and one SampleFragment.
+     */
+    public static class F_3ListRow2PageRow extends F_Base {
+        public F_3ListRow2PageRow() {
+            getMainFragmentRegistry().registerFragment(MyPageRow.class, new MyFragmentFactory());
+        }
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            if (savedInstanceState == null) {
+                prepareEntranceTransition();
+            }
+            new Handler().postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    setAdapter(create3ListRow2PageRowAdapter());
+                    startEntranceTransition();
+                }
+            }, 100);
+        }
+    }
+
+    /**
+     * A BrowseFragment with three ListRows, one SampleRowsFragment and one SampleFragment.
+     */
+    public static class F_2PageRow3ListRow extends F_Base {
+        public F_2PageRow3ListRow() {
+            getMainFragmentRegistry().registerFragment(MyPageRow.class, new MyFragmentFactory());
+        }
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            if (savedInstanceState == null) {
+                prepareEntranceTransition();
+            }
+            new Handler().postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    setAdapter(create2PageRow3ListRow());
+                    startEntranceTransition();
+                }
+            }, 100);
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
+    public void mixedBrowseFragmentRestoreToListRow() throws Throwable {
+        final SingleFragmentTestActivity activity = launchAndWaitActivity(
+                RowsFragmentTest.F_3ListRow2PageRow.class, 2000);
+        final F_3ListRow2PageRow fragment = ((F_3ListRow2PageRow) activity.getTestFragment());
+        fragment.assertExecutedEntranceTransition();
+
+        // select item 2 on row 1.
+        selectAndWaitFragmentAnimation(fragment, 1, 2);
+        Bundle savedState = saveActivityState(activity);
+        activity.finish();
+
+        // start a new activity with the state
+        SingleFragmentTestActivity activity2 = launchAndWaitActivity(
+                RowsFragmentTest.F_standard.class,
+                new Options().savedInstance(savedState), 2000);
+        final F_3ListRow2PageRow fragment2 = ((F_3ListRow2PageRow) activity2.getTestFragment());
+        assertFalse(fragment2.isShowingHeaders());
+        fragment2.assertNoEntranceTransition();
+        assertEquals(1, fragment2.getSelectedPosition());
+        assertEquals(2, ((ListRowPresenter.ViewHolder) fragment2.getSelectedRowViewHolder())
+                .getSelectedPosition());
+        activity2.finish();
+    }
+
+    void mixedBrowseFragmentRestoreToSampleRowsFragment(final boolean hideFastLane)
+            throws Throwable {
+        final SingleFragmentTestActivity activity = launchAndWaitActivity(
+                RowsFragmentTest.F_3ListRow2PageRow.class, 2000);
+        final F_3ListRow2PageRow fragment = ((F_3ListRow2PageRow) activity.getTestFragment());
+        fragment.assertExecutedEntranceTransition();
+
+        // select row 3 which is mapped to SampleRowsFragment.
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                fragment.setSelectedPosition(3, true);
+            }
+        });
+        // Wait SampleRowsFragment being created
+        final SampleRowsFragment mainFragment = (SampleRowsFragment) fragment.waitPageFragment(
+                SampleRowsFragment.class);
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                if (hideFastLane) {
+                    fragment.startHeadersTransition(false);
+                }
+            }
+        });
+        // Wait header transition finishes
+        waitForHeaderTransition(fragment);
+        // Select item 1 on row 1 in SampleRowsFragment
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mainFragment.setSelectedPosition(1, true,
+                        new ListRowPresenter.SelectItemViewHolderTask(1));
+            }
+        });
+        // Save activity state
+        Bundle savedState = saveActivityState(activity);
+        activity.finish();
+
+        SingleFragmentTestActivity activity2 = launchAndWaitActivity(
+                RowsFragmentTest.F_3ListRow2PageRow.class,
+                new Options().savedInstance(savedState), 2000);
+        final F_3ListRow2PageRow fragment2 = ((F_3ListRow2PageRow) activity2.getTestFragment());
+        final SampleRowsFragment mainFragment2 = (SampleRowsFragment) fragment2.waitPageFragment(
+                SampleRowsFragment.class);
+        assertEquals(!hideFastLane, fragment2.isShowingHeaders());
+        fragment2.assertNoEntranceTransition();
+        // Validate BrowseFragment selected row 3 (mapped to SampleRowsFragment)
+        assertEquals(3, fragment2.getSelectedPosition());
+        // Validate SampleRowsFragment's selected row and selected item
+        assertEquals(1, mainFragment2.getSelectedPosition());
+        assertEquals(1, ((ListRowPresenter.ViewHolder) mainFragment2
+                .findRowViewHolderByPosition(1)).getSelectedPosition());
+        activity2.finish();
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
+    public void mixedBrowseFragmentRestoreToSampleRowsFragmentHideFastLane() throws Throwable {
+        mixedBrowseFragmentRestoreToSampleRowsFragment(true);
+
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
+    public void mixedBrowseFragmentRestoreToSampleRowsFragmentShowFastLane() throws Throwable {
+        mixedBrowseFragmentRestoreToSampleRowsFragment(false);
+    }
+
+    void mixedBrowseFragmentRestoreToSampleFragment(final boolean hideFastLane)
+            throws Throwable {
+        final SingleFragmentTestActivity activity = launchAndWaitActivity(
+                RowsFragmentTest.F_3ListRow2PageRow.class, 2000);
+        final F_3ListRow2PageRow fragment = ((F_3ListRow2PageRow) activity.getTestFragment());
+        fragment.assertExecutedEntranceTransition();
+
+        // select row 3 which is mapped to SampleFragment.
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                fragment.setSelectedPosition(4, true);
+            }
+        });
+        // Wait SampleFragment to be created
+        final SampleFragment mainFragment = (SampleFragment) fragment.waitPageFragment(
+                SampleFragment.class);
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                if (hideFastLane) {
+                    fragment.startHeadersTransition(false);
+                }
+            }
+        });
+        waitForHeaderTransition(fragment);
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                // change TextView content which should be saved in states.
+                TextView t = mainFragment.getView().findViewById(R.id.tv2);
+                t.setText("changed text");
+            }
+        });
+        // Save activity state
+        Bundle savedState = saveActivityState(activity);
+        activity.finish();
+
+        SingleFragmentTestActivity activity2 = launchAndWaitActivity(
+                RowsFragmentTest.F_3ListRow2PageRow.class,
+                new Options().savedInstance(savedState), 2000);
+        final F_3ListRow2PageRow fragment2 = ((F_3ListRow2PageRow) activity2.getTestFragment());
+        final SampleFragment mainFragment2 = (SampleFragment) fragment2.waitPageFragment(
+                SampleFragment.class);
+        assertEquals(!hideFastLane, fragment2.isShowingHeaders());
+        fragment2.assertNoEntranceTransition();
+        // Validate BrowseFragment selected row 3 (mapped to SampleFragment)
+        assertEquals(4, fragment2.getSelectedPosition());
+        // Validate SampleFragment's view states are restored
+        TextView t = mainFragment2.getView().findViewById(R.id.tv2);
+        assertEquals("changed text", t.getText().toString());
+        activity2.finish();
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
+    public void mixedBrowseFragmentRestoreToSampleFragmentHideFastLane() throws Throwable {
+        mixedBrowseFragmentRestoreToSampleFragment(true);
+
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
+    public void mixedBrowseFragmentRestoreToSampleFragmentShowFastLane() throws Throwable {
+        mixedBrowseFragmentRestoreToSampleFragment(false);
+    }
+
+
+}
diff --git a/leanback/tests/java/android/support/v17/leanback/app/RowsSupportFragmentTest.java b/leanback/tests/java/android/support/v17/leanback/app/RowsSupportFragmentTest.java
new file mode 100644
index 0000000..eca3f09
--- /dev/null
+++ b/leanback/tests/java/android/support/v17/leanback/app/RowsSupportFragmentTest.java
@@ -0,0 +1,1351 @@
+/*
+ * Copyright (C) 2016 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.v17.leanback.app;
+
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertNotSame;
+import static junit.framework.Assert.assertNull;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import android.graphics.Rect;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.LargeTest;
+import android.support.test.filters.SdkSuppress;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v17.leanback.test.R;
+import android.support.v17.leanback.testutils.PollingCheck;
+import android.support.v17.leanback.widget.ArrayObjectAdapter;
+import android.support.v17.leanback.widget.HeaderItem;
+import android.support.v17.leanback.widget.HorizontalGridView;
+import android.support.v17.leanback.widget.ItemBridgeAdapter;
+import android.support.v17.leanback.widget.ListRow;
+import android.support.v17.leanback.widget.ListRowPresenter;
+import android.support.v17.leanback.widget.ObjectAdapter;
+import android.support.v17.leanback.widget.OnItemViewClickedListener;
+import android.support.v17.leanback.widget.PageRow;
+import android.support.v17.leanback.widget.Presenter;
+import android.support.v17.leanback.widget.Row;
+import android.support.v17.leanback.widget.RowPresenter;
+import android.support.v17.leanback.widget.SinglePresenterSelector;
+import android.support.v17.leanback.widget.VerticalGridView;
+import android.support.v4.app.Fragment;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class RowsSupportFragmentTest extends SingleSupportFragmentTestBase {
+
+    static final StringPresenter sCardPresenter = new StringPresenter();
+
+    static void loadData(ArrayObjectAdapter adapter, int numRows, int repeatPerRow) {
+        for (int i = 0; i < numRows; ++i) {
+            ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(sCardPresenter);
+            int index = 0;
+            for (int j = 0; j < repeatPerRow; ++j) {
+                listRowAdapter.add("Hello world-" + (index++));
+                listRowAdapter.add("This is a test-" + (index++));
+                listRowAdapter.add("Android TV-" + (index++));
+                listRowAdapter.add("Leanback-" + (index++));
+                listRowAdapter.add("Hello world-" + (index++));
+                listRowAdapter.add("Android TV-" + (index++));
+                listRowAdapter.add("Leanback-" + (index++));
+                listRowAdapter.add("GuidedStepSupportFragment-" + (index++));
+            }
+            HeaderItem header = new HeaderItem(i, "Row " + i);
+            adapter.add(new ListRow(header, listRowAdapter));
+        }
+    }
+
+    static Bundle saveActivityState(final SingleSupportFragmentTestActivity activity) {
+        final Bundle[] savedState = new Bundle[1];
+        // save activity state
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                savedState[0] = activity.performSaveInstanceState();
+            }
+        });
+        return savedState[0];
+    }
+
+    static void waitForHeaderTransition(final F_Base fragment) {
+        // Wait header transition finishes
+        SystemClock.sleep(100);
+        PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return !fragment.isInHeadersTransition();
+            }
+        });
+    }
+
+    static void selectAndWaitFragmentAnimation(final F_Base fragment, final int row,
+            final int item) {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                fragment.setSelectedPosition(row, true,
+                        new ListRowPresenter.SelectItemViewHolderTask(item));
+            }
+        });
+        // Wait header transition finishes and scrolling stops
+        SystemClock.sleep(100);
+        PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return !fragment.isInHeadersTransition()
+                        && !fragment.getHeadersSupportFragment().isScrolling();
+            }
+        });
+    }
+
+    public static class F_defaultAlignment extends RowsSupportFragment {
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            ListRowPresenter lrp = new ListRowPresenter();
+            ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+            setAdapter(adapter);
+            loadData(adapter, 10, 1);
+        }
+    }
+
+    @Test
+    public void defaultAlignment() throws Throwable {
+        SingleSupportFragmentTestActivity activity = launchAndWaitActivity(F_defaultAlignment.class, 1000);
+
+        final Rect rect = new Rect();
+
+        final VerticalGridView gridView = ((RowsSupportFragment) activity.getTestFragment())
+                .getVerticalGridView();
+        View row0 = gridView.findViewHolderForAdapterPosition(0).itemView;
+        rect.set(0, 0, row0.getWidth(), row0.getHeight());
+        gridView.offsetDescendantRectToMyCoords(row0, rect);
+        assertEquals("First row is initially aligned to top of screen", 0, rect.top);
+
+        sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+        waitForScrollIdle(gridView);
+        View row1 = gridView.findViewHolderForAdapterPosition(1).itemView;
+        PollingCheck.waitFor(new PollingCheck.ViewStableOnScreen(row1));
+
+        rect.set(0, 0, row1.getWidth(), row1.getHeight());
+        gridView.offsetDescendantRectToMyCoords(row1, rect);
+        assertTrue("Second row should not be aligned to top of screen", rect.top > 0);
+    }
+
+    public static class F_selectBeforeSetAdapter extends RowsSupportFragment {
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            setSelectedPosition(7, false);
+            new Handler().postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    getVerticalGridView().requestLayout();
+                }
+            }, 100);
+            new Handler().postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    ListRowPresenter lrp = new ListRowPresenter();
+                    ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+                    setAdapter(adapter);
+                    loadData(adapter, 10, 1);
+                }
+            }, 1000);
+        }
+    }
+
+    @Test
+    public void selectBeforeSetAdapter() throws InterruptedException {
+        SingleSupportFragmentTestActivity activity =
+                launchAndWaitActivity(F_selectBeforeSetAdapter.class, 2000);
+
+        final VerticalGridView gridView = ((RowsSupportFragment) activity.getTestFragment())
+                .getVerticalGridView();
+        assertEquals(7, gridView.getSelectedPosition());
+        assertNotNull(gridView.findViewHolderForAdapterPosition(7));
+    }
+
+    public static class F_selectBeforeAddData extends RowsSupportFragment {
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            ListRowPresenter lrp = new ListRowPresenter();
+            final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+            setAdapter(adapter);
+            setSelectedPosition(7, false);
+            new Handler().postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    getVerticalGridView().requestLayout();
+                }
+            }, 100);
+            new Handler().postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    loadData(adapter, 10, 1);
+                }
+            }, 1000);
+        }
+    }
+
+    @Test
+    public void selectBeforeAddData() throws InterruptedException {
+        SingleSupportFragmentTestActivity activity =
+                launchAndWaitActivity(F_selectBeforeAddData.class, 2000);
+
+        final VerticalGridView gridView = ((RowsSupportFragment) activity.getTestFragment())
+                .getVerticalGridView();
+        assertEquals(7, gridView.getSelectedPosition());
+        assertNotNull(gridView.findViewHolderForAdapterPosition(7));
+    }
+
+    public static class F_selectAfterAddData extends RowsSupportFragment {
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            ListRowPresenter lrp = new ListRowPresenter();
+            final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+            setAdapter(adapter);
+            loadData(adapter, 10, 1);
+            new Handler().postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    setSelectedPosition(7, false);
+                }
+            }, 1000);
+        }
+    }
+
+    @Test
+    public void selectAfterAddData() throws InterruptedException {
+        SingleSupportFragmentTestActivity activity =
+                launchAndWaitActivity(F_selectAfterAddData.class, 2000);
+
+        final VerticalGridView gridView = ((RowsSupportFragment) activity.getTestFragment())
+                .getVerticalGridView();
+        assertEquals(7, gridView.getSelectedPosition());
+        assertNotNull(gridView.findViewHolderForAdapterPosition(7));
+    }
+
+    static WeakReference<F_restoreSelection> sLastF_restoreSelection;
+
+    public static class F_restoreSelection extends RowsSupportFragment {
+        public F_restoreSelection() {
+            sLastF_restoreSelection = new WeakReference<F_restoreSelection>(this);
+        }
+
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            ListRowPresenter lrp = new ListRowPresenter();
+            final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+            setAdapter(adapter);
+            loadData(adapter, 10, 1);
+            if (savedInstanceState == null) {
+                setSelectedPosition(7, false);
+            }
+        }
+    }
+
+    @Test
+    public void restoreSelection() {
+        final SingleSupportFragmentTestActivity activity =
+                launchAndWaitActivity(F_restoreSelection.class, 1000);
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        activity.recreate();
+                    }
+                }
+        );
+        SystemClock.sleep(1000);
+
+        // mActivity is invalid after recreate(), a new Activity instance is created
+        // but we could get Fragment from static variable.
+        RowsSupportFragment fragment = sLastF_restoreSelection.get();
+        final VerticalGridView gridView = fragment.getVerticalGridView();
+        assertEquals(7, gridView.getSelectedPosition());
+        assertNotNull(gridView.findViewHolderForAdapterPosition(7));
+
+    }
+
+    public static class F_ListRowWithOnClick extends RowsSupportFragment {
+        Presenter.ViewHolder mLastClickedItemViewHolder;
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            setOnItemViewClickedListener(new OnItemViewClickedListener() {
+                @Override
+                public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
+                        RowPresenter.ViewHolder rowViewHolder, Row row) {
+                    mLastClickedItemViewHolder = itemViewHolder;
+                }
+            });
+            ListRowPresenter lrp = new ListRowPresenter();
+            ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+            setAdapter(adapter);
+            loadData(adapter, 10, 1);
+        }
+    }
+
+    @Test
+    public void prefetchChildItemsBeforeAttach() throws Throwable {
+        SingleSupportFragmentTestActivity activity =
+                launchAndWaitActivity(F_ListRowWithOnClick.class, 1000);
+
+        F_ListRowWithOnClick fragment = (F_ListRowWithOnClick) activity.getTestFragment();
+        final VerticalGridView gridView = fragment.getVerticalGridView();
+        View lastRow = gridView.getChildAt(gridView.getChildCount() - 1);
+        final int lastRowPos = gridView.getChildAdapterPosition(lastRow);
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(
+                new Runnable() {
+                    public void run() {
+                        gridView.setSelectedPositionSmooth(lastRowPos);
+                    }
+                }
+        );
+        waitForScrollIdle(gridView);
+        ItemBridgeAdapter.ViewHolder prefetchedBridgeVh = (ItemBridgeAdapter.ViewHolder)
+                gridView.findViewHolderForAdapterPosition(lastRowPos + 1);
+        RowPresenter prefetchedRowPresenter = (RowPresenter) prefetchedBridgeVh.getPresenter();
+        final ListRowPresenter.ViewHolder prefetchedListRowVh = (ListRowPresenter.ViewHolder)
+                prefetchedRowPresenter.getRowViewHolder(prefetchedBridgeVh.getViewHolder());
+
+        fragment.mLastClickedItemViewHolder = null;
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(
+                new Runnable() {
+                    public void run() {
+                        prefetchedListRowVh.getItemViewHolder(0).view.performClick();
+                    }
+                }
+        );
+        assertSame(prefetchedListRowVh.getItemViewHolder(0), fragment.mLastClickedItemViewHolder);
+    }
+
+    @Test
+    public void changeHasStableIdToTrueAfterViewCreated() throws InterruptedException {
+        SingleSupportFragmentTestActivity activity =
+                launchAndWaitActivity(RowsSupportFragment.class, 2000);
+        final RowsSupportFragment fragment = (RowsSupportFragment) activity.getTestFragment();
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(
+                new Runnable() {
+                    public void run() {
+                        ObjectAdapter adapter = new ObjectAdapter() {
+                            @Override
+                            public int size() {
+                                return 0;
+                            }
+
+                            @Override
+                            public Object get(int position) {
+                                return null;
+                            }
+
+                            @Override
+                            public long getId(int position) {
+                                return 1;
+                            }
+                        };
+                        adapter.setHasStableIds(true);
+                        fragment.setAdapter(adapter);
+                    }
+                }
+        );
+    }
+
+    static class StableIdAdapter extends ObjectAdapter {
+        ArrayList<Integer> mList = new ArrayList();
+
+        @Override
+        public long getId(int position) {
+            return mList.get(position).longValue();
+        }
+
+        @Override
+        public Object get(int position) {
+            return mList.get(position);
+        }
+
+        @Override
+        public int size() {
+            return mList.size();
+        }
+    }
+
+    public static class F_rowNotifyItemRangeChange extends BrowseSupportFragment {
+
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            ListRowPresenter lrp = new ListRowPresenter();
+            final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+            for (int i = 0; i < 2; i++) {
+                StableIdAdapter listRowAdapter = new StableIdAdapter();
+                listRowAdapter.setHasStableIds(true);
+                listRowAdapter.setPresenterSelector(
+                        new SinglePresenterSelector(sCardPresenter));
+                int index = 0;
+                listRowAdapter.mList.add(index++);
+                listRowAdapter.mList.add(index++);
+                listRowAdapter.mList.add(index++);
+                HeaderItem header = new HeaderItem(i, "Row " + i);
+                adapter.add(new ListRow(header, listRowAdapter));
+            }
+            setAdapter(adapter);
+            new Handler().postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    StableIdAdapter rowAdapter = (StableIdAdapter)
+                            ((ListRow) adapter.get(1)).getAdapter();
+                    rowAdapter.notifyItemRangeChanged(0, 3);
+                }
+            }, 500);
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
+    public void rowNotifyItemRangeChange() throws InterruptedException {
+        SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
+                RowsSupportFragmentTest.F_rowNotifyItemRangeChange.class, 2000);
+
+        VerticalGridView verticalGridView = ((BrowseSupportFragment) activity.getTestFragment())
+                .getRowsSupportFragment().getVerticalGridView();
+        for (int i = 0; i < verticalGridView.getChildCount(); i++) {
+            HorizontalGridView horizontalGridView = verticalGridView.getChildAt(i)
+                    .findViewById(R.id.row_content);
+            for (int j = 0; j < horizontalGridView.getChildCount(); j++) {
+                assertEquals(horizontalGridView.getPaddingTop(),
+                        horizontalGridView.getChildAt(j).getTop());
+            }
+        }
+    }
+
+    public static class F_rowNotifyItemRangeChangeWithTransition extends BrowseSupportFragment {
+
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            ListRowPresenter lrp = new ListRowPresenter();
+            prepareEntranceTransition();
+            final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+            for (int i = 0; i < 2; i++) {
+                StableIdAdapter listRowAdapter = new StableIdAdapter();
+                listRowAdapter.setHasStableIds(true);
+                listRowAdapter.setPresenterSelector(
+                        new SinglePresenterSelector(sCardPresenter));
+                int index = 0;
+                listRowAdapter.mList.add(index++);
+                listRowAdapter.mList.add(index++);
+                listRowAdapter.mList.add(index++);
+                HeaderItem header = new HeaderItem(i, "Row " + i);
+                adapter.add(new ListRow(header, listRowAdapter));
+            }
+            setAdapter(adapter);
+            new Handler().postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    StableIdAdapter rowAdapter = (StableIdAdapter)
+                            ((ListRow) adapter.get(1)).getAdapter();
+                    rowAdapter.notifyItemRangeChanged(0, 3);
+                }
+            }, 500);
+            new Handler().postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    startEntranceTransition();
+                }
+            }, 520);
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
+    public void rowNotifyItemRangeChangeWithTransition() throws InterruptedException {
+        SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
+                        RowsSupportFragmentTest.F_rowNotifyItemRangeChangeWithTransition.class, 3000);
+
+        VerticalGridView verticalGridView = ((BrowseSupportFragment) activity.getTestFragment())
+                .getRowsSupportFragment().getVerticalGridView();
+        for (int i = 0; i < verticalGridView.getChildCount(); i++) {
+            HorizontalGridView horizontalGridView = verticalGridView.getChildAt(i)
+                    .findViewById(R.id.row_content);
+            for (int j = 0; j < horizontalGridView.getChildCount(); j++) {
+                assertEquals(horizontalGridView.getPaddingTop(),
+                        horizontalGridView.getChildAt(j).getTop());
+                assertEquals(0, horizontalGridView.getChildAt(j).getTranslationY(), 0.1f);
+            }
+        }
+    }
+
+    public static class F_Base extends BrowseSupportFragment {
+
+        List<Long> mEntranceTransitionStartTS = new ArrayList();
+        List<Long> mEntranceTransitionEndTS = new ArrayList();
+
+        @Override
+        protected void onEntranceTransitionStart() {
+            super.onEntranceTransitionStart();
+            mEntranceTransitionStartTS.add(SystemClock.uptimeMillis());
+        }
+
+        @Override
+        protected void onEntranceTransitionEnd() {
+            super.onEntranceTransitionEnd();
+            mEntranceTransitionEndTS.add(SystemClock.uptimeMillis());
+        }
+
+        public void assertExecutedEntranceTransition() {
+            assertEquals(1, mEntranceTransitionStartTS.size());
+            assertEquals(1, mEntranceTransitionEndTS.size());
+            assertTrue(mEntranceTransitionEndTS.get(0) - mEntranceTransitionStartTS.get(0) > 100);
+        }
+
+        public void assertNoEntranceTransition() {
+            assertEquals(0, mEntranceTransitionStartTS.size());
+            assertEquals(0, mEntranceTransitionEndTS.size());
+        }
+
+        /**
+         * Util to wait PageFragment swapped.
+         */
+        Fragment waitPageFragment(final Class pageFragmentClass) {
+            PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+                @Override
+                public boolean canProceed() {
+                    return pageFragmentClass.isInstance(getMainFragment())
+                            && getMainFragment().getView() != null;
+                }
+            });
+            return getMainFragment();
+        }
+
+        /**
+         * Wait until a fragment for non-page Row is created. Does not apply to the case a
+         * RowsSupportFragment is created on a PageRow.
+         */
+        RowsSupportFragment waitRowsSupportFragment() {
+            PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+                @Override
+                public boolean canProceed() {
+                    return mMainFragmentListRowDataAdapter != null
+                            && getMainFragment() instanceof RowsSupportFragment
+                            && !(getMainFragment() instanceof SampleRowsSupportFragment);
+                }
+            });
+            return (RowsSupportFragment) getMainFragment();
+        }
+    }
+
+    static ObjectAdapter createListRowAdapter() {
+        StableIdAdapter listRowAdapter = new StableIdAdapter();
+        listRowAdapter.setHasStableIds(false);
+        listRowAdapter.setPresenterSelector(
+                new SinglePresenterSelector(sCardPresenter));
+        int index = 0;
+        listRowAdapter.mList.add(index++);
+        listRowAdapter.mList.add(index++);
+        listRowAdapter.mList.add(index++);
+        return listRowAdapter;
+    }
+
+    /**
+     * Create BrowseSupportFragmentAdapter with 3 ListRows
+     */
+    static ArrayObjectAdapter createListRowsAdapter() {
+        ListRowPresenter lrp = new ListRowPresenter();
+        final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+        for (int i = 0; i < 3; i++) {
+            ObjectAdapter listRowAdapter = createListRowAdapter();
+            HeaderItem header = new HeaderItem(i, "Row " + i);
+            adapter.add(new ListRow(header, listRowAdapter));
+        }
+        return adapter;
+    }
+
+    /**
+     * A typical BrowseSupportFragment with multiple rows that start entrance transition
+     */
+    public static class F_standard extends F_Base {
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            if (savedInstanceState == null) {
+                prepareEntranceTransition();
+            }
+            new Handler().postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    setAdapter(createListRowsAdapter());
+                    startEntranceTransition();
+                }
+            }, 100);
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
+    public void browseFragmentSetNullAdapter() throws InterruptedException {
+        final SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
+                RowsSupportFragmentTest.F_standard.class, 2000);
+        final F_standard fragment = ((F_standard) activity.getTestFragment());
+        fragment.assertExecutedEntranceTransition();
+
+        final ObjectAdapter adapter1 = fragment.getAdapter();
+        ListRowDataAdapter wrappedAdapter = fragment.mMainFragmentListRowDataAdapter;
+        assertTrue(adapter1.hasObserver());
+        assertTrue(wrappedAdapter.hasObserver());
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                fragment.setAdapter(null);
+            }
+        });
+        // adapter should no longer has observer and there is no reference to adapter from
+        // BrowseSupportFragment.
+        assertFalse(adapter1.hasObserver());
+        assertFalse(wrappedAdapter.hasObserver());
+        assertNull(fragment.getAdapter());
+        assertNull(fragment.mMainFragmentListRowDataAdapter);
+        // RowsSupportFragment is still there
+        assertTrue(fragment.mMainFragment instanceof RowsSupportFragment);
+        assertNotNull(fragment.mMainFragmentRowsAdapter);
+        assertNotNull(fragment.mMainFragmentAdapter);
+
+        // initialize to same adapter
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                fragment.setAdapter(adapter1);
+            }
+        });
+        assertTrue(adapter1.hasObserver());
+        assertNotSame(wrappedAdapter, fragment.mMainFragmentListRowDataAdapter);
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
+    public void browseFragmentChangeAdapter() throws InterruptedException {
+        final SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
+                RowsSupportFragmentTest.F_standard.class, 2000);
+        final F_standard fragment = ((F_standard) activity.getTestFragment());
+        fragment.assertExecutedEntranceTransition();
+
+        final ObjectAdapter adapter1 = fragment.getAdapter();
+        ListRowDataAdapter wrappedAdapter = fragment.mMainFragmentListRowDataAdapter;
+        assertTrue(adapter1.hasObserver());
+        assertTrue(wrappedAdapter.hasObserver());
+        final ObjectAdapter adapter2 = createListRowsAdapter();
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                fragment.setAdapter(adapter2);
+            }
+        });
+        // adapter1 should no longer has observer and adapter2 will have observer
+        assertFalse(adapter1.hasObserver());
+        assertFalse(wrappedAdapter.hasObserver());
+        assertSame(adapter2, fragment.getAdapter());
+        assertNotSame(wrappedAdapter, fragment.mMainFragmentListRowDataAdapter);
+        assertTrue(adapter2.hasObserver());
+        assertTrue(fragment.mMainFragmentListRowDataAdapter.hasObserver());
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
+    public void browseFragmentChangeAdapterToPage() throws InterruptedException {
+        final SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
+                RowsSupportFragmentTest.F_standard.class, 2000);
+        final F_standard fragment = ((F_standard) activity.getTestFragment());
+        fragment.assertExecutedEntranceTransition();
+
+        final ObjectAdapter adapter1 = fragment.getAdapter();
+        ListRowDataAdapter wrappedAdapter = fragment.mMainFragmentListRowDataAdapter;
+        assertTrue(adapter1.hasObserver());
+        assertTrue(wrappedAdapter.hasObserver());
+        final ObjectAdapter adapter2 = create2PageRow3ListRow();
+        fragment.getMainFragmentRegistry().registerFragment(MyPageRow.class,
+                new MyFragmentFactory());
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                fragment.setAdapter(adapter2);
+            }
+        });
+        fragment.waitPageFragment(SampleRowsSupportFragment.class);
+        // adapter1 should no longer has observer and adapter2 will have observer
+        assertFalse(adapter1.hasObserver());
+        assertFalse(wrappedAdapter.hasObserver());
+        assertSame(adapter2, fragment.getAdapter());
+        assertNull(fragment.mMainFragmentListRowDataAdapter);
+        assertTrue(adapter2.hasObserver());
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
+    public void browseFragmentNotifyDataChangeListRowToPage() throws InterruptedException {
+        final SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
+                RowsSupportFragmentTest.F_standard.class, 2000);
+        final F_standard fragment = ((F_standard) activity.getTestFragment());
+        fragment.assertExecutedEntranceTransition();
+
+        final ArrayObjectAdapter adapter1 = (ArrayObjectAdapter) fragment.getAdapter();
+        ListRowDataAdapter wrappedAdapter = fragment.mMainFragmentListRowDataAdapter;
+        assertTrue(adapter1.hasObserver());
+        assertTrue(wrappedAdapter.hasObserver());
+
+        fragment.getMainFragmentRegistry().registerFragment(MyPageRow.class,
+                new MyFragmentFactory());
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                adapter1.removeItems(0, 1);
+                adapter1.add(0, new MyPageRow(0));
+            }
+        });
+        fragment.waitPageFragment(SampleRowsSupportFragment.class);
+        assertTrue(adapter1.hasObserver());
+        assertNull(fragment.mMainFragmentListRowDataAdapter);
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
+    public void browseFragmentNotifyItemChangeListRowToPage() throws InterruptedException {
+        final SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
+                RowsSupportFragmentTest.F_standard.class, 2000);
+        final F_standard fragment = ((F_standard) activity.getTestFragment());
+        fragment.assertExecutedEntranceTransition();
+
+        final ArrayObjectAdapter adapter1 = (ArrayObjectAdapter) fragment.getAdapter();
+        ListRowDataAdapter wrappedAdapter = fragment.mMainFragmentListRowDataAdapter;
+        assertTrue(adapter1.hasObserver());
+        assertTrue(wrappedAdapter.hasObserver());
+
+        fragment.getMainFragmentRegistry().registerFragment(MyPageRow.class,
+                new MyFragmentFactory());
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                adapter1.replace(0, new MyPageRow(0));
+            }
+        });
+        fragment.waitPageFragment(SampleRowsSupportFragment.class);
+        assertTrue(adapter1.hasObserver());
+        assertNull(fragment.mMainFragmentListRowDataAdapter);
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
+    public void browseFragmentNotifyDataChangeListRowToListRow() throws InterruptedException {
+        final SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
+                RowsSupportFragmentTest.F_standard.class, 2000);
+        final F_standard fragment = ((F_standard) activity.getTestFragment());
+        fragment.assertExecutedEntranceTransition();
+
+        final ArrayObjectAdapter adapter1 = (ArrayObjectAdapter) fragment.getAdapter();
+        ListRowDataAdapter wrappedAdapter = fragment.mMainFragmentListRowDataAdapter;
+        assertTrue(adapter1.hasObserver());
+        assertTrue(wrappedAdapter.hasObserver());
+
+        fragment.getMainFragmentRegistry().registerFragment(MyPageRow.class,
+                new MyFragmentFactory());
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                ObjectAdapter listRowAdapter = createListRowAdapter();
+                HeaderItem header = new HeaderItem(0, "Row 0 changed");
+                adapter1.removeItems(0, 1);
+                adapter1.add(0, new ListRow(header, listRowAdapter));
+            }
+        });
+        assertTrue(adapter1.hasObserver());
+        assertTrue(wrappedAdapter.hasObserver());
+        assertSame(wrappedAdapter, fragment.mMainFragmentListRowDataAdapter);
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
+    public void browseFragmentNotifyItemChangeListRowToListRow() throws InterruptedException {
+        final SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
+                RowsSupportFragmentTest.F_standard.class, 2000);
+        final F_standard fragment = ((F_standard) activity.getTestFragment());
+        fragment.assertExecutedEntranceTransition();
+
+        final ArrayObjectAdapter adapter1 = (ArrayObjectAdapter) fragment.getAdapter();
+        ListRowDataAdapter wrappedAdapter = fragment.mMainFragmentListRowDataAdapter;
+        assertTrue(adapter1.hasObserver());
+        assertTrue(wrappedAdapter.hasObserver());
+
+        fragment.getMainFragmentRegistry().registerFragment(MyPageRow.class,
+                new MyFragmentFactory());
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                ObjectAdapter listRowAdapter = createListRowAdapter();
+                HeaderItem header = new HeaderItem(0, "Row 0 changed");
+                adapter1.replace(0, new ListRow(header, listRowAdapter));
+            }
+        });
+        assertTrue(adapter1.hasObserver());
+        assertTrue(wrappedAdapter.hasObserver());
+        assertSame(wrappedAdapter, fragment.mMainFragmentListRowDataAdapter);
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
+    public void browseFragmentChangeAdapterPageToPage() throws InterruptedException {
+        final SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
+                RowsSupportFragmentTest.F_2PageRow3ListRow.class, 2000);
+        final F_2PageRow3ListRow fragment = ((F_2PageRow3ListRow) activity.getTestFragment());
+        fragment.assertExecutedEntranceTransition();
+
+        final ObjectAdapter adapter1 = fragment.getAdapter();
+        assertNull(fragment.mMainFragmentListRowDataAdapter);
+        assertTrue(adapter1.hasObserver());
+        final ObjectAdapter adapter2 = create2PageRow3ListRow();
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                fragment.setAdapter(adapter2);
+            }
+        });
+        fragment.waitPageFragment(SampleRowsSupportFragment.class);
+        // adapter1 should no longer has observer and adapter2 will have observer
+        assertFalse(adapter1.hasObserver());
+        assertSame(adapter2, fragment.getAdapter());
+        assertNull(fragment.mMainFragmentListRowDataAdapter);
+        assertTrue(adapter2.hasObserver());
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
+    public void browseFragmentNotifyChangePageToPage() throws InterruptedException {
+        final SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
+                RowsSupportFragmentTest.F_2PageRow3ListRow.class, 2000);
+        final F_2PageRow3ListRow fragment = ((F_2PageRow3ListRow) activity.getTestFragment());
+        fragment.assertExecutedEntranceTransition();
+
+        final ArrayObjectAdapter adapter1 = (ArrayObjectAdapter) fragment.getAdapter();
+        assertNull(fragment.mMainFragmentListRowDataAdapter);
+        assertTrue(adapter1.hasObserver());
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                adapter1.removeItems(0, 1);
+                adapter1.add(0, new MyPageRow(1));
+            }
+        });
+        fragment.waitPageFragment(SampleFragment.class);
+        // adapter1 should no longer has observer and adapter2 will have observer
+        assertTrue(adapter1.hasObserver());
+        assertNull(fragment.mMainFragmentListRowDataAdapter);
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
+    public void browseFragmentNotifyItemChangePageToPage() throws InterruptedException {
+        final SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
+                RowsSupportFragmentTest.F_2PageRow3ListRow.class, 2000);
+        final F_2PageRow3ListRow fragment = ((F_2PageRow3ListRow) activity.getTestFragment());
+        fragment.assertExecutedEntranceTransition();
+
+        final ArrayObjectAdapter adapter1 = (ArrayObjectAdapter) fragment.getAdapter();
+        assertNull(fragment.mMainFragmentListRowDataAdapter);
+        assertTrue(adapter1.hasObserver());
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                adapter1.replace(0, new MyPageRow(1));
+            }
+        });
+        fragment.waitPageFragment(SampleFragment.class);
+        // adapter1 should no longer has observer and adapter2 will have observer
+        assertTrue(adapter1.hasObserver());
+        assertNull(fragment.mMainFragmentListRowDataAdapter);
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
+    public void browseFragmentChangeAdapterPageToListRow() throws InterruptedException {
+        final SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
+                RowsSupportFragmentTest.F_2PageRow3ListRow.class, 2000);
+        final F_2PageRow3ListRow fragment = ((F_2PageRow3ListRow) activity.getTestFragment());
+        fragment.assertExecutedEntranceTransition();
+
+        final ObjectAdapter adapter1 = fragment.getAdapter();
+        assertNull(fragment.mMainFragmentListRowDataAdapter);
+        assertTrue(adapter1.hasObserver());
+        final ObjectAdapter adapter2 = createListRowsAdapter();
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                fragment.setAdapter(adapter2);
+            }
+        });
+        fragment.waitRowsSupportFragment();
+        // adapter1 should no longer has observer and adapter2 will have observer
+        assertFalse(adapter1.hasObserver());
+        assertSame(adapter2, fragment.getAdapter());
+        assertTrue(adapter2.hasObserver());
+        assertTrue(fragment.mMainFragmentListRowDataAdapter.hasObserver());
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
+    public void browseFragmentNotifyDataChangePageToListRow() throws InterruptedException {
+        final SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
+                RowsSupportFragmentTest.F_2PageRow3ListRow.class, 2000);
+        final F_2PageRow3ListRow fragment = ((F_2PageRow3ListRow) activity.getTestFragment());
+        fragment.assertExecutedEntranceTransition();
+
+        final ArrayObjectAdapter adapter1 = (ArrayObjectAdapter) fragment.getAdapter();
+        assertNull(fragment.mMainFragmentListRowDataAdapter);
+        assertTrue(adapter1.hasObserver());
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                ObjectAdapter listRowAdapter = createListRowAdapter();
+                HeaderItem header = new HeaderItem(0, "Row 0 changed");
+                adapter1.removeItems(0, 1);
+                adapter1.add(0, new ListRow(header, listRowAdapter));
+            }
+        });
+        fragment.waitRowsSupportFragment();
+        assertTrue(adapter1.hasObserver());
+        assertTrue(fragment.mMainFragmentListRowDataAdapter.hasObserver());
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
+    public void browseFragmentNotifyItemChangePageToListRow() throws InterruptedException {
+        final SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
+                RowsSupportFragmentTest.F_2PageRow3ListRow.class, 2000);
+        final F_2PageRow3ListRow fragment = ((F_2PageRow3ListRow) activity.getTestFragment());
+        fragment.assertExecutedEntranceTransition();
+
+        final ArrayObjectAdapter adapter1 = (ArrayObjectAdapter) fragment.getAdapter();
+        assertNull(fragment.mMainFragmentListRowDataAdapter);
+        assertTrue(adapter1.hasObserver());
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                ObjectAdapter listRowAdapter = createListRowAdapter();
+                HeaderItem header = new HeaderItem(0, "Row 0 changed");
+                adapter1.replace(0, new ListRow(header, listRowAdapter));
+            }
+        });
+        fragment.waitRowsSupportFragment();
+        assertTrue(adapter1.hasObserver());
+        assertTrue(fragment.mMainFragmentListRowDataAdapter.hasObserver());
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
+    public void browseFragmentRestore() throws InterruptedException {
+        final SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
+                RowsSupportFragmentTest.F_standard.class, 2000);
+        final F_standard fragment = ((F_standard) activity.getTestFragment());
+        fragment.assertExecutedEntranceTransition();
+
+        // select item 2 on row 1
+        selectAndWaitFragmentAnimation(fragment, 1, 2);
+        // save activity to state
+        Bundle savedState = saveActivityState(activity);
+        activity.finish();
+
+        // recreate activity with saved state
+        SingleSupportFragmentTestActivity activity2 = launchAndWaitActivity(
+                RowsSupportFragmentTest.F_standard.class,
+                new Options().savedInstance(savedState), 2000);
+        final F_standard fragment2 = ((F_standard) activity2.getTestFragment());
+        // validate restored activity selected row and selected item
+        fragment2.assertNoEntranceTransition();
+        assertEquals(1, fragment2.getSelectedPosition());
+        assertEquals(2, ((ListRowPresenter.ViewHolder) fragment2.getSelectedRowViewHolder())
+                .getSelectedPosition());
+        activity2.finish();
+    }
+
+    public static class MyPageRow extends PageRow {
+        public int type;
+        public MyPageRow(int type) {
+            super(new HeaderItem(100 + type, "page type " + type));
+            this.type = type;
+        }
+    }
+
+    /**
+     * A RowsSupportFragment that is a separate page in BrowseSupportFragment.
+     */
+    public static class SampleRowsSupportFragment extends RowsSupportFragment {
+        public SampleRowsSupportFragment() {
+            // simulates late data loading:
+            new Handler().postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    setAdapter(createListRowsAdapter());
+                    if (getMainFragmentAdapter() != null) {
+                        getMainFragmentAdapter().getFragmentHost()
+                                .notifyDataReady(getMainFragmentAdapter());
+                    }
+                }
+            }, 500);
+        }
+    }
+
+    /**
+     * A custom Fragment that is a separate page in BrowseSupportFragment.
+     */
+    public static class SampleFragment extends Fragment implements
+            BrowseSupportFragment.MainFragmentAdapterProvider {
+
+        public static class PageFragmentAdapterImpl extends
+                BrowseSupportFragment.MainFragmentAdapter<SampleFragment> {
+
+            public PageFragmentAdapterImpl(SampleFragment fragment) {
+                super(fragment);
+                setScalingEnabled(true);
+            }
+
+            @Override
+            public void setEntranceTransitionState(boolean state) {
+                getFragment().setEntranceTransitionState(state);
+            }
+        }
+
+        final PageFragmentAdapterImpl mMainFragmentAdapter = new PageFragmentAdapterImpl(this);
+
+        void setEntranceTransitionState(boolean state) {
+            final View view = getView();
+            int visibility = state ? View.VISIBLE : View.INVISIBLE;
+            view.findViewById(R.id.tv1).setVisibility(visibility);
+            view.findViewById(R.id.tv2).setVisibility(visibility);
+            view.findViewById(R.id.tv3).setVisibility(visibility);
+        }
+
+        @Override
+        public View onCreateView(
+                final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+            View view = inflater.inflate(R.layout.page_fragment, container, false);
+            return view;
+        }
+
+        @Override
+        public void onViewCreated(View view, Bundle savedInstanceState) {
+            // static layout has view and data ready immediately
+            mMainFragmentAdapter.getFragmentHost().notifyViewCreated(mMainFragmentAdapter);
+            mMainFragmentAdapter.getFragmentHost().notifyDataReady(mMainFragmentAdapter);
+        }
+
+        @Override
+        public BrowseSupportFragment.MainFragmentAdapter getMainFragmentAdapter() {
+            return mMainFragmentAdapter;
+        }
+    }
+
+    /**
+     * Create BrowseSupportFragmentAdapter with 3 ListRows and 2 PageRows
+     */
+    private static ArrayObjectAdapter create3ListRow2PageRowAdapter() {
+        ListRowPresenter lrp = new ListRowPresenter();
+        final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+        for (int i = 0; i < 3; i++) {
+            StableIdAdapter listRowAdapter = new StableIdAdapter();
+            listRowAdapter.setHasStableIds(false);
+            listRowAdapter.setPresenterSelector(
+                    new SinglePresenterSelector(sCardPresenter));
+            int index = 0;
+            listRowAdapter.mList.add(index++);
+            listRowAdapter.mList.add(index++);
+            listRowAdapter.mList.add(index++);
+            HeaderItem header = new HeaderItem(i, "Row " + i);
+            adapter.add(new ListRow(header, listRowAdapter));
+        }
+        adapter.add(new MyPageRow(0));
+        adapter.add(new MyPageRow(1));
+        return adapter;
+    }
+
+    /**
+     * Create BrowseSupportFragmentAdapter with 2 PageRows then 3 ListRow
+     */
+    private static ArrayObjectAdapter create2PageRow3ListRow() {
+        ListRowPresenter lrp = new ListRowPresenter();
+        final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+        adapter.add(new MyPageRow(0));
+        adapter.add(new MyPageRow(1));
+        for (int i = 0; i < 3; i++) {
+            StableIdAdapter listRowAdapter = new StableIdAdapter();
+            listRowAdapter.setHasStableIds(false);
+            listRowAdapter.setPresenterSelector(
+                    new SinglePresenterSelector(sCardPresenter));
+            int index = 0;
+            listRowAdapter.mList.add(index++);
+            listRowAdapter.mList.add(index++);
+            listRowAdapter.mList.add(index++);
+            HeaderItem header = new HeaderItem(i, "Row " + i);
+            adapter.add(new ListRow(header, listRowAdapter));
+        }
+        return adapter;
+    }
+
+    static class MyFragmentFactory extends BrowseSupportFragment.FragmentFactory {
+        @Override
+        public Fragment createFragment(Object rowObj) {
+            MyPageRow row = (MyPageRow) rowObj;
+            if (row.type == 0) {
+                return new SampleRowsSupportFragment();
+            } else if (row.type == 1) {
+                return new SampleFragment();
+            }
+            return null;
+        }
+    }
+
+    /**
+     * A BrowseSupportFragment with three ListRows, one SampleRowsSupportFragment and one SampleFragment.
+     */
+    public static class F_3ListRow2PageRow extends F_Base {
+        public F_3ListRow2PageRow() {
+            getMainFragmentRegistry().registerFragment(MyPageRow.class, new MyFragmentFactory());
+        }
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            if (savedInstanceState == null) {
+                prepareEntranceTransition();
+            }
+            new Handler().postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    setAdapter(create3ListRow2PageRowAdapter());
+                    startEntranceTransition();
+                }
+            }, 100);
+        }
+    }
+
+    /**
+     * A BrowseSupportFragment with three ListRows, one SampleRowsSupportFragment and one SampleFragment.
+     */
+    public static class F_2PageRow3ListRow extends F_Base {
+        public F_2PageRow3ListRow() {
+            getMainFragmentRegistry().registerFragment(MyPageRow.class, new MyFragmentFactory());
+        }
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            if (savedInstanceState == null) {
+                prepareEntranceTransition();
+            }
+            new Handler().postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    setAdapter(create2PageRow3ListRow());
+                    startEntranceTransition();
+                }
+            }, 100);
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
+    public void mixedBrowseSupportFragmentRestoreToListRow() throws Throwable {
+        final SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
+                RowsSupportFragmentTest.F_3ListRow2PageRow.class, 2000);
+        final F_3ListRow2PageRow fragment = ((F_3ListRow2PageRow) activity.getTestFragment());
+        fragment.assertExecutedEntranceTransition();
+
+        // select item 2 on row 1.
+        selectAndWaitFragmentAnimation(fragment, 1, 2);
+        Bundle savedState = saveActivityState(activity);
+        activity.finish();
+
+        // start a new activity with the state
+        SingleSupportFragmentTestActivity activity2 = launchAndWaitActivity(
+                RowsSupportFragmentTest.F_standard.class,
+                new Options().savedInstance(savedState), 2000);
+        final F_3ListRow2PageRow fragment2 = ((F_3ListRow2PageRow) activity2.getTestFragment());
+        assertFalse(fragment2.isShowingHeaders());
+        fragment2.assertNoEntranceTransition();
+        assertEquals(1, fragment2.getSelectedPosition());
+        assertEquals(2, ((ListRowPresenter.ViewHolder) fragment2.getSelectedRowViewHolder())
+                .getSelectedPosition());
+        activity2.finish();
+    }
+
+    void mixedBrowseSupportFragmentRestoreToSampleRowsSupportFragment(final boolean hideFastLane)
+            throws Throwable {
+        final SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
+                RowsSupportFragmentTest.F_3ListRow2PageRow.class, 2000);
+        final F_3ListRow2PageRow fragment = ((F_3ListRow2PageRow) activity.getTestFragment());
+        fragment.assertExecutedEntranceTransition();
+
+        // select row 3 which is mapped to SampleRowsSupportFragment.
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                fragment.setSelectedPosition(3, true);
+            }
+        });
+        // Wait SampleRowsSupportFragment being created
+        final SampleRowsSupportFragment mainFragment = (SampleRowsSupportFragment) fragment.waitPageFragment(
+                SampleRowsSupportFragment.class);
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                if (hideFastLane) {
+                    fragment.startHeadersTransition(false);
+                }
+            }
+        });
+        // Wait header transition finishes
+        waitForHeaderTransition(fragment);
+        // Select item 1 on row 1 in SampleRowsSupportFragment
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mainFragment.setSelectedPosition(1, true,
+                        new ListRowPresenter.SelectItemViewHolderTask(1));
+            }
+        });
+        // Save activity state
+        Bundle savedState = saveActivityState(activity);
+        activity.finish();
+
+        SingleSupportFragmentTestActivity activity2 = launchAndWaitActivity(
+                RowsSupportFragmentTest.F_3ListRow2PageRow.class,
+                new Options().savedInstance(savedState), 2000);
+        final F_3ListRow2PageRow fragment2 = ((F_3ListRow2PageRow) activity2.getTestFragment());
+        final SampleRowsSupportFragment mainFragment2 = (SampleRowsSupportFragment) fragment2.waitPageFragment(
+                SampleRowsSupportFragment.class);
+        assertEquals(!hideFastLane, fragment2.isShowingHeaders());
+        fragment2.assertNoEntranceTransition();
+        // Validate BrowseSupportFragment selected row 3 (mapped to SampleRowsSupportFragment)
+        assertEquals(3, fragment2.getSelectedPosition());
+        // Validate SampleRowsSupportFragment's selected row and selected item
+        assertEquals(1, mainFragment2.getSelectedPosition());
+        assertEquals(1, ((ListRowPresenter.ViewHolder) mainFragment2
+                .findRowViewHolderByPosition(1)).getSelectedPosition());
+        activity2.finish();
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
+    public void mixedBrowseSupportFragmentRestoreToSampleRowsSupportFragmentHideFastLane() throws Throwable {
+        mixedBrowseSupportFragmentRestoreToSampleRowsSupportFragment(true);
+
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
+    public void mixedBrowseSupportFragmentRestoreToSampleRowsSupportFragmentShowFastLane() throws Throwable {
+        mixedBrowseSupportFragmentRestoreToSampleRowsSupportFragment(false);
+    }
+
+    void mixedBrowseSupportFragmentRestoreToSampleFragment(final boolean hideFastLane)
+            throws Throwable {
+        final SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
+                RowsSupportFragmentTest.F_3ListRow2PageRow.class, 2000);
+        final F_3ListRow2PageRow fragment = ((F_3ListRow2PageRow) activity.getTestFragment());
+        fragment.assertExecutedEntranceTransition();
+
+        // select row 3 which is mapped to SampleFragment.
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                fragment.setSelectedPosition(4, true);
+            }
+        });
+        // Wait SampleFragment to be created
+        final SampleFragment mainFragment = (SampleFragment) fragment.waitPageFragment(
+                SampleFragment.class);
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                if (hideFastLane) {
+                    fragment.startHeadersTransition(false);
+                }
+            }
+        });
+        waitForHeaderTransition(fragment);
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                // change TextView content which should be saved in states.
+                TextView t = mainFragment.getView().findViewById(R.id.tv2);
+                t.setText("changed text");
+            }
+        });
+        // Save activity state
+        Bundle savedState = saveActivityState(activity);
+        activity.finish();
+
+        SingleSupportFragmentTestActivity activity2 = launchAndWaitActivity(
+                RowsSupportFragmentTest.F_3ListRow2PageRow.class,
+                new Options().savedInstance(savedState), 2000);
+        final F_3ListRow2PageRow fragment2 = ((F_3ListRow2PageRow) activity2.getTestFragment());
+        final SampleFragment mainFragment2 = (SampleFragment) fragment2.waitPageFragment(
+                SampleFragment.class);
+        assertEquals(!hideFastLane, fragment2.isShowingHeaders());
+        fragment2.assertNoEntranceTransition();
+        // Validate BrowseSupportFragment selected row 3 (mapped to SampleFragment)
+        assertEquals(4, fragment2.getSelectedPosition());
+        // Validate SampleFragment's view states are restored
+        TextView t = mainFragment2.getView().findViewById(R.id.tv2);
+        assertEquals("changed text", t.getText().toString());
+        activity2.finish();
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
+    public void mixedBrowseSupportFragmentRestoreToSampleFragmentHideFastLane() throws Throwable {
+        mixedBrowseSupportFragmentRestoreToSampleFragment(true);
+
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
+    public void mixedBrowseSupportFragmentRestoreToSampleFragmentShowFastLane() throws Throwable {
+        mixedBrowseSupportFragmentRestoreToSampleFragment(false);
+    }
+
+
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestActivity.java b/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestActivity.java
similarity index 71%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestActivity.java
rename to leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestActivity.java
index 6047a1e..6596daa 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestActivity.java
+++ b/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestActivity.java
@@ -1,3 +1,6 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from SingleSupportFragmentTestActivity.java.  DO NOT MODIFY. */
+
 /*
  * Copyright (C) 2016 The Android Open Source Project
  *
@@ -15,12 +18,12 @@
  */
 package android.support.v17.leanback.app;
 
-import android.app.Activity;
-import android.app.Fragment;
-import android.app.FragmentTransaction;
 import android.content.Intent;
 import android.os.Bundle;
 import android.support.v17.leanback.test.R;
+import android.app.Fragment;
+import android.app.Activity;
+import android.app.FragmentTransaction;
 import android.util.Log;
 
 public class SingleFragmentTestActivity extends Activity {
@@ -33,10 +36,26 @@
     public static final String EXTRA_ACTIVITY_LAYOUT = "activityLayout";
 
     public static final String EXTRA_UI_VISIBILITY = "uiVisibility";
+
+    public static final String EXTRA_OVERRIDDEN_SAVED_INSTANCE_STATE =
+            "overriddenSavedInstanceState";
+
     private static final String TAG = "TestActivity";
 
+    private Bundle overrideSavedInstance(Bundle savedInstance) {
+        Intent intent = getIntent();
+        if (intent != null) {
+            Bundle b = intent.getBundleExtra(EXTRA_OVERRIDDEN_SAVED_INSTANCE_STATE);
+            if (b != null) {
+                return b;
+            }
+        }
+        return savedInstance;
+    }
+
     @Override
     public void onCreate(Bundle savedInstanceState) {
+        savedInstanceState = overrideSavedInstance(savedInstanceState);
         super.onCreate(savedInstanceState);
         Log.d(TAG, "onCreate " + this);
         Intent intent = getIntent();
@@ -61,6 +80,17 @@
         }
     }
 
+    @Override
+    protected void onRestoreInstanceState(Bundle savedInstanceState) {
+        super.onRestoreInstanceState(overrideSavedInstance(savedInstanceState));
+    }
+
+    public Bundle performSaveInstanceState() {
+        Bundle state = new Bundle();
+        onSaveInstanceState(state);
+        return state;
+    }
+
     public Fragment getTestFragment() {
         return getFragmentManager().findFragmentById(R.id.main_frame);
     }
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestBase.java b/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestBase.java
similarity index 88%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestBase.java
rename to leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestBase.java
index b26d92d..150ccae 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestBase.java
+++ b/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestBase.java
@@ -1,3 +1,6 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from SingleSupportFrgamentTestBase.java.  DO NOT MODIFY. */
+
 /*
  * Copyright (C) 2016 The Android Open Source Project
  *
@@ -16,6 +19,7 @@
 package android.support.v17.leanback.app;
 
 import android.content.Intent;
+import android.os.Bundle;
 import android.os.SystemClock;
 import android.support.test.InstrumentationRegistry;
 import android.support.test.rule.ActivityTestRule;
@@ -47,6 +51,7 @@
     public static class Options {
         int mActivityLayoutId;
         int mUiVisibility;
+        Bundle mSavedInstance;
 
         public Options() {
         }
@@ -61,6 +66,11 @@
             return this;
         }
 
+        public Options savedInstance(Bundle savedInstance) {
+            mSavedInstance = savedInstance;
+            return this;
+        }
+
         public void collect(Intent intent) {
             if (mActivityLayoutId != 0) {
                 intent.putExtra(SingleFragmentTestActivity.EXTRA_ACTIVITY_LAYOUT,
@@ -69,6 +79,10 @@
             if (mUiVisibility != 0) {
                 intent.putExtra(SingleFragmentTestActivity.EXTRA_UI_VISIBILITY, mUiVisibility);
             }
+            if (mSavedInstance != null) {
+                intent.putExtra(SingleFragmentTestActivity.EXTRA_OVERRIDDEN_SAVED_INSTANCE_STATE,
+                        mSavedInstance);
+            }
         }
     }
 
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestActivity.java b/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestActivity.java
similarity index 74%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestActivity.java
rename to leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestActivity.java
index 0fc3183..eeb6262 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestActivity.java
+++ b/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestActivity.java
@@ -1,6 +1,3 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from SingleFragmentTestActivity.java.  DO NOT MODIFY. */
-
 /*
  * Copyright (C) 2016 The Android Open Source Project
  *
@@ -18,12 +15,12 @@
  */
 package android.support.v17.leanback.app;
 
-import android.support.v4.app.FragmentActivity;
-import android.support.v4.app.Fragment;
-import android.support.v4.app.FragmentTransaction;
 import android.content.Intent;
 import android.os.Bundle;
 import android.support.v17.leanback.test.R;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.FragmentTransaction;
 import android.util.Log;
 
 public class SingleSupportFragmentTestActivity extends FragmentActivity {
@@ -36,10 +33,26 @@
     public static final String EXTRA_ACTIVITY_LAYOUT = "activityLayout";
 
     public static final String EXTRA_UI_VISIBILITY = "uiVisibility";
+
+    public static final String EXTRA_OVERRIDDEN_SAVED_INSTANCE_STATE =
+            "overriddenSavedInstanceState";
+
     private static final String TAG = "TestActivity";
 
+    private Bundle overrideSavedInstance(Bundle savedInstance) {
+        Intent intent = getIntent();
+        if (intent != null) {
+            Bundle b = intent.getBundleExtra(EXTRA_OVERRIDDEN_SAVED_INSTANCE_STATE);
+            if (b != null) {
+                return b;
+            }
+        }
+        return savedInstance;
+    }
+
     @Override
     public void onCreate(Bundle savedInstanceState) {
+        savedInstanceState = overrideSavedInstance(savedInstanceState);
         super.onCreate(savedInstanceState);
         Log.d(TAG, "onCreate " + this);
         Intent intent = getIntent();
@@ -64,6 +77,17 @@
         }
     }
 
+    @Override
+    protected void onRestoreInstanceState(Bundle savedInstanceState) {
+        super.onRestoreInstanceState(overrideSavedInstance(savedInstanceState));
+    }
+
+    public Bundle performSaveInstanceState() {
+        Bundle state = new Bundle();
+        onSaveInstanceState(state);
+        return state;
+    }
+
     public Fragment getTestFragment() {
         return getSupportFragmentManager().findFragmentById(R.id.main_frame);
     }
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestBase.java b/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestBase.java
similarity index 91%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestBase.java
rename to leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestBase.java
index 6c00923..8cce627 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestBase.java
+++ b/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestBase.java
@@ -1,6 +1,3 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from SingleFrgamentTestBase.java.  DO NOT MODIFY. */
-
 /*
  * Copyright (C) 2016 The Android Open Source Project
  *
@@ -19,6 +16,7 @@
 package android.support.v17.leanback.app;
 
 import android.content.Intent;
+import android.os.Bundle;
 import android.os.SystemClock;
 import android.support.test.InstrumentationRegistry;
 import android.support.test.rule.ActivityTestRule;
@@ -50,6 +48,7 @@
     public static class Options {
         int mActivityLayoutId;
         int mUiVisibility;
+        Bundle mSavedInstance;
 
         public Options() {
         }
@@ -64,6 +63,11 @@
             return this;
         }
 
+        public Options savedInstance(Bundle savedInstance) {
+            mSavedInstance = savedInstance;
+            return this;
+        }
+
         public void collect(Intent intent) {
             if (mActivityLayoutId != 0) {
                 intent.putExtra(SingleSupportFragmentTestActivity.EXTRA_ACTIVITY_LAYOUT,
@@ -72,6 +76,10 @@
             if (mUiVisibility != 0) {
                 intent.putExtra(SingleSupportFragmentTestActivity.EXTRA_UI_VISIBILITY, mUiVisibility);
             }
+            if (mSavedInstance != null) {
+                intent.putExtra(SingleSupportFragmentTestActivity.EXTRA_OVERRIDDEN_SAVED_INSTANCE_STATE,
+                        mSavedInstance);
+            }
         }
     }
 
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/StringPresenter.java b/leanback/tests/java/android/support/v17/leanback/app/StringPresenter.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/StringPresenter.java
rename to leanback/tests/java/android/support/v17/leanback/app/StringPresenter.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/TestActivity.java b/leanback/tests/java/android/support/v17/leanback/app/TestActivity.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/TestActivity.java
rename to leanback/tests/java/android/support/v17/leanback/app/TestActivity.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/VerticalGridFragmentTest.java b/leanback/tests/java/android/support/v17/leanback/app/VerticalGridFragmentTest.java
similarity index 94%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/VerticalGridFragmentTest.java
rename to leanback/tests/java/android/support/v17/leanback/app/VerticalGridFragmentTest.java
index 2c36cda..649689c 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/VerticalGridFragmentTest.java
+++ b/leanback/tests/java/android/support/v17/leanback/app/VerticalGridFragmentTest.java
@@ -1,3 +1,6 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from VerticalGridSupportFragmentTest.java.  DO NOT MODIFY. */
+
 /*
  * Copyright (C) 2016 The Android Open Source Project
  *
@@ -16,13 +19,13 @@
 
 package android.support.v17.leanback.app;
 
-import android.app.Fragment;
 import android.os.Bundle;
 import android.support.test.InstrumentationRegistry;
 import android.support.test.filters.MediumTest;
 import android.support.test.runner.AndroidJUnit4;
 import android.support.v17.leanback.widget.ArrayObjectAdapter;
 import android.support.v17.leanback.widget.VerticalGridPresenter;
+import android.app.Fragment;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/VerticalGridSupportFragmentTest.java b/leanback/tests/java/android/support/v17/leanback/app/VerticalGridSupportFragmentTest.java
similarity index 95%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/VerticalGridSupportFragmentTest.java
rename to leanback/tests/java/android/support/v17/leanback/app/VerticalGridSupportFragmentTest.java
index 9ca930a..ccbfa04 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/VerticalGridSupportFragmentTest.java
+++ b/leanback/tests/java/android/support/v17/leanback/app/VerticalGridSupportFragmentTest.java
@@ -1,6 +1,3 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from VerticalGridFragmentTest.java.  DO NOT MODIFY. */
-
 /*
  * Copyright (C) 2016 The Android Open Source Project
  *
@@ -19,13 +16,13 @@
 
 package android.support.v17.leanback.app;
 
-import android.support.v4.app.Fragment;
 import android.os.Bundle;
 import android.support.test.InstrumentationRegistry;
 import android.support.test.filters.MediumTest;
 import android.support.test.runner.AndroidJUnit4;
 import android.support.v17.leanback.widget.ArrayObjectAdapter;
 import android.support.v17.leanback.widget.VerticalGridPresenter;
+import android.support.v4.app.Fragment;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/VideoFragmentTest.java b/leanback/tests/java/android/support/v17/leanback/app/VideoFragmentTest.java
similarity index 98%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/VideoFragmentTest.java
rename to leanback/tests/java/android/support/v17/leanback/app/VideoFragmentTest.java
index 7fe3902..a8b65d8 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/VideoFragmentTest.java
+++ b/leanback/tests/java/android/support/v17/leanback/app/VideoFragmentTest.java
@@ -1,3 +1,6 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from VideoSupportFragmentTest.java.  DO NOT MODIFY. */
+
 /*
  * Copyright (C) 2016 The Android Open Source Project
  *
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/VideoSupportFragmentTest.java b/leanback/tests/java/android/support/v17/leanback/app/VideoSupportFragmentTest.java
similarity index 98%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/VideoSupportFragmentTest.java
rename to leanback/tests/java/android/support/v17/leanback/app/VideoSupportFragmentTest.java
index d96dc4d..4d66285 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/VideoSupportFragmentTest.java
+++ b/leanback/tests/java/android/support/v17/leanback/app/VideoSupportFragmentTest.java
@@ -1,6 +1,3 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from VideoFragmentTest.java.  DO NOT MODIFY. */
-
 /*
  * Copyright (C) 2016 The Android Open Source Project
  *
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/wizard/GuidedDatePickerTest.java b/leanback/tests/java/android/support/v17/leanback/app/wizard/GuidedDatePickerTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/wizard/GuidedDatePickerTest.java
rename to leanback/tests/java/android/support/v17/leanback/app/wizard/GuidedDatePickerTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/wizard/GuidedStepAttributesTest.java b/leanback/tests/java/android/support/v17/leanback/app/wizard/GuidedStepAttributesTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/wizard/GuidedStepAttributesTest.java
rename to leanback/tests/java/android/support/v17/leanback/app/wizard/GuidedStepAttributesTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/wizard/GuidedStepAttributesTestActivity.java b/leanback/tests/java/android/support/v17/leanback/app/wizard/GuidedStepAttributesTestActivity.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/wizard/GuidedStepAttributesTestActivity.java
rename to leanback/tests/java/android/support/v17/leanback/app/wizard/GuidedStepAttributesTestActivity.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/wizard/GuidedStepAttributesTestFragment.java b/leanback/tests/java/android/support/v17/leanback/app/wizard/GuidedStepAttributesTestFragment.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/wizard/GuidedStepAttributesTestFragment.java
rename to leanback/tests/java/android/support/v17/leanback/app/wizard/GuidedStepAttributesTestFragment.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/graphics/CompositeDrawableTest.java b/leanback/tests/java/android/support/v17/leanback/graphics/CompositeDrawableTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/graphics/CompositeDrawableTest.java
rename to leanback/tests/java/android/support/v17/leanback/graphics/CompositeDrawableTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/graphics/FitWidthBitmapDrawableTest.java b/leanback/tests/java/android/support/v17/leanback/graphics/FitWidthBitmapDrawableTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/graphics/FitWidthBitmapDrawableTest.java
rename to leanback/tests/java/android/support/v17/leanback/graphics/FitWidthBitmapDrawableTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/media/MediaControllerAdapterTest.java b/leanback/tests/java/android/support/v17/leanback/media/MediaControllerAdapterTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/media/MediaControllerAdapterTest.java
rename to leanback/tests/java/android/support/v17/leanback/media/MediaControllerAdapterTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/media/MediaPlayerGlueTest.java b/leanback/tests/java/android/support/v17/leanback/media/MediaPlayerGlueTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/media/MediaPlayerGlueTest.java
rename to leanback/tests/java/android/support/v17/leanback/media/MediaPlayerGlueTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackBannerControlGlueTest.java b/leanback/tests/java/android/support/v17/leanback/media/PlaybackBannerControlGlueTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackBannerControlGlueTest.java
rename to leanback/tests/java/android/support/v17/leanback/media/PlaybackBannerControlGlueTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackControlGlueTest.java b/leanback/tests/java/android/support/v17/leanback/media/PlaybackControlGlueTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackControlGlueTest.java
rename to leanback/tests/java/android/support/v17/leanback/media/PlaybackControlGlueTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackGlueHostImpl.java b/leanback/tests/java/android/support/v17/leanback/media/PlaybackGlueHostImpl.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackGlueHostImpl.java
rename to leanback/tests/java/android/support/v17/leanback/media/PlaybackGlueHostImpl.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackGlueTest.java b/leanback/tests/java/android/support/v17/leanback/media/PlaybackGlueTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackGlueTest.java
rename to leanback/tests/java/android/support/v17/leanback/media/PlaybackGlueTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackTransportControlGlueTest.java b/leanback/tests/java/android/support/v17/leanback/media/PlaybackTransportControlGlueTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackTransportControlGlueTest.java
rename to leanback/tests/java/android/support/v17/leanback/media/PlaybackTransportControlGlueTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/testutils/PollingCheck.java b/leanback/tests/java/android/support/v17/leanback/testutils/PollingCheck.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/testutils/PollingCheck.java
rename to leanback/tests/java/android/support/v17/leanback/testutils/PollingCheck.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/AssertHelper.java b/leanback/tests/java/android/support/v17/leanback/widget/AssertHelper.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/AssertHelper.java
rename to leanback/tests/java/android/support/v17/leanback/widget/AssertHelper.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/BaseCardViewTest.java b/leanback/tests/java/android/support/v17/leanback/widget/BaseCardViewTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/BaseCardViewTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/BaseCardViewTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/ControlBarTest.java b/leanback/tests/java/android/support/v17/leanback/widget/ControlBarTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/ControlBarTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/ControlBarTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/GridActivity.java b/leanback/tests/java/android/support/v17/leanback/widget/GridActivity.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/GridActivity.java
rename to leanback/tests/java/android/support/v17/leanback/widget/GridActivity.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/GridTest.java b/leanback/tests/java/android/support/v17/leanback/widget/GridTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/GridTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/GridTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetPrefetchTest.java b/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetPrefetchTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetPrefetchTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/GridWidgetPrefetchTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java b/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java
similarity index 97%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java
index 5de0aa7..7bf96a5 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java
+++ b/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java
@@ -23,8 +23,10 @@
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
 import android.content.Intent;
@@ -61,6 +63,7 @@
 import org.junit.Test;
 import org.junit.rules.TestName;
 import org.junit.runner.RunWith;
+import org.mockito.Mockito;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -3760,6 +3763,31 @@
     }
 
     @Test
+    public void testNotifyItemChangedSelectionEvent() throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.vertical_linear);
+        intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 10);
+        initActivity(intent);
+        mOrientation = BaseGridView.HORIZONTAL;
+        mNumRows = 1;
+
+        OnChildViewHolderSelectedListener listener =
+                Mockito.mock(OnChildViewHolderSelectedListener.class);
+        mGridView.setOnChildViewHolderSelectedListener(listener);
+
+        performAndWaitForAnimation(new Runnable() {
+            @Override
+            public void run() {
+                mGridView.getAdapter().notifyItemChanged(0, 1);
+            }
+        });
+        Mockito.verify(listener, times(1)).onChildViewHolderSelected(any(RecyclerView.class),
+                any(RecyclerView.ViewHolder.class), anyInt(), anyInt());
+        assertEquals(0, mGridView.getSelectedPosition());
+    }
+
+    @Test
     public void testSelectionSmoothAndAddItemInOneCycle() throws Throwable {
         Intent intent = new Intent();
         intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
@@ -3907,6 +3935,94 @@
     }
 
     @Test
+    public void testRestoreIndexAndAddItemsSelect1() throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.vertical_linear);
+        intent.putExtra(GridActivity.EXTRA_CHILD_LAYOUT_ID, R.layout.horizontal_item);
+        intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 4);
+        initActivity(intent);
+        mOrientation = BaseGridView.VERTICAL;
+        mNumRows = 1;
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mGridView.setSelectedPosition(1);
+            }
+
+        });
+        assertEquals(mGridView.getSelectedPosition(), 1);
+        final SparseArray<Parcelable> states = new SparseArray<>();
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mGridView.saveHierarchyState(states);
+                mGridView.setAdapter(null);
+            }
+
+        });
+        performAndWaitForAnimation(new Runnable() {
+            @Override
+            public void run() {
+                mGridView.restoreHierarchyState(states);
+                mActivity.attachToNewAdapter(new int[0]);
+                mActivity.addItems(0, new int[]{100, 100, 100, 100});
+            }
+
+        });
+        assertEquals(mGridView.getSelectedPosition(), 1);
+    }
+
+    @Test
+    public void testRestoreStateAfterAdapterChange() throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.vertical_linear);
+        intent.putExtra(GridActivity.EXTRA_CHILD_LAYOUT_ID, R.layout.selectable_text_view);
+        intent.putExtra(GridActivity.EXTRA_ITEMS, new int[]{50, 50, 50, 50});
+        initActivity(intent);
+        mOrientation = BaseGridView.VERTICAL;
+        mNumRows = 1;
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mGridView.setSelectedPosition(1);
+                mGridView.setSaveChildrenPolicy(VerticalGridView.SAVE_ALL_CHILD);
+            }
+
+        });
+        assertEquals(mGridView.getSelectedPosition(), 1);
+        final SparseArray<Parcelable> states = new SparseArray<>();
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                Selection.setSelection((Spannable) (((TextView) mGridView.getChildAt(0))
+                        .getText()), 1, 2);
+                Selection.setSelection((Spannable) (((TextView) mGridView.getChildAt(1))
+                        .getText()), 0, 1);
+                mGridView.saveHierarchyState(states);
+                mGridView.setAdapter(null);
+            }
+
+        });
+        performAndWaitForAnimation(new Runnable() {
+            @Override
+            public void run() {
+                mGridView.restoreHierarchyState(states);
+                mActivity.attachToNewAdapter(new int[]{50, 50, 50, 50});
+            }
+
+        });
+        assertEquals(mGridView.getSelectedPosition(), 1);
+        assertEquals(1, ((TextView) mGridView.getChildAt(0)).getSelectionStart());
+        assertEquals(2, ((TextView) mGridView.getChildAt(0)).getSelectionEnd());
+        assertEquals(0, ((TextView) mGridView.getChildAt(1)).getSelectionStart());
+        assertEquals(1, ((TextView) mGridView.getChildAt(1)).getSelectionEnd());
+    }
+
+    @Test
     public void test27766012() throws Throwable {
         Intent intent = new Intent();
         intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/GuidedActionStylistTest.java b/leanback/tests/java/android/support/v17/leanback/widget/GuidedActionStylistTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/GuidedActionStylistTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/GuidedActionStylistTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/HorizontalGridViewEx.java b/leanback/tests/java/android/support/v17/leanback/widget/HorizontalGridViewEx.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/HorizontalGridViewEx.java
rename to leanback/tests/java/android/support/v17/leanback/widget/HorizontalGridViewEx.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/ImageCardViewTest.java b/leanback/tests/java/android/support/v17/leanback/widget/ImageCardViewTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/ImageCardViewTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/ImageCardViewTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/ListRowPresenterTest.java b/leanback/tests/java/android/support/v17/leanback/widget/ListRowPresenterTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/ListRowPresenterTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/ListRowPresenterTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/MediaNowPlayingViewTest.java b/leanback/tests/java/android/support/v17/leanback/widget/MediaNowPlayingViewTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/MediaNowPlayingViewTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/MediaNowPlayingViewTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/ObjectAdapterTest.java b/leanback/tests/java/android/support/v17/leanback/widget/ObjectAdapterTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/ObjectAdapterTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/ObjectAdapterTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/PagingIndicatorTest.java b/leanback/tests/java/android/support/v17/leanback/widget/PagingIndicatorTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/PagingIndicatorTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/PagingIndicatorTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/ParallaxFloatEffectTest.java b/leanback/tests/java/android/support/v17/leanback/widget/ParallaxFloatEffectTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/ParallaxFloatEffectTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/ParallaxFloatEffectTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/ParallaxFloatTest.java b/leanback/tests/java/android/support/v17/leanback/widget/ParallaxFloatTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/ParallaxFloatTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/ParallaxFloatTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/ParallaxIntEffectTest.java b/leanback/tests/java/android/support/v17/leanback/widget/ParallaxIntEffectTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/ParallaxIntEffectTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/ParallaxIntEffectTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/ParallaxIntTest.java b/leanback/tests/java/android/support/v17/leanback/widget/ParallaxIntTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/ParallaxIntTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/ParallaxIntTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/PlaybackGlueHostImplWithViewHolder.java b/leanback/tests/java/android/support/v17/leanback/widget/PlaybackGlueHostImplWithViewHolder.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/PlaybackGlueHostImplWithViewHolder.java
rename to leanback/tests/java/android/support/v17/leanback/widget/PlaybackGlueHostImplWithViewHolder.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/PlaybackSeekProviderSample.java b/leanback/tests/java/android/support/v17/leanback/widget/PlaybackSeekProviderSample.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/PlaybackSeekProviderSample.java
rename to leanback/tests/java/android/support/v17/leanback/widget/PlaybackSeekProviderSample.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/PlaybackTransportRowPresenterTest.java b/leanback/tests/java/android/support/v17/leanback/widget/PlaybackTransportRowPresenterTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/PlaybackTransportRowPresenterTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/PlaybackTransportRowPresenterTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/PresenterTest.java b/leanback/tests/java/android/support/v17/leanback/widget/PresenterTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/PresenterTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/PresenterTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/ShadowOverlayContainerTest.java b/leanback/tests/java/android/support/v17/leanback/widget/ShadowOverlayContainerTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/ShadowOverlayContainerTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/ShadowOverlayContainerTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/SingleRowTest.java b/leanback/tests/java/android/support/v17/leanback/widget/SingleRowTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/SingleRowTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/SingleRowTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/StaggeredGridDefaultTest.java b/leanback/tests/java/android/support/v17/leanback/widget/StaggeredGridDefaultTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/StaggeredGridDefaultTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/StaggeredGridDefaultTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/ThumbsBarTest.java b/leanback/tests/java/android/support/v17/leanback/widget/ThumbsBarTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/ThumbsBarTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/ThumbsBarTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/TitleViewAdapterTest.java b/leanback/tests/java/android/support/v17/leanback/widget/TitleViewAdapterTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/TitleViewAdapterTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/TitleViewAdapterTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/VerticalGridViewEx.java b/leanback/tests/java/android/support/v17/leanback/widget/VerticalGridViewEx.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/VerticalGridViewEx.java
rename to leanback/tests/java/android/support/v17/leanback/widget/VerticalGridViewEx.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/picker/DatePickerActivity.java b/leanback/tests/java/android/support/v17/leanback/widget/picker/DatePickerActivity.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/picker/DatePickerActivity.java
rename to leanback/tests/java/android/support/v17/leanback/widget/picker/DatePickerActivity.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/picker/DatePickerTest.java b/leanback/tests/java/android/support/v17/leanback/widget/picker/DatePickerTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/picker/DatePickerTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/picker/DatePickerTest.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/picker/TimePickerActivity.java b/leanback/tests/java/android/support/v17/leanback/widget/picker/TimePickerActivity.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/picker/TimePickerActivity.java
rename to leanback/tests/java/android/support/v17/leanback/widget/picker/TimePickerActivity.java
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/picker/TimePickerTest.java b/leanback/tests/java/android/support/v17/leanback/widget/picker/TimePickerTest.java
similarity index 100%
rename from v17/leanback/tests/java/android/support/v17/leanback/widget/picker/TimePickerTest.java
rename to leanback/tests/java/android/support/v17/leanback/widget/picker/TimePickerTest.java
diff --git a/v17/leanback/tests/res/drawable/ic_action_a.png b/leanback/tests/res/drawable/ic_action_a.png
similarity index 100%
rename from v17/leanback/tests/res/drawable/ic_action_a.png
rename to leanback/tests/res/drawable/ic_action_a.png
Binary files differ
diff --git a/v17/leanback/tests/res/drawable/spiderman.jpg b/leanback/tests/res/drawable/spiderman.jpg
similarity index 100%
rename from v17/leanback/tests/res/drawable/spiderman.jpg
rename to leanback/tests/res/drawable/spiderman.jpg
Binary files differ
diff --git a/v17/leanback/tests/res/layout/browse.xml b/leanback/tests/res/layout/browse.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/browse.xml
rename to leanback/tests/res/layout/browse.xml
diff --git a/v17/leanback/tests/res/layout/datepicker_alone.xml b/leanback/tests/res/layout/datepicker_alone.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/datepicker_alone.xml
rename to leanback/tests/res/layout/datepicker_alone.xml
diff --git a/v17/leanback/tests/res/layout/datepicker_with_other_widgets.xml b/leanback/tests/res/layout/datepicker_with_other_widgets.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/datepicker_with_other_widgets.xml
rename to leanback/tests/res/layout/datepicker_with_other_widgets.xml
diff --git a/v17/leanback/tests/res/layout/details.xml b/leanback/tests/res/layout/details.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/details.xml
rename to leanback/tests/res/layout/details.xml
diff --git a/v17/leanback/tests/res/layout/horizontal_grid.xml b/leanback/tests/res/layout/horizontal_grid.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/horizontal_grid.xml
rename to leanback/tests/res/layout/horizontal_grid.xml
diff --git a/v17/leanback/tests/res/layout/horizontal_grid_testredundantappendremove2.xml b/leanback/tests/res/layout/horizontal_grid_testredundantappendremove2.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/horizontal_grid_testredundantappendremove2.xml
rename to leanback/tests/res/layout/horizontal_grid_testredundantappendremove2.xml
diff --git a/v17/leanback/tests/res/layout/horizontal_grid_wrap.xml b/leanback/tests/res/layout/horizontal_grid_wrap.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/horizontal_grid_wrap.xml
rename to leanback/tests/res/layout/horizontal_grid_wrap.xml
diff --git a/v17/leanback/tests/res/layout/horizontal_item.xml b/leanback/tests/res/layout/horizontal_item.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/horizontal_item.xml
rename to leanback/tests/res/layout/horizontal_item.xml
diff --git a/v17/leanback/tests/res/layout/horizontal_linear.xml b/leanback/tests/res/layout/horizontal_linear.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/horizontal_linear.xml
rename to leanback/tests/res/layout/horizontal_linear.xml
diff --git a/v17/leanback/tests/res/layout/horizontal_linear_rtl.xml b/leanback/tests/res/layout/horizontal_linear_rtl.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/horizontal_linear_rtl.xml
rename to leanback/tests/res/layout/horizontal_linear_rtl.xml
diff --git a/v17/leanback/tests/res/layout/horizontal_linear_wrap_content.xml b/leanback/tests/res/layout/horizontal_linear_wrap_content.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/horizontal_linear_wrap_content.xml
rename to leanback/tests/res/layout/horizontal_linear_wrap_content.xml
diff --git a/v17/leanback/tests/res/layout/item_button_at_bottom.xml b/leanback/tests/res/layout/item_button_at_bottom.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/item_button_at_bottom.xml
rename to leanback/tests/res/layout/item_button_at_bottom.xml
diff --git a/v17/leanback/tests/res/layout/item_button_at_top.xml b/leanback/tests/res/layout/item_button_at_top.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/item_button_at_top.xml
rename to leanback/tests/res/layout/item_button_at_top.xml
diff --git a/leanback/tests/res/layout/page_fragment.xml b/leanback/tests/res/layout/page_fragment.xml
new file mode 100644
index 0000000..9273f6f
--- /dev/null
+++ b/leanback/tests/res/layout/page_fragment.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        android:id="@+id/container_list"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        android:layout_alignParentRight="true"
+        android:layout_marginRight="128dp"
+        android:layout_centerVertical="true">
+
+        <EditText
+            android:id="@+id/tv1"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="Header 1"
+            android:layout_margin="16dp"
+            android:focusable="true"
+            android:textAppearance="@android:style/TextAppearance.DeviceDefault.Large" />
+
+        <EditText
+            android:id="@+id/tv2"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="Header 2"
+            android:layout_margin="16dp"
+            android:focusable="true"
+            android:textAppearance="@android:style/TextAppearance.DeviceDefault.Medium" />
+
+        <EditText
+            android:id="@+id/tv3"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="Header 3"
+            android:layout_margin="16dp"
+            android:focusable="true"
+            android:textAppearance="@android:style/TextAppearance.DeviceDefault.Small" />
+
+    </LinearLayout>
+</RelativeLayout>
\ No newline at end of file
diff --git a/v17/leanback/tests/res/layout/playback_controls_with_video.xml b/leanback/tests/res/layout/playback_controls_with_video.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/playback_controls_with_video.xml
rename to leanback/tests/res/layout/playback_controls_with_video.xml
diff --git a/v17/leanback/tests/res/layout/relative_layout.xml b/leanback/tests/res/layout/relative_layout.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/relative_layout.xml
rename to leanback/tests/res/layout/relative_layout.xml
diff --git a/v17/leanback/tests/res/layout/relative_layout2.xml b/leanback/tests/res/layout/relative_layout2.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/relative_layout2.xml
rename to leanback/tests/res/layout/relative_layout2.xml
diff --git a/v17/leanback/tests/res/layout/relative_layout3.xml b/leanback/tests/res/layout/relative_layout3.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/relative_layout3.xml
rename to leanback/tests/res/layout/relative_layout3.xml
diff --git a/v17/leanback/tests/res/layout/selectable_text_view.xml b/leanback/tests/res/layout/selectable_text_view.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/selectable_text_view.xml
rename to leanback/tests/res/layout/selectable_text_view.xml
diff --git a/v17/leanback/tests/res/layout/single_fragment.xml b/leanback/tests/res/layout/single_fragment.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/single_fragment.xml
rename to leanback/tests/res/layout/single_fragment.xml
diff --git a/v17/leanback/tests/res/layout/timepicker_alone.xml b/leanback/tests/res/layout/timepicker_alone.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/timepicker_alone.xml
rename to leanback/tests/res/layout/timepicker_alone.xml
diff --git a/v17/leanback/tests/res/layout/timepicker_with_other_widgets.xml b/leanback/tests/res/layout/timepicker_with_other_widgets.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/timepicker_with_other_widgets.xml
rename to leanback/tests/res/layout/timepicker_with_other_widgets.xml
diff --git a/v17/leanback/tests/res/layout/vertical_grid.xml b/leanback/tests/res/layout/vertical_grid.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/vertical_grid.xml
rename to leanback/tests/res/layout/vertical_grid.xml
diff --git a/v17/leanback/tests/res/layout/vertical_grid_ltr.xml b/leanback/tests/res/layout/vertical_grid_ltr.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/vertical_grid_ltr.xml
rename to leanback/tests/res/layout/vertical_grid_ltr.xml
diff --git a/v17/leanback/tests/res/layout/vertical_grid_rtl.xml b/leanback/tests/res/layout/vertical_grid_rtl.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/vertical_grid_rtl.xml
rename to leanback/tests/res/layout/vertical_grid_rtl.xml
diff --git a/v17/leanback/tests/res/layout/vertical_grid_testredundantappendremove.xml b/leanback/tests/res/layout/vertical_grid_testredundantappendremove.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/vertical_grid_testredundantappendremove.xml
rename to leanback/tests/res/layout/vertical_grid_testredundantappendremove.xml
diff --git a/v17/leanback/tests/res/layout/vertical_linear.xml b/leanback/tests/res/layout/vertical_linear.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/vertical_linear.xml
rename to leanback/tests/res/layout/vertical_linear.xml
diff --git a/v17/leanback/tests/res/layout/vertical_linear_measured_with_zero.xml b/leanback/tests/res/layout/vertical_linear_measured_with_zero.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/vertical_linear_measured_with_zero.xml
rename to leanback/tests/res/layout/vertical_linear_measured_with_zero.xml
diff --git a/v17/leanback/tests/res/layout/vertical_linear_with_button.xml b/leanback/tests/res/layout/vertical_linear_with_button.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/vertical_linear_with_button.xml
rename to leanback/tests/res/layout/vertical_linear_with_button.xml
diff --git a/v17/leanback/tests/res/layout/vertical_linear_with_button_onleft.xml b/leanback/tests/res/layout/vertical_linear_with_button_onleft.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/vertical_linear_with_button_onleft.xml
rename to leanback/tests/res/layout/vertical_linear_with_button_onleft.xml
diff --git a/v17/leanback/tests/res/layout/vertical_linear_wrap_content.xml b/leanback/tests/res/layout/vertical_linear_wrap_content.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/vertical_linear_wrap_content.xml
rename to leanback/tests/res/layout/vertical_linear_wrap_content.xml
diff --git a/v17/leanback/tests/res/layout/video_fragment_with_controls.xml b/leanback/tests/res/layout/video_fragment_with_controls.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/video_fragment_with_controls.xml
rename to leanback/tests/res/layout/video_fragment_with_controls.xml
diff --git a/v17/leanback/tests/res/raw/track_01.mp3 b/leanback/tests/res/raw/track_01.mp3
similarity index 100%
rename from v17/leanback/tests/res/raw/track_01.mp3
rename to leanback/tests/res/raw/track_01.mp3
Binary files differ
diff --git a/v17/leanback/tests/res/raw/video.mp4 b/leanback/tests/res/raw/video.mp4
similarity index 100%
rename from v17/leanback/tests/res/raw/video.mp4
rename to leanback/tests/res/raw/video.mp4
Binary files differ
diff --git a/v17/leanback/tests/res/values/strings.xml b/leanback/tests/res/values/strings.xml
similarity index 100%
rename from v17/leanback/tests/res/values/strings.xml
rename to leanback/tests/res/values/strings.xml
diff --git a/lifecycle/gradle/wrapper/gradle-wrapper.properties b/lifecycle/gradle/wrapper/gradle-wrapper.properties
index b519e0a..6051ae0 100644
--- a/lifecycle/gradle/wrapper/gradle-wrapper.properties
+++ b/lifecycle/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
 distributionPath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-3.4-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.3-bin.zip
diff --git a/media-compat-test-client/build.gradle b/media-compat-test-client/build.gradle
deleted file mode 100644
index 47141db..0000000
--- a/media-compat-test-client/build.gradle
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * 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.
- */
-
-plugins {
-    id("SupportAndroidLibraryPlugin")
-}
-
-dependencies {
-    androidTestImplementation project(':support-annotations')
-    androidTestImplementation project(':support-media-compat')
-    androidTestImplementation project(':support-media-compat-test-lib')
-    androidTestImplementation project(':support-testutils')
-
-    androidTestImplementation(libs.test_runner) {
-        exclude module: 'support-annotations'
-    }
-}
-
-android {
-    defaultConfig {
-        minSdkVersion 14
-    }
-}
-
-supportLibrary {
-    legacySourceLocation = true
-}
\ No newline at end of file
diff --git a/media-compat-test-client/tests/AndroidManifest.xml b/media-compat-test-client/tests/AndroidManifest.xml
deleted file mode 100644
index 8938399..0000000
--- a/media-compat-test-client/tests/AndroidManifest.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?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.mediacompat.client.test">
-    <uses-sdk android:targetSdkVersion="${target-sdk-version}"/>
-</manifest>
diff --git a/media-compat-test-client/tests/src/android/support/mediacompat/client/MediaBrowserCompatTest.java b/media-compat-test-client/tests/src/android/support/mediacompat/client/MediaBrowserCompatTest.java
deleted file mode 100644
index f9f24a0..0000000
--- a/media-compat-test-client/tests/src/android/support/mediacompat/client/MediaBrowserCompatTest.java
+++ /dev/null
@@ -1,672 +0,0 @@
-/*
- * 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.mediacompat.client;
-
-import static android.support.mediacompat.testlib.MediaBrowserConstants.EXTRAS_KEY;
-import static android.support.mediacompat.testlib.MediaBrowserConstants.EXTRAS_VALUE;
-import static android.support.mediacompat.testlib.MediaBrowserConstants.MEDIA_ID_CHILDREN;
-import static android.support.mediacompat.testlib.MediaBrowserConstants.MEDIA_ID_INVALID;
-import static android.support.mediacompat.testlib.MediaBrowserConstants
-        .MEDIA_ID_ON_LOAD_ITEM_NOT_IMPLEMENTED;
-import static android.support.mediacompat.testlib.MediaBrowserConstants.MEDIA_ID_ROOT;
-import static android.support.mediacompat.testlib.MediaBrowserConstants.NOTIFY_CHILDREN_CHANGED;
-import static android.support.test.InstrumentationRegistry.getContext;
-import static android.support.test.InstrumentationRegistry.getInstrumentation;
-
-import static junit.framework.Assert.assertEquals;
-import static junit.framework.Assert.assertFalse;
-import static junit.framework.Assert.assertNotNull;
-import static junit.framework.Assert.assertNull;
-import static junit.framework.Assert.assertTrue;
-import static junit.framework.Assert.fail;
-
-import android.content.ComponentName;
-import android.os.Bundle;
-import android.support.mediacompat.client.util.IntentUtil;
-import android.support.test.filters.LargeTest;
-import android.support.test.filters.MediumTest;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-import android.support.testutils.PollingCheck;
-import android.support.v4.media.MediaBrowserCompat;
-import android.support.v4.media.MediaBrowserCompat.MediaItem;
-import android.support.v4.media.MediaBrowserServiceCompat;
-import android.support.v4.media.MediaDescriptionCompat;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Test {@link android.support.v4.media.MediaBrowserCompat}.
- */
-@RunWith(AndroidJUnit4.class)
-public class MediaBrowserCompatTest {
-
-    // The maximum time to wait for an operation.
-    private static final long TIME_OUT_MS = 3000L;
-
-    /**
-     * To check {@link MediaBrowserCompat#unsubscribe} works properly,
-     * we notify to the browser after the unsubscription that the media items have changed.
-     * Then {@link MediaBrowserCompat.SubscriptionCallback#onChildrenLoaded} should not be called.
-     *
-     * The measured time from calling {@link MediaBrowserServiceCompat#notifyChildrenChanged}
-     * to {@link MediaBrowserCompat.SubscriptionCallback#onChildrenLoaded} being called is about
-     * 50ms.
-     * So we make the thread sleep for 100ms to properly check that the callback is not called.
-     */
-    private static final long SLEEP_MS = 100L;
-    private static final ComponentName TEST_BROWSER_SERVICE = new ComponentName(
-            "android.support.mediacompat.service.test",
-            "android.support.mediacompat.service.StubMediaBrowserServiceCompat");
-    private static final ComponentName TEST_INVALID_BROWSER_SERVICE = new ComponentName(
-            "invalid.package", "invalid.ServiceClassName");
-
-    private MediaBrowserCompat mMediaBrowser;
-    private StubConnectionCallback mConnectionCallback;
-    private StubSubscriptionCallback mSubscriptionCallback;
-    private StubItemCallback mItemCallback;
-    private Bundle mRootHints;
-
-    @Before
-    public void setUp() {
-        mConnectionCallback = new StubConnectionCallback();
-        mSubscriptionCallback = new StubSubscriptionCallback();
-        mItemCallback = new StubItemCallback();
-
-        mRootHints = new Bundle();
-        mRootHints.putBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_RECENT, true);
-        mRootHints.putBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_OFFLINE, true);
-        mRootHints.putBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_SUGGESTED, true);
-    }
-
-    @After
-    public void tearDown() {
-        if (mMediaBrowser != null && mMediaBrowser.isConnected()) {
-            mMediaBrowser.disconnect();
-        }
-    }
-
-    @Test
-    @SmallTest
-    public void testMediaBrowser() throws Exception {
-        createMediaBrowser(TEST_BROWSER_SERVICE);
-        assertFalse(mMediaBrowser.isConnected());
-
-        connectMediaBrowserService();
-        assertTrue(mMediaBrowser.isConnected());
-
-        assertEquals(TEST_BROWSER_SERVICE, mMediaBrowser.getServiceComponent());
-        assertEquals(MEDIA_ID_ROOT, mMediaBrowser.getRoot());
-        assertEquals(EXTRAS_VALUE, mMediaBrowser.getExtras().getString(EXTRAS_KEY));
-
-        mMediaBrowser.disconnect();
-        new PollingCheck(TIME_OUT_MS) {
-            @Override
-            protected boolean check() {
-                return !mMediaBrowser.isConnected();
-            }
-        }.run();
-    }
-
-    @Test
-    @SmallTest
-    public void testConnectTwice() throws Exception {
-        createMediaBrowser(TEST_BROWSER_SERVICE);
-        connectMediaBrowserService();
-        try {
-            mMediaBrowser.connect();
-            fail();
-        } catch (IllegalStateException e) {
-            // expected
-        }
-    }
-
-    @Test
-    @SmallTest
-    public void testConnectionFailed() throws Exception {
-        createMediaBrowser(TEST_INVALID_BROWSER_SERVICE);
-
-        synchronized (mConnectionCallback.mWaitLock) {
-            mMediaBrowser.connect();
-            mConnectionCallback.mWaitLock.wait(TIME_OUT_MS);
-        }
-        assertTrue(mConnectionCallback.mConnectionFailedCount > 0);
-        assertEquals(0, mConnectionCallback.mConnectedCount);
-        assertEquals(0, mConnectionCallback.mConnectionSuspendedCount);
-    }
-
-    @Test
-    @SmallTest
-    public void testReconnection() throws Exception {
-        createMediaBrowser(TEST_BROWSER_SERVICE);
-
-        getInstrumentation().runOnMainSync(new Runnable() {
-            @Override
-            public void run() {
-                mMediaBrowser.connect();
-                // Reconnect before the first connection was established.
-                mMediaBrowser.disconnect();
-                mMediaBrowser.connect();
-            }
-        });
-
-        synchronized (mConnectionCallback.mWaitLock) {
-            mConnectionCallback.mWaitLock.wait(TIME_OUT_MS);
-            assertEquals(1, mConnectionCallback.mConnectedCount);
-        }
-
-        synchronized (mSubscriptionCallback.mWaitLock) {
-            // Test subscribe.
-            resetCallbacks();
-            mMediaBrowser.subscribe(MEDIA_ID_ROOT, mSubscriptionCallback);
-            mSubscriptionCallback.mWaitLock.wait(TIME_OUT_MS);
-            assertTrue(mSubscriptionCallback.mChildrenLoadedCount > 0);
-            assertEquals(MEDIA_ID_ROOT, mSubscriptionCallback.mLastParentId);
-        }
-
-        synchronized (mItemCallback.mWaitLock) {
-            // Test getItem.
-            resetCallbacks();
-            mMediaBrowser.getItem(MEDIA_ID_CHILDREN[0], mItemCallback);
-            mItemCallback.mWaitLock.wait(TIME_OUT_MS);
-            assertEquals(MEDIA_ID_CHILDREN[0], mItemCallback.mLastMediaItem.getMediaId());
-        }
-
-        // Reconnect after connection was established.
-        mMediaBrowser.disconnect();
-        resetCallbacks();
-        connectMediaBrowserService();
-
-        synchronized (mItemCallback.mWaitLock) {
-            // Test getItem.
-            resetCallbacks();
-            mMediaBrowser.getItem(MEDIA_ID_CHILDREN[0], mItemCallback);
-            mItemCallback.mWaitLock.wait(TIME_OUT_MS);
-            assertEquals(MEDIA_ID_CHILDREN[0], mItemCallback.mLastMediaItem.getMediaId());
-        }
-    }
-
-    @Test
-    @SmallTest
-    public void testConnectionCallbackNotCalledAfterDisconnect() {
-        createMediaBrowser(TEST_BROWSER_SERVICE);
-
-        getInstrumentation().runOnMainSync(new Runnable() {
-            @Override
-            public void run() {
-                mMediaBrowser.connect();
-                mMediaBrowser.disconnect();
-                resetCallbacks();
-            }
-        });
-
-        try {
-            Thread.sleep(SLEEP_MS);
-        } catch (InterruptedException e) {
-            fail("Unexpected InterruptedException occurred.");
-        }
-        assertEquals(0, mConnectionCallback.mConnectedCount);
-        assertEquals(0, mConnectionCallback.mConnectionFailedCount);
-        assertEquals(0, mConnectionCallback.mConnectionSuspendedCount);
-    }
-
-    @Test
-    @SmallTest
-    public void testGetServiceComponentBeforeConnection() {
-        createMediaBrowser(TEST_BROWSER_SERVICE);
-        try {
-            ComponentName serviceComponent = mMediaBrowser.getServiceComponent();
-            fail();
-        } catch (IllegalStateException e) {
-            // expected
-        }
-    }
-
-    @Test
-    @SmallTest
-    public void testSubscribe() throws Exception {
-        createMediaBrowser(TEST_BROWSER_SERVICE);
-        connectMediaBrowserService();
-
-        synchronized (mSubscriptionCallback.mWaitLock) {
-            mMediaBrowser.subscribe(MEDIA_ID_ROOT, mSubscriptionCallback);
-            mSubscriptionCallback.mWaitLock.wait(TIME_OUT_MS);
-            assertTrue(mSubscriptionCallback.mChildrenLoadedCount > 0);
-            assertEquals(MEDIA_ID_ROOT, mSubscriptionCallback.mLastParentId);
-            assertEquals(MEDIA_ID_CHILDREN.length,
-                    mSubscriptionCallback.mLastChildMediaItems.size());
-            for (int i = 0; i < MEDIA_ID_CHILDREN.length; ++i) {
-                assertEquals(MEDIA_ID_CHILDREN[i],
-                        mSubscriptionCallback.mLastChildMediaItems.get(i).getMediaId());
-            }
-
-            // Test MediaBrowserServiceCompat.notifyChildrenChanged()
-            mSubscriptionCallback.reset();
-            callMediaBrowserServiceMethod(NOTIFY_CHILDREN_CHANGED, MEDIA_ID_ROOT);
-            mSubscriptionCallback.mWaitLock.wait(TIME_OUT_MS);
-            assertTrue(mSubscriptionCallback.mChildrenLoadedCount > 0);
-        }
-
-        // Test unsubscribe.
-        resetCallbacks();
-        mMediaBrowser.unsubscribe(MEDIA_ID_ROOT);
-
-        // After unsubscribing, make StubMediaBrowserServiceCompat notify that the children are
-        // changed.
-        callMediaBrowserServiceMethod(NOTIFY_CHILDREN_CHANGED, MEDIA_ID_ROOT);
-        try {
-            Thread.sleep(SLEEP_MS);
-        } catch (InterruptedException e) {
-            fail("Unexpected InterruptedException occurred.");
-        }
-        // onChildrenLoaded should not be called.
-        assertEquals(0, mSubscriptionCallback.mChildrenLoadedCount);
-    }
-
-    @Test
-    @SmallTest
-    public void testSubscribeWithOptions() throws Exception {
-        createMediaBrowser(TEST_BROWSER_SERVICE);
-        connectMediaBrowserService();
-        final int pageSize = 3;
-        final int lastPage = (MEDIA_ID_CHILDREN.length - 1) / pageSize;
-        Bundle options = new Bundle();
-        options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
-
-        synchronized (mSubscriptionCallback.mWaitLock) {
-            for (int page = 0; page <= lastPage; ++page) {
-                resetCallbacks();
-                options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
-                mMediaBrowser.subscribe(MEDIA_ID_ROOT, options, mSubscriptionCallback);
-                mSubscriptionCallback.mWaitLock.wait(TIME_OUT_MS);
-                assertTrue(mSubscriptionCallback.mChildrenLoadedWithOptionCount > 0);
-                assertEquals(MEDIA_ID_ROOT, mSubscriptionCallback.mLastParentId);
-                if (page != lastPage) {
-                    assertEquals(pageSize, mSubscriptionCallback.mLastChildMediaItems.size());
-                } else {
-                    assertEquals((MEDIA_ID_CHILDREN.length - 1) % pageSize + 1,
-                            mSubscriptionCallback.mLastChildMediaItems.size());
-                }
-                // Check whether all the items in the current page are loaded.
-                for (int i = 0; i < mSubscriptionCallback.mLastChildMediaItems.size(); ++i) {
-                    assertEquals(MEDIA_ID_CHILDREN[page * pageSize + i],
-                            mSubscriptionCallback.mLastChildMediaItems.get(i).getMediaId());
-                }
-            }
-
-            // Test MediaBrowserServiceCompat.notifyChildrenChanged()
-            mSubscriptionCallback.reset();
-            callMediaBrowserServiceMethod(NOTIFY_CHILDREN_CHANGED, MEDIA_ID_ROOT);
-            mSubscriptionCallback.mWaitLock.wait(TIME_OUT_MS);
-            assertTrue(mSubscriptionCallback.mChildrenLoadedWithOptionCount > 0);
-        }
-
-        // Test unsubscribe with callback argument.
-        resetCallbacks();
-        mMediaBrowser.unsubscribe(MEDIA_ID_ROOT, mSubscriptionCallback);
-
-        // After unsubscribing, make StubMediaBrowserServiceCompat notify that the children are
-        // changed.
-        callMediaBrowserServiceMethod(NOTIFY_CHILDREN_CHANGED, MEDIA_ID_ROOT);
-        try {
-            Thread.sleep(SLEEP_MS);
-        } catch (InterruptedException e) {
-            fail("Unexpected InterruptedException occurred.");
-        }
-        // onChildrenLoaded should not be called.
-        assertEquals(0, mSubscriptionCallback.mChildrenLoadedCount);
-    }
-
-    @Test
-    @SmallTest
-    public void testSubscribeInvalidItem() throws Exception {
-        createMediaBrowser(TEST_BROWSER_SERVICE);
-        connectMediaBrowserService();
-
-        synchronized (mSubscriptionCallback.mWaitLock) {
-            mMediaBrowser.subscribe(MEDIA_ID_INVALID, mSubscriptionCallback);
-            mSubscriptionCallback.mWaitLock.wait(TIME_OUT_MS);
-            assertEquals(MEDIA_ID_INVALID, mSubscriptionCallback.mLastErrorId);
-        }
-    }
-
-    @Test
-    @SmallTest
-    public void testSubscribeInvalidItemWithOptions() throws Exception {
-        createMediaBrowser(TEST_BROWSER_SERVICE);
-        connectMediaBrowserService();
-
-        final int pageSize = 5;
-        final int page = 2;
-        Bundle options = new Bundle();
-        options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
-        options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
-
-        synchronized (mSubscriptionCallback.mWaitLock) {
-            mMediaBrowser.subscribe(MEDIA_ID_INVALID, options, mSubscriptionCallback);
-            mSubscriptionCallback.mWaitLock.wait(TIME_OUT_MS);
-            assertEquals(MEDIA_ID_INVALID, mSubscriptionCallback.mLastErrorId);
-            assertNotNull(mSubscriptionCallback.mLastOptions);
-            assertEquals(page,
-                    mSubscriptionCallback.mLastOptions.getInt(MediaBrowserCompat.EXTRA_PAGE));
-            assertEquals(pageSize,
-                    mSubscriptionCallback.mLastOptions.getInt(MediaBrowserCompat.EXTRA_PAGE_SIZE));
-        }
-    }
-
-    @Test
-    @SmallTest
-    public void testUnsubscribeForMultipleSubscriptions() throws Exception {
-        createMediaBrowser(TEST_BROWSER_SERVICE);
-        connectMediaBrowserService();
-        final List<StubSubscriptionCallback> subscriptionCallbacks = new ArrayList<>();
-        final int pageSize = 1;
-
-        // Subscribe four pages, one item per page.
-        for (int page = 0; page < 4; page++) {
-            final StubSubscriptionCallback callback = new StubSubscriptionCallback();
-            subscriptionCallbacks.add(callback);
-
-            Bundle options = new Bundle();
-            options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
-            options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
-            mMediaBrowser.subscribe(MEDIA_ID_ROOT, options, callback);
-            synchronized (callback.mWaitLock) {
-                callback.mWaitLock.wait(TIME_OUT_MS);
-            }
-            // Each onChildrenLoaded() must be called.
-            assertEquals(1, callback.mChildrenLoadedWithOptionCount);
-        }
-
-        // Reset callbacks and unsubscribe.
-        for (StubSubscriptionCallback callback : subscriptionCallbacks) {
-            callback.reset();
-        }
-        mMediaBrowser.unsubscribe(MEDIA_ID_ROOT);
-
-        // After unsubscribing, make StubMediaBrowserServiceCompat notify that the children are
-        // changed.
-        callMediaBrowserServiceMethod(NOTIFY_CHILDREN_CHANGED, MEDIA_ID_ROOT);
-        try {
-            Thread.sleep(SLEEP_MS);
-        } catch (InterruptedException e) {
-            fail("Unexpected InterruptedException occurred.");
-        }
-
-        // onChildrenLoaded should not be called.
-        for (StubSubscriptionCallback callback : subscriptionCallbacks) {
-            assertEquals(0, callback.mChildrenLoadedWithOptionCount);
-        }
-    }
-
-    @Test
-    @MediumTest
-    public void testUnsubscribeWithSubscriptionCallbackForMultipleSubscriptions() throws Exception {
-        createMediaBrowser(TEST_BROWSER_SERVICE);
-        connectMediaBrowserService();
-        final List<StubSubscriptionCallback> subscriptionCallbacks = new ArrayList<>();
-        final int pageSize = 1;
-
-        // Subscribe four pages, one item per page.
-        for (int page = 0; page < 4; page++) {
-            final StubSubscriptionCallback callback = new StubSubscriptionCallback();
-            subscriptionCallbacks.add(callback);
-
-            Bundle options = new Bundle();
-            options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
-            options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
-            mMediaBrowser.subscribe(MEDIA_ID_ROOT, options,
-                    callback);
-            synchronized (callback.mWaitLock) {
-                callback.mWaitLock.wait(TIME_OUT_MS);
-            }
-            // Each onChildrenLoaded() must be called.
-            assertEquals(1, callback.mChildrenLoadedWithOptionCount);
-        }
-
-        // Unsubscribe existing subscriptions one-by-one.
-        final int[] orderOfRemovingCallbacks = {2, 0, 3, 1};
-        for (int i = 0; i < orderOfRemovingCallbacks.length; i++) {
-            // Reset callbacks
-            for (StubSubscriptionCallback callback : subscriptionCallbacks) {
-                callback.reset();
-            }
-
-            // Remove one subscription
-            mMediaBrowser.unsubscribe(MEDIA_ID_ROOT,
-                    subscriptionCallbacks.get(orderOfRemovingCallbacks[i]));
-
-            // Make StubMediaBrowserServiceCompat notify that the children are changed.
-            callMediaBrowserServiceMethod(NOTIFY_CHILDREN_CHANGED, MEDIA_ID_ROOT);
-            try {
-                Thread.sleep(SLEEP_MS);
-            } catch (InterruptedException e) {
-                fail("Unexpected InterruptedException occurred.");
-            }
-
-            // Only the remaining subscriptionCallbacks should be called.
-            for (int j = 0; j < 4; j++) {
-                int childrenLoadedWithOptionsCount = subscriptionCallbacks
-                        .get(orderOfRemovingCallbacks[j]).mChildrenLoadedWithOptionCount;
-                if (j <= i) {
-                    assertEquals(0, childrenLoadedWithOptionsCount);
-                } else {
-                    assertEquals(1, childrenLoadedWithOptionsCount);
-                }
-            }
-        }
-    }
-
-    @Test
-    @SmallTest
-    public void testGetItem() throws Exception {
-        createMediaBrowser(TEST_BROWSER_SERVICE);
-        connectMediaBrowserService();
-
-        synchronized (mItemCallback.mWaitLock) {
-            mMediaBrowser.getItem(MEDIA_ID_CHILDREN[0], mItemCallback);
-            mItemCallback.mWaitLock.wait(TIME_OUT_MS);
-            assertNotNull(mItemCallback.mLastMediaItem);
-            assertEquals(MEDIA_ID_CHILDREN[0], mItemCallback.mLastMediaItem.getMediaId());
-        }
-    }
-
-    @Test
-    @LargeTest
-    public void testGetItemWhenOnLoadItemIsNotImplemented() throws Exception {
-        createMediaBrowser(TEST_BROWSER_SERVICE);
-        connectMediaBrowserService();
-        synchronized (mItemCallback.mWaitLock) {
-            mMediaBrowser.getItem(MEDIA_ID_ON_LOAD_ITEM_NOT_IMPLEMENTED, mItemCallback);
-            mItemCallback.mWaitLock.wait(TIME_OUT_MS);
-            assertEquals(MEDIA_ID_ON_LOAD_ITEM_NOT_IMPLEMENTED, mItemCallback.mLastErrorId);
-        }
-    }
-
-    @Test
-    @SmallTest
-    public void testGetItemWhenMediaIdIsInvalid() throws Exception {
-        mItemCallback.mLastMediaItem = new MediaItem(new MediaDescriptionCompat.Builder()
-                .setMediaId("dummy_id").build(), MediaItem.FLAG_BROWSABLE);
-
-        createMediaBrowser(TEST_BROWSER_SERVICE);
-        connectMediaBrowserService();
-        synchronized (mItemCallback.mWaitLock) {
-            mMediaBrowser.getItem(MEDIA_ID_INVALID, mItemCallback);
-            mItemCallback.mWaitLock.wait(TIME_OUT_MS);
-            assertNull(mItemCallback.mLastMediaItem);
-            assertNull(mItemCallback.mLastErrorId);
-        }
-    }
-
-    private void createMediaBrowser(final ComponentName component) {
-        getInstrumentation().runOnMainSync(new Runnable() {
-            @Override
-            public void run() {
-                mMediaBrowser = new MediaBrowserCompat(getInstrumentation().getTargetContext(),
-                        component, mConnectionCallback, mRootHints);
-            }
-        });
-    }
-
-    private void connectMediaBrowserService() throws Exception {
-        synchronized (mConnectionCallback.mWaitLock) {
-            mMediaBrowser.connect();
-            mConnectionCallback.mWaitLock.wait(TIME_OUT_MS);
-            if (!mMediaBrowser.isConnected()) {
-                fail("Browser failed to connect!");
-            }
-        }
-    }
-
-    private void callMediaBrowserServiceMethod(int methodId, Object arg) {
-        IntentUtil.callMediaBrowserServiceMethod(methodId, arg, getContext());
-    }
-
-    private void resetCallbacks() {
-        mConnectionCallback.reset();
-        mSubscriptionCallback.reset();
-        mItemCallback.reset();
-    }
-
-    private class StubConnectionCallback extends MediaBrowserCompat.ConnectionCallback {
-        Object mWaitLock = new Object();
-        volatile int mConnectedCount;
-        volatile int mConnectionFailedCount;
-        volatile int mConnectionSuspendedCount;
-
-        public void reset() {
-            mConnectedCount = 0;
-            mConnectionFailedCount = 0;
-            mConnectionSuspendedCount = 0;
-        }
-
-        @Override
-        public void onConnected() {
-            synchronized (mWaitLock) {
-                mConnectedCount++;
-                mWaitLock.notify();
-            }
-        }
-
-        @Override
-        public void onConnectionFailed() {
-            synchronized (mWaitLock) {
-                mConnectionFailedCount++;
-                mWaitLock.notify();
-            }
-        }
-
-        @Override
-        public void onConnectionSuspended() {
-            synchronized (mWaitLock) {
-                mConnectionSuspendedCount++;
-                mWaitLock.notify();
-            }
-        }
-    }
-
-    private class StubSubscriptionCallback extends MediaBrowserCompat.SubscriptionCallback {
-        final Object mWaitLock = new Object();
-        private volatile int mChildrenLoadedCount;
-        private volatile int mChildrenLoadedWithOptionCount;
-        private volatile String mLastErrorId;
-        private volatile String mLastParentId;
-        private volatile Bundle mLastOptions;
-        private volatile List<MediaItem> mLastChildMediaItems;
-
-        public void reset() {
-            mChildrenLoadedCount = 0;
-            mChildrenLoadedWithOptionCount = 0;
-            mLastErrorId = null;
-            mLastParentId = null;
-            mLastOptions = null;
-            mLastChildMediaItems = null;
-        }
-
-        @Override
-        public void onChildrenLoaded(String parentId, List<MediaItem> children) {
-            synchronized (mWaitLock) {
-                mChildrenLoadedCount++;
-                mLastParentId = parentId;
-                mLastChildMediaItems = children;
-                mWaitLock.notify();
-            }
-        }
-
-        @Override
-        public void onChildrenLoaded(String parentId, List<MediaItem> children, Bundle options) {
-            synchronized (mWaitLock) {
-                mChildrenLoadedWithOptionCount++;
-                mLastParentId = parentId;
-                mLastOptions = options;
-                mLastChildMediaItems = children;
-                mWaitLock.notify();
-            }
-        }
-
-        @Override
-        public void onError(String id) {
-            synchronized (mWaitLock) {
-                mLastErrorId = id;
-                mWaitLock.notify();
-            }
-        }
-
-        @Override
-        public void onError(String id, Bundle options) {
-            synchronized (mWaitLock) {
-                mLastErrorId = id;
-                mLastOptions = options;
-                mWaitLock.notify();
-            }
-        }
-    }
-
-    private class StubItemCallback extends MediaBrowserCompat.ItemCallback {
-        final Object mWaitLock = new Object();
-        private volatile MediaItem mLastMediaItem;
-        private volatile String mLastErrorId;
-
-        public void reset() {
-            mLastMediaItem = null;
-            mLastErrorId = null;
-        }
-
-        @Override
-        public void onItemLoaded(MediaItem item) {
-            synchronized (mWaitLock) {
-                mLastMediaItem = item;
-                mWaitLock.notify();
-            }
-        }
-
-        @Override
-        public void onError(String id) {
-            synchronized (mWaitLock) {
-                mLastErrorId = id;
-                mWaitLock.notify();
-            }
-        }
-    }
-}
diff --git a/media-compat-test-client/tests/src/android/support/mediacompat/client/util/IntentUtil.java b/media-compat-test-client/tests/src/android/support/mediacompat/client/util/IntentUtil.java
deleted file mode 100644
index bcf33a4..0000000
--- a/media-compat-test-client/tests/src/android/support/mediacompat/client/util/IntentUtil.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * 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.mediacompat.client.util;
-
-import static android.support.mediacompat.testlib.IntentConstants
-        .ACTION_CALL_MEDIA_BROWSER_SERVICE_METHOD;
-import static android.support.mediacompat.testlib.IntentConstants.KEY_ARGUMENT;
-import static android.support.mediacompat.testlib.IntentConstants.KEY_METHOD_ID;
-
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Parcelable;
-
-import java.util.ArrayList;
-
-public class IntentUtil {
-
-    public static final ComponentName SERVICE_RECEIVER_COMPONENT_NAME = new ComponentName(
-            "android.support.mediacompat.service.test",
-            "android.support.mediacompat.service.ServiceBroadcastReceiver");
-
-    public static void callMediaBrowserServiceMethod(int methodId, Object arg, Context context) {
-        Intent intent = createIntent(SERVICE_RECEIVER_COMPONENT_NAME, methodId, arg);
-        intent.setAction(ACTION_CALL_MEDIA_BROWSER_SERVICE_METHOD);
-        if (Build.VERSION.SDK_INT >= 16) {
-            intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
-        }
-        context.sendBroadcast(intent);
-    }
-
-    private static Intent createIntent(ComponentName componentName, int methodId, Object arg) {
-        Intent intent = new Intent();
-        intent.setComponent(componentName);
-        intent.putExtra(KEY_METHOD_ID, methodId);
-
-        if (arg instanceof String) {
-            intent.putExtra(KEY_ARGUMENT, (String) arg);
-        } else if (arg instanceof Integer) {
-            intent.putExtra(KEY_ARGUMENT, (int) arg);
-        } else if (arg instanceof Long) {
-            intent.putExtra(KEY_ARGUMENT, (long) arg);
-        } else if (arg instanceof Boolean) {
-            intent.putExtra(KEY_ARGUMENT, (boolean) arg);
-        } else if (arg instanceof Parcelable) {
-            intent.putExtra(KEY_ARGUMENT, (Parcelable) arg);
-        } else if (arg instanceof ArrayList<?>) {
-            Bundle bundle = new Bundle();
-            bundle.putParcelableArrayList(KEY_ARGUMENT, (ArrayList<? extends Parcelable>) arg);
-            intent.putExtras(bundle);
-        } else if (arg instanceof Bundle) {
-            Bundle bundle = new Bundle();
-            bundle.putBundle(KEY_ARGUMENT, (Bundle) arg);
-            intent.putExtras(bundle);
-        }
-        return intent;
-    }
-}
diff --git a/media-compat-test-lib/OWNERS b/media-compat-test-lib/OWNERS
deleted file mode 100644
index 5529026..0000000
--- a/media-compat-test-lib/OWNERS
+++ /dev/null
@@ -1,2 +0,0 @@
-hdmoon@google.com
-sungsoo@google.com
\ No newline at end of file
diff --git a/media-compat-test-lib/src/main/java/android/support/mediacompat/testlib/IntentConstants.java b/media-compat-test-lib/src/main/java/android/support/mediacompat/testlib/IntentConstants.java
deleted file mode 100644
index bc35935..0000000
--- a/media-compat-test-lib/src/main/java/android/support/mediacompat/testlib/IntentConstants.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * 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.mediacompat.testlib;
-
-/**
- * Constants used for sending intent between client and service apks.
- */
-public class IntentConstants {
-    public static final String ACTION_CALL_MEDIA_BROWSER_SERVICE_METHOD =
-            "android.support.mediacompat.service.action.CALL_MEDIA_BROWSER_SERVICE_METHOD";
-    public static final String KEY_METHOD_ID = "method_id";
-    public static final String KEY_ARGUMENT = "argument";
-}
diff --git a/media-compat-test-service/OWNERS b/media-compat-test-service/OWNERS
deleted file mode 100644
index 5529026..0000000
--- a/media-compat-test-service/OWNERS
+++ /dev/null
@@ -1,2 +0,0 @@
-hdmoon@google.com
-sungsoo@google.com
\ No newline at end of file
diff --git a/media-compat-test-service/lint-baseline.xml b/media-compat-test-service/lint-baseline.xml
deleted file mode 100644
index 8bc6f6f..0000000
--- a/media-compat-test-service/lint-baseline.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="4" by="lint 3.0.0-alpha9">
-
-</issues>
diff --git a/media-compat-test-service/tests/src/android/support/mediacompat/service/ServiceBroadcastReceiver.java b/media-compat-test-service/tests/src/android/support/mediacompat/service/ServiceBroadcastReceiver.java
deleted file mode 100644
index d987fd8..0000000
--- a/media-compat-test-service/tests/src/android/support/mediacompat/service/ServiceBroadcastReceiver.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * 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.mediacompat.service;
-
-
-import static android.support.mediacompat.testlib.IntentConstants
-        .ACTION_CALL_MEDIA_BROWSER_SERVICE_METHOD;
-import static android.support.mediacompat.testlib.IntentConstants.KEY_ARGUMENT;
-import static android.support.mediacompat.testlib.IntentConstants.KEY_METHOD_ID;
-import static android.support.mediacompat.testlib.MediaBrowserConstants.CUSTOM_ACTION_SEND_ERROR;
-import static android.support.mediacompat.testlib.MediaBrowserConstants
-        .CUSTOM_ACTION_SEND_PROGRESS_UPDATE;
-import static android.support.mediacompat.testlib.MediaBrowserConstants.CUSTOM_ACTION_SEND_RESULT;
-import static android.support.mediacompat.testlib.MediaBrowserConstants.NOTIFY_CHILDREN_CHANGED;
-import static android.support.mediacompat.testlib.MediaBrowserConstants.SEND_DELAYED_ITEM_LOADED;
-import static android.support.mediacompat.testlib.MediaBrowserConstants
-        .SEND_DELAYED_NOTIFY_CHILDREN_CHANGED;
-import static android.support.mediacompat.testlib.MediaBrowserConstants.SET_SESSION_TOKEN;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.os.Bundle;
-
-public class ServiceBroadcastReceiver extends BroadcastReceiver {
-
-    @Override
-    public void onReceive(Context context, Intent intent) {
-        Bundle extras = intent.getExtras();
-        if (ACTION_CALL_MEDIA_BROWSER_SERVICE_METHOD.equals(intent.getAction()) && extras != null) {
-            StubMediaBrowserServiceCompat service = StubMediaBrowserServiceCompat.sInstance;
-            int method = extras.getInt(KEY_METHOD_ID, 0);
-
-            switch (method) {
-                case NOTIFY_CHILDREN_CHANGED:
-                    service.notifyChildrenChanged(extras.getString(KEY_ARGUMENT));
-                    break;
-                case SEND_DELAYED_NOTIFY_CHILDREN_CHANGED:
-                    service.sendDelayedNotifyChildrenChanged();
-                    break;
-                case SEND_DELAYED_ITEM_LOADED:
-                    service.sendDelayedItemLoaded();
-                    break;
-                case CUSTOM_ACTION_SEND_PROGRESS_UPDATE:
-                    service.mCustomActionResult.sendProgressUpdate(extras.getBundle(KEY_ARGUMENT));
-                    break;
-                case CUSTOM_ACTION_SEND_ERROR:
-                    service.mCustomActionResult.sendError(extras.getBundle(KEY_ARGUMENT));
-                    break;
-                case CUSTOM_ACTION_SEND_RESULT:
-                    service.mCustomActionResult.sendResult(extras.getBundle(KEY_ARGUMENT));
-                    break;
-                case SET_SESSION_TOKEN:
-                    StubMediaBrowserServiceCompatWithDelayedMediaSession.sInstance
-                            .callSetSessionToken();
-                    break;
-            }
-        }
-    }
-}
diff --git a/media-compat/tests/src/android/support/v4/media/session/MediaControllerCompatTest.java b/media-compat/tests/src/android/support/v4/media/session/MediaControllerCompatTest.java
index 53d7e47..b197a42 100644
--- a/media-compat/tests/src/android/support/v4/media/session/MediaControllerCompatTest.java
+++ b/media-compat/tests/src/android/support/v4/media/session/MediaControllerCompatTest.java
@@ -438,7 +438,7 @@
         final long expectedUpdateTime = waitDuration + stateSetTime;
         final long expectedPosition = (long) (TEST_PLAYBACK_SPEED * waitDuration) + TEST_POSITION;
 
-        final double updateTimeTolerance = 30L;
+        final double updateTimeTolerance = 50L;
         final double positionTolerance = updateTimeTolerance * TEST_PLAYBACK_SPEED;
 
         PlaybackStateCompat stateOut = mSession.getController().getPlaybackState();
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/media-compat-test-client/OWNERS b/media-compat/version-compat-tests/OWNERS
similarity index 100%
rename from media-compat-test-client/OWNERS
rename to media-compat/version-compat-tests/OWNERS
diff --git a/media-compat-test-client/AndroidManifest.xml b/media-compat/version-compat-tests/current/client/AndroidManifest.xml
similarity index 92%
rename from media-compat-test-client/AndroidManifest.xml
rename to media-compat/version-compat-tests/current/client/AndroidManifest.xml
index 290b67e..9724d2b 100644
--- a/media-compat-test-client/AndroidManifest.xml
+++ b/media-compat/version-compat-tests/current/client/AndroidManifest.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-   Copyright (C) 2017 The Android Open Source Project
+   Copyright 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.
diff --git a/media-compat-test-service/build.gradle b/media-compat/version-compat-tests/current/client/build.gradle
similarity index 79%
copy from media-compat-test-service/build.gradle
copy to media-compat/version-compat-tests/current/client/build.gradle
index 946d48b..d6f0e7d 100644
--- a/media-compat-test-service/build.gradle
+++ b/media-compat/version-compat-tests/current/client/build.gradle
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2017 The Android Open Source Project
+ * Copyright 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.
@@ -19,13 +19,10 @@
 }
 
 dependencies {
-    androidTestImplementation project(':support-annotations')
     androidTestImplementation project(':support-media-compat')
     androidTestImplementation project(':support-media-compat-test-lib')
 
-    androidTestImplementation(libs.test_runner) {
-        exclude module: 'support-annotations'
-    }
+    androidTestImplementation(libs.test_runner)
 }
 
 android {
@@ -36,4 +33,4 @@
 
 supportLibrary {
     legacySourceLocation = true
-}
+}
\ No newline at end of file
diff --git a/media-compat-test-service/AndroidManifest.xml b/media-compat/version-compat-tests/current/client/lint-baseline.xml
similarity index 78%
copy from media-compat-test-service/AndroidManifest.xml
copy to media-compat/version-compat-tests/current/client/lint-baseline.xml
index 0390e8a..ed7ade1 100644
--- a/media-compat-test-service/AndroidManifest.xml
+++ b/media-compat/version-compat-tests/current/client/lint-baseline.xml
@@ -1,6 +1,6 @@
-<?xml version="1.0" encoding="utf-8"?>
+<?xml version="1.0" encoding="UTF-8"?>
 <!--
-   Copyright (C) 2017 The Android Open Source Project
+   Copyright 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.
@@ -14,4 +14,6 @@
    See the License for the specific language governing permissions and
    limitations under the License.
 -->
-<manifest package="android.support.mediacompat.service"/>
+<issues format="4" by="lint 3.0.0-alpha9">
+
+</issues>
diff --git a/media-compat/version-compat-tests/current/client/tests/AndroidManifest.xml b/media-compat/version-compat-tests/current/client/tests/AndroidManifest.xml
new file mode 100644
index 0000000..afe1865
--- /dev/null
+++ b/media-compat/version-compat-tests/current/client/tests/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+   Copyright 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.mediacompat.client.test">
+    <application android:supportsRtl="true">
+        <receiver android:name="android.support.mediacompat.client.ClientBroadcastReceiver">
+            <intent-filter>
+                <action android:name="android.support.mediacompat.service.action.CALL_MEDIA_CONTROLLER_METHOD"/>
+                <action android:name="android.support.mediacompat.service.action.CALL_TRANSPORT_CONTROLS_METHOD"/>
+            </intent-filter>
+        </receiver>
+    </application>
+</manifest>
diff --git a/design/jvm-tests/NO_DOCS b/media-compat/version-compat-tests/current/client/tests/NO_DOCS
similarity index 92%
rename from design/jvm-tests/NO_DOCS
rename to media-compat/version-compat-tests/current/client/tests/NO_DOCS
index 092a39c..61c9b1a 100644
--- a/design/jvm-tests/NO_DOCS
+++ b/media-compat/version-compat-tests/current/client/tests/NO_DOCS
@@ -1,4 +1,4 @@
-# Copyright (C) 2016 The Android Open Source Project
+# Copyright 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.
diff --git a/media-compat/version-compat-tests/current/client/tests/src/android/support/mediacompat/client/ClientBroadcastReceiver.java b/media-compat/version-compat-tests/current/client/tests/src/android/support/mediacompat/client/ClientBroadcastReceiver.java
new file mode 100644
index 0000000..3166e55
--- /dev/null
+++ b/media-compat/version-compat-tests/current/client/tests/src/android/support/mediacompat/client/ClientBroadcastReceiver.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright 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.mediacompat.client;
+
+import static android.support.mediacompat.testlib.MediaControllerConstants.ADD_QUEUE_ITEM;
+import static android.support.mediacompat.testlib.MediaControllerConstants
+        .ADD_QUEUE_ITEM_WITH_INDEX;
+import static android.support.mediacompat.testlib.MediaControllerConstants.FAST_FORWARD;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PAUSE;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PLAY;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PLAY_FROM_MEDIA_ID;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PLAY_FROM_SEARCH;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PLAY_FROM_URI;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PREPARE;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PREPARE_FROM_MEDIA_ID;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PREPARE_FROM_SEARCH;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PREPARE_FROM_URI;
+import static android.support.mediacompat.testlib.MediaControllerConstants.REMOVE_QUEUE_ITEM;
+import static android.support.mediacompat.testlib.MediaControllerConstants.REWIND;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SEEK_TO;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SEND_COMMAND;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SEND_CUSTOM_ACTION;
+import static android.support.mediacompat.testlib.MediaControllerConstants
+        .SEND_CUSTOM_ACTION_PARCELABLE;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SET_CAPTIONING_ENABLED;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SET_RATING;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SET_REPEAT_MODE;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SET_SHUFFLE_MODE;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SKIP_TO_NEXT;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SKIP_TO_PREVIOUS;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SKIP_TO_QUEUE_ITEM;
+import static android.support.mediacompat.testlib.MediaControllerConstants.STOP;
+import static android.support.mediacompat.testlib.util.IntentUtil
+        .ACTION_CALL_MEDIA_CONTROLLER_METHOD;
+import static android.support.mediacompat.testlib.util.IntentUtil
+        .ACTION_CALL_TRANSPORT_CONTROLS_METHOD;
+import static android.support.mediacompat.testlib.util.IntentUtil.KEY_ARGUMENT;
+import static android.support.mediacompat.testlib.util.IntentUtil.KEY_METHOD_ID;
+import static android.support.mediacompat.testlib.util.IntentUtil.KEY_SESSION_TOKEN;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.support.v4.media.MediaDescriptionCompat;
+import android.support.v4.media.RatingCompat;
+import android.support.v4.media.session.MediaControllerCompat;
+import android.support.v4.media.session.MediaControllerCompat.TransportControls;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+
+public class ClientBroadcastReceiver extends BroadcastReceiver {
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        Bundle extras = intent.getExtras();
+        MediaControllerCompat controller;
+        try {
+            controller = new MediaControllerCompat(context,
+                    (MediaSessionCompat.Token) extras.getParcelable(KEY_SESSION_TOKEN));
+        } catch (RemoteException ex) {
+            // Do nothing.
+            return;
+        }
+        int method = extras.getInt(KEY_METHOD_ID, 0);
+
+        if (ACTION_CALL_MEDIA_CONTROLLER_METHOD.equals(intent.getAction()) && extras != null) {
+            Bundle arguments;
+            switch (method) {
+                case SEND_COMMAND:
+                    arguments = extras.getBundle(KEY_ARGUMENT);
+                    controller.sendCommand(
+                            arguments.getString("command"),
+                            arguments.getBundle("extras"),
+                            new ResultReceiver(null));
+                    break;
+                case ADD_QUEUE_ITEM:
+                    controller.addQueueItem(
+                            (MediaDescriptionCompat) extras.getParcelable(KEY_ARGUMENT));
+                    break;
+                case ADD_QUEUE_ITEM_WITH_INDEX:
+                    arguments = extras.getBundle(KEY_ARGUMENT);
+                    controller.addQueueItem(
+                            (MediaDescriptionCompat) arguments.getParcelable("description"),
+                            arguments.getInt("index"));
+                    break;
+                case REMOVE_QUEUE_ITEM:
+                    controller.removeQueueItem(
+                            (MediaDescriptionCompat) extras.getParcelable(KEY_ARGUMENT));
+                    break;
+            }
+        } else if (ACTION_CALL_TRANSPORT_CONTROLS_METHOD.equals(intent.getAction())
+                && extras != null) {
+            TransportControls controls = controller.getTransportControls();
+            Bundle arguments;
+            switch (method) {
+                case PLAY:
+                    controls.play();
+                    break;
+                case PAUSE:
+                    controls.pause();
+                    break;
+                case STOP:
+                    controls.stop();
+                    break;
+                case FAST_FORWARD:
+                    controls.fastForward();
+                    break;
+                case REWIND:
+                    controls.rewind();
+                    break;
+                case SKIP_TO_PREVIOUS:
+                    controls.skipToPrevious();
+                    break;
+                case SKIP_TO_NEXT:
+                    controls.skipToNext();
+                    break;
+                case SEEK_TO:
+                    controls.seekTo(extras.getLong(KEY_ARGUMENT));
+                    break;
+                case SET_RATING:
+                    controls.setRating((RatingCompat) extras.getParcelable(KEY_ARGUMENT));
+                    break;
+                case PLAY_FROM_MEDIA_ID:
+                    arguments = extras.getBundle(KEY_ARGUMENT);
+                    controls.playFromMediaId(
+                            arguments.getString("mediaId"),
+                            arguments.getBundle("extras"));
+                    break;
+                case PLAY_FROM_SEARCH:
+                    arguments = extras.getBundle(KEY_ARGUMENT);
+                    controls.playFromSearch(
+                            arguments.getString("query"),
+                            arguments.getBundle("extras"));
+                    break;
+                case PLAY_FROM_URI:
+                    arguments = extras.getBundle(KEY_ARGUMENT);
+                    controls.playFromUri(
+                            (Uri) arguments.getParcelable("uri"),
+                            arguments.getBundle("extras"));
+                    break;
+                case SEND_CUSTOM_ACTION:
+                    arguments = extras.getBundle(KEY_ARGUMENT);
+                    controls.sendCustomAction(
+                            arguments.getString("action"),
+                            arguments.getBundle("extras"));
+                    break;
+                case SEND_CUSTOM_ACTION_PARCELABLE:
+                    arguments = extras.getBundle(KEY_ARGUMENT);
+                    controls.sendCustomAction(
+                            (PlaybackStateCompat.CustomAction)
+                                    arguments.getParcelable("action"),
+                            arguments.getBundle("extras"));
+                    break;
+                case SKIP_TO_QUEUE_ITEM:
+                    controls.skipToQueueItem(extras.getLong(KEY_ARGUMENT));
+                    break;
+                case PREPARE:
+                    controls.prepare();
+                    break;
+                case PREPARE_FROM_MEDIA_ID:
+                    arguments = extras.getBundle(KEY_ARGUMENT);
+                    controls.prepareFromMediaId(
+                            arguments.getString("mediaId"),
+                            arguments.getBundle("extras"));
+                    break;
+                case PREPARE_FROM_SEARCH:
+                    arguments = extras.getBundle(KEY_ARGUMENT);
+                    controls.prepareFromSearch(
+                            arguments.getString("query"),
+                            arguments.getBundle("extras"));
+                    break;
+                case PREPARE_FROM_URI:
+                    arguments = extras.getBundle(KEY_ARGUMENT);
+                    controls.prepareFromUri(
+                            (Uri) arguments.getParcelable("uri"),
+                            arguments.getBundle("extras"));
+                    break;
+                case SET_CAPTIONING_ENABLED:
+                    controls.setCaptioningEnabled(extras.getBoolean(KEY_ARGUMENT));
+                    break;
+                case SET_REPEAT_MODE:
+                    controls.setRepeatMode(extras.getInt(KEY_ARGUMENT));
+                    break;
+                case SET_SHUFFLE_MODE:
+                    controls.setShuffleMode(extras.getInt(KEY_ARGUMENT));
+                    break;
+            }
+        }
+    }
+}
diff --git a/media-compat/version-compat-tests/current/client/tests/src/android/support/mediacompat/client/MediaBrowserCompatTest.java b/media-compat/version-compat-tests/current/client/tests/src/android/support/mediacompat/client/MediaBrowserCompatTest.java
new file mode 100644
index 0000000..31bdb7a
--- /dev/null
+++ b/media-compat/version-compat-tests/current/client/tests/src/android/support/mediacompat/client/MediaBrowserCompatTest.java
@@ -0,0 +1,1012 @@
+/*
+ * Copyright 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.mediacompat.client;
+
+import static android.support.mediacompat.testlib.MediaBrowserConstants.CUSTOM_ACTION;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.CUSTOM_ACTION_FOR_ERROR;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.CUSTOM_ACTION_SEND_ERROR;
+import static android.support.mediacompat.testlib.MediaBrowserConstants
+        .CUSTOM_ACTION_SEND_PROGRESS_UPDATE;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.CUSTOM_ACTION_SEND_RESULT;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.EXTRAS_KEY;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.EXTRAS_VALUE;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.MEDIA_ID_CHILDREN;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.MEDIA_ID_CHILDREN_DELAYED;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.MEDIA_ID_INVALID;
+import static android.support.mediacompat.testlib.MediaBrowserConstants
+        .MEDIA_ID_ON_LOAD_ITEM_NOT_IMPLEMENTED;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.MEDIA_ID_ROOT;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.NOTIFY_CHILDREN_CHANGED;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.SEARCH_QUERY;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.SEARCH_QUERY_FOR_ERROR;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.SEARCH_QUERY_FOR_NO_RESULT;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.SEND_DELAYED_ITEM_LOADED;
+import static android.support.mediacompat.testlib.MediaBrowserConstants
+        .SEND_DELAYED_NOTIFY_CHILDREN_CHANGED;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.SET_SESSION_TOKEN;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.TEST_KEY_1;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.TEST_KEY_2;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.TEST_KEY_3;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.TEST_KEY_4;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.TEST_VALUE_1;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.TEST_VALUE_2;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.TEST_VALUE_3;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.TEST_VALUE_4;
+import static android.support.mediacompat.testlib.VersionConstants.KEY_SERVICE_VERSION;
+import static android.support.mediacompat.testlib.util.IntentUtil.callMediaBrowserServiceMethod;
+import static android.support.test.InstrumentationRegistry.getArguments;
+import static android.support.test.InstrumentationRegistry.getContext;
+import static android.support.test.InstrumentationRegistry.getInstrumentation;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertNull;
+import static junit.framework.Assert.assertTrue;
+import static junit.framework.Assert.fail;
+
+import android.content.ComponentName;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.mediacompat.testlib.util.PollingCheck;
+import android.support.test.filters.MediumTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.media.MediaBrowserCompat;
+import android.support.v4.media.MediaBrowserCompat.MediaItem;
+import android.support.v4.media.MediaBrowserServiceCompat;
+import android.support.v4.media.MediaDescriptionCompat;
+import android.util.Log;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Test {@link android.support.v4.media.MediaBrowserCompat}.
+ */
+@RunWith(AndroidJUnit4.class)
+public class MediaBrowserCompatTest {
+
+    private static final String TAG = "MediaBrowserCompatTest";
+
+    // The maximum time to wait for an operation.
+    private static final long TIME_OUT_MS = 3000L;
+    private static final long WAIT_TIME_FOR_NO_RESPONSE_MS = 300L;
+
+    /**
+     * To check {@link MediaBrowserCompat#unsubscribe} works properly,
+     * we notify to the browser after the unsubscription that the media items have changed.
+     * Then {@link MediaBrowserCompat.SubscriptionCallback#onChildrenLoaded} should not be called.
+     *
+     * The measured time from calling {@link MediaBrowserServiceCompat#notifyChildrenChanged}
+     * to {@link MediaBrowserCompat.SubscriptionCallback#onChildrenLoaded} being called is about
+     * 50ms.
+     * So we make the thread sleep for 100ms to properly check that the callback is not called.
+     */
+    private static final long SLEEP_MS = 100L;
+    private static final ComponentName TEST_BROWSER_SERVICE = new ComponentName(
+            "android.support.mediacompat.service.test",
+            "android.support.mediacompat.service.StubMediaBrowserServiceCompat");
+    private static final ComponentName TEST_BROWSER_SERVICE_DELAYED_MEDIA_SESSION =
+            new ComponentName(
+                    "android.support.mediacompat.service.test",
+                    "android.support.mediacompat.service"
+                            + ".StubMediaBrowserServiceCompatWithDelayedMediaSession");
+
+    private String mServiceVersion;
+    private MediaBrowserCompat mMediaBrowser;
+    private StubConnectionCallback mConnectionCallback;
+    private StubSubscriptionCallback mSubscriptionCallback;
+    private StubItemCallback mItemCallback;
+    private StubSearchCallback mSearchCallback;
+    private CustomActionCallback mCustomActionCallback;
+    private Bundle mRootHints;
+
+    @Before
+    public void setUp() {
+        // The version of the service app is provided through the instrumentation arguments.
+        mServiceVersion = getArguments().getString(KEY_SERVICE_VERSION, "");
+        Log.d(TAG, "Service app version: " + mServiceVersion);
+
+        mConnectionCallback = new StubConnectionCallback();
+        mSubscriptionCallback = new StubSubscriptionCallback();
+        mItemCallback = new StubItemCallback();
+        mSearchCallback = new StubSearchCallback();
+        mCustomActionCallback = new CustomActionCallback();
+
+        mRootHints = new Bundle();
+        mRootHints.putBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_RECENT, true);
+        mRootHints.putBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_OFFLINE, true);
+        mRootHints.putBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_SUGGESTED, true);
+
+        getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mMediaBrowser = new MediaBrowserCompat(getInstrumentation().getTargetContext(),
+                        TEST_BROWSER_SERVICE, mConnectionCallback, mRootHints);
+            }
+        });
+    }
+
+    @After
+    public void tearDown() {
+        if (mMediaBrowser != null && mMediaBrowser.isConnected()) {
+            mMediaBrowser.disconnect();
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testMediaBrowser() throws Exception {
+        assertFalse(mMediaBrowser.isConnected());
+
+        connectMediaBrowserService();
+        assertTrue(mMediaBrowser.isConnected());
+
+        assertEquals(TEST_BROWSER_SERVICE, mMediaBrowser.getServiceComponent());
+        assertEquals(MEDIA_ID_ROOT, mMediaBrowser.getRoot());
+        assertEquals(EXTRAS_VALUE, mMediaBrowser.getExtras().getString(EXTRAS_KEY));
+
+        mMediaBrowser.disconnect();
+        new PollingCheck(TIME_OUT_MS) {
+            @Override
+            protected boolean check() {
+                return !mMediaBrowser.isConnected();
+            }
+        }.run();
+    }
+
+    @Test
+    @SmallTest
+    public void testConnectTwice() throws Exception {
+        connectMediaBrowserService();
+        try {
+            mMediaBrowser.connect();
+            fail();
+        } catch (IllegalStateException e) {
+            // expected
+        }
+    }
+
+    @Test
+    @MediumTest
+    public void testReconnection() throws Exception {
+        getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mMediaBrowser.connect();
+                // Reconnect before the first connection was established.
+                mMediaBrowser.disconnect();
+                mMediaBrowser.connect();
+            }
+        });
+
+        synchronized (mConnectionCallback.mWaitLock) {
+            mConnectionCallback.mWaitLock.wait(TIME_OUT_MS);
+            assertEquals(1, mConnectionCallback.mConnectedCount);
+        }
+
+        synchronized (mSubscriptionCallback.mWaitLock) {
+            // Test subscribe.
+            resetCallbacks();
+            mMediaBrowser.subscribe(MEDIA_ID_ROOT, mSubscriptionCallback);
+            mSubscriptionCallback.mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mSubscriptionCallback.mChildrenLoadedCount > 0);
+            assertEquals(MEDIA_ID_ROOT, mSubscriptionCallback.mLastParentId);
+        }
+
+        synchronized (mItemCallback.mWaitLock) {
+            // Test getItem.
+            resetCallbacks();
+            mMediaBrowser.getItem(MEDIA_ID_CHILDREN[0], mItemCallback);
+            mItemCallback.mWaitLock.wait(TIME_OUT_MS);
+            assertEquals(MEDIA_ID_CHILDREN[0], mItemCallback.mLastMediaItem.getMediaId());
+        }
+
+        // Reconnect after connection was established.
+        mMediaBrowser.disconnect();
+        resetCallbacks();
+        connectMediaBrowserService();
+
+        synchronized (mItemCallback.mWaitLock) {
+            // Test getItem.
+            resetCallbacks();
+            mMediaBrowser.getItem(MEDIA_ID_CHILDREN[0], mItemCallback);
+            mItemCallback.mWaitLock.wait(TIME_OUT_MS);
+            assertEquals(MEDIA_ID_CHILDREN[0], mItemCallback.mLastMediaItem.getMediaId());
+        }
+    }
+
+    @Test
+    @MediumTest
+    public void testConnectionCallbackNotCalledAfterDisconnect() {
+        getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mMediaBrowser.connect();
+                mMediaBrowser.disconnect();
+                resetCallbacks();
+            }
+        });
+
+        try {
+            Thread.sleep(SLEEP_MS);
+        } catch (InterruptedException e) {
+            fail("Unexpected InterruptedException occurred.");
+        }
+        assertEquals(0, mConnectionCallback.mConnectedCount);
+        assertEquals(0, mConnectionCallback.mConnectionFailedCount);
+        assertEquals(0, mConnectionCallback.mConnectionSuspendedCount);
+    }
+
+//    @Test
+//    @MediumTest
+    public void testSubscribe() throws Exception {
+        connectMediaBrowserService();
+
+        synchronized (mSubscriptionCallback.mWaitLock) {
+            mMediaBrowser.subscribe(MEDIA_ID_ROOT, mSubscriptionCallback);
+            mSubscriptionCallback.mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mSubscriptionCallback.mChildrenLoadedCount > 0);
+            assertEquals(MEDIA_ID_ROOT, mSubscriptionCallback.mLastParentId);
+            assertEquals(MEDIA_ID_CHILDREN.length,
+                    mSubscriptionCallback.mLastChildMediaItems.size());
+            for (int i = 0; i < MEDIA_ID_CHILDREN.length; ++i) {
+                assertEquals(MEDIA_ID_CHILDREN[i],
+                        mSubscriptionCallback.mLastChildMediaItems.get(i).getMediaId());
+            }
+
+            // Test MediaBrowserServiceCompat.notifyChildrenChanged()
+            mSubscriptionCallback.reset();
+            callMediaBrowserServiceMethod(NOTIFY_CHILDREN_CHANGED, MEDIA_ID_ROOT, getContext());
+            mSubscriptionCallback.mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mSubscriptionCallback.mChildrenLoadedCount > 0);
+        }
+
+        // Test unsubscribe.
+        resetCallbacks();
+        mMediaBrowser.unsubscribe(MEDIA_ID_ROOT);
+
+        // After unsubscribing, make StubMediaBrowserServiceCompat notify that the children are
+        // changed.
+        callMediaBrowserServiceMethod(NOTIFY_CHILDREN_CHANGED, MEDIA_ID_ROOT, getContext());
+        try {
+            Thread.sleep(SLEEP_MS);
+        } catch (InterruptedException e) {
+            fail("Unexpected InterruptedException occurred.");
+        }
+        // onChildrenLoaded should not be called.
+        assertEquals(0, mSubscriptionCallback.mChildrenLoadedCount);
+    }
+
+//    @Test
+//    @MediumTest
+    public void testSubscribeWithOptions() throws Exception {
+        connectMediaBrowserService();
+        final int pageSize = 3;
+        final int lastPage = (MEDIA_ID_CHILDREN.length - 1) / pageSize;
+        Bundle options = new Bundle();
+        options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
+
+        synchronized (mSubscriptionCallback.mWaitLock) {
+            for (int page = 0; page <= lastPage; ++page) {
+                resetCallbacks();
+                options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
+                mMediaBrowser.subscribe(MEDIA_ID_ROOT, options, mSubscriptionCallback);
+                mSubscriptionCallback.mWaitLock.wait(TIME_OUT_MS);
+                assertTrue(mSubscriptionCallback.mChildrenLoadedWithOptionCount > 0);
+                assertEquals(MEDIA_ID_ROOT, mSubscriptionCallback.mLastParentId);
+                if (page != lastPage) {
+                    assertEquals(pageSize, mSubscriptionCallback.mLastChildMediaItems.size());
+                } else {
+                    assertEquals((MEDIA_ID_CHILDREN.length - 1) % pageSize + 1,
+                            mSubscriptionCallback.mLastChildMediaItems.size());
+                }
+                // Check whether all the items in the current page are loaded.
+                for (int i = 0; i < mSubscriptionCallback.mLastChildMediaItems.size(); ++i) {
+                    assertEquals(MEDIA_ID_CHILDREN[page * pageSize + i],
+                            mSubscriptionCallback.mLastChildMediaItems.get(i).getMediaId());
+                }
+            }
+
+            // Test MediaBrowserServiceCompat.notifyChildrenChanged()
+            mSubscriptionCallback.reset();
+            callMediaBrowserServiceMethod(NOTIFY_CHILDREN_CHANGED, MEDIA_ID_ROOT, getContext());
+            mSubscriptionCallback.mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mSubscriptionCallback.mChildrenLoadedWithOptionCount > 0);
+        }
+
+        // Test unsubscribe with callback argument.
+        resetCallbacks();
+        mMediaBrowser.unsubscribe(MEDIA_ID_ROOT, mSubscriptionCallback);
+
+        // After unsubscribing, make StubMediaBrowserServiceCompat notify that the children are
+        // changed.
+        callMediaBrowserServiceMethod(NOTIFY_CHILDREN_CHANGED, MEDIA_ID_ROOT, getContext());
+        try {
+            Thread.sleep(SLEEP_MS);
+        } catch (InterruptedException e) {
+            fail("Unexpected InterruptedException occurred.");
+        }
+        // onChildrenLoaded should not be called.
+        assertEquals(0, mSubscriptionCallback.mChildrenLoadedCount);
+    }
+
+    @Test
+    @MediumTest
+    public void testSubscribeDelayedItems() throws Exception {
+        connectMediaBrowserService();
+        synchronized (mSubscriptionCallback.mWaitLock) {
+            mSubscriptionCallback.reset();
+            mMediaBrowser.subscribe(MEDIA_ID_CHILDREN_DELAYED, mSubscriptionCallback);
+            mSubscriptionCallback.mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
+            assertEquals(0, mSubscriptionCallback.mChildrenLoadedCount);
+
+            callMediaBrowserServiceMethod(
+                    SEND_DELAYED_NOTIFY_CHILDREN_CHANGED, MEDIA_ID_CHILDREN_DELAYED, getContext());
+            mSubscriptionCallback.mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mSubscriptionCallback.mChildrenLoadedCount > 0);
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testSubscribeInvalidItem() throws Exception {
+        connectMediaBrowserService();
+
+        synchronized (mSubscriptionCallback.mWaitLock) {
+            mMediaBrowser.subscribe(MEDIA_ID_INVALID, mSubscriptionCallback);
+            mSubscriptionCallback.mWaitLock.wait(TIME_OUT_MS);
+            assertEquals(MEDIA_ID_INVALID, mSubscriptionCallback.mLastErrorId);
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testSubscribeInvalidItemWithOptions() throws Exception {
+        connectMediaBrowserService();
+
+        final int pageSize = 5;
+        final int page = 2;
+        Bundle options = new Bundle();
+        options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
+        options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
+
+        synchronized (mSubscriptionCallback.mWaitLock) {
+            mMediaBrowser.subscribe(MEDIA_ID_INVALID, options, mSubscriptionCallback);
+            mSubscriptionCallback.mWaitLock.wait(TIME_OUT_MS);
+            assertEquals(MEDIA_ID_INVALID, mSubscriptionCallback.mLastErrorId);
+            assertNotNull(mSubscriptionCallback.mLastOptions);
+            assertEquals(page,
+                    mSubscriptionCallback.mLastOptions.getInt(MediaBrowserCompat.EXTRA_PAGE));
+            assertEquals(pageSize,
+                    mSubscriptionCallback.mLastOptions.getInt(MediaBrowserCompat.EXTRA_PAGE_SIZE));
+        }
+    }
+
+    @Test
+    @MediumTest
+    public void testUnsubscribeForMultipleSubscriptions() throws Exception {
+        connectMediaBrowserService();
+        final List<StubSubscriptionCallback> subscriptionCallbacks = new ArrayList<>();
+        final int pageSize = 1;
+
+        // Subscribe four pages, one item per page.
+        for (int page = 0; page < 4; page++) {
+            final StubSubscriptionCallback callback = new StubSubscriptionCallback();
+            subscriptionCallbacks.add(callback);
+
+            Bundle options = new Bundle();
+            options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
+            options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
+            mMediaBrowser.subscribe(MEDIA_ID_ROOT, options, callback);
+            synchronized (callback.mWaitLock) {
+                callback.mWaitLock.wait(TIME_OUT_MS);
+            }
+            // Each onChildrenLoaded() must be called.
+            assertEquals(1, callback.mChildrenLoadedWithOptionCount);
+        }
+
+        // Reset callbacks and unsubscribe.
+        for (StubSubscriptionCallback callback : subscriptionCallbacks) {
+            callback.reset();
+        }
+        mMediaBrowser.unsubscribe(MEDIA_ID_ROOT);
+
+        // After unsubscribing, make StubMediaBrowserServiceCompat notify that the children are
+        // changed.
+        callMediaBrowserServiceMethod(NOTIFY_CHILDREN_CHANGED, MEDIA_ID_ROOT, getContext());
+        try {
+            Thread.sleep(SLEEP_MS);
+        } catch (InterruptedException e) {
+            fail("Unexpected InterruptedException occurred.");
+        }
+
+        // onChildrenLoaded should not be called.
+        for (StubSubscriptionCallback callback : subscriptionCallbacks) {
+            assertEquals(0, callback.mChildrenLoadedWithOptionCount);
+        }
+    }
+
+//    @Test
+//    @MediumTest
+    public void testUnsubscribeWithSubscriptionCallbackForMultipleSubscriptions() throws Exception {
+        connectMediaBrowserService();
+        final List<StubSubscriptionCallback> subscriptionCallbacks = new ArrayList<>();
+        final int pageSize = 1;
+
+        // Subscribe four pages, one item per page.
+        for (int page = 0; page < 4; page++) {
+            final StubSubscriptionCallback callback = new StubSubscriptionCallback();
+            subscriptionCallbacks.add(callback);
+
+            Bundle options = new Bundle();
+            options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
+            options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
+            mMediaBrowser.subscribe(MEDIA_ID_ROOT, options, callback);
+            synchronized (callback.mWaitLock) {
+                callback.mWaitLock.wait(TIME_OUT_MS);
+            }
+            // Each onChildrenLoaded() must be called.
+            assertEquals(1, callback.mChildrenLoadedWithOptionCount);
+        }
+
+        // Unsubscribe existing subscriptions one-by-one.
+        final int[] orderOfRemovingCallbacks = {2, 0, 3, 1};
+        for (int i = 0; i < orderOfRemovingCallbacks.length; i++) {
+            // Reset callbacks
+            for (StubSubscriptionCallback callback : subscriptionCallbacks) {
+                callback.reset();
+            }
+
+            // Remove one subscription
+            mMediaBrowser.unsubscribe(MEDIA_ID_ROOT,
+                    subscriptionCallbacks.get(orderOfRemovingCallbacks[i]));
+
+            // Make StubMediaBrowserServiceCompat notify that the children are changed.
+            callMediaBrowserServiceMethod(NOTIFY_CHILDREN_CHANGED, MEDIA_ID_ROOT, getContext());
+            try {
+                Thread.sleep(SLEEP_MS);
+            } catch (InterruptedException e) {
+                fail("Unexpected InterruptedException occurred.");
+            }
+
+            // Only the remaining subscriptionCallbacks should be called.
+            for (int j = 0; j < 4; j++) {
+                int childrenLoadedWithOptionsCount = subscriptionCallbacks
+                        .get(orderOfRemovingCallbacks[j]).mChildrenLoadedWithOptionCount;
+                if (j <= i) {
+                    assertEquals(0, childrenLoadedWithOptionsCount);
+                } else {
+                    assertEquals(1, childrenLoadedWithOptionsCount);
+                }
+            }
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testGetItem() throws Exception {
+        connectMediaBrowserService();
+
+        synchronized (mItemCallback.mWaitLock) {
+            mMediaBrowser.getItem(MEDIA_ID_CHILDREN[0], mItemCallback);
+            mItemCallback.mWaitLock.wait(TIME_OUT_MS);
+            assertNotNull(mItemCallback.mLastMediaItem);
+            assertEquals(MEDIA_ID_CHILDREN[0], mItemCallback.mLastMediaItem.getMediaId());
+        }
+    }
+
+    @Test
+    @MediumTest
+    public void testGetItemDelayed() throws Exception {
+        connectMediaBrowserService();
+
+        synchronized (mItemCallback.mWaitLock) {
+            mMediaBrowser.getItem(MEDIA_ID_CHILDREN_DELAYED, mItemCallback);
+            mItemCallback.mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
+            assertNull(mItemCallback.mLastMediaItem);
+
+            mItemCallback.reset();
+            callMediaBrowserServiceMethod(SEND_DELAYED_ITEM_LOADED, new Bundle(), getContext());
+            mItemCallback.mWaitLock.wait(TIME_OUT_MS);
+            assertNotNull(mItemCallback.mLastMediaItem);
+            assertEquals(MEDIA_ID_CHILDREN_DELAYED, mItemCallback.mLastMediaItem.getMediaId());
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testGetItemWhenOnLoadItemIsNotImplemented() throws Exception {
+        connectMediaBrowserService();
+        synchronized (mItemCallback.mWaitLock) {
+            mMediaBrowser.getItem(MEDIA_ID_ON_LOAD_ITEM_NOT_IMPLEMENTED, mItemCallback);
+            mItemCallback.mWaitLock.wait(TIME_OUT_MS);
+            assertEquals(MEDIA_ID_ON_LOAD_ITEM_NOT_IMPLEMENTED, mItemCallback.mLastErrorId);
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testGetItemWhenMediaIdIsInvalid() throws Exception {
+        mItemCallback.mLastMediaItem = new MediaItem(new MediaDescriptionCompat.Builder()
+                .setMediaId("dummy_id").build(), MediaItem.FLAG_BROWSABLE);
+
+        connectMediaBrowserService();
+        synchronized (mItemCallback.mWaitLock) {
+            mMediaBrowser.getItem(MEDIA_ID_INVALID, mItemCallback);
+            mItemCallback.mWaitLock.wait(TIME_OUT_MS);
+            assertNull(mItemCallback.mLastMediaItem);
+            assertNull(mItemCallback.mLastErrorId);
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testSearch() throws Exception {
+        connectMediaBrowserService();
+
+        final String key = "test-key";
+        final String val = "test-val";
+
+        synchronized (mSearchCallback.mWaitLock) {
+            mSearchCallback.reset();
+            mMediaBrowser.search(SEARCH_QUERY_FOR_NO_RESULT, null, mSearchCallback);
+            mSearchCallback.mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
+            assertTrue(mSearchCallback.mOnSearchResult);
+            assertTrue(mSearchCallback.mSearchResults != null
+                    && mSearchCallback.mSearchResults.size() == 0);
+            assertEquals(null, mSearchCallback.mSearchExtras);
+
+            mSearchCallback.reset();
+            mMediaBrowser.search(SEARCH_QUERY_FOR_ERROR, null, mSearchCallback);
+            mSearchCallback.mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
+            assertTrue(mSearchCallback.mOnSearchResult);
+            assertNull(mSearchCallback.mSearchResults);
+            assertEquals(null, mSearchCallback.mSearchExtras);
+
+            mSearchCallback.reset();
+            Bundle extras = new Bundle();
+            extras.putString(key, val);
+            mMediaBrowser.search(SEARCH_QUERY, extras, mSearchCallback);
+            mSearchCallback.mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
+            assertTrue(mSearchCallback.mOnSearchResult);
+            assertNotNull(mSearchCallback.mSearchResults);
+            for (MediaItem item : mSearchCallback.mSearchResults) {
+                assertNotNull(item.getMediaId());
+                assertTrue(item.getMediaId().contains(SEARCH_QUERY));
+            }
+            assertNotNull(mSearchCallback.mSearchExtras);
+            assertEquals(val, mSearchCallback.mSearchExtras.getString(key));
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testSendCustomAction() throws Exception {
+        connectMediaBrowserService();
+
+        synchronized (mCustomActionCallback.mWaitLock) {
+            Bundle customActionExtras = new Bundle();
+            customActionExtras.putString(TEST_KEY_1, TEST_VALUE_1);
+            mMediaBrowser.sendCustomAction(
+                    CUSTOM_ACTION, customActionExtras, mCustomActionCallback);
+
+            mCustomActionCallback.reset();
+            Bundle data1 = new Bundle();
+            data1.putString(TEST_KEY_2, TEST_VALUE_2);
+            callMediaBrowserServiceMethod(CUSTOM_ACTION_SEND_PROGRESS_UPDATE, data1, getContext());
+            mCustomActionCallback.mWaitLock.wait(TIME_OUT_MS);
+
+            assertTrue(mCustomActionCallback.mOnProgressUpdateCalled);
+            assertEquals(CUSTOM_ACTION, mCustomActionCallback.mAction);
+            assertNotNull(mCustomActionCallback.mExtras);
+            assertEquals(TEST_VALUE_1, mCustomActionCallback.mExtras.getString(TEST_KEY_1));
+            assertNotNull(mCustomActionCallback.mData);
+            assertEquals(TEST_VALUE_2, mCustomActionCallback.mData.getString(TEST_KEY_2));
+
+            mCustomActionCallback.reset();
+            Bundle data2 = new Bundle();
+            data2.putString(TEST_KEY_3, TEST_VALUE_3);
+            callMediaBrowserServiceMethod(CUSTOM_ACTION_SEND_PROGRESS_UPDATE, data2, getContext());
+            mCustomActionCallback.mWaitLock.wait(TIME_OUT_MS);
+
+            assertTrue(mCustomActionCallback.mOnProgressUpdateCalled);
+            assertEquals(CUSTOM_ACTION, mCustomActionCallback.mAction);
+            assertNotNull(mCustomActionCallback.mExtras);
+            assertEquals(TEST_VALUE_1, mCustomActionCallback.mExtras.getString(TEST_KEY_1));
+            assertNotNull(mCustomActionCallback.mData);
+            assertEquals(TEST_VALUE_3, mCustomActionCallback.mData.getString(TEST_KEY_3));
+
+            Bundle resultData = new Bundle();
+            resultData.putString(TEST_KEY_4, TEST_VALUE_4);
+            mCustomActionCallback.reset();
+            callMediaBrowserServiceMethod(CUSTOM_ACTION_SEND_RESULT, resultData, getContext());
+            mCustomActionCallback.mWaitLock.wait(TIME_OUT_MS);
+
+            assertTrue(mCustomActionCallback.mOnResultCalled);
+            assertEquals(CUSTOM_ACTION, mCustomActionCallback.mAction);
+            assertNotNull(mCustomActionCallback.mExtras);
+            assertEquals(TEST_VALUE_1, mCustomActionCallback.mExtras.getString(TEST_KEY_1));
+            assertNotNull(mCustomActionCallback.mData);
+            assertEquals(TEST_VALUE_4, mCustomActionCallback.mData.getString(TEST_KEY_4));
+        }
+    }
+
+
+    @Test
+    @MediumTest
+    public void testSendCustomActionWithDetachedError() throws Exception {
+        connectMediaBrowserService();
+
+        synchronized (mCustomActionCallback.mWaitLock) {
+            Bundle customActionExtras = new Bundle();
+            customActionExtras.putString(TEST_KEY_1, TEST_VALUE_1);
+            mMediaBrowser.sendCustomAction(
+                    CUSTOM_ACTION, customActionExtras, mCustomActionCallback);
+            mCustomActionCallback.mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
+
+            mCustomActionCallback.reset();
+            Bundle progressUpdateData = new Bundle();
+            progressUpdateData.putString(TEST_KEY_2, TEST_VALUE_2);
+            callMediaBrowserServiceMethod(
+                    CUSTOM_ACTION_SEND_PROGRESS_UPDATE, progressUpdateData, getContext());
+            mCustomActionCallback.mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCustomActionCallback.mOnProgressUpdateCalled);
+            assertEquals(CUSTOM_ACTION, mCustomActionCallback.mAction);
+            assertNotNull(mCustomActionCallback.mExtras);
+            assertEquals(TEST_VALUE_1, mCustomActionCallback.mExtras.getString(TEST_KEY_1));
+            assertNotNull(mCustomActionCallback.mData);
+            assertEquals(TEST_VALUE_2, mCustomActionCallback.mData.getString(TEST_KEY_2));
+
+            mCustomActionCallback.reset();
+            Bundle errorData = new Bundle();
+            errorData.putString(TEST_KEY_3, TEST_VALUE_3);
+            callMediaBrowserServiceMethod(CUSTOM_ACTION_SEND_ERROR, errorData, getContext());
+            mCustomActionCallback.mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCustomActionCallback.mOnErrorCalled);
+            assertEquals(CUSTOM_ACTION, mCustomActionCallback.mAction);
+            assertNotNull(mCustomActionCallback.mExtras);
+            assertEquals(TEST_VALUE_1, mCustomActionCallback.mExtras.getString(TEST_KEY_1));
+            assertNotNull(mCustomActionCallback.mData);
+            assertEquals(TEST_VALUE_3, mCustomActionCallback.mData.getString(TEST_KEY_3));
+        }
+    }
+
+    @Test
+    @MediumTest
+    public void testSendCustomActionWithNullCallback() throws Exception {
+        connectMediaBrowserService();
+
+        Bundle customActionExtras = new Bundle();
+        customActionExtras.putString(TEST_KEY_1, TEST_VALUE_1);
+        mMediaBrowser.sendCustomAction(CUSTOM_ACTION, customActionExtras, null);
+
+        // These calls should not make any exceptions.
+        callMediaBrowserServiceMethod(CUSTOM_ACTION_SEND_PROGRESS_UPDATE, new Bundle(),
+                getContext());
+        callMediaBrowserServiceMethod(CUSTOM_ACTION_SEND_RESULT, new Bundle(), getContext());
+        Thread.sleep(WAIT_TIME_FOR_NO_RESPONSE_MS);
+    }
+
+    @Test
+    @SmallTest
+    public void testSendCustomActionWithError() throws Exception {
+        connectMediaBrowserService();
+
+        synchronized (mCustomActionCallback.mWaitLock) {
+            mMediaBrowser.sendCustomAction(CUSTOM_ACTION_FOR_ERROR, null, mCustomActionCallback);
+            mCustomActionCallback.mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCustomActionCallback.mOnErrorCalled);
+        }
+    }
+
+    @Test
+    @MediumTest
+    public void testDelayedSetSessionToken() throws Exception {
+        // This test has no meaning in API 21. The framework MediaBrowserService just connects to
+        // the media browser without waiting setMediaSession() to be called.
+        if (Build.VERSION.SDK_INT == 21) {
+            return;
+        }
+        final ConnectionCallbackForDelayedMediaSession callback =
+                new ConnectionCallbackForDelayedMediaSession();
+
+        getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mMediaBrowser = new MediaBrowserCompat(
+                        getInstrumentation().getTargetContext(),
+                        TEST_BROWSER_SERVICE_DELAYED_MEDIA_SESSION,
+                        callback,
+                        null);
+            }
+        });
+
+        synchronized (callback.mWaitLock) {
+            mMediaBrowser.connect();
+            callback.mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
+            assertEquals(0, callback.mConnectedCount);
+
+            callMediaBrowserServiceMethod(SET_SESSION_TOKEN, new Bundle(), getContext());
+            callback.mWaitLock.wait(TIME_OUT_MS);
+            assertEquals(1, callback.mConnectedCount);
+
+            if (Build.VERSION.SDK_INT >= 21) {
+                assertNotNull(mMediaBrowser.getSessionToken().getExtraBinder());
+            }
+        }
+    }
+
+    private void connectMediaBrowserService() throws Exception {
+        synchronized (mConnectionCallback.mWaitLock) {
+            mMediaBrowser.connect();
+            mConnectionCallback.mWaitLock.wait(TIME_OUT_MS);
+            if (!mMediaBrowser.isConnected()) {
+                fail("Browser failed to connect!");
+            }
+        }
+    }
+
+    private void resetCallbacks() {
+        mConnectionCallback.reset();
+        mSubscriptionCallback.reset();
+        mItemCallback.reset();
+    }
+
+    private class StubConnectionCallback extends MediaBrowserCompat.ConnectionCallback {
+        final Object mWaitLock = new Object();
+        volatile int mConnectedCount;
+        volatile int mConnectionFailedCount;
+        volatile int mConnectionSuspendedCount;
+
+        public void reset() {
+            mConnectedCount = 0;
+            mConnectionFailedCount = 0;
+            mConnectionSuspendedCount = 0;
+        }
+
+        @Override
+        public void onConnected() {
+            synchronized (mWaitLock) {
+                mConnectedCount++;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onConnectionFailed() {
+            synchronized (mWaitLock) {
+                mConnectionFailedCount++;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onConnectionSuspended() {
+            synchronized (mWaitLock) {
+                mConnectionSuspendedCount++;
+                mWaitLock.notify();
+            }
+        }
+    }
+
+    private class StubSubscriptionCallback extends MediaBrowserCompat.SubscriptionCallback {
+        final Object mWaitLock = new Object();
+        private volatile int mChildrenLoadedCount;
+        private volatile int mChildrenLoadedWithOptionCount;
+        private volatile String mLastErrorId;
+        private volatile String mLastParentId;
+        private volatile Bundle mLastOptions;
+        private volatile List<MediaItem> mLastChildMediaItems;
+
+        public void reset() {
+            mChildrenLoadedCount = 0;
+            mChildrenLoadedWithOptionCount = 0;
+            mLastErrorId = null;
+            mLastParentId = null;
+            mLastOptions = null;
+            mLastChildMediaItems = null;
+        }
+
+        @Override
+        public void onChildrenLoaded(@NonNull String parentId, @NonNull List<MediaItem> children) {
+            synchronized (mWaitLock) {
+                mChildrenLoadedCount++;
+                mLastParentId = parentId;
+                mLastChildMediaItems = children;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onChildrenLoaded(@NonNull String parentId, @NonNull List<MediaItem> children,
+                @NonNull Bundle options) {
+            synchronized (mWaitLock) {
+                mChildrenLoadedWithOptionCount++;
+                mLastParentId = parentId;
+                mLastOptions = options;
+                mLastChildMediaItems = children;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onError(@NonNull String id) {
+            synchronized (mWaitLock) {
+                mLastErrorId = id;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onError(@NonNull String id, @NonNull Bundle options) {
+            synchronized (mWaitLock) {
+                mLastErrorId = id;
+                mLastOptions = options;
+                mWaitLock.notify();
+            }
+        }
+    }
+
+    private class StubItemCallback extends MediaBrowserCompat.ItemCallback {
+        final Object mWaitLock = new Object();
+        private volatile MediaItem mLastMediaItem;
+        private volatile String mLastErrorId;
+
+        public void reset() {
+            mLastMediaItem = null;
+            mLastErrorId = null;
+        }
+
+        @Override
+        public void onItemLoaded(MediaItem item) {
+            synchronized (mWaitLock) {
+                mLastMediaItem = item;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onError(@NonNull String id) {
+            synchronized (mWaitLock) {
+                mLastErrorId = id;
+                mWaitLock.notify();
+            }
+        }
+    }
+
+    private class StubSearchCallback extends MediaBrowserCompat.SearchCallback {
+        final Object mWaitLock = new Object();
+        boolean mOnSearchResult;
+        Bundle mSearchExtras;
+        List<MediaItem> mSearchResults;
+
+        @Override
+        public void onSearchResult(@NonNull String query, Bundle extras,
+                @NonNull List<MediaItem> items) {
+            synchronized (mWaitLock) {
+                mOnSearchResult = true;
+                mSearchResults = items;
+                mSearchExtras = extras;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onError(@NonNull String query, Bundle extras) {
+            synchronized (mWaitLock) {
+                mOnSearchResult = true;
+                mSearchResults = null;
+                mSearchExtras = extras;
+                mWaitLock.notify();
+            }
+        }
+
+        public void reset() {
+            mOnSearchResult = false;
+            mSearchExtras = null;
+            mSearchResults = null;
+        }
+    }
+
+    private class CustomActionCallback extends MediaBrowserCompat.CustomActionCallback {
+        final Object mWaitLock = new Object();
+        String mAction;
+        Bundle mExtras;
+        Bundle mData;
+        boolean mOnProgressUpdateCalled;
+        boolean mOnResultCalled;
+        boolean mOnErrorCalled;
+
+        @Override
+        public void onProgressUpdate(String action, Bundle extras, Bundle data) {
+            synchronized (mWaitLock) {
+                mOnProgressUpdateCalled = true;
+                mAction = action;
+                mExtras = extras;
+                mData = data;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onResult(String action, Bundle extras, Bundle resultData) {
+            synchronized (mWaitLock) {
+                mOnResultCalled = true;
+                mAction = action;
+                mExtras = extras;
+                mData = resultData;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onError(String action, Bundle extras, Bundle data) {
+            synchronized (mWaitLock) {
+                mOnErrorCalled = true;
+                mAction = action;
+                mExtras = extras;
+                mData = data;
+                mWaitLock.notify();
+            }
+        }
+
+        public void reset() {
+            mOnResultCalled = false;
+            mOnProgressUpdateCalled = false;
+            mOnErrorCalled = false;
+            mAction = null;
+            mExtras = null;
+            mData = null;
+        }
+    }
+
+    private class ConnectionCallbackForDelayedMediaSession extends
+            MediaBrowserCompat.ConnectionCallback {
+        final Object mWaitLock = new Object();
+        private int mConnectedCount = 0;
+
+        @Override
+        public void onConnected() {
+            synchronized (mWaitLock) {
+                mConnectedCount++;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onConnectionFailed() {
+            synchronized (mWaitLock) {
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onConnectionSuspended() {
+            synchronized (mWaitLock) {
+                mWaitLock.notify();
+            }
+        }
+    }
+}
diff --git a/media-compat/version-compat-tests/current/client/tests/src/android/support/mediacompat/client/MediaControllerCompatCallbackTest.java b/media-compat/version-compat-tests/current/client/tests/src/android/support/mediacompat/client/MediaControllerCompatCallbackTest.java
new file mode 100644
index 0000000..79993ef
--- /dev/null
+++ b/media-compat/version-compat-tests/current/client/tests/src/android/support/mediacompat/client/MediaControllerCompatCallbackTest.java
@@ -0,0 +1,718 @@
+/*
+ * Copyright 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.mediacompat.client;
+
+import static android.media.AudioManager.STREAM_MUSIC;
+import static android.support.mediacompat.testlib.MediaSessionConstants.RELEASE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SEND_SESSION_EVENT;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_CAPTIONING_ENABLED;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_EXTRAS;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_FLAGS;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_METADATA;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_PLAYBACK_STATE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_PLAYBACK_TO_LOCAL;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_PLAYBACK_TO_REMOTE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_QUEUE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_QUEUE_TITLE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_RATING_TYPE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_REPEAT_MODE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_SESSION_ACTIVITY;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_SHUFFLE_MODE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_ACTION;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_CURRENT_VOLUME;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_ERROR_CODE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_ERROR_MSG;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_FLAGS;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_KEY;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_MAX_VOLUME;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_MEDIA_ID_1;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_MEDIA_ID_2;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_QUEUE_ID_1;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_QUEUE_ID_2;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_SESSION_EVENT;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_VALUE;
+import static android.support.mediacompat.testlib.VersionConstants.KEY_SERVICE_VERSION;
+import static android.support.mediacompat.testlib.util.IntentUtil.callMediaSessionMethod;
+import static android.support.mediacompat.testlib.util.TestUtil.assertBundleEquals;
+import static android.support.test.InstrumentationRegistry.getArguments;
+import static android.support.test.InstrumentationRegistry.getContext;
+import static android.support.test.InstrumentationRegistry.getInstrumentation;
+import static android.support.test.InstrumentationRegistry.getTargetContext;
+import static android.support.v4.media.MediaMetadataCompat.METADATA_KEY_RATING;
+
+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.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.media.AudioManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.mediacompat.testlib.util.PollingCheck;
+import android.support.test.filters.LargeTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.media.MediaBrowserCompat;
+import android.support.v4.media.MediaDescriptionCompat;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.RatingCompat;
+import android.support.v4.media.VolumeProviderCompat;
+import android.support.v4.media.session.MediaControllerCompat;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.MediaSessionCompat.QueueItem;
+import android.support.v4.media.session.ParcelableVolumeInfo;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.util.Log;
+
+import junit.framework.Assert;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Test {@link MediaControllerCompat.Callback}.
+ */
+@RunWith(AndroidJUnit4.class)
+public class MediaControllerCompatCallbackTest {
+
+    private static final String TAG = "MediaControllerCompatCallbackTest";
+
+    // The maximum time to wait for an operation, that is expected to happen.
+    private static final long TIME_OUT_MS = 3000L;
+    private static final int MAX_AUDIO_INFO_CHANGED_CALLBACK_COUNT = 10;
+
+    private static final ComponentName TEST_BROWSER_SERVICE = new ComponentName(
+            "android.support.mediacompat.service.test",
+            "android.support.mediacompat.service.StubMediaBrowserServiceCompat");
+
+    private final Handler mHandler = new Handler(Looper.getMainLooper());
+    private final Object mWaitLock = new Object();
+
+    private String mServiceVersion;
+
+    // MediaBrowserCompat object to get the session token.
+    private MediaBrowserCompat mMediaBrowser;
+    private ConnectionCallback mConnectionCallback = new ConnectionCallback();
+
+    private MediaControllerCompat mController;
+    private MediaControllerCallback mMediaControllerCallback = new MediaControllerCallback();
+
+    @Before
+    public void setUp() throws Exception {
+        // The version of the service app is provided through the instrumentation arguments.
+        mServiceVersion = getArguments().getString(KEY_SERVICE_VERSION, "");
+        Log.d(TAG, "Service app version: " + mServiceVersion);
+
+        getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mMediaBrowser = new MediaBrowserCompat(getInstrumentation().getTargetContext(),
+                        TEST_BROWSER_SERVICE, mConnectionCallback, new Bundle());
+            }
+        });
+
+        synchronized (mConnectionCallback.mWaitLock) {
+            mMediaBrowser.connect();
+            mConnectionCallback.mWaitLock.wait(TIME_OUT_MS);
+            if (!mMediaBrowser.isConnected()) {
+                Assert.fail("Browser failed to connect!");
+            }
+        }
+        mController =
+                new MediaControllerCompat(getTargetContext(), mMediaBrowser.getSessionToken());
+        mController.registerCallback(mMediaControllerCallback, mHandler);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (mMediaBrowser != null && mMediaBrowser.isConnected()) {
+            mMediaBrowser.disconnect();
+        }
+    }
+
+    /**
+     * Tests {@link MediaSessionCompat#setExtras}.
+     */
+    @Test
+    @SmallTest
+    public void testSetExtras() throws Exception {
+        synchronized (mWaitLock) {
+            mMediaControllerCallback.resetLocked();
+
+            Bundle extras = new Bundle();
+            extras.putString(TEST_KEY, TEST_VALUE);
+            callMediaSessionMethod(SET_EXTRAS, extras, getContext());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mMediaControllerCallback.mOnExtraChangedCalled);
+
+            assertBundleEquals(extras, mMediaControllerCallback.mExtras);
+            assertBundleEquals(extras, mController.getExtras());
+        }
+    }
+
+    /**
+     * Tests {@link MediaSessionCompat#setFlags}.
+     */
+    @Test
+    @SmallTest
+    public void testSetFlags() throws Exception {
+        synchronized (mWaitLock) {
+            mMediaControllerCallback.resetLocked();
+
+            callMediaSessionMethod(SET_FLAGS, TEST_FLAGS, getContext());
+            new PollingCheck(TIME_OUT_MS) {
+                @Override
+                public boolean check() {
+                    return TEST_FLAGS == mController.getFlags();
+                }
+            }.run();
+        }
+    }
+
+    /**
+     * Tests {@link MediaSessionCompat#setMetadata}.
+     */
+    @Test
+    @SmallTest
+    public void testSetMetadata() throws Exception {
+        synchronized (mWaitLock) {
+            mMediaControllerCallback.resetLocked();
+            RatingCompat rating = RatingCompat.newHeartRating(true);
+            MediaMetadataCompat metadata = new MediaMetadataCompat.Builder()
+                    .putString(TEST_KEY, TEST_VALUE)
+                    .putRating(METADATA_KEY_RATING, rating)
+                    .build();
+
+            callMediaSessionMethod(SET_METADATA, metadata, getContext());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mMediaControllerCallback.mOnMetadataChangedCalled);
+
+            MediaMetadataCompat metadataOut = mMediaControllerCallback.mMediaMetadata;
+            assertNotNull(metadataOut);
+            assertEquals(TEST_VALUE, metadataOut.getString(TEST_KEY));
+
+            metadataOut = mController.getMetadata();
+            assertNotNull(metadataOut);
+            assertEquals(TEST_VALUE, metadataOut.getString(TEST_KEY));
+
+            assertNotNull(metadataOut.getRating(METADATA_KEY_RATING));
+            RatingCompat ratingOut = metadataOut.getRating(METADATA_KEY_RATING);
+            assertEquals(rating.getRatingStyle(), ratingOut.getRatingStyle());
+            assertEquals(rating.getPercentRating(), ratingOut.getPercentRating(), 0.0f);
+        }
+    }
+
+    /**
+     * Tests {@link MediaSessionCompat#setMetadata} with artwork bitmaps.
+     */
+    @Test
+    @SmallTest
+    public void testSetMetadataWithArtworks() throws Exception {
+        // TODO: Add test with a large bitmap.
+        // Using large bitmap makes other tests that are executed after this fail.
+        final Bitmap bitmapSmall = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
+
+        synchronized (mWaitLock) {
+            mMediaControllerCallback.resetLocked();
+            MediaMetadataCompat metadata = new MediaMetadataCompat.Builder()
+                    .putString(TEST_KEY, TEST_VALUE)
+                    .putBitmap(MediaMetadataCompat.METADATA_KEY_ART, bitmapSmall)
+                    .build();
+
+            callMediaSessionMethod(SET_METADATA, metadata, getContext());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mMediaControllerCallback.mOnMetadataChangedCalled);
+
+            MediaMetadataCompat metadataOut = mMediaControllerCallback.mMediaMetadata;
+            assertNotNull(metadataOut);
+            assertEquals(TEST_VALUE, metadataOut.getString(TEST_KEY));
+
+            Bitmap bitmapSmallOut = metadataOut.getBitmap(MediaMetadataCompat.METADATA_KEY_ART);
+            assertNotNull(bitmapSmallOut);
+            assertEquals(bitmapSmall.getHeight(), bitmapSmallOut.getHeight());
+            assertEquals(bitmapSmall.getWidth(), bitmapSmallOut.getWidth());
+            assertEquals(bitmapSmall.getConfig(), bitmapSmallOut.getConfig());
+
+            bitmapSmallOut.recycle();
+        }
+        bitmapSmall.recycle();
+    }
+
+    /**
+     * Tests {@link MediaSessionCompat#setPlaybackState}.
+     */
+    @Test
+    @SmallTest
+    public void testSetPlaybackState() throws Exception {
+        synchronized (mWaitLock) {
+            mMediaControllerCallback.resetLocked();
+            PlaybackStateCompat state =
+                    new PlaybackStateCompat.Builder()
+                            .setActions(TEST_ACTION)
+                            .setErrorMessage(TEST_ERROR_CODE, TEST_ERROR_MSG)
+                            .build();
+
+            callMediaSessionMethod(SET_PLAYBACK_STATE, state, getContext());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mMediaControllerCallback.mOnPlaybackStateChangedCalled);
+
+            PlaybackStateCompat stateOut = mMediaControllerCallback.mPlaybackState;
+            assertNotNull(stateOut);
+            assertEquals(TEST_ACTION, stateOut.getActions());
+            assertEquals(TEST_ERROR_CODE, stateOut.getErrorCode());
+            assertEquals(TEST_ERROR_MSG, stateOut.getErrorMessage().toString());
+
+            stateOut = mController.getPlaybackState();
+            assertNotNull(stateOut);
+            assertEquals(TEST_ACTION, stateOut.getActions());
+            assertEquals(TEST_ERROR_CODE, stateOut.getErrorCode());
+            assertEquals(TEST_ERROR_MSG, stateOut.getErrorMessage().toString());
+        }
+    }
+
+    /**
+     * Tests {@link MediaSessionCompat#setQueue} and {@link MediaSessionCompat#setQueueTitle}.
+     */
+    @Test
+    @SmallTest
+    public void testSetQueueAndSetQueueTitle() throws Exception {
+        synchronized (mWaitLock) {
+            mMediaControllerCallback.resetLocked();
+            List<QueueItem> queue = new ArrayList<>();
+
+            MediaDescriptionCompat description1 =
+                    new MediaDescriptionCompat.Builder().setMediaId(TEST_MEDIA_ID_1).build();
+            MediaDescriptionCompat description2 =
+                    new MediaDescriptionCompat.Builder().setMediaId(TEST_MEDIA_ID_2).build();
+            QueueItem item1 = new MediaSessionCompat.QueueItem(description1, TEST_QUEUE_ID_1);
+            QueueItem item2 = new MediaSessionCompat.QueueItem(description2, TEST_QUEUE_ID_2);
+            queue.add(item1);
+            queue.add(item2);
+
+            callMediaSessionMethod(SET_QUEUE, queue, getContext());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mMediaControllerCallback.mOnQueueChangedCalled);
+
+            callMediaSessionMethod(SET_QUEUE_TITLE, TEST_VALUE, getContext());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mMediaControllerCallback.mOnQueueTitleChangedCalled);
+
+            assertEquals(TEST_VALUE, mMediaControllerCallback.mTitle);
+            assertQueueEquals(queue, mMediaControllerCallback.mQueue);
+
+            assertEquals(TEST_VALUE, mController.getQueueTitle());
+            assertQueueEquals(queue, mController.getQueue());
+
+            mMediaControllerCallback.resetLocked();
+            callMediaSessionMethod(SET_QUEUE, null, getContext());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mMediaControllerCallback.mOnQueueChangedCalled);
+
+            callMediaSessionMethod(SET_QUEUE_TITLE, null, getContext());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mMediaControllerCallback.mOnQueueTitleChangedCalled);
+
+            assertNull(mMediaControllerCallback.mTitle);
+            assertNull(mMediaControllerCallback.mQueue);
+            assertNull(mController.getQueueTitle());
+            assertNull(mController.getQueue());
+        }
+    }
+
+    /**
+     * Tests {@link MediaSessionCompat#setSessionActivity}.
+     */
+    @Test
+    @SmallTest
+    public void testSessionActivity() throws Exception {
+        synchronized (mWaitLock) {
+            Intent intent = new Intent("MEDIA_SESSION_ACTION");
+            final int requestCode = 555;
+            final PendingIntent pi =
+                    PendingIntent.getActivity(getTargetContext(), requestCode, intent, 0);
+
+            callMediaSessionMethod(SET_SESSION_ACTIVITY, pi, getContext());
+            new PollingCheck(TIME_OUT_MS) {
+                @Override
+                public boolean check() {
+                    return pi.equals(mController.getSessionActivity());
+                }
+            }.run();
+        }
+    }
+
+    /**
+     * Tests {@link MediaSessionCompat#setCaptioningEnabled}.
+     */
+    @Test
+    @SmallTest
+    public void testSetCaptioningEnabled() throws Exception {
+        synchronized (mWaitLock) {
+            mMediaControllerCallback.resetLocked();
+            callMediaSessionMethod(SET_CAPTIONING_ENABLED, true, getContext());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mMediaControllerCallback.mOnCaptioningEnabledChangedCalled);
+            assertEquals(true, mMediaControllerCallback.mCaptioningEnabled);
+            assertEquals(true, mController.isCaptioningEnabled());
+
+            mMediaControllerCallback.resetLocked();
+            callMediaSessionMethod(SET_CAPTIONING_ENABLED, false, getContext());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mMediaControllerCallback.mOnCaptioningEnabledChangedCalled);
+            assertEquals(false, mMediaControllerCallback.mCaptioningEnabled);
+            assertEquals(false, mController.isCaptioningEnabled());
+        }
+    }
+
+    /**
+     * Tests {@link MediaSessionCompat#setRepeatMode}.
+     */
+    @Test
+    @SmallTest
+    public void testSetRepeatMode() throws Exception {
+        synchronized (mWaitLock) {
+            mMediaControllerCallback.resetLocked();
+            final int repeatMode = PlaybackStateCompat.REPEAT_MODE_ALL;
+            callMediaSessionMethod(SET_REPEAT_MODE, repeatMode, getContext());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mMediaControllerCallback.mOnRepeatModeChangedCalled);
+            assertEquals(repeatMode, mMediaControllerCallback.mRepeatMode);
+            assertEquals(repeatMode, mController.getRepeatMode());
+        }
+    }
+
+    /**
+     * Tests {@link MediaSessionCompat#setShuffleMode}.
+     */
+    @Test
+    @SmallTest
+    public void testSetShuffleMode() throws Exception {
+        final int shuffleMode = PlaybackStateCompat.SHUFFLE_MODE_ALL;
+        synchronized (mWaitLock) {
+            mMediaControllerCallback.resetLocked();
+            callMediaSessionMethod(SET_SHUFFLE_MODE, shuffleMode, getContext());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mMediaControllerCallback.mOnShuffleModeChangedCalled);
+            assertEquals(shuffleMode, mMediaControllerCallback.mShuffleMode);
+            assertEquals(shuffleMode, mController.getShuffleMode());
+        }
+    }
+
+    /**
+     * Tests {@link MediaSessionCompat#sendSessionEvent}.
+     */
+    @Test
+    @SmallTest
+    public void testSendSessionEvent() throws Exception {
+        synchronized (mWaitLock) {
+            mMediaControllerCallback.resetLocked();
+
+            Bundle arguments = new Bundle();
+            arguments.putString("event", TEST_SESSION_EVENT);
+
+            Bundle extras = new Bundle();
+            extras.putString(TEST_KEY, TEST_VALUE);
+            arguments.putBundle("extras", extras);
+            callMediaSessionMethod(SEND_SESSION_EVENT, arguments, getContext());
+
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mMediaControllerCallback.mOnSessionEventCalled);
+            assertEquals(TEST_SESSION_EVENT, mMediaControllerCallback.mEvent);
+            assertBundleEquals(extras, mMediaControllerCallback.mExtras);
+        }
+    }
+
+    /**
+     * Tests {@link MediaSessionCompat#release}.
+     */
+    @Test
+    @SmallTest
+    public void testRelease() throws Exception {
+        synchronized (mWaitLock) {
+            mMediaControllerCallback.resetLocked();
+            callMediaSessionMethod(RELEASE, null, getContext());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mMediaControllerCallback.mOnSessionDestroyedCalled);
+        }
+    }
+
+    /**
+     * Tests {@link MediaSessionCompat#setPlaybackToLocal} and
+     * {@link MediaSessionCompat#setPlaybackToRemote}.
+     */
+    @LargeTest
+    public void testPlaybackToLocalAndRemote() throws Exception {
+        synchronized (mWaitLock) {
+            mMediaControllerCallback.resetLocked();
+            ParcelableVolumeInfo volumeInfo = new ParcelableVolumeInfo(
+                    MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE,
+                    STREAM_MUSIC,
+                    VolumeProviderCompat.VOLUME_CONTROL_FIXED,
+                    TEST_MAX_VOLUME,
+                    TEST_CURRENT_VOLUME);
+
+            callMediaSessionMethod(SET_PLAYBACK_TO_REMOTE, volumeInfo, getContext());
+            MediaControllerCompat.PlaybackInfo info = null;
+            for (int i = 0; i < MAX_AUDIO_INFO_CHANGED_CALLBACK_COUNT; ++i) {
+                mMediaControllerCallback.mOnAudioInfoChangedCalled = false;
+                mWaitLock.wait(TIME_OUT_MS);
+                assertTrue(mMediaControllerCallback.mOnAudioInfoChangedCalled);
+                info = mMediaControllerCallback.mPlaybackInfo;
+                if (info != null && info.getCurrentVolume() == TEST_CURRENT_VOLUME
+                        && info.getMaxVolume() == TEST_MAX_VOLUME
+                        && info.getVolumeControl() == VolumeProviderCompat.VOLUME_CONTROL_FIXED
+                        && info.getPlaybackType()
+                                == MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE) {
+                    break;
+                }
+            }
+            assertNotNull(info);
+            assertEquals(MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE,
+                    info.getPlaybackType());
+            assertEquals(TEST_MAX_VOLUME, info.getMaxVolume());
+            assertEquals(TEST_CURRENT_VOLUME, info.getCurrentVolume());
+            assertEquals(VolumeProviderCompat.VOLUME_CONTROL_FIXED,
+                    info.getVolumeControl());
+
+            info = mController.getPlaybackInfo();
+            assertNotNull(info);
+            assertEquals(MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE,
+                    info.getPlaybackType());
+            assertEquals(TEST_MAX_VOLUME, info.getMaxVolume());
+            assertEquals(TEST_CURRENT_VOLUME, info.getCurrentVolume());
+            assertEquals(VolumeProviderCompat.VOLUME_CONTROL_FIXED, info.getVolumeControl());
+
+            // test setPlaybackToLocal
+            mMediaControllerCallback.mOnAudioInfoChangedCalled = false;
+            callMediaSessionMethod(SET_PLAYBACK_TO_LOCAL, AudioManager.STREAM_RING, getContext());
+
+            // In API 21 and 22, onAudioInfoChanged is not called.
+            if (Build.VERSION.SDK_INT == 21 || Build.VERSION.SDK_INT == 22) {
+                Thread.sleep(TIME_OUT_MS);
+            } else {
+                mWaitLock.wait(TIME_OUT_MS);
+                assertTrue(mMediaControllerCallback.mOnAudioInfoChangedCalled);
+            }
+
+            info = mController.getPlaybackInfo();
+            assertNotNull(info);
+            assertEquals(MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_LOCAL,
+                    info.getPlaybackType());
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testGetRatingType() {
+        assertEquals("Default rating type of a session must be RatingCompat.RATING_NONE",
+                RatingCompat.RATING_NONE, mController.getRatingType());
+
+        callMediaSessionMethod(SET_RATING_TYPE, RatingCompat.RATING_5_STARS, getContext());
+        new PollingCheck(TIME_OUT_MS) {
+            @Override
+            public boolean check() {
+                return RatingCompat.RATING_5_STARS == mController.getRatingType();
+            }
+        }.run();
+    }
+
+    private void assertQueueEquals(List<QueueItem> expected, List<QueueItem> observed) {
+        if (expected == null || observed == null) {
+            assertTrue(expected == observed);
+            return;
+        }
+
+        assertEquals(expected.size(), observed.size());
+        for (int i = 0; i < expected.size(); i++) {
+            QueueItem expectedItem = expected.get(i);
+            QueueItem observedItem = observed.get(i);
+
+            assertEquals(expectedItem.getQueueId(), observedItem.getQueueId());
+            assertEquals(expectedItem.getDescription().getMediaId(),
+                    observedItem.getDescription().getMediaId());
+        }
+    }
+
+    private class MediaControllerCallback extends MediaControllerCompat.Callback {
+        private volatile boolean mOnPlaybackStateChangedCalled;
+        private volatile boolean mOnMetadataChangedCalled;
+        private volatile boolean mOnQueueChangedCalled;
+        private volatile boolean mOnQueueTitleChangedCalled;
+        private volatile boolean mOnExtraChangedCalled;
+        private volatile boolean mOnAudioInfoChangedCalled;
+        private volatile boolean mOnSessionDestroyedCalled;
+        private volatile boolean mOnSessionEventCalled;
+        private volatile boolean mOnCaptioningEnabledChangedCalled;
+        private volatile boolean mOnRepeatModeChangedCalled;
+        private volatile boolean mOnShuffleModeChangedCalled;
+
+        private volatile PlaybackStateCompat mPlaybackState;
+        private volatile MediaMetadataCompat mMediaMetadata;
+        private volatile List<QueueItem> mQueue;
+        private volatile CharSequence mTitle;
+        private volatile String mEvent;
+        private volatile Bundle mExtras;
+        private volatile MediaControllerCompat.PlaybackInfo mPlaybackInfo;
+        private volatile boolean mCaptioningEnabled;
+        private volatile int mRepeatMode;
+        private volatile int mShuffleMode;
+
+        public void resetLocked() {
+            mOnPlaybackStateChangedCalled = false;
+            mOnMetadataChangedCalled = false;
+            mOnQueueChangedCalled = false;
+            mOnQueueTitleChangedCalled = false;
+            mOnExtraChangedCalled = false;
+            mOnAudioInfoChangedCalled = false;
+            mOnSessionDestroyedCalled = false;
+            mOnSessionEventCalled = false;
+            mOnRepeatModeChangedCalled = false;
+            mOnShuffleModeChangedCalled = false;
+
+            mPlaybackState = null;
+            mMediaMetadata = null;
+            mQueue = null;
+            mTitle = null;
+            mExtras = null;
+            mPlaybackInfo = null;
+            mCaptioningEnabled = false;
+            mRepeatMode = PlaybackStateCompat.REPEAT_MODE_NONE;
+            mShuffleMode = PlaybackStateCompat.SHUFFLE_MODE_NONE;
+        }
+
+        @Override
+        public void onPlaybackStateChanged(PlaybackStateCompat state) {
+            synchronized (mWaitLock) {
+                mOnPlaybackStateChangedCalled = true;
+                mPlaybackState = state;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onMetadataChanged(MediaMetadataCompat metadata) {
+            synchronized (mWaitLock) {
+                mOnMetadataChangedCalled = true;
+                mMediaMetadata = metadata;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onQueueChanged(List<QueueItem> queue) {
+            synchronized (mWaitLock) {
+                mOnQueueChangedCalled = true;
+                mQueue = queue;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onQueueTitleChanged(CharSequence title) {
+            synchronized (mWaitLock) {
+                mOnQueueTitleChangedCalled = true;
+                mTitle = title;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onExtrasChanged(Bundle extras) {
+            synchronized (mWaitLock) {
+                mOnExtraChangedCalled = true;
+                mExtras = extras;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onAudioInfoChanged(MediaControllerCompat.PlaybackInfo info) {
+            synchronized (mWaitLock) {
+                mOnAudioInfoChangedCalled = true;
+                mPlaybackInfo = info;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onSessionDestroyed() {
+            synchronized (mWaitLock) {
+                mOnSessionDestroyedCalled = true;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onSessionEvent(String event, Bundle extras) {
+            synchronized (mWaitLock) {
+                mOnSessionEventCalled = true;
+                mEvent = event;
+                mExtras = (Bundle) extras.clone();
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onCaptioningEnabledChanged(boolean enabled) {
+            synchronized (mWaitLock) {
+                mOnCaptioningEnabledChangedCalled = true;
+                mCaptioningEnabled = enabled;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onRepeatModeChanged(int repeatMode) {
+            synchronized (mWaitLock) {
+                mOnRepeatModeChangedCalled = true;
+                mRepeatMode = repeatMode;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onShuffleModeChanged(int shuffleMode) {
+            synchronized (mWaitLock) {
+                mOnShuffleModeChangedCalled = true;
+                mShuffleMode = shuffleMode;
+                mWaitLock.notify();
+            }
+        }
+    }
+
+    private class ConnectionCallback extends MediaBrowserCompat.ConnectionCallback {
+        final Object mWaitLock = new Object();
+
+        @Override
+        public void onConnected() {
+            synchronized (mWaitLock) {
+                mWaitLock.notify();
+            }
+        }
+    }
+}
diff --git a/media-compat-test-service/AndroidManifest.xml b/media-compat/version-compat-tests/current/service/AndroidManifest.xml
similarity index 92%
rename from media-compat-test-service/AndroidManifest.xml
rename to media-compat/version-compat-tests/current/service/AndroidManifest.xml
index 0390e8a..5e25a83 100644
--- a/media-compat-test-service/AndroidManifest.xml
+++ b/media-compat/version-compat-tests/current/service/AndroidManifest.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-   Copyright (C) 2017 The Android Open Source Project
+   Copyright 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.
diff --git a/media-compat-test-service/build.gradle b/media-compat/version-compat-tests/current/service/build.gradle
similarity index 80%
rename from media-compat-test-service/build.gradle
rename to media-compat/version-compat-tests/current/service/build.gradle
index 946d48b..2cfa5ce 100644
--- a/media-compat-test-service/build.gradle
+++ b/media-compat/version-compat-tests/current/service/build.gradle
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2017 The Android Open Source Project
+ * Copyright 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.
@@ -19,13 +19,10 @@
 }
 
 dependencies {
-    androidTestImplementation project(':support-annotations')
     androidTestImplementation project(':support-media-compat')
     androidTestImplementation project(':support-media-compat-test-lib')
 
-    androidTestImplementation(libs.test_runner) {
-        exclude module: 'support-annotations'
-    }
+    androidTestImplementation(libs.test_runner)
 }
 
 android {
diff --git a/media-compat-test-service/AndroidManifest.xml b/media-compat/version-compat-tests/current/service/lint-baseline.xml
similarity index 78%
copy from media-compat-test-service/AndroidManifest.xml
copy to media-compat/version-compat-tests/current/service/lint-baseline.xml
index 0390e8a..ed7ade1 100644
--- a/media-compat-test-service/AndroidManifest.xml
+++ b/media-compat/version-compat-tests/current/service/lint-baseline.xml
@@ -1,6 +1,6 @@
-<?xml version="1.0" encoding="utf-8"?>
+<?xml version="1.0" encoding="UTF-8"?>
 <!--
-   Copyright (C) 2017 The Android Open Source Project
+   Copyright 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.
@@ -14,4 +14,6 @@
    See the License for the specific language governing permissions and
    limitations under the License.
 -->
-<manifest package="android.support.mediacompat.service"/>
+<issues format="4" by="lint 3.0.0-alpha9">
+
+</issues>
diff --git a/media-compat-test-service/tests/AndroidManifest.xml b/media-compat/version-compat-tests/current/service/tests/AndroidManifest.xml
similarity index 91%
rename from media-compat-test-service/tests/AndroidManifest.xml
rename to media-compat/version-compat-tests/current/service/tests/AndroidManifest.xml
index 3e1eff9..b47eecf 100644
--- a/media-compat-test-service/tests/AndroidManifest.xml
+++ b/media-compat/version-compat-tests/current/service/tests/AndroidManifest.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-   Copyright (C) 2017 The Android Open Source Project
+   Copyright 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.
@@ -20,6 +20,7 @@
         <receiver android:name="android.support.mediacompat.service.ServiceBroadcastReceiver">
             <intent-filter>
                 <action android:name="android.support.mediacompat.service.action.CALL_MEDIA_BROWSER_SERVICE_METHOD"/>
+                <action android:name="android.support.mediacompat.service.action.CALL_MEDIA_SESSION_METHOD"/>
             </intent-filter>
         </receiver>
 
diff --git a/design/jvm-tests/NO_DOCS b/media-compat/version-compat-tests/current/service/tests/NO_DOCS
similarity index 92%
copy from design/jvm-tests/NO_DOCS
copy to media-compat/version-compat-tests/current/service/tests/NO_DOCS
index 092a39c..61c9b1a 100644
--- a/design/jvm-tests/NO_DOCS
+++ b/media-compat/version-compat-tests/current/service/tests/NO_DOCS
@@ -1,4 +1,4 @@
-# Copyright (C) 2016 The Android Open Source Project
+# Copyright 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.
diff --git a/media-compat/version-compat-tests/current/service/tests/src/android/support/mediacompat/service/MediaSessionCompatCallbackTest.java b/media-compat/version-compat-tests/current/service/tests/src/android/support/mediacompat/service/MediaSessionCompatCallbackTest.java
new file mode 100644
index 0000000..d36eba3
--- /dev/null
+++ b/media-compat/version-compat-tests/current/service/tests/src/android/support/mediacompat/service/MediaSessionCompatCallbackTest.java
@@ -0,0 +1,720 @@
+/*
+ * Copyright 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.mediacompat.service;
+
+import static android.support.mediacompat.testlib.MediaControllerConstants.ADD_QUEUE_ITEM;
+import static android.support.mediacompat.testlib.MediaControllerConstants
+        .ADD_QUEUE_ITEM_WITH_INDEX;
+import static android.support.mediacompat.testlib.MediaControllerConstants.FAST_FORWARD;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PAUSE;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PLAY;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PLAY_FROM_MEDIA_ID;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PLAY_FROM_SEARCH;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PLAY_FROM_URI;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PREPARE;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PREPARE_FROM_MEDIA_ID;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PREPARE_FROM_SEARCH;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PREPARE_FROM_URI;
+import static android.support.mediacompat.testlib.MediaControllerConstants.REMOVE_QUEUE_ITEM;
+import static android.support.mediacompat.testlib.MediaControllerConstants.REWIND;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SEEK_TO;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SEND_COMMAND;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SEND_CUSTOM_ACTION;
+import static android.support.mediacompat.testlib.MediaControllerConstants
+        .SEND_CUSTOM_ACTION_PARCELABLE;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SET_CAPTIONING_ENABLED;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SET_RATING;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SET_REPEAT_MODE;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SET_SHUFFLE_MODE;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SKIP_TO_NEXT;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SKIP_TO_PREVIOUS;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SKIP_TO_QUEUE_ITEM;
+import static android.support.mediacompat.testlib.MediaControllerConstants.STOP;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_COMMAND;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_KEY;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_SESSION_TAG;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_VALUE;
+import static android.support.mediacompat.testlib.VersionConstants.KEY_CLIENT_VERSION;
+import static android.support.mediacompat.testlib.util.IntentUtil.callMediaControllerMethod;
+import static android.support.mediacompat.testlib.util.IntentUtil.callTransportControlsMethod;
+import static android.support.mediacompat.testlib.util.TestUtil.assertBundleEquals;
+import static android.support.test.InstrumentationRegistry.getArguments;
+import static android.support.test.InstrumentationRegistry.getContext;
+import static android.support.test.InstrumentationRegistry.getInstrumentation;
+import static android.support.test.InstrumentationRegistry.getTargetContext;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.ResultReceiver;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.media.MediaDescriptionCompat;
+import android.support.v4.media.RatingCompat;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.util.Log;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Test {@link MediaSessionCompat.Callback}.
+ */
+@RunWith(AndroidJUnit4.class)
+public class MediaSessionCompatCallbackTest {
+
+    private static final String TAG = "MediaSessionCompatCallbackTest";
+
+    // The maximum time to wait for an operation.
+    private static final long TIME_OUT_MS = 3000L;
+    private static final float DELTA = 1e-4f;
+    private static final boolean ENABLED = true;
+
+    private final Object mWaitLock = new Object();
+    private final Handler mHandler = new Handler(Looper.getMainLooper());
+    private String mClientVersion;
+    private MediaSessionCompat mSession;
+    private MediaSessionCallback mCallback = new MediaSessionCallback();
+
+    @Before
+    public void setUp() throws Exception {
+        // The version of the client app is provided through the instrumentation arguments.
+        mClientVersion = getArguments().getString(KEY_CLIENT_VERSION, "");
+        Log.d(TAG, "Client app version: " + mClientVersion);
+
+        getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mSession = new MediaSessionCompat(getTargetContext(), TEST_SESSION_TAG);
+                mSession.setCallback(mCallback, mHandler);
+                mSession.setFlags(MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS);
+            }
+        });
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mSession.release();
+    }
+
+    @Test
+    @SmallTest
+    public void testSendCommand() throws Exception {
+        synchronized (mWaitLock) {
+            mCallback.reset();
+
+            Bundle arguments = new Bundle();
+            arguments.putString("command", TEST_COMMAND);
+            Bundle extras = new Bundle();
+            extras.putString(TEST_KEY, TEST_VALUE);
+            arguments.putBundle("extras", extras);
+            callMediaControllerMethod(
+                    SEND_COMMAND, arguments, getContext(), mSession.getSessionToken());
+
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnCommandCalled);
+            assertNotNull(mCallback.mCommandCallback);
+            assertEquals(TEST_COMMAND, mCallback.mCommand);
+            assertBundleEquals(extras, mCallback.mExtras);
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testAddRemoveQueueItems() throws Exception {
+        final String mediaId1 = "media_id_1";
+        final String mediaTitle1 = "media_title_1";
+        MediaDescriptionCompat itemDescription1 = new MediaDescriptionCompat.Builder()
+                .setMediaId(mediaId1).setTitle(mediaTitle1).build();
+
+        final String mediaId2 = "media_id_2";
+        final String mediaTitle2 = "media_title_2";
+        MediaDescriptionCompat itemDescription2 = new MediaDescriptionCompat.Builder()
+                .setMediaId(mediaId2).setTitle(mediaTitle2).build();
+
+        synchronized (mWaitLock) {
+            mCallback.reset();
+            callMediaControllerMethod(
+                    ADD_QUEUE_ITEM, itemDescription1, getContext(), mSession.getSessionToken());
+
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnAddQueueItemCalled);
+            assertEquals(-1, mCallback.mQueueIndex);
+            assertEquals(mediaId1, mCallback.mQueueDescription.getMediaId());
+            assertEquals(mediaTitle1, mCallback.mQueueDescription.getTitle());
+
+            mCallback.reset();
+            Bundle arguments = new Bundle();
+            arguments.putParcelable("description", itemDescription2);
+            arguments.putInt("index", 0);
+            callMediaControllerMethod(
+                    ADD_QUEUE_ITEM_WITH_INDEX, arguments, getContext(), mSession.getSessionToken());
+
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnAddQueueItemAtCalled);
+            assertEquals(0, mCallback.mQueueIndex);
+            assertEquals(mediaId2, mCallback.mQueueDescription.getMediaId());
+            assertEquals(mediaTitle2, mCallback.mQueueDescription.getTitle());
+
+            mCallback.reset();
+            callMediaControllerMethod(
+                    REMOVE_QUEUE_ITEM, itemDescription1, getContext(), mSession.getSessionToken());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnRemoveQueueItemCalled);
+            assertEquals(mediaId1, mCallback.mQueueDescription.getMediaId());
+            assertEquals(mediaTitle1, mCallback.mQueueDescription.getTitle());
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testTransportControlsAndMediaSessionCallback() throws Exception {
+        synchronized (mWaitLock) {
+            mCallback.reset();
+            callTransportControlsMethod(PLAY, null, getContext(), mSession.getSessionToken());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnPlayCalled);
+
+            mCallback.reset();
+            callTransportControlsMethod(PAUSE, null, getContext(), mSession.getSessionToken());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnPauseCalled);
+
+            mCallback.reset();
+            callTransportControlsMethod(STOP, null, getContext(), mSession.getSessionToken());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnStopCalled);
+
+            mCallback.reset();
+            callTransportControlsMethod(
+                    FAST_FORWARD, null, getContext(), mSession.getSessionToken());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnFastForwardCalled);
+
+            mCallback.reset();
+            callTransportControlsMethod(REWIND, null, getContext(), mSession.getSessionToken());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnRewindCalled);
+
+            mCallback.reset();
+            callTransportControlsMethod(
+                    SKIP_TO_PREVIOUS, null, getContext(), mSession.getSessionToken());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnSkipToPreviousCalled);
+
+            mCallback.reset();
+            callTransportControlsMethod(
+                    SKIP_TO_NEXT, null, getContext(), mSession.getSessionToken());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnSkipToNextCalled);
+
+            mCallback.reset();
+            final long seekPosition = 1000;
+            callTransportControlsMethod(
+                    SEEK_TO, seekPosition, getContext(), mSession.getSessionToken());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnSeekToCalled);
+            assertEquals(seekPosition, mCallback.mSeekPosition);
+
+            mCallback.reset();
+            final RatingCompat rating =
+                    RatingCompat.newStarRating(RatingCompat.RATING_5_STARS, 3f);
+            callTransportControlsMethod(
+                    SET_RATING, rating, getContext(), mSession.getSessionToken());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnSetRatingCalled);
+            assertEquals(rating.getRatingStyle(), mCallback.mRating.getRatingStyle());
+            assertEquals(rating.getStarRating(), mCallback.mRating.getStarRating(), DELTA);
+
+            mCallback.reset();
+            final String mediaId = "test-media-id";
+            final Bundle extras = new Bundle();
+            extras.putString(TEST_KEY, TEST_VALUE);
+            Bundle arguments = new Bundle();
+            arguments.putString("mediaId", mediaId);
+            arguments.putBundle("extras", extras);
+            callTransportControlsMethod(
+                    PLAY_FROM_MEDIA_ID, arguments, getContext(), mSession.getSessionToken());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnPlayFromMediaIdCalled);
+            assertEquals(mediaId, mCallback.mMediaId);
+            assertBundleEquals(extras, mCallback.mExtras);
+
+            mCallback.reset();
+            final String query = "test-query";
+            arguments = new Bundle();
+            arguments.putString("query", query);
+            arguments.putBundle("extras", extras);
+            callTransportControlsMethod(
+                    PLAY_FROM_SEARCH, arguments, getContext(), mSession.getSessionToken());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnPlayFromSearchCalled);
+            assertEquals(query, mCallback.mQuery);
+            assertBundleEquals(extras, mCallback.mExtras);
+
+            mCallback.reset();
+            final Uri uri = Uri.parse("content://test/popcorn.mod");
+            arguments = new Bundle();
+            arguments.putParcelable("uri", uri);
+            arguments.putBundle("extras", extras);
+            callTransportControlsMethod(
+                    PLAY_FROM_URI, arguments, getContext(), mSession.getSessionToken());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnPlayFromUriCalled);
+            assertEquals(uri, mCallback.mUri);
+            assertBundleEquals(extras, mCallback.mExtras);
+
+            mCallback.reset();
+            final String action = "test-action";
+            arguments = new Bundle();
+            arguments.putString("action", action);
+            arguments.putBundle("extras", extras);
+            callTransportControlsMethod(
+                    SEND_CUSTOM_ACTION, arguments, getContext(), mSession.getSessionToken());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnCustomActionCalled);
+            assertEquals(action, mCallback.mAction);
+            assertBundleEquals(extras, mCallback.mExtras);
+
+            mCallback.reset();
+            mCallback.mOnCustomActionCalled = false;
+            final PlaybackStateCompat.CustomAction customAction =
+                    new PlaybackStateCompat.CustomAction.Builder(action, action, -1)
+                            .setExtras(extras)
+                            .build();
+            arguments = new Bundle();
+            arguments.putParcelable("action", customAction);
+            arguments.putBundle("extras", extras);
+            callTransportControlsMethod(
+                    SEND_CUSTOM_ACTION_PARCELABLE,
+                    arguments,
+                    getContext(),
+                    mSession.getSessionToken());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnCustomActionCalled);
+            assertEquals(action, mCallback.mAction);
+            assertBundleEquals(extras, mCallback.mExtras);
+
+            mCallback.reset();
+            final long queueItemId = 1000;
+            callTransportControlsMethod(
+                    SKIP_TO_QUEUE_ITEM, queueItemId, getContext(), mSession.getSessionToken());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnSkipToQueueItemCalled);
+            assertEquals(queueItemId, mCallback.mQueueItemId);
+
+            mCallback.reset();
+            callTransportControlsMethod(
+                    PREPARE, null, getContext(), mSession.getSessionToken());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnPrepareCalled);
+
+            mCallback.reset();
+            arguments = new Bundle();
+            arguments.putString("mediaId", mediaId);
+            arguments.putBundle("extras", extras);
+            callTransportControlsMethod(
+                    PREPARE_FROM_MEDIA_ID, arguments, getContext(), mSession.getSessionToken());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnPrepareFromMediaIdCalled);
+            assertEquals(mediaId, mCallback.mMediaId);
+            assertBundleEquals(extras, mCallback.mExtras);
+
+            mCallback.reset();
+            arguments = new Bundle();
+            arguments.putString("query", query);
+            arguments.putBundle("extras", extras);
+            callTransportControlsMethod(
+                    PREPARE_FROM_SEARCH, arguments, getContext(), mSession.getSessionToken());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnPrepareFromSearchCalled);
+            assertEquals(query, mCallback.mQuery);
+            assertBundleEquals(extras, mCallback.mExtras);
+
+            mCallback.reset();
+            arguments = new Bundle();
+            arguments.putParcelable("uri", uri);
+            arguments.putBundle("extras", extras);
+            callTransportControlsMethod(
+                    PREPARE_FROM_URI, arguments, getContext(), mSession.getSessionToken());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnPrepareFromUriCalled);
+            assertEquals(uri, mCallback.mUri);
+            assertBundleEquals(extras, mCallback.mExtras);
+
+            mCallback.reset();
+            callTransportControlsMethod(
+                    SET_CAPTIONING_ENABLED, ENABLED, getContext(), mSession.getSessionToken());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnSetCaptioningEnabledCalled);
+            assertEquals(ENABLED, mCallback.mCaptioningEnabled);
+
+            mCallback.reset();
+            final int repeatMode = PlaybackStateCompat.REPEAT_MODE_ALL;
+            callTransportControlsMethod(
+                    SET_REPEAT_MODE, repeatMode, getContext(), mSession.getSessionToken());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnSetRepeatModeCalled);
+            assertEquals(repeatMode, mCallback.mRepeatMode);
+
+            mCallback.reset();
+            final int shuffleMode = PlaybackStateCompat.SHUFFLE_MODE_ALL;
+            callTransportControlsMethod(
+                    SET_SHUFFLE_MODE, shuffleMode, getContext(), mSession.getSessionToken());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnSetShuffleModeCalled);
+            assertEquals(shuffleMode, mCallback.mShuffleMode);
+        }
+    }
+
+    private class MediaSessionCallback extends MediaSessionCompat.Callback {
+        private long mSeekPosition;
+        private long mQueueItemId;
+        private RatingCompat mRating;
+        private String mMediaId;
+        private String mQuery;
+        private Uri mUri;
+        private String mAction;
+        private String mCommand;
+        private Bundle mExtras;
+        private ResultReceiver mCommandCallback;
+        private boolean mCaptioningEnabled;
+        private int mRepeatMode;
+        private int mShuffleMode;
+        private int mQueueIndex;
+        private MediaDescriptionCompat mQueueDescription;
+        private List<MediaSessionCompat.QueueItem> mQueue = new ArrayList<>();
+
+        private boolean mOnPlayCalled;
+        private boolean mOnPauseCalled;
+        private boolean mOnStopCalled;
+        private boolean mOnFastForwardCalled;
+        private boolean mOnRewindCalled;
+        private boolean mOnSkipToPreviousCalled;
+        private boolean mOnSkipToNextCalled;
+        private boolean mOnSeekToCalled;
+        private boolean mOnSkipToQueueItemCalled;
+        private boolean mOnSetRatingCalled;
+        private boolean mOnPlayFromMediaIdCalled;
+        private boolean mOnPlayFromSearchCalled;
+        private boolean mOnPlayFromUriCalled;
+        private boolean mOnCustomActionCalled;
+        private boolean mOnCommandCalled;
+        private boolean mOnPrepareCalled;
+        private boolean mOnPrepareFromMediaIdCalled;
+        private boolean mOnPrepareFromSearchCalled;
+        private boolean mOnPrepareFromUriCalled;
+        private boolean mOnSetCaptioningEnabledCalled;
+        private boolean mOnSetRepeatModeCalled;
+        private boolean mOnSetShuffleModeCalled;
+        private boolean mOnAddQueueItemCalled;
+        private boolean mOnAddQueueItemAtCalled;
+        private boolean mOnRemoveQueueItemCalled;
+
+        public void reset() {
+            mSeekPosition = -1;
+            mQueueItemId = -1;
+            mRating = null;
+            mMediaId = null;
+            mQuery = null;
+            mUri = null;
+            mAction = null;
+            mExtras = null;
+            mCommand = null;
+            mCommandCallback = null;
+            mCaptioningEnabled = false;
+            mRepeatMode = PlaybackStateCompat.REPEAT_MODE_NONE;
+            mShuffleMode = PlaybackStateCompat.SHUFFLE_MODE_NONE;
+            mQueueIndex = -1;
+            mQueueDescription = null;
+
+            mOnPlayCalled = false;
+            mOnPauseCalled = false;
+            mOnStopCalled = false;
+            mOnFastForwardCalled = false;
+            mOnRewindCalled = false;
+            mOnSkipToPreviousCalled = false;
+            mOnSkipToNextCalled = false;
+            mOnSkipToQueueItemCalled = false;
+            mOnSeekToCalled = false;
+            mOnSetRatingCalled = false;
+            mOnPlayFromMediaIdCalled = false;
+            mOnPlayFromSearchCalled = false;
+            mOnPlayFromUriCalled = false;
+            mOnCustomActionCalled = false;
+            mOnCommandCalled = false;
+            mOnPrepareCalled = false;
+            mOnPrepareFromMediaIdCalled = false;
+            mOnPrepareFromSearchCalled = false;
+            mOnPrepareFromUriCalled = false;
+            mOnSetCaptioningEnabledCalled = false;
+            mOnSetRepeatModeCalled = false;
+            mOnSetShuffleModeCalled = false;
+            mOnAddQueueItemCalled = false;
+            mOnAddQueueItemAtCalled = false;
+            mOnRemoveQueueItemCalled = false;
+        }
+
+        @Override
+        public void onPlay() {
+            synchronized (mWaitLock) {
+                mOnPlayCalled = true;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onPause() {
+            synchronized (mWaitLock) {
+                mOnPauseCalled = true;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onStop() {
+            synchronized (mWaitLock) {
+                mOnStopCalled = true;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onFastForward() {
+            synchronized (mWaitLock) {
+                mOnFastForwardCalled = true;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onRewind() {
+            synchronized (mWaitLock) {
+                mOnRewindCalled = true;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onSkipToPrevious() {
+            synchronized (mWaitLock) {
+                mOnSkipToPreviousCalled = true;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onSkipToNext() {
+            synchronized (mWaitLock) {
+                mOnSkipToNextCalled = true;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onSeekTo(long pos) {
+            synchronized (mWaitLock) {
+                mOnSeekToCalled = true;
+                mSeekPosition = pos;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onSetRating(RatingCompat rating) {
+            synchronized (mWaitLock) {
+                mOnSetRatingCalled = true;
+                mRating = rating;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onPlayFromMediaId(String mediaId, Bundle extras) {
+            synchronized (mWaitLock) {
+                mOnPlayFromMediaIdCalled = true;
+                mMediaId = mediaId;
+                mExtras = extras;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onPlayFromSearch(String query, Bundle extras) {
+            synchronized (mWaitLock) {
+                mOnPlayFromSearchCalled = true;
+                mQuery = query;
+                mExtras = extras;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onPlayFromUri(Uri uri, Bundle extras) {
+            synchronized (mWaitLock) {
+                mOnPlayFromUriCalled = true;
+                mUri = uri;
+                mExtras = extras;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onCustomAction(String action, Bundle extras) {
+            synchronized (mWaitLock) {
+                mOnCustomActionCalled = true;
+                mAction = action;
+                mExtras = extras;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onSkipToQueueItem(long id) {
+            synchronized (mWaitLock) {
+                mOnSkipToQueueItemCalled = true;
+                mQueueItemId = id;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onCommand(String command, Bundle extras, ResultReceiver cb) {
+            synchronized (mWaitLock) {
+                mOnCommandCalled = true;
+                mCommand = command;
+                mExtras = extras;
+                mCommandCallback = cb;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onPrepare() {
+            synchronized (mWaitLock) {
+                mOnPrepareCalled = true;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onPrepareFromMediaId(String mediaId, Bundle extras) {
+            synchronized (mWaitLock) {
+                mOnPrepareFromMediaIdCalled = true;
+                mMediaId = mediaId;
+                mExtras = extras;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onPrepareFromSearch(String query, Bundle extras) {
+            synchronized (mWaitLock) {
+                mOnPrepareFromSearchCalled = true;
+                mQuery = query;
+                mExtras = extras;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onPrepareFromUri(Uri uri, Bundle extras) {
+            synchronized (mWaitLock) {
+                mOnPrepareFromUriCalled = true;
+                mUri = uri;
+                mExtras = extras;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onSetRepeatMode(int repeatMode) {
+            synchronized (mWaitLock) {
+                mOnSetRepeatModeCalled = true;
+                mRepeatMode = repeatMode;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onAddQueueItem(MediaDescriptionCompat description) {
+            synchronized (mWaitLock) {
+                mOnAddQueueItemCalled = true;
+                mQueueDescription = description;
+                mQueue.add(new MediaSessionCompat.QueueItem(description, mQueue.size()));
+                mSession.setQueue(mQueue);
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onAddQueueItem(MediaDescriptionCompat description, int index) {
+            synchronized (mWaitLock) {
+                mOnAddQueueItemAtCalled = true;
+                mQueueIndex = index;
+                mQueueDescription = description;
+                mQueue.add(index, new MediaSessionCompat.QueueItem(description, mQueue.size()));
+                mSession.setQueue(mQueue);
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onRemoveQueueItem(MediaDescriptionCompat description) {
+            synchronized (mWaitLock) {
+                mOnRemoveQueueItemCalled = true;
+                String mediaId = description.getMediaId();
+                for (int i = mQueue.size() - 1; i >= 0; --i) {
+                    if (mediaId.equals(mQueue.get(i).getDescription().getMediaId())) {
+                        mQueueDescription = mQueue.remove(i).getDescription();
+                        mSession.setQueue(mQueue);
+                        break;
+                    }
+                }
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onSetCaptioningEnabled(boolean enabled) {
+            synchronized (mWaitLock) {
+                mOnSetCaptioningEnabledCalled = true;
+                mCaptioningEnabled = enabled;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onSetShuffleMode(int shuffleMode) {
+            synchronized (mWaitLock) {
+                mOnSetShuffleModeCalled = true;
+                mShuffleMode = shuffleMode;
+                mWaitLock.notify();
+            }
+        }
+    }
+}
diff --git a/media-compat/version-compat-tests/current/service/tests/src/android/support/mediacompat/service/ServiceBroadcastReceiver.java b/media-compat/version-compat-tests/current/service/tests/src/android/support/mediacompat/service/ServiceBroadcastReceiver.java
new file mode 100644
index 0000000..57364b7
--- /dev/null
+++ b/media-compat/version-compat-tests/current/service/tests/src/android/support/mediacompat/service/ServiceBroadcastReceiver.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright 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.mediacompat.service;
+
+
+import static android.support.mediacompat.testlib.MediaBrowserConstants.CUSTOM_ACTION_SEND_ERROR;
+import static android.support.mediacompat.testlib.MediaBrowserConstants
+        .CUSTOM_ACTION_SEND_PROGRESS_UPDATE;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.CUSTOM_ACTION_SEND_RESULT;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.NOTIFY_CHILDREN_CHANGED;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.SEND_DELAYED_ITEM_LOADED;
+import static android.support.mediacompat.testlib.MediaBrowserConstants
+        .SEND_DELAYED_NOTIFY_CHILDREN_CHANGED;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.SET_SESSION_TOKEN;
+import static android.support.mediacompat.testlib.MediaSessionConstants.RELEASE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SEND_SESSION_EVENT;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_ACTIVE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_CAPTIONING_ENABLED;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_EXTRAS;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_FLAGS;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_METADATA;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_PLAYBACK_STATE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_PLAYBACK_TO_LOCAL;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_PLAYBACK_TO_REMOTE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_QUEUE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_QUEUE_TITLE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_RATING_TYPE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_REPEAT_MODE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_SESSION_ACTIVITY;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_SHUFFLE_MODE;
+import static android.support.mediacompat.testlib.util.IntentUtil
+        .ACTION_CALL_MEDIA_BROWSER_SERVICE_METHOD;
+import static android.support.mediacompat.testlib.util.IntentUtil.ACTION_CALL_MEDIA_SESSION_METHOD;
+import static android.support.mediacompat.testlib.util.IntentUtil.KEY_ARGUMENT;
+import static android.support.mediacompat.testlib.util.IntentUtil.KEY_METHOD_ID;
+
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.RatingCompat;
+import android.support.v4.media.VolumeProviderCompat;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.MediaSessionCompat.QueueItem;
+import android.support.v4.media.session.ParcelableVolumeInfo;
+import android.support.v4.media.session.PlaybackStateCompat;
+
+import java.util.List;
+
+public class ServiceBroadcastReceiver extends BroadcastReceiver {
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        Bundle extras = intent.getExtras();
+        if (ACTION_CALL_MEDIA_BROWSER_SERVICE_METHOD.equals(intent.getAction()) && extras != null) {
+            StubMediaBrowserServiceCompat service = StubMediaBrowserServiceCompat.sInstance;
+            int method = extras.getInt(KEY_METHOD_ID, 0);
+
+            switch (method) {
+                case NOTIFY_CHILDREN_CHANGED:
+                    service.notifyChildrenChanged(extras.getString(KEY_ARGUMENT));
+                    break;
+                case SEND_DELAYED_NOTIFY_CHILDREN_CHANGED:
+                    service.sendDelayedNotifyChildrenChanged();
+                    break;
+                case SEND_DELAYED_ITEM_LOADED:
+                    service.sendDelayedItemLoaded();
+                    break;
+                case CUSTOM_ACTION_SEND_PROGRESS_UPDATE:
+                    service.mCustomActionResult.sendProgressUpdate(extras.getBundle(KEY_ARGUMENT));
+                    break;
+                case CUSTOM_ACTION_SEND_ERROR:
+                    service.mCustomActionResult.sendError(extras.getBundle(KEY_ARGUMENT));
+                    break;
+                case CUSTOM_ACTION_SEND_RESULT:
+                    service.mCustomActionResult.sendResult(extras.getBundle(KEY_ARGUMENT));
+                    break;
+                case SET_SESSION_TOKEN:
+                    StubMediaBrowserServiceCompatWithDelayedMediaSession.sInstance
+                            .callSetSessionToken();
+                    break;
+            }
+        } else if (ACTION_CALL_MEDIA_SESSION_METHOD.equals(intent.getAction()) && extras != null) {
+            MediaSessionCompat session = StubMediaBrowserServiceCompat.sSession;
+            int method = extras.getInt(KEY_METHOD_ID, 0);
+
+            switch (method) {
+                case SET_EXTRAS:
+                    session.setExtras(extras.getBundle(KEY_ARGUMENT));
+                    break;
+                case SET_FLAGS:
+                    session.setFlags(extras.getInt(KEY_ARGUMENT));
+                    break;
+                case SET_METADATA:
+                    session.setMetadata((MediaMetadataCompat) extras.getParcelable(KEY_ARGUMENT));
+                    break;
+                case SET_PLAYBACK_STATE:
+                    session.setPlaybackState(
+                            (PlaybackStateCompat) extras.getParcelable(KEY_ARGUMENT));
+                    break;
+                case SET_QUEUE:
+                    List<QueueItem> items = extras.getParcelableArrayList(KEY_ARGUMENT);
+                    session.setQueue(items);
+                    break;
+                case SET_QUEUE_TITLE:
+                    session.setQueueTitle(extras.getCharSequence(KEY_ARGUMENT));
+                    break;
+                case SET_SESSION_ACTIVITY:
+                    session.setSessionActivity((PendingIntent) extras.getParcelable(KEY_ARGUMENT));
+                    break;
+                case SET_CAPTIONING_ENABLED:
+                    session.setCaptioningEnabled(extras.getBoolean(KEY_ARGUMENT));
+                    break;
+                case SET_REPEAT_MODE:
+                    session.setRepeatMode(extras.getInt(KEY_ARGUMENT));
+                    break;
+                case SET_SHUFFLE_MODE:
+                    session.setShuffleMode(extras.getInt(KEY_ARGUMENT));
+                    break;
+                case SEND_SESSION_EVENT:
+                    Bundle arguments = extras.getBundle(KEY_ARGUMENT);
+                    session.sendSessionEvent(
+                            arguments.getString("event"), arguments.getBundle("extras"));
+                    break;
+                case SET_ACTIVE:
+                    session.setActive(extras.getBoolean(KEY_ARGUMENT));
+                    break;
+                case RELEASE:
+                    session.release();
+                    break;
+                case SET_PLAYBACK_TO_LOCAL:
+                    session.setPlaybackToLocal(extras.getInt(KEY_ARGUMENT));
+                    break;
+                case SET_PLAYBACK_TO_REMOTE:
+                    ParcelableVolumeInfo volumeInfo = extras.getParcelable(KEY_ARGUMENT);
+                    session.setPlaybackToRemote(new VolumeProviderCompat(
+                            volumeInfo.controlType,
+                            volumeInfo.maxVolume,
+                            volumeInfo.currentVolume) {});
+                    break;
+                case SET_RATING_TYPE:
+                    session.setRatingType(RatingCompat.RATING_5_STARS);
+                    break;
+            }
+        }
+    }
+}
diff --git a/media-compat-test-service/tests/src/android/support/mediacompat/service/StubMediaBrowserServiceCompat.java b/media-compat/version-compat-tests/current/service/tests/src/android/support/mediacompat/service/StubMediaBrowserServiceCompat.java
similarity index 98%
rename from media-compat-test-service/tests/src/android/support/mediacompat/service/StubMediaBrowserServiceCompat.java
rename to media-compat/version-compat-tests/current/service/tests/src/android/support/mediacompat/service/StubMediaBrowserServiceCompat.java
index fa9f1c5..61c33f8 100644
--- a/media-compat-test-service/tests/src/android/support/mediacompat/service/StubMediaBrowserServiceCompat.java
+++ b/media-compat/version-compat-tests/current/service/tests/src/android/support/mediacompat/service/StubMediaBrowserServiceCompat.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2017 The Android Open Source Project
+ * Copyright 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.
diff --git a/media-compat-test-service/tests/src/android/support/mediacompat/service/StubMediaBrowserServiceCompatWithDelayedMediaSession.java b/media-compat/version-compat-tests/current/service/tests/src/android/support/mediacompat/service/StubMediaBrowserServiceCompatWithDelayedMediaSession.java
similarity index 97%
rename from media-compat-test-service/tests/src/android/support/mediacompat/service/StubMediaBrowserServiceCompatWithDelayedMediaSession.java
rename to media-compat/version-compat-tests/current/service/tests/src/android/support/mediacompat/service/StubMediaBrowserServiceCompatWithDelayedMediaSession.java
index 12cb358..509e13f 100644
--- a/media-compat-test-service/tests/src/android/support/mediacompat/service/StubMediaBrowserServiceCompatWithDelayedMediaSession.java
+++ b/media-compat/version-compat-tests/current/service/tests/src/android/support/mediacompat/service/StubMediaBrowserServiceCompatWithDelayedMediaSession.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2017 The Android Open Source Project
+ * Copyright 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.
diff --git a/media-compat-test-service/AndroidManifest.xml b/media-compat/version-compat-tests/lib/AndroidManifest.xml
similarity index 84%
copy from media-compat-test-service/AndroidManifest.xml
copy to media-compat/version-compat-tests/lib/AndroidManifest.xml
index 0390e8a..857e61c 100644
--- a/media-compat-test-service/AndroidManifest.xml
+++ b/media-compat/version-compat-tests/lib/AndroidManifest.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-   Copyright (C) 2017 The Android Open Source Project
+   Copyright 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.
@@ -14,4 +14,4 @@
    See the License for the specific language governing permissions and
    limitations under the License.
 -->
-<manifest package="android.support.mediacompat.service"/>
+<manifest package="android.support.mediacompat.testlib"/>
diff --git a/media-compat-test-lib/build.gradle b/media-compat/version-compat-tests/lib/build.gradle
similarity index 72%
rename from media-compat-test-lib/build.gradle
rename to media-compat/version-compat-tests/lib/build.gradle
index 26594e5..a9be453 100644
--- a/media-compat-test-lib/build.gradle
+++ b/media-compat/version-compat-tests/lib/build.gradle
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2017 The Android Open Source Project
+ * Copyright 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.
@@ -14,4 +14,16 @@
  * limitations under the License.
  */
 
-apply plugin: 'java'
+plugins {
+    id("SupportAndroidLibraryPlugin")
+}
+
+android {
+    defaultConfig {
+        minSdkVersion 14
+    }
+}
+
+supportLibrary {
+    legacySourceLocation = true
+}
diff --git a/media-compat-test-service/AndroidManifest.xml b/media-compat/version-compat-tests/lib/lint-baseline.xml
similarity index 78%
copy from media-compat-test-service/AndroidManifest.xml
copy to media-compat/version-compat-tests/lib/lint-baseline.xml
index 0390e8a..4dd17af 100644
--- a/media-compat-test-service/AndroidManifest.xml
+++ b/media-compat/version-compat-tests/lib/lint-baseline.xml
@@ -1,6 +1,6 @@
-<?xml version="1.0" encoding="utf-8"?>
+<?xml version="1.0" encoding="UTF-8"?>
 <!--
-   Copyright (C) 2017 The Android Open Source Project
+   Copyright 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.
@@ -14,4 +14,6 @@
    See the License for the specific language governing permissions and
    limitations under the License.
 -->
-<manifest package="android.support.mediacompat.service"/>
+<issues format="4" by="lint 3.0.0">
+
+</issues>
diff --git a/media-compat-test-lib/src/main/java/android/support/mediacompat/testlib/MediaBrowserConstants.java b/media-compat/version-compat-tests/lib/src/main/java/android/support/mediacompat/testlib/MediaBrowserConstants.java
similarity index 97%
rename from media-compat-test-lib/src/main/java/android/support/mediacompat/testlib/MediaBrowserConstants.java
rename to media-compat/version-compat-tests/lib/src/main/java/android/support/mediacompat/testlib/MediaBrowserConstants.java
index 8ef0a35..86024d9 100644
--- a/media-compat-test-lib/src/main/java/android/support/mediacompat/testlib/MediaBrowserConstants.java
+++ b/media-compat/version-compat-tests/lib/src/main/java/android/support/mediacompat/testlib/MediaBrowserConstants.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2017 The Android Open Source Project
+ * Copyright 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.
diff --git a/media-compat/version-compat-tests/lib/src/main/java/android/support/mediacompat/testlib/MediaControllerConstants.java b/media-compat/version-compat-tests/lib/src/main/java/android/support/mediacompat/testlib/MediaControllerConstants.java
new file mode 100644
index 0000000..5fa086b
--- /dev/null
+++ b/media-compat/version-compat-tests/lib/src/main/java/android/support/mediacompat/testlib/MediaControllerConstants.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 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.mediacompat.testlib;
+
+/**
+ * Constants for testing the media controller.
+ */
+public class MediaControllerConstants {
+
+    // MediaControllerCompat methods.
+    public static final int SEND_COMMAND = 201;
+    public static final int ADD_QUEUE_ITEM = 202;
+    public static final int ADD_QUEUE_ITEM_WITH_INDEX = 203;
+    public static final int REMOVE_QUEUE_ITEM = 204;
+
+    // TransportControls methods.
+    public static final int PLAY = 301;
+    public static final int PAUSE = 302;
+    public static final int STOP = 303;
+    public static final int FAST_FORWARD = 304;
+    public static final int REWIND = 305;
+    public static final int SKIP_TO_PREVIOUS = 306;
+    public static final int SKIP_TO_NEXT = 307;
+    public static final int SEEK_TO = 308;
+    public static final int SET_RATING = 309;
+    public static final int PLAY_FROM_MEDIA_ID = 310;
+    public static final int PLAY_FROM_SEARCH = 311;
+    public static final int PLAY_FROM_URI = 312;
+    public static final int SEND_CUSTOM_ACTION = 313;
+    public static final int SEND_CUSTOM_ACTION_PARCELABLE = 314;
+    public static final int SKIP_TO_QUEUE_ITEM = 315;
+    public static final int PREPARE = 316;
+    public static final int PREPARE_FROM_MEDIA_ID = 317;
+    public static final int PREPARE_FROM_SEARCH = 318;
+    public static final int PREPARE_FROM_URI = 319;
+    public static final int SET_CAPTIONING_ENABLED = 320;
+    public static final int SET_REPEAT_MODE = 321;
+    public static final int SET_SHUFFLE_MODE = 322;
+}
diff --git a/media-compat/version-compat-tests/lib/src/main/java/android/support/mediacompat/testlib/MediaSessionConstants.java b/media-compat/version-compat-tests/lib/src/main/java/android/support/mediacompat/testlib/MediaSessionConstants.java
new file mode 100644
index 0000000..cbdccc1
--- /dev/null
+++ b/media-compat/version-compat-tests/lib/src/main/java/android/support/mediacompat/testlib/MediaSessionConstants.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 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.mediacompat.testlib;
+
+/**
+ * Constants for testing the media session.
+ */
+public class MediaSessionConstants {
+
+    // MediaSessionCompat methods.
+    public static final int SET_EXTRAS = 101;
+    public static final int SET_FLAGS = 102;
+    public static final int SET_METADATA = 103;
+    public static final int SET_PLAYBACK_STATE = 104;
+    public static final int SET_QUEUE = 105;
+    public static final int SET_QUEUE_TITLE = 106;
+    public static final int SET_SESSION_ACTIVITY = 107;
+    public static final int SET_CAPTIONING_ENABLED = 108;
+    public static final int SET_REPEAT_MODE = 109;
+    public static final int SET_SHUFFLE_MODE = 110;
+    public static final int SEND_SESSION_EVENT = 112;
+    public static final int SET_ACTIVE = 113;
+    public static final int RELEASE = 114;
+    public static final int SET_PLAYBACK_TO_LOCAL = 115;
+    public static final int SET_PLAYBACK_TO_REMOTE = 116;
+    public static final int SET_RATING_TYPE = 117;
+
+    public static final String TEST_SESSION_TAG = "test-session-tag";
+    public static final String TEST_KEY = "test-key";
+    public static final String TEST_VALUE = "test-val";
+    public static final String TEST_SESSION_EVENT = "test-session-event";
+    public static final String TEST_COMMAND = "test-command";
+    public static final int TEST_FLAGS = 5;
+    public static final int TEST_CURRENT_VOLUME = 10;
+    public static final int TEST_MAX_VOLUME = 11;
+    public static final long TEST_QUEUE_ID_1 = 10L;
+    public static final long TEST_QUEUE_ID_2 = 20L;
+    public static final String TEST_MEDIA_ID_1 = "media_id_1";
+    public static final String TEST_MEDIA_ID_2 = "media_id_2";
+    public static final long TEST_ACTION = 55L;
+
+    public static final int TEST_ERROR_CODE = 0x3;
+    public static final String TEST_ERROR_MSG = "test-error-msg";
+}
diff --git a/media-compat-test-lib/build.gradle b/media-compat/version-compat-tests/lib/src/main/java/android/support/mediacompat/testlib/VersionConstants.java
similarity index 61%
copy from media-compat-test-lib/build.gradle
copy to media-compat/version-compat-tests/lib/src/main/java/android/support/mediacompat/testlib/VersionConstants.java
index 26594e5..6533ee1 100644
--- a/media-compat-test-lib/build.gradle
+++ b/media-compat/version-compat-tests/lib/src/main/java/android/support/mediacompat/testlib/VersionConstants.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2017 The Android Open Source Project
+ * Copyright 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.
@@ -14,4 +14,12 @@
  * limitations under the License.
  */
 
-apply plugin: 'java'
+package android.support.mediacompat.testlib;
+
+/**
+ * Constants for getting support library version information.
+ */
+public class VersionConstants {
+    public static final String KEY_CLIENT_VERSION = "client_version";
+    public static final String KEY_SERVICE_VERSION = "service_version";
+}
diff --git a/media-compat/version-compat-tests/lib/src/main/java/android/support/mediacompat/testlib/util/IntentUtil.java b/media-compat/version-compat-tests/lib/src/main/java/android/support/mediacompat/testlib/util/IntentUtil.java
new file mode 100644
index 0000000..bbf9752
--- /dev/null
+++ b/media-compat/version-compat-tests/lib/src/main/java/android/support/mediacompat/testlib/util/IntentUtil.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 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.mediacompat.testlib.util;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Parcelable;
+
+import java.util.ArrayList;
+
+/**
+ * Methods and constants used for sending intent between client and service apps.
+ */
+public class IntentUtil {
+
+    public static final ComponentName SERVICE_RECEIVER_COMPONENT_NAME = new ComponentName(
+            "android.support.mediacompat.service.test",
+            "android.support.mediacompat.service.ServiceBroadcastReceiver");
+    public static final ComponentName CLIENT_RECEIVER_COMPONENT_NAME = new ComponentName(
+            "android.support.mediacompat.client.test",
+            "android.support.mediacompat.client.ClientBroadcastReceiver");
+
+    public static final String ACTION_CALL_MEDIA_BROWSER_SERVICE_METHOD =
+            "android.support.mediacompat.service.action.CALL_MEDIA_BROWSER_SERVICE_METHOD";
+    public static final String ACTION_CALL_MEDIA_SESSION_METHOD =
+            "android.support.mediacompat.service.action.CALL_MEDIA_SESSION_METHOD";
+    public static final String ACTION_CALL_MEDIA_CONTROLLER_METHOD =
+            "android.support.mediacompat.client.action.CALL_MEDIA_CONTROLLER_METHOD";
+    public static final String ACTION_CALL_TRANSPORT_CONTROLS_METHOD =
+            "android.support.mediacompat.client.action.CALL_TRANSPORT_CONTROLS_METHOD";
+
+    public static final String KEY_METHOD_ID = "method_id";
+    public static final String KEY_ARGUMENT = "argument";
+    public static final String KEY_SESSION_TOKEN = "session_token";
+
+    /**
+     * Calls a method of MediaBrowserService. Used by client app.
+     */
+    public static void callMediaBrowserServiceMethod(int methodId, Object arg, Context context) {
+        Intent intent = createIntent(SERVICE_RECEIVER_COMPONENT_NAME, methodId, arg);
+        intent.setAction(ACTION_CALL_MEDIA_BROWSER_SERVICE_METHOD);
+        if (Build.VERSION.SDK_INT >= 16) {
+            intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+        }
+        context.sendBroadcast(intent);
+    }
+
+    /**
+     * Calls a method of MediaSession. Used by client app.
+     */
+    public static void callMediaSessionMethod(int methodId, Object arg, Context context) {
+        Intent intent = createIntent(SERVICE_RECEIVER_COMPONENT_NAME, methodId, arg);
+        intent.setAction(ACTION_CALL_MEDIA_SESSION_METHOD);
+        if (Build.VERSION.SDK_INT >= 16) {
+            intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+        }
+        context.sendBroadcast(intent);
+    }
+
+    /**
+     * Calls a method of MediaController. Used by service app.
+     */
+    public static void callMediaControllerMethod(
+            int methodId, Object arg, Context context, Parcelable token) {
+        Intent intent = createIntent(CLIENT_RECEIVER_COMPONENT_NAME, methodId, arg);
+        intent.setAction(ACTION_CALL_MEDIA_CONTROLLER_METHOD);
+        intent.putExtra(KEY_SESSION_TOKEN, token);
+        if (Build.VERSION.SDK_INT >= 16) {
+            intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+        }
+        context.sendBroadcast(intent);
+    }
+
+    /**
+     * Calls a method of TransportControls. Used by service app.
+     */
+    public static void callTransportControlsMethod(
+            int methodId, Object arg, Context context, Parcelable token) {
+        Intent intent = createIntent(CLIENT_RECEIVER_COMPONENT_NAME, methodId, arg);
+        intent.setAction(ACTION_CALL_TRANSPORT_CONTROLS_METHOD);
+        intent.putExtra(KEY_SESSION_TOKEN, token);
+        if (Build.VERSION.SDK_INT >= 16) {
+            intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+        }
+        context.sendBroadcast(intent);
+    }
+
+    private static Intent createIntent(ComponentName componentName, int methodId, Object arg) {
+        Intent intent = new Intent();
+        intent.setComponent(componentName);
+        intent.putExtra(KEY_METHOD_ID, methodId);
+
+        if (arg instanceof String) {
+            intent.putExtra(KEY_ARGUMENT, (String) arg);
+        } else if (arg instanceof Integer) {
+            intent.putExtra(KEY_ARGUMENT, (int) arg);
+        } else if (arg instanceof Long) {
+            intent.putExtra(KEY_ARGUMENT, (long) arg);
+        } else if (arg instanceof Boolean) {
+            intent.putExtra(KEY_ARGUMENT, (boolean) arg);
+        } else if (arg instanceof Parcelable) {
+            intent.putExtra(KEY_ARGUMENT, (Parcelable) arg);
+        } else if (arg instanceof ArrayList<?>) {
+            Bundle bundle = new Bundle();
+            bundle.putParcelableArrayList(KEY_ARGUMENT, (ArrayList<? extends Parcelable>) arg);
+            intent.putExtras(bundle);
+        } else if (arg instanceof Bundle) {
+            Bundle bundle = new Bundle();
+            bundle.putBundle(KEY_ARGUMENT, (Bundle) arg);
+            intent.putExtras(bundle);
+        }
+        return intent;
+    }
+}
diff --git a/media-compat/version-compat-tests/lib/src/main/java/android/support/mediacompat/testlib/util/PollingCheck.java b/media-compat/version-compat-tests/lib/src/main/java/android/support/mediacompat/testlib/util/PollingCheck.java
new file mode 100644
index 0000000..3412da0
--- /dev/null
+++ b/media-compat/version-compat-tests/lib/src/main/java/android/support/mediacompat/testlib/util/PollingCheck.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 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.mediacompat.testlib.util;
+
+import junit.framework.Assert;
+
+/**
+ * Utility used for testing that allows to poll for a certain condition to happen within a timeout.
+ * (Copied from testutils/src/main/java/android/support/testutils/PollingCheck.java.)
+ */
+public abstract class PollingCheck {
+    private static final long DEFAULT_TIMEOUT = 3000;
+    private static final long TIME_SLICE = 50;
+    private final long mTimeout;
+
+    /**
+     * The condition that the PollingCheck should use to proceed successfully.
+     */
+    public interface PollingCheckCondition {
+        /**
+         * @return Whether the polling condition has been met.
+         */
+        boolean canProceed();
+    }
+
+    public PollingCheck(long timeout) {
+        mTimeout = timeout;
+    }
+
+    protected abstract boolean check();
+
+    /**
+     * Start running the polling check.
+     */
+    public void run() {
+        if (check()) {
+            return;
+        }
+
+        long timeout = mTimeout;
+        while (timeout > 0) {
+            try {
+                Thread.sleep(TIME_SLICE);
+            } catch (InterruptedException e) {
+                Assert.fail("unexpected InterruptedException");
+            }
+
+            if (check()) {
+                return;
+            }
+
+            timeout -= TIME_SLICE;
+        }
+
+        Assert.fail("unexpected timeout");
+    }
+
+    /**
+     * Instantiate and start polling for a given condition with a default 3000ms timeout.
+     * @param condition The condition to check for success.
+     */
+    public static void waitFor(final PollingCheckCondition condition) {
+        new PollingCheck(DEFAULT_TIMEOUT) {
+            @Override
+            protected boolean check() {
+                return condition.canProceed();
+            }
+        }.run();
+    }
+
+    /**
+     * Instantiate and start polling for a given condition.
+     * @param timeout Time out in ms
+     * @param condition The condition to check for success.
+     */
+    public static void waitFor(long timeout, final PollingCheckCondition condition) {
+        new PollingCheck(timeout) {
+            @Override
+            protected boolean check() {
+                return condition.canProceed();
+            }
+        }.run();
+    }
+}
diff --git a/media-compat/version-compat-tests/lib/src/main/java/android/support/mediacompat/testlib/util/TestUtil.java b/media-compat/version-compat-tests/lib/src/main/java/android/support/mediacompat/testlib/util/TestUtil.java
new file mode 100644
index 0000000..d105510
--- /dev/null
+++ b/media-compat/version-compat-tests/lib/src/main/java/android/support/mediacompat/testlib/util/TestUtil.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 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.mediacompat.testlib.util;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertSame;
+
+import android.os.Bundle;
+
+/**
+ * Utility methods used for testing.
+ */
+public final class TestUtil {
+
+    /**
+     * Asserts that two Bundles are equal.
+     */
+    public static void assertBundleEquals(Bundle expected, Bundle observed) {
+        if (expected == null || observed == null) {
+            assertSame(expected, observed);
+        }
+        assertEquals(expected.size(), observed.size());
+        for (String key : expected.keySet()) {
+            assertEquals(expected.get(key), observed.get(key));
+        }
+    }
+}
diff --git a/media-compat-test-client/AndroidManifest.xml b/media-compat/version-compat-tests/previous/client/AndroidManifest.xml
similarity index 92%
copy from media-compat-test-client/AndroidManifest.xml
copy to media-compat/version-compat-tests/previous/client/AndroidManifest.xml
index 290b67e..9724d2b 100644
--- a/media-compat-test-client/AndroidManifest.xml
+++ b/media-compat/version-compat-tests/previous/client/AndroidManifest.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-   Copyright (C) 2017 The Android Open Source Project
+   Copyright 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.
diff --git a/media-compat-test-service/build.gradle b/media-compat/version-compat-tests/previous/client/build.gradle
similarity index 74%
copy from media-compat-test-service/build.gradle
copy to media-compat/version-compat-tests/previous/client/build.gradle
index 946d48b..01b3847 100644
--- a/media-compat-test-service/build.gradle
+++ b/media-compat/version-compat-tests/previous/client/build.gradle
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2017 The Android Open Source Project
+ * Copyright 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.
@@ -19,13 +19,10 @@
 }
 
 dependencies {
-    androidTestImplementation project(':support-annotations')
-    androidTestImplementation project(':support-media-compat')
     androidTestImplementation project(':support-media-compat-test-lib')
+    androidTestImplementation "com.android.support:support-media-compat:27.0.1"
 
-    androidTestImplementation(libs.test_runner) {
-        exclude module: 'support-annotations'
-    }
+    androidTestImplementation(libs.test_runner)
 }
 
 android {
@@ -36,4 +33,4 @@
 
 supportLibrary {
     legacySourceLocation = true
-}
+}
\ No newline at end of file
diff --git a/media-compat-test-service/AndroidManifest.xml b/media-compat/version-compat-tests/previous/client/lint-baseline.xml
similarity index 78%
copy from media-compat-test-service/AndroidManifest.xml
copy to media-compat/version-compat-tests/previous/client/lint-baseline.xml
index 0390e8a..ed7ade1 100644
--- a/media-compat-test-service/AndroidManifest.xml
+++ b/media-compat/version-compat-tests/previous/client/lint-baseline.xml
@@ -1,6 +1,6 @@
-<?xml version="1.0" encoding="utf-8"?>
+<?xml version="1.0" encoding="UTF-8"?>
 <!--
-   Copyright (C) 2017 The Android Open Source Project
+   Copyright 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.
@@ -14,4 +14,6 @@
    See the License for the specific language governing permissions and
    limitations under the License.
 -->
-<manifest package="android.support.mediacompat.service"/>
+<issues format="4" by="lint 3.0.0-alpha9">
+
+</issues>
diff --git a/media-compat/version-compat-tests/previous/client/tests/AndroidManifest.xml b/media-compat/version-compat-tests/previous/client/tests/AndroidManifest.xml
new file mode 100644
index 0000000..afe1865
--- /dev/null
+++ b/media-compat/version-compat-tests/previous/client/tests/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+   Copyright 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.mediacompat.client.test">
+    <application android:supportsRtl="true">
+        <receiver android:name="android.support.mediacompat.client.ClientBroadcastReceiver">
+            <intent-filter>
+                <action android:name="android.support.mediacompat.service.action.CALL_MEDIA_CONTROLLER_METHOD"/>
+                <action android:name="android.support.mediacompat.service.action.CALL_TRANSPORT_CONTROLS_METHOD"/>
+            </intent-filter>
+        </receiver>
+    </application>
+</manifest>
diff --git a/design/jvm-tests/NO_DOCS b/media-compat/version-compat-tests/previous/client/tests/NO_DOCS
similarity index 92%
copy from design/jvm-tests/NO_DOCS
copy to media-compat/version-compat-tests/previous/client/tests/NO_DOCS
index 092a39c..61c9b1a 100644
--- a/design/jvm-tests/NO_DOCS
+++ b/media-compat/version-compat-tests/previous/client/tests/NO_DOCS
@@ -1,4 +1,4 @@
-# Copyright (C) 2016 The Android Open Source Project
+# Copyright 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.
diff --git a/media-compat/version-compat-tests/previous/client/tests/src/android/support/mediacompat/client/ClientBroadcastReceiver.java b/media-compat/version-compat-tests/previous/client/tests/src/android/support/mediacompat/client/ClientBroadcastReceiver.java
new file mode 100644
index 0000000..3166e55
--- /dev/null
+++ b/media-compat/version-compat-tests/previous/client/tests/src/android/support/mediacompat/client/ClientBroadcastReceiver.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright 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.mediacompat.client;
+
+import static android.support.mediacompat.testlib.MediaControllerConstants.ADD_QUEUE_ITEM;
+import static android.support.mediacompat.testlib.MediaControllerConstants
+        .ADD_QUEUE_ITEM_WITH_INDEX;
+import static android.support.mediacompat.testlib.MediaControllerConstants.FAST_FORWARD;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PAUSE;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PLAY;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PLAY_FROM_MEDIA_ID;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PLAY_FROM_SEARCH;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PLAY_FROM_URI;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PREPARE;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PREPARE_FROM_MEDIA_ID;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PREPARE_FROM_SEARCH;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PREPARE_FROM_URI;
+import static android.support.mediacompat.testlib.MediaControllerConstants.REMOVE_QUEUE_ITEM;
+import static android.support.mediacompat.testlib.MediaControllerConstants.REWIND;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SEEK_TO;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SEND_COMMAND;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SEND_CUSTOM_ACTION;
+import static android.support.mediacompat.testlib.MediaControllerConstants
+        .SEND_CUSTOM_ACTION_PARCELABLE;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SET_CAPTIONING_ENABLED;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SET_RATING;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SET_REPEAT_MODE;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SET_SHUFFLE_MODE;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SKIP_TO_NEXT;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SKIP_TO_PREVIOUS;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SKIP_TO_QUEUE_ITEM;
+import static android.support.mediacompat.testlib.MediaControllerConstants.STOP;
+import static android.support.mediacompat.testlib.util.IntentUtil
+        .ACTION_CALL_MEDIA_CONTROLLER_METHOD;
+import static android.support.mediacompat.testlib.util.IntentUtil
+        .ACTION_CALL_TRANSPORT_CONTROLS_METHOD;
+import static android.support.mediacompat.testlib.util.IntentUtil.KEY_ARGUMENT;
+import static android.support.mediacompat.testlib.util.IntentUtil.KEY_METHOD_ID;
+import static android.support.mediacompat.testlib.util.IntentUtil.KEY_SESSION_TOKEN;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.support.v4.media.MediaDescriptionCompat;
+import android.support.v4.media.RatingCompat;
+import android.support.v4.media.session.MediaControllerCompat;
+import android.support.v4.media.session.MediaControllerCompat.TransportControls;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+
+public class ClientBroadcastReceiver extends BroadcastReceiver {
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        Bundle extras = intent.getExtras();
+        MediaControllerCompat controller;
+        try {
+            controller = new MediaControllerCompat(context,
+                    (MediaSessionCompat.Token) extras.getParcelable(KEY_SESSION_TOKEN));
+        } catch (RemoteException ex) {
+            // Do nothing.
+            return;
+        }
+        int method = extras.getInt(KEY_METHOD_ID, 0);
+
+        if (ACTION_CALL_MEDIA_CONTROLLER_METHOD.equals(intent.getAction()) && extras != null) {
+            Bundle arguments;
+            switch (method) {
+                case SEND_COMMAND:
+                    arguments = extras.getBundle(KEY_ARGUMENT);
+                    controller.sendCommand(
+                            arguments.getString("command"),
+                            arguments.getBundle("extras"),
+                            new ResultReceiver(null));
+                    break;
+                case ADD_QUEUE_ITEM:
+                    controller.addQueueItem(
+                            (MediaDescriptionCompat) extras.getParcelable(KEY_ARGUMENT));
+                    break;
+                case ADD_QUEUE_ITEM_WITH_INDEX:
+                    arguments = extras.getBundle(KEY_ARGUMENT);
+                    controller.addQueueItem(
+                            (MediaDescriptionCompat) arguments.getParcelable("description"),
+                            arguments.getInt("index"));
+                    break;
+                case REMOVE_QUEUE_ITEM:
+                    controller.removeQueueItem(
+                            (MediaDescriptionCompat) extras.getParcelable(KEY_ARGUMENT));
+                    break;
+            }
+        } else if (ACTION_CALL_TRANSPORT_CONTROLS_METHOD.equals(intent.getAction())
+                && extras != null) {
+            TransportControls controls = controller.getTransportControls();
+            Bundle arguments;
+            switch (method) {
+                case PLAY:
+                    controls.play();
+                    break;
+                case PAUSE:
+                    controls.pause();
+                    break;
+                case STOP:
+                    controls.stop();
+                    break;
+                case FAST_FORWARD:
+                    controls.fastForward();
+                    break;
+                case REWIND:
+                    controls.rewind();
+                    break;
+                case SKIP_TO_PREVIOUS:
+                    controls.skipToPrevious();
+                    break;
+                case SKIP_TO_NEXT:
+                    controls.skipToNext();
+                    break;
+                case SEEK_TO:
+                    controls.seekTo(extras.getLong(KEY_ARGUMENT));
+                    break;
+                case SET_RATING:
+                    controls.setRating((RatingCompat) extras.getParcelable(KEY_ARGUMENT));
+                    break;
+                case PLAY_FROM_MEDIA_ID:
+                    arguments = extras.getBundle(KEY_ARGUMENT);
+                    controls.playFromMediaId(
+                            arguments.getString("mediaId"),
+                            arguments.getBundle("extras"));
+                    break;
+                case PLAY_FROM_SEARCH:
+                    arguments = extras.getBundle(KEY_ARGUMENT);
+                    controls.playFromSearch(
+                            arguments.getString("query"),
+                            arguments.getBundle("extras"));
+                    break;
+                case PLAY_FROM_URI:
+                    arguments = extras.getBundle(KEY_ARGUMENT);
+                    controls.playFromUri(
+                            (Uri) arguments.getParcelable("uri"),
+                            arguments.getBundle("extras"));
+                    break;
+                case SEND_CUSTOM_ACTION:
+                    arguments = extras.getBundle(KEY_ARGUMENT);
+                    controls.sendCustomAction(
+                            arguments.getString("action"),
+                            arguments.getBundle("extras"));
+                    break;
+                case SEND_CUSTOM_ACTION_PARCELABLE:
+                    arguments = extras.getBundle(KEY_ARGUMENT);
+                    controls.sendCustomAction(
+                            (PlaybackStateCompat.CustomAction)
+                                    arguments.getParcelable("action"),
+                            arguments.getBundle("extras"));
+                    break;
+                case SKIP_TO_QUEUE_ITEM:
+                    controls.skipToQueueItem(extras.getLong(KEY_ARGUMENT));
+                    break;
+                case PREPARE:
+                    controls.prepare();
+                    break;
+                case PREPARE_FROM_MEDIA_ID:
+                    arguments = extras.getBundle(KEY_ARGUMENT);
+                    controls.prepareFromMediaId(
+                            arguments.getString("mediaId"),
+                            arguments.getBundle("extras"));
+                    break;
+                case PREPARE_FROM_SEARCH:
+                    arguments = extras.getBundle(KEY_ARGUMENT);
+                    controls.prepareFromSearch(
+                            arguments.getString("query"),
+                            arguments.getBundle("extras"));
+                    break;
+                case PREPARE_FROM_URI:
+                    arguments = extras.getBundle(KEY_ARGUMENT);
+                    controls.prepareFromUri(
+                            (Uri) arguments.getParcelable("uri"),
+                            arguments.getBundle("extras"));
+                    break;
+                case SET_CAPTIONING_ENABLED:
+                    controls.setCaptioningEnabled(extras.getBoolean(KEY_ARGUMENT));
+                    break;
+                case SET_REPEAT_MODE:
+                    controls.setRepeatMode(extras.getInt(KEY_ARGUMENT));
+                    break;
+                case SET_SHUFFLE_MODE:
+                    controls.setShuffleMode(extras.getInt(KEY_ARGUMENT));
+                    break;
+            }
+        }
+    }
+}
diff --git a/media-compat/version-compat-tests/previous/client/tests/src/android/support/mediacompat/client/MediaBrowserCompatTest.java b/media-compat/version-compat-tests/previous/client/tests/src/android/support/mediacompat/client/MediaBrowserCompatTest.java
new file mode 100644
index 0000000..31bdb7a
--- /dev/null
+++ b/media-compat/version-compat-tests/previous/client/tests/src/android/support/mediacompat/client/MediaBrowserCompatTest.java
@@ -0,0 +1,1012 @@
+/*
+ * Copyright 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.mediacompat.client;
+
+import static android.support.mediacompat.testlib.MediaBrowserConstants.CUSTOM_ACTION;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.CUSTOM_ACTION_FOR_ERROR;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.CUSTOM_ACTION_SEND_ERROR;
+import static android.support.mediacompat.testlib.MediaBrowserConstants
+        .CUSTOM_ACTION_SEND_PROGRESS_UPDATE;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.CUSTOM_ACTION_SEND_RESULT;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.EXTRAS_KEY;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.EXTRAS_VALUE;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.MEDIA_ID_CHILDREN;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.MEDIA_ID_CHILDREN_DELAYED;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.MEDIA_ID_INVALID;
+import static android.support.mediacompat.testlib.MediaBrowserConstants
+        .MEDIA_ID_ON_LOAD_ITEM_NOT_IMPLEMENTED;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.MEDIA_ID_ROOT;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.NOTIFY_CHILDREN_CHANGED;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.SEARCH_QUERY;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.SEARCH_QUERY_FOR_ERROR;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.SEARCH_QUERY_FOR_NO_RESULT;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.SEND_DELAYED_ITEM_LOADED;
+import static android.support.mediacompat.testlib.MediaBrowserConstants
+        .SEND_DELAYED_NOTIFY_CHILDREN_CHANGED;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.SET_SESSION_TOKEN;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.TEST_KEY_1;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.TEST_KEY_2;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.TEST_KEY_3;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.TEST_KEY_4;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.TEST_VALUE_1;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.TEST_VALUE_2;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.TEST_VALUE_3;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.TEST_VALUE_4;
+import static android.support.mediacompat.testlib.VersionConstants.KEY_SERVICE_VERSION;
+import static android.support.mediacompat.testlib.util.IntentUtil.callMediaBrowserServiceMethod;
+import static android.support.test.InstrumentationRegistry.getArguments;
+import static android.support.test.InstrumentationRegistry.getContext;
+import static android.support.test.InstrumentationRegistry.getInstrumentation;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertNull;
+import static junit.framework.Assert.assertTrue;
+import static junit.framework.Assert.fail;
+
+import android.content.ComponentName;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.mediacompat.testlib.util.PollingCheck;
+import android.support.test.filters.MediumTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.media.MediaBrowserCompat;
+import android.support.v4.media.MediaBrowserCompat.MediaItem;
+import android.support.v4.media.MediaBrowserServiceCompat;
+import android.support.v4.media.MediaDescriptionCompat;
+import android.util.Log;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Test {@link android.support.v4.media.MediaBrowserCompat}.
+ */
+@RunWith(AndroidJUnit4.class)
+public class MediaBrowserCompatTest {
+
+    private static final String TAG = "MediaBrowserCompatTest";
+
+    // The maximum time to wait for an operation.
+    private static final long TIME_OUT_MS = 3000L;
+    private static final long WAIT_TIME_FOR_NO_RESPONSE_MS = 300L;
+
+    /**
+     * To check {@link MediaBrowserCompat#unsubscribe} works properly,
+     * we notify to the browser after the unsubscription that the media items have changed.
+     * Then {@link MediaBrowserCompat.SubscriptionCallback#onChildrenLoaded} should not be called.
+     *
+     * The measured time from calling {@link MediaBrowserServiceCompat#notifyChildrenChanged}
+     * to {@link MediaBrowserCompat.SubscriptionCallback#onChildrenLoaded} being called is about
+     * 50ms.
+     * So we make the thread sleep for 100ms to properly check that the callback is not called.
+     */
+    private static final long SLEEP_MS = 100L;
+    private static final ComponentName TEST_BROWSER_SERVICE = new ComponentName(
+            "android.support.mediacompat.service.test",
+            "android.support.mediacompat.service.StubMediaBrowserServiceCompat");
+    private static final ComponentName TEST_BROWSER_SERVICE_DELAYED_MEDIA_SESSION =
+            new ComponentName(
+                    "android.support.mediacompat.service.test",
+                    "android.support.mediacompat.service"
+                            + ".StubMediaBrowserServiceCompatWithDelayedMediaSession");
+
+    private String mServiceVersion;
+    private MediaBrowserCompat mMediaBrowser;
+    private StubConnectionCallback mConnectionCallback;
+    private StubSubscriptionCallback mSubscriptionCallback;
+    private StubItemCallback mItemCallback;
+    private StubSearchCallback mSearchCallback;
+    private CustomActionCallback mCustomActionCallback;
+    private Bundle mRootHints;
+
+    @Before
+    public void setUp() {
+        // The version of the service app is provided through the instrumentation arguments.
+        mServiceVersion = getArguments().getString(KEY_SERVICE_VERSION, "");
+        Log.d(TAG, "Service app version: " + mServiceVersion);
+
+        mConnectionCallback = new StubConnectionCallback();
+        mSubscriptionCallback = new StubSubscriptionCallback();
+        mItemCallback = new StubItemCallback();
+        mSearchCallback = new StubSearchCallback();
+        mCustomActionCallback = new CustomActionCallback();
+
+        mRootHints = new Bundle();
+        mRootHints.putBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_RECENT, true);
+        mRootHints.putBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_OFFLINE, true);
+        mRootHints.putBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_SUGGESTED, true);
+
+        getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mMediaBrowser = new MediaBrowserCompat(getInstrumentation().getTargetContext(),
+                        TEST_BROWSER_SERVICE, mConnectionCallback, mRootHints);
+            }
+        });
+    }
+
+    @After
+    public void tearDown() {
+        if (mMediaBrowser != null && mMediaBrowser.isConnected()) {
+            mMediaBrowser.disconnect();
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testMediaBrowser() throws Exception {
+        assertFalse(mMediaBrowser.isConnected());
+
+        connectMediaBrowserService();
+        assertTrue(mMediaBrowser.isConnected());
+
+        assertEquals(TEST_BROWSER_SERVICE, mMediaBrowser.getServiceComponent());
+        assertEquals(MEDIA_ID_ROOT, mMediaBrowser.getRoot());
+        assertEquals(EXTRAS_VALUE, mMediaBrowser.getExtras().getString(EXTRAS_KEY));
+
+        mMediaBrowser.disconnect();
+        new PollingCheck(TIME_OUT_MS) {
+            @Override
+            protected boolean check() {
+                return !mMediaBrowser.isConnected();
+            }
+        }.run();
+    }
+
+    @Test
+    @SmallTest
+    public void testConnectTwice() throws Exception {
+        connectMediaBrowserService();
+        try {
+            mMediaBrowser.connect();
+            fail();
+        } catch (IllegalStateException e) {
+            // expected
+        }
+    }
+
+    @Test
+    @MediumTest
+    public void testReconnection() throws Exception {
+        getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mMediaBrowser.connect();
+                // Reconnect before the first connection was established.
+                mMediaBrowser.disconnect();
+                mMediaBrowser.connect();
+            }
+        });
+
+        synchronized (mConnectionCallback.mWaitLock) {
+            mConnectionCallback.mWaitLock.wait(TIME_OUT_MS);
+            assertEquals(1, mConnectionCallback.mConnectedCount);
+        }
+
+        synchronized (mSubscriptionCallback.mWaitLock) {
+            // Test subscribe.
+            resetCallbacks();
+            mMediaBrowser.subscribe(MEDIA_ID_ROOT, mSubscriptionCallback);
+            mSubscriptionCallback.mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mSubscriptionCallback.mChildrenLoadedCount > 0);
+            assertEquals(MEDIA_ID_ROOT, mSubscriptionCallback.mLastParentId);
+        }
+
+        synchronized (mItemCallback.mWaitLock) {
+            // Test getItem.
+            resetCallbacks();
+            mMediaBrowser.getItem(MEDIA_ID_CHILDREN[0], mItemCallback);
+            mItemCallback.mWaitLock.wait(TIME_OUT_MS);
+            assertEquals(MEDIA_ID_CHILDREN[0], mItemCallback.mLastMediaItem.getMediaId());
+        }
+
+        // Reconnect after connection was established.
+        mMediaBrowser.disconnect();
+        resetCallbacks();
+        connectMediaBrowserService();
+
+        synchronized (mItemCallback.mWaitLock) {
+            // Test getItem.
+            resetCallbacks();
+            mMediaBrowser.getItem(MEDIA_ID_CHILDREN[0], mItemCallback);
+            mItemCallback.mWaitLock.wait(TIME_OUT_MS);
+            assertEquals(MEDIA_ID_CHILDREN[0], mItemCallback.mLastMediaItem.getMediaId());
+        }
+    }
+
+    @Test
+    @MediumTest
+    public void testConnectionCallbackNotCalledAfterDisconnect() {
+        getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mMediaBrowser.connect();
+                mMediaBrowser.disconnect();
+                resetCallbacks();
+            }
+        });
+
+        try {
+            Thread.sleep(SLEEP_MS);
+        } catch (InterruptedException e) {
+            fail("Unexpected InterruptedException occurred.");
+        }
+        assertEquals(0, mConnectionCallback.mConnectedCount);
+        assertEquals(0, mConnectionCallback.mConnectionFailedCount);
+        assertEquals(0, mConnectionCallback.mConnectionSuspendedCount);
+    }
+
+//    @Test
+//    @MediumTest
+    public void testSubscribe() throws Exception {
+        connectMediaBrowserService();
+
+        synchronized (mSubscriptionCallback.mWaitLock) {
+            mMediaBrowser.subscribe(MEDIA_ID_ROOT, mSubscriptionCallback);
+            mSubscriptionCallback.mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mSubscriptionCallback.mChildrenLoadedCount > 0);
+            assertEquals(MEDIA_ID_ROOT, mSubscriptionCallback.mLastParentId);
+            assertEquals(MEDIA_ID_CHILDREN.length,
+                    mSubscriptionCallback.mLastChildMediaItems.size());
+            for (int i = 0; i < MEDIA_ID_CHILDREN.length; ++i) {
+                assertEquals(MEDIA_ID_CHILDREN[i],
+                        mSubscriptionCallback.mLastChildMediaItems.get(i).getMediaId());
+            }
+
+            // Test MediaBrowserServiceCompat.notifyChildrenChanged()
+            mSubscriptionCallback.reset();
+            callMediaBrowserServiceMethod(NOTIFY_CHILDREN_CHANGED, MEDIA_ID_ROOT, getContext());
+            mSubscriptionCallback.mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mSubscriptionCallback.mChildrenLoadedCount > 0);
+        }
+
+        // Test unsubscribe.
+        resetCallbacks();
+        mMediaBrowser.unsubscribe(MEDIA_ID_ROOT);
+
+        // After unsubscribing, make StubMediaBrowserServiceCompat notify that the children are
+        // changed.
+        callMediaBrowserServiceMethod(NOTIFY_CHILDREN_CHANGED, MEDIA_ID_ROOT, getContext());
+        try {
+            Thread.sleep(SLEEP_MS);
+        } catch (InterruptedException e) {
+            fail("Unexpected InterruptedException occurred.");
+        }
+        // onChildrenLoaded should not be called.
+        assertEquals(0, mSubscriptionCallback.mChildrenLoadedCount);
+    }
+
+//    @Test
+//    @MediumTest
+    public void testSubscribeWithOptions() throws Exception {
+        connectMediaBrowserService();
+        final int pageSize = 3;
+        final int lastPage = (MEDIA_ID_CHILDREN.length - 1) / pageSize;
+        Bundle options = new Bundle();
+        options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
+
+        synchronized (mSubscriptionCallback.mWaitLock) {
+            for (int page = 0; page <= lastPage; ++page) {
+                resetCallbacks();
+                options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
+                mMediaBrowser.subscribe(MEDIA_ID_ROOT, options, mSubscriptionCallback);
+                mSubscriptionCallback.mWaitLock.wait(TIME_OUT_MS);
+                assertTrue(mSubscriptionCallback.mChildrenLoadedWithOptionCount > 0);
+                assertEquals(MEDIA_ID_ROOT, mSubscriptionCallback.mLastParentId);
+                if (page != lastPage) {
+                    assertEquals(pageSize, mSubscriptionCallback.mLastChildMediaItems.size());
+                } else {
+                    assertEquals((MEDIA_ID_CHILDREN.length - 1) % pageSize + 1,
+                            mSubscriptionCallback.mLastChildMediaItems.size());
+                }
+                // Check whether all the items in the current page are loaded.
+                for (int i = 0; i < mSubscriptionCallback.mLastChildMediaItems.size(); ++i) {
+                    assertEquals(MEDIA_ID_CHILDREN[page * pageSize + i],
+                            mSubscriptionCallback.mLastChildMediaItems.get(i).getMediaId());
+                }
+            }
+
+            // Test MediaBrowserServiceCompat.notifyChildrenChanged()
+            mSubscriptionCallback.reset();
+            callMediaBrowserServiceMethod(NOTIFY_CHILDREN_CHANGED, MEDIA_ID_ROOT, getContext());
+            mSubscriptionCallback.mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mSubscriptionCallback.mChildrenLoadedWithOptionCount > 0);
+        }
+
+        // Test unsubscribe with callback argument.
+        resetCallbacks();
+        mMediaBrowser.unsubscribe(MEDIA_ID_ROOT, mSubscriptionCallback);
+
+        // After unsubscribing, make StubMediaBrowserServiceCompat notify that the children are
+        // changed.
+        callMediaBrowserServiceMethod(NOTIFY_CHILDREN_CHANGED, MEDIA_ID_ROOT, getContext());
+        try {
+            Thread.sleep(SLEEP_MS);
+        } catch (InterruptedException e) {
+            fail("Unexpected InterruptedException occurred.");
+        }
+        // onChildrenLoaded should not be called.
+        assertEquals(0, mSubscriptionCallback.mChildrenLoadedCount);
+    }
+
+    @Test
+    @MediumTest
+    public void testSubscribeDelayedItems() throws Exception {
+        connectMediaBrowserService();
+        synchronized (mSubscriptionCallback.mWaitLock) {
+            mSubscriptionCallback.reset();
+            mMediaBrowser.subscribe(MEDIA_ID_CHILDREN_DELAYED, mSubscriptionCallback);
+            mSubscriptionCallback.mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
+            assertEquals(0, mSubscriptionCallback.mChildrenLoadedCount);
+
+            callMediaBrowserServiceMethod(
+                    SEND_DELAYED_NOTIFY_CHILDREN_CHANGED, MEDIA_ID_CHILDREN_DELAYED, getContext());
+            mSubscriptionCallback.mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mSubscriptionCallback.mChildrenLoadedCount > 0);
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testSubscribeInvalidItem() throws Exception {
+        connectMediaBrowserService();
+
+        synchronized (mSubscriptionCallback.mWaitLock) {
+            mMediaBrowser.subscribe(MEDIA_ID_INVALID, mSubscriptionCallback);
+            mSubscriptionCallback.mWaitLock.wait(TIME_OUT_MS);
+            assertEquals(MEDIA_ID_INVALID, mSubscriptionCallback.mLastErrorId);
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testSubscribeInvalidItemWithOptions() throws Exception {
+        connectMediaBrowserService();
+
+        final int pageSize = 5;
+        final int page = 2;
+        Bundle options = new Bundle();
+        options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
+        options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
+
+        synchronized (mSubscriptionCallback.mWaitLock) {
+            mMediaBrowser.subscribe(MEDIA_ID_INVALID, options, mSubscriptionCallback);
+            mSubscriptionCallback.mWaitLock.wait(TIME_OUT_MS);
+            assertEquals(MEDIA_ID_INVALID, mSubscriptionCallback.mLastErrorId);
+            assertNotNull(mSubscriptionCallback.mLastOptions);
+            assertEquals(page,
+                    mSubscriptionCallback.mLastOptions.getInt(MediaBrowserCompat.EXTRA_PAGE));
+            assertEquals(pageSize,
+                    mSubscriptionCallback.mLastOptions.getInt(MediaBrowserCompat.EXTRA_PAGE_SIZE));
+        }
+    }
+
+    @Test
+    @MediumTest
+    public void testUnsubscribeForMultipleSubscriptions() throws Exception {
+        connectMediaBrowserService();
+        final List<StubSubscriptionCallback> subscriptionCallbacks = new ArrayList<>();
+        final int pageSize = 1;
+
+        // Subscribe four pages, one item per page.
+        for (int page = 0; page < 4; page++) {
+            final StubSubscriptionCallback callback = new StubSubscriptionCallback();
+            subscriptionCallbacks.add(callback);
+
+            Bundle options = new Bundle();
+            options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
+            options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
+            mMediaBrowser.subscribe(MEDIA_ID_ROOT, options, callback);
+            synchronized (callback.mWaitLock) {
+                callback.mWaitLock.wait(TIME_OUT_MS);
+            }
+            // Each onChildrenLoaded() must be called.
+            assertEquals(1, callback.mChildrenLoadedWithOptionCount);
+        }
+
+        // Reset callbacks and unsubscribe.
+        for (StubSubscriptionCallback callback : subscriptionCallbacks) {
+            callback.reset();
+        }
+        mMediaBrowser.unsubscribe(MEDIA_ID_ROOT);
+
+        // After unsubscribing, make StubMediaBrowserServiceCompat notify that the children are
+        // changed.
+        callMediaBrowserServiceMethod(NOTIFY_CHILDREN_CHANGED, MEDIA_ID_ROOT, getContext());
+        try {
+            Thread.sleep(SLEEP_MS);
+        } catch (InterruptedException e) {
+            fail("Unexpected InterruptedException occurred.");
+        }
+
+        // onChildrenLoaded should not be called.
+        for (StubSubscriptionCallback callback : subscriptionCallbacks) {
+            assertEquals(0, callback.mChildrenLoadedWithOptionCount);
+        }
+    }
+
+//    @Test
+//    @MediumTest
+    public void testUnsubscribeWithSubscriptionCallbackForMultipleSubscriptions() throws Exception {
+        connectMediaBrowserService();
+        final List<StubSubscriptionCallback> subscriptionCallbacks = new ArrayList<>();
+        final int pageSize = 1;
+
+        // Subscribe four pages, one item per page.
+        for (int page = 0; page < 4; page++) {
+            final StubSubscriptionCallback callback = new StubSubscriptionCallback();
+            subscriptionCallbacks.add(callback);
+
+            Bundle options = new Bundle();
+            options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
+            options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
+            mMediaBrowser.subscribe(MEDIA_ID_ROOT, options, callback);
+            synchronized (callback.mWaitLock) {
+                callback.mWaitLock.wait(TIME_OUT_MS);
+            }
+            // Each onChildrenLoaded() must be called.
+            assertEquals(1, callback.mChildrenLoadedWithOptionCount);
+        }
+
+        // Unsubscribe existing subscriptions one-by-one.
+        final int[] orderOfRemovingCallbacks = {2, 0, 3, 1};
+        for (int i = 0; i < orderOfRemovingCallbacks.length; i++) {
+            // Reset callbacks
+            for (StubSubscriptionCallback callback : subscriptionCallbacks) {
+                callback.reset();
+            }
+
+            // Remove one subscription
+            mMediaBrowser.unsubscribe(MEDIA_ID_ROOT,
+                    subscriptionCallbacks.get(orderOfRemovingCallbacks[i]));
+
+            // Make StubMediaBrowserServiceCompat notify that the children are changed.
+            callMediaBrowserServiceMethod(NOTIFY_CHILDREN_CHANGED, MEDIA_ID_ROOT, getContext());
+            try {
+                Thread.sleep(SLEEP_MS);
+            } catch (InterruptedException e) {
+                fail("Unexpected InterruptedException occurred.");
+            }
+
+            // Only the remaining subscriptionCallbacks should be called.
+            for (int j = 0; j < 4; j++) {
+                int childrenLoadedWithOptionsCount = subscriptionCallbacks
+                        .get(orderOfRemovingCallbacks[j]).mChildrenLoadedWithOptionCount;
+                if (j <= i) {
+                    assertEquals(0, childrenLoadedWithOptionsCount);
+                } else {
+                    assertEquals(1, childrenLoadedWithOptionsCount);
+                }
+            }
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testGetItem() throws Exception {
+        connectMediaBrowserService();
+
+        synchronized (mItemCallback.mWaitLock) {
+            mMediaBrowser.getItem(MEDIA_ID_CHILDREN[0], mItemCallback);
+            mItemCallback.mWaitLock.wait(TIME_OUT_MS);
+            assertNotNull(mItemCallback.mLastMediaItem);
+            assertEquals(MEDIA_ID_CHILDREN[0], mItemCallback.mLastMediaItem.getMediaId());
+        }
+    }
+
+    @Test
+    @MediumTest
+    public void testGetItemDelayed() throws Exception {
+        connectMediaBrowserService();
+
+        synchronized (mItemCallback.mWaitLock) {
+            mMediaBrowser.getItem(MEDIA_ID_CHILDREN_DELAYED, mItemCallback);
+            mItemCallback.mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
+            assertNull(mItemCallback.mLastMediaItem);
+
+            mItemCallback.reset();
+            callMediaBrowserServiceMethod(SEND_DELAYED_ITEM_LOADED, new Bundle(), getContext());
+            mItemCallback.mWaitLock.wait(TIME_OUT_MS);
+            assertNotNull(mItemCallback.mLastMediaItem);
+            assertEquals(MEDIA_ID_CHILDREN_DELAYED, mItemCallback.mLastMediaItem.getMediaId());
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testGetItemWhenOnLoadItemIsNotImplemented() throws Exception {
+        connectMediaBrowserService();
+        synchronized (mItemCallback.mWaitLock) {
+            mMediaBrowser.getItem(MEDIA_ID_ON_LOAD_ITEM_NOT_IMPLEMENTED, mItemCallback);
+            mItemCallback.mWaitLock.wait(TIME_OUT_MS);
+            assertEquals(MEDIA_ID_ON_LOAD_ITEM_NOT_IMPLEMENTED, mItemCallback.mLastErrorId);
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testGetItemWhenMediaIdIsInvalid() throws Exception {
+        mItemCallback.mLastMediaItem = new MediaItem(new MediaDescriptionCompat.Builder()
+                .setMediaId("dummy_id").build(), MediaItem.FLAG_BROWSABLE);
+
+        connectMediaBrowserService();
+        synchronized (mItemCallback.mWaitLock) {
+            mMediaBrowser.getItem(MEDIA_ID_INVALID, mItemCallback);
+            mItemCallback.mWaitLock.wait(TIME_OUT_MS);
+            assertNull(mItemCallback.mLastMediaItem);
+            assertNull(mItemCallback.mLastErrorId);
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testSearch() throws Exception {
+        connectMediaBrowserService();
+
+        final String key = "test-key";
+        final String val = "test-val";
+
+        synchronized (mSearchCallback.mWaitLock) {
+            mSearchCallback.reset();
+            mMediaBrowser.search(SEARCH_QUERY_FOR_NO_RESULT, null, mSearchCallback);
+            mSearchCallback.mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
+            assertTrue(mSearchCallback.mOnSearchResult);
+            assertTrue(mSearchCallback.mSearchResults != null
+                    && mSearchCallback.mSearchResults.size() == 0);
+            assertEquals(null, mSearchCallback.mSearchExtras);
+
+            mSearchCallback.reset();
+            mMediaBrowser.search(SEARCH_QUERY_FOR_ERROR, null, mSearchCallback);
+            mSearchCallback.mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
+            assertTrue(mSearchCallback.mOnSearchResult);
+            assertNull(mSearchCallback.mSearchResults);
+            assertEquals(null, mSearchCallback.mSearchExtras);
+
+            mSearchCallback.reset();
+            Bundle extras = new Bundle();
+            extras.putString(key, val);
+            mMediaBrowser.search(SEARCH_QUERY, extras, mSearchCallback);
+            mSearchCallback.mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
+            assertTrue(mSearchCallback.mOnSearchResult);
+            assertNotNull(mSearchCallback.mSearchResults);
+            for (MediaItem item : mSearchCallback.mSearchResults) {
+                assertNotNull(item.getMediaId());
+                assertTrue(item.getMediaId().contains(SEARCH_QUERY));
+            }
+            assertNotNull(mSearchCallback.mSearchExtras);
+            assertEquals(val, mSearchCallback.mSearchExtras.getString(key));
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testSendCustomAction() throws Exception {
+        connectMediaBrowserService();
+
+        synchronized (mCustomActionCallback.mWaitLock) {
+            Bundle customActionExtras = new Bundle();
+            customActionExtras.putString(TEST_KEY_1, TEST_VALUE_1);
+            mMediaBrowser.sendCustomAction(
+                    CUSTOM_ACTION, customActionExtras, mCustomActionCallback);
+
+            mCustomActionCallback.reset();
+            Bundle data1 = new Bundle();
+            data1.putString(TEST_KEY_2, TEST_VALUE_2);
+            callMediaBrowserServiceMethod(CUSTOM_ACTION_SEND_PROGRESS_UPDATE, data1, getContext());
+            mCustomActionCallback.mWaitLock.wait(TIME_OUT_MS);
+
+            assertTrue(mCustomActionCallback.mOnProgressUpdateCalled);
+            assertEquals(CUSTOM_ACTION, mCustomActionCallback.mAction);
+            assertNotNull(mCustomActionCallback.mExtras);
+            assertEquals(TEST_VALUE_1, mCustomActionCallback.mExtras.getString(TEST_KEY_1));
+            assertNotNull(mCustomActionCallback.mData);
+            assertEquals(TEST_VALUE_2, mCustomActionCallback.mData.getString(TEST_KEY_2));
+
+            mCustomActionCallback.reset();
+            Bundle data2 = new Bundle();
+            data2.putString(TEST_KEY_3, TEST_VALUE_3);
+            callMediaBrowserServiceMethod(CUSTOM_ACTION_SEND_PROGRESS_UPDATE, data2, getContext());
+            mCustomActionCallback.mWaitLock.wait(TIME_OUT_MS);
+
+            assertTrue(mCustomActionCallback.mOnProgressUpdateCalled);
+            assertEquals(CUSTOM_ACTION, mCustomActionCallback.mAction);
+            assertNotNull(mCustomActionCallback.mExtras);
+            assertEquals(TEST_VALUE_1, mCustomActionCallback.mExtras.getString(TEST_KEY_1));
+            assertNotNull(mCustomActionCallback.mData);
+            assertEquals(TEST_VALUE_3, mCustomActionCallback.mData.getString(TEST_KEY_3));
+
+            Bundle resultData = new Bundle();
+            resultData.putString(TEST_KEY_4, TEST_VALUE_4);
+            mCustomActionCallback.reset();
+            callMediaBrowserServiceMethod(CUSTOM_ACTION_SEND_RESULT, resultData, getContext());
+            mCustomActionCallback.mWaitLock.wait(TIME_OUT_MS);
+
+            assertTrue(mCustomActionCallback.mOnResultCalled);
+            assertEquals(CUSTOM_ACTION, mCustomActionCallback.mAction);
+            assertNotNull(mCustomActionCallback.mExtras);
+            assertEquals(TEST_VALUE_1, mCustomActionCallback.mExtras.getString(TEST_KEY_1));
+            assertNotNull(mCustomActionCallback.mData);
+            assertEquals(TEST_VALUE_4, mCustomActionCallback.mData.getString(TEST_KEY_4));
+        }
+    }
+
+
+    @Test
+    @MediumTest
+    public void testSendCustomActionWithDetachedError() throws Exception {
+        connectMediaBrowserService();
+
+        synchronized (mCustomActionCallback.mWaitLock) {
+            Bundle customActionExtras = new Bundle();
+            customActionExtras.putString(TEST_KEY_1, TEST_VALUE_1);
+            mMediaBrowser.sendCustomAction(
+                    CUSTOM_ACTION, customActionExtras, mCustomActionCallback);
+            mCustomActionCallback.mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
+
+            mCustomActionCallback.reset();
+            Bundle progressUpdateData = new Bundle();
+            progressUpdateData.putString(TEST_KEY_2, TEST_VALUE_2);
+            callMediaBrowserServiceMethod(
+                    CUSTOM_ACTION_SEND_PROGRESS_UPDATE, progressUpdateData, getContext());
+            mCustomActionCallback.mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCustomActionCallback.mOnProgressUpdateCalled);
+            assertEquals(CUSTOM_ACTION, mCustomActionCallback.mAction);
+            assertNotNull(mCustomActionCallback.mExtras);
+            assertEquals(TEST_VALUE_1, mCustomActionCallback.mExtras.getString(TEST_KEY_1));
+            assertNotNull(mCustomActionCallback.mData);
+            assertEquals(TEST_VALUE_2, mCustomActionCallback.mData.getString(TEST_KEY_2));
+
+            mCustomActionCallback.reset();
+            Bundle errorData = new Bundle();
+            errorData.putString(TEST_KEY_3, TEST_VALUE_3);
+            callMediaBrowserServiceMethod(CUSTOM_ACTION_SEND_ERROR, errorData, getContext());
+            mCustomActionCallback.mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCustomActionCallback.mOnErrorCalled);
+            assertEquals(CUSTOM_ACTION, mCustomActionCallback.mAction);
+            assertNotNull(mCustomActionCallback.mExtras);
+            assertEquals(TEST_VALUE_1, mCustomActionCallback.mExtras.getString(TEST_KEY_1));
+            assertNotNull(mCustomActionCallback.mData);
+            assertEquals(TEST_VALUE_3, mCustomActionCallback.mData.getString(TEST_KEY_3));
+        }
+    }
+
+    @Test
+    @MediumTest
+    public void testSendCustomActionWithNullCallback() throws Exception {
+        connectMediaBrowserService();
+
+        Bundle customActionExtras = new Bundle();
+        customActionExtras.putString(TEST_KEY_1, TEST_VALUE_1);
+        mMediaBrowser.sendCustomAction(CUSTOM_ACTION, customActionExtras, null);
+
+        // These calls should not make any exceptions.
+        callMediaBrowserServiceMethod(CUSTOM_ACTION_SEND_PROGRESS_UPDATE, new Bundle(),
+                getContext());
+        callMediaBrowserServiceMethod(CUSTOM_ACTION_SEND_RESULT, new Bundle(), getContext());
+        Thread.sleep(WAIT_TIME_FOR_NO_RESPONSE_MS);
+    }
+
+    @Test
+    @SmallTest
+    public void testSendCustomActionWithError() throws Exception {
+        connectMediaBrowserService();
+
+        synchronized (mCustomActionCallback.mWaitLock) {
+            mMediaBrowser.sendCustomAction(CUSTOM_ACTION_FOR_ERROR, null, mCustomActionCallback);
+            mCustomActionCallback.mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCustomActionCallback.mOnErrorCalled);
+        }
+    }
+
+    @Test
+    @MediumTest
+    public void testDelayedSetSessionToken() throws Exception {
+        // This test has no meaning in API 21. The framework MediaBrowserService just connects to
+        // the media browser without waiting setMediaSession() to be called.
+        if (Build.VERSION.SDK_INT == 21) {
+            return;
+        }
+        final ConnectionCallbackForDelayedMediaSession callback =
+                new ConnectionCallbackForDelayedMediaSession();
+
+        getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mMediaBrowser = new MediaBrowserCompat(
+                        getInstrumentation().getTargetContext(),
+                        TEST_BROWSER_SERVICE_DELAYED_MEDIA_SESSION,
+                        callback,
+                        null);
+            }
+        });
+
+        synchronized (callback.mWaitLock) {
+            mMediaBrowser.connect();
+            callback.mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
+            assertEquals(0, callback.mConnectedCount);
+
+            callMediaBrowserServiceMethod(SET_SESSION_TOKEN, new Bundle(), getContext());
+            callback.mWaitLock.wait(TIME_OUT_MS);
+            assertEquals(1, callback.mConnectedCount);
+
+            if (Build.VERSION.SDK_INT >= 21) {
+                assertNotNull(mMediaBrowser.getSessionToken().getExtraBinder());
+            }
+        }
+    }
+
+    private void connectMediaBrowserService() throws Exception {
+        synchronized (mConnectionCallback.mWaitLock) {
+            mMediaBrowser.connect();
+            mConnectionCallback.mWaitLock.wait(TIME_OUT_MS);
+            if (!mMediaBrowser.isConnected()) {
+                fail("Browser failed to connect!");
+            }
+        }
+    }
+
+    private void resetCallbacks() {
+        mConnectionCallback.reset();
+        mSubscriptionCallback.reset();
+        mItemCallback.reset();
+    }
+
+    private class StubConnectionCallback extends MediaBrowserCompat.ConnectionCallback {
+        final Object mWaitLock = new Object();
+        volatile int mConnectedCount;
+        volatile int mConnectionFailedCount;
+        volatile int mConnectionSuspendedCount;
+
+        public void reset() {
+            mConnectedCount = 0;
+            mConnectionFailedCount = 0;
+            mConnectionSuspendedCount = 0;
+        }
+
+        @Override
+        public void onConnected() {
+            synchronized (mWaitLock) {
+                mConnectedCount++;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onConnectionFailed() {
+            synchronized (mWaitLock) {
+                mConnectionFailedCount++;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onConnectionSuspended() {
+            synchronized (mWaitLock) {
+                mConnectionSuspendedCount++;
+                mWaitLock.notify();
+            }
+        }
+    }
+
+    private class StubSubscriptionCallback extends MediaBrowserCompat.SubscriptionCallback {
+        final Object mWaitLock = new Object();
+        private volatile int mChildrenLoadedCount;
+        private volatile int mChildrenLoadedWithOptionCount;
+        private volatile String mLastErrorId;
+        private volatile String mLastParentId;
+        private volatile Bundle mLastOptions;
+        private volatile List<MediaItem> mLastChildMediaItems;
+
+        public void reset() {
+            mChildrenLoadedCount = 0;
+            mChildrenLoadedWithOptionCount = 0;
+            mLastErrorId = null;
+            mLastParentId = null;
+            mLastOptions = null;
+            mLastChildMediaItems = null;
+        }
+
+        @Override
+        public void onChildrenLoaded(@NonNull String parentId, @NonNull List<MediaItem> children) {
+            synchronized (mWaitLock) {
+                mChildrenLoadedCount++;
+                mLastParentId = parentId;
+                mLastChildMediaItems = children;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onChildrenLoaded(@NonNull String parentId, @NonNull List<MediaItem> children,
+                @NonNull Bundle options) {
+            synchronized (mWaitLock) {
+                mChildrenLoadedWithOptionCount++;
+                mLastParentId = parentId;
+                mLastOptions = options;
+                mLastChildMediaItems = children;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onError(@NonNull String id) {
+            synchronized (mWaitLock) {
+                mLastErrorId = id;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onError(@NonNull String id, @NonNull Bundle options) {
+            synchronized (mWaitLock) {
+                mLastErrorId = id;
+                mLastOptions = options;
+                mWaitLock.notify();
+            }
+        }
+    }
+
+    private class StubItemCallback extends MediaBrowserCompat.ItemCallback {
+        final Object mWaitLock = new Object();
+        private volatile MediaItem mLastMediaItem;
+        private volatile String mLastErrorId;
+
+        public void reset() {
+            mLastMediaItem = null;
+            mLastErrorId = null;
+        }
+
+        @Override
+        public void onItemLoaded(MediaItem item) {
+            synchronized (mWaitLock) {
+                mLastMediaItem = item;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onError(@NonNull String id) {
+            synchronized (mWaitLock) {
+                mLastErrorId = id;
+                mWaitLock.notify();
+            }
+        }
+    }
+
+    private class StubSearchCallback extends MediaBrowserCompat.SearchCallback {
+        final Object mWaitLock = new Object();
+        boolean mOnSearchResult;
+        Bundle mSearchExtras;
+        List<MediaItem> mSearchResults;
+
+        @Override
+        public void onSearchResult(@NonNull String query, Bundle extras,
+                @NonNull List<MediaItem> items) {
+            synchronized (mWaitLock) {
+                mOnSearchResult = true;
+                mSearchResults = items;
+                mSearchExtras = extras;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onError(@NonNull String query, Bundle extras) {
+            synchronized (mWaitLock) {
+                mOnSearchResult = true;
+                mSearchResults = null;
+                mSearchExtras = extras;
+                mWaitLock.notify();
+            }
+        }
+
+        public void reset() {
+            mOnSearchResult = false;
+            mSearchExtras = null;
+            mSearchResults = null;
+        }
+    }
+
+    private class CustomActionCallback extends MediaBrowserCompat.CustomActionCallback {
+        final Object mWaitLock = new Object();
+        String mAction;
+        Bundle mExtras;
+        Bundle mData;
+        boolean mOnProgressUpdateCalled;
+        boolean mOnResultCalled;
+        boolean mOnErrorCalled;
+
+        @Override
+        public void onProgressUpdate(String action, Bundle extras, Bundle data) {
+            synchronized (mWaitLock) {
+                mOnProgressUpdateCalled = true;
+                mAction = action;
+                mExtras = extras;
+                mData = data;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onResult(String action, Bundle extras, Bundle resultData) {
+            synchronized (mWaitLock) {
+                mOnResultCalled = true;
+                mAction = action;
+                mExtras = extras;
+                mData = resultData;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onError(String action, Bundle extras, Bundle data) {
+            synchronized (mWaitLock) {
+                mOnErrorCalled = true;
+                mAction = action;
+                mExtras = extras;
+                mData = data;
+                mWaitLock.notify();
+            }
+        }
+
+        public void reset() {
+            mOnResultCalled = false;
+            mOnProgressUpdateCalled = false;
+            mOnErrorCalled = false;
+            mAction = null;
+            mExtras = null;
+            mData = null;
+        }
+    }
+
+    private class ConnectionCallbackForDelayedMediaSession extends
+            MediaBrowserCompat.ConnectionCallback {
+        final Object mWaitLock = new Object();
+        private int mConnectedCount = 0;
+
+        @Override
+        public void onConnected() {
+            synchronized (mWaitLock) {
+                mConnectedCount++;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onConnectionFailed() {
+            synchronized (mWaitLock) {
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onConnectionSuspended() {
+            synchronized (mWaitLock) {
+                mWaitLock.notify();
+            }
+        }
+    }
+}
diff --git a/media-compat/version-compat-tests/previous/client/tests/src/android/support/mediacompat/client/MediaControllerCompatCallbackTest.java b/media-compat/version-compat-tests/previous/client/tests/src/android/support/mediacompat/client/MediaControllerCompatCallbackTest.java
new file mode 100644
index 0000000..79993ef
--- /dev/null
+++ b/media-compat/version-compat-tests/previous/client/tests/src/android/support/mediacompat/client/MediaControllerCompatCallbackTest.java
@@ -0,0 +1,718 @@
+/*
+ * Copyright 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.mediacompat.client;
+
+import static android.media.AudioManager.STREAM_MUSIC;
+import static android.support.mediacompat.testlib.MediaSessionConstants.RELEASE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SEND_SESSION_EVENT;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_CAPTIONING_ENABLED;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_EXTRAS;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_FLAGS;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_METADATA;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_PLAYBACK_STATE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_PLAYBACK_TO_LOCAL;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_PLAYBACK_TO_REMOTE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_QUEUE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_QUEUE_TITLE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_RATING_TYPE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_REPEAT_MODE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_SESSION_ACTIVITY;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_SHUFFLE_MODE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_ACTION;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_CURRENT_VOLUME;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_ERROR_CODE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_ERROR_MSG;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_FLAGS;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_KEY;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_MAX_VOLUME;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_MEDIA_ID_1;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_MEDIA_ID_2;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_QUEUE_ID_1;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_QUEUE_ID_2;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_SESSION_EVENT;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_VALUE;
+import static android.support.mediacompat.testlib.VersionConstants.KEY_SERVICE_VERSION;
+import static android.support.mediacompat.testlib.util.IntentUtil.callMediaSessionMethod;
+import static android.support.mediacompat.testlib.util.TestUtil.assertBundleEquals;
+import static android.support.test.InstrumentationRegistry.getArguments;
+import static android.support.test.InstrumentationRegistry.getContext;
+import static android.support.test.InstrumentationRegistry.getInstrumentation;
+import static android.support.test.InstrumentationRegistry.getTargetContext;
+import static android.support.v4.media.MediaMetadataCompat.METADATA_KEY_RATING;
+
+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.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.media.AudioManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.mediacompat.testlib.util.PollingCheck;
+import android.support.test.filters.LargeTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.media.MediaBrowserCompat;
+import android.support.v4.media.MediaDescriptionCompat;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.RatingCompat;
+import android.support.v4.media.VolumeProviderCompat;
+import android.support.v4.media.session.MediaControllerCompat;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.MediaSessionCompat.QueueItem;
+import android.support.v4.media.session.ParcelableVolumeInfo;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.util.Log;
+
+import junit.framework.Assert;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Test {@link MediaControllerCompat.Callback}.
+ */
+@RunWith(AndroidJUnit4.class)
+public class MediaControllerCompatCallbackTest {
+
+    private static final String TAG = "MediaControllerCompatCallbackTest";
+
+    // The maximum time to wait for an operation, that is expected to happen.
+    private static final long TIME_OUT_MS = 3000L;
+    private static final int MAX_AUDIO_INFO_CHANGED_CALLBACK_COUNT = 10;
+
+    private static final ComponentName TEST_BROWSER_SERVICE = new ComponentName(
+            "android.support.mediacompat.service.test",
+            "android.support.mediacompat.service.StubMediaBrowserServiceCompat");
+
+    private final Handler mHandler = new Handler(Looper.getMainLooper());
+    private final Object mWaitLock = new Object();
+
+    private String mServiceVersion;
+
+    // MediaBrowserCompat object to get the session token.
+    private MediaBrowserCompat mMediaBrowser;
+    private ConnectionCallback mConnectionCallback = new ConnectionCallback();
+
+    private MediaControllerCompat mController;
+    private MediaControllerCallback mMediaControllerCallback = new MediaControllerCallback();
+
+    @Before
+    public void setUp() throws Exception {
+        // The version of the service app is provided through the instrumentation arguments.
+        mServiceVersion = getArguments().getString(KEY_SERVICE_VERSION, "");
+        Log.d(TAG, "Service app version: " + mServiceVersion);
+
+        getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mMediaBrowser = new MediaBrowserCompat(getInstrumentation().getTargetContext(),
+                        TEST_BROWSER_SERVICE, mConnectionCallback, new Bundle());
+            }
+        });
+
+        synchronized (mConnectionCallback.mWaitLock) {
+            mMediaBrowser.connect();
+            mConnectionCallback.mWaitLock.wait(TIME_OUT_MS);
+            if (!mMediaBrowser.isConnected()) {
+                Assert.fail("Browser failed to connect!");
+            }
+        }
+        mController =
+                new MediaControllerCompat(getTargetContext(), mMediaBrowser.getSessionToken());
+        mController.registerCallback(mMediaControllerCallback, mHandler);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (mMediaBrowser != null && mMediaBrowser.isConnected()) {
+            mMediaBrowser.disconnect();
+        }
+    }
+
+    /**
+     * Tests {@link MediaSessionCompat#setExtras}.
+     */
+    @Test
+    @SmallTest
+    public void testSetExtras() throws Exception {
+        synchronized (mWaitLock) {
+            mMediaControllerCallback.resetLocked();
+
+            Bundle extras = new Bundle();
+            extras.putString(TEST_KEY, TEST_VALUE);
+            callMediaSessionMethod(SET_EXTRAS, extras, getContext());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mMediaControllerCallback.mOnExtraChangedCalled);
+
+            assertBundleEquals(extras, mMediaControllerCallback.mExtras);
+            assertBundleEquals(extras, mController.getExtras());
+        }
+    }
+
+    /**
+     * Tests {@link MediaSessionCompat#setFlags}.
+     */
+    @Test
+    @SmallTest
+    public void testSetFlags() throws Exception {
+        synchronized (mWaitLock) {
+            mMediaControllerCallback.resetLocked();
+
+            callMediaSessionMethod(SET_FLAGS, TEST_FLAGS, getContext());
+            new PollingCheck(TIME_OUT_MS) {
+                @Override
+                public boolean check() {
+                    return TEST_FLAGS == mController.getFlags();
+                }
+            }.run();
+        }
+    }
+
+    /**
+     * Tests {@link MediaSessionCompat#setMetadata}.
+     */
+    @Test
+    @SmallTest
+    public void testSetMetadata() throws Exception {
+        synchronized (mWaitLock) {
+            mMediaControllerCallback.resetLocked();
+            RatingCompat rating = RatingCompat.newHeartRating(true);
+            MediaMetadataCompat metadata = new MediaMetadataCompat.Builder()
+                    .putString(TEST_KEY, TEST_VALUE)
+                    .putRating(METADATA_KEY_RATING, rating)
+                    .build();
+
+            callMediaSessionMethod(SET_METADATA, metadata, getContext());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mMediaControllerCallback.mOnMetadataChangedCalled);
+
+            MediaMetadataCompat metadataOut = mMediaControllerCallback.mMediaMetadata;
+            assertNotNull(metadataOut);
+            assertEquals(TEST_VALUE, metadataOut.getString(TEST_KEY));
+
+            metadataOut = mController.getMetadata();
+            assertNotNull(metadataOut);
+            assertEquals(TEST_VALUE, metadataOut.getString(TEST_KEY));
+
+            assertNotNull(metadataOut.getRating(METADATA_KEY_RATING));
+            RatingCompat ratingOut = metadataOut.getRating(METADATA_KEY_RATING);
+            assertEquals(rating.getRatingStyle(), ratingOut.getRatingStyle());
+            assertEquals(rating.getPercentRating(), ratingOut.getPercentRating(), 0.0f);
+        }
+    }
+
+    /**
+     * Tests {@link MediaSessionCompat#setMetadata} with artwork bitmaps.
+     */
+    @Test
+    @SmallTest
+    public void testSetMetadataWithArtworks() throws Exception {
+        // TODO: Add test with a large bitmap.
+        // Using large bitmap makes other tests that are executed after this fail.
+        final Bitmap bitmapSmall = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
+
+        synchronized (mWaitLock) {
+            mMediaControllerCallback.resetLocked();
+            MediaMetadataCompat metadata = new MediaMetadataCompat.Builder()
+                    .putString(TEST_KEY, TEST_VALUE)
+                    .putBitmap(MediaMetadataCompat.METADATA_KEY_ART, bitmapSmall)
+                    .build();
+
+            callMediaSessionMethod(SET_METADATA, metadata, getContext());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mMediaControllerCallback.mOnMetadataChangedCalled);
+
+            MediaMetadataCompat metadataOut = mMediaControllerCallback.mMediaMetadata;
+            assertNotNull(metadataOut);
+            assertEquals(TEST_VALUE, metadataOut.getString(TEST_KEY));
+
+            Bitmap bitmapSmallOut = metadataOut.getBitmap(MediaMetadataCompat.METADATA_KEY_ART);
+            assertNotNull(bitmapSmallOut);
+            assertEquals(bitmapSmall.getHeight(), bitmapSmallOut.getHeight());
+            assertEquals(bitmapSmall.getWidth(), bitmapSmallOut.getWidth());
+            assertEquals(bitmapSmall.getConfig(), bitmapSmallOut.getConfig());
+
+            bitmapSmallOut.recycle();
+        }
+        bitmapSmall.recycle();
+    }
+
+    /**
+     * Tests {@link MediaSessionCompat#setPlaybackState}.
+     */
+    @Test
+    @SmallTest
+    public void testSetPlaybackState() throws Exception {
+        synchronized (mWaitLock) {
+            mMediaControllerCallback.resetLocked();
+            PlaybackStateCompat state =
+                    new PlaybackStateCompat.Builder()
+                            .setActions(TEST_ACTION)
+                            .setErrorMessage(TEST_ERROR_CODE, TEST_ERROR_MSG)
+                            .build();
+
+            callMediaSessionMethod(SET_PLAYBACK_STATE, state, getContext());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mMediaControllerCallback.mOnPlaybackStateChangedCalled);
+
+            PlaybackStateCompat stateOut = mMediaControllerCallback.mPlaybackState;
+            assertNotNull(stateOut);
+            assertEquals(TEST_ACTION, stateOut.getActions());
+            assertEquals(TEST_ERROR_CODE, stateOut.getErrorCode());
+            assertEquals(TEST_ERROR_MSG, stateOut.getErrorMessage().toString());
+
+            stateOut = mController.getPlaybackState();
+            assertNotNull(stateOut);
+            assertEquals(TEST_ACTION, stateOut.getActions());
+            assertEquals(TEST_ERROR_CODE, stateOut.getErrorCode());
+            assertEquals(TEST_ERROR_MSG, stateOut.getErrorMessage().toString());
+        }
+    }
+
+    /**
+     * Tests {@link MediaSessionCompat#setQueue} and {@link MediaSessionCompat#setQueueTitle}.
+     */
+    @Test
+    @SmallTest
+    public void testSetQueueAndSetQueueTitle() throws Exception {
+        synchronized (mWaitLock) {
+            mMediaControllerCallback.resetLocked();
+            List<QueueItem> queue = new ArrayList<>();
+
+            MediaDescriptionCompat description1 =
+                    new MediaDescriptionCompat.Builder().setMediaId(TEST_MEDIA_ID_1).build();
+            MediaDescriptionCompat description2 =
+                    new MediaDescriptionCompat.Builder().setMediaId(TEST_MEDIA_ID_2).build();
+            QueueItem item1 = new MediaSessionCompat.QueueItem(description1, TEST_QUEUE_ID_1);
+            QueueItem item2 = new MediaSessionCompat.QueueItem(description2, TEST_QUEUE_ID_2);
+            queue.add(item1);
+            queue.add(item2);
+
+            callMediaSessionMethod(SET_QUEUE, queue, getContext());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mMediaControllerCallback.mOnQueueChangedCalled);
+
+            callMediaSessionMethod(SET_QUEUE_TITLE, TEST_VALUE, getContext());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mMediaControllerCallback.mOnQueueTitleChangedCalled);
+
+            assertEquals(TEST_VALUE, mMediaControllerCallback.mTitle);
+            assertQueueEquals(queue, mMediaControllerCallback.mQueue);
+
+            assertEquals(TEST_VALUE, mController.getQueueTitle());
+            assertQueueEquals(queue, mController.getQueue());
+
+            mMediaControllerCallback.resetLocked();
+            callMediaSessionMethod(SET_QUEUE, null, getContext());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mMediaControllerCallback.mOnQueueChangedCalled);
+
+            callMediaSessionMethod(SET_QUEUE_TITLE, null, getContext());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mMediaControllerCallback.mOnQueueTitleChangedCalled);
+
+            assertNull(mMediaControllerCallback.mTitle);
+            assertNull(mMediaControllerCallback.mQueue);
+            assertNull(mController.getQueueTitle());
+            assertNull(mController.getQueue());
+        }
+    }
+
+    /**
+     * Tests {@link MediaSessionCompat#setSessionActivity}.
+     */
+    @Test
+    @SmallTest
+    public void testSessionActivity() throws Exception {
+        synchronized (mWaitLock) {
+            Intent intent = new Intent("MEDIA_SESSION_ACTION");
+            final int requestCode = 555;
+            final PendingIntent pi =
+                    PendingIntent.getActivity(getTargetContext(), requestCode, intent, 0);
+
+            callMediaSessionMethod(SET_SESSION_ACTIVITY, pi, getContext());
+            new PollingCheck(TIME_OUT_MS) {
+                @Override
+                public boolean check() {
+                    return pi.equals(mController.getSessionActivity());
+                }
+            }.run();
+        }
+    }
+
+    /**
+     * Tests {@link MediaSessionCompat#setCaptioningEnabled}.
+     */
+    @Test
+    @SmallTest
+    public void testSetCaptioningEnabled() throws Exception {
+        synchronized (mWaitLock) {
+            mMediaControllerCallback.resetLocked();
+            callMediaSessionMethod(SET_CAPTIONING_ENABLED, true, getContext());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mMediaControllerCallback.mOnCaptioningEnabledChangedCalled);
+            assertEquals(true, mMediaControllerCallback.mCaptioningEnabled);
+            assertEquals(true, mController.isCaptioningEnabled());
+
+            mMediaControllerCallback.resetLocked();
+            callMediaSessionMethod(SET_CAPTIONING_ENABLED, false, getContext());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mMediaControllerCallback.mOnCaptioningEnabledChangedCalled);
+            assertEquals(false, mMediaControllerCallback.mCaptioningEnabled);
+            assertEquals(false, mController.isCaptioningEnabled());
+        }
+    }
+
+    /**
+     * Tests {@link MediaSessionCompat#setRepeatMode}.
+     */
+    @Test
+    @SmallTest
+    public void testSetRepeatMode() throws Exception {
+        synchronized (mWaitLock) {
+            mMediaControllerCallback.resetLocked();
+            final int repeatMode = PlaybackStateCompat.REPEAT_MODE_ALL;
+            callMediaSessionMethod(SET_REPEAT_MODE, repeatMode, getContext());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mMediaControllerCallback.mOnRepeatModeChangedCalled);
+            assertEquals(repeatMode, mMediaControllerCallback.mRepeatMode);
+            assertEquals(repeatMode, mController.getRepeatMode());
+        }
+    }
+
+    /**
+     * Tests {@link MediaSessionCompat#setShuffleMode}.
+     */
+    @Test
+    @SmallTest
+    public void testSetShuffleMode() throws Exception {
+        final int shuffleMode = PlaybackStateCompat.SHUFFLE_MODE_ALL;
+        synchronized (mWaitLock) {
+            mMediaControllerCallback.resetLocked();
+            callMediaSessionMethod(SET_SHUFFLE_MODE, shuffleMode, getContext());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mMediaControllerCallback.mOnShuffleModeChangedCalled);
+            assertEquals(shuffleMode, mMediaControllerCallback.mShuffleMode);
+            assertEquals(shuffleMode, mController.getShuffleMode());
+        }
+    }
+
+    /**
+     * Tests {@link MediaSessionCompat#sendSessionEvent}.
+     */
+    @Test
+    @SmallTest
+    public void testSendSessionEvent() throws Exception {
+        synchronized (mWaitLock) {
+            mMediaControllerCallback.resetLocked();
+
+            Bundle arguments = new Bundle();
+            arguments.putString("event", TEST_SESSION_EVENT);
+
+            Bundle extras = new Bundle();
+            extras.putString(TEST_KEY, TEST_VALUE);
+            arguments.putBundle("extras", extras);
+            callMediaSessionMethod(SEND_SESSION_EVENT, arguments, getContext());
+
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mMediaControllerCallback.mOnSessionEventCalled);
+            assertEquals(TEST_SESSION_EVENT, mMediaControllerCallback.mEvent);
+            assertBundleEquals(extras, mMediaControllerCallback.mExtras);
+        }
+    }
+
+    /**
+     * Tests {@link MediaSessionCompat#release}.
+     */
+    @Test
+    @SmallTest
+    public void testRelease() throws Exception {
+        synchronized (mWaitLock) {
+            mMediaControllerCallback.resetLocked();
+            callMediaSessionMethod(RELEASE, null, getContext());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mMediaControllerCallback.mOnSessionDestroyedCalled);
+        }
+    }
+
+    /**
+     * Tests {@link MediaSessionCompat#setPlaybackToLocal} and
+     * {@link MediaSessionCompat#setPlaybackToRemote}.
+     */
+    @LargeTest
+    public void testPlaybackToLocalAndRemote() throws Exception {
+        synchronized (mWaitLock) {
+            mMediaControllerCallback.resetLocked();
+            ParcelableVolumeInfo volumeInfo = new ParcelableVolumeInfo(
+                    MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE,
+                    STREAM_MUSIC,
+                    VolumeProviderCompat.VOLUME_CONTROL_FIXED,
+                    TEST_MAX_VOLUME,
+                    TEST_CURRENT_VOLUME);
+
+            callMediaSessionMethod(SET_PLAYBACK_TO_REMOTE, volumeInfo, getContext());
+            MediaControllerCompat.PlaybackInfo info = null;
+            for (int i = 0; i < MAX_AUDIO_INFO_CHANGED_CALLBACK_COUNT; ++i) {
+                mMediaControllerCallback.mOnAudioInfoChangedCalled = false;
+                mWaitLock.wait(TIME_OUT_MS);
+                assertTrue(mMediaControllerCallback.mOnAudioInfoChangedCalled);
+                info = mMediaControllerCallback.mPlaybackInfo;
+                if (info != null && info.getCurrentVolume() == TEST_CURRENT_VOLUME
+                        && info.getMaxVolume() == TEST_MAX_VOLUME
+                        && info.getVolumeControl() == VolumeProviderCompat.VOLUME_CONTROL_FIXED
+                        && info.getPlaybackType()
+                                == MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE) {
+                    break;
+                }
+            }
+            assertNotNull(info);
+            assertEquals(MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE,
+                    info.getPlaybackType());
+            assertEquals(TEST_MAX_VOLUME, info.getMaxVolume());
+            assertEquals(TEST_CURRENT_VOLUME, info.getCurrentVolume());
+            assertEquals(VolumeProviderCompat.VOLUME_CONTROL_FIXED,
+                    info.getVolumeControl());
+
+            info = mController.getPlaybackInfo();
+            assertNotNull(info);
+            assertEquals(MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE,
+                    info.getPlaybackType());
+            assertEquals(TEST_MAX_VOLUME, info.getMaxVolume());
+            assertEquals(TEST_CURRENT_VOLUME, info.getCurrentVolume());
+            assertEquals(VolumeProviderCompat.VOLUME_CONTROL_FIXED, info.getVolumeControl());
+
+            // test setPlaybackToLocal
+            mMediaControllerCallback.mOnAudioInfoChangedCalled = false;
+            callMediaSessionMethod(SET_PLAYBACK_TO_LOCAL, AudioManager.STREAM_RING, getContext());
+
+            // In API 21 and 22, onAudioInfoChanged is not called.
+            if (Build.VERSION.SDK_INT == 21 || Build.VERSION.SDK_INT == 22) {
+                Thread.sleep(TIME_OUT_MS);
+            } else {
+                mWaitLock.wait(TIME_OUT_MS);
+                assertTrue(mMediaControllerCallback.mOnAudioInfoChangedCalled);
+            }
+
+            info = mController.getPlaybackInfo();
+            assertNotNull(info);
+            assertEquals(MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_LOCAL,
+                    info.getPlaybackType());
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testGetRatingType() {
+        assertEquals("Default rating type of a session must be RatingCompat.RATING_NONE",
+                RatingCompat.RATING_NONE, mController.getRatingType());
+
+        callMediaSessionMethod(SET_RATING_TYPE, RatingCompat.RATING_5_STARS, getContext());
+        new PollingCheck(TIME_OUT_MS) {
+            @Override
+            public boolean check() {
+                return RatingCompat.RATING_5_STARS == mController.getRatingType();
+            }
+        }.run();
+    }
+
+    private void assertQueueEquals(List<QueueItem> expected, List<QueueItem> observed) {
+        if (expected == null || observed == null) {
+            assertTrue(expected == observed);
+            return;
+        }
+
+        assertEquals(expected.size(), observed.size());
+        for (int i = 0; i < expected.size(); i++) {
+            QueueItem expectedItem = expected.get(i);
+            QueueItem observedItem = observed.get(i);
+
+            assertEquals(expectedItem.getQueueId(), observedItem.getQueueId());
+            assertEquals(expectedItem.getDescription().getMediaId(),
+                    observedItem.getDescription().getMediaId());
+        }
+    }
+
+    private class MediaControllerCallback extends MediaControllerCompat.Callback {
+        private volatile boolean mOnPlaybackStateChangedCalled;
+        private volatile boolean mOnMetadataChangedCalled;
+        private volatile boolean mOnQueueChangedCalled;
+        private volatile boolean mOnQueueTitleChangedCalled;
+        private volatile boolean mOnExtraChangedCalled;
+        private volatile boolean mOnAudioInfoChangedCalled;
+        private volatile boolean mOnSessionDestroyedCalled;
+        private volatile boolean mOnSessionEventCalled;
+        private volatile boolean mOnCaptioningEnabledChangedCalled;
+        private volatile boolean mOnRepeatModeChangedCalled;
+        private volatile boolean mOnShuffleModeChangedCalled;
+
+        private volatile PlaybackStateCompat mPlaybackState;
+        private volatile MediaMetadataCompat mMediaMetadata;
+        private volatile List<QueueItem> mQueue;
+        private volatile CharSequence mTitle;
+        private volatile String mEvent;
+        private volatile Bundle mExtras;
+        private volatile MediaControllerCompat.PlaybackInfo mPlaybackInfo;
+        private volatile boolean mCaptioningEnabled;
+        private volatile int mRepeatMode;
+        private volatile int mShuffleMode;
+
+        public void resetLocked() {
+            mOnPlaybackStateChangedCalled = false;
+            mOnMetadataChangedCalled = false;
+            mOnQueueChangedCalled = false;
+            mOnQueueTitleChangedCalled = false;
+            mOnExtraChangedCalled = false;
+            mOnAudioInfoChangedCalled = false;
+            mOnSessionDestroyedCalled = false;
+            mOnSessionEventCalled = false;
+            mOnRepeatModeChangedCalled = false;
+            mOnShuffleModeChangedCalled = false;
+
+            mPlaybackState = null;
+            mMediaMetadata = null;
+            mQueue = null;
+            mTitle = null;
+            mExtras = null;
+            mPlaybackInfo = null;
+            mCaptioningEnabled = false;
+            mRepeatMode = PlaybackStateCompat.REPEAT_MODE_NONE;
+            mShuffleMode = PlaybackStateCompat.SHUFFLE_MODE_NONE;
+        }
+
+        @Override
+        public void onPlaybackStateChanged(PlaybackStateCompat state) {
+            synchronized (mWaitLock) {
+                mOnPlaybackStateChangedCalled = true;
+                mPlaybackState = state;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onMetadataChanged(MediaMetadataCompat metadata) {
+            synchronized (mWaitLock) {
+                mOnMetadataChangedCalled = true;
+                mMediaMetadata = metadata;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onQueueChanged(List<QueueItem> queue) {
+            synchronized (mWaitLock) {
+                mOnQueueChangedCalled = true;
+                mQueue = queue;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onQueueTitleChanged(CharSequence title) {
+            synchronized (mWaitLock) {
+                mOnQueueTitleChangedCalled = true;
+                mTitle = title;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onExtrasChanged(Bundle extras) {
+            synchronized (mWaitLock) {
+                mOnExtraChangedCalled = true;
+                mExtras = extras;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onAudioInfoChanged(MediaControllerCompat.PlaybackInfo info) {
+            synchronized (mWaitLock) {
+                mOnAudioInfoChangedCalled = true;
+                mPlaybackInfo = info;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onSessionDestroyed() {
+            synchronized (mWaitLock) {
+                mOnSessionDestroyedCalled = true;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onSessionEvent(String event, Bundle extras) {
+            synchronized (mWaitLock) {
+                mOnSessionEventCalled = true;
+                mEvent = event;
+                mExtras = (Bundle) extras.clone();
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onCaptioningEnabledChanged(boolean enabled) {
+            synchronized (mWaitLock) {
+                mOnCaptioningEnabledChangedCalled = true;
+                mCaptioningEnabled = enabled;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onRepeatModeChanged(int repeatMode) {
+            synchronized (mWaitLock) {
+                mOnRepeatModeChangedCalled = true;
+                mRepeatMode = repeatMode;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onShuffleModeChanged(int shuffleMode) {
+            synchronized (mWaitLock) {
+                mOnShuffleModeChangedCalled = true;
+                mShuffleMode = shuffleMode;
+                mWaitLock.notify();
+            }
+        }
+    }
+
+    private class ConnectionCallback extends MediaBrowserCompat.ConnectionCallback {
+        final Object mWaitLock = new Object();
+
+        @Override
+        public void onConnected() {
+            synchronized (mWaitLock) {
+                mWaitLock.notify();
+            }
+        }
+    }
+}
diff --git a/media-compat-test-service/AndroidManifest.xml b/media-compat/version-compat-tests/previous/service/AndroidManifest.xml
similarity index 92%
copy from media-compat-test-service/AndroidManifest.xml
copy to media-compat/version-compat-tests/previous/service/AndroidManifest.xml
index 0390e8a..5e25a83 100644
--- a/media-compat-test-service/AndroidManifest.xml
+++ b/media-compat/version-compat-tests/previous/service/AndroidManifest.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-   Copyright (C) 2017 The Android Open Source Project
+   Copyright 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.
diff --git a/media-compat-test-service/build.gradle b/media-compat/version-compat-tests/previous/service/build.gradle
similarity index 74%
copy from media-compat-test-service/build.gradle
copy to media-compat/version-compat-tests/previous/service/build.gradle
index 946d48b..03f95ce 100644
--- a/media-compat-test-service/build.gradle
+++ b/media-compat/version-compat-tests/previous/service/build.gradle
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2017 The Android Open Source Project
+ * Copyright 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.
@@ -19,13 +19,10 @@
 }
 
 dependencies {
-    androidTestImplementation project(':support-annotations')
-    androidTestImplementation project(':support-media-compat')
     androidTestImplementation project(':support-media-compat-test-lib')
+    androidTestImplementation "com.android.support:support-media-compat:27.0.1"
 
-    androidTestImplementation(libs.test_runner) {
-        exclude module: 'support-annotations'
-    }
+    androidTestImplementation(libs.test_runner)
 }
 
 android {
diff --git a/media-compat-test-service/AndroidManifest.xml b/media-compat/version-compat-tests/previous/service/lint-baseline.xml
similarity index 78%
copy from media-compat-test-service/AndroidManifest.xml
copy to media-compat/version-compat-tests/previous/service/lint-baseline.xml
index 0390e8a..ed7ade1 100644
--- a/media-compat-test-service/AndroidManifest.xml
+++ b/media-compat/version-compat-tests/previous/service/lint-baseline.xml
@@ -1,6 +1,6 @@
-<?xml version="1.0" encoding="utf-8"?>
+<?xml version="1.0" encoding="UTF-8"?>
 <!--
-   Copyright (C) 2017 The Android Open Source Project
+   Copyright 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.
@@ -14,4 +14,6 @@
    See the License for the specific language governing permissions and
    limitations under the License.
 -->
-<manifest package="android.support.mediacompat.service"/>
+<issues format="4" by="lint 3.0.0-alpha9">
+
+</issues>
diff --git a/media-compat-test-service/tests/AndroidManifest.xml b/media-compat/version-compat-tests/previous/service/tests/AndroidManifest.xml
similarity index 91%
copy from media-compat-test-service/tests/AndroidManifest.xml
copy to media-compat/version-compat-tests/previous/service/tests/AndroidManifest.xml
index 3e1eff9..b47eecf 100644
--- a/media-compat-test-service/tests/AndroidManifest.xml
+++ b/media-compat/version-compat-tests/previous/service/tests/AndroidManifest.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-   Copyright (C) 2017 The Android Open Source Project
+   Copyright 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.
@@ -20,6 +20,7 @@
         <receiver android:name="android.support.mediacompat.service.ServiceBroadcastReceiver">
             <intent-filter>
                 <action android:name="android.support.mediacompat.service.action.CALL_MEDIA_BROWSER_SERVICE_METHOD"/>
+                <action android:name="android.support.mediacompat.service.action.CALL_MEDIA_SESSION_METHOD"/>
             </intent-filter>
         </receiver>
 
diff --git a/design/jvm-tests/NO_DOCS b/media-compat/version-compat-tests/previous/service/tests/NO_DOCS
similarity index 92%
copy from design/jvm-tests/NO_DOCS
copy to media-compat/version-compat-tests/previous/service/tests/NO_DOCS
index 092a39c..61c9b1a 100644
--- a/design/jvm-tests/NO_DOCS
+++ b/media-compat/version-compat-tests/previous/service/tests/NO_DOCS
@@ -1,4 +1,4 @@
-# Copyright (C) 2016 The Android Open Source Project
+# Copyright 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.
diff --git a/media-compat/version-compat-tests/previous/service/tests/src/android/support/mediacompat/service/MediaSessionCompatCallbackTest.java b/media-compat/version-compat-tests/previous/service/tests/src/android/support/mediacompat/service/MediaSessionCompatCallbackTest.java
new file mode 100644
index 0000000..d36eba3
--- /dev/null
+++ b/media-compat/version-compat-tests/previous/service/tests/src/android/support/mediacompat/service/MediaSessionCompatCallbackTest.java
@@ -0,0 +1,720 @@
+/*
+ * Copyright 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.mediacompat.service;
+
+import static android.support.mediacompat.testlib.MediaControllerConstants.ADD_QUEUE_ITEM;
+import static android.support.mediacompat.testlib.MediaControllerConstants
+        .ADD_QUEUE_ITEM_WITH_INDEX;
+import static android.support.mediacompat.testlib.MediaControllerConstants.FAST_FORWARD;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PAUSE;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PLAY;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PLAY_FROM_MEDIA_ID;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PLAY_FROM_SEARCH;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PLAY_FROM_URI;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PREPARE;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PREPARE_FROM_MEDIA_ID;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PREPARE_FROM_SEARCH;
+import static android.support.mediacompat.testlib.MediaControllerConstants.PREPARE_FROM_URI;
+import static android.support.mediacompat.testlib.MediaControllerConstants.REMOVE_QUEUE_ITEM;
+import static android.support.mediacompat.testlib.MediaControllerConstants.REWIND;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SEEK_TO;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SEND_COMMAND;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SEND_CUSTOM_ACTION;
+import static android.support.mediacompat.testlib.MediaControllerConstants
+        .SEND_CUSTOM_ACTION_PARCELABLE;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SET_CAPTIONING_ENABLED;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SET_RATING;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SET_REPEAT_MODE;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SET_SHUFFLE_MODE;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SKIP_TO_NEXT;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SKIP_TO_PREVIOUS;
+import static android.support.mediacompat.testlib.MediaControllerConstants.SKIP_TO_QUEUE_ITEM;
+import static android.support.mediacompat.testlib.MediaControllerConstants.STOP;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_COMMAND;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_KEY;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_SESSION_TAG;
+import static android.support.mediacompat.testlib.MediaSessionConstants.TEST_VALUE;
+import static android.support.mediacompat.testlib.VersionConstants.KEY_CLIENT_VERSION;
+import static android.support.mediacompat.testlib.util.IntentUtil.callMediaControllerMethod;
+import static android.support.mediacompat.testlib.util.IntentUtil.callTransportControlsMethod;
+import static android.support.mediacompat.testlib.util.TestUtil.assertBundleEquals;
+import static android.support.test.InstrumentationRegistry.getArguments;
+import static android.support.test.InstrumentationRegistry.getContext;
+import static android.support.test.InstrumentationRegistry.getInstrumentation;
+import static android.support.test.InstrumentationRegistry.getTargetContext;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.ResultReceiver;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.media.MediaDescriptionCompat;
+import android.support.v4.media.RatingCompat;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.util.Log;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Test {@link MediaSessionCompat.Callback}.
+ */
+@RunWith(AndroidJUnit4.class)
+public class MediaSessionCompatCallbackTest {
+
+    private static final String TAG = "MediaSessionCompatCallbackTest";
+
+    // The maximum time to wait for an operation.
+    private static final long TIME_OUT_MS = 3000L;
+    private static final float DELTA = 1e-4f;
+    private static final boolean ENABLED = true;
+
+    private final Object mWaitLock = new Object();
+    private final Handler mHandler = new Handler(Looper.getMainLooper());
+    private String mClientVersion;
+    private MediaSessionCompat mSession;
+    private MediaSessionCallback mCallback = new MediaSessionCallback();
+
+    @Before
+    public void setUp() throws Exception {
+        // The version of the client app is provided through the instrumentation arguments.
+        mClientVersion = getArguments().getString(KEY_CLIENT_VERSION, "");
+        Log.d(TAG, "Client app version: " + mClientVersion);
+
+        getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mSession = new MediaSessionCompat(getTargetContext(), TEST_SESSION_TAG);
+                mSession.setCallback(mCallback, mHandler);
+                mSession.setFlags(MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS);
+            }
+        });
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mSession.release();
+    }
+
+    @Test
+    @SmallTest
+    public void testSendCommand() throws Exception {
+        synchronized (mWaitLock) {
+            mCallback.reset();
+
+            Bundle arguments = new Bundle();
+            arguments.putString("command", TEST_COMMAND);
+            Bundle extras = new Bundle();
+            extras.putString(TEST_KEY, TEST_VALUE);
+            arguments.putBundle("extras", extras);
+            callMediaControllerMethod(
+                    SEND_COMMAND, arguments, getContext(), mSession.getSessionToken());
+
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnCommandCalled);
+            assertNotNull(mCallback.mCommandCallback);
+            assertEquals(TEST_COMMAND, mCallback.mCommand);
+            assertBundleEquals(extras, mCallback.mExtras);
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testAddRemoveQueueItems() throws Exception {
+        final String mediaId1 = "media_id_1";
+        final String mediaTitle1 = "media_title_1";
+        MediaDescriptionCompat itemDescription1 = new MediaDescriptionCompat.Builder()
+                .setMediaId(mediaId1).setTitle(mediaTitle1).build();
+
+        final String mediaId2 = "media_id_2";
+        final String mediaTitle2 = "media_title_2";
+        MediaDescriptionCompat itemDescription2 = new MediaDescriptionCompat.Builder()
+                .setMediaId(mediaId2).setTitle(mediaTitle2).build();
+
+        synchronized (mWaitLock) {
+            mCallback.reset();
+            callMediaControllerMethod(
+                    ADD_QUEUE_ITEM, itemDescription1, getContext(), mSession.getSessionToken());
+
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnAddQueueItemCalled);
+            assertEquals(-1, mCallback.mQueueIndex);
+            assertEquals(mediaId1, mCallback.mQueueDescription.getMediaId());
+            assertEquals(mediaTitle1, mCallback.mQueueDescription.getTitle());
+
+            mCallback.reset();
+            Bundle arguments = new Bundle();
+            arguments.putParcelable("description", itemDescription2);
+            arguments.putInt("index", 0);
+            callMediaControllerMethod(
+                    ADD_QUEUE_ITEM_WITH_INDEX, arguments, getContext(), mSession.getSessionToken());
+
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnAddQueueItemAtCalled);
+            assertEquals(0, mCallback.mQueueIndex);
+            assertEquals(mediaId2, mCallback.mQueueDescription.getMediaId());
+            assertEquals(mediaTitle2, mCallback.mQueueDescription.getTitle());
+
+            mCallback.reset();
+            callMediaControllerMethod(
+                    REMOVE_QUEUE_ITEM, itemDescription1, getContext(), mSession.getSessionToken());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnRemoveQueueItemCalled);
+            assertEquals(mediaId1, mCallback.mQueueDescription.getMediaId());
+            assertEquals(mediaTitle1, mCallback.mQueueDescription.getTitle());
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testTransportControlsAndMediaSessionCallback() throws Exception {
+        synchronized (mWaitLock) {
+            mCallback.reset();
+            callTransportControlsMethod(PLAY, null, getContext(), mSession.getSessionToken());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnPlayCalled);
+
+            mCallback.reset();
+            callTransportControlsMethod(PAUSE, null, getContext(), mSession.getSessionToken());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnPauseCalled);
+
+            mCallback.reset();
+            callTransportControlsMethod(STOP, null, getContext(), mSession.getSessionToken());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnStopCalled);
+
+            mCallback.reset();
+            callTransportControlsMethod(
+                    FAST_FORWARD, null, getContext(), mSession.getSessionToken());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnFastForwardCalled);
+
+            mCallback.reset();
+            callTransportControlsMethod(REWIND, null, getContext(), mSession.getSessionToken());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnRewindCalled);
+
+            mCallback.reset();
+            callTransportControlsMethod(
+                    SKIP_TO_PREVIOUS, null, getContext(), mSession.getSessionToken());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnSkipToPreviousCalled);
+
+            mCallback.reset();
+            callTransportControlsMethod(
+                    SKIP_TO_NEXT, null, getContext(), mSession.getSessionToken());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnSkipToNextCalled);
+
+            mCallback.reset();
+            final long seekPosition = 1000;
+            callTransportControlsMethod(
+                    SEEK_TO, seekPosition, getContext(), mSession.getSessionToken());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnSeekToCalled);
+            assertEquals(seekPosition, mCallback.mSeekPosition);
+
+            mCallback.reset();
+            final RatingCompat rating =
+                    RatingCompat.newStarRating(RatingCompat.RATING_5_STARS, 3f);
+            callTransportControlsMethod(
+                    SET_RATING, rating, getContext(), mSession.getSessionToken());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnSetRatingCalled);
+            assertEquals(rating.getRatingStyle(), mCallback.mRating.getRatingStyle());
+            assertEquals(rating.getStarRating(), mCallback.mRating.getStarRating(), DELTA);
+
+            mCallback.reset();
+            final String mediaId = "test-media-id";
+            final Bundle extras = new Bundle();
+            extras.putString(TEST_KEY, TEST_VALUE);
+            Bundle arguments = new Bundle();
+            arguments.putString("mediaId", mediaId);
+            arguments.putBundle("extras", extras);
+            callTransportControlsMethod(
+                    PLAY_FROM_MEDIA_ID, arguments, getContext(), mSession.getSessionToken());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnPlayFromMediaIdCalled);
+            assertEquals(mediaId, mCallback.mMediaId);
+            assertBundleEquals(extras, mCallback.mExtras);
+
+            mCallback.reset();
+            final String query = "test-query";
+            arguments = new Bundle();
+            arguments.putString("query", query);
+            arguments.putBundle("extras", extras);
+            callTransportControlsMethod(
+                    PLAY_FROM_SEARCH, arguments, getContext(), mSession.getSessionToken());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnPlayFromSearchCalled);
+            assertEquals(query, mCallback.mQuery);
+            assertBundleEquals(extras, mCallback.mExtras);
+
+            mCallback.reset();
+            final Uri uri = Uri.parse("content://test/popcorn.mod");
+            arguments = new Bundle();
+            arguments.putParcelable("uri", uri);
+            arguments.putBundle("extras", extras);
+            callTransportControlsMethod(
+                    PLAY_FROM_URI, arguments, getContext(), mSession.getSessionToken());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnPlayFromUriCalled);
+            assertEquals(uri, mCallback.mUri);
+            assertBundleEquals(extras, mCallback.mExtras);
+
+            mCallback.reset();
+            final String action = "test-action";
+            arguments = new Bundle();
+            arguments.putString("action", action);
+            arguments.putBundle("extras", extras);
+            callTransportControlsMethod(
+                    SEND_CUSTOM_ACTION, arguments, getContext(), mSession.getSessionToken());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnCustomActionCalled);
+            assertEquals(action, mCallback.mAction);
+            assertBundleEquals(extras, mCallback.mExtras);
+
+            mCallback.reset();
+            mCallback.mOnCustomActionCalled = false;
+            final PlaybackStateCompat.CustomAction customAction =
+                    new PlaybackStateCompat.CustomAction.Builder(action, action, -1)
+                            .setExtras(extras)
+                            .build();
+            arguments = new Bundle();
+            arguments.putParcelable("action", customAction);
+            arguments.putBundle("extras", extras);
+            callTransportControlsMethod(
+                    SEND_CUSTOM_ACTION_PARCELABLE,
+                    arguments,
+                    getContext(),
+                    mSession.getSessionToken());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnCustomActionCalled);
+            assertEquals(action, mCallback.mAction);
+            assertBundleEquals(extras, mCallback.mExtras);
+
+            mCallback.reset();
+            final long queueItemId = 1000;
+            callTransportControlsMethod(
+                    SKIP_TO_QUEUE_ITEM, queueItemId, getContext(), mSession.getSessionToken());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnSkipToQueueItemCalled);
+            assertEquals(queueItemId, mCallback.mQueueItemId);
+
+            mCallback.reset();
+            callTransportControlsMethod(
+                    PREPARE, null, getContext(), mSession.getSessionToken());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnPrepareCalled);
+
+            mCallback.reset();
+            arguments = new Bundle();
+            arguments.putString("mediaId", mediaId);
+            arguments.putBundle("extras", extras);
+            callTransportControlsMethod(
+                    PREPARE_FROM_MEDIA_ID, arguments, getContext(), mSession.getSessionToken());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnPrepareFromMediaIdCalled);
+            assertEquals(mediaId, mCallback.mMediaId);
+            assertBundleEquals(extras, mCallback.mExtras);
+
+            mCallback.reset();
+            arguments = new Bundle();
+            arguments.putString("query", query);
+            arguments.putBundle("extras", extras);
+            callTransportControlsMethod(
+                    PREPARE_FROM_SEARCH, arguments, getContext(), mSession.getSessionToken());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnPrepareFromSearchCalled);
+            assertEquals(query, mCallback.mQuery);
+            assertBundleEquals(extras, mCallback.mExtras);
+
+            mCallback.reset();
+            arguments = new Bundle();
+            arguments.putParcelable("uri", uri);
+            arguments.putBundle("extras", extras);
+            callTransportControlsMethod(
+                    PREPARE_FROM_URI, arguments, getContext(), mSession.getSessionToken());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnPrepareFromUriCalled);
+            assertEquals(uri, mCallback.mUri);
+            assertBundleEquals(extras, mCallback.mExtras);
+
+            mCallback.reset();
+            callTransportControlsMethod(
+                    SET_CAPTIONING_ENABLED, ENABLED, getContext(), mSession.getSessionToken());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnSetCaptioningEnabledCalled);
+            assertEquals(ENABLED, mCallback.mCaptioningEnabled);
+
+            mCallback.reset();
+            final int repeatMode = PlaybackStateCompat.REPEAT_MODE_ALL;
+            callTransportControlsMethod(
+                    SET_REPEAT_MODE, repeatMode, getContext(), mSession.getSessionToken());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnSetRepeatModeCalled);
+            assertEquals(repeatMode, mCallback.mRepeatMode);
+
+            mCallback.reset();
+            final int shuffleMode = PlaybackStateCompat.SHUFFLE_MODE_ALL;
+            callTransportControlsMethod(
+                    SET_SHUFFLE_MODE, shuffleMode, getContext(), mSession.getSessionToken());
+            mWaitLock.wait(TIME_OUT_MS);
+            assertTrue(mCallback.mOnSetShuffleModeCalled);
+            assertEquals(shuffleMode, mCallback.mShuffleMode);
+        }
+    }
+
+    private class MediaSessionCallback extends MediaSessionCompat.Callback {
+        private long mSeekPosition;
+        private long mQueueItemId;
+        private RatingCompat mRating;
+        private String mMediaId;
+        private String mQuery;
+        private Uri mUri;
+        private String mAction;
+        private String mCommand;
+        private Bundle mExtras;
+        private ResultReceiver mCommandCallback;
+        private boolean mCaptioningEnabled;
+        private int mRepeatMode;
+        private int mShuffleMode;
+        private int mQueueIndex;
+        private MediaDescriptionCompat mQueueDescription;
+        private List<MediaSessionCompat.QueueItem> mQueue = new ArrayList<>();
+
+        private boolean mOnPlayCalled;
+        private boolean mOnPauseCalled;
+        private boolean mOnStopCalled;
+        private boolean mOnFastForwardCalled;
+        private boolean mOnRewindCalled;
+        private boolean mOnSkipToPreviousCalled;
+        private boolean mOnSkipToNextCalled;
+        private boolean mOnSeekToCalled;
+        private boolean mOnSkipToQueueItemCalled;
+        private boolean mOnSetRatingCalled;
+        private boolean mOnPlayFromMediaIdCalled;
+        private boolean mOnPlayFromSearchCalled;
+        private boolean mOnPlayFromUriCalled;
+        private boolean mOnCustomActionCalled;
+        private boolean mOnCommandCalled;
+        private boolean mOnPrepareCalled;
+        private boolean mOnPrepareFromMediaIdCalled;
+        private boolean mOnPrepareFromSearchCalled;
+        private boolean mOnPrepareFromUriCalled;
+        private boolean mOnSetCaptioningEnabledCalled;
+        private boolean mOnSetRepeatModeCalled;
+        private boolean mOnSetShuffleModeCalled;
+        private boolean mOnAddQueueItemCalled;
+        private boolean mOnAddQueueItemAtCalled;
+        private boolean mOnRemoveQueueItemCalled;
+
+        public void reset() {
+            mSeekPosition = -1;
+            mQueueItemId = -1;
+            mRating = null;
+            mMediaId = null;
+            mQuery = null;
+            mUri = null;
+            mAction = null;
+            mExtras = null;
+            mCommand = null;
+            mCommandCallback = null;
+            mCaptioningEnabled = false;
+            mRepeatMode = PlaybackStateCompat.REPEAT_MODE_NONE;
+            mShuffleMode = PlaybackStateCompat.SHUFFLE_MODE_NONE;
+            mQueueIndex = -1;
+            mQueueDescription = null;
+
+            mOnPlayCalled = false;
+            mOnPauseCalled = false;
+            mOnStopCalled = false;
+            mOnFastForwardCalled = false;
+            mOnRewindCalled = false;
+            mOnSkipToPreviousCalled = false;
+            mOnSkipToNextCalled = false;
+            mOnSkipToQueueItemCalled = false;
+            mOnSeekToCalled = false;
+            mOnSetRatingCalled = false;
+            mOnPlayFromMediaIdCalled = false;
+            mOnPlayFromSearchCalled = false;
+            mOnPlayFromUriCalled = false;
+            mOnCustomActionCalled = false;
+            mOnCommandCalled = false;
+            mOnPrepareCalled = false;
+            mOnPrepareFromMediaIdCalled = false;
+            mOnPrepareFromSearchCalled = false;
+            mOnPrepareFromUriCalled = false;
+            mOnSetCaptioningEnabledCalled = false;
+            mOnSetRepeatModeCalled = false;
+            mOnSetShuffleModeCalled = false;
+            mOnAddQueueItemCalled = false;
+            mOnAddQueueItemAtCalled = false;
+            mOnRemoveQueueItemCalled = false;
+        }
+
+        @Override
+        public void onPlay() {
+            synchronized (mWaitLock) {
+                mOnPlayCalled = true;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onPause() {
+            synchronized (mWaitLock) {
+                mOnPauseCalled = true;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onStop() {
+            synchronized (mWaitLock) {
+                mOnStopCalled = true;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onFastForward() {
+            synchronized (mWaitLock) {
+                mOnFastForwardCalled = true;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onRewind() {
+            synchronized (mWaitLock) {
+                mOnRewindCalled = true;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onSkipToPrevious() {
+            synchronized (mWaitLock) {
+                mOnSkipToPreviousCalled = true;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onSkipToNext() {
+            synchronized (mWaitLock) {
+                mOnSkipToNextCalled = true;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onSeekTo(long pos) {
+            synchronized (mWaitLock) {
+                mOnSeekToCalled = true;
+                mSeekPosition = pos;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onSetRating(RatingCompat rating) {
+            synchronized (mWaitLock) {
+                mOnSetRatingCalled = true;
+                mRating = rating;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onPlayFromMediaId(String mediaId, Bundle extras) {
+            synchronized (mWaitLock) {
+                mOnPlayFromMediaIdCalled = true;
+                mMediaId = mediaId;
+                mExtras = extras;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onPlayFromSearch(String query, Bundle extras) {
+            synchronized (mWaitLock) {
+                mOnPlayFromSearchCalled = true;
+                mQuery = query;
+                mExtras = extras;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onPlayFromUri(Uri uri, Bundle extras) {
+            synchronized (mWaitLock) {
+                mOnPlayFromUriCalled = true;
+                mUri = uri;
+                mExtras = extras;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onCustomAction(String action, Bundle extras) {
+            synchronized (mWaitLock) {
+                mOnCustomActionCalled = true;
+                mAction = action;
+                mExtras = extras;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onSkipToQueueItem(long id) {
+            synchronized (mWaitLock) {
+                mOnSkipToQueueItemCalled = true;
+                mQueueItemId = id;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onCommand(String command, Bundle extras, ResultReceiver cb) {
+            synchronized (mWaitLock) {
+                mOnCommandCalled = true;
+                mCommand = command;
+                mExtras = extras;
+                mCommandCallback = cb;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onPrepare() {
+            synchronized (mWaitLock) {
+                mOnPrepareCalled = true;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onPrepareFromMediaId(String mediaId, Bundle extras) {
+            synchronized (mWaitLock) {
+                mOnPrepareFromMediaIdCalled = true;
+                mMediaId = mediaId;
+                mExtras = extras;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onPrepareFromSearch(String query, Bundle extras) {
+            synchronized (mWaitLock) {
+                mOnPrepareFromSearchCalled = true;
+                mQuery = query;
+                mExtras = extras;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onPrepareFromUri(Uri uri, Bundle extras) {
+            synchronized (mWaitLock) {
+                mOnPrepareFromUriCalled = true;
+                mUri = uri;
+                mExtras = extras;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onSetRepeatMode(int repeatMode) {
+            synchronized (mWaitLock) {
+                mOnSetRepeatModeCalled = true;
+                mRepeatMode = repeatMode;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onAddQueueItem(MediaDescriptionCompat description) {
+            synchronized (mWaitLock) {
+                mOnAddQueueItemCalled = true;
+                mQueueDescription = description;
+                mQueue.add(new MediaSessionCompat.QueueItem(description, mQueue.size()));
+                mSession.setQueue(mQueue);
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onAddQueueItem(MediaDescriptionCompat description, int index) {
+            synchronized (mWaitLock) {
+                mOnAddQueueItemAtCalled = true;
+                mQueueIndex = index;
+                mQueueDescription = description;
+                mQueue.add(index, new MediaSessionCompat.QueueItem(description, mQueue.size()));
+                mSession.setQueue(mQueue);
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onRemoveQueueItem(MediaDescriptionCompat description) {
+            synchronized (mWaitLock) {
+                mOnRemoveQueueItemCalled = true;
+                String mediaId = description.getMediaId();
+                for (int i = mQueue.size() - 1; i >= 0; --i) {
+                    if (mediaId.equals(mQueue.get(i).getDescription().getMediaId())) {
+                        mQueueDescription = mQueue.remove(i).getDescription();
+                        mSession.setQueue(mQueue);
+                        break;
+                    }
+                }
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onSetCaptioningEnabled(boolean enabled) {
+            synchronized (mWaitLock) {
+                mOnSetCaptioningEnabledCalled = true;
+                mCaptioningEnabled = enabled;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onSetShuffleMode(int shuffleMode) {
+            synchronized (mWaitLock) {
+                mOnSetShuffleModeCalled = true;
+                mShuffleMode = shuffleMode;
+                mWaitLock.notify();
+            }
+        }
+    }
+}
diff --git a/media-compat/version-compat-tests/previous/service/tests/src/android/support/mediacompat/service/ServiceBroadcastReceiver.java b/media-compat/version-compat-tests/previous/service/tests/src/android/support/mediacompat/service/ServiceBroadcastReceiver.java
new file mode 100644
index 0000000..57364b7
--- /dev/null
+++ b/media-compat/version-compat-tests/previous/service/tests/src/android/support/mediacompat/service/ServiceBroadcastReceiver.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright 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.mediacompat.service;
+
+
+import static android.support.mediacompat.testlib.MediaBrowserConstants.CUSTOM_ACTION_SEND_ERROR;
+import static android.support.mediacompat.testlib.MediaBrowserConstants
+        .CUSTOM_ACTION_SEND_PROGRESS_UPDATE;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.CUSTOM_ACTION_SEND_RESULT;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.NOTIFY_CHILDREN_CHANGED;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.SEND_DELAYED_ITEM_LOADED;
+import static android.support.mediacompat.testlib.MediaBrowserConstants
+        .SEND_DELAYED_NOTIFY_CHILDREN_CHANGED;
+import static android.support.mediacompat.testlib.MediaBrowserConstants.SET_SESSION_TOKEN;
+import static android.support.mediacompat.testlib.MediaSessionConstants.RELEASE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SEND_SESSION_EVENT;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_ACTIVE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_CAPTIONING_ENABLED;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_EXTRAS;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_FLAGS;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_METADATA;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_PLAYBACK_STATE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_PLAYBACK_TO_LOCAL;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_PLAYBACK_TO_REMOTE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_QUEUE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_QUEUE_TITLE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_RATING_TYPE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_REPEAT_MODE;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_SESSION_ACTIVITY;
+import static android.support.mediacompat.testlib.MediaSessionConstants.SET_SHUFFLE_MODE;
+import static android.support.mediacompat.testlib.util.IntentUtil
+        .ACTION_CALL_MEDIA_BROWSER_SERVICE_METHOD;
+import static android.support.mediacompat.testlib.util.IntentUtil.ACTION_CALL_MEDIA_SESSION_METHOD;
+import static android.support.mediacompat.testlib.util.IntentUtil.KEY_ARGUMENT;
+import static android.support.mediacompat.testlib.util.IntentUtil.KEY_METHOD_ID;
+
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.RatingCompat;
+import android.support.v4.media.VolumeProviderCompat;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.MediaSessionCompat.QueueItem;
+import android.support.v4.media.session.ParcelableVolumeInfo;
+import android.support.v4.media.session.PlaybackStateCompat;
+
+import java.util.List;
+
+public class ServiceBroadcastReceiver extends BroadcastReceiver {
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        Bundle extras = intent.getExtras();
+        if (ACTION_CALL_MEDIA_BROWSER_SERVICE_METHOD.equals(intent.getAction()) && extras != null) {
+            StubMediaBrowserServiceCompat service = StubMediaBrowserServiceCompat.sInstance;
+            int method = extras.getInt(KEY_METHOD_ID, 0);
+
+            switch (method) {
+                case NOTIFY_CHILDREN_CHANGED:
+                    service.notifyChildrenChanged(extras.getString(KEY_ARGUMENT));
+                    break;
+                case SEND_DELAYED_NOTIFY_CHILDREN_CHANGED:
+                    service.sendDelayedNotifyChildrenChanged();
+                    break;
+                case SEND_DELAYED_ITEM_LOADED:
+                    service.sendDelayedItemLoaded();
+                    break;
+                case CUSTOM_ACTION_SEND_PROGRESS_UPDATE:
+                    service.mCustomActionResult.sendProgressUpdate(extras.getBundle(KEY_ARGUMENT));
+                    break;
+                case CUSTOM_ACTION_SEND_ERROR:
+                    service.mCustomActionResult.sendError(extras.getBundle(KEY_ARGUMENT));
+                    break;
+                case CUSTOM_ACTION_SEND_RESULT:
+                    service.mCustomActionResult.sendResult(extras.getBundle(KEY_ARGUMENT));
+                    break;
+                case SET_SESSION_TOKEN:
+                    StubMediaBrowserServiceCompatWithDelayedMediaSession.sInstance
+                            .callSetSessionToken();
+                    break;
+            }
+        } else if (ACTION_CALL_MEDIA_SESSION_METHOD.equals(intent.getAction()) && extras != null) {
+            MediaSessionCompat session = StubMediaBrowserServiceCompat.sSession;
+            int method = extras.getInt(KEY_METHOD_ID, 0);
+
+            switch (method) {
+                case SET_EXTRAS:
+                    session.setExtras(extras.getBundle(KEY_ARGUMENT));
+                    break;
+                case SET_FLAGS:
+                    session.setFlags(extras.getInt(KEY_ARGUMENT));
+                    break;
+                case SET_METADATA:
+                    session.setMetadata((MediaMetadataCompat) extras.getParcelable(KEY_ARGUMENT));
+                    break;
+                case SET_PLAYBACK_STATE:
+                    session.setPlaybackState(
+                            (PlaybackStateCompat) extras.getParcelable(KEY_ARGUMENT));
+                    break;
+                case SET_QUEUE:
+                    List<QueueItem> items = extras.getParcelableArrayList(KEY_ARGUMENT);
+                    session.setQueue(items);
+                    break;
+                case SET_QUEUE_TITLE:
+                    session.setQueueTitle(extras.getCharSequence(KEY_ARGUMENT));
+                    break;
+                case SET_SESSION_ACTIVITY:
+                    session.setSessionActivity((PendingIntent) extras.getParcelable(KEY_ARGUMENT));
+                    break;
+                case SET_CAPTIONING_ENABLED:
+                    session.setCaptioningEnabled(extras.getBoolean(KEY_ARGUMENT));
+                    break;
+                case SET_REPEAT_MODE:
+                    session.setRepeatMode(extras.getInt(KEY_ARGUMENT));
+                    break;
+                case SET_SHUFFLE_MODE:
+                    session.setShuffleMode(extras.getInt(KEY_ARGUMENT));
+                    break;
+                case SEND_SESSION_EVENT:
+                    Bundle arguments = extras.getBundle(KEY_ARGUMENT);
+                    session.sendSessionEvent(
+                            arguments.getString("event"), arguments.getBundle("extras"));
+                    break;
+                case SET_ACTIVE:
+                    session.setActive(extras.getBoolean(KEY_ARGUMENT));
+                    break;
+                case RELEASE:
+                    session.release();
+                    break;
+                case SET_PLAYBACK_TO_LOCAL:
+                    session.setPlaybackToLocal(extras.getInt(KEY_ARGUMENT));
+                    break;
+                case SET_PLAYBACK_TO_REMOTE:
+                    ParcelableVolumeInfo volumeInfo = extras.getParcelable(KEY_ARGUMENT);
+                    session.setPlaybackToRemote(new VolumeProviderCompat(
+                            volumeInfo.controlType,
+                            volumeInfo.maxVolume,
+                            volumeInfo.currentVolume) {});
+                    break;
+                case SET_RATING_TYPE:
+                    session.setRatingType(RatingCompat.RATING_5_STARS);
+                    break;
+            }
+        }
+    }
+}
diff --git a/media-compat-test-service/tests/src/android/support/mediacompat/service/StubMediaBrowserServiceCompat.java b/media-compat/version-compat-tests/previous/service/tests/src/android/support/mediacompat/service/StubMediaBrowserServiceCompat.java
similarity index 98%
copy from media-compat-test-service/tests/src/android/support/mediacompat/service/StubMediaBrowserServiceCompat.java
copy to media-compat/version-compat-tests/previous/service/tests/src/android/support/mediacompat/service/StubMediaBrowserServiceCompat.java
index fa9f1c5..61c33f8 100644
--- a/media-compat-test-service/tests/src/android/support/mediacompat/service/StubMediaBrowserServiceCompat.java
+++ b/media-compat/version-compat-tests/previous/service/tests/src/android/support/mediacompat/service/StubMediaBrowserServiceCompat.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2017 The Android Open Source Project
+ * Copyright 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.
diff --git a/media-compat-test-service/tests/src/android/support/mediacompat/service/StubMediaBrowserServiceCompatWithDelayedMediaSession.java b/media-compat/version-compat-tests/previous/service/tests/src/android/support/mediacompat/service/StubMediaBrowserServiceCompatWithDelayedMediaSession.java
similarity index 97%
copy from media-compat-test-service/tests/src/android/support/mediacompat/service/StubMediaBrowserServiceCompatWithDelayedMediaSession.java
copy to media-compat/version-compat-tests/previous/service/tests/src/android/support/mediacompat/service/StubMediaBrowserServiceCompatWithDelayedMediaSession.java
index 12cb358..509e13f 100644
--- a/media-compat-test-service/tests/src/android/support/mediacompat/service/StubMediaBrowserServiceCompatWithDelayedMediaSession.java
+++ b/media-compat/version-compat-tests/previous/service/tests/src/android/support/mediacompat/service/StubMediaBrowserServiceCompatWithDelayedMediaSession.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2017 The Android Open Source Project
+ * Copyright 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.
diff --git a/media-compat/version-compat-tests/runtest.sh b/media-compat/version-compat-tests/runtest.sh
new file mode 100755
index 0000000..d1a3c3a
--- /dev/null
+++ b/media-compat/version-compat-tests/runtest.sh
@@ -0,0 +1,105 @@
+#!/bin/bash
+# Copyright 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.
+
+# A script that runs media-compat-test between different versions.
+#
+# Preconditions:
+#  - Exactly one test device should be connected.
+#
+# TODO:
+#  - The test result should be easily seen. (Can we report the results to the Sponge?)
+#  - Run specific combination of the test (e.g. Only want to test ToT-ToT)
+#  - Run specific test class / method by using argument.
+#  - Support simultaneous multiple device connection
+
+# Usage './runtest.sh'
+
+CLIENT_MODULE_NAME_BASE="support-media-compat-test-client"
+SERVICE_MODULE_NAME_BASE="support-media-compat-test-service"
+CLIENT_VERSION=""
+SERVICE_VERSION=""
+
+function runTest() {
+  echo "Running test: Client-$CLIENT_VERSION / Service-$SERVICE_VERSION"
+
+  local CLIENT_MODULE_NAME="$CLIENT_MODULE_NAME_BASE$([ "$CLIENT_VERSION" = "tot" ] || echo "-previous")"
+  local SERVICE_MODULE_NAME="$SERVICE_MODULE_NAME_BASE$([ "$SERVICE_VERSION" = "tot" ] || echo "-previous")"
+
+  # Build test apks
+  ./gradlew $CLIENT_MODULE_NAME:assembleDebugAndroidTest || (echo "Build failed. Aborting."; return 1)
+  ./gradlew $SERVICE_MODULE_NAME:assembleDebugAndroidTest || (echo "Build failed. Aborting."; return 1)
+
+  # Install the apks
+  adb install -r -d "../../out/dist/$CLIENT_MODULE_NAME.apk" || (echo "Apk installation failed. Aborting."; return 1)
+  adb install -r -d "../../out/dist/$SERVICE_MODULE_NAME.apk" || (echo "Apk installation failed. Aborting."; return 1)
+
+  # Run the tests
+  echo ">>>>>>>>>>>>>>>>>>>>>>>> Test Started: Client-$CLIENT_VERSION & Service-$SERVICE_VERSION <<<<<<<<<<<<<<<<<<<<<<<<"
+  adb shell am instrument -w -r -e package android.support.mediacompat.client -e debug false -e client_version $CLIENT_VERSION \
+     -e service_version $SERVICE_VERSION android.support.mediacompat.client.test/android.support.test.runner.AndroidJUnitRunner
+  adb shell am instrument -w -r -e package android.support.mediacompat.service -e debug false -e client_version $CLIENT_VERSION \
+     -e service_version $SERVICE_VERSION android.support.mediacompat.service.test/android.support.test.runner.AndroidJUnitRunner
+  echo ">>>>>>>>>>>>>>>>>>>>>>>> Test Ended: Client-$CLIENT_VERSION & Service-$SERVICE_VERSION <<<<<<<<<<<<<<<<<<<<<<<<<<"
+}
+
+
+OLD_PWD=$(pwd)
+if [[ $OLD_PWD != *"frameworks/support"* ]]; then
+  echo "Current working directory is" $OLD_PWD.
+  echo "Please re-run this script in any folder under frameworks/support."
+  exit 1;
+else
+  # Change working directory to frameworks/support
+  cd "$(echo $OLD_PWD | awk -F'frameworks/support' '{print $1}')"/frameworks/support
+fi
+
+echo "Choose the support library versions of the test you want to run:"
+echo "    1. Client-ToT             / Service-ToT"
+echo "    2. Client-ToT             / Service-Latest release"
+echo "    3. Client-Latest release  / Service-ToT"
+echo "    4. Run all of the above"
+printf "Pick one of them: "
+
+read ANSWER
+case $ANSWER in
+  1)
+     CLIENT_VERSION="tot"
+     SERVICE_VERSION="tot"
+     runTest
+     ;;
+  2)
+     CLIENT_VERSION="tot"
+     SERVICE_VERSION="previous"
+     runTest
+     ;;
+  3)
+     CLIENT_VERSION="previous"
+     SERVICE_VERSION="tot"
+     runTest
+     ;;
+  4)
+     CLIENT_VERSION="tot"
+     SERVICE_VERSION="tot"
+     runTest
+
+     CLIENT_VERSION="tot"
+     SERVICE_VERSION="previous"
+     runTest
+
+     CLIENT_VERSION="previous"
+     SERVICE_VERSION="tot"
+     runTest
+     ;;
+esac
diff --git a/v17/preference-leanback/Android.mk b/preference-leanback/Android.mk
similarity index 100%
rename from v17/preference-leanback/Android.mk
rename to preference-leanback/Android.mk
diff --git a/v17/preference-leanback/AndroidManifest.xml b/preference-leanback/AndroidManifest.xml
similarity index 100%
rename from v17/preference-leanback/AndroidManifest.xml
rename to preference-leanback/AndroidManifest.xml
diff --git a/v17/preference-leanback/OWNERS b/preference-leanback/OWNERS
similarity index 100%
rename from v17/preference-leanback/OWNERS
rename to preference-leanback/OWNERS
diff --git a/v17/preference-leanback/api/26.0.0.txt b/preference-leanback/api/26.0.0.txt
similarity index 100%
rename from v17/preference-leanback/api/26.0.0.txt
rename to preference-leanback/api/26.0.0.txt
diff --git a/v17/preference-leanback/api/26.1.0.txt b/preference-leanback/api/26.1.0.txt
similarity index 100%
rename from v17/preference-leanback/api/26.1.0.txt
rename to preference-leanback/api/26.1.0.txt
diff --git a/v17/preference-leanback/api/27.0.0.txt b/preference-leanback/api/27.0.0.txt
similarity index 100%
rename from v17/preference-leanback/api/27.0.0.txt
rename to preference-leanback/api/27.0.0.txt
diff --git a/v17/preference-leanback/api/current.txt b/preference-leanback/api/current.txt
similarity index 100%
rename from v17/preference-leanback/api/current.txt
rename to preference-leanback/api/current.txt
diff --git a/v17/preference-leanback/api21/android/support/v17/internal/widget/OutlineOnlyWithChildrenFrameLayout.java b/preference-leanback/api21/android/support/v17/internal/widget/OutlineOnlyWithChildrenFrameLayout.java
similarity index 100%
rename from v17/preference-leanback/api21/android/support/v17/internal/widget/OutlineOnlyWithChildrenFrameLayout.java
rename to preference-leanback/api21/android/support/v17/internal/widget/OutlineOnlyWithChildrenFrameLayout.java
diff --git a/v17/preference-leanback/api21/android/support/v17/preference/LeanbackPreferenceFragmentTransitionHelperApi21.java b/preference-leanback/api21/android/support/v17/preference/LeanbackPreferenceFragmentTransitionHelperApi21.java
similarity index 100%
rename from v17/preference-leanback/api21/android/support/v17/preference/LeanbackPreferenceFragmentTransitionHelperApi21.java
rename to preference-leanback/api21/android/support/v17/preference/LeanbackPreferenceFragmentTransitionHelperApi21.java
diff --git a/v17/preference-leanback/build.gradle b/preference-leanback/build.gradle
similarity index 100%
rename from v17/preference-leanback/build.gradle
rename to preference-leanback/build.gradle
diff --git a/v17/preference-leanback/lint-baseline.xml b/preference-leanback/lint-baseline.xml
similarity index 100%
rename from v17/preference-leanback/lint-baseline.xml
rename to preference-leanback/lint-baseline.xml
diff --git a/v17/preference-leanback/res/color/lb_preference_item_primary_text_color.xml b/preference-leanback/res/color/lb_preference_item_primary_text_color.xml
similarity index 100%
rename from v17/preference-leanback/res/color/lb_preference_item_primary_text_color.xml
rename to preference-leanback/res/color/lb_preference_item_primary_text_color.xml
diff --git a/v17/preference-leanback/res/color/lb_preference_item_secondary_text_color.xml b/preference-leanback/res/color/lb_preference_item_secondary_text_color.xml
similarity index 100%
rename from v17/preference-leanback/res/color/lb_preference_item_secondary_text_color.xml
rename to preference-leanback/res/color/lb_preference_item_secondary_text_color.xml
diff --git a/v17/preference-leanback/res/layout-v21/leanback_preference_category.xml b/preference-leanback/res/layout-v21/leanback_preference_category.xml
similarity index 100%
rename from v17/preference-leanback/res/layout-v21/leanback_preference_category.xml
rename to preference-leanback/res/layout-v21/leanback_preference_category.xml
diff --git a/v17/preference-leanback/res/layout-v21/leanback_settings_fragment.xml b/preference-leanback/res/layout-v21/leanback_settings_fragment.xml
similarity index 100%
rename from v17/preference-leanback/res/layout-v21/leanback_settings_fragment.xml
rename to preference-leanback/res/layout-v21/leanback_settings_fragment.xml
diff --git a/v17/preference-leanback/res/layout/leanback_list_preference_fragment.xml b/preference-leanback/res/layout/leanback_list_preference_fragment.xml
similarity index 100%
rename from v17/preference-leanback/res/layout/leanback_list_preference_fragment.xml
rename to preference-leanback/res/layout/leanback_list_preference_fragment.xml
diff --git a/v17/preference-leanback/res/layout/leanback_list_preference_item_multi.xml b/preference-leanback/res/layout/leanback_list_preference_item_multi.xml
similarity index 100%
rename from v17/preference-leanback/res/layout/leanback_list_preference_item_multi.xml
rename to preference-leanback/res/layout/leanback_list_preference_item_multi.xml
diff --git a/v17/preference-leanback/res/layout/leanback_list_preference_item_single.xml b/preference-leanback/res/layout/leanback_list_preference_item_single.xml
similarity index 100%
rename from v17/preference-leanback/res/layout/leanback_list_preference_item_single.xml
rename to preference-leanback/res/layout/leanback_list_preference_item_single.xml
diff --git a/v17/preference-leanback/res/layout/leanback_preference.xml b/preference-leanback/res/layout/leanback_preference.xml
similarity index 100%
rename from v17/preference-leanback/res/layout/leanback_preference.xml
rename to preference-leanback/res/layout/leanback_preference.xml
diff --git a/v17/preference-leanback/res/layout/leanback_preference_category.xml b/preference-leanback/res/layout/leanback_preference_category.xml
similarity index 100%
rename from v17/preference-leanback/res/layout/leanback_preference_category.xml
rename to preference-leanback/res/layout/leanback_preference_category.xml
diff --git a/v17/preference-leanback/res/layout/leanback_preference_fragment.xml b/preference-leanback/res/layout/leanback_preference_fragment.xml
similarity index 100%
rename from v17/preference-leanback/res/layout/leanback_preference_fragment.xml
rename to preference-leanback/res/layout/leanback_preference_fragment.xml
diff --git a/v17/preference-leanback/res/layout/leanback_preference_information.xml b/preference-leanback/res/layout/leanback_preference_information.xml
similarity index 100%
rename from v17/preference-leanback/res/layout/leanback_preference_information.xml
rename to preference-leanback/res/layout/leanback_preference_information.xml
diff --git a/v17/preference-leanback/res/layout/leanback_preference_widget_seekbar.xml b/preference-leanback/res/layout/leanback_preference_widget_seekbar.xml
similarity index 100%
rename from v17/preference-leanback/res/layout/leanback_preference_widget_seekbar.xml
rename to preference-leanback/res/layout/leanback_preference_widget_seekbar.xml
diff --git a/v17/preference-leanback/res/layout/leanback_preferences_list.xml b/preference-leanback/res/layout/leanback_preferences_list.xml
similarity index 100%
rename from v17/preference-leanback/res/layout/leanback_preferences_list.xml
rename to preference-leanback/res/layout/leanback_preferences_list.xml
diff --git a/v17/preference-leanback/res/layout/leanback_settings_fragment.xml b/preference-leanback/res/layout/leanback_settings_fragment.xml
similarity index 100%
rename from v17/preference-leanback/res/layout/leanback_settings_fragment.xml
rename to preference-leanback/res/layout/leanback_settings_fragment.xml
diff --git a/v17/preference-leanback/res/values/colors.xml b/preference-leanback/res/values/colors.xml
similarity index 100%
rename from v17/preference-leanback/res/values/colors.xml
rename to preference-leanback/res/values/colors.xml
diff --git a/v17/preference-leanback/res/values/dimens.xml b/preference-leanback/res/values/dimens.xml
similarity index 100%
rename from v17/preference-leanback/res/values/dimens.xml
rename to preference-leanback/res/values/dimens.xml
diff --git a/v17/preference-leanback/res/values/styles.xml b/preference-leanback/res/values/styles.xml
similarity index 100%
rename from v17/preference-leanback/res/values/styles.xml
rename to preference-leanback/res/values/styles.xml
diff --git a/v17/preference-leanback/res/values/themes.xml b/preference-leanback/res/values/themes.xml
similarity index 100%
rename from v17/preference-leanback/res/values/themes.xml
rename to preference-leanback/res/values/themes.xml
diff --git a/v17/preference-leanback/src/android/support/v17/preference/BaseLeanbackPreferenceFragment.java b/preference-leanback/src/android/support/v17/preference/BaseLeanbackPreferenceFragment.java
similarity index 100%
rename from v17/preference-leanback/src/android/support/v17/preference/BaseLeanbackPreferenceFragment.java
rename to preference-leanback/src/android/support/v17/preference/BaseLeanbackPreferenceFragment.java
diff --git a/v17/preference-leanback/src/android/support/v17/preference/LeanbackListPreferenceDialogFragment.java b/preference-leanback/src/android/support/v17/preference/LeanbackListPreferenceDialogFragment.java
similarity index 100%
rename from v17/preference-leanback/src/android/support/v17/preference/LeanbackListPreferenceDialogFragment.java
rename to preference-leanback/src/android/support/v17/preference/LeanbackListPreferenceDialogFragment.java
diff --git a/v17/preference-leanback/src/android/support/v17/preference/LeanbackPreferenceDialogFragment.java b/preference-leanback/src/android/support/v17/preference/LeanbackPreferenceDialogFragment.java
similarity index 100%
rename from v17/preference-leanback/src/android/support/v17/preference/LeanbackPreferenceDialogFragment.java
rename to preference-leanback/src/android/support/v17/preference/LeanbackPreferenceDialogFragment.java
diff --git a/v17/preference-leanback/src/android/support/v17/preference/LeanbackPreferenceFragment.java b/preference-leanback/src/android/support/v17/preference/LeanbackPreferenceFragment.java
similarity index 100%
rename from v17/preference-leanback/src/android/support/v17/preference/LeanbackPreferenceFragment.java
rename to preference-leanback/src/android/support/v17/preference/LeanbackPreferenceFragment.java
diff --git a/v17/preference-leanback/src/android/support/v17/preference/LeanbackSettingsFragment.java b/preference-leanback/src/android/support/v17/preference/LeanbackSettingsFragment.java
similarity index 100%
rename from v17/preference-leanback/src/android/support/v17/preference/LeanbackSettingsFragment.java
rename to preference-leanback/src/android/support/v17/preference/LeanbackSettingsFragment.java
diff --git a/v17/preference-leanback/src/android/support/v17/preference/LeanbackSettingsRootView.java b/preference-leanback/src/android/support/v17/preference/LeanbackSettingsRootView.java
similarity index 100%
rename from v17/preference-leanback/src/android/support/v17/preference/LeanbackSettingsRootView.java
rename to preference-leanback/src/android/support/v17/preference/LeanbackSettingsRootView.java
diff --git a/recyclerview-selection/Android.mk b/recyclerview-selection/Android.mk
new file mode 100644
index 0000000..ed93fa2
--- /dev/null
+++ b/recyclerview-selection/Android.mk
@@ -0,0 +1,30 @@
+# Copyright 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)
+
+include $(CLEAR_VARS)
+LOCAL_USE_AAPT2 := true
+LOCAL_MODULE := android-support-recyclerview-selection
+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-v7-recyclerview \
+    android-support-compat \
+    android-support-annotations
+LOCAL_JAR_EXCLUDE_FILES := none
+LOCAL_JAVA_LANGUAGE_VERSION := 1.7
+LOCAL_AAPT_FLAGS := --add-javadoc-annotation doconly
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/recyclerview-selection/AndroidManifest.xml b/recyclerview-selection/AndroidManifest.xml
new file mode 100644
index 0000000..320ae3a
--- /dev/null
+++ b/recyclerview-selection/AndroidManifest.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 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="androidx.recyclerview.selection">
+    <uses-sdk android:minSdkVersion="14" />
+</manifest>
diff --git a/recyclerview-selection/build.gradle b/recyclerview-selection/build.gradle
new file mode 100644
index 0000000..ab1ab23
--- /dev/null
+++ b/recyclerview-selection/build.gradle
@@ -0,0 +1,49 @@
+/*
+ * Copyright 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.
+ */
+
+plugins {
+    id("SupportAndroidLibraryPlugin")
+}
+
+dependencies {
+    api project(':recyclerview-v7')
+    api project(':support-annotations')
+    api project(':support-compat')
+
+    androidTestImplementation libs.junit
+    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 14
+    }
+    sourceSets {
+        main.res.srcDirs 'res', 'res-public'
+    }
+}
+
+supportLibrary {
+    name 'Android RecyclerView Selection'
+    publish false
+    legacySourceLocation true
+    inceptionYear '2017'
+    description 'Library providing item selection framework for RecyclerView. Support for single and multi selection is provided.'
+}
diff --git a/media-compat-test-client/lint-baseline.xml b/recyclerview-selection/lint-baseline.xml
similarity index 100%
copy from media-compat-test-client/lint-baseline.xml
copy to recyclerview-selection/lint-baseline.xml
diff --git a/recyclerview-selection/res/drawable/selection_band_overlay.xml b/recyclerview-selection/res/drawable/selection_band_overlay.xml
new file mode 100644
index 0000000..f780178
--- /dev/null
+++ b/recyclerview-selection/res/drawable/selection_band_overlay.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright 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
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="rectangle">
+    <solid android:color="#339999ff" />
+    <stroke android:width="1dp" android:color="#44000000" />
+</shape>
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/ActivationCallbacks.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/ActivationCallbacks.java
new file mode 100644
index 0000000..606f35a
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/ActivationCallbacks.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.support.annotation.RestrictTo;
+import android.view.MotionEvent;
+
+import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
+
+/**
+ * Override methods in this class to connect specialized behaviors of the selection
+ * code to the application environment.
+ *
+ * @param <K> Selection key type. Usually String or Long.
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public abstract class ActivationCallbacks<K> {
+
+    static <K> ActivationCallbacks<K> dummy() {
+        return new ActivationCallbacks<K>() {
+            @Override
+            public boolean onItemActivated(ItemDetails item, MotionEvent e) {
+                return false;
+            }
+        };
+    }
+
+    /**
+     * Called when an item is activated. An item is activitated, for example, when
+     * there is no active selection and the user double clicks an item with a
+     * pointing device like a Mouse.
+     *
+     * @param item details of the item.
+     * @param e the event associated with item.
+     * @return true if the event was handled.
+     */
+    public abstract boolean onItemActivated(ItemDetails<K> item, MotionEvent e);
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/AutoScroller.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/AutoScroller.java
new file mode 100644
index 0000000..13e87bd
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/AutoScroller.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.graphics.Point;
+import android.support.annotation.RestrictTo;
+
+/**
+ * Provides support for auto-scrolling a view.
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public abstract class AutoScroller {
+
+    /**
+     * Resets state of the scroller. Call this when the user activity that is driving
+     * auto-scrolling is done.
+     */
+    protected abstract void reset();
+
+    /**
+     * Processes a new input location.
+     * @param location
+     */
+    protected abstract void scroll(Point location);
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/BandPredicate.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/BandPredicate.java
new file mode 100644
index 0000000..9a5ae47
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/BandPredicate.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import static android.support.v4.util.Preconditions.checkArgument;
+
+import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
+import android.support.v7.widget.GridLayoutManager;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.MotionEvent;
+import android.view.View;
+
+/**
+ * Provides a means of controlling when and where band selection can be initiated.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public abstract class BandPredicate {
+
+    /** @return true if band selection can be initiated in response to the {@link MotionEvent}. */
+    public abstract boolean canInitiate(MotionEvent e);
+
+    private static boolean hasSupportedLayoutManager(RecyclerView recView) {
+        RecyclerView.LayoutManager lm = recView.getLayoutManager();
+        return lm instanceof GridLayoutManager
+                || lm instanceof LinearLayoutManager;
+    }
+
+    /**
+     * Creates a new band predicate that permits initiation of band on areas
+     * of a RecyclerView that map to RecyclerView.NO_POSITION.
+     *
+     * @param recView
+     * @return
+     */
+    @SuppressWarnings("unused")
+    public static BandPredicate noPosition(RecyclerView recView) {
+        return new NoPosition(recView);
+    }
+
+    /**
+     * Creates a new band predicate that permits initiation of band
+     * anywhere doesn't correspond to a draggable region of a item.
+     *
+     * @param detailsLookup
+     * @return
+     */
+    public static BandPredicate notDraggable(
+            RecyclerView recView, ItemDetailsLookup detailsLookup) {
+        return new NotDraggable(recView, detailsLookup);
+    }
+
+    /**
+     * A BandPredicate that allows initiation of band selection only in areas of RecyclerView
+     * that have {@link RecyclerView#NO_POSITION}. In most cases, this will be the empty areas
+     * between views.
+     */
+    private static final class NoPosition extends BandPredicate {
+
+        private final RecyclerView mRecView;
+
+        NoPosition(RecyclerView recView) {
+            checkArgument(recView != null);
+
+            mRecView = recView;
+        }
+
+        @Override
+        public boolean canInitiate(MotionEvent e) {
+            if (!hasSupportedLayoutManager(mRecView)
+                    || mRecView.hasPendingAdapterUpdates()) {
+                return false;
+            }
+
+            View itemView = mRecView.findChildViewUnder(e.getX(), e.getY());
+            int position = itemView != null
+                    ? mRecView.getChildAdapterPosition(itemView)
+                    : RecyclerView.NO_POSITION;
+
+            return position == RecyclerView.NO_POSITION;
+        }
+    }
+
+    /**
+     * A BandPredicate that allows initiation of band selection in any area that is not
+     * draggable as determined by consulting
+     * {@link ItemDetailsLookup#inItemDragRegion(MotionEvent)}.
+     */
+    private static final class NotDraggable extends BandPredicate {
+
+        private final RecyclerView mRecView;
+        private final ItemDetailsLookup mDetailsLookup;
+
+        NotDraggable(RecyclerView recView, ItemDetailsLookup detailsLookup) {
+            checkArgument(recView != null);
+            checkArgument(detailsLookup != null);
+
+            mRecView = recView;
+            mDetailsLookup = detailsLookup;
+        }
+
+        @Override
+        public boolean canInitiate(MotionEvent e) {
+            if (!hasSupportedLayoutManager(mRecView)
+                    || mRecView.hasPendingAdapterUpdates()) {
+                return false;
+            }
+
+            @Nullable ItemDetailsLookup.ItemDetails details = mDetailsLookup.getItemDetails(e);
+            return (details == null) || !details.inDragRegion(e);
+        }
+    }
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/BandSelectionHelper.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/BandSelectionHelper.java
new file mode 100644
index 0000000..5362e2b
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/BandSelectionHelper.java
@@ -0,0 +1,355 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import static android.support.v4.util.Preconditions.checkArgument;
+import static android.support.v4.util.Preconditions.checkState;
+
+import static androidx.recyclerview.selection.Shared.VERBOSE;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.OnItemTouchListener;
+import android.support.v7.widget.RecyclerView.OnScrollListener;
+import android.util.Log;
+import android.view.MotionEvent;
+
+import java.util.Set;
+
+import androidx.recyclerview.selection.SelectionHelper.SelectionPredicate;
+
+/**
+ * Provides mouse driven band-selection support when used in conjunction with a {@link RecyclerView}
+ * instance. This class is responsible for rendering a band overlay and manipulating selection
+ * status of the items it intersects with.
+ *
+ * <p> Given the recycling nature of RecyclerView items that have scrolled off-screen would not
+ * be selectable with a band that itself was partially rendered off-screen. To address this,
+ * BandSelectionController builds a model of the list/grid information presented by RecyclerView as
+ * the user interacts with items using their pointer (and the band). Selectable items that intersect
+ * with the band, both on and off screen, are selected on pointer up.
+ *
+ * @param <K> Selection key type. Usually String or Long.
+ */
+class BandSelectionHelper<K> implements OnItemTouchListener {
+
+    static final String TAG = "BandSelectionHelper";
+    static final boolean DEBUG = false;
+
+    private final BandHost mHost;
+    private final ItemKeyProvider<K> mKeyProvider;
+    private final SelectionHelper<K> mSelectionHelper;
+    private final SelectionPredicate<K> mSelectionPredicate;
+    private final BandPredicate mBandPredicate;
+    private final FocusCallbacks<K> mFocusCallbacks;
+    private final ContentLock mLock;
+    private final AutoScroller mScroller;
+    private final GridModel.SelectionObserver mGridObserver;
+
+    private @Nullable Point mCurrentPosition;
+    private @Nullable Point mOrigin;
+    private @Nullable GridModel mModel;
+
+    /**
+     * See {@link BandSelectionHelper#create}.
+     */
+    BandSelectionHelper(
+            BandHost host,
+            AutoScroller scroller,
+            ItemKeyProvider<K> keyProvider,
+            SelectionHelper<K> selectionHelper,
+            SelectionPredicate<K> selectionPredicate,
+            BandPredicate bandPredicate,
+            FocusCallbacks<K> focusCallbacks,
+            ContentLock lock) {
+
+        checkArgument(host != null);
+        checkArgument(scroller != null);
+        checkArgument(keyProvider != null);
+        checkArgument(selectionHelper != null);
+        checkArgument(selectionPredicate != null);
+        checkArgument(bandPredicate != null);
+        checkArgument(focusCallbacks != null);
+        checkArgument(lock != null);
+
+        mHost = host;
+        mKeyProvider = keyProvider;
+        mSelectionHelper = selectionHelper;
+        mSelectionPredicate = selectionPredicate;
+        mBandPredicate = bandPredicate;
+        mFocusCallbacks = focusCallbacks;
+        mLock = lock;
+
+        mHost.addOnScrollListener(
+                new OnScrollListener() {
+                    @Override
+                    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+                        BandSelectionHelper.this.onScrolled(recyclerView, dx, dy);
+                    }
+                });
+
+        mScroller = scroller;
+
+        mGridObserver = new GridModel.SelectionObserver<K>() {
+            @Override
+            public void onSelectionChanged(Set<K> updatedSelection) {
+                mSelectionHelper.setProvisionalSelection(updatedSelection);
+            }
+        };
+    }
+
+    /**
+     * Creates a new instance.
+     *
+     * @return new BandSelectionHelper instance.
+     */
+    static <K> BandSelectionHelper create(
+            RecyclerView recView,
+            AutoScroller scroller,
+            @DrawableRes int bandOverlayId,
+            ItemKeyProvider<K> keyProvider,
+            SelectionHelper<K> selectionHelper,
+            SelectionPredicate<K> selectionPredicate,
+            BandPredicate bandPredicate,
+            FocusCallbacks<K> focusCallbacks,
+            ContentLock lock) {
+
+        return new BandSelectionHelper<>(
+                new DefaultBandHost<>(recView, bandOverlayId, keyProvider, selectionPredicate),
+                scroller,
+                keyProvider,
+                selectionHelper,
+                selectionPredicate,
+                bandPredicate,
+                focusCallbacks,
+                lock);
+    }
+
+    @VisibleForTesting
+    boolean isActive() {
+        boolean active = mModel != null;
+        if (DEBUG && active) {
+            mLock.checkLocked();
+        }
+        return active;
+    }
+
+    /**
+     * Clients must call reset when there are any material changes to the layout of items
+     * in RecyclerView.
+     */
+    void reset() {
+        if (!isActive()) {
+            return;
+        }
+
+        mHost.hideBand();
+        if (mModel != null) {
+            mModel.stopCapturing();
+            mModel.onDestroy();
+        }
+
+        mModel = null;
+        mOrigin = null;
+
+        mScroller.reset();
+        mLock.unblock();
+    }
+
+    @VisibleForTesting
+    boolean shouldStart(MotionEvent e) {
+        // b/30146357 && b/23793622. onInterceptTouchEvent does not dispatch events to onTouchEvent
+        // unless the event is != ACTION_DOWN. Thus, we need to actually start band selection when
+        // mouse moves.
+        return MotionEvents.isPrimaryButtonPressed(e)
+                && MotionEvents.isActionMove(e)
+                && mBandPredicate.canInitiate(e)
+                && !isActive();
+    }
+
+    @VisibleForTesting
+    boolean shouldStop(MotionEvent e) {
+        return isActive()
+                && (MotionEvents.isActionUp(e)
+                || MotionEvents.isActionPointerUp(e)
+                || MotionEvents.isActionCancel(e));
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(RecyclerView unused, MotionEvent e) {
+        if (shouldStart(e)) {
+            startBandSelect(e);
+        } else if (shouldStop(e)) {
+            endBandSelect();
+        }
+
+        return isActive();
+    }
+
+    /**
+     * Processes a MotionEvent by starting, ending, or resizing the band select overlay.
+     */
+    @Override
+    public void onTouchEvent(RecyclerView unused, MotionEvent e) {
+        if (shouldStop(e)) {
+            endBandSelect();
+            return;
+        }
+
+        // We shouldn't get any events in this method when band select is not active,
+        // but it turns some guests show up late to the party.
+        // Probably happening when a re-layout is happening to the ReyclerView (ie. Pull-To-Refresh)
+        if (!isActive()) {
+            return;
+        }
+
+        if (DEBUG) {
+            checkArgument(MotionEvents.isActionMove(e));
+            checkState(mModel != null);
+        }
+
+        mCurrentPosition = MotionEvents.getOrigin(e);
+
+        mModel.resizeSelection(mCurrentPosition);
+
+        resizeBand();
+        mScroller.scroll(mCurrentPosition);
+    }
+
+    @Override
+    public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+    }
+
+    /**
+     * Starts band select by adding the drawable to the RecyclerView's overlay.
+     */
+    private void startBandSelect(MotionEvent e) {
+        checkState(!isActive());
+
+        if (!MotionEvents.isCtrlKeyPressed(e)) {
+            mSelectionHelper.clearSelection();
+        }
+
+        Point origin = MotionEvents.getOrigin(e);
+        if (DEBUG) Log.d(TAG, "Starting band select @ " + origin);
+
+        mModel = mHost.createGridModel();
+        mModel.addOnSelectionChangedListener(mGridObserver);
+
+        mLock.block();
+        mFocusCallbacks.clearFocus();
+        mOrigin = origin;
+        // NOTE: Pay heed that resizeBand modifies the y coordinates
+        // in onScrolled. Not sure if model expects this. If not
+        // it should be defending against this.
+        mModel.startCapturing(mOrigin);
+    }
+
+    /**
+     * Resizes the band select rectangle by using the origin and the current pointer position as
+     * two opposite corners of the selection.
+     */
+    private void resizeBand() {
+        Rect bounds = new Rect(Math.min(mOrigin.x, mCurrentPosition.x),
+                Math.min(mOrigin.y, mCurrentPosition.y),
+                Math.max(mOrigin.x, mCurrentPosition.x),
+                Math.max(mOrigin.y, mCurrentPosition.y));
+
+        if (VERBOSE) Log.v(TAG, "Resizing band! " + bounds);
+        mHost.showBand(bounds);
+    }
+
+    /**
+     * Ends band select by removing the overlay.
+     */
+    private void endBandSelect() {
+        if (DEBUG) {
+            Log.d(TAG, "Ending band select.");
+            checkState(mModel != null);
+        }
+
+        // TODO: Currently when a band select operation ends outside
+        // of an item (e.g. in the empty area between items),
+        // getPositionNearestOrigin may return an unselected item.
+        // Since the point of this code is to establish the
+        // anchor point for subsequent range operations (SHIFT+CLICK)
+        // we really want to do a better job figuring out the last
+        // item selected (and nearest to the cursor).
+        int firstSelected = mModel.getPositionNearestOrigin();
+        if (firstSelected != GridModel.NOT_SET
+                && mSelectionHelper.isSelected(mKeyProvider.getKey(firstSelected))) {
+            // Establish the band selection point as range anchor. This
+            // allows touch and keyboard based selection activities
+            // to be based on the band selection anchor point.
+            mSelectionHelper.anchorRange(firstSelected);
+        }
+
+        mSelectionHelper.mergeProvisionalSelection();
+        reset();
+    }
+
+    /**
+     * @see RecyclerView.OnScrollListener
+     */
+    private void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+        if (!isActive()) {
+            return;
+        }
+
+        // Adjust the y-coordinate of the origin the opposite number of pixels so that the
+        // origin remains in the same place relative to the view's items.
+        mOrigin.y -= dy;
+        resizeBand();
+    }
+
+    /**
+     * Provides functionality for BandController. Exists primarily to tests that are
+     * fully isolated from RecyclerView.
+     *
+     * @param <K> Selection key type. Usually String or Long.
+     */
+    abstract static class BandHost<K> {
+
+        /**
+         * Returns a new GridModel instance.
+         */
+        abstract GridModel<K> createGridModel();
+
+        /**
+         * Show the band covering the bounds.
+         *
+         * @param bounds The boundaries of the band to show.
+         */
+        abstract void showBand(Rect bounds);
+
+        /**
+         * Hide the band.
+         */
+        abstract void hideBand();
+
+        /**
+         * Add a listener to be notified on scroll events.
+         *
+         * @param listener
+         */
+        abstract void addOnScrollListener(RecyclerView.OnScrollListener listener);
+    }
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/ContentLock.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/ContentLock.java
new file mode 100644
index 0000000..6891eab
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/ContentLock.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import static android.support.v4.util.Preconditions.checkState;
+
+import static androidx.recyclerview.selection.Shared.DEBUG;
+
+import android.content.Loader;
+import android.support.annotation.MainThread;
+import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
+import android.util.Log;
+
+/**
+ * ContentLock provides a mechanism to block content from reloading while selection
+ * activities like gesture and band selection are active. Clients using live data
+ * (data loaded, for example by a {@link Loader}), should route calls to load
+ * content through this lock using {@link ContentLock#runWhenUnlocked(Runnable)}.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public final class ContentLock {
+
+    private static final String TAG = "ContentLock";
+
+    private int mLocks = 0;
+    private @Nullable Runnable mCallback;
+
+    /**
+     * Increment the block count by 1
+     */
+    @MainThread
+    synchronized void block() {
+        mLocks++;
+        if (DEBUG) Log.v(TAG, "Incremented content lock count to " + mLocks + ".");
+    }
+
+    /**
+     * Decrement the block count by 1; If no other object is trying to block and there exists some
+     * callback, that callback will be run
+     */
+    @MainThread
+    synchronized void unblock() {
+        checkState(mLocks > 0);
+
+        mLocks--;
+        if (DEBUG) Log.v(TAG, "Decremented content lock count to " + mLocks + ".");
+
+        if (mLocks == 0 && mCallback != null) {
+            mCallback.run();
+            mCallback = null;
+        }
+    }
+
+    /**
+     * Attempts to run the given Runnable if not-locked, or else the Runnable is set to be ran next
+     * (replacing any previous set Runnables).
+     */
+    @SuppressWarnings("unused")
+    public synchronized void runWhenUnlocked(Runnable runnable) {
+        if (mLocks == 0) {
+            runnable.run();
+        } else {
+            mCallback = runnable;
+        }
+    }
+
+    /**
+     * Allows other selection code to perform a precondition check asserting the state is locked.
+     */
+    void checkLocked() {
+        checkState(mLocks > 0);
+    }
+
+    /**
+     * Allows other selection code to perform a precondition check asserting the state is unlocked.
+     */
+    void checkUnlocked() {
+        checkState(mLocks == 0);
+    }
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/DefaultBandHost.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/DefaultBandHost.java
new file mode 100644
index 0000000..f0fd4fe
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/DefaultBandHost.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import static android.support.v4.util.Preconditions.checkArgument;
+
+import android.graphics.Canvas;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.DrawableRes;
+import android.support.v7.widget.GridLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.ItemDecoration;
+import android.view.View;
+
+import androidx.recyclerview.selection.SelectionHelper.SelectionPredicate;
+
+/**
+ * RecyclerView backed {@link BandSelectionHelper.BandHost}.
+ */
+final class DefaultBandHost<K> extends GridModel.GridHost<K> {
+
+    private static final Rect NILL_RECT = new Rect(0, 0, 0, 0);
+
+    private final RecyclerView mRecView;
+    private final Drawable mBand;
+    private final ItemKeyProvider<K> mKeyProvider;
+    private final SelectionPredicate<K> mSelectionPredicate;
+
+    DefaultBandHost(
+            RecyclerView recView,
+            @DrawableRes int bandOverlayId,
+            ItemKeyProvider<K> keyProvider,
+            SelectionPredicate<K> selectionPredicate) {
+
+        checkArgument(recView != null);
+
+        mRecView = recView;
+        mBand = mRecView.getContext().getResources().getDrawable(bandOverlayId);
+
+        checkArgument(mBand != null);
+        checkArgument(keyProvider != null);
+        checkArgument(selectionPredicate != null);
+
+        mKeyProvider = keyProvider;
+        mSelectionPredicate = selectionPredicate;
+
+        mRecView.addItemDecoration(
+                new ItemDecoration() {
+                    @Override
+                    public void onDrawOver(
+                            Canvas canvas,
+                            RecyclerView unusedParent,
+                            RecyclerView.State unusedState) {
+                        DefaultBandHost.this.onDrawBand(canvas);
+                    }
+                });
+    }
+
+    @Override
+    GridModel<K> createGridModel() {
+        return new GridModel<>(this, mKeyProvider, mSelectionPredicate);
+    }
+
+    @Override
+    int getAdapterPositionAt(int index) {
+        return mRecView.getChildAdapterPosition(mRecView.getChildAt(index));
+    }
+
+    @Override
+    void addOnScrollListener(RecyclerView.OnScrollListener listener) {
+        mRecView.addOnScrollListener(listener);
+    }
+
+    @Override
+    void removeOnScrollListener(RecyclerView.OnScrollListener listener) {
+        mRecView.removeOnScrollListener(listener);
+    }
+
+    @Override
+    Point createAbsolutePoint(Point relativePoint) {
+        return new Point(relativePoint.x + mRecView.computeHorizontalScrollOffset(),
+                relativePoint.y + mRecView.computeVerticalScrollOffset());
+    }
+
+    @Override
+    Rect getAbsoluteRectForChildViewAt(int index) {
+        final View child = mRecView.getChildAt(index);
+        final Rect childRect = new Rect();
+        child.getHitRect(childRect);
+        childRect.left += mRecView.computeHorizontalScrollOffset();
+        childRect.right += mRecView.computeHorizontalScrollOffset();
+        childRect.top += mRecView.computeVerticalScrollOffset();
+        childRect.bottom += mRecView.computeVerticalScrollOffset();
+        return childRect;
+    }
+
+    @Override
+    int getVisibleChildCount() {
+        return mRecView.getChildCount();
+    }
+
+    @Override
+    int getColumnCount() {
+        RecyclerView.LayoutManager layoutManager = mRecView.getLayoutManager();
+        if (layoutManager instanceof GridLayoutManager) {
+            return ((GridLayoutManager) layoutManager).getSpanCount();
+        }
+
+        // Otherwise, it is a list with 1 column.
+        return 1;
+    }
+
+    @Override
+    void showBand(Rect rect) {
+        mBand.setBounds(rect);
+        // TODO: mRecView.invalidateItemDecorations() should work, but it isn't currently.
+        // NOTE: That without invalidating rv, the band only gets updated
+        // when the pointer moves off a the item view into "NO_POSITION" territory.
+        mRecView.invalidate();
+    }
+
+    @Override
+    void hideBand() {
+        mBand.setBounds(NILL_RECT);
+        // TODO: mRecView.invalidateItemDecorations() should work, but it isn't currently.
+        mRecView.invalidate();
+    }
+
+    private void onDrawBand(Canvas c) {
+        mBand.draw(c);
+    }
+
+    @Override
+    boolean hasView(int pos) {
+        return mRecView.findViewHolderForAdapterPosition(pos) != null;
+    }
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/DefaultSelectionHelper.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/DefaultSelectionHelper.java
new file mode 100644
index 0000000..5625e3d
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/DefaultSelectionHelper.java
@@ -0,0 +1,475 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import static android.support.v4.util.Preconditions.checkArgument;
+import static android.support.v4.util.Preconditions.checkState;
+
+import static androidx.recyclerview.selection.Shared.DEBUG;
+
+import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import androidx.recyclerview.selection.Range.RangeType;
+
+/**
+ * {@link SelectionHelper} providing support for traditional multi-item selection on top
+ * of {@link RecyclerView}.
+ *
+ * <p>The class supports running in a single-select mode, which can be enabled
+ * by passing {@code #MODE_SINGLE} to the constructor.
+ *
+ * @param <K> Selection key type. Usually String or Long.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public class DefaultSelectionHelper<K> extends SelectionHelper<K> {
+
+    private static final String TAG = "DefaultSelectionHelper";
+
+    private final Selection<K> mSelection = new Selection<>();
+    private final List<SelectionObserver> mObservers = new ArrayList<>(1);
+    private final ItemKeyProvider<K> mKeyProvider;
+    private final SelectionPredicate<K> mSelectionPredicate;
+    private final RangeCallbacks mRangeCallbacks;
+    private final boolean mSingleSelect;
+
+    private @Nullable Range mRange;
+
+    /**
+     * Creates a new instance.
+     *
+     * @param keyProvider client supplied class providing access to stable ids.
+     * @param selectionPredicate A predicate allowing the client to disallow selection
+     *     of individual elements.
+     */
+    public DefaultSelectionHelper(
+            ItemKeyProvider keyProvider,
+            SelectionPredicate selectionPredicate) {
+
+        checkArgument(keyProvider != null);
+        checkArgument(selectionPredicate != null);
+
+        mKeyProvider = keyProvider;
+        mSelectionPredicate = selectionPredicate;
+        mRangeCallbacks = new RangeCallbacks();
+
+        mSingleSelect = !selectionPredicate.canSelectMultiple();
+    }
+
+    @Override
+    public void addObserver(SelectionObserver callback) {
+        checkArgument(callback != null);
+        mObservers.add(callback);
+    }
+
+    @Override
+    public boolean hasSelection() {
+        return !mSelection.isEmpty();
+    }
+
+    @Override
+    public Selection getSelection() {
+        return mSelection;
+    }
+
+    @Override
+    public void copySelection(Selection dest) {
+        dest.copyFrom(mSelection);
+    }
+
+    @Override
+    public boolean isSelected(@Nullable K key) {
+        return mSelection.contains(key);
+    }
+
+    @Override
+    public void restoreSelection(Selection other) {
+        checkArgument(other != null);
+        setItemsSelectedQuietly(other.mSelection, true);
+        // NOTE: We intentionally don't restore provisional selection. It's provisional.
+        notifySelectionRestored();
+    }
+
+    @Override
+    public boolean setItemsSelected(Iterable<K> keys, boolean selected) {
+        boolean changed = setItemsSelectedQuietly(keys, selected);
+        notifySelectionChanged();
+        return changed;
+    }
+
+    private boolean setItemsSelectedQuietly(Iterable<K> keys, boolean selected) {
+        boolean changed = false;
+        for (K key: keys) {
+            boolean itemChanged = selected
+                    ? canSetState(key, true) && mSelection.add(key)
+                    : canSetState(key, false) && mSelection.remove(key);
+            if (itemChanged) {
+                notifyItemStateChanged(key, selected);
+            }
+            changed |= itemChanged;
+        }
+        return changed;
+    }
+
+    @Override
+    public void clearSelection() {
+        if (!hasSelection()) {
+            return;
+        }
+
+        Selection prev = clearSelectionQuietly();
+        notifySelectionCleared(prev);
+        notifySelectionChanged();
+    }
+
+    @Override
+    public boolean clear() {
+        boolean somethingChanged = hasSelection();
+        clearProvisionalSelection();
+        clearSelection();
+        return somethingChanged;
+    }
+
+    /**
+     * Clears the selection, without notifying selection listeners.
+     * Returns items in previous selection. Callers are responsible for notifying
+     * listeners about changes.
+     */
+    private Selection clearSelectionQuietly() {
+        mRange = null;
+
+        Selection prevSelection = new Selection();
+        if (hasSelection()) {
+            copySelection(prevSelection);
+            mSelection.clear();
+        }
+
+        return prevSelection;
+    }
+
+    @Override
+    public boolean select(K key) {
+        checkArgument(key != null);
+
+        if (!mSelection.contains(key)) {
+            if (!canSetState(key, true)) {
+                if (DEBUG) Log.d(TAG, "Select cancelled by selection predicate test.");
+                return false;
+            }
+
+            // Enforce single selection policy.
+            if (mSingleSelect && hasSelection()) {
+                Selection prev = clearSelectionQuietly();
+                notifySelectionCleared(prev);
+            }
+
+            mSelection.add(key);
+            notifyItemStateChanged(key, true);
+            notifySelectionChanged();
+
+            return true;
+        }
+
+        return false;
+    }
+
+    @Override
+    public boolean deselect(K key) {
+        checkArgument(key != null);
+
+        if (mSelection.contains(key)) {
+            if (!canSetState(key, false)) {
+                if (DEBUG) Log.d(TAG, "Deselect cancelled by selection predicate test.");
+                return false;
+            }
+            mSelection.remove(key);
+            notifyItemStateChanged(key, false);
+            notifySelectionChanged();
+            if (mSelection.isEmpty() && isRangeActive()) {
+                // if there's nothing in the selection and there is an active ranger it results
+                // in unexpected behavior when the user tries to start range selection: the item
+                // which the ranger 'thinks' is the already selected anchor becomes unselectable
+                endRange();
+            }
+            return true;
+        }
+
+        return false;
+    }
+
+    @Override
+    public void startRange(int position) {
+        select(mKeyProvider.getKey(position));
+        anchorRange(position);
+    }
+
+    @Override
+    public void extendRange(int position) {
+        extendRange(position, Range.TYPE_PRIMARY);
+    }
+
+    @Override
+    public void endRange() {
+        mRange = null;
+        // Clean up in case there was any leftover provisional selection
+        clearProvisionalSelection();
+    }
+
+    @Override
+    public void anchorRange(int position) {
+        checkArgument(position != RecyclerView.NO_POSITION);
+        checkArgument(mSelection.contains(mKeyProvider.getKey(position)));
+
+        mRange = new Range(position, mRangeCallbacks);
+    }
+
+    @Override
+    public void extendProvisionalRange(int position) {
+        if (mSingleSelect) {
+            return;
+        }
+
+        if (DEBUG) Log.i(TAG, "Extending provision range to position: " + position);
+        checkState(isRangeActive(), "Range start point not set.");
+        extendRange(position, Range.TYPE_PROVISIONAL);
+    }
+
+    /**
+     * Sets the end point for the current range selection, started by a call to
+     * {@link #startRange(int)}. This function should only be called when a range selection
+     * is active (see {@link #isRangeActive()}. Items in the range [anchor, end] will be
+     * selected or in provisional select, depending on the type supplied. Note that if the type is
+     * provisional selection, one should do {@link #mergeProvisionalSelection()} at some
+     * point before calling on {@link #endRange()}.
+     *
+     * @param position The new end position for the selection range.
+     * @param type The type of selection the range should utilize.
+     */
+    private void extendRange(int position, @RangeType int type) {
+        checkState(isRangeActive(), "Range start point not set.");
+
+        mRange.extendRange(position, type);
+
+        // We're being lazy here notifying even when something might not have changed.
+        // To make this more correct, we'd need to update the Ranger class to return
+        // information about what has changed.
+        notifySelectionChanged();
+    }
+
+    @Override
+    public void setProvisionalSelection(Set<K> newSelection) {
+        if (mSingleSelect) {
+            return;
+        }
+
+        Map<K, Boolean> delta = mSelection.setProvisionalSelection(newSelection);
+        for (Map.Entry<K, Boolean> entry: delta.entrySet()) {
+            notifyItemStateChanged(entry.getKey(), entry.getValue());
+        }
+
+        notifySelectionChanged();
+    }
+
+    @Override
+    public void mergeProvisionalSelection() {
+        mSelection.mergeProvisionalSelection();
+
+        // Note, that for almost all functional purposes, merging a provisional selection
+        // into a the primary selection doesn't change the selection, just an internal
+        // representation of it. But there are some nuanced areas cases where
+        // that isn't true. equality for 1. So, we notify regardless.
+
+        notifySelectionChanged();
+    }
+
+    @Override
+    public void clearProvisionalSelection() {
+        for (K key : mSelection.mProvisionalSelection) {
+            notifyItemStateChanged(key, false);
+        }
+        mSelection.clearProvisionalSelection();
+    }
+
+    @Override
+    public boolean isRangeActive() {
+        return mRange != null;
+    }
+
+    private boolean canSetState(K key, boolean nextState) {
+        return mSelectionPredicate.canSetStateForKey(key, nextState);
+    }
+
+    @Override
+    void onDataSetChanged() {
+        mSelection.clearProvisionalSelection();
+
+        notifySelectionReset();
+
+        for (K key : mSelection) {
+            // If the underlying data set has changed, before restoring
+            // selection we must re-verify that it can be selected.
+            // Why? Because if the dataset has changed, then maybe the
+            // selectability of an item has changed.
+            if (!canSetState(key, true)) {
+                deselect(key);
+            } else {
+                int lastListener = mObservers.size() - 1;
+                for (int i = lastListener; i >= 0; i--) {
+                    mObservers.get(i).onItemStateChanged(key, true);
+                }
+            }
+        }
+
+        notifySelectionChanged();
+    }
+
+    /**
+     * Notifies registered listeners when the selection status of a single item
+     * (identified by {@code position}) changes.
+     */
+    private void notifyItemStateChanged(K key, boolean selected) {
+        checkArgument(key != null);
+
+        int lastListenerIndex = mObservers.size() - 1;
+        for (int i = lastListenerIndex; i >= 0; i--) {
+            mObservers.get(i).onItemStateChanged(key, selected);
+        }
+    }
+
+    private void notifySelectionCleared(Selection<K> selection) {
+        for (K key: selection.mSelection) {
+            notifyItemStateChanged(key, false);
+        }
+        for (K key: selection.mProvisionalSelection) {
+            notifyItemStateChanged(key, false);
+        }
+    }
+
+    /**
+     * Notifies registered listeners when the selection has changed. This
+     * notification should be sent only once a full series of changes
+     * is complete, e.g. clearingSelection, or updating the single
+     * selection from one item to another.
+     */
+    private void notifySelectionChanged() {
+        int lastListenerIndex = mObservers.size() - 1;
+        for (int i = lastListenerIndex; i >= 0; i--) {
+            mObservers.get(i).onSelectionChanged();
+        }
+    }
+
+    private void notifySelectionRestored() {
+        int lastListenerIndex = mObservers.size() - 1;
+        for (int i = lastListenerIndex; i >= 0; i--) {
+            mObservers.get(i).onSelectionRestored();
+        }
+    }
+
+    private void notifySelectionReset() {
+        int lastListenerIndex = mObservers.size() - 1;
+        for (int i = lastListenerIndex; i >= 0; i--) {
+            mObservers.get(i).onSelectionReset();
+        }
+    }
+
+    private void updateForRange(int begin, int end, boolean selected, @RangeType int type) {
+        switch (type) {
+            case Range.TYPE_PRIMARY:
+                updateForRegularRange(begin, end, selected);
+                break;
+            case Range.TYPE_PROVISIONAL:
+                updateForProvisionalRange(begin, end, selected);
+                break;
+            default:
+                throw new IllegalArgumentException("Invalid range type: " + type);
+        }
+    }
+
+    private void updateForRegularRange(int begin, int end, boolean selected) {
+        checkArgument(end >= begin);
+
+        for (int i = begin; i <= end; i++) {
+            K key = mKeyProvider.getKey(i);
+            if (key == null) {
+                continue;
+            }
+
+            if (selected) {
+                select(key);
+            } else {
+                deselect(key);
+            }
+        }
+    }
+
+    private void updateForProvisionalRange(int begin, int end, boolean selected) {
+        checkArgument(end >= begin);
+
+        for (int i = begin; i <= end; i++) {
+            K key = mKeyProvider.getKey(i);
+            if (key == null) {
+                continue;
+            }
+
+            boolean changedState = false;
+            if (selected) {
+                boolean canSelect = canSetState(key, true);
+                if (canSelect && !mSelection.mSelection.contains(key)) {
+                    mSelection.mProvisionalSelection.add(key);
+                    changedState = true;
+                }
+            } else {
+                mSelection.mProvisionalSelection.remove(key);
+                changedState = true;
+            }
+
+            // Only notify item callbacks when something's state is actually changed in provisional
+            // selection.
+            if (changedState) {
+                notifyItemStateChanged(key, selected);
+            }
+        }
+
+        notifySelectionChanged();
+    }
+
+    private final class RangeCallbacks extends Range.Callbacks {
+        @Override
+        void updateForRange(int begin, int end, boolean selected, int type) {
+            switch (type) {
+                case Range.TYPE_PRIMARY:
+                    updateForRegularRange(begin, end, selected);
+                    break;
+                case Range.TYPE_PROVISIONAL:
+                    updateForProvisionalRange(begin, end, selected);
+                    break;
+                default:
+                    throw new IllegalArgumentException("Invalid range type: " + type);
+            }
+        }
+    }
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/EventBridge.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/EventBridge.java
new file mode 100644
index 0000000..b418ad4
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/EventBridge.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import static android.support.v4.util.Preconditions.checkArgument;
+
+import static androidx.recyclerview.selection.Shared.VERBOSE;
+
+import android.support.annotation.RestrictTo;
+import android.support.annotation.VisibleForTesting;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+
+/**
+ * Provides the necessary glue to notify RecyclerView when selection data changes,
+ * and to notify SelectionHelper when the underlying RecyclerView.Adapter data changes.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+@VisibleForTesting
+public class EventBridge {
+
+    private static final String TAG = "EventsRelays";
+
+    /**
+     * Installs the event bridge for on the supplied adapter/helper.
+     *
+     * @param adapter
+     * @param selectionHelper
+     * @param keyProvider
+     * @param <K>
+     */
+    @VisibleForTesting
+    public static <K> void install(
+            RecyclerView.Adapter<?> adapter,
+            SelectionHelper<K> selectionHelper,
+            ItemKeyProvider<K> keyProvider) {
+        new AdapterToSelectionHelper(adapter, selectionHelper);
+        new SelectionHelperToAdapter<>(selectionHelper, keyProvider, adapter);
+    }
+
+    private static final class AdapterToSelectionHelper extends RecyclerView.AdapterDataObserver {
+
+        private final SelectionHelper<?> mSelectionHelper;
+
+        AdapterToSelectionHelper(
+                RecyclerView.Adapter<?> adapter,
+                SelectionHelper<?> selectionHelper) {
+            adapter.registerAdapterDataObserver(this);
+
+            checkArgument(selectionHelper != null);
+            mSelectionHelper = selectionHelper;
+        }
+
+        @Override
+        public void onChanged() {
+            mSelectionHelper.onDataSetChanged();
+        }
+
+        @Override
+        public void onItemRangeChanged(int startPosition, int itemCount, Object payload) {
+            // No change in position. Ignore, since we assume
+            // selection is a user driven activity. So changes
+            // in properties of items shouldn't result in a
+            // change of selection.
+            // TODO: It is possible properties of items chould change to make them unselectable.
+        }
+
+        @Override
+        public void onItemRangeInserted(int startPosition, int itemCount) {
+            // Uninteresting to us since selection is stable ID based.
+        }
+
+        @Override
+        public void onItemRangeRemoved(int startPosition, int itemCount) {
+            // Uninteresting to us since selection is stable ID based.
+        }
+
+        @Override
+        public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
+            // Uninteresting to us since selection is stable ID based.
+        }
+    }
+
+    private static final class SelectionHelperToAdapter<K>
+            extends SelectionHelper.SelectionObserver<K> {
+
+        private final ItemKeyProvider<K> mKeyProvider;
+        private final RecyclerView.Adapter<?> mAdapter;
+
+        SelectionHelperToAdapter(
+                SelectionHelper<K> selectionHelper,
+                ItemKeyProvider<K> keyProvider,
+                RecyclerView.Adapter<?> adapter) {
+
+            selectionHelper.addObserver(this);
+
+            checkArgument(keyProvider != null);
+            checkArgument(adapter != null);
+
+            mKeyProvider = keyProvider;
+            mAdapter = adapter;
+        }
+
+        /**
+         * Called when state of an item has been changed.
+         */
+        @Override
+        public void onItemStateChanged(K key, boolean selected) {
+            int position = mKeyProvider.getPosition(key);
+            if (VERBOSE) Log.v(TAG, "ITEM " + key + " CHANGED at pos: " + position);
+
+            if (position < 0) {
+                Log.w(TAG, "Item change notification received for unknown item: " + key);
+                return;
+            }
+
+            mAdapter.notifyItemChanged(position, SelectionHelper.SELECTION_CHANGED_MARKER);
+        }
+    }
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/FocusCallbacks.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/FocusCallbacks.java
new file mode 100644
index 0000000..4c1c12e
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/FocusCallbacks.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.support.annotation.RestrictTo;
+import android.support.v7.widget.RecyclerView;
+
+import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
+
+/**
+ * Override methods in this class to connect specialized behaviors of the selection
+ * code to the application environment.
+ *
+ * @param <K> Selection key type. Usually String or Long.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public abstract class FocusCallbacks<K> {
+
+    static final <K> FocusCallbacks<K> dummy() {
+        return new FocusCallbacks<K>() {
+            @Override
+            public void focusItem(ItemDetails<K> item) {
+            }
+
+            @Override
+            public boolean hasFocusedItem() {
+                return false;
+            }
+
+            @Override
+            public int getFocusedPosition() {
+                return RecyclerView.NO_POSITION;
+            }
+
+            @Override
+            public void clearFocus() {
+            }
+        };
+    }
+
+    /**
+     * If environment supports focus, focus {@code item}.
+     */
+    public abstract void focusItem(ItemDetails<K> item);
+
+    /**
+     * @return true if there is a focused item.
+     */
+    public abstract boolean hasFocusedItem();
+
+    /**
+     * @return the position of the currently focused item, if any.
+     */
+    public abstract int getFocusedPosition();
+
+    /**
+     * If the environment supports focus and something is focused, unfocus it.
+     */
+    public abstract void clearFocus();
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/GestureRouter.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/GestureRouter.java
new file mode 100644
index 0000000..82fab87
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/GestureRouter.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import static android.support.v4.util.Preconditions.checkArgument;
+
+import android.support.annotation.Nullable;
+import android.view.GestureDetector.OnDoubleTapListener;
+import android.view.GestureDetector.OnGestureListener;
+import android.view.GestureDetector.SimpleOnGestureListener;
+import android.view.MotionEvent;
+
+/**
+ * GestureRouter is responsible for routing gestures detected by a GestureDetector
+ * to registered handlers. The primary function is to divide events by tool-type
+ * allowing handlers to cleanly implement tool-type specific policies.
+ *
+ * @param <T> listener type. Must extend OnGestureListener & OnDoubleTapListener.
+ */
+final class GestureRouter<T extends OnGestureListener & OnDoubleTapListener>
+        implements OnGestureListener, OnDoubleTapListener {
+
+    private final ToolHandlerRegistry<T> mDelegates;
+
+    GestureRouter(T defaultDelegate) {
+        checkArgument(defaultDelegate != null);
+        mDelegates = new ToolHandlerRegistry<>(defaultDelegate);
+    }
+
+    GestureRouter() {
+        this((T) new SimpleOnGestureListener());
+    }
+
+    /**
+     * @param toolType
+     * @param delegate the delegate, or null to unregister.
+     */
+    public void register(int toolType, @Nullable T delegate) {
+        mDelegates.set(toolType, delegate);
+    }
+
+    @Override
+    public boolean onSingleTapConfirmed(MotionEvent e) {
+        return mDelegates.get(e).onSingleTapConfirmed(e);
+    }
+
+    @Override
+    public boolean onDoubleTap(MotionEvent e) {
+        return mDelegates.get(e).onDoubleTap(e);
+    }
+
+    @Override
+    public boolean onDoubleTapEvent(MotionEvent e) {
+        return mDelegates.get(e).onDoubleTapEvent(e);
+    }
+
+    @Override
+    public boolean onDown(MotionEvent e) {
+        return mDelegates.get(e).onDown(e);
+    }
+
+    @Override
+    public void onShowPress(MotionEvent e) {
+        mDelegates.get(e).onShowPress(e);
+    }
+
+    @Override
+    public boolean onSingleTapUp(MotionEvent e) {
+        return mDelegates.get(e).onSingleTapUp(e);
+    }
+
+    @Override
+    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+        return mDelegates.get(e2).onScroll(e1, e2, distanceX, distanceY);
+    }
+
+    @Override
+    public void onLongPress(MotionEvent e) {
+        mDelegates.get(e).onLongPress(e);
+    }
+
+    @Override
+    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+        return mDelegates.get(e2).onFling(e1, e2, velocityX, velocityY);
+    }
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/GestureSelectionHelper.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/GestureSelectionHelper.java
new file mode 100644
index 0000000..2a28fc5
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/GestureSelectionHelper.java
@@ -0,0 +1,287 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import static android.support.v4.util.Preconditions.checkArgument;
+import static android.support.v4.util.Preconditions.checkState;
+
+import android.graphics.Point;
+import android.support.annotation.VisibleForTesting;
+import android.support.v4.view.ViewCompat;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.OnItemTouchListener;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+
+/**
+ * GestureSelectionHelper provides logic that interprets a combination
+ * of motions and gestures in order to provide gesture driven selection support
+ * when used in conjunction with RecyclerView and other classes in the ReyclerView
+ * selection support package.
+ */
+final class GestureSelectionHelper implements OnItemTouchListener {
+
+    private static final String TAG = "GestureSelectionHelper";
+
+    private final SelectionHelper<?> mSelectionMgr;
+    private final AutoScroller mScroller;
+    private final ViewDelegate mView;
+    private final ContentLock mLock;
+
+    private int mLastStartedItemPos = -1;
+    private boolean mStarted = false;
+    private Point mLastInterceptedPoint;
+
+    /**
+     * See {@link #create(SelectionHelper, RecyclerView, AutoScroller, ContentLock)} for convenience
+     * method.
+     */
+    GestureSelectionHelper(
+            SelectionHelper<?> selectionHelper,
+            ViewDelegate view,
+            AutoScroller scroller,
+            ContentLock lock) {
+
+        checkArgument(selectionHelper != null);
+        checkArgument(view != null);
+        checkArgument(scroller != null);
+        checkArgument(lock != null);
+
+        mSelectionMgr = selectionHelper;
+        mView = view;
+        mScroller = scroller;
+        mLock = lock;
+    }
+
+    /**
+     * Explicitly kicks off a gesture multi-select.
+     */
+    void start() {
+        checkState(!mStarted);
+        checkState(mLastStartedItemPos > -1);
+
+        // Partner code in MotionInputHandler ensures items
+        // are selected and range established prior to
+        // start being called.
+        // Verify the truth of that statement here
+        // to make the implicit coupling less of a time bomb.
+        checkState(mSelectionMgr.isRangeActive());
+
+        mLock.checkUnlocked();
+
+        mStarted = true;
+        mLock.block();
+    }
+
+    @Override
+    /** @hide */
+    public boolean onInterceptTouchEvent(RecyclerView unused, MotionEvent e) {
+        if (MotionEvents.isMouseEvent(e)) {
+            if (Shared.DEBUG) Log.w(TAG, "Unexpected Mouse event. Check configuration.");
+        }
+
+        switch (e.getActionMasked()) {
+            case MotionEvent.ACTION_DOWN:
+                // NOTE: Unlike events with other actions, RecyclerView eats
+                // "DOWN" events. So even if we return true here we'll
+                // never see an event w/ ACTION_DOWN passed to onTouchEvent.
+                return handleInterceptedDownEvent(e);
+            case MotionEvent.ACTION_MOVE:
+                return mStarted;
+        }
+
+        return false;
+    }
+
+    @Override
+    /** @hide */
+    public void onTouchEvent(RecyclerView unused, MotionEvent e) {
+        checkState(mStarted);
+
+        switch (e.getActionMasked()) {
+            case MotionEvent.ACTION_MOVE:
+                handleMoveEvent(e);
+                break;
+            case MotionEvent.ACTION_UP:
+                handleUpEvent(e);
+                break;
+            case MotionEvent.ACTION_CANCEL:
+                handleCancelEvent(e);
+                break;
+        }
+    }
+
+    @Override
+    /** @hide */
+    public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+    }
+
+    // Called when an ACTION_DOWN event is intercepted.
+    // If down event happens on an item, we mark that item's position as last started.
+    private boolean handleInterceptedDownEvent(MotionEvent e) {
+        mLastStartedItemPos = mView.getItemUnder(e);
+        return mLastStartedItemPos != RecyclerView.NO_POSITION;
+    }
+
+    // Called when ACTION_UP event is to be handled.
+    // Essentially, since this means all gesture movement is over, reset everything and apply
+    // provisional selection.
+    private void handleUpEvent(MotionEvent e) {
+        mSelectionMgr.mergeProvisionalSelection();
+        endSelection();
+        if (mLastStartedItemPos > -1) {
+            mSelectionMgr.startRange(mLastStartedItemPos);
+        }
+    }
+
+    // Called when ACTION_CANCEL event is to be handled.
+    // This means this gesture selection is aborted, so reset everything and abandon provisional
+    // selection.
+    private void handleCancelEvent(MotionEvent unused) {
+        mSelectionMgr.clearProvisionalSelection();
+        endSelection();
+    }
+
+    private void endSelection() {
+        checkState(mStarted);
+
+        mLastStartedItemPos = -1;
+        mStarted = false;
+        mScroller.reset();
+        mLock.unblock();
+    }
+
+    // Call when an intercepted ACTION_MOVE event is passed down.
+    // At this point, we are sure user wants to gesture multi-select.
+    private void handleMoveEvent(MotionEvent e) {
+        mLastInterceptedPoint = MotionEvents.getOrigin(e);
+
+        int lastGlidedItemPos = mView.getLastGlidedItemPosition(e);
+        if (lastGlidedItemPos != RecyclerView.NO_POSITION) {
+            extendSelection(lastGlidedItemPos);
+        }
+
+        mScroller.scroll(mLastInterceptedPoint);
+    }
+
+    // It's possible for events to go over the top/bottom of the RecyclerView.
+    // We want to get a Y-coordinate within the RecyclerView so we can find the childView underneath
+    // correctly.
+    private static float getInboundY(float max, float y) {
+        if (y < 0f) {
+            return 0f;
+        } else if (y > max) {
+            return max;
+        }
+        return y;
+    }
+
+    /* Given the end position, select everything in-between.
+     * @param endPos  The adapter position of the end item.
+     */
+    private void extendSelection(int endPos) {
+        mSelectionMgr.extendProvisionalRange(endPos);
+    }
+
+    /**
+     * Returns a new instance of GestureSelectionHelper.
+     */
+    static GestureSelectionHelper create(
+            SelectionHelper selectionMgr,
+            RecyclerView recView,
+            AutoScroller scroller,
+            ContentLock lock) {
+
+        return new GestureSelectionHelper(
+                selectionMgr,
+                new RecyclerViewDelegate(recView),
+                scroller,
+                lock);
+    }
+
+    @VisibleForTesting
+    abstract static class ViewDelegate {
+        abstract int getHeight();
+
+        abstract int getItemUnder(MotionEvent e);
+
+        abstract int getLastGlidedItemPosition(MotionEvent e);
+    }
+
+    @VisibleForTesting
+    static final class RecyclerViewDelegate extends ViewDelegate {
+
+        private final RecyclerView mRecView;
+
+        RecyclerViewDelegate(RecyclerView view) {
+            checkArgument(view != null);
+            mRecView = view;
+        }
+
+        @Override
+        int getHeight() {
+            return mRecView.getHeight();
+        }
+
+        @Override
+        int getItemUnder(MotionEvent e) {
+            View child = mRecView.findChildViewUnder(e.getX(), e.getY());
+            return child != null
+                    ? mRecView.getChildAdapterPosition(child)
+                    : RecyclerView.NO_POSITION;
+        }
+
+        @Override
+        int getLastGlidedItemPosition(MotionEvent e) {
+            // If user has moved his pointer to the bottom-right empty pane (ie. to the right of the
+            // last item of the recycler view), we would want to set that as the currentItemPos
+            View lastItem = mRecView.getLayoutManager()
+                    .getChildAt(mRecView.getLayoutManager().getChildCount() - 1);
+            int direction = ViewCompat.getLayoutDirection(mRecView);
+            final boolean pastLastItem = isPastLastItem(lastItem.getTop(),
+                    lastItem.getLeft(),
+                    lastItem.getRight(),
+                    e,
+                    direction);
+
+            // Since views get attached & detached from RecyclerView,
+            // {@link LayoutManager#getChildCount} can return a different number from the actual
+            // number
+            // of items in the adapter. Using the adapter is the for sure way to get the actual last
+            // item position.
+            final float inboundY = getInboundY(mRecView.getHeight(), e.getY());
+            return (pastLastItem) ? mRecView.getAdapter().getItemCount() - 1
+                    : mRecView.getChildAdapterPosition(
+                            mRecView.findChildViewUnder(e.getX(), inboundY));
+        }
+
+        /*
+         * Check to see if MotionEvent if past a particular item, i.e. to the right or to the bottom
+         * of the item.
+         * For RTL, it would to be to the left or to the bottom of the item.
+         */
+        @VisibleForTesting
+        static boolean isPastLastItem(int top, int left, int right, MotionEvent e, int direction) {
+            if (direction == View.LAYOUT_DIRECTION_LTR) {
+                return e.getX() > right && e.getY() > top;
+            } else {
+                return e.getX() < left && e.getY() > top;
+            }
+        }
+    }
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/GridModel.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/GridModel.java
new file mode 100644
index 0000000..4358958
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/GridModel.java
@@ -0,0 +1,786 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import static android.support.v4.util.Preconditions.checkArgument;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.support.annotation.VisibleForTesting;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.OnScrollListener;
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+import android.util.SparseIntArray;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import androidx.recyclerview.selection.SelectionHelper.SelectionPredicate;
+
+/**
+ * Provides a band selection item model for views within a RecyclerView. This class queries the
+ * RecyclerView to determine where its items are placed; then, once band selection is underway,
+ * it alerts listeners of which items are covered by the selections.
+ *
+ * @param <K> Selection key type. Usually String or Long.
+ */
+final class GridModel<K> {
+
+    // Magical value indicating that a value has not been previously set. primitive null :)
+    static final int NOT_SET = -1;
+
+    // Enum values used to determine the corner at which the origin is located within the
+    private static final int UPPER = 0x00;
+    private static final int LOWER = 0x01;
+    private static final int LEFT = 0x00;
+    private static final int RIGHT = 0x02;
+    private static final int UPPER_LEFT = UPPER | LEFT;
+    private static final int UPPER_RIGHT = UPPER | RIGHT;
+    private static final int LOWER_LEFT = LOWER | LEFT;
+    private static final int LOWER_RIGHT = LOWER | RIGHT;
+
+    private final GridHost<K> mHost;
+    private final ItemKeyProvider<K> mKeyProvider;
+    private final SelectionPredicate<K> mSelectionPredicate;
+
+    private final List<SelectionObserver> mOnSelectionChangedListeners = new ArrayList<>();
+
+    // Map from the x-value of the left side of a SparseBooleanArray of adapter positions, keyed
+    // by their y-offset. For example, if the first column of the view starts at an x-value of 5,
+    // mColumns.get(5) would return an array of positions in that column. Within that array, the
+    // value for key y is the adapter position for the item whose y-offset is y.
+    private final SparseArray<SparseIntArray> mColumns = new SparseArray<>();
+
+    // List of limits along the x-axis (columns).
+    // This list is sorted from furthest left to furthest right.
+    private final List<Limits> mColumnBounds = new ArrayList<>();
+
+    // List of limits along the y-axis (rows). Note that this list only contains items which
+    // have been in the viewport.
+    private final List<Limits> mRowBounds = new ArrayList<>();
+
+    // The adapter positions which have been recorded so far.
+    private final SparseBooleanArray mKnownPositions = new SparseBooleanArray();
+
+    // Array passed to registered OnSelectionChangedListeners. One array is created and reused
+    // throughout the lifetime of the object.
+    private final Set<K> mSelection = new HashSet<>();
+
+    // The current pointer (in absolute positioning from the top of the view).
+    private Point mPointer;
+
+    // The bounds of the band selection.
+    private RelativePoint mRelOrigin;
+    private RelativePoint mRelPointer;
+
+    private boolean mIsActive;
+
+    // Tracks where the band select originated from. This is used to determine where selections
+    // should expand from when Shift+click is used.
+    private int mPositionNearestOrigin = NOT_SET;
+
+    private final OnScrollListener mScrollListener;
+
+    GridModel(
+            GridHost host,
+            ItemKeyProvider<K> keyProvider,
+            SelectionPredicate<K> selectionPredicate) {
+
+        checkArgument(host != null);
+        checkArgument(keyProvider != null);
+        checkArgument(selectionPredicate != null);
+
+        mHost = host;
+        mKeyProvider = keyProvider;
+        mSelectionPredicate = selectionPredicate;
+
+        mScrollListener = new OnScrollListener() {
+            @Override
+            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+                GridModel.this.onScrolled(recyclerView, dx, dy);
+            }
+        };
+
+        mHost.addOnScrollListener(mScrollListener);
+    }
+
+    /**
+     * Start a band select operation at the given point.
+     *
+     * @param relativeOrigin The origin of the band select operation, relative to the viewport.
+     *                       For example, if the view is scrolled to the bottom, the top-left of
+     *                       the
+     *                       viewport
+     *                       would have a relative origin of (0, 0), even though its absolute point
+     *                       has a higher
+     *                       y-value.
+     */
+    void startCapturing(Point relativeOrigin) {
+        recordVisibleChildren();
+        if (isEmpty()) {
+            // The selection band logic works only if there is at least one visible child.
+            return;
+        }
+
+        mIsActive = true;
+        mPointer = mHost.createAbsolutePoint(relativeOrigin);
+        mRelOrigin = createRelativePoint(mPointer);
+        mRelPointer = createRelativePoint(mPointer);
+        computeCurrentSelection();
+        notifySelectionChanged();
+    }
+
+    /**
+     * Ends the band selection.
+     */
+    void stopCapturing() {
+        mIsActive = false;
+    }
+
+    /**
+     * Resizes the selection by adjusting the pointer (i.e., the corner of the selection
+     * opposite the origin.
+     *
+     * @param relativePointer The pointer (opposite of the origin) of the band select operation,
+     *                        relative to the viewport. For example, if the view is scrolled to the
+     *                        bottom, the
+     *                        top-left of the viewport would have a relative origin of (0, 0), even
+     *                        though its
+     *                        absolute point has a higher y-value.
+     */
+    @VisibleForTesting
+    void resizeSelection(Point relativePointer) {
+        mPointer = mHost.createAbsolutePoint(relativePointer);
+        updateModel();
+    }
+
+    /**
+     * @return The adapter position for the item nearest the origin corresponding to the latest
+     * band select operation, or NOT_SET if the selection did not cover any items.
+     */
+    int getPositionNearestOrigin() {
+        return mPositionNearestOrigin;
+    }
+
+    private void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+        if (!mIsActive) {
+            return;
+        }
+
+        mPointer.x += dx;
+        mPointer.y += dy;
+        recordVisibleChildren();
+        updateModel();
+    }
+
+    /**
+     * Queries the view for all children and records their location metadata.
+     */
+    private void recordVisibleChildren() {
+        for (int i = 0; i < mHost.getVisibleChildCount(); i++) {
+            int adapterPosition = mHost.getAdapterPositionAt(i);
+            // Sometimes the view is not attached, as we notify the multi selection manager
+            // synchronously, while views are attached asynchronously. As a result items which
+            // are in the adapter may not actually have a corresponding view (yet).
+            if (mHost.hasView(adapterPosition)
+                    && mSelectionPredicate.canSetStateAtPosition(adapterPosition, true)
+                    && !mKnownPositions.get(adapterPosition)) {
+                mKnownPositions.put(adapterPosition, true);
+                recordItemData(mHost.getAbsoluteRectForChildViewAt(i), adapterPosition);
+            }
+        }
+    }
+
+    /**
+     * Checks if there are any recorded children.
+     */
+    private boolean isEmpty() {
+        return mColumnBounds.size() == 0 || mRowBounds.size() == 0;
+    }
+
+    /**
+     * Updates the limits lists and column map with the given item metadata.
+     *
+     * @param absoluteChildRect The absolute rectangle for the child view being processed.
+     * @param adapterPosition   The position of the child view being processed.
+     */
+    private void recordItemData(Rect absoluteChildRect, int adapterPosition) {
+        if (mColumnBounds.size() != mHost.getColumnCount()) {
+            // If not all x-limits have been recorded, record this one.
+            recordLimits(
+                    mColumnBounds, new Limits(absoluteChildRect.left, absoluteChildRect.right));
+        }
+
+        recordLimits(mRowBounds, new Limits(absoluteChildRect.top, absoluteChildRect.bottom));
+
+        SparseIntArray columnList = mColumns.get(absoluteChildRect.left);
+        if (columnList == null) {
+            columnList = new SparseIntArray();
+            mColumns.put(absoluteChildRect.left, columnList);
+        }
+        columnList.put(absoluteChildRect.top, adapterPosition);
+    }
+
+    /**
+     * Ensures limits exists within the sorted list limitsList, and adds it to the list if it
+     * does not exist.
+     */
+    private void recordLimits(List<Limits> limitsList, Limits limits) {
+        int index = Collections.binarySearch(limitsList, limits);
+        if (index < 0) {
+            limitsList.add(~index, limits);
+        }
+    }
+
+    /**
+     * Handles a moved pointer; this function determines whether the pointer movement resulted
+     * in a selection change and, if it has, notifies listeners of this change.
+     */
+    private void updateModel() {
+        RelativePoint old = mRelPointer;
+        mRelPointer = createRelativePoint(mPointer);
+        if (old != null && mRelPointer.equals(old)) {
+            return;
+        }
+
+        computeCurrentSelection();
+        notifySelectionChanged();
+    }
+
+    /**
+     * Computes the currently-selected items.
+     */
+    private void computeCurrentSelection() {
+        if (areItemsCoveredByBand(mRelPointer, mRelOrigin)) {
+            updateSelection(computeBounds());
+        } else {
+            mSelection.clear();
+            mPositionNearestOrigin = NOT_SET;
+        }
+    }
+
+    /**
+     * Notifies all listeners of a selection change. Note that this function simply passes
+     * mSelection, so computeCurrentSelection() should be called before this
+     * function.
+     */
+    private void notifySelectionChanged() {
+        for (SelectionObserver listener : mOnSelectionChangedListeners) {
+            listener.onSelectionChanged(mSelection);
+        }
+    }
+
+    /**
+     * @param rect Rectangle including all covered items.
+     */
+    private void updateSelection(Rect rect) {
+        int columnStart =
+                Collections.binarySearch(mColumnBounds, new Limits(rect.left, rect.left));
+
+        checkArgument(columnStart >= 0, "Rect doesn't intesect any known column.");
+
+        int columnEnd = columnStart;
+
+        for (int i = columnStart; i < mColumnBounds.size()
+                && mColumnBounds.get(i).lowerLimit <= rect.right; i++) {
+            columnEnd = i;
+        }
+
+        int rowStart = Collections.binarySearch(mRowBounds, new Limits(rect.top, rect.top));
+        if (rowStart < 0) {
+            mPositionNearestOrigin = NOT_SET;
+            return;
+        }
+
+        int rowEnd = rowStart;
+        for (int i = rowStart; i < mRowBounds.size()
+                && mRowBounds.get(i).lowerLimit <= rect.bottom; i++) {
+            rowEnd = i;
+        }
+
+        updateSelection(columnStart, columnEnd, rowStart, rowEnd);
+    }
+
+    /**
+     * Computes the selection given the previously-computed start- and end-indices for each
+     * row and column.
+     */
+    private void updateSelection(
+            int columnStartIndex, int columnEndIndex, int rowStartIndex, int rowEndIndex) {
+
+        if (BandSelectionHelper.DEBUG) {
+            Log.d(BandSelectionHelper.TAG, String.format(
+                    "updateSelection: %d, %d, %d, %d",
+                    columnStartIndex, columnEndIndex, rowStartIndex, rowEndIndex));
+        }
+
+        mSelection.clear();
+        for (int column = columnStartIndex; column <= columnEndIndex; column++) {
+            SparseIntArray items = mColumns.get(mColumnBounds.get(column).lowerLimit);
+            for (int row = rowStartIndex; row <= rowEndIndex; row++) {
+                // The default return value for SparseIntArray.get is 0, which is a valid
+                // position. Use a sentry value to prevent erroneously selecting item 0.
+                final int rowKey = mRowBounds.get(row).lowerLimit;
+                int position = items.get(rowKey, NOT_SET);
+                if (position != NOT_SET) {
+                    K key = mKeyProvider.getKey(position);
+                    if (key != null) {
+                        // The adapter inserts items for UI layout purposes that aren't
+                        // associated with files. Those will have a null model ID.
+                        // Don't select them.
+                        if (canSelect(key)) {
+                            mSelection.add(key);
+                        }
+                    }
+                    if (isPossiblePositionNearestOrigin(column, columnStartIndex, columnEndIndex,
+                            row, rowStartIndex, rowEndIndex)) {
+                        // If this is the position nearest the origin, record it now so that it
+                        // can be returned by endSelection() later.
+                        mPositionNearestOrigin = position;
+                    }
+                }
+            }
+        }
+    }
+
+    private boolean canSelect(K key) {
+        return mSelectionPredicate.canSetStateForKey(key, true);
+    }
+
+    /**
+     * @return Returns true if the position is the nearest to the origin, or, in the case of the
+     * lower-right corner, whether it is possible that the position is the nearest to the
+     * origin. See comment below for reasoning for this special case.
+     */
+    private boolean isPossiblePositionNearestOrigin(int columnIndex, int columnStartIndex,
+            int columnEndIndex, int rowIndex, int rowStartIndex, int rowEndIndex) {
+        int corner = computeCornerNearestOrigin();
+        switch (corner) {
+            case UPPER_LEFT:
+                return columnIndex == columnStartIndex && rowIndex == rowStartIndex;
+            case UPPER_RIGHT:
+                return columnIndex == columnEndIndex && rowIndex == rowStartIndex;
+            case LOWER_LEFT:
+                return columnIndex == columnStartIndex && rowIndex == rowEndIndex;
+            case LOWER_RIGHT:
+                // Note that in some cases, the last row will not have as many items as there
+                // are columns (e.g., if there are 4 items and 3 columns, the second row will
+                // only have one item in the first column). This function is invoked for each
+                // position from left to right, so return true for any position in the bottom
+                // row and only the right-most position in the bottom row will be recorded.
+                return rowIndex == rowEndIndex;
+            default:
+                throw new RuntimeException("Invalid corner type.");
+        }
+    }
+
+    /**
+     * Listener for changes in which items have been band selected.
+     */
+    public abstract static class SelectionObserver<K> {
+        abstract void onSelectionChanged(Set<K> updatedSelection);
+    }
+
+    void addOnSelectionChangedListener(SelectionObserver listener) {
+        mOnSelectionChangedListeners.add(listener);
+    }
+
+    /**
+     * Called when {@link BandSelectionHelper} is finished with a GridModel.
+     */
+    void onDestroy() {
+        mOnSelectionChangedListeners.clear();
+        // Cleanup listeners to prevent memory leaks.
+        mHost.removeOnScrollListener(mScrollListener);
+    }
+
+    /**
+     * Limits of a view item. For example, if an item's left side is at x-value 5 and its right side
+     * is at x-value 10, the limits would be from 5 to 10. Used to record the left- and right sides
+     * of item columns and the top- and bottom sides of item rows so that it can be determined
+     * whether the pointer is located within the bounds of an item.
+     */
+    private static class Limits implements Comparable<Limits> {
+        public int lowerLimit;
+        public int upperLimit;
+
+        Limits(int lowerLimit, int upperLimit) {
+            this.lowerLimit = lowerLimit;
+            this.upperLimit = upperLimit;
+        }
+
+        @Override
+        public int compareTo(Limits other) {
+            return lowerLimit - other.lowerLimit;
+        }
+
+        @Override
+        public int hashCode() {
+            return lowerLimit ^ upperLimit;
+        }
+
+        @Override
+        public boolean equals(Object other) {
+            if (!(other instanceof Limits)) {
+                return false;
+            }
+
+            return ((Limits) other).lowerLimit == lowerLimit
+                    && ((Limits) other).upperLimit == upperLimit;
+        }
+
+        @Override
+        public String toString() {
+            return "(" + lowerLimit + ", " + upperLimit + ")";
+        }
+    }
+
+    /**
+     * The location of a coordinate relative to items. This class represents a general area of the
+     * view as it relates to band selection rather than an explicit point. For example, two
+     * different points within an item are considered to have the same "location" because band
+     * selection originating within the item would select the same items no matter which point
+     * was used. Same goes for points between items as well as those at the very beginning or end
+     * of the view.
+     *
+     * Tracking a coordinate (e.g., an x-value) as a CoordinateLocation instead of as an int has the
+     * advantage of tying the value to the Limits of items along that axis. This allows easy
+     * selection of items within those Limits as opposed to a search through every item to see if a
+     * given coordinate value falls within those Limits.
+     */
+    private static class RelativeCoordinate
+            implements Comparable<RelativeCoordinate> {
+        /**
+         * Location describing points after the last known item.
+         */
+        static final int AFTER_LAST_ITEM = 0;
+
+        /**
+         * Location describing points before the first known item.
+         */
+        static final int BEFORE_FIRST_ITEM = 1;
+
+        /**
+         * Location describing points between two items.
+         */
+        static final int BETWEEN_TWO_ITEMS = 2;
+
+        /**
+         * Location describing points within the limits of one item.
+         */
+        static final int WITHIN_LIMITS = 3;
+
+        /**
+         * The type of this coordinate, which is one of AFTER_LAST_ITEM, BEFORE_FIRST_ITEM,
+         * BETWEEN_TWO_ITEMS, or WITHIN_LIMITS.
+         */
+        public final int type;
+
+        /**
+         * The limits before the coordinate; only populated when type == WITHIN_LIMITS or type ==
+         * BETWEEN_TWO_ITEMS.
+         */
+        public Limits limitsBeforeCoordinate;
+
+        /**
+         * The limits after the coordinate; only populated when type == BETWEEN_TWO_ITEMS.
+         */
+        public Limits limitsAfterCoordinate;
+
+        // Limits of the first known item; only populated when type == BEFORE_FIRST_ITEM.
+        public Limits mFirstKnownItem;
+        // Limits of the last known item; only populated when type == AFTER_LAST_ITEM.
+        public Limits mLastKnownItem;
+
+        /**
+         * @param limitsList The sorted limits list for the coordinate type. If this
+         *                   CoordinateLocation is an x-value, mXLimitsList should be passed;
+         *                   otherwise,
+         *                   mYLimitsList should be pased.
+         * @param value      The coordinate value.
+         */
+        RelativeCoordinate(List<Limits> limitsList, int value) {
+            int index = Collections.binarySearch(limitsList, new Limits(value, value));
+
+            if (index >= 0) {
+                this.type = WITHIN_LIMITS;
+                this.limitsBeforeCoordinate = limitsList.get(index);
+            } else if (~index == 0) {
+                this.type = BEFORE_FIRST_ITEM;
+                this.mFirstKnownItem = limitsList.get(0);
+            } else if (~index == limitsList.size()) {
+                Limits lastLimits = limitsList.get(limitsList.size() - 1);
+                if (lastLimits.lowerLimit <= value && value <= lastLimits.upperLimit) {
+                    this.type = WITHIN_LIMITS;
+                    this.limitsBeforeCoordinate = lastLimits;
+                } else {
+                    this.type = AFTER_LAST_ITEM;
+                    this.mLastKnownItem = lastLimits;
+                }
+            } else {
+                Limits limitsBeforeIndex = limitsList.get(~index - 1);
+                if (limitsBeforeIndex.lowerLimit <= value
+                        && value <= limitsBeforeIndex.upperLimit) {
+                    this.type = WITHIN_LIMITS;
+                    this.limitsBeforeCoordinate = limitsList.get(~index - 1);
+                } else {
+                    this.type = BETWEEN_TWO_ITEMS;
+                    this.limitsBeforeCoordinate = limitsList.get(~index - 1);
+                    this.limitsAfterCoordinate = limitsList.get(~index);
+                }
+            }
+        }
+
+        int toComparisonValue() {
+            if (type == BEFORE_FIRST_ITEM) {
+                return mFirstKnownItem.lowerLimit - 1;
+            } else if (type == AFTER_LAST_ITEM) {
+                return mLastKnownItem.upperLimit + 1;
+            } else if (type == BETWEEN_TWO_ITEMS) {
+                return limitsBeforeCoordinate.upperLimit + 1;
+            } else {
+                return limitsBeforeCoordinate.lowerLimit;
+            }
+        }
+
+        @Override
+        public int hashCode() {
+            return mFirstKnownItem.lowerLimit
+                    ^ mLastKnownItem.upperLimit
+                    ^ limitsBeforeCoordinate.upperLimit
+                    ^ limitsBeforeCoordinate.lowerLimit;
+        }
+
+        @Override
+        public boolean equals(Object other) {
+            if (!(other instanceof RelativeCoordinate)) {
+                return false;
+            }
+
+            RelativeCoordinate otherCoordinate = (RelativeCoordinate) other;
+            return toComparisonValue() == otherCoordinate.toComparisonValue();
+        }
+
+        @Override
+        public int compareTo(RelativeCoordinate other) {
+            return toComparisonValue() - other.toComparisonValue();
+        }
+    }
+
+    RelativePoint createRelativePoint(Point point) {
+        return new RelativePoint(
+                new RelativeCoordinate(mColumnBounds, point.x),
+                new RelativeCoordinate(mRowBounds, point.y));
+    }
+
+    /**
+     * The location of a point relative to the Limits of nearby items; consists of both an x- and
+     * y-RelativeCoordinateLocation.
+     */
+    private static class RelativePoint {
+
+        final RelativeCoordinate mX;
+        final RelativeCoordinate mY;
+
+        RelativePoint(List<Limits> columnLimits, List<Limits> rowLimits, Point point) {
+            this.mX = new RelativeCoordinate(columnLimits, point.x);
+            this.mY = new RelativeCoordinate(rowLimits, point.y);
+        }
+
+        RelativePoint(RelativeCoordinate x, RelativeCoordinate y) {
+            this.mX = x;
+            this.mY = y;
+        }
+
+        @Override
+        public int hashCode() {
+            return mX.toComparisonValue() ^ mY.toComparisonValue();
+        }
+
+        @Override
+        public boolean equals(Object other) {
+            if (!(other instanceof RelativePoint)) {
+                return false;
+            }
+
+            RelativePoint otherPoint = (RelativePoint) other;
+            return mX.equals(otherPoint.mX) && mY.equals(otherPoint.mY);
+        }
+    }
+
+    /**
+     * Generates a rectangle which contains the items selected by the pointer and origin.
+     *
+     * @return The rectangle, or null if no items were selected.
+     */
+    private Rect computeBounds() {
+        Rect rect = new Rect();
+        rect.left = getCoordinateValue(
+                min(mRelOrigin.mX, mRelPointer.mX),
+                mColumnBounds,
+                true);
+        rect.right = getCoordinateValue(
+                max(mRelOrigin.mX, mRelPointer.mX),
+                mColumnBounds,
+                false);
+        rect.top = getCoordinateValue(
+                min(mRelOrigin.mY, mRelPointer.mY),
+                mRowBounds,
+                true);
+        rect.bottom = getCoordinateValue(
+                max(mRelOrigin.mY, mRelPointer.mY),
+                mRowBounds,
+                false);
+        return rect;
+    }
+
+    /**
+     * Computes the corner of the selection nearest the origin.
+     */
+    private int computeCornerNearestOrigin() {
+        int cornerValue = 0;
+
+        if (mRelOrigin.mY.equals(min(mRelOrigin.mY, mRelPointer.mY))) {
+            cornerValue |= UPPER;
+        } else {
+            cornerValue |= LOWER;
+        }
+
+        if (mRelOrigin.mX.equals(min(mRelOrigin.mX, mRelPointer.mX))) {
+            cornerValue |= LEFT;
+        } else {
+            cornerValue |= RIGHT;
+        }
+
+        return cornerValue;
+    }
+
+    private RelativeCoordinate min(RelativeCoordinate first, RelativeCoordinate second) {
+        return first.compareTo(second) < 0 ? first : second;
+    }
+
+    private RelativeCoordinate max(RelativeCoordinate first, RelativeCoordinate second) {
+        return first.compareTo(second) > 0 ? first : second;
+    }
+
+    /**
+     * @return The absolute coordinate (i.e., the x- or y-value) of the given relative
+     * coordinate.
+     */
+    private int getCoordinateValue(
+            RelativeCoordinate coordinate, List<Limits> limitsList, boolean isStartOfRange) {
+
+        switch (coordinate.type) {
+            case RelativeCoordinate.BEFORE_FIRST_ITEM:
+                return limitsList.get(0).lowerLimit;
+            case RelativeCoordinate.AFTER_LAST_ITEM:
+                return limitsList.get(limitsList.size() - 1).upperLimit;
+            case RelativeCoordinate.BETWEEN_TWO_ITEMS:
+                if (isStartOfRange) {
+                    return coordinate.limitsAfterCoordinate.lowerLimit;
+                } else {
+                    return coordinate.limitsBeforeCoordinate.upperLimit;
+                }
+            case RelativeCoordinate.WITHIN_LIMITS:
+                return coordinate.limitsBeforeCoordinate.lowerLimit;
+        }
+
+        throw new RuntimeException("Invalid coordinate value.");
+    }
+
+    private boolean areItemsCoveredByBand(
+            RelativePoint first, RelativePoint second) {
+
+        return doesCoordinateLocationCoverItems(first.mX, second.mX)
+                && doesCoordinateLocationCoverItems(first.mY, second.mY);
+    }
+
+    private boolean doesCoordinateLocationCoverItems(
+            RelativeCoordinate pointerCoordinate, RelativeCoordinate originCoordinate) {
+
+        if (pointerCoordinate.type == RelativeCoordinate.BEFORE_FIRST_ITEM
+                && originCoordinate.type == RelativeCoordinate.BEFORE_FIRST_ITEM) {
+            return false;
+        }
+
+        if (pointerCoordinate.type == RelativeCoordinate.AFTER_LAST_ITEM
+                && originCoordinate.type == RelativeCoordinate.AFTER_LAST_ITEM) {
+            return false;
+        }
+
+        if (pointerCoordinate.type == RelativeCoordinate.BETWEEN_TWO_ITEMS
+                && originCoordinate.type == RelativeCoordinate.BETWEEN_TWO_ITEMS
+                && pointerCoordinate.limitsBeforeCoordinate.equals(
+                originCoordinate.limitsBeforeCoordinate)
+                && pointerCoordinate.limitsAfterCoordinate.equals(
+                originCoordinate.limitsAfterCoordinate)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Provides functionality for BandController. Exists primarily to tests that are
+     * fully isolated from RecyclerView.
+     *
+     * @param <K> Selection key type. Usually String or Long.
+     */
+    abstract static class GridHost<K> extends BandSelectionHelper.BandHost<K> {
+
+        /**
+         * Remove the listener.
+         *
+         * @param listener
+         */
+        abstract void removeOnScrollListener(RecyclerView.OnScrollListener listener);
+
+        /**
+         * @param relativePoint for which to create absolute point.
+         * @return absolute point.
+         */
+        abstract Point createAbsolutePoint(Point relativePoint);
+
+        /**
+         * @param index index of child.
+         * @return rectangle describing child at {@code index}.
+         */
+        abstract Rect getAbsoluteRectForChildViewAt(int index);
+
+        /**
+         * @param index index of child.
+         * @return child adapter position for the child at {@code index}
+         */
+        abstract int getAdapterPositionAt(int index);
+
+        /** @return column count. */
+        abstract int getColumnCount();
+
+        /** @return number of children visible in the view. */
+        abstract int getVisibleChildCount();
+
+        /**
+         * @return true if the item at adapter position is attached to a view.
+         */
+        abstract boolean hasView(int adapterPosition);
+    }
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/ItemDetailsLookup.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/ItemDetailsLookup.java
new file mode 100644
index 0000000..da30c97
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/ItemDetailsLookup.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
+import android.support.v7.widget.RecyclerView;
+import android.view.MotionEvent;
+
+/**
+ * Provides event handlers w/ access to details about documents details
+ * view items Documents in the UI (RecyclerView).
+ *
+ * @param <K> Selection key type. Usually String or Long.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public abstract class ItemDetailsLookup<K> {
+
+    /** @return true if there is an item under the finger/cursor. */
+    public boolean overItem(MotionEvent e) {
+        return getItemPosition(e) != RecyclerView.NO_POSITION;
+    }
+
+    /** @return true if there is an item w/ a stable ID under the finger/cursor. */
+    public boolean overItemWithSelectionKey(MotionEvent e) {
+        return overItem(e) && hasSelectionKey(getItemDetails(e));
+    }
+
+    /**
+     * @return true if the event is over an area that can be dragged via touch
+     * or via mouse. List items have a white area that is not draggable.
+     */
+    public boolean inItemDragRegion(MotionEvent e) {
+        return overItem(e) && getItemDetails(e).inDragRegion(e);
+    }
+
+    /**
+     * @return true if the event is in the "selection hot spot" region.
+     * The hot spot region instantly selects in touch mode, vs launches.
+     */
+    public boolean inItemSelectRegion(MotionEvent e) {
+        return overItem(e) && getItemDetails(e).inSelectionHotspot(e);
+    }
+
+    /**
+     * @return the adapter position of the item under the finger/cursor.
+     */
+    public int getItemPosition(MotionEvent e) {
+        @Nullable ItemDetails<?> item = getItemDetails(e);
+        return item != null
+                ? item.getPosition()
+                : RecyclerView.NO_POSITION;
+    }
+
+    private static boolean hasSelectionKey(@Nullable ItemDetails<?> item) {
+        return item != null && item.getSelectionKey() != null;
+    }
+
+    private static boolean hasPosition(@Nullable ItemDetails<?> item) {
+        return item != null && item.getPosition() != RecyclerView.NO_POSITION;
+    }
+
+    /**
+     * @return the DocumentDetails for the item under the event, or null.
+     */
+    public abstract @Nullable ItemDetails<K> getItemDetails(MotionEvent e);
+
+    /**
+     * Abstract class providing helper classes with access to information about
+     * RecyclerView item associated with a MotionEvent.
+     *
+     * @param <K> Selection key type. Usually String or Long.
+     */
+    // TODO: Can this be merged with ViewHolder?
+    public abstract static class ItemDetails<K> {
+
+        /** @return the position of an item. */
+        public abstract int getPosition();
+
+        /** @return true if the item has a stable id. */
+        public boolean hasSelectionKey() {
+            return getSelectionKey() != null;
+        }
+
+        /** @return the stable id of an item. */
+        public abstract @Nullable K getSelectionKey();
+
+        /**
+         * @return true if the event is in an area of the item that should be
+         * directly interpreted as a user wishing to select the item. This
+         * is useful for checkboxes and other UI affordances focused on enabling
+         * selection.
+         */
+        public boolean inSelectionHotspot(MotionEvent e) {
+            return false;
+        }
+
+        /**
+         * Events in the drag region will dealt with differently that events outside
+         * of the drag region. This allows the client to implement custom handling
+         * for events related to drag and drop.
+         */
+        public boolean inDragRegion(MotionEvent e) {
+            return false;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (obj instanceof ItemDetails) {
+                return isEqualTo((ItemDetails) obj);
+            }
+            return false;
+        }
+
+        private boolean isEqualTo(ItemDetails other) {
+            K key = getSelectionKey();
+            boolean sameKeys = false;
+            if (key == null) {
+                sameKeys = other.getSelectionKey() == null;
+            } else {
+                sameKeys = key.equals(other.getSelectionKey());
+            }
+            return sameKeys && this.getPosition() == other.getPosition();
+        }
+
+        @Override
+        public int hashCode() {
+            return getPosition() >>> 8;
+        }
+    }
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/ItemKeyProvider.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/ItemKeyProvider.java
new file mode 100644
index 0000000..134c442
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/ItemKeyProvider.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import static android.support.v4.util.Preconditions.checkArgument;
+
+import android.support.annotation.IntDef;
+import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
+import android.support.annotation.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Provides support for sting based stable ids in the RecyclerView selection helper.
+ * Client code can use this to look up stable ids when working with selection
+ * in application code.
+ *
+ * @param <K> Selection key type. Usually String or Long.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public abstract class ItemKeyProvider<K> {
+
+    /**
+     * Provides access to all data, regardless of whether it is bound to a view or not.
+     * Key providers with this access type enjoy support for enhanced features like:
+     * SHIFT+click range selection, and band selection.
+     */
+    @VisibleForTesting  // otherwise protected would do nicely.
+    public static final int SCOPE_MAPPED = 0;
+
+    /**
+     * Provides access cached data based on what was recently bound in the view.
+     * Employing this provider will result in a reduced feature-set, as some
+     * featuers like SHIFT+click range selection and band selection are dependent
+     * on mapped access.
+     */
+    @VisibleForTesting  // otherwise protected would do nicely.
+    public static final int SCOPE_CACHED = 1;
+
+    @IntDef({
+            SCOPE_MAPPED,
+            SCOPE_CACHED
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    protected @interface Scope {}
+
+    private final @Scope int mScope;
+
+    /**
+     * Creates a new provider with the given scope.
+     * @param scope Scope can't change at runtime (at least code won't adapt)
+     *         so it must be specified in the constructor.
+     */
+    protected ItemKeyProvider(@Scope int scope) {
+        checkArgument(scope == SCOPE_MAPPED || scope == SCOPE_CACHED);
+
+        mScope = scope;
+    }
+
+    final boolean hasAccess(@Scope int scope) {
+        return scope == mScope;
+    }
+
+    /**
+     * @return The selection key of the item at the given adapter position.
+     */
+    public abstract @Nullable K getKey(int position);
+
+    /**
+     * @return the position of a stable ID, or RecyclerView.NO_POSITION.
+     */
+    public abstract int getPosition(K key);
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/MotionEvents.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/MotionEvents.java
new file mode 100644
index 0000000..dd9e54f
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/MotionEvents.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import android.graphics.Point;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+/**
+ * Utility methods for working with {@link MotionEvent} instances.
+ */
+final class MotionEvents {
+
+    private MotionEvents() {}
+
+    static boolean isMouseEvent(MotionEvent e) {
+        return e.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE;
+    }
+
+    static boolean isTouchEvent(MotionEvent e) {
+        return e.getToolType(0) == MotionEvent.TOOL_TYPE_FINGER;
+    }
+
+    static boolean isActionMove(MotionEvent e) {
+        return e.getActionMasked() == MotionEvent.ACTION_MOVE;
+    }
+
+    static boolean isActionDown(MotionEvent e) {
+        return e.getActionMasked() == MotionEvent.ACTION_DOWN;
+    }
+
+    static boolean isActionUp(MotionEvent e) {
+        return e.getActionMasked() == MotionEvent.ACTION_UP;
+    }
+
+    static boolean isActionPointerUp(MotionEvent e) {
+        return e.getActionMasked() == MotionEvent.ACTION_POINTER_UP;
+    }
+
+    @SuppressWarnings("unused")
+    static boolean isActionPointerDown(MotionEvent e) {
+        return e.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN;
+    }
+
+    static boolean isActionCancel(MotionEvent e) {
+        return e.getActionMasked() == MotionEvent.ACTION_CANCEL;
+    }
+
+    static Point getOrigin(MotionEvent e) {
+        return new Point((int) e.getX(), (int) e.getY());
+    }
+
+    static boolean isPrimaryButtonPressed(MotionEvent e) {
+        return isButtonPressed(e, MotionEvent.BUTTON_PRIMARY);
+    }
+
+    static boolean isSecondaryButtonPressed(MotionEvent e) {
+        return isButtonPressed(e, MotionEvent.BUTTON_SECONDARY);
+    }
+
+    static boolean isTertiaryButtonPressed(MotionEvent e) {
+        return isButtonPressed(e, MotionEvent.BUTTON_TERTIARY);
+    }
+
+    // TODO: Replace with MotionEvent.isButtonPressed once targeting 21 or higher.
+    private static boolean isButtonPressed(MotionEvent e, int button) {
+        if (button == 0) {
+            return false;
+        }
+        return (e.getButtonState() & button) == button;
+    }
+
+    static boolean isShiftKeyPressed(MotionEvent e) {
+        return hasBit(e.getMetaState(), KeyEvent.META_SHIFT_ON);
+    }
+
+    static boolean isCtrlKeyPressed(MotionEvent e) {
+        return hasBit(e.getMetaState(), KeyEvent.META_CTRL_ON);
+    }
+
+    static boolean isAltKeyPressed(MotionEvent e) {
+        return hasBit(e.getMetaState(), KeyEvent.META_ALT_ON);
+    }
+
+    static boolean isTouchpadScroll(MotionEvent e) {
+        // Touchpad inputs are treated as mouse inputs, and when scrolling, there are no buttons
+        // returned.
+        return isMouseEvent(e) && isActionMove(e) && e.getButtonState() == 0;
+    }
+
+    private static boolean hasBit(int metaState, int bit) {
+        return (metaState & bit) != 0;
+    }
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/MotionInputHandler.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/MotionInputHandler.java
new file mode 100644
index 0000000..1c06302
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/MotionInputHandler.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import static android.support.v4.util.Preconditions.checkArgument;
+import static android.support.v4.util.Preconditions.checkState;
+
+import android.support.annotation.Nullable;
+import android.support.v7.widget.RecyclerView;
+import android.view.GestureDetector.SimpleOnGestureListener;
+import android.view.MotionEvent;
+
+import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
+
+/**
+ * Base class for handlers that can be registered w/ {@link GestureRouter}.
+ */
+abstract class MotionInputHandler<K> extends SimpleOnGestureListener {
+
+    protected final SelectionHelper<K> mSelectionHelper;
+
+    private final ItemKeyProvider<K> mKeyProvider;
+    private final FocusCallbacks<K> mFocusCallbacks;
+
+    MotionInputHandler(
+            SelectionHelper<K> selectionHelper,
+            ItemKeyProvider<K> keyProvider,
+            FocusCallbacks<K> focusCallbacks) {
+
+        checkArgument(selectionHelper != null);
+        checkArgument(keyProvider != null);
+        checkArgument(focusCallbacks != null);
+
+        mSelectionHelper = selectionHelper;
+        mKeyProvider = keyProvider;
+        mFocusCallbacks = focusCallbacks;
+    }
+
+    final boolean selectItem(ItemDetails<K> details) {
+        checkArgument(details != null);
+        checkArgument(hasPosition(details));
+        checkArgument(hasSelectionKey(details));
+
+        if (mSelectionHelper.select(details.getSelectionKey())) {
+            mSelectionHelper.anchorRange(details.getPosition());
+        }
+
+        // we set the focus on this doc so it will be the origin for keyboard events or shift+clicks
+        // if there is only a single item selected, otherwise clear focus
+        if (mSelectionHelper.getSelection().size() == 1) {
+            mFocusCallbacks.focusItem(details);
+        } else {
+            mFocusCallbacks.clearFocus();
+        }
+        return true;
+    }
+
+    protected final boolean focusItem(ItemDetails<K> details) {
+        checkArgument(details != null);
+        checkArgument(hasSelectionKey(details));
+
+        mSelectionHelper.clearSelection();
+        mFocusCallbacks.focusItem(details);
+        return true;
+    }
+
+    protected final void extendSelectionRange(ItemDetails<K> details) {
+        checkState(mKeyProvider.hasAccess(ItemKeyProvider.SCOPE_MAPPED));
+        checkArgument(hasPosition(details));
+        checkArgument(hasSelectionKey(details));
+
+        mSelectionHelper.extendRange(details.getPosition());
+        mFocusCallbacks.focusItem(details);
+    }
+
+    final boolean isRangeExtension(MotionEvent e) {
+        return MotionEvents.isShiftKeyPressed(e)
+                && mSelectionHelper.isRangeActive()
+                // Without full corpus access we can't reliably implement range
+                // as a user can scroll *anywhere* then SHIFT+click.
+                && mKeyProvider.hasAccess(ItemKeyProvider.SCOPE_MAPPED);
+    }
+
+    boolean shouldClearSelection(MotionEvent e, ItemDetails<K> item) {
+        return !MotionEvents.isCtrlKeyPressed(e)
+                && !item.inSelectionHotspot(e)
+                && !mSelectionHelper.isSelected(item.getSelectionKey());
+    }
+
+    static boolean hasSelectionKey(@Nullable ItemDetails<?> item) {
+        return item != null && item.getSelectionKey() != null;
+    }
+
+    static boolean hasPosition(@Nullable ItemDetails<?> item) {
+        return item != null && item.getPosition() != RecyclerView.NO_POSITION;
+    }
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/MouseCallbacks.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/MouseCallbacks.java
new file mode 100644
index 0000000..05c47c1
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/MouseCallbacks.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.support.annotation.RestrictTo;
+import android.view.MotionEvent;
+
+/**
+ * Override methods in this class to connect specialized behaviors of the selection
+ * code to the application environment.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public abstract class MouseCallbacks {
+
+    static final MouseCallbacks DUMMY = new MouseCallbacks() {
+        @Override
+        public boolean onContextClick(MotionEvent e) {
+            return false;
+        }
+    };
+
+    /**
+     * Called when user performs a context click, usually via mouse pointer
+     * right-click.
+     *
+     * @param e the event associated with the click.
+     * @return true if the event was handled.
+     */
+    public abstract boolean onContextClick(MotionEvent e);
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/MouseInputHandler.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/MouseInputHandler.java
new file mode 100644
index 0000000..0b4ea2c
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/MouseInputHandler.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import static android.support.v4.util.Preconditions.checkArgument;
+import static android.support.v4.util.Preconditions.checkState;
+
+import static androidx.recyclerview.selection.Shared.DEBUG;
+import static androidx.recyclerview.selection.Shared.VERBOSE;
+
+import android.support.annotation.Nullable;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.view.MotionEvent;
+
+import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
+
+/**
+ * A MotionInputHandler that provides the high-level glue for mouse/stylus driven selection. This
+ * class works with {@link RecyclerView}, {@link GestureRouter}, and {@link GestureSelectionHelper}
+ * to provide robust user drive selection support.
+ */
+final class MouseInputHandler<K> extends MotionInputHandler<K> {
+
+    private static final String TAG = "MouseInputDelegate";
+
+    private final ItemDetailsLookup<K> mDetailsLookup;
+    private final MouseCallbacks mMouseCallbacks;
+    private final ActivationCallbacks<K> mActivationCallbacks;
+    private final FocusCallbacks<K> mFocusCallbacks;
+
+    // The event has been handled in onSingleTapUp
+    private boolean mHandledTapUp;
+    // true when the previous event has consumed a right click motion event
+    private boolean mHandledOnDown;
+
+    MouseInputHandler(
+            SelectionHelper<K> selectionHelper,
+            ItemKeyProvider<K> keyProvider,
+            ItemDetailsLookup<K> detailsLookup,
+            MouseCallbacks mouseCallbacks,
+            ActivationCallbacks<K> activationCallbacks,
+            FocusCallbacks<K> focusCallbacks) {
+
+        super(selectionHelper, keyProvider, focusCallbacks);
+
+        checkArgument(detailsLookup != null);
+        checkArgument(mouseCallbacks != null);
+        checkArgument(activationCallbacks != null);
+
+        mDetailsLookup = detailsLookup;
+        mMouseCallbacks = mouseCallbacks;
+        mActivationCallbacks = activationCallbacks;
+        mFocusCallbacks = focusCallbacks;
+    }
+
+    @Override
+    public boolean onDown(MotionEvent e) {
+        if (VERBOSE) Log.v(TAG, "Delegated onDown event.");
+        if ((MotionEvents.isAltKeyPressed(e) && MotionEvents.isPrimaryButtonPressed(e))
+                || MotionEvents.isSecondaryButtonPressed(e)) {
+            mHandledOnDown = true;
+            return onRightClick(e);
+        }
+
+        return false;
+    }
+
+    @Override
+    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+        // Don't scroll content window in response to mouse drag
+        // If it's two-finger trackpad scrolling, we want to scroll
+        return !MotionEvents.isTouchpadScroll(e2);
+    }
+
+    @Override
+    public boolean onSingleTapUp(MotionEvent e) {
+        // See b/27377794. Since we don't get a button state back from UP events, we have to
+        // explicitly save this state to know whether something was previously handled by
+        // DOWN events or not.
+        if (mHandledOnDown) {
+            if (VERBOSE) Log.v(TAG, "Ignoring onSingleTapUp, previously handled in onDown.");
+            mHandledOnDown = false;
+            return false;
+        }
+
+        if (!mDetailsLookup.overItemWithSelectionKey(e)) {
+            if (DEBUG) Log.d(TAG, "Tap not associated w/ model item. Clearing selection.");
+            mSelectionHelper.clearSelection();
+            mFocusCallbacks.clearFocus();
+            return false;
+        }
+
+        if (MotionEvents.isTertiaryButtonPressed(e)) {
+            if (DEBUG) Log.d(TAG, "Ignoring middle click");
+            return false;
+        }
+
+        if (mSelectionHelper.hasSelection()) {
+            onItemClick(e, mDetailsLookup.getItemDetails(e));
+            mHandledTapUp = true;
+            return true;
+        }
+
+        return false;
+    }
+
+    // tap on an item when there is an existing selection. We could extend
+    // a selection, we could clear selection (then launch)
+    private void onItemClick(MotionEvent e, ItemDetails<K> item) {
+        checkState(mSelectionHelper.hasSelection());
+        checkArgument(item != null);
+
+        if (isRangeExtension(e)) {
+            extendSelectionRange(item);
+        } else {
+            if (shouldClearSelection(e, item)) {
+                mSelectionHelper.clearSelection();
+            }
+            if (mSelectionHelper.isSelected(item.getSelectionKey())) {
+                if (mSelectionHelper.deselect(item.getSelectionKey())) {
+                    mFocusCallbacks.clearFocus();
+                }
+            } else {
+                selectOrFocusItem(item, e);
+            }
+        }
+    }
+
+    @Override
+    public boolean onSingleTapConfirmed(MotionEvent e) {
+        if (mHandledTapUp) {
+            if (VERBOSE) {
+                Log.v(TAG,
+                        "Ignoring onSingleTapConfirmed, previously handled in onSingleTapUp.");
+            }
+            mHandledTapUp = false;
+            return false;
+        }
+
+        if (mSelectionHelper.hasSelection()) {
+            return false;  // should have been handled by onSingleTapUp.
+        }
+
+        if (!mDetailsLookup.overItem(e)) {
+            if (DEBUG) Log.d(TAG, "Ignoring Confirmed Tap on non-item.");
+            return false;
+        }
+
+        if (MotionEvents.isTertiaryButtonPressed(e)) {
+            if (DEBUG) Log.d(TAG, "Ignoring middle click");
+            return false;
+        }
+
+        @Nullable ItemDetails<K> item = mDetailsLookup.getItemDetails(e);
+        if (item == null || !item.hasSelectionKey()) {
+            return false;
+        }
+
+        if (mFocusCallbacks.hasFocusedItem() && MotionEvents.isShiftKeyPressed(e)) {
+            mSelectionHelper.startRange(mFocusCallbacks.getFocusedPosition());
+            mSelectionHelper.extendRange(item.getPosition());
+        } else {
+            selectOrFocusItem(item, e);
+        }
+        return true;
+    }
+
+    @Override
+    public boolean onDoubleTap(MotionEvent e) {
+        mHandledTapUp = false;
+
+        if (!mDetailsLookup.overItemWithSelectionKey(e)) {
+            if (DEBUG) Log.d(TAG, "Ignoring DoubleTap on non-model-backed item.");
+            return false;
+        }
+
+        if (MotionEvents.isTertiaryButtonPressed(e)) {
+            if (DEBUG) Log.d(TAG, "Ignoring middle click");
+            return false;
+        }
+
+        ItemDetails<K> item = mDetailsLookup.getItemDetails(e);
+        return (item != null) && mActivationCallbacks.onItemActivated(item, e);
+    }
+
+    private boolean onRightClick(MotionEvent e) {
+        if (mDetailsLookup.overItemWithSelectionKey(e)) {
+            @Nullable ItemDetails<K> item = mDetailsLookup.getItemDetails(e);
+            if (item != null && !mSelectionHelper.isSelected(item.getSelectionKey())) {
+                mSelectionHelper.clearSelection();
+                selectItem(item);
+            }
+        }
+
+        // We always delegate final handling of the event,
+        // since the handler might want to show a context menu
+        // in an empty area or some other weirdo view.
+        return mMouseCallbacks.onContextClick(e);
+    }
+
+    private void selectOrFocusItem(ItemDetails<K> item, MotionEvent e) {
+        if (item.inSelectionHotspot(e) || MotionEvents.isCtrlKeyPressed(e)) {
+            selectItem(item);
+        } else {
+            focusItem(item);
+        }
+    }
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/MutableSelection.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/MutableSelection.java
new file mode 100644
index 0000000..6e11698
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/MutableSelection.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.support.annotation.RestrictTo;
+
+/**
+ * Subclass of Selection exposing public support for mutating the underlying selection data.
+ * This is useful for clients of {@link SelectionHelper} that wish to manipulate
+ * a copy of selection data obtained via {@link SelectionHelper#copySelection(Selection)}.
+ *
+ * @param <K> Selection key type. Usually String or Long.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public final class MutableSelection<K> extends Selection<K> {
+
+    @Override
+    public boolean add(K key) {
+        return super.add(key);
+    }
+
+    @Override
+    public boolean remove(K key) {
+        return super.remove(key);
+    }
+
+    @Override
+    public void copyFrom(Selection<K> source) {
+        super.copyFrom(source);
+    }
+
+    @Override
+    public void clear() {
+        super.clear();
+    }
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/Range.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/Range.java
new file mode 100644
index 0000000..632e436
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/Range.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import static android.support.v4.util.Preconditions.checkArgument;
+import static android.support.v7.widget.RecyclerView.NO_POSITION;
+
+import static androidx.recyclerview.selection.Shared.DEBUG;
+
+import android.support.annotation.IntDef;
+import android.util.Log;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Class providing support for managing range selections.
+ */
+final class Range {
+
+    static final int TYPE_PRIMARY = 0;
+
+    /**
+     * "Provisional" selection represents a overlay on the primary selection. A provisional
+     * selection maybe be eventually added to the primary selection, or it may be abandoned.
+     *
+     * <p>E.g. BandSelectionHelper creates a provisional selection while a user is actively
+     * selecting items with a band. GestureSelectionHelper creates a provisional selection
+     * while a user is active selecting via gesture.
+     *
+     * <p>Provisionally selected items are considered to be selected in
+     * {@link Selection#contains(String)} and related methods. A provisional may be abandoned or
+     * merged into the promary selection.
+     *
+     * <p>A provisional selection may intersect with the primary selection, however clearing the
+     * provisional selection will not affect the primary selection where the two may intersect.
+     */
+    static final int TYPE_PROVISIONAL = 1;
+    @IntDef({
+            TYPE_PRIMARY,
+            TYPE_PROVISIONAL
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @interface RangeType {}
+
+    private static final String TAG = "Range";
+
+    private final Callbacks mCallbacks;
+    private final int mBegin;
+    private int mEnd = NO_POSITION;
+
+    /**
+     * Creates a new range anchored at {@code position}.
+     *
+     * @param position
+     * @param callbacks
+     */
+    Range(int position, Callbacks callbacks) {
+        mBegin = position;
+        mCallbacks = callbacks;
+        if (DEBUG) Log.d(TAG, "Creating new Range anchored @ " + position);
+    }
+
+    void extendRange(int position, @RangeType int type) {
+        checkArgument(position != NO_POSITION, "Position cannot be NO_POSITION.");
+
+        if (mEnd == NO_POSITION || mEnd == mBegin) {
+            // Reset mEnd so it can be established in establishRange.
+            mEnd = NO_POSITION;
+            establishRange(position, type);
+        } else {
+            reviseRange(position, type);
+        }
+    }
+
+    private void establishRange(int position, @RangeType int type) {
+        checkArgument(mEnd == NO_POSITION, "End has already been set.");
+
+        mEnd = position;
+
+        if (position > mBegin) {
+            if (DEBUG) log(type, "Establishing initial range at @ " + position);
+            updateRange(mBegin + 1, position, true, type);
+        } else if (position < mBegin) {
+            if (DEBUG) log(type, "Establishing initial range at @ " + position);
+            updateRange(position, mBegin - 1, true, type);
+        }
+    }
+
+    private void reviseRange(int position, @RangeType int type) {
+        checkArgument(mEnd != NO_POSITION, "End must already be set.");
+        checkArgument(mBegin != mEnd, "Beging and end point to same position.");
+
+        if (position == mEnd) {
+            if (DEBUG) log(type, "Ignoring no-op revision for range @ " + position);
+        }
+
+        if (mEnd > mBegin) {
+            reviseAscending(position, type);
+        } else if (mEnd < mBegin) {
+            reviseDescending(position, type);
+        }
+        // the "else" case is covered by checkState at beginning of method.
+
+        mEnd = position;
+    }
+
+    /**
+     * Updates an existing ascending selection.
+     */
+    private void reviseAscending(int position, @RangeType int type) {
+        if (DEBUG) log(type, "*ascending* Revising range @ " + position);
+
+        if (position < mEnd) {
+            if (position < mBegin) {
+                updateRange(mBegin + 1, mEnd, false, type);
+                updateRange(position, mBegin - 1, true, type);
+            } else {
+                updateRange(position + 1, mEnd, false, type);
+            }
+        } else if (position > mEnd) {   // Extending the range...
+            updateRange(mEnd + 1, position, true, type);
+        }
+    }
+
+    private void reviseDescending(int position, @RangeType int type) {
+        if (DEBUG) log(type, "*descending* Revising range @ " + position);
+
+        if (position > mEnd) {
+            if (position > mBegin) {
+                updateRange(mEnd, mBegin - 1, false, type);
+                updateRange(mBegin + 1, position, true, type);
+            } else {
+                updateRange(mEnd, position - 1, false, type);
+            }
+        } else if (position < mEnd) {   // Extending the range...
+            updateRange(position, mEnd - 1, true, type);
+        }
+    }
+
+    /**
+     * Try to set selection state for all elements in range. Not that callbacks can cancel
+     * selection of specific items, so some or even all items may not reflect the desired state
+     * after the update is complete.
+     *
+     * @param begin    Adapter position for range start (inclusive).
+     * @param end      Adapter position for range end (inclusive).
+     * @param selected New selection state.
+     */
+    private void updateRange(
+            int begin, int end, boolean selected, @RangeType int type) {
+        mCallbacks.updateForRange(begin, end, selected, type);
+    }
+
+    @Override
+    public String toString() {
+        return "Range{begin=" + mBegin + ", end=" + mEnd + "}";
+    }
+
+    private void log(@RangeType int type, String message) {
+        String opType = type == TYPE_PRIMARY ? "PRIMARY" : "PROVISIONAL";
+        Log.d(TAG, String.valueOf(this) + ": " + message + " (" + opType + ")");
+    }
+
+    /*
+     * @see {@link DefaultSelectionHelper#updateForRange(int, int , boolean, int)}.
+     */
+    abstract static class Callbacks {
+        abstract void updateForRange(
+                int begin, int end, boolean selected, @RangeType int type);
+    }
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/Selection.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/Selection.java
new file mode 100644
index 0000000..a622530
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/Selection.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
+import android.support.annotation.VisibleForTesting;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Object representing the current selection and provisional selection. Provides read only public
+ * access, and private write access.
+ * <p>
+ * This class tracks selected items by managing two sets:
+ *
+ * <li>primary selection
+ *
+ * Primary selection consists of items tapped by a user or by lassoed by band select operation.
+ *
+ * <li>provisional selection
+ *
+ * Provisional selections are selections which have been temporarily created
+ * by an in-progress band select or gesture selection. Once the user releases the mouse button
+ * or lifts their finger the corresponding provisional selection should be converted into
+ * primary selection.
+ *
+ * <p>The total selection is the combination of
+ * both the core selection and the provisional selection. Tracking both separately is necessary to
+ * ensure that items in the core selection are not "erased" from the core selection when they
+ * are temporarily included in a secondary selection (like band selection).
+ *
+ * @param <K> Selection key type. Usually String or Long.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public class Selection<K> implements Iterable<K> {
+
+    // NOTE: Not currently private as DefaultSelectionHelper directly manipulates values.
+    final Set<K> mSelection;
+    final Set<K> mProvisionalSelection;
+
+    Selection() {
+        mSelection = new HashSet<>();
+        mProvisionalSelection = new HashSet<>();
+    }
+
+    /**
+     * Used by {@link SelectionStorage} when restoring selection.
+     */
+    Selection(Set<K> selection) {
+        mSelection = selection;
+        mProvisionalSelection = new HashSet<>();
+    }
+
+    /**
+     * @param key
+     * @return true if the position is currently selected.
+     */
+    public boolean contains(@Nullable K key) {
+        return mSelection.contains(key) || mProvisionalSelection.contains(key);
+    }
+
+    /**
+     * Returns an {@link Iterator} that iterators over the selection, *excluding*
+     * any provisional selection.
+     *
+     * {@inheritDoc}
+     */
+    @Override
+    public Iterator<K> iterator() {
+        return mSelection.iterator();
+    }
+
+    /**
+     * @return size of the selection including both final and provisional selected items.
+     */
+    public int size() {
+        return mSelection.size() + mProvisionalSelection.size();
+    }
+
+    /**
+     * @return true if the selection is empty.
+     */
+    public boolean isEmpty() {
+        return mSelection.isEmpty() && mProvisionalSelection.isEmpty();
+    }
+
+    /**
+     * Sets the provisional selection, which is a temporary selection that can be saved,
+     * canceled, or adjusted at a later time. When a new provision selection is applied, the old
+     * one (if it exists) is abandoned.
+     * @return Map of ids added or removed. Added ids have a value of true, removed are false.
+     */
+    Map<K, Boolean> setProvisionalSelection(Set<K> newSelection) {
+        Map<K, Boolean> delta = new HashMap<>();
+
+        for (K key: mProvisionalSelection) {
+            // Mark each item that used to be in the provisional selection
+            // but is not in the new provisional selection.
+            if (!newSelection.contains(key) && !mSelection.contains(key)) {
+                delta.put(key, false);
+            }
+        }
+
+        for (K key: mSelection) {
+            // Mark each item that used to be in the selection but is unsaved and not in the new
+            // provisional selection.
+            if (!newSelection.contains(key)) {
+                delta.put(key, false);
+            }
+        }
+
+        for (K key: newSelection) {
+            // Mark each item that was not previously in the selection but is in the new
+            // provisional selection.
+            if (!mSelection.contains(key) && !mProvisionalSelection.contains(key)) {
+                delta.put(key, true);
+            }
+        }
+
+        // Now, iterate through the changes and actually add/remove them to/from the current
+        // selection. This could not be done in the previous loops because changing the size of
+        // the selection mid-iteration changes iteration order erroneously.
+        for (Map.Entry<K, Boolean> entry: delta.entrySet()) {
+            K key = entry.getKey();
+            if (entry.getValue()) {
+                mProvisionalSelection.add(key);
+            } else {
+                mProvisionalSelection.remove(key);
+            }
+        }
+
+        return delta;
+    }
+
+    /**
+     * Saves the existing provisional selection. Once the provisional selection is saved,
+     * subsequent provisional selections which are different from this existing one cannot
+     * cause items in this existing provisional selection to become deselected.
+     */
+    @VisibleForTesting
+    protected void mergeProvisionalSelection() {
+        mSelection.addAll(mProvisionalSelection);
+        mProvisionalSelection.clear();
+    }
+
+    /**
+     * Abandons the existing provisional selection so that all items provisionally selected are
+     * now deselected.
+     */
+    @VisibleForTesting
+    void clearProvisionalSelection() {
+        mProvisionalSelection.clear();
+    }
+
+    /**
+     * Adds a new item to the primary selection.
+     *
+     * @return true if the operation resulted in a modification to the selection.
+     */
+    boolean add(K key) {
+        if (mSelection.contains(key)) {
+            return false;
+        }
+
+        mSelection.add(key);
+        return true;
+    }
+
+    /**
+     * Removes an item from the primary selection.
+     *
+     * @return true if the operation resulted in a modification to the selection.
+     */
+    boolean remove(K key) {
+        if (!mSelection.contains(key)) {
+            return false;
+        }
+
+        mSelection.remove(key);
+        return true;
+    }
+
+    /**
+     * Clears the primary selection. The provisional selection, if any, is unaffected.
+     */
+    void clear() {
+        mSelection.clear();
+    }
+
+    /**
+     * Clones primary and provisional selection from supplied {@link Selection}.
+     * Does not copy active range data.
+     */
+    void copyFrom(Selection<K> source) {
+        mSelection.clear();
+        mSelection.addAll(source.mSelection);
+
+        mProvisionalSelection.clear();
+        mProvisionalSelection.addAll(source.mProvisionalSelection);
+    }
+
+    @Override
+    public String toString() {
+        if (size() <= 0) {
+            return "size=0, items=[]";
+        }
+
+        StringBuilder buffer = new StringBuilder(size() * 28);
+        buffer.append("Selection{")
+            .append("primary{size=" + mSelection.size())
+            .append(", entries=" + mSelection)
+            .append("}, provisional{size=" + mProvisionalSelection.size())
+            .append(", entries=" + mProvisionalSelection)
+            .append("}}");
+        return buffer.toString();
+    }
+
+    @Override
+    public int hashCode() {
+        return mSelection.hashCode() ^ mProvisionalSelection.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        }
+
+        return other instanceof Selection && isEqualTo((Selection) other);
+    }
+
+    private boolean isEqualTo(Selection other) {
+        return mSelection.equals(other.mSelection)
+                && mProvisionalSelection.equals(other.mProvisionalSelection);
+    }
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/SelectionHelper.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/SelectionHelper.java
new file mode 100644
index 0000000..276f903
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/SelectionHelper.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
+
+import java.util.Set;
+
+/**
+ * SelectionManager provides support for managing selection within a RecyclerView instance.
+ *
+ * @see DefaultSelectionHelper for details on instantiation.
+ *
+ * @param <K> Selection key type. Usually String or Long.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public abstract class SelectionHelper<K> {
+
+    /**
+     * This value is included in the payload when SelectionHelper implementations
+     * notify RecyclerView of changes. Clients can look for this in
+     * {@code onBindViewHolder} to know if the bind event is occurring in response
+     * to a selection state change.
+     */
+    public static final String SELECTION_CHANGED_MARKER = "Selection-Changed";
+
+    /**
+     * Adds {@code observer} to be notified when changes to selection occur.
+     * This method allows observers to closely track changes to selection
+     * avoiding the need to poll selection at performance critical points.
+     */
+    public abstract void addObserver(SelectionObserver observer);
+
+    /** @return true if has a selection */
+    public abstract boolean hasSelection();
+
+    /**
+     * Returns a Selection object that provides a live view on the current selection.
+     *
+     * @return The current selection.
+     * @see #copySelection(Selection) on how to get a snapshot
+     * of the selection that will not reflect future changes
+     * to selection.
+     */
+    public abstract Selection getSelection();
+
+    /**
+     * Updates {@code dest} to reflect the current selection.
+     */
+    public abstract void copySelection(Selection dest);
+
+    /**
+     * @return true if the item specified by its id is selected. Shorthand for
+     * {@code getSelection().contains(K)}.
+     */
+    public abstract boolean isSelected(@Nullable K key);
+
+    /**
+     * Restores the selected state of specified items. Used in cases such as restore the selection
+     * after rotation etc. Provisional selection, being provisional 'n all, isn't restored.
+     *
+     * <p>This affords clients the ability to restore selection from selection saved
+     * in Activity state. See {@link android.app.Activity#onCreate(Bundle)}.
+     *
+     * @param savedSelection selection being restored.
+     */
+    public abstract void restoreSelection(Selection savedSelection);
+
+    abstract void onDataSetChanged();
+
+    /**
+     * Clears both primary selection and provisional selection.
+     *
+     * @return true if anything changed.
+     */
+    public abstract boolean clear();
+
+    /**
+     * Clears the selection and notifies (if something changes).
+     */
+    public abstract void clearSelection();
+
+    /**
+     * Sets the selected state of the specified items. Note that the callback will NOT
+     * be consulted to see if an item can be selected.
+     */
+    public abstract boolean setItemsSelected(Iterable<K> keys, boolean selected);
+
+    /**
+     * Attempts to select an item.
+     *
+     * @return true if the item was selected. False if the item was not selected, or was
+     * was already selected prior to the method being called.
+     */
+    public abstract boolean select(K key);
+
+    /**
+     * Attempts to deselect an item.
+     *
+     * @return true if the item was deselected. False if the item was not deselected, or was
+     * was already deselected prior to the method being called.
+     */
+    public abstract boolean deselect(K key);
+
+    /**
+     * Selects the item at position and establishes the "anchor" for a range selection,
+     * replacing any existing range anchor.
+     *
+     * @param position The anchor position for the selection range.
+     */
+    public abstract void startRange(int position);
+
+    /**
+     * Sets the end point for the active range selection.
+     *
+     * <p>This function should only be called when a range selection is active
+     * (see {@link #isRangeActive()}. Items in the range [anchor, end] will be
+     * selected.
+     *
+     * @param position  The new end position for the selection range.
+     * @throws IllegalStateException if a range selection is not active. Range selection
+     *         must have been started by a call to {@link #startRange(int)}.
+     */
+    public abstract void extendRange(int position);
+
+    /**
+     * Stops an in-progress range selection. All selection done with
+     * {@link #extendProvisionalRange(int)} will be lost if
+     * {@link Selection#mergeProvisionalSelection()} is not called beforehand.
+     */
+    public abstract void endRange();
+
+    /**
+     * @return Whether or not there is a current range selection active.
+     */
+    public abstract boolean isRangeActive();
+
+    /**
+     * Establishes the "anchor" at which a selection range begins. This "anchor" is consulted
+     * when determining how to extend, and modify selection ranges. Calling this when a
+     * range selection is active will reset the range selection.
+     *
+     * @param position the anchor position. Must already be selected.
+     */
+    protected abstract void anchorRange(int position);
+
+    /**
+     * @param position
+     */
+    // TODO: This is smelly. Maybe this type of logic needs to move into range selection,
+    // then selection manager can have a startProvisionalRange and startRange. Or
+    // maybe ranges always start life as provisional.
+    protected abstract void extendProvisionalRange(int position);
+
+    /**
+     * Sets the provisional selection, replacing any existing selection.
+     * @param newSelection
+     */
+    public abstract void setProvisionalSelection(Set<K> newSelection);
+
+    /** Clears any existing provisional selection */
+    public abstract void clearProvisionalSelection();
+
+    /**
+     * Converts the provisional selection into primary selection, then clears
+     * provisional selection.
+     */
+    public abstract void mergeProvisionalSelection();
+
+    /**
+     * Observer interface providing access to information about Selection state changes.
+     *
+     * @param <K> Selection key type. Usually String or Long.
+     *
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public abstract static class SelectionObserver<K> {
+
+        /**
+         * Called when state of an item has been changed.
+         */
+        public void onItemStateChanged(K key, boolean selected) {
+        }
+
+        /**
+         * Called when the underlying data set has change. After this method is called
+         * the selection manager will attempt traverse the existing selection,
+         * calling {@link #onItemStateChanged(K, boolean)} for each selected item,
+         * and deselecting any items that cannot be selected given the updated dataset.
+         */
+        public void onSelectionReset() {
+        }
+
+        /**
+         * Called immediately after completion of any set of changes, excluding
+         * those resulting in calls to {@link #onSelectionReset()} and
+         * {@link #onSelectionRestored()}.
+         */
+        public void onSelectionChanged() {
+        }
+
+        /**
+         * Called immediately after selection is restored.
+         * {@link #onItemStateChanged(K, boolean)} will not be called
+         * for individual items in the selection.
+         */
+        public void onSelectionRestored() {
+        }
+    }
+
+    /**
+     * Implement SelectionPredicate to control when items can be selected or unselected.
+     *
+     * @param <K> Selection key type. Usually String or Long.
+     *
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public abstract static class SelectionPredicate<K> {
+
+        /** @return true if the item at {@code id} can be set to {@code nextState}. */
+        public abstract boolean canSetStateForKey(K key, boolean nextState);
+
+        /** @return true if the item at {@code id} can be set to {@code nextState}. */
+        public abstract boolean canSetStateAtPosition(int position, boolean nextState);
+
+        /** @return true if more than a single item can be selected. */
+        public abstract boolean canSelectMultiple();
+    }
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/SelectionHelperBuilder.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/SelectionHelperBuilder.java
new file mode 100644
index 0000000..abdefaf
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/SelectionHelperBuilder.java
@@ -0,0 +1,342 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import static android.support.v4.util.Preconditions.checkArgument;
+
+import android.content.Context;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.RestrictTo;
+import android.support.v7.widget.RecyclerView;
+import android.view.GestureDetector;
+import android.view.HapticFeedbackConstants;
+import android.view.MotionEvent;
+
+import androidx.recyclerview.selection.SelectionHelper.SelectionPredicate;
+
+/**
+ * Builder class for assembling selection support. Example usage:
+ *
+ * <p><pre>SelectionHelperBuilder selSupport = new SelectionHelperBuilder(
+        mRecView, new DemoStableIdProvider(mAdapter), detailsLookup);
+
+ // By default multi-select is supported.
+ SelectionHelper selHelper = selSupport
+       .build();
+
+ // This configuration support single selection for any element.
+ SelectionHelper selHelper = selSupport
+       .withSelectionPredicate(SelectionHelper.SelectionPredicate.SINGLE_ANYTHING)
+       .build();
+
+ // Lazily bind SelectionHelper. Allows us to defer initialization of the
+ // SelectionHelper dependency until after the adapter is created.
+ mAdapter.bindSelectionHelper(selHelper);
+
+ * </pre></p>
+ *
+ * @see SelectionStorage for important deatils on retaining selection across Activity
+ * lifecycle events.
+ *
+ * @param <K> Selection key type. Usually String or Long.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public final class SelectionHelperBuilder<K> {
+
+    private final RecyclerView mRecView;
+    private final RecyclerView.Adapter<?> mAdapter;
+    private final Context mContext;
+
+    // Content lock provides a mechanism to block content reload while selection
+    // activities are active. If using a loader to load content, route
+    // the call through the content lock using ContentLock#runWhenUnlocked.
+    // This is especially useful when listening on content change notification.
+    private final ContentLock mLock = new ContentLock();
+
+    private SelectionPredicate<K> mSelectionPredicate = SelectionPredicates.selectAnything();
+    private ItemKeyProvider<K> mKeyProvider;
+    private ItemDetailsLookup<K> mDetailsLookup;
+
+    private ActivationCallbacks<K> mActivationCallbacks = ActivationCallbacks.dummy();
+    private FocusCallbacks<K> mFocusCallbacks = FocusCallbacks.dummy();
+    private TouchCallbacks mTouchCallbacks = TouchCallbacks.DUMMY;
+    private MouseCallbacks mMouseCallbacks = MouseCallbacks.DUMMY;
+
+    private BandPredicate mBandPredicate;
+    private int mBandOverlayId = R.drawable.selection_band_overlay;
+
+    private int[] mGestureToolTypes = new int[] {
+        MotionEvent.TOOL_TYPE_FINGER,
+        MotionEvent.TOOL_TYPE_UNKNOWN
+    };
+
+    private int[] mBandToolTypes = new int[] {
+        MotionEvent.TOOL_TYPE_MOUSE,
+        MotionEvent.TOOL_TYPE_STYLUS
+    };
+
+    public SelectionHelperBuilder(
+            RecyclerView recView,
+            ItemKeyProvider<K> keyProvider,
+            ItemDetailsLookup<K> detailsLookup) {
+
+        checkArgument(recView != null);
+
+        mRecView = recView;
+        mContext = recView.getContext();
+        mAdapter = recView.getAdapter();
+
+        checkArgument(mAdapter != null);
+        checkArgument(keyProvider != null);
+        checkArgument(detailsLookup != null);
+
+        mDetailsLookup = detailsLookup;
+        mKeyProvider = keyProvider;
+
+        mBandPredicate = BandPredicate.notDraggable(mRecView, detailsLookup);
+    }
+
+    /**
+     * Install seleciton predicate.
+     * @param predicate
+     * @return
+     */
+    public SelectionHelperBuilder<K> withSelectionPredicate(SelectionPredicate<K> predicate) {
+        checkArgument(predicate != null);
+        mSelectionPredicate = predicate;
+        return this;
+    }
+
+    /**
+     * Add activation callbacks to respond to taps/enter/double-click on items.
+     *
+     * @param callbacks
+     * @return
+     */
+    public SelectionHelperBuilder<K> withActivationCallbacks(ActivationCallbacks<K> callbacks) {
+        checkArgument(callbacks != null);
+        mActivationCallbacks = callbacks;
+        return this;
+    }
+
+    /**
+     * Add focus callbacks to interfact with selection related focus changes.
+     * @param callbacks
+     * @return
+     */
+    public SelectionHelperBuilder<K> withFocusCallbacks(FocusCallbacks<K> callbacks) {
+        checkArgument(callbacks != null);
+        mFocusCallbacks = callbacks;
+        return this;
+    }
+
+    /**
+     * Configures mouse callbacks, replacing defaults.
+     *
+     * @param callbacks
+     * @return
+     */
+    public SelectionHelperBuilder<K> withMouseCallbacks(MouseCallbacks callbacks) {
+        checkArgument(callbacks != null);
+
+        mMouseCallbacks = callbacks;
+        return this;
+    }
+
+    /**
+     * Replaces default touch callbacks.
+     *
+     * @param callbacks
+     * @return
+     */
+    public SelectionHelperBuilder<K> withTouchCallbacks(TouchCallbacks callbacks) {
+        checkArgument(callbacks != null);
+
+        mTouchCallbacks = callbacks;
+        return this;
+    }
+
+    /**
+     * Replaces default gesture tooltypes.
+     * @param toolTypes
+     * @return
+     */
+    public SelectionHelperBuilder<K> withTouchTooltypes(int... toolTypes) {
+        mGestureToolTypes = toolTypes;
+        return this;
+    }
+
+    /**
+     * Replaces default band overlay.
+     *
+     * @param bandOverlayId
+     * @return
+     */
+    public SelectionHelperBuilder<K> withBandOverlay(@DrawableRes int bandOverlayId) {
+        mBandOverlayId = bandOverlayId;
+        return this;
+    }
+
+    /**
+     * Replaces default band predicate.
+     * @param bandPredicate
+     * @return
+     */
+    public SelectionHelperBuilder<K> withBandPredicate(BandPredicate bandPredicate) {
+
+        checkArgument(bandPredicate != null);
+
+        mBandPredicate = bandPredicate;
+        return this;
+    }
+
+    /**
+     * Replaces default band tools types.
+     * @param toolTypes
+     * @return
+     */
+    public SelectionHelperBuilder<K> withBandTooltypes(int... toolTypes) {
+        mBandToolTypes = toolTypes;
+        return this;
+    }
+
+    /**
+     * Prepares selection support and returns the corresponding SelectionHelper.
+     *
+     * @return
+     */
+    public SelectionHelper<K> build() {
+
+        SelectionHelper<K> selectionHelper =
+                new DefaultSelectionHelper<>(mKeyProvider, mSelectionPredicate);
+
+        // Event glue between RecyclerView and SelectionHelper keeps the classes separate
+        // so that a SelectionHelper can be shared across RecyclerView instances that
+        // represent the same data in different ways.
+        EventBridge.install(mAdapter, selectionHelper, mKeyProvider);
+
+        AutoScroller scroller = new ViewAutoScroller(ViewAutoScroller.createScrollHost(mRecView));
+
+        // Setup basic input handling, with the touch handler as the default consumer
+        // of events. If mouse handling is configured as well, the mouse input
+        // related handlers will intercept mouse input events.
+
+        // GestureRouter is responsible for routing GestureDetector events
+        // to tool-type specific handlers.
+        GestureRouter<MotionInputHandler> gestureRouter = new GestureRouter<>();
+        GestureDetector gestureDetector = new GestureDetector(mContext, gestureRouter);
+
+        // TouchEventRouter takes its name from RecyclerView#OnItemTouchListener.
+        // Despite "Touch" being in the name, it receives events for all types of tools.
+        // This class is responsible for routing events to tool-type specific handlers,
+        // and if not handled by a handler, on to a GestureDetector for analysis.
+        TouchEventRouter eventRouter = new TouchEventRouter(gestureDetector);
+
+        // GestureSelectionHelper provides logic that interprets a combination
+        // of motions and gestures in order to provide gesture driven selection support
+        // when used in conjunction with RecyclerView.
+        final GestureSelectionHelper gestureHelper =
+                GestureSelectionHelper.create(selectionHelper, mRecView, scroller, mLock);
+
+        // Finally hook the framework up to listening to recycle view events.
+        mRecView.addOnItemTouchListener(eventRouter);
+
+        // But before you move on, there's more work to do. Event plumbing has been
+        // installed, but we haven't registered any of our helpers or callbacks.
+        // Helpers contain predefined logic converting events into selection related events.
+        // Callbacks provide authors the ability to reponspond to other types of
+        // events (like "active" a tapped item). This is broken up into two main
+        // suites, one for "touch" and one for "mouse", though both can and should (usually)
+        // be configued to handle other types of input (to satisfy user expectation).);
+
+        // Provides high level glue for binding touch events
+        // and gestures to selection framework.
+        TouchInputHandler<K> touchHandler = new TouchInputHandler<K>(
+                selectionHelper,
+                mKeyProvider,
+                mDetailsLookup,
+                mSelectionPredicate,
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        if (mSelectionPredicate.canSelectMultiple()) {
+                            gestureHelper.start();
+                        }
+                    }
+                },
+                mTouchCallbacks,
+                mActivationCallbacks,
+                mFocusCallbacks,
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        mRecView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+                    }
+                });
+
+        for (int toolType : mGestureToolTypes) {
+            gestureRouter.register(toolType, touchHandler);
+            eventRouter.register(toolType, gestureHelper);
+        }
+
+        // Provides high level glue for binding mouse/stylus events and gestures
+        // to selection framework.
+        MouseInputHandler<K> mouseHandler = new MouseInputHandler<>(
+                selectionHelper,
+                mKeyProvider,
+                mDetailsLookup,
+                mMouseCallbacks,
+                mActivationCallbacks,
+                mFocusCallbacks);
+
+        for (int toolType : mBandToolTypes) {
+            gestureRouter.register(toolType, mouseHandler);
+        }
+
+        // Band selection not supported in single select mode, or when key access
+        // is limited to anything less than the entire corpus.
+        // TODO: Since we cach grid info from laid out items, we could cache key too.
+        // Then we couldn't have to limit to CORPUS access.
+        if (mKeyProvider.hasAccess(ItemKeyProvider.SCOPE_MAPPED)
+                && mSelectionPredicate.canSelectMultiple()) {
+            // BandSelectionHelper provides support for band selection on-top of a RecyclerView
+            // instance. Given the recycling nature of RecyclerView BandSelectionController
+            // necessarily models and caches list/grid information as the user's pointer
+            // interacts with the item in the RecyclerView. Selectable items that intersect
+            // with the band, both on and off screen, are selected.
+            BandSelectionHelper bandHelper = BandSelectionHelper.create(
+                    mRecView,
+                    scroller,
+                    mBandOverlayId,
+                    mKeyProvider,
+                    selectionHelper,
+                    mSelectionPredicate,
+                    mBandPredicate,
+                    mFocusCallbacks,
+                    mLock);
+
+            for (int toolType : mBandToolTypes) {
+                eventRouter.register(toolType, bandHelper);
+            }
+        }
+
+        return selectionHelper;
+    }
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/SelectionPredicates.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/SelectionPredicates.java
new file mode 100644
index 0000000..26253d9
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/SelectionPredicates.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.support.annotation.RestrictTo;
+
+import androidx.recyclerview.selection.SelectionHelper.SelectionPredicate;
+
+/**
+ * Utility class for creating SelectionPredicate instances.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public final class SelectionPredicates {
+
+    private SelectionPredicates() {}
+
+    /**
+     * Returns a selection predicate that allows multiples items to be selected, without
+     * any restrictions on which items can be selected.
+     * @param <K>
+     * @return
+     */
+    public static <K> SelectionPredicate<K> selectAnything() {
+        return new SelectionPredicate<K>() {
+            @Override
+            public boolean canSetStateForKey(K key, boolean nextState) {
+                return true;
+            }
+
+            @Override
+            public boolean canSetStateAtPosition(int position, boolean nextState) {
+                return true;
+            }
+
+            @Override
+            public boolean canSelectMultiple() {
+                return true;
+            }
+        };
+    }
+
+    /**
+     * Returns a selection predicate that allows a single item to be selected, without
+     * any restrictions on which item can be selected.
+     * @param <K>
+     * @return
+     */
+    public static <K> SelectionPredicate<K> selectSingleAnything() {
+        return new SelectionPredicate<K>() {
+            @Override
+            public boolean canSetStateForKey(K key, boolean nextState) {
+                return true;
+            }
+
+            @Override
+            public boolean canSetStateAtPosition(int position, boolean nextState) {
+                return true;
+            }
+
+            @Override
+            public boolean canSelectMultiple() {
+                return false;
+            }
+        };
+    }
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/SelectionStorage.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/SelectionStorage.java
new file mode 100644
index 0000000..81db30f
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/SelectionStorage.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import static android.support.v4.util.Preconditions.checkArgument;
+
+import android.os.Bundle;
+import android.support.annotation.IntDef;
+import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
+import android.support.annotation.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Set;
+
+/**
+ * Helper class binding SelectionHelper and Activity lifecycle events facilitating
+ * persistence of selection across activity lifecycle events.
+ *
+ * <p>Usage:<br><pre>
+ void onCreate() {
+    mLifecycleHelper = new SelectionStorage<>(SelectionStorage.TYPE_STRING, mSelectionHelper);
+    if (savedInstanceState != null) {
+        mSelectionStorage.onRestoreInstanceState(savedInstanceState);
+    }
+ }
+ protected void onSaveInstanceState(Bundle outState) {
+     super.onSaveInstanceState(outState);
+     mSelectionStorage.onSaveInstanceState(outState);
+ }
+ </pre>
+ * @param <K> Selection key type. Usually String or Long.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public final class SelectionStorage<K> {
+
+    @VisibleForTesting
+    static final String EXTRA_SAVED_SELECTION_TYPE = "androidx.recyclerview.selection.type";
+
+    @VisibleForTesting
+    static final String EXTRA_SAVED_SELECTION_ENTRIES = "androidx.recyclerview.selection.entries";
+
+    public static final int TYPE_STRING = 0;
+    public static final int TYPE_LONG = 1;
+    @IntDef({
+            TYPE_STRING,
+            TYPE_LONG
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @interface KeyType {}
+
+    private final @KeyType int mKeyType;
+    private final SelectionHelper<K> mHelper;
+
+    /**
+     * Creates a new lifecycle helper. {@code keyType}.
+     *
+     * @param keyType
+     * @param helper
+     */
+    public SelectionStorage(@KeyType int keyType, SelectionHelper<K> helper) {
+        checkArgument(
+                keyType == TYPE_STRING || keyType == TYPE_LONG,
+                "Only String and Integer presistence are supported by default.");
+        checkArgument(helper != null);
+
+        mKeyType = keyType;
+        mHelper = helper;
+    }
+
+    /**
+     * Preserves selection, if any.
+     *
+     * @param state
+     */
+    @SuppressWarnings("unchecked")
+    public void onSaveInstanceState(Bundle state) {
+        MutableSelection<K> sel = new MutableSelection<>();
+        mHelper.copySelection(sel);
+
+        state.putInt(EXTRA_SAVED_SELECTION_TYPE, mKeyType);
+        switch (mKeyType) {
+            case TYPE_STRING:
+                writeStringSelection(state, ((Selection<String>) sel).mSelection);
+                break;
+            case TYPE_LONG:
+                writeLongSelection(state, ((Selection<Long>) sel).mSelection);
+                break;
+            default:
+                throw new UnsupportedOperationException("Unsupported key type: " + mKeyType);
+        }
+    }
+
+    /**
+     * Restores selection from previously saved state.
+     *
+     * @param state
+     */
+    public void onRestoreInstanceState(@Nullable Bundle state) {
+        if (state == null) {
+            return;
+        }
+
+        int keyType = state.getInt(EXTRA_SAVED_SELECTION_TYPE, -1);
+        switch(keyType) {
+            case TYPE_STRING:
+                Selection<String> stringSel = readStringSelection(state);
+                if (stringSel != null && !stringSel.isEmpty()) {
+                    mHelper.restoreSelection(stringSel);
+                }
+                break;
+            case TYPE_LONG:
+                Selection<Long> longSel = readLongSelection(state);
+                if (longSel != null && !longSel.isEmpty()) {
+                    mHelper.restoreSelection(longSel);
+                }
+                break;
+            default:
+                throw new UnsupportedOperationException("Unsupported selection key type.");
+        }
+    }
+
+    private @Nullable Selection<String> readStringSelection(@Nullable Bundle state) {
+        @Nullable ArrayList<String> stored =
+                state.getStringArrayList(EXTRA_SAVED_SELECTION_ENTRIES);
+        if (stored == null) {
+            return null;
+        }
+
+        Selection<String> selection = new Selection<>();
+        selection.mSelection.addAll(stored);
+        return selection;
+    }
+
+    private @Nullable Selection<Long> readLongSelection(@Nullable Bundle state) {
+        @Nullable long[] stored = state.getLongArray(EXTRA_SAVED_SELECTION_ENTRIES);
+        if (stored == null) {
+            return null;
+        }
+
+        Selection<Long> selection = new Selection<>();
+        for (long key : stored) {
+            selection.mSelection.add(key);
+        }
+        return selection;
+    }
+
+    private void writeStringSelection(Bundle state, Set<String> selected) {
+        ArrayList<String> value = new ArrayList<>(selected.size());
+        value.addAll(selected);
+        state.putStringArrayList(EXTRA_SAVED_SELECTION_ENTRIES, value);
+    }
+
+    private void writeLongSelection(Bundle state, Set<Long> selected) {
+        long[] value = new long[selected.size()];
+        int i = 0;
+        for (Long key : selected) {
+            value[i++] = key;
+        }
+        state.putLongArray(EXTRA_SAVED_SELECTION_ENTRIES, value);
+    }
+}
diff --git a/media-compat-test-lib/build.gradle b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/Shared.java
similarity index 66%
copy from media-compat-test-lib/build.gradle
copy to recyclerview-selection/src/main/java/androidx/recyclerview/selection/Shared.java
index 26594e5..3b79120 100644
--- a/media-compat-test-lib/build.gradle
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/Shared.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2017 The Android Open Source Project
+ * Copyright 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.
@@ -14,4 +14,15 @@
  * limitations under the License.
  */
 
-apply plugin: 'java'
+package androidx.recyclerview.selection;
+
+/**
+ * Shared constants used in this package.
+ */
+final class Shared {
+
+    static final boolean DEBUG = false;
+    static final boolean VERBOSE = true;
+
+    private Shared() {}
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/StableIdKeyProvider.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/StableIdKeyProvider.java
new file mode 100644
index 0000000..3dc78ca
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/StableIdKeyProvider.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.OnChildAttachStateChangeListener;
+import android.util.SparseArray;
+import android.view.View;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * ItemKeyProvider that provides stable ids by way of cached RecyclerView.Adapter stable ids.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public final class StableIdKeyProvider extends ItemKeyProvider<Long> {
+
+    private final SparseArray<Long> mPositionToKey = new SparseArray<>();
+    private final Map<Long, Integer> mKeyToPosition = new HashMap<Long, Integer>();
+    private final RecyclerView mRecView;
+
+    public StableIdKeyProvider(RecyclerView recView) {
+
+        // Since this provide is based on stable ids based on whats laid out in the window
+        // we can only satisfy "window" scope key access.
+        super(SCOPE_CACHED);
+
+        mRecView = recView;
+
+        mRecView.addOnChildAttachStateChangeListener(
+                new OnChildAttachStateChangeListener() {
+                    @Override
+                    public void onChildViewAttachedToWindow(View view) {
+                        onAttached(view);
+                    }
+
+                    @Override
+                    public void onChildViewDetachedFromWindow(View view) {
+                        onDetached(view);
+                    }
+                }
+        );
+
+    }
+
+    private void onAttached(View view) {
+        RecyclerView.ViewHolder holder = mRecView.findContainingViewHolder(view);
+        int position = holder.getAdapterPosition();
+        long id = holder.getItemId();
+        if (position != RecyclerView.NO_POSITION && id != RecyclerView.NO_ID) {
+            mPositionToKey.put(position, id);
+            mKeyToPosition.put(id, position);
+        }
+    }
+
+    private void onDetached(View view) {
+        RecyclerView.ViewHolder holder = mRecView.findContainingViewHolder(view);
+        int position = holder.getAdapterPosition();
+        long id = holder.getItemId();
+        if (position != RecyclerView.NO_POSITION && id != RecyclerView.NO_ID) {
+            mPositionToKey.delete(position);
+            mKeyToPosition.remove(id);
+        }
+    }
+
+    @Override
+    public @Nullable Long getKey(int position) {
+        return mPositionToKey.get(position, null);
+    }
+
+    @Override
+    public int getPosition(Long key) {
+        if (mKeyToPosition.containsKey(key)) {
+            return mKeyToPosition.get(key);
+        }
+        return RecyclerView.NO_POSITION;
+    }
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/ToolHandlerRegistry.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/ToolHandlerRegistry.java
new file mode 100644
index 0000000..c735529
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/ToolHandlerRegistry.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import static android.support.v4.util.Preconditions.checkArgument;
+import static android.support.v4.util.Preconditions.checkState;
+
+import android.support.annotation.Nullable;
+import android.view.MotionEvent;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Registry for tool specific event handler.
+ */
+final class ToolHandlerRegistry<T> {
+
+    // Currently there are four known input types. ERASER is the last one, so has the
+    // highest value. UNKNOWN is zero, so we add one. This allows delegates to be
+    // registered by type, and avoid the auto-boxing that would be necessary were we
+    // to store delegates in a Map<Integer, Delegate>.
+    private static final int sNumInputTypes = MotionEvent.TOOL_TYPE_ERASER + 1;
+
+    private final List<T> mHandlers = Arrays.asList(null, null, null, null, null);
+    private final T mDefault;
+
+    ToolHandlerRegistry(T defaultDelegate) {
+        checkArgument(defaultDelegate != null);
+        mDefault = defaultDelegate;
+
+        // Initialize all values to null.
+        for (int i = 0; i < sNumInputTypes; i++) {
+            mHandlers.set(i, null);
+        }
+    }
+
+    /**
+     * @param toolType
+     * @param delegate the delegate, or null to unregister.
+     * @throws IllegalStateException if an tooltype handler is already registered.
+     */
+    void set(int toolType, @Nullable T delegate) {
+        checkArgument(toolType >= 0 && toolType <= MotionEvent.TOOL_TYPE_ERASER);
+        checkState(mHandlers.get(toolType) == null);
+
+        mHandlers.set(toolType, delegate);
+    }
+
+    T get(MotionEvent e) {
+        T d = mHandlers.get(e.getToolType(0));
+        return d != null ? d : mDefault;
+    }
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/TouchCallbacks.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/TouchCallbacks.java
new file mode 100644
index 0000000..5905392
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/TouchCallbacks.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.support.annotation.RestrictTo;
+import android.view.MotionEvent;
+
+/**
+ * Override methods in this class to connect specialized behaviors of the selection
+ * code to the application environment.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public abstract class TouchCallbacks {
+
+    static final TouchCallbacks DUMMY = new TouchCallbacks() {
+        @Override
+        public boolean onDragInitiated(MotionEvent e) {
+            return false;
+        }
+    };
+
+    /**
+     * Called when a drag is initiated. Touch input handler only considers
+     * a drag to be initiated on long press on an existing selection,
+     * as normal touch and drag events are strongly associated with scrolling of the view.
+     *
+     * <p>Drag will only be initiated when the item under the event is already selected.
+     *
+     * <p>The RecyclerView item at the coordinates of the MotionEvent is not supplied as a parameter
+     * to this method as there may be multiple items selected. Clients can obtain the current
+     * list of selected items from {@link SelectionHelper#copySelection(Selection)}.
+     *
+     * @param e the event associated with the drag.
+     * @return true if the event was handled.
+     */
+    public abstract boolean onDragInitiated(MotionEvent e);
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/TouchEventRouter.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/TouchEventRouter.java
new file mode 100644
index 0000000..fbbca23
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/TouchEventRouter.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import static android.support.v4.util.Preconditions.checkArgument;
+
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.OnItemTouchListener;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+
+/**
+ * A class responsible for routing MotionEvents to tool-type specific handlers,
+ * and if not handled by a handler, on to a {@link GestureDetector} for further
+ * processing.
+ *
+ * <p>TouchEventRouter takes its name from
+ * {@link RecyclerView#addOnItemTouchListener(OnItemTouchListener)}. Despite "Touch"
+ * being in the name, it receives MotionEvents for all types of tools.
+ */
+final class TouchEventRouter implements OnItemTouchListener {
+
+    private static final String TAG = "TouchEventRouter";
+
+    private final GestureDetector mDetector;
+    private final ToolHandlerRegistry<OnItemTouchListener> mDelegates;
+
+    TouchEventRouter(GestureDetector detector, OnItemTouchListener defaultDelegate) {
+        checkArgument(detector != null);
+        checkArgument(defaultDelegate != null);
+
+        mDetector = detector;
+        mDelegates = new ToolHandlerRegistry<>(defaultDelegate);
+    }
+
+    TouchEventRouter(GestureDetector detector) {
+        this(
+                detector,
+                // Supply a fallback listener does nothing...because the caller
+                // didn't supply a fallback.
+                new OnItemTouchListener() {
+                    @Override
+                    public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
+                        return false;
+                    }
+
+                    @Override
+                    public void onTouchEvent(RecyclerView rv, MotionEvent e) {
+                    }
+
+                    @Override
+                    public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+                    }
+                });
+    }
+
+    /**
+     * @param toolType See MotionEvent for details on available types.
+     * @param delegate An {@link OnItemTouchListener} to receive events
+     *     of {@code toolType}.
+     */
+    void register(int toolType, OnItemTouchListener delegate) {
+        checkArgument(delegate != null);
+        mDelegates.set(toolType, delegate);
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
+        boolean handled = mDelegates.get(e).onInterceptTouchEvent(rv, e);
+
+        // Forward all events to UserInputHandler.
+        // This is necessary since UserInputHandler needs to always see the first DOWN event. Or
+        // else all future UP events will be tossed.
+        handled |= mDetector.onTouchEvent(e);
+
+        return handled;
+    }
+
+    @Override
+    public void onTouchEvent(RecyclerView rv, MotionEvent e) {
+        mDelegates.get(e).onTouchEvent(rv, e);
+
+        // Note: even though this event is being handled as part of gestures such as drag and band,
+        // continue forwarding to the GestureDetector. The detector needs to see the entire cluster
+        // of events in order to properly interpret other gestures, such as long press.
+        mDetector.onTouchEvent(e);
+    }
+
+    @Override
+    public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {}
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/TouchInputHandler.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/TouchInputHandler.java
new file mode 100644
index 0000000..e07aeb1
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/TouchInputHandler.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import static android.support.v4.util.Preconditions.checkArgument;
+
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.view.MotionEvent;
+
+import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
+import androidx.recyclerview.selection.SelectionHelper.SelectionPredicate;
+
+/**
+ * A MotionInputHandler that provides the high-level glue for touch driven selection. This class
+ * works with {@link RecyclerView}, {@link GestureRouter}, and {@link GestureSelectionHelper} to
+ * provide robust user drive selection support.
+ */
+final class TouchInputHandler<K> extends MotionInputHandler<K> {
+
+    private static final String TAG = "TouchInputDelegate";
+    private static final boolean DEBUG = false;
+
+    private final ItemDetailsLookup<K> mDetailsLookup;
+    private final SelectionPredicate<K> mSelectionPredicate;
+    private final ActivationCallbacks<K> mActivationCallbacks;
+    private final TouchCallbacks mTouchCallbacks;
+    private final Runnable mGestureStarter;
+    private final Runnable mHapticPerformer;
+
+    TouchInputHandler(
+            SelectionHelper<K> selectionHelper,
+            ItemKeyProvider<K> keyProvider,
+            ItemDetailsLookup<K> detailsLookup,
+            SelectionPredicate<K> selectionPredicate,
+            Runnable gestureStarter,
+            TouchCallbacks touchCallbacks,
+            ActivationCallbacks<K> activationCallbacks,
+            FocusCallbacks<K> focusCallbacks,
+            Runnable hapticPerformer) {
+
+        super(selectionHelper, keyProvider, focusCallbacks);
+
+        checkArgument(detailsLookup != null);
+        checkArgument(selectionPredicate != null);
+        checkArgument(gestureStarter != null);
+        checkArgument(activationCallbacks != null);
+        checkArgument(touchCallbacks != null);
+        checkArgument(hapticPerformer != null);
+
+        mDetailsLookup = detailsLookup;
+        mSelectionPredicate = selectionPredicate;
+        mGestureStarter = gestureStarter;
+        mActivationCallbacks = activationCallbacks;
+        mTouchCallbacks = touchCallbacks;
+        mHapticPerformer = hapticPerformer;
+    }
+
+    @Override
+    public boolean onSingleTapUp(MotionEvent e) {
+        if (!mDetailsLookup.overItemWithSelectionKey(e)) {
+            if (DEBUG) Log.d(TAG, "Tap not associated w/ model item. Clearing selection.");
+            mSelectionHelper.clearSelection();
+            return false;
+        }
+
+        ItemDetails<K> item = mDetailsLookup.getItemDetails(e);
+        // Should really not be null at this point, but...
+        if (item == null) {
+            return false;
+        }
+
+        if (mSelectionHelper.hasSelection()) {
+            if (isRangeExtension(e)) {
+                extendSelectionRange(item);
+            } else if (mSelectionHelper.isSelected(item.getSelectionKey())) {
+                mSelectionHelper.deselect(item.getSelectionKey());
+            } else {
+                selectItem(item);
+            }
+
+            return true;
+        }
+
+        // Touch events select if they occur in the selection hotspot,
+        // otherwise they activate.
+        return item.inSelectionHotspot(e)
+                ? selectItem(item)
+                : mActivationCallbacks.onItemActivated(item, e);
+    }
+
+    @Override
+    public void onLongPress(MotionEvent e) {
+        if (!mDetailsLookup.overItemWithSelectionKey(e)) {
+            if (DEBUG) Log.d(TAG, "Ignoring LongPress on non-model-backed item.");
+            return;
+        }
+
+        ItemDetails<K> item = mDetailsLookup.getItemDetails(e);
+        // Should really not be null at this point, but...
+        if (item == null) {
+            return;
+        }
+
+        boolean handled = false;
+
+        if (isRangeExtension(e)) {
+            extendSelectionRange(item);
+            handled = true;
+        } else {
+            if (!mSelectionHelper.isSelected(item.getSelectionKey())
+                    && mSelectionPredicate.canSetStateForKey(item.getSelectionKey(), true)) {
+                // If we cannot select it, we didn't apply anchoring - therefore should not
+                // start gesture selection
+                if (selectItem(item)) {
+                    // And finally if the item was selected && we can select multiple
+                    // we kick off gesture selection.
+                    if (mSelectionPredicate.canSelectMultiple()) {
+                        mGestureStarter.run();
+                    }
+                    handled = true;
+                }
+            } else {
+                // We only initiate drag and drop on long press for touch to allow regular
+                // touch-based scrolling
+                mTouchCallbacks.onDragInitiated(e);
+                handled = true;
+            }
+        }
+
+        if (handled) {
+            mHapticPerformer.run();
+        }
+    }
+}
diff --git a/recyclerview-selection/src/main/java/androidx/recyclerview/selection/ViewAutoScroller.java b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/ViewAutoScroller.java
new file mode 100644
index 0000000..d13b0f2
--- /dev/null
+++ b/recyclerview-selection/src/main/java/androidx/recyclerview/selection/ViewAutoScroller.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import static android.support.v4.util.Preconditions.checkArgument;
+import static android.support.v4.util.Preconditions.checkState;
+
+import static androidx.recyclerview.selection.Shared.DEBUG;
+import static androidx.recyclerview.selection.Shared.VERBOSE;
+
+import android.graphics.Point;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.support.v4.view.ViewCompat;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+
+/**
+ * Provides auto-scrolling upon request when user's interaction with the application
+ * introduces a natural intent to scroll. Used by BandSelectionHelper and GestureSelectionHelper,
+ * to provide auto scrolling when user is performing selection operations.
+ */
+final class ViewAutoScroller extends AutoScroller {
+
+    private static final String TAG = "ViewAutoScroller";
+
+    // ratio used to calculate the top/bottom hotspot region; used with view height
+    private static final float DEFAULT_SCROLL_THRESHOLD_RATIO = 0.125f;
+    private static final int MAX_SCROLL_STEP = 70;
+
+    private final float mScrollThresholdRatio;
+
+    private final ScrollHost mHost;
+    private final Runnable mRunner;
+
+    private @Nullable Point mOrigin;
+    private @Nullable Point mLastLocation;
+    private boolean mPassedInitialMotionThreshold;
+
+    ViewAutoScroller(ScrollHost scrollHost) {
+        this(scrollHost, DEFAULT_SCROLL_THRESHOLD_RATIO);
+    }
+
+    @VisibleForTesting
+    ViewAutoScroller(ScrollHost scrollHost, float scrollThresholdRatio) {
+
+        checkArgument(scrollHost != null);
+
+        mHost = scrollHost;
+        mScrollThresholdRatio = scrollThresholdRatio;
+
+        mRunner = new Runnable() {
+            @Override
+            public void run() {
+                runScroll();
+            }
+        };
+    }
+
+    @Override
+    protected void reset() {
+        mHost.removeCallback(mRunner);
+        mOrigin = null;
+        mLastLocation = null;
+        mPassedInitialMotionThreshold = false;
+    }
+
+    @Override
+    protected void scroll(Point location) {
+        mLastLocation = location;
+
+        // See #aboveMotionThreshold for details on how we track initial location.
+        if (mOrigin == null) {
+            mOrigin = location;
+            if (VERBOSE) Log.v(TAG, "Origin @ " + mOrigin);
+        }
+
+        if (VERBOSE) Log.v(TAG, "Current location @ " + mLastLocation);
+
+        mHost.runAtNextFrame(mRunner);
+    }
+
+    /**
+     * Attempts to smooth-scroll the view at the given UI frame. Application should be
+     * responsible to do any clean up (such as unsubscribing scrollListeners) after the run has
+     * finished, and re-run this method on the next UI frame if applicable.
+     */
+    private void runScroll() {
+        if (DEBUG) checkState(mLastLocation != null);
+
+        if (VERBOSE) Log.v(TAG, "Running in background using event location @ " + mLastLocation);
+
+        // Compute the number of pixels the pointer's y-coordinate is past the view.
+        // Negative values mean the pointer is at or before the top of the view, and
+        // positive values mean that the pointer is at or after the bottom of the view. Note
+        // that top/bottom threshold is added here so that the view still scrolls when the
+        // pointer are in these buffer pixels.
+        int pixelsPastView = 0;
+
+        final int verticalThreshold = (int) (mHost.getViewHeight()
+                * mScrollThresholdRatio);
+
+        if (mLastLocation.y <= verticalThreshold) {
+            pixelsPastView = mLastLocation.y - verticalThreshold;
+        } else if (mLastLocation.y >= mHost.getViewHeight()
+                - verticalThreshold) {
+            pixelsPastView = mLastLocation.y - mHost.getViewHeight()
+                    + verticalThreshold;
+        }
+
+        if (pixelsPastView == 0) {
+            // If the operation that started the scrolling is no longer inactive, or if it is active
+            // but not at the edge of the view, no scrolling is necessary.
+            return;
+        }
+
+        // We're in one of the endzones. Now determine if there's enough of a difference
+        // from the orgin to take any action. Basically if a user has somehow initiated
+        // selection, but is hovering at or near their initial contact point, we don't
+        // scroll. This avoids a situation where the user initiates selection in an "endzone"
+        // only to have scrolling start automatically.
+        if (!mPassedInitialMotionThreshold && !aboveMotionThreshold(mLastLocation)) {
+            if (VERBOSE) Log.v(TAG, "Ignoring event below motion threshold.");
+            return;
+        }
+        mPassedInitialMotionThreshold = true;
+
+        if (pixelsPastView > verticalThreshold) {
+            pixelsPastView = verticalThreshold;
+        }
+
+        // Compute the number of pixels to scroll, and scroll that many pixels.
+        final int numPixels = computeScrollDistance(pixelsPastView);
+        mHost.scrollBy(numPixels);
+
+        // Replace any existing scheduled jobs with the latest and greatest..
+        mHost.removeCallback(mRunner);
+        mHost.runAtNextFrame(mRunner);
+    }
+
+    private boolean aboveMotionThreshold(Point location) {
+        // We reuse the scroll threshold to calculate a much smaller area
+        // in which we ignore motion initially.
+        int motionThreshold =
+                (int) ((mHost.getViewHeight() * mScrollThresholdRatio)
+                        * (mScrollThresholdRatio * 2));
+        return Math.abs(mOrigin.y - location.y) >= motionThreshold;
+    }
+
+    /**
+     * Computes the number of pixels to scroll based on how far the pointer is past the end
+     * of the region. Roughly based on ItemTouchHelper's algorithm for computing the number of
+     * pixels to scroll when an item is dragged to the end of a view.
+     * @return
+     */
+    @VisibleForTesting
+    int computeScrollDistance(int pixelsPastView) {
+        final int topBottomThreshold =
+                (int) (mHost.getViewHeight() * mScrollThresholdRatio);
+
+        final int direction = (int) Math.signum(pixelsPastView);
+        final int absPastView = Math.abs(pixelsPastView);
+
+        // Calculate the ratio of how far out of the view the pointer currently resides to
+        // the top/bottom scrolling hotspot of the view.
+        final float outOfBoundsRatio = Math.min(
+                1.0f, (float) absPastView / topBottomThreshold);
+        // Interpolate this ratio and use it to compute the maximum scroll that should be
+        // possible for this step.
+        final int cappedScrollStep =
+                (int) (direction * MAX_SCROLL_STEP * smoothOutOfBoundsRatio(outOfBoundsRatio));
+
+        // If the final number of pixels to scroll ends up being 0, the view should still
+        // scroll at least one pixel.
+        return cappedScrollStep != 0 ? cappedScrollStep : direction;
+    }
+
+    /**
+     * Interpolates the given out of bounds ratio on a curve which starts at (0,0) and ends
+     * at (1,1) and quickly approaches 1 near the start of that interval. This ensures that
+     * drags that are at the edge or barely past the edge of the threshold does little to no
+     * scrolling, while drags that are near the edge of the view does a lot of
+     * scrolling. The equation y=x^10 is used, but this could also be tweaked if
+     * needed.
+     * @param ratio A ratio which is in the range [0, 1].
+     * @return A "smoothed" value, also in the range [0, 1].
+     */
+    private float smoothOutOfBoundsRatio(float ratio) {
+        return (float) Math.pow(ratio, 10);
+    }
+
+    /**
+     * Used by to calculate the proper amount of pixels to scroll given time passed
+     * since scroll started, and to properly scroll / proper listener clean up if necessary.
+     *
+     * Callback used by scroller to perform UI tasks, such as scrolling and rerunning at next UI
+     * cycle.
+     */
+    abstract static class ScrollHost {
+        /**
+         * @return height of the view.
+         */
+        abstract int getViewHeight();
+
+        /**
+         * @param dy distance to scroll.
+         */
+        abstract void scrollBy(int dy);
+
+        /**
+         * @param r schedule runnable to be run at next convenient time.
+         */
+        abstract void runAtNextFrame(Runnable r);
+
+        /**
+         * @param r remove runnable from being run.
+         */
+        abstract void removeCallback(Runnable r);
+    }
+
+    public static ScrollHost createScrollHost(final RecyclerView view) {
+        return new RuntimeHost(view);
+    }
+
+    /**
+     * Tracks location of last surface contact as reported by RecyclerView.
+     */
+    private static final class RuntimeHost extends ScrollHost {
+
+        private final RecyclerView mRecView;
+
+        RuntimeHost(RecyclerView recView) {
+            mRecView = recView;
+        }
+
+        @Override
+        void runAtNextFrame(Runnable r) {
+            ViewCompat.postOnAnimation(mRecView, r);
+        }
+
+        @Override
+        void removeCallback(Runnable r) {
+            mRecView.removeCallbacks(r);
+        }
+
+        @Override
+        void scrollBy(int dy) {
+            if (VERBOSE) Log.v(TAG, "Scrolling view by: " + dy);
+            mRecView.scrollBy(0, dy);
+        }
+
+        @Override
+        int getViewHeight() {
+            return mRecView.getHeight();
+        }
+    }
+}
diff --git a/recyclerview-selection/tests/AndroidManifest.xml b/recyclerview-selection/tests/AndroidManifest.xml
new file mode 100644
index 0000000..85f42d6
--- /dev/null
+++ b/recyclerview-selection/tests/AndroidManifest.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright 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"
+          xmlns:tools="http://schemas.android.com/tools"
+          package="androidx.recyclerview.selection.test">
+    <uses-sdk android:minSdkVersion="14" />
+
+    <application android:supportsRtl="true">
+    </application>
+</manifest>
diff --git a/design/jvm-tests/NO_DOCS b/recyclerview-selection/tests/NO_DOCS
similarity index 92%
copy from design/jvm-tests/NO_DOCS
copy to recyclerview-selection/tests/NO_DOCS
index 092a39c..61c9b1a 100644
--- a/design/jvm-tests/NO_DOCS
+++ b/recyclerview-selection/tests/NO_DOCS
@@ -1,4 +1,4 @@
-# Copyright (C) 2016 The Android Open Source Project
+# Copyright 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.
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/BandSelectionHelperTest.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/BandSelectionHelperTest.java
new file mode 100644
index 0000000..8399539
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/BandSelectionHelperTest.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.graphics.Rect;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v7.widget.RecyclerView.OnScrollListener;
+import android.view.MotionEvent;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Collections;
+import java.util.List;
+
+import androidx.recyclerview.selection.SelectionHelper.SelectionPredicate;
+import androidx.recyclerview.selection.testing.TestAdapter;
+import androidx.recyclerview.selection.testing.TestAutoScroller;
+import androidx.recyclerview.selection.testing.TestBandPredicate;
+import androidx.recyclerview.selection.testing.TestData;
+import androidx.recyclerview.selection.testing.TestEvents.Builder;
+import androidx.recyclerview.selection.testing.TestItemKeyProvider;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class BandSelectionHelperTest {
+
+    private List<String> mItems;
+    private BandSelectionHelper<String> mBandController;
+    private boolean mIsActive;
+    private Builder mStartBuilder;
+    private Builder mStopBuilder;
+    private MotionEvent mStartEvent;
+    private MotionEvent mStopEvent;
+    private TestBandHost mBandHost;
+    private TestBandPredicate mBandPredicate;
+    private TestAdapter<String> mAdapter;
+
+    @Before
+    public void setup() throws Exception {
+        mItems = TestData.createStringData(10);
+        mIsActive = false;
+        mAdapter = new TestAdapter<String>();
+        mAdapter.updateTestModelIds(mItems);
+        mBandHost = new TestBandHost();
+        mBandPredicate = new TestBandPredicate();
+        ItemKeyProvider<String> keyProvider =
+                new TestItemKeyProvider<>(ItemKeyProvider.SCOPE_MAPPED, mAdapter);
+        ContentLock contentLock = new ContentLock();
+        SelectionPredicate<String> selectionTest = SelectionPredicates.selectAnything();
+
+        SelectionHelper<String> helper = new DefaultSelectionHelper<String>(
+                keyProvider,
+                selectionTest);
+
+        EventBridge.install(mAdapter, helper, keyProvider);
+        FocusCallbacks<String> focusCallbacks = FocusCallbacks.dummy();
+
+        mBandController = new BandSelectionHelper<String>(
+                mBandHost,
+                new TestAutoScroller(),
+                keyProvider,
+                helper,
+                selectionTest,
+                mBandPredicate,
+                focusCallbacks,
+                contentLock) {
+                    @Override
+                    public boolean isActive() {
+                        return mIsActive;
+                    }
+                };
+
+        mStartBuilder = new Builder().mouse().primary().action(MotionEvent.ACTION_MOVE);
+        mStopBuilder = new Builder().mouse().action(MotionEvent.ACTION_UP);
+        mStartEvent = mStartBuilder.build();
+        mStopEvent = mStopBuilder.build();
+    }
+
+    @Test
+    public void testStartsBand() {
+        assertTrue(mBandController.shouldStart(mStartEvent));
+    }
+
+    @Test
+    public void testStartsBand_NoItems() {
+        mAdapter.updateTestModelIds(Collections.EMPTY_LIST);
+        assertTrue(mBandController.shouldStart(mStartEvent));
+    }
+
+    @Test
+    public void testBadStart_NoButtons() {
+        assertFalse(mBandController.shouldStart(
+                mStartBuilder.releaseButton(MotionEvent.BUTTON_PRIMARY).build()));
+    }
+
+    @Test
+    public void testBadStart_SecondaryButton() {
+        assertFalse(
+                mBandController.shouldStart(mStartBuilder.secondary().build()));
+    }
+
+    @Test
+    public void testBadStart_TertiaryButton() {
+        assertFalse(
+                mBandController.shouldStart(mStartBuilder.tertiary().build()));
+    }
+
+    @Test
+    public void testBadStart_Touch() {
+        assertFalse(mBandController.shouldStart(
+                mStartBuilder.touch().releaseButton(MotionEvent.BUTTON_PRIMARY).build()));
+    }
+
+    @Test
+    public void testBadStart_RespectsCanInitiateBand() {
+        mBandPredicate.setCanInitiate(false);
+        assertFalse(mBandController.shouldStart(mStartEvent));
+    }
+
+    @Test
+    public void testBadStart_ActionDown() {
+        assertFalse(mBandController
+                .shouldStart(mStartBuilder.action(MotionEvent.ACTION_DOWN).build()));
+    }
+
+    @Test
+    public void testBadStart_ActionUp() {
+        assertFalse(mBandController
+                .shouldStart(mStartBuilder.action(MotionEvent.ACTION_UP).build()));
+    }
+
+    @Test
+    public void testBadStart_ActionPointerDown() {
+        assertFalse(mBandController.shouldStart(
+                mStartBuilder.action(MotionEvent.ACTION_POINTER_DOWN).build()));
+    }
+
+    @Test
+    public void testBadStart_ActionPointerUp() {
+        assertFalse(mBandController.shouldStart(
+                mStartBuilder.action(MotionEvent.ACTION_POINTER_UP).build()));
+    }
+
+    @Test
+    public void testBadStart_alreadyActive() {
+        mIsActive = true;
+        assertFalse(mBandController.shouldStart(mStartEvent));
+    }
+
+    @Test
+    public void testGoodStop() {
+        mIsActive = true;
+        assertTrue(mBandController.shouldStop(mStopEvent));
+    }
+
+    @Test
+    public void testGoodStop_PointerUp() {
+        mIsActive = true;
+        assertTrue(mBandController
+                .shouldStop(mStopBuilder.action(MotionEvent.ACTION_POINTER_UP).build()));
+    }
+
+    @Test
+    public void testGoodStop_Cancel() {
+        mIsActive = true;
+        assertTrue(mBandController
+                .shouldStop(mStopBuilder.action(MotionEvent.ACTION_CANCEL).build()));
+    }
+
+    @Test
+    public void testBadStop_NotActive() {
+        assertFalse(mBandController.shouldStop(mStopEvent));
+    }
+
+    @Test
+    public void testBadStop_Move() {
+        mIsActive = true;
+        assertFalse(mBandController.shouldStop(
+                mStopBuilder.action(MotionEvent.ACTION_MOVE).touch().build()));
+    }
+
+    @Test
+    public void testBadStop_Down() {
+        mIsActive = true;
+        assertFalse(mBandController.shouldStop(
+                mStopBuilder.action(MotionEvent.ACTION_DOWN).touch().build()));
+    }
+
+    private final class TestBandHost extends BandSelectionHelper.BandHost<String> {
+        @Override
+        GridModel<String> createGridModel() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        void showBand(Rect rect) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        void hideBand() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        void addOnScrollListener(OnScrollListener listener) {
+        }
+    }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/ContentLockTest.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/ContentLockTest.java
new file mode 100644
index 0000000..b012f84
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/ContentLockTest.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+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;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ContentLockTest {
+
+    private ContentLock mLock = new ContentLock();
+    private boolean mCalled;
+
+    private Runnable mRunnable = new Runnable() {
+        @Override
+        public void run() {
+            mCalled = true;
+        }
+    };
+
+    @Before
+    public void setUp() {
+        mCalled = false;
+    }
+
+    @Test
+    public void testNotBlocking_callbackNotBlocked() {
+        mLock.runWhenUnlocked(mRunnable);
+        assertTrue(mCalled);
+    }
+
+    @Test
+    public void testToggleBlocking_callbackNotBlocked() {
+        mLock.block();
+        mLock.unblock();
+        mLock.runWhenUnlocked(mRunnable);
+        assertTrue(mCalled);
+    }
+
+    @Test
+    public void testBlocking_callbackBlocked() {
+        mLock.block();
+        mLock.runWhenUnlocked(mRunnable);
+        assertFalse(mCalled);
+    }
+
+    @Test
+    public void testBlockthenUnblock_callbackNotBlocked() {
+        mLock.block();
+        mLock.runWhenUnlocked(mRunnable);
+        mLock.unblock();
+        assertTrue(mCalled);
+    }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/DefaultSelectionHelperTest.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/DefaultSelectionHelperTest.java
new file mode 100644
index 0000000..ddcd9a8
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/DefaultSelectionHelperTest.java
@@ -0,0 +1,429 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import static org.junit.Assert.assertFalse;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.SparseBooleanArray;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import androidx.recyclerview.selection.SelectionHelper.SelectionPredicate;
+import androidx.recyclerview.selection.testing.SelectionProbe;
+import androidx.recyclerview.selection.testing.TestAdapter;
+import androidx.recyclerview.selection.testing.TestItemKeyProvider;
+import androidx.recyclerview.selection.testing.TestSelectionObserver;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class DefaultSelectionHelperTest {
+
+    private List<String> mItems;
+    private Set<String> mIgnored;
+    private TestAdapter mAdapter;
+    private DefaultSelectionHelper<String> mHelper;
+    private TestSelectionObserver<String> mListener;
+    private SelectionProbe mSelection;
+
+    @Before
+    public void setUp() throws Exception {
+        mIgnored = new HashSet<>();
+        mItems = TestAdapter.createItemList(100);
+        mListener = new TestSelectionObserver<>();
+        mAdapter = new TestAdapter();
+        mAdapter.updateTestModelIds(mItems);
+
+        SelectionPredicate selectionPredicate = new SelectionPredicate<String>() {
+
+            @Override
+            public boolean canSetStateForKey(String id, boolean nextState) {
+                return !nextState || !mIgnored.contains(id);
+            }
+
+            @Override
+            public boolean canSetStateAtPosition(int position, boolean nextState) {
+                throw new UnsupportedOperationException("Not implemented.");
+            }
+
+            @Override
+            public boolean canSelectMultiple() {
+                return true;
+            }
+        };
+
+        ItemKeyProvider<String> keyProvider =
+                new TestItemKeyProvider<String>(ItemKeyProvider.SCOPE_MAPPED, mAdapter);
+        mHelper = new DefaultSelectionHelper<>(
+                keyProvider,
+                selectionPredicate);
+
+        EventBridge.install(mAdapter, mHelper, keyProvider);
+
+        mHelper.addObserver(mListener);
+
+        mSelection = new SelectionProbe(mHelper, mListener);
+
+        mIgnored.clear();
+    }
+
+    @Test
+    public void testSelect() {
+        mHelper.select(mItems.get(7));
+
+        mSelection.assertSelection(7);
+    }
+
+    @Test
+    public void testDeselect() {
+        mHelper.select(mItems.get(7));
+        mHelper.deselect(mItems.get(7));
+
+        mSelection.assertNoSelection();
+    }
+
+    @Test
+    public void testSelection_DoNothingOnUnselectableItem() {
+        mIgnored.add(mItems.get(7));
+        boolean selected = mHelper.select(mItems.get(7));
+
+        assertFalse(selected);
+        mSelection.assertNoSelection();
+    }
+
+    @Test
+    public void testSelect_NotifiesListenersOfChange() {
+        mHelper.select(mItems.get(7));
+
+        mListener.assertSelectionChanged();
+    }
+
+    @Test
+    public void testSelect_NotifiesAdapterOfSelect() {
+        mHelper.select(mItems.get(7));
+
+        mAdapter.assertNotifiedOfSelectionChange(7);
+    }
+
+    @Test
+    public void testSelect_NotifiesAdapterOfDeselect() {
+        mHelper.select(mItems.get(7));
+        mAdapter.resetSelectionNotifications();
+        mHelper.deselect(mItems.get(7));
+        mAdapter.assertNotifiedOfSelectionChange(7);
+    }
+
+    @Test
+    public void testDeselect_NotifiesSelectionChanged() {
+        mHelper.select(mItems.get(7));
+        mHelper.deselect(mItems.get(7));
+
+        mListener.assertSelectionChanged();
+    }
+
+    @Test
+    public void testSelection_PersistsOnUpdate() {
+        mHelper.select(mItems.get(7));
+        mAdapter.updateTestModelIds(mItems);
+
+        mSelection.assertSelection(7);
+    }
+
+    @Test
+    public void testSetItemsSelected() {
+        mHelper.setItemsSelected(getStringIds(6, 7, 8), true);
+
+        mSelection.assertRangeSelected(6, 8);
+    }
+
+    @Test
+    public void testSetItemsSelected_SkipUnselectableItem() {
+        mIgnored.add(mItems.get(7));
+
+        mHelper.setItemsSelected(getStringIds(6, 7, 8), true);
+
+        mSelection.assertSelected(6);
+        mSelection.assertNotSelected(7);
+        mSelection.assertSelected(8);
+    }
+
+    @Test
+    public void testClear_RemovesPrimarySelection() {
+        mHelper.select(mItems.get(1));
+        mHelper.select(mItems.get(2));
+        mHelper.clear();
+
+        assertFalse(mHelper.hasSelection());
+    }
+
+    @Test
+    public void testClear_RemovesProvisionalSelection() {
+        Set<String> prov = new HashSet<>();
+        prov.add(mItems.get(1));
+        prov.add(mItems.get(2));
+        mHelper.clear();
+        // if there is a provisional selection, convert it to regular selection so we can poke it.
+        mHelper.mergeProvisionalSelection();
+
+        assertFalse(mHelper.hasSelection());
+    }
+
+    @Test
+    public void testRangeSelection() {
+        mHelper.startRange(15);
+        mHelper.extendRange(19);
+        mSelection.assertRangeSelection(15, 19);
+    }
+
+    @Test
+    public void testRangeSelection_SkipUnselectableItem() {
+        mIgnored.add(mItems.get(17));
+
+        mHelper.startRange(15);
+        mHelper.extendRange(19);
+
+        mSelection.assertRangeSelected(15, 16);
+        mSelection.assertNotSelected(17);
+        mSelection.assertRangeSelected(18, 19);
+    }
+
+    @Test
+    public void testRangeSelection_snapExpand() {
+        mHelper.startRange(15);
+        mHelper.extendRange(19);
+        mHelper.extendRange(27);
+        mSelection.assertRangeSelection(15, 27);
+    }
+
+    @Test
+    public void testRangeSelection_snapContract() {
+        mHelper.startRange(15);
+        mHelper.extendRange(27);
+        mHelper.extendRange(19);
+        mSelection.assertRangeSelection(15, 19);
+    }
+
+    @Test
+    public void testRangeSelection_snapInvert() {
+        mHelper.startRange(15);
+        mHelper.extendRange(27);
+        mHelper.extendRange(3);
+        mSelection.assertRangeSelection(3, 15);
+    }
+
+    @Test
+    public void testRangeSelection_multiple() {
+        mHelper.startRange(15);
+        mHelper.extendRange(27);
+        mHelper.endRange();
+        mHelper.startRange(42);
+        mHelper.extendRange(57);
+        mSelection.assertSelectionSize(29);
+        mSelection.assertRangeSelected(15, 27);
+        mSelection.assertRangeSelected(42, 57);
+    }
+
+    @Test
+    public void testProvisionalRangeSelection() {
+        mHelper.startRange(13);
+        mHelper.extendProvisionalRange(15);
+        mSelection.assertRangeSelection(13, 15);
+        mHelper.getSelection().mergeProvisionalSelection();
+        mHelper.endRange();
+        mSelection.assertSelectionSize(3);
+    }
+
+    @Test
+    public void testProvisionalRangeSelection_endEarly() {
+        mHelper.startRange(13);
+        mHelper.extendProvisionalRange(15);
+        mSelection.assertRangeSelection(13, 15);
+
+        mHelper.endRange();
+        // If we end range selection prematurely for provision selection, nothing should be selected
+        // except the first item
+        mSelection.assertSelectionSize(1);
+    }
+
+    @Test
+    public void testProvisionalRangeSelection_snapExpand() {
+        mHelper.startRange(13);
+        mHelper.extendProvisionalRange(15);
+        mSelection.assertRangeSelection(13, 15);
+        mHelper.getSelection().mergeProvisionalSelection();
+        mHelper.extendRange(18);
+        mSelection.assertRangeSelection(13, 18);
+    }
+
+    @Test
+    public void testCombinationRangeSelection_IntersectsOldSelection() {
+        mHelper.startRange(13);
+        mHelper.extendRange(15);
+        mSelection.assertRangeSelection(13, 15);
+
+        mHelper.startRange(11);
+        mHelper.extendProvisionalRange(18);
+        mSelection.assertRangeSelected(11, 18);
+        mHelper.endRange();
+        mSelection.assertRangeSelected(13, 15);
+        mSelection.assertRangeSelected(11, 11);
+        mSelection.assertSelectionSize(4);
+    }
+
+    @Test
+    public void testProvisionalSelection() {
+        Selection s = mHelper.getSelection();
+        mSelection.assertNoSelection();
+
+        // Mimicking band selection case -- BandController notifies item callback by itself.
+        mListener.onItemStateChanged(mItems.get(1), true);
+        mListener.onItemStateChanged(mItems.get(2), true);
+
+        SparseBooleanArray provisional = new SparseBooleanArray();
+        provisional.append(1, true);
+        provisional.append(2, true);
+        s.setProvisionalSelection(getItemIds(provisional));
+        mSelection.assertSelection(1, 2);
+    }
+
+    @Test
+    public void testProvisionalSelection_Replace() {
+        Selection s = mHelper.getSelection();
+
+        // Mimicking band selection case -- BandController notifies item callback by itself.
+        mListener.onItemStateChanged(mItems.get(1), true);
+        mListener.onItemStateChanged(mItems.get(2), true);
+        SparseBooleanArray provisional = new SparseBooleanArray();
+        provisional.append(1, true);
+        provisional.append(2, true);
+        s.setProvisionalSelection(getItemIds(provisional));
+
+        mListener.onItemStateChanged(mItems.get(1), false);
+        mListener.onItemStateChanged(mItems.get(2), false);
+        provisional.clear();
+
+        mListener.onItemStateChanged(mItems.get(3), true);
+        mListener.onItemStateChanged(mItems.get(4), true);
+        provisional.append(3, true);
+        provisional.append(4, true);
+        s.setProvisionalSelection(getItemIds(provisional));
+        mSelection.assertSelection(3, 4);
+    }
+
+    @Test
+    public void testProvisionalSelection_IntersectsExistingProvisionalSelection() {
+        Selection s = mHelper.getSelection();
+
+        // Mimicking band selection case -- BandController notifies item callback by itself.
+        mListener.onItemStateChanged(mItems.get(1), true);
+        mListener.onItemStateChanged(mItems.get(2), true);
+        SparseBooleanArray provisional = new SparseBooleanArray();
+        provisional.append(1, true);
+        provisional.append(2, true);
+        s.setProvisionalSelection(getItemIds(provisional));
+
+        mListener.onItemStateChanged(mItems.get(1), false);
+        mListener.onItemStateChanged(mItems.get(2), false);
+        provisional.clear();
+
+        mListener.onItemStateChanged(mItems.get(1), true);
+        provisional.append(1, true);
+        s.setProvisionalSelection(getItemIds(provisional));
+        mSelection.assertSelection(1);
+    }
+
+    @Test
+    public void testProvisionalSelection_Apply() {
+        Selection s = mHelper.getSelection();
+
+        // Mimicking band selection case -- BandController notifies item callback by itself.
+        mListener.onItemStateChanged(mItems.get(1), true);
+        mListener.onItemStateChanged(mItems.get(2), true);
+        SparseBooleanArray provisional = new SparseBooleanArray();
+        provisional.append(1, true);
+        provisional.append(2, true);
+        s.setProvisionalSelection(getItemIds(provisional));
+        s.mergeProvisionalSelection();
+
+        mSelection.assertSelection(1, 2);
+    }
+
+    @Test
+    public void testProvisionalSelection_Cancel() {
+        mHelper.select(mItems.get(1));
+        mHelper.select(mItems.get(2));
+        Selection s = mHelper.getSelection();
+
+        SparseBooleanArray provisional = new SparseBooleanArray();
+        provisional.append(3, true);
+        provisional.append(4, true);
+        s.setProvisionalSelection(getItemIds(provisional));
+        s.clearProvisionalSelection();
+
+        // Original selection should remain.
+        mSelection.assertSelection(1, 2);
+    }
+
+    @Test
+    public void testProvisionalSelection_IntersectsAppliedSelection() {
+        mHelper.select(mItems.get(1));
+        mHelper.select(mItems.get(2));
+        Selection s = mHelper.getSelection();
+
+        // Mimicking band selection case -- BandController notifies item callback by itself.
+        mListener.onItemStateChanged(mItems.get(3), true);
+        SparseBooleanArray provisional = new SparseBooleanArray();
+        provisional.append(2, true);
+        provisional.append(3, true);
+        s.setProvisionalSelection(getItemIds(provisional));
+        mSelection.assertSelection(1, 2, 3);
+    }
+
+    private Set<String> getItemIds(SparseBooleanArray selection) {
+        Set<String> ids = new HashSet<>();
+
+        int count = selection.size();
+        for (int i = 0; i < count; ++i) {
+            ids.add(mItems.get(selection.keyAt(i)));
+        }
+
+        return ids;
+    }
+
+    @Test
+    public void testObserverOnChanged_NotifiesListenersOfChange() {
+        mAdapter.notifyDataSetChanged();
+
+        mListener.assertSelectionChanged();
+    }
+
+    private Iterable<String> getStringIds(int... ids) {
+        List<String> stringIds = new ArrayList<>(ids.length);
+        for (int id : ids) {
+            stringIds.add(mItems.get(id));
+        }
+        return stringIds;
+    }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/DefaultSelectionHelper_SingleSelectTest.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/DefaultSelectionHelper_SingleSelectTest.java
new file mode 100644
index 0000000..02527b1
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/DefaultSelectionHelper_SingleSelectTest.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import static org.junit.Assert.fail;
+
+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 java.util.List;
+
+import androidx.recyclerview.selection.testing.SelectionProbe;
+import androidx.recyclerview.selection.testing.TestAdapter;
+import androidx.recyclerview.selection.testing.TestItemKeyProvider;
+import androidx.recyclerview.selection.testing.TestSelectionObserver;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class DefaultSelectionHelper_SingleSelectTest {
+
+    private List<String> mItems;
+    private SelectionHelper<String> mHelper;
+    private TestSelectionObserver<String> mListener;
+    private SelectionProbe mSelection;
+
+    @Before
+    public void setUp() throws Exception {
+        mItems = TestAdapter.createItemList(100);
+        mListener = new TestSelectionObserver<>();
+        TestAdapter adapter = new TestAdapter();
+        adapter.updateTestModelIds(mItems);
+
+        ItemKeyProvider<String> keyProvider =
+                new TestItemKeyProvider<>(ItemKeyProvider.SCOPE_MAPPED, adapter);
+        mHelper = new DefaultSelectionHelper<>(
+                keyProvider,
+                SelectionPredicates.selectSingleAnything());
+        EventBridge.install(adapter, mHelper, keyProvider);
+
+        mHelper.addObserver(mListener);
+
+        mSelection = new SelectionProbe(mHelper);
+    }
+
+    @Test
+    public void testSimpleSelect() {
+        mHelper.select(mItems.get(3));
+        mHelper.select(mItems.get(4));
+        mListener.assertSelectionChanged();
+        mSelection.assertSelection(4);
+    }
+
+    @Test
+    public void testRangeSelectionNotEstablished() {
+        mHelper.select(mItems.get(3));
+        mListener.reset();
+
+        try {
+            mHelper.extendRange(10);
+            fail("Should have thrown.");
+        } catch (Exception expected) { }
+
+        mListener.assertSelectionUnchanged();
+        mSelection.assertSelection(3);
+    }
+
+    @Test
+    public void testProvisionalRangeSelection_Ignored() {
+        mHelper.startRange(13);
+        mHelper.extendProvisionalRange(15);
+        mSelection.assertSelection(13);
+    }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/GestureRouterTest.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/GestureRouterTest.java
new file mode 100644
index 0000000..0e5a5a9
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/GestureRouterTest.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyFloat;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.GestureDetector.OnDoubleTapListener;
+import android.view.GestureDetector.OnGestureListener;
+import android.view.MotionEvent;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+import androidx.recyclerview.selection.testing.TestEvents.Mouse;
+import androidx.recyclerview.selection.testing.TestEvents.Touch;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class GestureRouterTest {
+
+    private TestHandler mHandler;
+    private TestHandler mAlt;
+    private GestureRouter<TestHandler> mRouter;
+
+    @Before
+    public void setUp() {
+        mAlt = new TestHandler();
+        mHandler = new TestHandler();
+    }
+
+    @Test
+    public void testDelegates() {
+        mRouter = new GestureRouter<>();
+        mRouter.register(MotionEvent.TOOL_TYPE_MOUSE, mHandler);
+        mRouter.register(MotionEvent.TOOL_TYPE_FINGER, mAlt);
+
+        mRouter.onDown(Mouse.CLICK);
+        mHandler.assertCalled_onDown(Mouse.CLICK);
+        mAlt.assertNotCalled_onDown();
+
+        mRouter.onShowPress(Mouse.CLICK);
+        mHandler.assertCalled_onShowPress(Mouse.CLICK);
+        mAlt.assertNotCalled_onShowPress();
+
+        mRouter.onSingleTapUp(Mouse.CLICK);
+        mHandler.assertCalled_onSingleTapUp(Mouse.CLICK);
+        mAlt.assertNotCalled_onSingleTapUp();
+
+        mRouter.onScroll(null, Mouse.CLICK, -1, -1);
+        mHandler.assertCalled_onScroll(null, Mouse.CLICK, -1, -1);
+        mAlt.assertNotCalled_onScroll();
+
+        mRouter.onLongPress(Mouse.CLICK);
+        mHandler.assertCalled_onLongPress(Mouse.CLICK);
+        mAlt.assertNotCalled_onLongPress();
+
+        mRouter.onFling(null, Mouse.CLICK, -1, -1);
+        mHandler.assertCalled_onFling(null, Mouse.CLICK, -1, -1);
+        mAlt.assertNotCalled_onFling();
+
+        mRouter.onSingleTapConfirmed(Mouse.CLICK);
+        mHandler.assertCalled_onSingleTapConfirmed(Mouse.CLICK);
+        mAlt.assertNotCalled_onSingleTapConfirmed();
+
+        mRouter.onDoubleTap(Mouse.CLICK);
+        mHandler.assertCalled_onDoubleTap(Mouse.CLICK);
+        mAlt.assertNotCalled_onDoubleTap();
+
+        mRouter.onDoubleTapEvent(Mouse.CLICK);
+        mHandler.assertCalled_onDoubleTapEvent(Mouse.CLICK);
+        mAlt.assertNotCalled_onDoubleTapEvent();
+    }
+
+    @Test
+    public void testFallsback() {
+        mRouter = new GestureRouter<>(mAlt);
+        mRouter.register(MotionEvent.TOOL_TYPE_MOUSE, mHandler);
+
+        mRouter.onDown(Touch.TAP);
+        mAlt.assertCalled_onDown(Touch.TAP);
+
+        mRouter.onShowPress(Touch.TAP);
+        mAlt.assertCalled_onShowPress(Touch.TAP);
+
+        mRouter.onSingleTapUp(Touch.TAP);
+        mAlt.assertCalled_onSingleTapUp(Touch.TAP);
+
+        mRouter.onScroll(null, Touch.TAP, -1, -1);
+        mAlt.assertCalled_onScroll(null, Touch.TAP, -1, -1);
+
+        mRouter.onLongPress(Touch.TAP);
+        mAlt.assertCalled_onLongPress(Touch.TAP);
+
+        mRouter.onFling(null, Touch.TAP, -1, -1);
+        mAlt.assertCalled_onFling(null, Touch.TAP, -1, -1);
+
+        mRouter.onSingleTapConfirmed(Touch.TAP);
+        mAlt.assertCalled_onSingleTapConfirmed(Touch.TAP);
+
+        mRouter.onDoubleTap(Touch.TAP);
+        mAlt.assertCalled_onDoubleTap(Touch.TAP);
+
+        mRouter.onDoubleTapEvent(Touch.TAP);
+        mAlt.assertCalled_onDoubleTapEvent(Touch.TAP);
+    }
+
+    @Test
+    public void testEatsEventsWhenNoFallback() {
+        mRouter = new GestureRouter<>();
+        // Register the the delegate on mouse so touch events don't get handled.
+        mRouter.register(MotionEvent.TOOL_TYPE_MOUSE, mHandler);
+
+        mRouter.onDown(Touch.TAP);
+        mAlt.assertNotCalled_onDown();
+
+        mRouter.onShowPress(Touch.TAP);
+        mAlt.assertNotCalled_onShowPress();
+
+        mRouter.onSingleTapUp(Touch.TAP);
+        mAlt.assertNotCalled_onSingleTapUp();
+
+        mRouter.onScroll(null, Touch.TAP, -1, -1);
+        mAlt.assertNotCalled_onScroll();
+
+        mRouter.onLongPress(Touch.TAP);
+        mAlt.assertNotCalled_onLongPress();
+
+        mRouter.onFling(null, Touch.TAP, -1, -1);
+        mAlt.assertNotCalled_onFling();
+
+        mRouter.onSingleTapConfirmed(Touch.TAP);
+        mAlt.assertNotCalled_onSingleTapConfirmed();
+
+        mRouter.onDoubleTap(Touch.TAP);
+        mAlt.assertNotCalled_onDoubleTap();
+
+        mRouter.onDoubleTapEvent(Touch.TAP);
+        mAlt.assertNotCalled_onDoubleTapEvent();
+    }
+
+    private static final class TestHandler implements OnGestureListener, OnDoubleTapListener {
+
+        private final Spy mSpy = Mockito.mock(Spy.class);
+
+        @Override
+        public boolean onDown(MotionEvent e) {
+            return mSpy.onDown(e);
+        }
+
+        @Override
+        public void onShowPress(MotionEvent e) {
+            mSpy.onShowPress(e);
+        }
+
+        @Override
+        public boolean onSingleTapUp(MotionEvent e) {
+            return mSpy.onSingleTapUp(e);
+        }
+
+        @Override
+        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+            return mSpy.onScroll(e1, e2, distanceX, distanceY);
+        }
+
+        @Override
+        public void onLongPress(MotionEvent e) {
+            mSpy.onLongPress(e);
+        }
+
+        @Override
+        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+            return mSpy.onFling(e1, e2, velocityX, velocityY);
+        }
+
+        @Override
+        public boolean onSingleTapConfirmed(MotionEvent e) {
+            return mSpy.onSingleTapConfirmed(e);
+        }
+
+        @Override
+        public boolean onDoubleTap(MotionEvent e) {
+            return mSpy.onDoubleTap(e);
+        }
+
+        @Override
+        public boolean onDoubleTapEvent(MotionEvent e) {
+            return mSpy.onDoubleTapEvent(e);
+        }
+
+        void assertCalled_onDown(MotionEvent e) {
+            verify(mSpy).onDown(e);
+        }
+
+        void assertCalled_onShowPress(MotionEvent e) {
+            verify(mSpy).onShowPress(e);
+        }
+
+        void assertCalled_onSingleTapUp(MotionEvent e) {
+            verify(mSpy).onSingleTapUp(e);
+        }
+
+        void assertCalled_onScroll(MotionEvent e1, MotionEvent e2, float x, float y) {
+            verify(mSpy).onScroll(e1, e2, x, y);
+        }
+
+        void assertCalled_onLongPress(MotionEvent e) {
+            verify(mSpy).onLongPress(e);
+        }
+
+        void assertCalled_onFling(MotionEvent e1, MotionEvent e2, float x, float y) {
+            Mockito.verify(mSpy).onFling(e1, e2, x, y);
+        }
+
+        void assertCalled_onSingleTapConfirmed(MotionEvent e) {
+            Mockito.verify(mSpy).onSingleTapConfirmed(e);
+        }
+
+        void assertCalled_onDoubleTap(MotionEvent e) {
+            Mockito.verify(mSpy).onDoubleTap(e);
+        }
+
+        void assertCalled_onDoubleTapEvent(MotionEvent e) {
+            Mockito.verify(mSpy).onDoubleTapEvent(e);
+        }
+
+        void assertNotCalled_onDown() {
+            verify(mSpy, never()).onDown((MotionEvent) any());
+        }
+
+        void assertNotCalled_onShowPress() {
+            verify(mSpy, never()).onShowPress((MotionEvent) any());
+        }
+
+        void assertNotCalled_onSingleTapUp() {
+            verify(mSpy, never()).onSingleTapUp((MotionEvent) any());
+        }
+
+        void assertNotCalled_onScroll() {
+            verify(mSpy, never()).onScroll(
+                    (MotionEvent) any(), (MotionEvent) any(), anyFloat(), anyFloat());
+        }
+
+        void assertNotCalled_onLongPress() {
+            verify(mSpy, never()).onLongPress((MotionEvent) any());
+        }
+
+        void assertNotCalled_onFling() {
+            Mockito.verify(mSpy, never()).onFling(
+                    (MotionEvent) any(), (MotionEvent) any(), anyFloat(), anyFloat());
+        }
+
+        void assertNotCalled_onSingleTapConfirmed() {
+            Mockito.verify(mSpy, never()).onSingleTapConfirmed((MotionEvent) any());
+        }
+
+        void assertNotCalled_onDoubleTap() {
+            Mockito.verify(mSpy, never()).onDoubleTap((MotionEvent) any());
+        }
+
+        void assertNotCalled_onDoubleTapEvent() {
+            Mockito.verify(mSpy, never()).onDoubleTapEvent((MotionEvent) any());
+        }
+    }
+
+    private interface Spy extends OnGestureListener, OnDoubleTapListener {
+    }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/GestureSelectionHelperTest.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/GestureSelectionHelperTest.java
new file mode 100644
index 0000000..f1e38d9
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/GestureSelectionHelperTest.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v7.widget.RecyclerView;
+import android.view.MotionEvent;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+import androidx.recyclerview.selection.testing.SelectionHelpers;
+import androidx.recyclerview.selection.testing.SelectionProbe;
+import androidx.recyclerview.selection.testing.TestAutoScroller;
+import androidx.recyclerview.selection.testing.TestData;
+import androidx.recyclerview.selection.testing.TestEvents;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class GestureSelectionHelperTest {
+
+    private static final List<String> ITEMS = TestData.createStringData(100);
+    private static final MotionEvent DOWN = TestEvents.builder()
+            .action(MotionEvent.ACTION_DOWN)
+            .location(1, 1)
+            .build();
+
+    private static final MotionEvent MOVE = TestEvents.builder()
+            .action(MotionEvent.ACTION_MOVE)
+            .location(1, 1)
+            .build();
+
+    private static final MotionEvent UP = TestEvents.builder()
+            .action(MotionEvent.ACTION_UP)
+            .location(1, 1)
+            .build();
+
+    private GestureSelectionHelper mHelper;
+    private SelectionHelper<String> mSelectionHelper;
+    private SelectionProbe mSelection;
+    private ContentLock mLock;
+    private TestViewDelegate mView;
+
+    @Before
+    public void setUp() {
+        mSelectionHelper = SelectionHelpers.createTestInstance(ITEMS);
+        mSelection = new SelectionProbe(mSelectionHelper);
+        mLock = new ContentLock();
+        mView = new TestViewDelegate();
+        mHelper = new GestureSelectionHelper(
+                mSelectionHelper, mView, new TestAutoScroller(), mLock);
+    }
+
+    @Test
+    public void testIgnoresDownOnNoPosition() {
+        mView.mNextPosition = RecyclerView.NO_POSITION;
+        assertFalse(mHelper.onInterceptTouchEvent(null, DOWN));
+    }
+
+
+    @Test
+    public void testNoStartOnIllegalPosition() {
+        mHelper.onInterceptTouchEvent(null, DOWN);
+        try {
+            mHelper.start();
+            fail("Should have thrown.");
+        } catch (Exception expected) {
+        }
+    }
+
+    @Test
+    public void testClaimsDownOnItem() {
+        mView.mNextPosition = 0;
+        assertTrue(mHelper.onInterceptTouchEvent(null, DOWN));
+    }
+
+    @Test
+    public void testClaimsMoveIfStarted() {
+        mView.mNextPosition = 0;
+        assertTrue(mHelper.onInterceptTouchEvent(null, DOWN));
+
+        // Normally, this is controller by the TouchSelectionHelper via a a long press gesture.
+        mSelectionHelper.select("1");
+        mSelectionHelper.anchorRange(1);
+        mHelper.start();
+        assertTrue(mHelper.onInterceptTouchEvent(null, MOVE));
+    }
+
+    @Test
+    public void testCreatesRangeSelection() {
+        mView.mNextPosition = 1;
+        mHelper.onInterceptTouchEvent(null, DOWN);
+        // Another way we are implicitly coupled to TouchInputHandler, is that we depend on
+        // long press to establish the initial anchor point. Without that we'll get an
+        // error when we try to extend the range.
+
+        mSelectionHelper.select("1");
+        mSelectionHelper.anchorRange(1);
+        mHelper.start();
+
+        mHelper.onTouchEvent(null, MOVE);
+
+        mView.mNextPosition = 9;
+        mHelper.onTouchEvent(null, MOVE);
+        mHelper.onTouchEvent(null, UP);
+
+        mSelection.assertRangeSelected(1,  9);
+    }
+
+    private static final class TestViewDelegate extends GestureSelectionHelper.ViewDelegate {
+
+        private int mNextPosition = RecyclerView.NO_POSITION;
+
+        @Override
+        int getHeight() {
+            return 1000;
+        }
+
+        @Override
+        int getItemUnder(MotionEvent e) {
+            return mNextPosition;
+        }
+
+        @Override
+        int getLastGlidedItemPosition(MotionEvent e) {
+            return mNextPosition;
+        }
+    }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/GestureSelectionHelper_RecyclerViewDelegateTest.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/GestureSelectionHelper_RecyclerViewDelegateTest.java
new file mode 100644
index 0000000..7e5251a
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/GestureSelectionHelper_RecyclerViewDelegateTest.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.MotionEvent;
+import android.view.View;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import androidx.recyclerview.selection.GestureSelectionHelper.RecyclerViewDelegate;
+import androidx.recyclerview.selection.testing.TestEvents;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class GestureSelectionHelper_RecyclerViewDelegateTest {
+
+    // Simulate a (20, 20) box locating at (20, 20)
+    static final int LEFT_BORDER = 20;
+    static final int RIGHT_BORDER = 40;
+    static final int TOP_BORDER = 20;
+    static final int BOTTOM_BORDER = 40;
+
+    @Test
+    public void testLtrPastLastItem() {
+        MotionEvent event = createEvent(100, 100);
+        assertTrue(RecyclerViewDelegate.isPastLastItem(
+                TOP_BORDER, LEFT_BORDER, RIGHT_BORDER, event, View.LAYOUT_DIRECTION_LTR));
+    }
+
+    @Test
+    public void testLtrPastLastItem_Inverse() {
+        MotionEvent event = createEvent(10, 10);
+        assertFalse(RecyclerViewDelegate.isPastLastItem(
+                TOP_BORDER, LEFT_BORDER, RIGHT_BORDER, event, View.LAYOUT_DIRECTION_LTR));
+    }
+
+    @Test
+    public void testRtlPastLastItem() {
+        MotionEvent event = createEvent(10, 30);
+        assertTrue(RecyclerViewDelegate.isPastLastItem(
+                TOP_BORDER, LEFT_BORDER, RIGHT_BORDER, event, View.LAYOUT_DIRECTION_RTL));
+    }
+
+    @Test
+    public void testRtlPastLastItem_Inverse() {
+        MotionEvent event = createEvent(100, 100);
+        assertFalse(RecyclerViewDelegate.isPastLastItem(
+                TOP_BORDER, LEFT_BORDER, RIGHT_BORDER, event, View.LAYOUT_DIRECTION_RTL));
+    }
+
+    private static MotionEvent createEvent(int x, int y) {
+        return TestEvents.builder().action(MotionEvent.ACTION_MOVE).location(x, y).build();
+    }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/GridModelTest.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/GridModelTest.java
new file mode 100644
index 0000000..8924c52
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/GridModelTest.java
@@ -0,0 +1,467 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import static androidx.recyclerview.selection.GridModel.NOT_SET;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v7.widget.RecyclerView.OnScrollListener;
+
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+import androidx.recyclerview.selection.testing.TestAdapter;
+import androidx.recyclerview.selection.testing.TestItemKeyProvider;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class GridModelTest {
+
+    private static final int VIEW_PADDING_PX = 5;
+    private static final int CHILD_VIEW_EDGE_PX = 100;
+    private static final int VIEWPORT_HEIGHT = 500;
+
+    private GridModel<String> mModel;
+    private TestHost mHost;
+    private TestAdapter mAdapter;
+    private Set<String> mLastSelection;
+    private int mViewWidth;
+
+    // TLDR: Don't call model.{start|resize}Selection; use the local #startSelection and
+    // #resizeSelection methods instead.
+    //
+    // The reason for this is that selection is stateful and involves operations that take the
+    // current UI state (e.g scrolling) into account. This test maintains its own copy of the
+    // selection bounds as control data for verifying selections. Keep this data in sync by calling
+    // #startSelection and
+    // #resizeSelection.
+    private Point mSelectionOrigin;
+    private Point mSelectionPoint;
+
+    @After
+    public void tearDown() {
+        mModel = null;
+        mHost = null;
+        mLastSelection = null;
+    }
+
+    @Test
+    public void testSelectionLeftOfItems() {
+        initData(20, 5);
+        startSelection(new Point(0, 10));
+        resizeSelection(new Point(1, 11));
+        assertNoSelection();
+        assertEquals(NOT_SET, mModel.getPositionNearestOrigin());
+    }
+
+    @Test
+    public void testSelectionRightOfItems() {
+        initData(20, 4);
+        startSelection(new Point(mViewWidth - 1, 10));
+        resizeSelection(new Point(mViewWidth - 2, 11));
+        assertNoSelection();
+        assertEquals(NOT_SET, mModel.getPositionNearestOrigin());
+    }
+
+    @Test
+    public void testSelectionAboveItems() {
+        initData(20, 4);
+        startSelection(new Point(10, 0));
+        resizeSelection(new Point(11, 1));
+        assertNoSelection();
+        assertEquals(NOT_SET, mModel.getPositionNearestOrigin());
+    }
+
+    @Test
+    public void testSelectionBelowItems() {
+        initData(5, 4);
+        startSelection(new Point(10, VIEWPORT_HEIGHT - 1));
+        resizeSelection(new Point(11, VIEWPORT_HEIGHT - 2));
+        assertNoSelection();
+        assertEquals(NOT_SET, mModel.getPositionNearestOrigin());
+    }
+
+    @Test
+    public void testVerticalSelectionBetweenItems() {
+        initData(20, 4);
+        startSelection(new Point(106, 0));
+        resizeSelection(new Point(107, 200));
+        assertNoSelection();
+        assertEquals(NOT_SET, mModel.getPositionNearestOrigin());
+    }
+
+    @Test
+    public void testHorizontalSelectionBetweenItems() {
+        initData(20, 4);
+        startSelection(new Point(0, 105));
+        resizeSelection(new Point(200, 106));
+        assertNoSelection();
+        assertEquals(NOT_SET, mModel.getPositionNearestOrigin());
+    }
+
+    @Test
+    public void testGrowingAndShrinkingSelection() {
+        initData(20, 4);
+        startSelection(new Point(0, 0));
+
+        resizeSelection(new Point(5, 5));
+        verifySelection();
+
+        resizeSelection(new Point(109, 109));
+        verifySelection();
+
+        resizeSelection(new Point(110, 109));
+        verifySelection();
+
+        resizeSelection(new Point(110, 110));
+        verifySelection();
+
+        resizeSelection(new Point(214, 214));
+        verifySelection();
+
+        resizeSelection(new Point(215, 214));
+        verifySelection();
+
+        resizeSelection(new Point(214, 214));
+        verifySelection();
+
+        resizeSelection(new Point(110, 110));
+        verifySelection();
+
+        resizeSelection(new Point(110, 109));
+        verifySelection();
+
+        resizeSelection(new Point(109, 109));
+        verifySelection();
+
+        resizeSelection(new Point(5, 5));
+        verifySelection();
+
+        resizeSelection(new Point(0, 0));
+        verifySelection();
+
+        assertEquals(NOT_SET, mModel.getPositionNearestOrigin());
+    }
+
+    @Test
+    public void testSelectionMovingAroundOrigin() {
+        initData(16, 4);
+
+        startSelection(new Point(210, 210));
+        resizeSelection(new Point(mViewWidth - 1, 0));
+        verifySelection();
+
+        resizeSelection(new Point(0, 0));
+        verifySelection();
+
+        resizeSelection(new Point(0, 420));
+        verifySelection();
+
+        resizeSelection(new Point(mViewWidth - 1, 420));
+        verifySelection();
+
+        // This is manually figured and will need to be adjusted if the separator position is
+        // changed.
+        assertEquals(7, mModel.getPositionNearestOrigin());
+    }
+
+    @Test
+    public void testScrollingBandSelect() {
+        initData(40, 4);
+
+        startSelection(new Point(0, 0));
+        resizeSelection(new Point(100, VIEWPORT_HEIGHT - 1));
+        verifySelection();
+
+        scroll(CHILD_VIEW_EDGE_PX);
+        verifySelection();
+
+        resizeSelection(new Point(200, VIEWPORT_HEIGHT - 1));
+        verifySelection();
+
+        scroll(CHILD_VIEW_EDGE_PX);
+        verifySelection();
+
+        scroll(-2 * CHILD_VIEW_EDGE_PX);
+        verifySelection();
+
+        resizeSelection(new Point(100, VIEWPORT_HEIGHT - 1));
+        verifySelection();
+
+        assertEquals(0, mModel.getPositionNearestOrigin());
+    }
+
+    private void initData(final int numChildren, int numColumns) {
+        mHost = new TestHost(numChildren, numColumns);
+        mAdapter = new TestAdapter() {
+            @Override
+            public String getSelectionKey(int position) {
+                return Integer.toString(position);
+            }
+
+            @Override
+            public int getItemCount() {
+                return numChildren;
+            }
+        };
+
+        mViewWidth = VIEW_PADDING_PX + numColumns * (VIEW_PADDING_PX + CHILD_VIEW_EDGE_PX);
+
+        mModel = new GridModel<String>(
+                mHost,
+                new TestItemKeyProvider(ItemKeyProvider.SCOPE_MAPPED, mAdapter),
+                SelectionPredicates.<String>selectAnything());
+
+        mModel.addOnSelectionChangedListener(
+                new GridModel.SelectionObserver<String>() {
+                    @Override
+                    public void onSelectionChanged(Set<String> updatedSelection) {
+                        mLastSelection = updatedSelection;
+                    }
+                });
+    }
+
+    /** Returns the current selection area as a Rect. */
+    private Rect getSelectionArea() {
+        // Construct a rect from the two selection points.
+        Rect selectionArea = new Rect(
+                mSelectionOrigin.x, mSelectionOrigin.y, mSelectionOrigin.x, mSelectionOrigin.y);
+        selectionArea.union(mSelectionPoint.x, mSelectionPoint.y);
+        // Rect intersection tests are exclusive of bounds, while the MSM's selection code is
+        // inclusive. Expand the rect by 1 pixel in all directions to account for this.
+        selectionArea.inset(-1, -1);
+
+        return selectionArea;
+    }
+
+    /** Asserts that the selection is currently empty. */
+    private void assertNoSelection() {
+        assertEquals("Unexpected mItems " + mLastSelection + " in selection " + getSelectionArea(),
+                0, mLastSelection.size());
+    }
+
+    /** Verifies the selection using actual bbox checks. */
+    private void verifySelection() {
+        Rect selectionArea = getSelectionArea();
+        for (TestHost.Item item: mHost.mItems) {
+            if (Rect.intersects(selectionArea, item.rect)) {
+                assertTrue("Expected item " + item + " was not in selection " + selectionArea,
+                        mLastSelection.contains(item.name));
+            } else {
+                assertFalse("Unexpected item " + item + " in selection" + selectionArea,
+                        mLastSelection.contains(item.name));
+            }
+        }
+    }
+
+    private void startSelection(Point p) {
+        mModel.startCapturing(p);
+        mSelectionOrigin = mHost.createAbsolutePoint(p);
+    }
+
+    private void resizeSelection(Point p) {
+        mModel.resizeSelection(p);
+        mSelectionPoint = mHost.createAbsolutePoint(p);
+    }
+
+    private void scroll(int dy) {
+        assertTrue(mHost.verticalOffset + VIEWPORT_HEIGHT + dy <= mHost.getTotalHeight());
+        mHost.verticalOffset += dy;
+        // Correct the cached selection point as well.
+        mSelectionPoint.y += dy;
+        mHost.mScrollListener.onScrolled(null, 0, dy);
+    }
+
+    private static final class TestHost extends GridModel.GridHost<String> {
+
+        private final int mNumColumns;
+        private final int mNumRows;
+        private final int mNumChildren;
+        private final int mSeparatorPosition;
+
+        public int horizontalOffset = 0;
+        public int verticalOffset = 0;
+        private List<Item> mItems = new ArrayList<>();
+
+        // Installed by GridModel on construction.
+        private @Nullable OnScrollListener mScrollListener;
+
+        TestHost(int numChildren, int numColumns) {
+            mNumChildren = numChildren;
+            mNumColumns = numColumns;
+            mSeparatorPosition = mNumColumns + 1;
+            mNumRows = setupGrid();
+        }
+
+        private int setupGrid() {
+            // Split the input set into folders and documents. Do this such that there is a
+            // partially-populated row in the middle of the grid, to test corner cases in layout
+            // code.
+            int y = VIEW_PADDING_PX;
+            int i = 0;
+            int numRows = 0;
+            while (i < mNumChildren) {
+                int top = y;
+                int height = CHILD_VIEW_EDGE_PX;
+                int width = CHILD_VIEW_EDGE_PX;
+                for (int j = 0; j < mNumColumns && i < mNumChildren; j++) {
+                    int left = VIEW_PADDING_PX + (j * (width + VIEW_PADDING_PX));
+                    mItems.add(new Item(
+                            Integer.toString(i),
+                            new Rect(
+                                    left,
+                                    top,
+                                    left + width - 1,
+                                    top + height - 1)));
+
+                    // Create a partially populated row at the separator position.
+                    if (++i == mSeparatorPosition) {
+                        break;
+                    }
+                }
+                y += height + VIEW_PADDING_PX;
+                numRows++;
+            }
+
+            return numRows;
+        }
+
+        private int getTotalHeight() {
+            return CHILD_VIEW_EDGE_PX * mNumRows + VIEW_PADDING_PX * (mNumRows + 1);
+        }
+
+        private int getFirstVisibleRowIndex() {
+            return verticalOffset / (CHILD_VIEW_EDGE_PX + VIEW_PADDING_PX);
+        }
+
+        private int getLastVisibleRowIndex() {
+            int lastVisibleRowUncapped =
+                    (VIEWPORT_HEIGHT + verticalOffset - 1) / (CHILD_VIEW_EDGE_PX + VIEW_PADDING_PX);
+            return Math.min(lastVisibleRowUncapped, mNumRows - 1);
+        }
+
+        private int getNumItemsInRow(int index) {
+            assertTrue(index >= 0 && index < mNumRows);
+            int mod = mSeparatorPosition % mNumColumns;
+            if (index == (mSeparatorPosition / mNumColumns)) {
+                // The row containing the separator may be incomplete
+                return mod > 0 ? mod : mNumColumns;
+            }
+            // Account for the partial separator row in the final row tally.
+            if (index == mNumRows - 1) {
+                // The last row may be incomplete
+                int finalRowCount = (mNumChildren - mod) % mNumColumns;
+                return finalRowCount > 0 ? finalRowCount : mNumColumns;
+            }
+
+            return mNumColumns;
+        }
+
+        @Override
+        public GridModel<String> createGridModel() {
+            throw new UnsupportedOperationException("Not implemented.");
+        }
+
+        @Override
+        public void addOnScrollListener(OnScrollListener listener) {
+            mScrollListener = listener;
+        }
+
+        @Override
+        public void removeOnScrollListener(OnScrollListener listener) {}
+
+        @Override
+        public Point createAbsolutePoint(Point relativePoint) {
+            return new Point(
+                    relativePoint.x + horizontalOffset, relativePoint.y + verticalOffset);
+        }
+
+        @Override
+        public int getVisibleChildCount() {
+            int childCount = 0;
+            for (int i = getFirstVisibleRowIndex(); i <= getLastVisibleRowIndex(); i++) {
+                childCount += getNumItemsInRow(i);
+            }
+            return childCount;
+        }
+
+        @Override
+        public int getAdapterPositionAt(int index) {
+            // Account for partial rows by actually tallying up the mItems in hidden rows.
+            int hiddenCount = 0;
+            for (int i = 0; i < getFirstVisibleRowIndex(); i++) {
+                hiddenCount += getNumItemsInRow(i);
+            }
+            return index + hiddenCount;
+        }
+
+        @Override
+        public Rect getAbsoluteRectForChildViewAt(int index) {
+            int adapterPosition = getAdapterPositionAt(index);
+            return mItems.get(adapterPosition).rect;
+        }
+
+        @Override
+        public int getColumnCount() {
+            return mNumColumns;
+        }
+
+        @Override
+        public void showBand(Rect rect) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public void hideBand() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public boolean hasView(int adapterPosition) {
+            return true;
+        }
+
+        public static final class Item {
+            public String name;
+            public Rect rect;
+
+            Item(String n, Rect r) {
+                name = n;
+                rect = r;
+            }
+
+            @Override
+            public String toString() {
+                return name + ": " + rect;
+            }
+        }
+    }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/MouseInputHandlerTest.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/MouseInputHandlerTest.java
new file mode 100644
index 0000000..b0e7276
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/MouseInputHandlerTest.java
@@ -0,0 +1,301 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import static androidx.recyclerview.selection.testing.TestEvents.Mouse.ALT_CLICK;
+import static androidx.recyclerview.selection.testing.TestEvents.Mouse.CLICK;
+import static androidx.recyclerview.selection.testing.TestEvents.Mouse.CTRL_CLICK;
+import static androidx.recyclerview.selection.testing.TestEvents.Mouse.SECONDARY_CLICK;
+import static androidx.recyclerview.selection.testing.TestEvents.Mouse.SHIFT_CLICK;
+import static androidx.recyclerview.selection.testing.TestEvents.Mouse.TERTIARY_CLICK;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v7.widget.RecyclerView;
+import android.view.MotionEvent;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+import androidx.recyclerview.selection.testing.SelectionHelpers;
+import androidx.recyclerview.selection.testing.SelectionProbe;
+import androidx.recyclerview.selection.testing.TestActivationCallbacks;
+import androidx.recyclerview.selection.testing.TestAdapter;
+import androidx.recyclerview.selection.testing.TestData;
+import androidx.recyclerview.selection.testing.TestEvents;
+import androidx.recyclerview.selection.testing.TestFocusCallbacks;
+import androidx.recyclerview.selection.testing.TestItemDetails;
+import androidx.recyclerview.selection.testing.TestItemDetailsLookup;
+import androidx.recyclerview.selection.testing.TestItemKeyProvider;
+import androidx.recyclerview.selection.testing.TestMouseCallbacks;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class MouseInputHandlerTest {
+
+    private static final List<String> ITEMS = TestData.createStringData(100);
+
+    private MouseInputHandler mInputDelegate;
+
+    private TestMouseCallbacks mMouseCallbacks;
+    private TestActivationCallbacks mActivationCallbacks;
+    private TestFocusCallbacks mFocusCallbacks;
+
+    private TestItemDetailsLookup mDetailsLookup;
+    private SelectionProbe mSelection;
+    private SelectionHelper mSelectionMgr;
+
+    private TestEvents.Builder mEvent;
+
+    @Before
+    public void setUp() {
+
+        mSelectionMgr = SelectionHelpers.createTestInstance(ITEMS);
+        mDetailsLookup = new TestItemDetailsLookup();
+        mSelection = new SelectionProbe(mSelectionMgr);
+
+        mMouseCallbacks = new TestMouseCallbacks();
+        mActivationCallbacks = new TestActivationCallbacks();
+        mFocusCallbacks = new TestFocusCallbacks();
+
+        mInputDelegate = new MouseInputHandler(
+                mSelectionMgr,
+                new TestItemKeyProvider(ItemKeyProvider.SCOPE_MAPPED, new TestAdapter(ITEMS)),
+                mDetailsLookup,
+                mMouseCallbacks,
+                mActivationCallbacks,
+                mFocusCallbacks);
+
+        mEvent = TestEvents.builder().mouse();
+        mDetailsLookup.initAt(RecyclerView.NO_POSITION);
+    }
+
+    @Test
+    public void testConfirmedClick_StartsSelection() {
+        mDetailsLookup.initAt(11).setInItemSelectRegion(true);
+        mInputDelegate.onSingleTapConfirmed(CLICK);
+
+        mSelection.assertSelection(11);
+    }
+
+    @Test
+    public void testClickOnSelectRegion_AddsToSelection() {
+        mDetailsLookup.initAt(11).setInItemSelectRegion(true);
+        mInputDelegate.onSingleTapConfirmed(CLICK);
+
+        mDetailsLookup.initAt(10).setInItemSelectRegion(true);
+        mInputDelegate.onSingleTapUp(CLICK);
+
+        mSelection.assertSelected(10, 11);
+    }
+
+    @Test
+    public void testClickOnIconOfSelectedItem_RemovesFromSelection() {
+        mDetailsLookup.initAt(8).setInItemSelectRegion(true);
+        mInputDelegate.onSingleTapConfirmed(CLICK);
+
+        mDetailsLookup.initAt(11);
+        mInputDelegate.onSingleTapUp(SHIFT_CLICK);
+        mSelection.assertSelected(8, 9, 10, 11);
+
+        mDetailsLookup.initAt(9);
+        mInputDelegate.onSingleTapUp(CLICK);
+        mSelection.assertSelected(8, 10, 11);
+    }
+
+    @Test
+    public void testRightClickDown_StartsContextMenu() {
+        mInputDelegate.onDown(SECONDARY_CLICK);
+
+        mMouseCallbacks.assertLastEvent(SECONDARY_CLICK);
+    }
+
+    @Test
+    public void testAltClickDown_StartsContextMenu() {
+        mInputDelegate.onDown(ALT_CLICK);
+
+        mMouseCallbacks.assertLastEvent(ALT_CLICK);
+    }
+
+    @Test
+    public void testScroll_shouldTrap() {
+        mDetailsLookup.initAt(0);
+        assertTrue(mInputDelegate.onScroll(
+                null,
+                mEvent.action(MotionEvent.ACTION_MOVE).primary().build(),
+                -1,
+                -1));
+    }
+
+    @Test
+    public void testScroll_NoTrapForTwoFinger() {
+        mDetailsLookup.initAt(0);
+        assertFalse(mInputDelegate.onScroll(
+                null,
+                mEvent.action(MotionEvent.ACTION_MOVE).build(),
+                -1,
+                -1));
+    }
+
+    @Test
+    public void testUnconfirmedCtrlClick_AddsToExistingSelection() {
+        mDetailsLookup.initAt(7).setInItemSelectRegion(true);
+        mInputDelegate.onSingleTapConfirmed(CLICK);
+
+        mDetailsLookup.initAt(11);
+        mInputDelegate.onSingleTapUp(CTRL_CLICK);
+
+        mSelection.assertSelection(7, 11);
+    }
+
+    @Test
+    public void testUnconfirmedShiftClick_ExtendsSelection() {
+        mDetailsLookup.initAt(7).setInItemSelectRegion(true);
+        mInputDelegate.onSingleTapConfirmed(CLICK);
+
+        mDetailsLookup.initAt(11);
+        mInputDelegate.onSingleTapUp(SHIFT_CLICK);
+
+        mSelection.assertSelection(7, 8, 9, 10, 11);
+    }
+
+    @Test
+    public void testConfirmedShiftClick_ExtendsSelectionFromFocus() {
+        TestItemDetails item = mDetailsLookup.initAt(7);
+        mFocusCallbacks.focusItem(item);
+
+        // There should be no selected item at this point, just focus on "7".
+        mDetailsLookup.initAt(11);
+        mInputDelegate.onSingleTapConfirmed(SHIFT_CLICK);
+        mSelection.assertSelection(7, 8, 9, 10, 11);
+    }
+
+    @Test
+    public void testUnconfirmedShiftClick_RotatesAroundOrigin() {
+        mDetailsLookup.initAt(7).setInItemSelectRegion(true);
+        mInputDelegate.onSingleTapConfirmed(CLICK);
+
+        mDetailsLookup.initAt(11);
+        mInputDelegate.onSingleTapUp(SHIFT_CLICK);
+        mSelection.assertSelection(7, 8, 9, 10, 11);
+
+        mDetailsLookup.initAt(5);
+        mInputDelegate.onSingleTapUp(SHIFT_CLICK);
+
+        mSelection.assertSelection(5, 6, 7);
+        mSelection.assertNotSelected(8, 9, 10, 11);
+    }
+
+    @Test
+    public void testUnconfirmedShiftCtrlClick_Combination() {
+        mDetailsLookup.initAt(7).setInItemSelectRegion(true);
+        mInputDelegate.onSingleTapConfirmed(CLICK);
+
+        mDetailsLookup.initAt(11);
+        mInputDelegate.onSingleTapUp(SHIFT_CLICK);
+        mSelection.assertSelection(7, 8, 9, 10, 11);
+
+        mDetailsLookup.initAt(5);
+        mInputDelegate.onSingleTapUp(CTRL_CLICK);
+
+        mSelection.assertSelection(5, 7, 8, 9, 10, 11);
+    }
+
+    @Test
+    public void testUnconfirmedShiftCtrlClick_ShiftTakesPriority() {
+        mDetailsLookup.initAt(7).setInItemSelectRegion(true);
+        mInputDelegate.onSingleTapConfirmed(CLICK);
+
+        mDetailsLookup.initAt(11);
+        mInputDelegate.onSingleTapUp(mEvent.ctrl().shift().build());
+
+        mSelection.assertSelection(7, 8, 9, 10, 11);
+    }
+
+    // TODO: Add testSpaceBar_Previews, but we need to set a system property
+    // to have a deterministic state.
+
+    @Test
+    public void testDoubleClick_Opens() {
+        TestItemDetails doc = mDetailsLookup.initAt(11);
+        mInputDelegate.onDoubleTap(CLICK);
+
+        mActivationCallbacks.assertActivated(doc);
+    }
+
+    @Test
+    public void testMiddleClick_DoesNothing() {
+        mDetailsLookup.initAt(11).setInItemSelectRegion(true);
+        mInputDelegate.onSingleTapConfirmed(TERTIARY_CLICK);
+
+        mSelection.assertNoSelection();
+    }
+
+    @Test
+    public void testClickOff_ClearsSelection() {
+        mDetailsLookup.initAt(11).setInItemSelectRegion(true);
+        mInputDelegate.onSingleTapConfirmed(CLICK);
+
+        mDetailsLookup.initAt(RecyclerView.NO_POSITION);
+        mInputDelegate.onSingleTapUp(CLICK);
+
+        mSelection.assertNoSelection();
+    }
+
+    @Test
+    public void testClick_Focuses() {
+        mDetailsLookup.initAt(11).setInItemSelectRegion(false);
+        mInputDelegate.onSingleTapConfirmed(CLICK);
+
+        mFocusCallbacks.assertHasFocus(true);
+        mFocusCallbacks.assertFocused("11");
+    }
+
+    @Test
+    public void testClickOff_ClearsFocus() {
+        mDetailsLookup.initAt(11).setInItemSelectRegion(false);
+        mInputDelegate.onSingleTapConfirmed(CLICK);
+        mFocusCallbacks.assertHasFocus(true);
+
+        mDetailsLookup.initAt(RecyclerView.NO_POSITION);
+        mInputDelegate.onSingleTapUp(CLICK);
+        mFocusCallbacks.assertHasFocus(false);
+    }
+
+    @Test
+    public void testClickOffSelection_RemovesSelectionAndFocuses() {
+        mDetailsLookup.initAt(1).setInItemSelectRegion(true);
+        mInputDelegate.onSingleTapConfirmed(CLICK);
+
+        mDetailsLookup.initAt(5);
+        mInputDelegate.onSingleTapUp(SHIFT_CLICK);
+
+        mSelection.assertSelection(1, 2, 3, 4, 5);
+
+        mDetailsLookup.initAt(11);
+        mInputDelegate.onSingleTapUp(CLICK);
+
+        mFocusCallbacks.assertFocused("11");
+        mSelection.assertNoSelection();
+    }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/MouseInputHandler_RangeTest.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/MouseInputHandler_RangeTest.java
new file mode 100644
index 0000000..3295ea0
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/MouseInputHandler_RangeTest.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import static androidx.recyclerview.selection.testing.TestEvents.Mouse.CLICK;
+import static androidx.recyclerview.selection.testing.TestEvents.Mouse.SECONDARY_CLICK;
+import static androidx.recyclerview.selection.testing.TestEvents.Mouse.SHIFT_CLICK;
+
+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 java.util.List;
+
+import androidx.recyclerview.selection.testing.SelectionHelpers;
+import androidx.recyclerview.selection.testing.SelectionProbe;
+import androidx.recyclerview.selection.testing.TestActivationCallbacks;
+import androidx.recyclerview.selection.testing.TestAdapter;
+import androidx.recyclerview.selection.testing.TestData;
+import androidx.recyclerview.selection.testing.TestFocusCallbacks;
+import androidx.recyclerview.selection.testing.TestItemDetails;
+import androidx.recyclerview.selection.testing.TestItemDetailsLookup;
+import androidx.recyclerview.selection.testing.TestItemKeyProvider;
+import androidx.recyclerview.selection.testing.TestMouseCallbacks;
+
+/**
+ * MouseInputDelegate / SelectHelper integration test covering the shared
+ * responsibility of range selection.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class MouseInputHandler_RangeTest {
+
+    private static final List<String> ITEMS = TestData.createStringData(100);
+
+    private MouseInputHandler<String> mInputDelegate;
+    private SelectionProbe mSelection;
+    private TestFocusCallbacks<String> mFocusCallbacks;
+    private TestItemDetailsLookup mDetailsLookup;
+
+    @Before
+    public void setUp() {
+        SelectionHelper<String> selectionMgr = SelectionHelpers.createTestInstance(ITEMS);
+        TestMouseCallbacks mouseCallbacks = new TestMouseCallbacks();
+        TestActivationCallbacks<String> activationCallbacks = new TestActivationCallbacks<>();
+
+        mDetailsLookup = new TestItemDetailsLookup();
+        mSelection = new SelectionProbe(selectionMgr);
+        mFocusCallbacks = new TestFocusCallbacks<>();
+
+        mInputDelegate = new MouseInputHandler<>(
+                selectionMgr,
+                new TestItemKeyProvider(ItemKeyProvider.SCOPE_MAPPED, new TestAdapter(ITEMS)),
+                mDetailsLookup,
+                mouseCallbacks,
+                activationCallbacks,
+                mFocusCallbacks);
+    }
+
+    @Test
+    public void testExtendRange() {
+        // uni-click just focuses.
+        mDetailsLookup.initAt(7).setInItemSelectRegion(true);
+        mInputDelegate.onSingleTapConfirmed(CLICK);
+
+        mDetailsLookup.initAt(11);
+        mInputDelegate.onSingleTapUp(SHIFT_CLICK);
+
+        mSelection.assertRangeSelection(7, 11);
+    }
+
+    @Test
+    public void testExtendRangeContinues() {
+        mDetailsLookup.initAt(7).setInItemSelectRegion(true);
+        mInputDelegate.onSingleTapConfirmed(CLICK);
+
+        mDetailsLookup.initAt(11);
+        mInputDelegate.onSingleTapUp(SHIFT_CLICK);
+
+        mDetailsLookup.initAt(21);
+        mInputDelegate.onSingleTapUp(SHIFT_CLICK);
+
+        mSelection.assertRangeSelection(7, 21);
+    }
+
+    @Test
+    public void testMultipleContiguousRanges() {
+        mDetailsLookup.initAt(7).setInItemSelectRegion(true);
+        mInputDelegate.onSingleTapConfirmed(CLICK);
+
+        mDetailsLookup.initAt(11);
+        mInputDelegate.onSingleTapUp(SHIFT_CLICK);
+
+        // click without shift sets a new range start point.
+        TestItemDetails item = mDetailsLookup.initAt(20);
+        mInputDelegate.onSingleTapUp(CLICK);
+        mInputDelegate.onSingleTapConfirmed(CLICK);
+
+        mFocusCallbacks.focusItem(item);
+
+        mDetailsLookup.initAt(25);
+        mInputDelegate.onSingleTapUp(SHIFT_CLICK);
+        mInputDelegate.onSingleTapConfirmed(SHIFT_CLICK);
+
+        mSelection.assertRangeNotSelected(7, 11);
+        mSelection.assertRangeSelected(20, 25);
+        mSelection.assertSelectionSize(6);
+    }
+
+    @Test
+    public void testReducesSelectionRange() {
+        mDetailsLookup.initAt(7).setInItemSelectRegion(true);
+        mInputDelegate.onSingleTapConfirmed(CLICK);
+
+        mDetailsLookup.initAt(17);
+        mInputDelegate.onSingleTapUp(SHIFT_CLICK);
+
+        mDetailsLookup.initAt(10);
+        mInputDelegate.onSingleTapUp(SHIFT_CLICK);
+
+        mSelection.assertRangeSelection(7, 10);
+    }
+
+    @Test
+    public void testReducesSelectionRange_Reverse() {
+        mDetailsLookup.initAt(17).setInItemSelectRegion(true);
+        mInputDelegate.onSingleTapConfirmed(CLICK);
+
+        mDetailsLookup.initAt(7);
+        mInputDelegate.onSingleTapUp(SHIFT_CLICK);
+
+        mDetailsLookup.initAt(14);
+        mInputDelegate.onSingleTapUp(SHIFT_CLICK);
+
+        mSelection.assertRangeSelection(14, 17);
+    }
+
+    @Test
+    public void testExtendsRange_Reverse() {
+        mDetailsLookup.initAt(12).setInItemSelectRegion(true);
+        mInputDelegate.onSingleTapConfirmed(CLICK);
+
+        mDetailsLookup.initAt(5);
+        mInputDelegate.onSingleTapUp(SHIFT_CLICK);
+
+        mSelection.assertRangeSelection(5, 12);
+    }
+
+    @Test
+    public void testExtendsRange_ReversesAfterForwardClick() {
+        mDetailsLookup.initAt(7).setInItemSelectRegion(true);
+        mInputDelegate.onSingleTapConfirmed(CLICK);
+
+        mDetailsLookup.initAt(11);
+        mInputDelegate.onSingleTapUp(SHIFT_CLICK);
+
+        mDetailsLookup.initAt(0);
+        mInputDelegate.onSingleTapUp(SHIFT_CLICK);
+
+        mSelection.assertRangeSelection(0, 7);
+    }
+
+    @Test
+    public void testRightClickEstablishesRange() {
+
+        mDetailsLookup.initAt(7).setInItemSelectRegion(true);
+        mInputDelegate.onDown(SECONDARY_CLICK);
+        // This next method call simulates the behavior of the system event dispatch code.
+        // UserInputHandler depends on a specific sequence of events for internal
+        // state to remain valid. It's not an awesome arrangement, but it is currently
+        // necessary.
+        //
+        // See: UserInputHandler.MouseDelegate#mHandledOnDown;
+        mInputDelegate.onSingleTapUp(SECONDARY_CLICK);
+
+        mDetailsLookup.initAt(11);
+        // Now we can send a subsequent event that should extend selection.
+        mInputDelegate.onDown(SHIFT_CLICK);
+        mInputDelegate.onSingleTapUp(SHIFT_CLICK);
+
+        mSelection.assertRangeSelection(7, 11);
+    }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/RangeTest.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/RangeTest.java
new file mode 100644
index 0000000..0daeb41
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/RangeTest.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import static junit.framework.Assert.assertEquals;
+
+import static androidx.recyclerview.selection.Range.TYPE_PRIMARY;
+import static androidx.recyclerview.selection.Range.TYPE_PROVISIONAL;
+
+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 java.util.List;
+import java.util.Stack;
+
+import androidx.recyclerview.selection.testing.TestData;
+
+/**
+ * MouseInputDelegate / SelectHelper integration test covering the shared
+ * responsibility of range selection.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class RangeTest {
+
+    private static final List<String> ITEMS = TestData.createStringData(100);
+
+    private RangeSpy mSpy;
+    private Stack<Capture> mOperations;
+    private Range mRange;
+
+    @Before
+    public void setUp() {
+        mOperations = new Stack<>();
+        mSpy = new RangeSpy(mOperations);
+    }
+
+    @Test
+    public void testEstablishRange() {
+        mRange = new Range(0, mSpy);
+        mRange.extendRange(5, TYPE_PRIMARY);
+
+        // Origin is expected to have already been selected.
+        mOperations.pop().assertChanged(1, 5, true);
+    }
+
+    @Test
+    public void testExpandRange() {
+        mRange = new Range(0, mSpy);
+        mRange.extendRange(5, TYPE_PRIMARY);
+        mRange.extendRange(10, TYPE_PRIMARY);
+
+        mOperations.pop().assertChanged(6, 10, true);
+    }
+
+    @Test
+    public void testContractRange() {
+        mRange = new Range(0, mSpy);
+        mRange.extendRange(10, TYPE_PRIMARY);
+        mRange.extendRange(5, TYPE_PRIMARY);
+        mOperations.pop().assertChanged(6, 10, false);
+    }
+
+
+    @Test
+    public void testFlipRange_InitiallyDescending() {
+        mRange = new Range(10, mSpy);
+        mRange.extendRange(20, TYPE_PRIMARY);
+        mRange.extendRange(5, TYPE_PRIMARY);
+
+        // When a revision results in a flip two changes
+        // are sent to the callback. 1 to unselect the old items
+        // and one to select the new items.
+        mOperations.pop().assertChanged(5, 9, true);
+        // note that range never modifies the anchor.
+        mOperations.pop().assertChanged(11, 20, false);
+    }
+
+    @Test
+    public void testFlipRange_InitiallyAscending() {
+        mRange = new Range(10, mSpy);
+        mRange.extendRange(5, TYPE_PRIMARY);
+        mRange.extendRange(20, TYPE_PRIMARY);
+
+        // When a revision results in a flip two changes
+        // are sent to the callback. 1 to unselect the old items
+        // and one to select the new items.
+        mOperations.pop().assertChanged(11, 20, true);
+        // note that range never modifies the anchor.
+        mOperations.pop().assertChanged(5, 9, false);
+    }
+
+    // NOTE: The operation type is conveyed among methods, then
+    // returned to the caller. It's more of something we coury
+    // for the caller. So we won't verify courying the value
+    // with all behaviors. Just this once.
+    @Test
+    public void testCouriesRangeType() {
+        mRange = new Range(0, mSpy);
+
+        mRange.extendRange(5, TYPE_PRIMARY);
+        mOperations.pop().assertType(TYPE_PRIMARY);
+
+        mRange.extendRange(10, TYPE_PROVISIONAL);
+        mOperations.pop().assertType(TYPE_PROVISIONAL);
+    }
+
+    private static class Capture {
+
+        private int mBegin;
+        private int mEnd;
+        private boolean mSelected;
+        private int mType;
+
+        private Capture(int begin, int end, boolean selected, int type) {
+            mBegin = begin;
+            mEnd = end;
+            mSelected = selected;
+            mType = type;
+        }
+
+        private void assertType(int expected) {
+            assertEquals(expected, mType);
+        }
+
+        private void assertChanged(int begin, int end, boolean selected) {
+            assertEquals(begin, mBegin);
+            assertEquals(end, mEnd);
+            assertEquals(selected, mSelected);
+        }
+    }
+
+    private static final class RangeSpy extends Range.Callbacks {
+
+        private final Stack<Capture> mOperations;
+
+        RangeSpy(Stack<Capture> operations) {
+            mOperations = operations;
+        }
+
+        @Override
+        void updateForRange(int begin, int end, boolean selected, int type) {
+            mOperations.push(new Capture(begin, end, selected, type));
+        }
+
+        Capture popOp() {
+            return mOperations.pop();
+        }
+    }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/SelectionStorage_LongsTest.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/SelectionStorage_LongsTest.java
new file mode 100644
index 0000000..5535935
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/SelectionStorage_LongsTest.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import static junit.framework.Assert.assertTrue;
+
+import static org.junit.Assert.assertEquals;
+
+import android.os.Bundle;
+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 androidx.recyclerview.selection.testing.Bundles;
+import androidx.recyclerview.selection.testing.SelectionHelpers;
+import androidx.recyclerview.selection.testing.TestData;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class SelectionStorage_LongsTest {
+
+    private SelectionHelper<Long> mSelectionHelper;
+    private SelectionStorage<Long> mSelectionStorage;
+    private Bundle mBundle;
+
+    @Before
+    public void setUp() {
+        mSelectionHelper = SelectionHelpers.createTestInstance(TestData.createLongData(100));
+        mSelectionStorage = new SelectionStorage<>(
+                SelectionStorage.TYPE_LONG, mSelectionHelper);
+        mBundle = new Bundle();
+    }
+
+    @Test
+    public void testWritesSelectionToBundle() {
+        mSelectionHelper.select(3L);
+        mSelectionStorage.onSaveInstanceState(mBundle);
+        Bundle out = Bundles.forceParceling(mBundle);
+        assertTrue(out.containsKey(SelectionStorage.EXTRA_SAVED_SELECTION_TYPE));
+        assertTrue(out.containsKey(SelectionStorage.EXTRA_SAVED_SELECTION_ENTRIES));
+    }
+
+    @Test
+    public void testRestoresFromSelectionInBundle() {
+        mSelectionHelper.select(3L);
+        mSelectionHelper.select(13L);
+        mSelectionHelper.select(33L);
+
+        MutableSelection orig = new MutableSelection();
+        mSelectionHelper.copySelection(orig);
+        mSelectionStorage.onSaveInstanceState(mBundle);
+        Bundle out = Bundles.forceParceling(mBundle);
+
+        mSelectionHelper.clearSelection();
+        mSelectionStorage.onRestoreInstanceState(out);
+        MutableSelection restored = new MutableSelection();
+        mSelectionHelper.copySelection(restored);
+        assertEquals(orig, restored);
+    }
+
+    @Test
+    public void testIgnoresNullBundle() {
+        mSelectionStorage.onRestoreInstanceState(null);
+    }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/SelectionStorage_StringsTest.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/SelectionStorage_StringsTest.java
new file mode 100644
index 0000000..9442be9
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/SelectionStorage_StringsTest.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import static junit.framework.Assert.assertTrue;
+
+import static org.junit.Assert.assertEquals;
+
+import android.os.Bundle;
+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 androidx.recyclerview.selection.testing.Bundles;
+import androidx.recyclerview.selection.testing.SelectionHelpers;
+import androidx.recyclerview.selection.testing.TestData;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class SelectionStorage_StringsTest {
+
+    private SelectionHelper<String> mSelectionHelper;
+    private SelectionStorage<String> mSelectionStorage;
+    private Bundle mBundle;
+
+    @Before
+    public void setUp() {
+        mSelectionHelper = SelectionHelpers.createTestInstance(TestData.createStringData(100));
+        mSelectionStorage = new SelectionStorage<>(
+                SelectionStorage.TYPE_STRING, mSelectionHelper);
+        mBundle = new Bundle();
+    }
+
+    @Test
+    public void testWritesSelectionToBundle() {
+        mSelectionHelper.select("3");
+        mSelectionStorage.onSaveInstanceState(mBundle);
+        Bundle out = Bundles.forceParceling(mBundle);
+
+        assertTrue(mBundle.containsKey(SelectionStorage.EXTRA_SAVED_SELECTION_ENTRIES));
+    }
+
+    @Test
+    public void testRestoresFromSelectionInBundle() {
+        mSelectionHelper.select("3");
+        mSelectionHelper.select("13");
+        mSelectionHelper.select("33");
+
+        MutableSelection orig = new MutableSelection();
+        mSelectionHelper.copySelection(orig);
+        mSelectionStorage.onSaveInstanceState(mBundle);
+        Bundle out = Bundles.forceParceling(mBundle);
+
+        mSelectionHelper.clearSelection();
+
+        mSelectionStorage.onRestoreInstanceState(mBundle);
+        MutableSelection restored = new MutableSelection();
+        mSelectionHelper.copySelection(restored);
+        assertEquals(orig, restored);
+    }
+
+    @Test
+    public void testIgnoresNullBundle() {
+        mSelectionStorage.onRestoreInstanceState(null);
+    }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/SelectionTest.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/SelectionTest.java
new file mode 100644
index 0000000..64bf01d
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/SelectionTest.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+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 java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class SelectionTest {
+
+    private final String[] mIds = new String[] {
+            "foo",
+            "43",
+            "auth|id=@53di*/f3#d"
+    };
+
+    private Selection mSelection;
+
+    @Before
+    public void setUp() throws Exception {
+        mSelection = new Selection();
+        mSelection.add(mIds[0]);
+        mSelection.add(mIds[1]);
+        mSelection.add(mIds[2]);
+    }
+
+    @Test
+    public void testAdd() {
+        // We added in setUp.
+        assertEquals(3, mSelection.size());
+        assertContains(mIds[0]);
+        assertContains(mIds[1]);
+        assertContains(mIds[2]);
+    }
+
+    @Test
+    public void testRemove() {
+        mSelection.remove(mIds[0]);
+        mSelection.remove(mIds[2]);
+        assertEquals(1, mSelection.size());
+        assertContains(mIds[1]);
+    }
+
+    @Test
+    public void testClear() {
+        mSelection.clear();
+        assertEquals(0, mSelection.size());
+    }
+
+    @Test
+    public void testIsEmpty() {
+        assertTrue(new Selection().isEmpty());
+        mSelection.clear();
+        assertTrue(mSelection.isEmpty());
+    }
+
+    @Test
+    public void testSize() {
+        Selection other = new Selection();
+        for (int i = 0; i < mSelection.size(); i++) {
+            other.add(mIds[i]);
+        }
+        assertEquals(mSelection.size(), other.size());
+    }
+
+    @Test
+    public void testEqualsSelf() {
+        assertEquals(mSelection, mSelection);
+    }
+
+    @Test
+    public void testEqualsOther() {
+        Selection other = new Selection();
+        other.add(mIds[0]);
+        other.add(mIds[1]);
+        other.add(mIds[2]);
+        assertEquals(mSelection, other);
+        assertEquals(mSelection.hashCode(), other.hashCode());
+    }
+
+    @Test
+    public void testEqualsCopy() {
+        Selection other = new Selection();
+        other.copyFrom(mSelection);
+        assertEquals(mSelection, other);
+        assertEquals(mSelection.hashCode(), other.hashCode());
+    }
+
+    @Test
+    public void testNotEquals() {
+        Selection other = new Selection();
+        other.add("foobar");
+        assertFalse(mSelection.equals(other));
+    }
+
+    private void assertContains(String id) {
+        String err = String.format("Selection %s does not contain %s", mSelection, id);
+        assertTrue(err, mSelection.contains(id));
+    }
+
+    public static <E> Set<E> newSet(E... elements) {
+        HashSet<E> set = new HashSet<>(elements.length);
+        Collections.addAll(set, elements);
+        return set;
+    }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/TouchInputHandlerTest.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/TouchInputHandlerTest.java
new file mode 100644
index 0000000..476684a
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/TouchInputHandlerTest.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import static org.junit.Assert.assertFalse;
+
+import static androidx.recyclerview.selection.testing.TestEvents.Touch.TAP;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v7.widget.RecyclerView;
+import android.view.MotionEvent;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
+import androidx.recyclerview.selection.testing.SelectionHelpers;
+import androidx.recyclerview.selection.testing.SelectionProbe;
+import androidx.recyclerview.selection.testing.TestActivationCallbacks;
+import androidx.recyclerview.selection.testing.TestAdapter;
+import androidx.recyclerview.selection.testing.TestData;
+import androidx.recyclerview.selection.testing.TestFocusCallbacks;
+import androidx.recyclerview.selection.testing.TestItemDetailsLookup;
+import androidx.recyclerview.selection.testing.TestItemKeyProvider;
+import androidx.recyclerview.selection.testing.TestRunnable;
+import androidx.recyclerview.selection.testing.TestSelectionPredicate;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class TouchInputHandlerTest {
+
+    private static final List<String> ITEMS = TestData.createStringData(100);
+
+    private TouchInputHandler mInputDelegate;
+    private SelectionHelper mSelectionMgr;
+    private TestSelectionPredicate mSelectionPredicate;
+    private TestRunnable mGestureStarted;
+    private TestRunnable mHapticPerformer;
+    private TestTouchCallbacks mMouseCallbacks;
+    private TestActivationCallbacks mActivationCallbacks;
+    private TestFocusCallbacks mFocusCallbacks;
+    private TestItemDetailsLookup mDetailsLookup;
+    private SelectionProbe mSelection;
+
+    @Before
+    public void setUp() {
+        mSelectionMgr = SelectionHelpers.createTestInstance(ITEMS);
+        mDetailsLookup = new TestItemDetailsLookup();
+        mSelectionPredicate = new TestSelectionPredicate();
+        mSelection = new SelectionProbe(mSelectionMgr);
+        mGestureStarted = new TestRunnable();
+        mHapticPerformer = new TestRunnable();
+        mMouseCallbacks = new TestTouchCallbacks();
+        mActivationCallbacks = new TestActivationCallbacks();
+        mFocusCallbacks = new TestFocusCallbacks();
+
+        mInputDelegate = new TouchInputHandler(
+                mSelectionMgr,
+                new TestItemKeyProvider(ItemKeyProvider.SCOPE_MAPPED, new TestAdapter(ITEMS)),
+                mDetailsLookup,
+                mSelectionPredicate,
+                mGestureStarted,
+                mMouseCallbacks,
+                mActivationCallbacks,
+                mFocusCallbacks,
+                mHapticPerformer);
+    }
+
+    @Test
+    public void testTap_ActivatesWhenNoExistingSelection() {
+        ItemDetails doc = mDetailsLookup.initAt(11);
+        mInputDelegate.onSingleTapUp(TAP);
+
+        mActivationCallbacks.assertActivated(doc);
+    }
+
+    @Test
+    public void testScroll_shouldNotBeTrapped() {
+        assertFalse(mInputDelegate.onScroll(null, TAP, -1, -1));
+    }
+
+    @Test
+    public void testLongPress_SelectsItem() {
+        mSelectionPredicate.setReturnValue(true);
+
+        mDetailsLookup.initAt(7);
+        mInputDelegate.onLongPress(TAP);
+
+        mSelection.assertSelection(7);
+    }
+
+    @Test
+    public void testLongPress_StartsGestureSelection() {
+        mSelectionPredicate.setReturnValue(true);
+
+        mDetailsLookup.initAt(7);
+        mInputDelegate.onLongPress(TAP);
+        mGestureStarted.assertRan();
+    }
+
+    @Test
+    public void testSelectHotspot_StartsSelectionMode() {
+        mSelectionPredicate.setReturnValue(true);
+
+        mDetailsLookup.initAt(7).setInItemSelectRegion(true);
+        mInputDelegate.onSingleTapUp(TAP);
+
+        mSelection.assertSelection(7);
+    }
+
+    @Test
+    public void testSelectionHotspot_UnselectsSelectedItem() {
+        mSelectionMgr.select("11");
+
+        mDetailsLookup.initAt(11).setInItemSelectRegion(true);
+        mInputDelegate.onSingleTapUp(TAP);
+
+        mSelection.assertNoSelection();
+    }
+
+    @Test
+    public void testStartsSelection_PerformsHapticFeedback() {
+        mSelectionPredicate.setReturnValue(true);
+
+        mDetailsLookup.initAt(7);
+        mInputDelegate.onLongPress(TAP);
+
+        mHapticPerformer.assertRan();
+    }
+
+    @Test
+    public void testLongPress_AddsToSelection() {
+        mSelectionPredicate.setReturnValue(true);
+
+        mDetailsLookup.initAt(7);
+        mInputDelegate.onLongPress(TAP);
+
+        mDetailsLookup.initAt(99);
+        mInputDelegate.onLongPress(TAP);
+
+        mDetailsLookup.initAt(13);
+        mInputDelegate.onLongPress(TAP);
+
+        mSelection.assertSelection(7, 13, 99);
+    }
+
+    @Test
+    public void testTap_UnselectsSelectedItem() {
+        mSelectionMgr.select("11");
+
+        mDetailsLookup.initAt(11);
+        mInputDelegate.onSingleTapUp(TAP);
+
+        mSelection.assertNoSelection();
+    }
+
+    @Test
+    public void testTapOff_ClearsSelection() {
+        mSelectionMgr.select("7");
+        mDetailsLookup.initAt(7);
+
+        mInputDelegate.onLongPress(TAP);
+
+        mSelectionMgr.select("11");
+        mDetailsLookup.initAt(11);
+        mInputDelegate.onSingleTapUp(TAP);
+
+        mDetailsLookup.initAt(RecyclerView.NO_POSITION).setInItemSelectRegion(false);
+        mInputDelegate.onSingleTapUp(TAP);
+
+        mSelection.assertNoSelection();
+    }
+
+    private static final class TestTouchCallbacks extends TouchCallbacks {
+        @Override
+        public boolean onDragInitiated(MotionEvent e) {
+            return false;
+        }
+    }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/ViewAutoScrollerTest.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/ViewAutoScrollerTest.java
new file mode 100644
index 0000000..66cb721
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/ViewAutoScrollerTest.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertNotNull;
+
+import static org.junit.Assert.assertNull;
+
+import android.graphics.Point;
+import android.support.annotation.Nullable;
+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 androidx.recyclerview.selection.ViewAutoScroller.ScrollHost;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class ViewAutoScrollerTest {
+
+    private static final float SCROLL_THRESHOLD_RATIO = 0.125f;
+    private static final int VIEW_HEIGHT = 100;
+    private static final int TOP_Y_POINT = (int) (VIEW_HEIGHT * SCROLL_THRESHOLD_RATIO) - 1;
+    private static final int BOTTOM_Y_POINT =
+            VIEW_HEIGHT - (int) (VIEW_HEIGHT * SCROLL_THRESHOLD_RATIO) + 1;
+
+    private ViewAutoScroller mScroller;
+    private TestHost mHost;
+
+    @Before
+    public void setUp() {
+        mHost = new TestHost();
+        mScroller = new ViewAutoScroller(mHost, SCROLL_THRESHOLD_RATIO);
+    }
+
+    @Test
+    public void testNoScrollWhenOutOfScrollZone() {
+        mScroller.scroll(new Point(0, VIEW_HEIGHT / 2));
+        mHost.run();
+        mHost.assertNotScrolled();
+    }
+
+//    @Test
+//    public void testNoScrollWhenDisabled() {
+//        mScroller.reset();
+//        mScroller.scroll(mEvent.location(0, TOP_Y_POINT).build());
+//        mHost.assertNotScrolled();
+//    }
+
+    @Test
+    public void testMotionThreshold() {
+        mScroller.scroll(new Point(0, TOP_Y_POINT));
+        mHost.run();
+
+        mScroller.scroll(new Point(0, TOP_Y_POINT - 1));
+        mHost.run();
+
+        mHost.assertNotScrolled();
+    }
+
+    @Test
+    public void testMotionThreshold_Resets() {
+        int expectedScrollDistance = mScroller.computeScrollDistance(-21);
+        mScroller.scroll(new Point(0, TOP_Y_POINT));
+        mHost.run();
+        // We need enough y motion to overcome motion threshold
+        mScroller.scroll(new Point(0, TOP_Y_POINT - 20));
+        mHost.run();
+
+        mHost.reset();
+        // After resetting events should be required to cross the motion threshold
+        // before auto-scrolling again.
+        mScroller.reset();
+
+        mScroller.scroll(new Point(0, TOP_Y_POINT));
+        mHost.run();
+
+        mHost.assertNotScrolled();
+    }
+
+    @Test
+    public void testAutoScrolls_Top() {
+        int expectedScrollDistance = mScroller.computeScrollDistance(-21);
+        mScroller.scroll(new Point(0, TOP_Y_POINT));
+        mHost.run();
+        // We need enough y motion to overcome motion threshold
+        mScroller.scroll(new Point(0, TOP_Y_POINT - 20));
+        mHost.run();
+
+        mHost.assertScrolledBy(expectedScrollDistance);
+    }
+
+    @Test
+    public void testAutoScrolls_Bottom() {
+        int expectedScrollDistance = mScroller.computeScrollDistance(21);
+        mScroller.scroll(new Point(0, BOTTOM_Y_POINT));
+        mHost.run();
+        // We need enough y motion to overcome motion threshold
+        mScroller.scroll(new Point(0, BOTTOM_Y_POINT + 20));
+        mHost.run();
+
+        mHost.assertScrolledBy(expectedScrollDistance);
+    }
+
+    private final class TestHost extends ScrollHost {
+
+        private @Nullable Integer mScrollDistance;
+        private @Nullable Runnable mRunnable;
+
+        @Override
+        int getViewHeight() {
+            return VIEW_HEIGHT;
+        }
+
+        @Override
+        void scrollBy(int distance) {
+            mScrollDistance = distance;
+        }
+
+        @Override
+        void runAtNextFrame(Runnable r) {
+            mRunnable = r;
+        }
+
+        @Override
+        void removeCallback(Runnable r) {
+        }
+
+        private void reset() {
+            mScrollDistance = null;
+            mRunnable = null;
+        }
+
+        private void run() {
+            mRunnable.run();
+        }
+
+        private void assertNotScrolled() {
+            assertNull(mScrollDistance);
+        }
+
+        private void assertScrolledBy(int expectedDistance) {
+            assertNotNull(mScrollDistance);
+            assertEquals(expectedDistance, mScrollDistance.intValue());
+        }
+    }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/Bundles.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/Bundles.java
new file mode 100644
index 0000000..6ceba34
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/Bundles.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection.testing;
+
+import android.os.Bundle;
+import android.os.Parcel;
+
+public final class Bundles {
+
+    private Bundles() {
+    }
+
+    public static Bundle forceParceling(Bundle in) {
+        Parcel parcel = Parcel.obtain();
+        in.writeToParcel(parcel, 0);
+
+        parcel.setDataPosition(0);
+        return parcel.readBundle();
+    }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/SelectionHelpers.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/SelectionHelpers.java
new file mode 100644
index 0000000..9a031a9
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/SelectionHelpers.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection.testing;
+
+import java.util.List;
+
+import androidx.recyclerview.selection.DefaultSelectionHelper;
+import androidx.recyclerview.selection.EventBridge;
+import androidx.recyclerview.selection.ItemKeyProvider;
+import androidx.recyclerview.selection.SelectionHelper;
+import androidx.recyclerview.selection.SelectionPredicates;
+
+public final class SelectionHelpers {
+
+    private SelectionHelpers() {}
+
+    public static <K> SelectionHelper<K> createTestInstance(List<K> items) {
+        TestAdapter<K> adapter = new TestAdapter<>(items);
+        ItemKeyProvider<K> keyProvider =
+                new TestItemKeyProvider<>(ItemKeyProvider.SCOPE_MAPPED, adapter);
+        SelectionHelper<K> helper = new DefaultSelectionHelper<>(
+                keyProvider,
+                SelectionPredicates.selectAnything());
+
+        EventBridge.install(adapter, helper, keyProvider);
+
+        return helper;
+    }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/SelectionProbe.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/SelectionProbe.java
new file mode 100644
index 0000000..4501078
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/SelectionProbe.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection.testing;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import androidx.recyclerview.selection.DefaultSelectionHelper;
+import androidx.recyclerview.selection.Selection;
+import androidx.recyclerview.selection.SelectionHelper;
+
+/**
+ * Helper class for making assertions against the state of a {@link DefaultSelectionHelper} instance
+ * and the consistency of states between {@link DefaultSelectionHelper} and
+ * {@link DefaultSelectionHelper.SelectionObserver}.
+ */
+public final class SelectionProbe {
+
+    private final SelectionHelper<String> mMgr;
+    private final TestSelectionObserver<String> mSelectionListener;
+
+    public SelectionProbe(SelectionHelper<String> mgr) {
+        mMgr = mgr;
+        mSelectionListener = new TestSelectionObserver<String>();
+        mMgr.addObserver(mSelectionListener);
+    }
+
+    public SelectionProbe(
+            SelectionHelper<String> mgr, TestSelectionObserver<String> selectionListener) {
+        mMgr = mgr;
+        mSelectionListener = selectionListener;
+    }
+
+    public void assertRangeSelected(int begin, int end) {
+        for (int i = begin; i <= end; i++) {
+            assertSelected(i);
+        }
+    }
+
+    public void assertRangeNotSelected(int begin, int end) {
+        for (int i = begin; i <= end; i++) {
+            assertNotSelected(i);
+        }
+    }
+
+    public void assertRangeSelection(int begin, int end) {
+        assertSelectionSize(end - begin + 1);
+        assertRangeSelected(begin, end);
+    }
+
+    public void assertSelectionSize(int expected) {
+        Selection selection = mMgr.getSelection();
+        assertEquals(selection.toString(), expected, selection.size());
+
+        mSelectionListener.assertSelectionSize(expected);
+    }
+
+    public void assertNoSelection() {
+        assertSelectionSize(0);
+
+        mSelectionListener.assertNoSelection();
+    }
+
+    public void assertSelection(int... ids) {
+        assertSelected(ids);
+        assertEquals(ids.length, mMgr.getSelection().size());
+
+        mSelectionListener.assertSelectionSize(ids.length);
+    }
+
+    public void assertSelected(int... ids) {
+        Selection<String> sel = mMgr.getSelection();
+        for (int id : ids) {
+            String sid = String.valueOf(id);
+            assertTrue(sid + " is not in selection " + sel, sel.contains(sid));
+
+            mSelectionListener.assertSelected(sid);
+        }
+    }
+
+    public void assertNotSelected(int... ids) {
+        Selection<String> sel = mMgr.getSelection();
+        for (int id : ids) {
+            String sid = String.valueOf(id);
+            assertFalse(sid + " is in selection " + sel, sel.contains(sid));
+
+            mSelectionListener.assertNotSelected(sid);
+        }
+    }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestActivationCallbacks.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestActivationCallbacks.java
new file mode 100644
index 0000000..b106a92
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestActivationCallbacks.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection.testing;
+
+import static org.junit.Assert.assertEquals;
+
+import android.view.MotionEvent;
+
+import androidx.recyclerview.selection.ActivationCallbacks;
+import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
+
+public final class TestActivationCallbacks<K> extends ActivationCallbacks<K> {
+
+    private ItemDetails<K> mActivated;
+
+    @Override
+    public boolean onItemActivated(ItemDetails<K> item, MotionEvent e) {
+        mActivated = item;
+        return true;
+    }
+
+    public void assertActivated(ItemDetails<K> expected) {
+        assertEquals(expected, mActivated);
+    }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestAdapter.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestAdapter.java
new file mode 100644
index 0000000..d9e952e
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestAdapter.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection.testing;
+
+import static org.junit.Assert.assertTrue;
+
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.Adapter;
+import android.support.v7.widget.RecyclerView.AdapterDataObserver;
+import android.view.ViewGroup;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import androidx.recyclerview.selection.SelectionHelper;
+
+public class TestAdapter<K> extends Adapter<TestHolder> {
+
+    private final List<K> mItems = new ArrayList<>();
+    private final List<Integer> mNotifiedOfSelection = new ArrayList<>();
+    private final AdapterDataObserver mAdapterObserver;
+
+    public TestAdapter() {
+        this(Collections.EMPTY_LIST);
+    }
+
+    public TestAdapter(List<K> items) {
+        mItems.addAll(items);
+        mAdapterObserver = new RecyclerView.AdapterDataObserver() {
+
+            @Override
+            public void onChanged() {
+            }
+
+            @Override
+            public void onItemRangeChanged(int startPosition, int itemCount, Object payload) {
+                if (SelectionHelper.SELECTION_CHANGED_MARKER.equals(payload)) {
+                    int last = startPosition + itemCount;
+                    for (int i = startPosition; i < last; i++) {
+                        mNotifiedOfSelection.add(i);
+                    }
+                }
+            }
+
+            @Override
+            public void onItemRangeInserted(int startPosition, int itemCount) {
+            }
+
+            @Override
+            public void onItemRangeRemoved(int startPosition, int itemCount) {
+            }
+
+            @Override
+            public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
+                throw new UnsupportedOperationException();
+            }
+        };
+
+        registerAdapterDataObserver(mAdapterObserver);
+    }
+
+    @Override
+    public TestHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+        return new TestHolder(parent);
+    }
+
+    @Override
+    public void onBindViewHolder(TestHolder holder, int position) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int getItemCount() {
+        return mItems.size();
+    }
+
+    public void updateTestModelIds(List<K> items) {
+        mItems.clear();
+        mItems.addAll(items);
+
+        notifyDataSetChanged();
+    }
+
+    public int getPosition(K key) {
+        return mItems.indexOf(key);
+    }
+
+    public K getSelectionKey(int position) {
+        return mItems.get(position);
+    }
+
+
+    public void resetSelectionNotifications() {
+        mNotifiedOfSelection.clear();
+    }
+
+    public void assertNotifiedOfSelectionChange(int position) {
+        assertTrue(mNotifiedOfSelection.contains(position));
+    }
+
+    public static List<String> createItemList(int num) {
+        List<String> items = new ArrayList<>(num);
+        for (int i = 0; i < num; ++i) {
+            items.add(Integer.toString(i));
+        }
+        return items;
+    }
+}
diff --git a/media-compat-test-lib/build.gradle b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestAutoScroller.java
similarity index 60%
copy from media-compat-test-lib/build.gradle
copy to recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestAutoScroller.java
index 26594e5..4232205 100644
--- a/media-compat-test-lib/build.gradle
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestAutoScroller.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2017 The Android Open Source Project
+ * Copyright 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.
@@ -14,4 +14,19 @@
  * limitations under the License.
  */
 
-apply plugin: 'java'
+package androidx.recyclerview.selection.testing;
+
+import android.graphics.Point;
+
+import androidx.recyclerview.selection.AutoScroller;
+
+public class TestAutoScroller extends AutoScroller {
+
+    @Override
+    protected void reset() {
+    }
+
+    @Override
+    protected void scroll(Point location) {
+    }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestBandPredicate.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestBandPredicate.java
new file mode 100644
index 0000000..ca21b9c
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestBandPredicate.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection.testing;
+
+import android.view.MotionEvent;
+
+import androidx.recyclerview.selection.BandPredicate;
+
+public class TestBandPredicate extends BandPredicate {
+
+    private boolean mCanInitiate = true;
+
+    public void setCanInitiate(boolean canInitiate) {
+        mCanInitiate = canInitiate;
+    }
+
+    @Override
+    public boolean canInitiate(MotionEvent e) {
+        return mCanInitiate;
+    }
+
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestData.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestData.java
new file mode 100644
index 0000000..14e27aa
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestData.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection.testing;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class TestData {
+
+    public static List<String> createStringData(int num) {
+        List<String> items = new ArrayList<>(num);
+        for (int i = 0; i < num; ++i) {
+            items.add(Integer.toString(i));
+        }
+        return items;
+    }
+
+    public static List<Long> createLongData(int num) {
+        List<Long> items = new ArrayList<>(num);
+        for (int i = 0; i < num; ++i) {
+            items.add(new Long(i));
+        }
+        return items;
+    }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestEvents.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestEvents.java
new file mode 100644
index 0000000..fd4bea4
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestEvents.java
@@ -0,0 +1,278 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection.testing;
+
+import android.graphics.Point;
+import android.support.annotation.IntDef;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.MotionEvent.PointerCoords;
+import android.view.MotionEvent.PointerProperties;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Handy-dandy wrapper class to facilitate the creation of MotionEvents.
+ */
+public final class TestEvents {
+
+    /**
+     * Common mouse event types...for your convenience.
+     */
+    public static final class Mouse {
+        public static final MotionEvent CLICK =
+                TestEvents.builder().mouse().primary().build();
+        public static final MotionEvent CTRL_CLICK =
+                TestEvents.builder().mouse().primary().ctrl().build();
+        public static final MotionEvent ALT_CLICK =
+                TestEvents.builder().mouse().primary().alt().build();
+        public static final MotionEvent SHIFT_CLICK =
+                TestEvents.builder().mouse().primary().shift().build();
+        public static final MotionEvent SECONDARY_CLICK =
+                TestEvents.builder().mouse().secondary().build();
+        public static final MotionEvent TERTIARY_CLICK =
+                TestEvents.builder().mouse().tertiary().build();
+    }
+
+    /**
+     * Common touch event types...for your convenience.
+     */
+    public static final class Touch {
+        public static final MotionEvent TAP =
+                TestEvents.builder().touch().build();
+    }
+
+    static final int ACTION_UNSET = -1;
+
+    // Add other actions from MotionEvent.ACTION_ as needed.
+    @IntDef(flag = true, value = {
+            MotionEvent.ACTION_DOWN,
+            MotionEvent.ACTION_MOVE,
+            MotionEvent.ACTION_UP
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Action {}
+
+    // Add other types from MotionEvent.TOOL_TYPE_ as needed.
+    @IntDef(flag = true, value = {
+            MotionEvent.TOOL_TYPE_FINGER,
+            MotionEvent.TOOL_TYPE_MOUSE,
+            MotionEvent.TOOL_TYPE_STYLUS,
+            MotionEvent.TOOL_TYPE_UNKNOWN
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ToolType {}
+
+    @IntDef(flag = true, value = {
+            MotionEvent.BUTTON_PRIMARY,
+            MotionEvent.BUTTON_SECONDARY
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Button {}
+
+    @IntDef(flag = true, value = {
+            KeyEvent.META_SHIFT_ON,
+            KeyEvent.META_CTRL_ON
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Key {}
+
+    private static final class State {
+        private @Action int mAction = ACTION_UNSET;
+        private @ToolType int mToolType = MotionEvent.TOOL_TYPE_UNKNOWN;
+        private int mPointerCount = 1;
+        private Set<Integer> mButtons = new HashSet<>();
+        private Set<Integer> mKeys = new HashSet<>();
+        private Point mLocation = new Point(0, 0);
+        private Point mRawLocation = new Point(0, 0);
+    }
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /**
+     * Test event builder with convenience methods for common event attrs.
+     */
+    public static final class Builder {
+
+        private State mState = new State();
+
+        /**
+         * @param action Any action specified in {@link MotionEvent}.
+         * @return
+         */
+        public Builder action(int action) {
+            mState.mAction = action;
+            return this;
+        }
+
+        public Builder type(@ToolType int type) {
+            mState.mToolType = type;
+            return this;
+        }
+
+        public Builder location(int x, int y) {
+            mState.mLocation = new Point(x, y);
+            return this;
+        }
+
+        public Builder rawLocation(int x, int y) {
+            mState.mRawLocation = new Point(x, y);
+            return this;
+        }
+
+        public Builder pointerCount(int count) {
+            mState.mPointerCount = count;
+            return this;
+        }
+
+        /**
+         * Adds one or more button press attributes.
+         */
+        public Builder pressButton(@Button int... buttons) {
+            for (int button : buttons) {
+                mState.mButtons.add(button);
+            }
+            return this;
+        }
+
+        /**
+         * Removes one or more button press attributes.
+         */
+        public Builder releaseButton(@Button int... buttons) {
+            for (int button : buttons) {
+                mState.mButtons.remove(button);
+            }
+            return this;
+        }
+
+        /**
+         * Adds one or more key press attributes.
+         */
+        public Builder pressKey(@Key int... keys) {
+            for (int key : keys) {
+                mState.mKeys.add(key);
+            }
+            return this;
+        }
+
+        /**
+         * Removes one or more key press attributes.
+         */
+        public Builder releaseKey(@Button int... keys) {
+            for (int key : keys) {
+                mState.mKeys.remove(key);
+            }
+            return this;
+        }
+
+        public Builder touch() {
+            type(MotionEvent.TOOL_TYPE_FINGER);
+            return this;
+        }
+
+        public Builder mouse() {
+            type(MotionEvent.TOOL_TYPE_MOUSE);
+            return this;
+        }
+
+        public Builder shift() {
+            pressKey(KeyEvent.META_SHIFT_ON);
+            return this;
+        }
+
+        public Builder unshift() {
+            releaseKey(KeyEvent.META_SHIFT_ON);
+            return this;
+        }
+
+        public Builder ctrl() {
+            pressKey(KeyEvent.META_CTRL_ON);
+            return this;
+        }
+
+        public Builder alt() {
+            pressKey(KeyEvent.META_ALT_ON);
+            return this;
+        }
+
+        public Builder primary() {
+            pressButton(MotionEvent.BUTTON_PRIMARY);
+            releaseButton(MotionEvent.BUTTON_SECONDARY);
+            releaseButton(MotionEvent.BUTTON_TERTIARY);
+            return this;
+        }
+
+        public Builder secondary() {
+            pressButton(MotionEvent.BUTTON_SECONDARY);
+            releaseButton(MotionEvent.BUTTON_PRIMARY);
+            releaseButton(MotionEvent.BUTTON_TERTIARY);
+            return this;
+        }
+
+        public Builder tertiary() {
+            pressButton(MotionEvent.BUTTON_TERTIARY);
+            releaseButton(MotionEvent.BUTTON_PRIMARY);
+            releaseButton(MotionEvent.BUTTON_SECONDARY);
+            return this;
+        }
+
+        public MotionEvent build() {
+
+            PointerProperties[] pointers = new PointerProperties[1];
+            pointers[0] = new PointerProperties();
+            pointers[0].id = 0;
+            pointers[0].toolType = mState.mToolType;
+
+            PointerCoords[] coords = new PointerCoords[1];
+            coords[0] = new PointerCoords();
+            coords[0].x = mState.mLocation.x;
+            coords[0].y = mState.mLocation.y;
+
+            int buttons = 0;
+            for (Integer button : mState.mButtons) {
+                buttons |= button;
+            }
+
+            int keys = 0;
+            for (Integer key : mState.mKeys) {
+                keys |= key;
+            }
+
+            return MotionEvent.obtain(
+                    0,     // down time
+                    1,     // event time
+                    mState.mAction,
+                    1,  // pointerCount,
+                    pointers,
+                    coords,
+                    keys,
+                    buttons,
+                    1.0f,  // x precision
+                    1.0f,  // y precision
+                    0,     // device id
+                    0,     // edge flags
+                    0,     // int source,
+                    0      // int flags
+                    );
+        }
+    }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestFocusCallbacks.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestFocusCallbacks.java
new file mode 100644
index 0000000..46b02ad
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestFocusCallbacks.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection.testing;
+
+import static org.junit.Assert.assertEquals;
+
+import android.support.v7.widget.RecyclerView;
+
+import androidx.recyclerview.selection.FocusCallbacks;
+import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
+
+public final class TestFocusCallbacks<K> extends FocusCallbacks<K> {
+
+    private K mFocusItemId;
+    private int mFocusPosition;
+
+    @Override
+    public void clearFocus() {
+        mFocusPosition = RecyclerView.NO_POSITION;
+        mFocusItemId = null;
+    }
+
+    @Override
+    public void focusItem(ItemDetails<K> item) {
+        mFocusItemId = item.getSelectionKey();
+        mFocusPosition = item.getPosition();
+    }
+
+    @Override
+    public int getFocusedPosition() {
+        return mFocusPosition;
+    }
+
+    @Override
+    public boolean hasFocusedItem() {
+        return mFocusItemId != null;
+    }
+
+    public void assertHasFocus(boolean focused) {
+        assertEquals(focused, hasFocusedItem());
+    }
+
+    public void assertFocused(String expectedId) {
+        assertEquals(expectedId, mFocusItemId);
+    }
+}
diff --git a/media-compat-test-lib/build.gradle b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestHolder.java
similarity index 64%
copy from media-compat-test-lib/build.gradle
copy to recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestHolder.java
index 26594e5..25c6581 100644
--- a/media-compat-test-lib/build.gradle
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestHolder.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2017 The Android Open Source Project
+ * Copyright 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.
@@ -14,4 +14,13 @@
  * limitations under the License.
  */
 
-apply plugin: 'java'
+package androidx.recyclerview.selection.testing;
+
+import android.support.v7.widget.RecyclerView.ViewHolder;
+import android.view.View;
+
+public class TestHolder extends ViewHolder {
+    public TestHolder(View itemView) {
+        super(itemView);
+    }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestItemDetails.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestItemDetails.java
new file mode 100644
index 0000000..f06e32c
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestItemDetails.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection.testing;
+
+import android.support.v7.widget.RecyclerView;
+import android.view.MotionEvent;
+
+import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
+
+public final class TestItemDetails extends ItemDetails<String> {
+
+    private int mPosition;
+    private String mSelectionKey;
+    private boolean mInDragRegion;
+    private boolean mInSelectionHotspot;
+
+    public TestItemDetails() {
+        mPosition = RecyclerView.NO_POSITION;
+    }
+
+    public TestItemDetails(TestItemDetails source) {
+        mPosition = source.mPosition;
+        mSelectionKey = source.mSelectionKey;
+        mInDragRegion = source.mInDragRegion;
+        mInSelectionHotspot = source.mInSelectionHotspot;
+    }
+
+    public void at(int position) {
+        mPosition = position;  // this is both "adapter position" and "item position".
+        mSelectionKey = (position == RecyclerView.NO_POSITION)
+                ? null
+                : String.valueOf(position);
+    }
+
+    public void setInItemDragRegion(boolean inHotspot) {
+        mInDragRegion = inHotspot;
+    }
+
+    public void setInItemSelectRegion(boolean over) {
+        mInSelectionHotspot = over;
+    }
+
+    @Override
+    public boolean inDragRegion(MotionEvent event) {
+        return mInDragRegion;
+    }
+
+    @Override
+    public int hashCode() {
+        return mPosition;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+
+        if (!(o instanceof TestItemDetails)) {
+            return false;
+        }
+
+        TestItemDetails other = (TestItemDetails) o;
+        return mPosition == other.mPosition
+                && mSelectionKey == other.mSelectionKey;
+    }
+
+    @Override
+    public int getPosition() {
+        return mPosition;
+    }
+
+    @Override
+    public String getSelectionKey() {
+        return mSelectionKey;
+    }
+
+    @Override
+    public boolean inSelectionHotspot(MotionEvent e) {
+        return mInSelectionHotspot;
+    }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestItemDetailsLookup.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestItemDetailsLookup.java
new file mode 100644
index 0000000..c201575
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestItemDetailsLookup.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection.testing;
+
+import android.view.MotionEvent;
+
+import javax.annotation.Nullable;
+
+import androidx.recyclerview.selection.ItemDetailsLookup;
+
+/**
+ * Test impl of ItemDetailsLookup.
+ */
+public class TestItemDetailsLookup extends ItemDetailsLookup<String> {
+
+    private @Nullable TestItemDetails mItem;
+
+    @Override
+    public @Nullable ItemDetails<String> getItemDetails(MotionEvent e) {
+        return mItem;
+    }
+
+    /**
+     * Creates/installs/returns a new test document. Subsequent calls to
+     * any EventDocLookup methods will consult the newly created doc.
+     */
+    public TestItemDetails initAt(int position) {
+        TestItemDetails doc = new TestItemDetails();
+        doc.at(position);
+        mItem = doc;
+        return doc;
+    }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestItemKeyProvider.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestItemKeyProvider.java
new file mode 100644
index 0000000..c874ac5
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestItemKeyProvider.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection.testing;
+
+import static android.support.v4.util.Preconditions.checkArgument;
+
+import androidx.recyclerview.selection.ItemKeyProvider;
+
+/**
+ * Provides RecyclerView selection code access to stable ids backed
+ * by TestAdapter.
+ */
+public final class TestItemKeyProvider<K> extends ItemKeyProvider<K> {
+
+    private final TestAdapter<K> mAdapter;
+
+    public TestItemKeyProvider(@Scope int scope, TestAdapter<K> adapter) {
+        super(scope);
+        checkArgument(adapter != null);
+        mAdapter = adapter;
+    }
+
+    @Override
+    public K getKey(int position) {
+        return mAdapter.getSelectionKey(position);
+    }
+
+    @Override
+    public int getPosition(K key) {
+        return mAdapter.getPosition(key);
+    }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestMouseCallbacks.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestMouseCallbacks.java
new file mode 100644
index 0000000..321ac7f
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestMouseCallbacks.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection.testing;
+
+import static org.junit.Assert.assertTrue;
+
+import android.view.MotionEvent;
+
+import androidx.recyclerview.selection.MouseCallbacks;
+
+public final class TestMouseCallbacks extends MouseCallbacks {
+
+    private MotionEvent mLastContextEvent;
+
+    @Override
+    public boolean onContextClick(MotionEvent e) {
+        mLastContextEvent = e;
+        return false;
+    }
+
+    public void assertLastEvent(MotionEvent expected) {
+        // sadly, MotionEvent doesn't implement equals, so we compare references.
+        assertTrue(expected == mLastContextEvent);
+    }
+}
diff --git a/media-compat-test-lib/build.gradle b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestRunnable.java
similarity index 60%
copy from media-compat-test-lib/build.gradle
copy to recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestRunnable.java
index 26594e5..7a1d45c 100644
--- a/media-compat-test-lib/build.gradle
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestRunnable.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2017 The Android Open Source Project
+ * Copyright 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.
@@ -14,4 +14,20 @@
  * limitations under the License.
  */
 
-apply plugin: 'java'
+package androidx.recyclerview.selection.testing;
+
+import static junit.framework.Assert.assertTrue;
+
+public final class TestRunnable implements Runnable {
+
+    private boolean mRan;
+
+    @Override
+    public void run() {
+        mRan = true;
+    }
+
+    public void assertRan() {
+        assertTrue(mRan);
+    }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestSelectionObserver.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestSelectionObserver.java
new file mode 100644
index 0000000..3185d78
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestSelectionObserver.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection.testing;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import androidx.recyclerview.selection.SelectionHelper.SelectionObserver;
+
+public class TestSelectionObserver<K> extends SelectionObserver<K> {
+
+    private final Set<K> mSelected = new HashSet<>();
+    private boolean mSelectionChanged = false;
+    private boolean mSelectionReset = false;
+    private boolean mSelectionRestored = false;
+
+    public void reset() {
+        mSelected.clear();
+        mSelectionChanged = false;
+        mSelectionReset = false;
+    }
+
+    @Override
+    public void onItemStateChanged(K key, boolean selected) {
+        if (selected) {
+            assertNotSelected(key);
+            mSelected.add(key);
+        } else {
+            assertSelected(key);
+            mSelected.remove(key);
+        }
+    }
+
+    @Override
+    public void onSelectionReset() {
+        mSelectionReset = true;
+        mSelected.clear();
+    }
+
+    @Override
+    public void onSelectionChanged() {
+        mSelectionChanged = true;
+    }
+
+    @Override
+    public void onSelectionRestored() {
+        mSelectionRestored = true;
+    }
+
+    void assertNoSelection() {
+        assertTrue(mSelected.isEmpty());
+    }
+
+    void assertSelectionSize(int expected) {
+        assertEquals(expected, mSelected.size());
+    }
+
+    void assertSelected(K key) {
+        assertTrue(key + " is not selected.", mSelected.contains(key));
+    }
+
+    void assertNotSelected(K key) {
+        assertFalse(key + " is already selected", mSelected.contains(key));
+    }
+
+    public void assertSelectionChanged() {
+        assertTrue(mSelectionChanged);
+    }
+
+    public void assertSelectionUnchanged() {
+        assertFalse(mSelectionChanged);
+    }
+
+    public void assertSelectionReset() {
+        assertTrue(mSelectionReset);
+    }
+
+    public void assertSelectionRestored() {
+        assertTrue(mSelectionRestored);
+    }
+}
diff --git a/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestSelectionPredicate.java b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestSelectionPredicate.java
new file mode 100644
index 0000000..4baeb2b
--- /dev/null
+++ b/recyclerview-selection/tests/java/androidx/recyclerview/selection/testing/TestSelectionPredicate.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 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 androidx.recyclerview.selection.testing;
+
+import androidx.recyclerview.selection.SelectionHelper.SelectionPredicate;
+
+public final class TestSelectionPredicate<K> extends SelectionPredicate<K> {
+
+    private final boolean mMultiSelect;
+
+    private boolean mValue;
+
+    public TestSelectionPredicate(boolean multiSelect) {
+        mMultiSelect = multiSelect;
+    }
+
+    public TestSelectionPredicate() {
+        this(true);
+    }
+
+    public void setReturnValue(boolean value) {
+        mValue = value;
+    }
+
+    @Override
+    public boolean canSetStateForKey(K key, boolean nextState) {
+        return mValue;
+    }
+
+    @Override
+    public boolean canSetStateAtPosition(int position, boolean nextState) {
+        return mValue;
+    }
+
+    @Override
+    public boolean canSelectMultiple() {
+        return mMultiSelect;
+    }
+}
diff --git a/room/gradle/wrapper/gradle-wrapper.properties b/room/gradle/wrapper/gradle-wrapper.properties
index 2f8bf03..383477d 100644
--- a/room/gradle/wrapper/gradle-wrapper.properties
+++ b/room/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
 distributionPath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-3.4-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.3-bin.zip
diff --git a/samples/Support7Demos/build.gradle b/samples/Support7Demos/build.gradle
index 0b562c8..14d128a 100644
--- a/samples/Support7Demos/build.gradle
+++ b/samples/Support7Demos/build.gradle
@@ -7,6 +7,7 @@
     implementation project(':mediarouter-v7')
     implementation project(':palette-v7')
     implementation project(':recyclerview-v7')
+    implementation project(':recyclerview-selection')
 }
 
 android {
diff --git a/samples/Support7Demos/src/main/AndroidManifest.xml b/samples/Support7Demos/src/main/AndroidManifest.xml
index 1604267..d32ea97 100644
--- a/samples/Support7Demos/src/main/AndroidManifest.xml
+++ b/samples/Support7Demos/src/main/AndroidManifest.xml
@@ -563,7 +563,26 @@
                 <category android:name="com.example.android.supportv7.SAMPLE_CODE"/>
             </intent-filter>
         </activity>
-    </application>
 
+        <!-- Selection helper demo activity -->
+        <activity android:name=".widget.selection.simple.SimpleSelectionDemoActivity"
+                  android:label="@string/simple_selection_demo_activity"
+                  android:theme="@style/Theme.AppCompat.Light">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="com.example.android.supportv7.SAMPLE_CODE"/>
+            </intent-filter>
+        </activity>
+
+        <!-- Selection helper demo activity -->
+        <activity android:name=".widget.selection.fancy.FancySelectionDemoActivity"
+                  android:label="@string/fancy_selection_demo_activity"
+                  android:theme="@style/Theme.AppCompat.Light">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="com.example.android.supportv7.SAMPLE_CODE"/>
+            </intent-filter>
+        </activity>
+    </application>
 
 </manifest>
diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/ContentUriKeyProvider.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/ContentUriKeyProvider.java
new file mode 100644
index 0000000..4b9c158
--- /dev/null
+++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/ContentUriKeyProvider.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 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 com.example.android.supportv7.widget.selection.fancy;
+
+import android.net.Uri;
+import android.support.annotation.Nullable;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import androidx.recyclerview.selection.ItemKeyProvider;
+
+class ContentUriKeyProvider extends ItemKeyProvider<Uri> {
+
+    private final Uri[] mUris;
+    private final Map<Uri, Integer> mPositions;
+
+    ContentUriKeyProvider(String authority, String[] values) {
+        // Advise the world we can supply ids/position for entire copus
+        // at any time.
+        super(SCOPE_MAPPED);
+
+        mUris = new Uri[values.length];
+        mPositions = new HashMap<>();
+
+        for (int i = 0; i < values.length; i++) {
+            mUris[i] = new Uri.Builder()
+                    .scheme("content")
+                    .encodedAuthority(authority)
+                    .appendPath(values[i])
+                    .build();
+            mPositions.put(mUris[i], i);
+        }
+    }
+
+    @Override
+    public @Nullable Uri getKey(int position) {
+        return mUris[position];
+    }
+
+    @Override
+    public int getPosition(Uri key) {
+        return mPositions.get(key);
+    }
+}
diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/FancyDetailsLookup.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/FancyDetailsLookup.java
new file mode 100644
index 0000000..249d3c2
--- /dev/null
+++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/FancyDetailsLookup.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 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 com.example.android.supportv7.widget.selection.fancy;
+
+import android.net.Uri;
+import android.support.annotation.Nullable;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.ViewHolder;
+import android.view.MotionEvent;
+import android.view.View;
+
+import androidx.recyclerview.selection.ItemDetailsLookup;
+
+/**
+ * Access to details of an item associated with a {@link MotionEvent} instance.
+ */
+final class FancyDetailsLookup extends ItemDetailsLookup<Uri> {
+
+    private final RecyclerView mRecView;
+
+    FancyDetailsLookup(RecyclerView view) {
+        mRecView = view;
+    }
+
+    @Override
+    public ItemDetails<Uri> getItemDetails(MotionEvent e) {
+        @Nullable View view = mRecView.findChildViewUnder(e.getX(), e.getY());
+        if (view != null) {
+            ViewHolder holder = mRecView.getChildViewHolder(view);
+            if (holder instanceof FancyHolder) {
+                return ((FancyHolder) holder).getItemDetails();
+            }
+        }
+        return null;
+    }
+}
diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/FancyHolder.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/FancyHolder.java
new file mode 100644
index 0000000..f4b22b3
--- /dev/null
+++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/FancyHolder.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 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 com.example.android.supportv7.widget.selection.fancy;
+
+import android.graphics.Rect;
+import android.net.Uri;
+import android.support.annotation.Nullable;
+import android.support.v7.widget.RecyclerView;
+import android.view.MotionEvent;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.example.android.supportv7.R;
+
+import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
+
+final class FancyHolder extends RecyclerView.ViewHolder {
+
+    private final LinearLayout mContainer;
+    public final TextView mSelector;
+    public final TextView mLabel;
+    private final ItemDetails<Uri> mDetails;
+
+    private @Nullable Uri mKey;
+
+    FancyHolder(LinearLayout layout) {
+        super(layout);
+        mContainer = layout.findViewById(R.id.container);
+        mSelector = layout.findViewById(R.id.selector);
+        mLabel = layout.findViewById(R.id.label);
+        mDetails = new ItemDetails<Uri>() {
+            @Override
+            public int getPosition() {
+                return FancyHolder.this.getAdapterPosition();
+            }
+
+            @Override
+            public Uri getSelectionKey() {
+                return FancyHolder.this.mKey;
+            }
+
+            @Override
+            public boolean inDragRegion(MotionEvent e) {
+                return FancyHolder.this.inDragRegion(e);
+            }
+
+            @Override
+            public boolean inSelectionHotspot(MotionEvent e) {
+                return FancyHolder.this.inSelectRegion(e);
+            }
+        };
+    }
+
+    void update(Uri key, String label, boolean selected) {
+        mKey = key;
+        mLabel.setText(label);
+        setSelected(selected);
+    }
+
+    private void setSelected(boolean selected) {
+        mContainer.setActivated(selected);
+        mSelector.setActivated(selected);
+    }
+
+    boolean inDragRegion(MotionEvent event) {
+        // If itemView is activated = selected, then whole region is interactive
+        if (itemView.isActivated()) {
+            return true;
+        }
+
+        // Do everything in global coordinates - it makes things simpler.
+        int[] coords = new int[2];
+        mSelector.getLocationOnScreen(coords);
+
+        Rect textBounds = new Rect();
+        mLabel.getPaint().getTextBounds(
+                mLabel.getText().toString(), 0, mLabel.getText().length(), textBounds);
+
+        Rect rect = new Rect(
+                coords[0],
+                coords[1],
+                coords[0] + mSelector.getWidth() + textBounds.width(),
+                coords[1] + Math.max(mSelector.getHeight(), textBounds.height()));
+
+        // If the tap occurred inside icon or the text, these are interactive spots.
+        return rect.contains((int) event.getRawX(), (int) event.getRawY());
+    }
+
+    boolean inSelectRegion(MotionEvent e) {
+        Rect iconRect = new Rect();
+        mSelector.getGlobalVisibleRect(iconRect);
+        return iconRect.contains((int) e.getRawX(), (int) e.getRawY());
+    }
+
+    ItemDetails<Uri> getItemDetails() {
+        return mDetails;
+    }
+}
diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/FancySelectionDemoActivity.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/FancySelectionDemoActivity.java
new file mode 100644
index 0000000..6c389d5
--- /dev/null
+++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/FancySelectionDemoActivity.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright 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 com.example.android.supportv7.widget.selection.fancy;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.annotation.CallSuper;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.GridLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.widget.Toast;
+
+import com.example.android.supportv7.R;
+
+import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
+import androidx.recyclerview.selection.ItemKeyProvider;
+import androidx.recyclerview.selection.SelectionHelper;
+import androidx.recyclerview.selection.SelectionHelper.SelectionObserver;
+import androidx.recyclerview.selection.SelectionHelperBuilder;
+import androidx.recyclerview.selection.SelectionPredicates;
+import androidx.recyclerview.selection.SelectionStorage;
+
+/**
+ * ContentPager demo activity.
+ */
+public class FancySelectionDemoActivity extends AppCompatActivity {
+
+    private static final String TAG = "SelectionDemos";
+    private static final String EXTRA_COLUMN_COUNT = "demo-column-count";
+
+    private FancySelectionDemoAdapter mAdapter;
+    private SelectionHelper<Uri> mSelectionHelper;
+    private SelectionStorage<Uri> mSelectionStorage;
+
+    private GridLayoutManager mLayout;
+    private int mColumnCount = 1;  // This will get updated when layout changes.
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.selection_demo_layout);
+        RecyclerView recView = (RecyclerView) findViewById(R.id.list);
+
+        mLayout = new GridLayoutManager(this, mColumnCount);
+        recView.setLayoutManager(mLayout);
+        mAdapter = new FancySelectionDemoAdapter(this);
+        recView.setAdapter(mAdapter);
+        ItemKeyProvider<Uri> keyProvider = mAdapter.getItemKeyProvider();
+
+        SelectionHelperBuilder<Uri> builder = new SelectionHelperBuilder<>(
+                recView,
+                keyProvider,
+                new FancyDetailsLookup(recView));
+
+        // Override default behaviors and build in multi select mode.
+        // Call .withSelectionPredicate(SelectionHelper.SelectionPredicate.SINGLE_ANYTHING)
+        // for single selection mode.
+        mSelectionHelper = builder
+                .withSelectionPredicate(SelectionPredicates.selectAnything())
+                .withTouchCallbacks(new TouchCallbacks(this))
+                .withMouseCallbacks(new MouseCallbacks(this))
+                .withActivationCallbacks(new ActivationCallbacks(this))
+                .withFocusCallbacks(new FocusCallbacks(this))
+                .withBandOverlay(R.drawable.selection_demo_band_overlay)
+                .build();
+
+        // Provide glue between activity lifecycle and selection for purposes
+        // restoring selection.
+        mSelectionStorage = new SelectionStorage<>(
+                SelectionStorage.TYPE_STRING, mSelectionHelper);
+
+        // Lazily bind SelectionHelper. Allows us to defer initialization of the
+        // SelectionHelper dependency until after the adapter is created.
+        mAdapter.bindSelectionHelper(mSelectionHelper);
+
+        // TODO: Glue selection to ActionMode, since that'll be a common practice.
+        mSelectionHelper.addObserver(
+                new SelectionObserver<Long>() {
+                    @Override
+                    public void onSelectionChanged() {
+                        Log.i(TAG, "Selection changed to: " + mSelectionHelper.getSelection());
+                    }
+                });
+
+        // Restore selection from saved state.
+        updateFromSavedState(savedInstanceState);
+    }
+
+    @Override
+    protected void onSaveInstanceState(Bundle state) {
+        super.onSaveInstanceState(state);
+        mSelectionStorage.onSaveInstanceState(state);
+        state.putInt(EXTRA_COLUMN_COUNT, mColumnCount);
+    }
+
+    private void updateFromSavedState(Bundle state) {
+        mSelectionStorage.onRestoreInstanceState(state);
+
+        if (state != null) {
+            if (state.containsKey(EXTRA_COLUMN_COUNT)) {
+                mColumnCount = state.getInt(EXTRA_COLUMN_COUNT);
+                mLayout.setSpanCount(mColumnCount);
+            }
+        }
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        boolean showMenu = super.onCreateOptionsMenu(menu);
+        getMenuInflater().inflate(R.menu.selection_demo_actions, menu);
+        return showMenu;
+    }
+
+    @Override
+    @CallSuper
+    public boolean onPrepareOptionsMenu(Menu menu) {
+        super.onPrepareOptionsMenu(menu);
+        menu.findItem(R.id.option_menu_add_column).setEnabled(mColumnCount <= 3);
+        menu.findItem(R.id.option_menu_remove_column).setEnabled(mColumnCount > 1);
+        return true;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+            case R.id.option_menu_add_column:
+                // TODO: Add columns
+                mLayout.setSpanCount(++mColumnCount);
+                return true;
+
+            case R.id.option_menu_remove_column:
+                mLayout.setSpanCount(--mColumnCount);
+                return true;
+
+            default:
+                return super.onOptionsItemSelected(item);
+        }
+    }
+
+    @Override
+    public void onBackPressed() {
+        if (mSelectionHelper.clear()) {
+            return;
+        } else {
+            super.onBackPressed();
+        }
+    }
+
+    private static void toast(Context context, String msg) {
+        Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
+    }
+
+    @Override
+    protected void onDestroy() {
+        mSelectionHelper.clearSelection();
+        super.onDestroy();
+    }
+
+    @Override
+    protected void onStart() {
+        super.onStart();
+        mAdapter.loadData();
+    }
+
+    // Implementation of MouseInputHandler.Callbacks allows handling
+    // of higher level events, like onActivated.
+    private static final class ActivationCallbacks extends
+            androidx.recyclerview.selection.ActivationCallbacks<Uri> {
+
+        private final Context mContext;
+
+        ActivationCallbacks(Context context) {
+            mContext = context;
+        }
+
+        @Override
+        public boolean onItemActivated(ItemDetails<Uri> item, MotionEvent e) {
+            toast(mContext, "Activate item: " + item.getSelectionKey());
+            return true;
+        }
+    }
+
+    private static final class FocusCallbacks extends
+            androidx.recyclerview.selection.FocusCallbacks<Uri> {
+
+        private final Context mContext;
+
+        private FocusCallbacks(Context context) {
+            mContext = context;
+        }
+
+        @Override
+        public void focusItem(ItemDetails<Uri> item) {
+            toast(mContext, "Focused item: " + item.getSelectionKey());
+        }
+
+        @Override
+        public boolean hasFocusedItem() {
+            return false;
+        }
+
+        @Override
+        public int getFocusedPosition() {
+            return 0;
+        }
+
+        @Override
+        public void clearFocus() {
+            toast(mContext, "Cleared focus.");
+        }
+    }
+
+    // Implementation of MouseInputHandler.Callbacks allows handling
+    // of higher level events, like onActivated.
+    private static final class MouseCallbacks extends
+            androidx.recyclerview.selection.MouseCallbacks {
+
+        private final Context mContext;
+
+        MouseCallbacks(Context context) {
+            mContext = context;
+        }
+
+        @Override
+        public boolean onContextClick(MotionEvent e) {
+            toast(mContext, "Context click received.");
+            return true;
+        }
+    };
+
+    private static final class TouchCallbacks extends
+            androidx.recyclerview.selection.TouchCallbacks {
+
+        private final Context mContext;
+
+        private TouchCallbacks(Context context) {
+            mContext = context;
+        }
+
+        public boolean onDragInitiated(MotionEvent e) {
+            toast(mContext, "onDragInitiated received.");
+            return true;
+        }
+    }
+}
diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/FancySelectionDemoAdapter.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/FancySelectionDemoAdapter.java
new file mode 100644
index 0000000..204ec9e
--- /dev/null
+++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/fancy/FancySelectionDemoAdapter.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 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 com.example.android.supportv7.widget.selection.fancy;
+
+import static android.support.v4.util.Preconditions.checkArgument;
+
+import android.content.Context;
+import android.net.Uri;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+
+import com.example.android.supportv7.Cheeses;
+import com.example.android.supportv7.R;
+
+import androidx.recyclerview.selection.ItemKeyProvider;
+import androidx.recyclerview.selection.SelectionHelper;
+
+final class FancySelectionDemoAdapter extends RecyclerView.Adapter<FancyHolder> {
+
+    private final ItemKeyProvider<Uri> mKeyProvider;
+    private final Context mContext;
+
+    // This should be replaced at "bind" time with a real test that
+    // asks SelectionHelper.
+    private SelectionTest mSelTest;
+
+    FancySelectionDemoAdapter(Context context) {
+        mContext = context;
+        mKeyProvider = new ContentUriKeyProvider("cheeses", Cheeses.sCheeseStrings);
+        mSelTest = new SelectionTest() {
+            @Override
+            public boolean isSelected(Uri id) {
+                throw new IllegalStateException(
+                        "Adapter must be initialized with SelectionHelper.");
+            }
+        };
+
+        // In the fancy edition of selection support we supply access to stable
+        // ids using content URI. Since we can map between position and selection key
+        // at will we get fancy dependent functionality like band selection and range support.
+        setHasStableIds(false);
+    }
+
+    ItemKeyProvider<Uri> getItemKeyProvider() {
+        return mKeyProvider;
+    }
+
+    // Glue together SelectionHelper and the adapter.
+    public void bindSelectionHelper(final SelectionHelper<Uri> selectionHelper) {
+        checkArgument(selectionHelper != null);
+        mSelTest = new SelectionTest() {
+            @Override
+            public boolean isSelected(Uri id) {
+                return selectionHelper.isSelected(id);
+            }
+        };
+    }
+
+    void loadData() {
+        onDataReady();
+    }
+
+    private void onDataReady() {
+        notifyDataSetChanged();
+    }
+
+    @Override
+    public int getItemCount() {
+        return Cheeses.sCheeseStrings.length;
+    }
+
+    @Override
+    public long getItemId(int position) {
+        return position;
+    }
+
+    @Override
+    public void onBindViewHolder(FancyHolder holder, int position) {
+        Uri uri = mKeyProvider.getKey(position);
+        holder.update(uri, uri.getLastPathSegment(), mSelTest.isSelected(uri));
+    }
+
+    @Override
+    public FancyHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+        LinearLayout layout = inflateLayout(mContext, parent, R.layout.selection_demo_list_item);
+        return new FancyHolder(layout);
+    }
+
+    @SuppressWarnings("TypeParameterUnusedInFormals")  // Convenience to avoid clumsy cast.
+    private static <V extends View> V inflateLayout(
+            Context context, ViewGroup parent, int layout) {
+
+        return (V) LayoutInflater.from(context).inflate(layout, parent, false);
+    }
+
+    private interface SelectionTest {
+        boolean isSelected(Uri id);
+    }
+}
diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/simple/DemoDetailsLookup.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/simple/DemoDetailsLookup.java
new file mode 100644
index 0000000..15aa086
--- /dev/null
+++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/simple/DemoDetailsLookup.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 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 com.example.android.supportv7.widget.selection.simple;
+
+import android.support.annotation.Nullable;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.ViewHolder;
+import android.view.MotionEvent;
+import android.view.View;
+
+import androidx.recyclerview.selection.ItemDetailsLookup;
+
+/**
+ * Access to details of an item associated with a {@link MotionEvent} instance.
+ */
+final class DemoDetailsLookup extends ItemDetailsLookup<Long> {
+
+    private final RecyclerView mRecView;
+
+    DemoDetailsLookup(RecyclerView view) {
+        mRecView = view;
+    }
+
+    @Override
+    public ItemDetails<Long> getItemDetails(MotionEvent e) {
+        @Nullable View view = mRecView.findChildViewUnder(e.getX(), e.getY());
+        if (view != null) {
+            ViewHolder holder = mRecView.getChildViewHolder(view);
+            if (holder instanceof DemoHolder) {
+                return ((DemoHolder) holder).getItemDetails();
+            }
+        }
+        return null;
+    }
+}
diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/simple/DemoHolder.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/simple/DemoHolder.java
new file mode 100644
index 0000000..c1d8b99
--- /dev/null
+++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/simple/DemoHolder.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 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 com.example.android.supportv7.widget.selection.simple;
+
+import android.graphics.Rect;
+import android.support.annotation.Nullable;
+import android.support.v7.widget.RecyclerView;
+import android.view.MotionEvent;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.example.android.supportv7.R;
+
+import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
+
+final class DemoHolder extends RecyclerView.ViewHolder {
+
+    private final LinearLayout mContainer;
+    private final TextView mSelector;
+    private final TextView mLabel;
+    private final ItemDetails<Long> mDetails;
+    private @Nullable Long mKey;
+
+    DemoHolder(LinearLayout layout) {
+        super(layout);
+        mContainer = layout.findViewById(R.id.container);
+        mSelector = layout.findViewById(R.id.selector);
+        mLabel = layout.findViewById(R.id.label);
+        mDetails = new ItemDetails<Long>() {
+            @Override
+            public int getPosition() {
+                return DemoHolder.this.getAdapterPosition();
+            }
+
+            @Override
+            public Long getSelectionKey() {
+                return DemoHolder.this.getItemId();
+            }
+
+            @Override
+            public boolean inDragRegion(MotionEvent e) {
+                return DemoHolder.this.inDragRegion(e);
+            }
+
+            @Override
+            public boolean inSelectionHotspot(MotionEvent e) {
+                return DemoHolder.this.inSelectRegion(e);
+            }
+        };
+    }
+
+    void update(String label, boolean selected) {
+        mLabel.setText(label);
+        setSelected(selected);
+    }
+
+    private void setSelected(boolean selected) {
+        mContainer.setActivated(selected);
+        mSelector.setActivated(selected);
+    }
+
+    boolean inDragRegion(MotionEvent event) {
+        // If itemView is activated = selected, then whole region is interactive
+        if (itemView.isActivated()) {
+            return true;
+        }
+
+        // Do everything in global coordinates - it makes things simpler.
+        int[] coords = new int[2];
+        mSelector.getLocationOnScreen(coords);
+
+        Rect textBounds = new Rect();
+        mLabel.getPaint().getTextBounds(
+                mLabel.getText().toString(), 0, mLabel.getText().length(), textBounds);
+
+        Rect rect = new Rect(
+                coords[0],
+                coords[1],
+                coords[0] + mSelector.getWidth() + textBounds.width(),
+                coords[1] + Math.max(mSelector.getHeight(), textBounds.height()));
+
+        // If the tap occurred inside icon or the text, these are interactive spots.
+        return rect.contains((int) event.getRawX(), (int) event.getRawY());
+    }
+
+    boolean inSelectRegion(MotionEvent e) {
+        Rect iconRect = new Rect();
+        mSelector.getGlobalVisibleRect(iconRect);
+        return iconRect.contains((int) e.getRawX(), (int) e.getRawY());
+    }
+
+    ItemDetails<Long> getItemDetails() {
+        return mDetails;
+    }
+}
diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/simple/SimpleSelectionDemoActivity.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/simple/SimpleSelectionDemoActivity.java
new file mode 100644
index 0000000..74d2fcc
--- /dev/null
+++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/simple/SimpleSelectionDemoActivity.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright 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 com.example.android.supportv7.widget.selection.simple;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.support.annotation.CallSuper;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.GridLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.widget.Toast;
+
+import com.example.android.supportv7.R;
+
+import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
+import androidx.recyclerview.selection.ItemKeyProvider;
+import androidx.recyclerview.selection.SelectionHelper;
+import androidx.recyclerview.selection.SelectionHelper.SelectionObserver;
+import androidx.recyclerview.selection.SelectionHelperBuilder;
+import androidx.recyclerview.selection.SelectionPredicates;
+import androidx.recyclerview.selection.SelectionStorage;
+import androidx.recyclerview.selection.StableIdKeyProvider;
+
+/**
+ * ContentPager demo activity.
+ */
+public class SimpleSelectionDemoActivity extends AppCompatActivity {
+
+    private static final String TAG = "SelectionDemos";
+    private static final String EXTRA_COLUMN_COUNT = "demo-column-count";
+
+    private SimpleSelectionDemoAdapter mAdapter;
+    private SelectionHelper<Long> mSelectionHelper;
+    private SelectionStorage<Long> mSelectionStorage;
+
+    private GridLayoutManager mLayout;
+    private int mColumnCount = 1;  // This will get updated when layout changes.
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.selection_demo_layout);
+        RecyclerView recView = (RecyclerView) findViewById(R.id.list);
+
+        // keyProvider depends on mAdapter.setHasStableIds(true).
+        ItemKeyProvider<Long> keyProvider = new StableIdKeyProvider(recView);
+
+        mLayout = new GridLayoutManager(this, mColumnCount);
+        recView.setLayoutManager(mLayout);
+
+        mAdapter = new SimpleSelectionDemoAdapter(this, keyProvider);
+        // The adapter is paired with a key provider that supports
+        // the native RecyclerView stableId. For this to work correctly
+        // the adapter must report that it supports stable ids.
+        mAdapter.setHasStableIds(true);
+
+        recView.setAdapter(mAdapter);
+
+        SelectionHelperBuilder<Long> builder = new SelectionHelperBuilder<>(
+                recView,
+                keyProvider,
+                new DemoDetailsLookup(recView));
+
+        // Override default behaviors and build in multi select mode.
+        // Call .withSelectionPredicate(SelectionHelper.SelectionPredicate.SINGLE_ANYTHING)
+        // for single selection mode.
+        mSelectionHelper = builder
+                .withSelectionPredicate(SelectionPredicates.selectAnything())
+                .withTouchCallbacks(new TouchCallbacks(this))
+                .withMouseCallbacks(new MouseCallbacks(this))
+                .withActivationCallbacks(new ActivationCallbacks(this))
+                .withFocusCallbacks(new FocusCallbacks(this))
+                .withBandOverlay(R.drawable.selection_demo_band_overlay)
+                .build();
+
+        // Provide glue between activity lifecycle and selection for purposes
+        // restoring selection.
+        mSelectionStorage = new SelectionStorage<>(
+                SelectionStorage.TYPE_STRING, mSelectionHelper);
+
+        // Lazily bind SelectionHelper. Allows us to defer initialization of the
+        // SelectionHelper dependency until after the adapter is created.
+        mAdapter.bindSelectionHelper(mSelectionHelper);
+
+        // TODO: Glue selection to ActionMode, since that'll be a common practice.
+        mSelectionHelper.addObserver(
+                new SelectionObserver<Long>() {
+                    @Override
+                    public void onSelectionChanged() {
+                        Log.i(TAG, "Selection changed to: " + mSelectionHelper.getSelection());
+                    }
+                });
+
+        // Restore selection from saved state.
+        updateFromSavedState(savedInstanceState);
+    }
+
+    @Override
+    protected void onSaveInstanceState(Bundle state) {
+        super.onSaveInstanceState(state);
+        mSelectionStorage.onSaveInstanceState(state);
+        state.putInt(EXTRA_COLUMN_COUNT, mColumnCount);
+    }
+
+    private void updateFromSavedState(Bundle state) {
+        mSelectionStorage.onRestoreInstanceState(state);
+
+        if (state != null) {
+            if (state.containsKey(EXTRA_COLUMN_COUNT)) {
+                mColumnCount = state.getInt(EXTRA_COLUMN_COUNT);
+                mLayout.setSpanCount(mColumnCount);
+            }
+        }
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        boolean showMenu = super.onCreateOptionsMenu(menu);
+        getMenuInflater().inflate(R.menu.selection_demo_actions, menu);
+        return showMenu;
+    }
+
+    @Override
+    @CallSuper
+    public boolean onPrepareOptionsMenu(Menu menu) {
+        super.onPrepareOptionsMenu(menu);
+        menu.findItem(R.id.option_menu_add_column).setEnabled(mColumnCount <= 3);
+        menu.findItem(R.id.option_menu_remove_column).setEnabled(mColumnCount > 1);
+        return true;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+            case R.id.option_menu_add_column:
+                // TODO: Add columns
+                mLayout.setSpanCount(++mColumnCount);
+                return true;
+
+            case R.id.option_menu_remove_column:
+                mLayout.setSpanCount(--mColumnCount);
+                return true;
+
+            default:
+                return super.onOptionsItemSelected(item);
+        }
+    }
+
+    @Override
+    public void onBackPressed() {
+        if (mSelectionHelper.clear()) {
+            return;
+        } else {
+            super.onBackPressed();
+        }
+    }
+
+    private static void toast(Context context, String msg) {
+        Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
+    }
+
+    @Override
+    protected void onDestroy() {
+        mSelectionHelper.clearSelection();
+        super.onDestroy();
+    }
+
+    @Override
+    protected void onStart() {
+        super.onStart();
+        mAdapter.loadData();
+    }
+
+    // Implementation of MouseInputHandler.Callbacks allows handling
+    // of higher level events, like onActivated.
+    private static final class ActivationCallbacks extends
+            androidx.recyclerview.selection.ActivationCallbacks<Long> {
+
+        private final Context mContext;
+
+        ActivationCallbacks(Context context) {
+            mContext = context;
+        }
+
+        @Override
+        public boolean onItemActivated(ItemDetails<Long> item, MotionEvent e) {
+            toast(mContext, "Activate item: " + item.getSelectionKey());
+            return true;
+        }
+    }
+
+    private static final class FocusCallbacks extends
+            androidx.recyclerview.selection.FocusCallbacks<Long> {
+
+        private final Context mContext;
+
+        private FocusCallbacks(Context context) {
+            mContext = context;
+        }
+
+        @Override
+        public void focusItem(ItemDetails<Long> item) {
+            toast(mContext, "Focused item: " + item.getSelectionKey());
+        }
+
+        @Override
+        public boolean hasFocusedItem() {
+            return false;
+        }
+
+        @Override
+        public int getFocusedPosition() {
+            return 0;
+        }
+
+        @Override
+        public void clearFocus() {
+            toast(mContext, "Cleared focus.");
+        }
+    }
+
+    // Implementation of MouseInputHandler.Callbacks allows handling
+    // of higher level events, like onActivated.
+    private static final class MouseCallbacks extends
+            androidx.recyclerview.selection.MouseCallbacks {
+
+        private final Context mContext;
+
+        MouseCallbacks(Context context) {
+            mContext = context;
+        }
+
+        @Override
+        public boolean onContextClick(MotionEvent e) {
+            toast(mContext, "Context click received.");
+            return true;
+        }
+    };
+
+    private static final class TouchCallbacks extends
+            androidx.recyclerview.selection.TouchCallbacks {
+
+        private final Context mContext;
+
+        private TouchCallbacks(Context context) {
+            mContext = context;
+        }
+
+        public boolean onDragInitiated(MotionEvent e) {
+            toast(mContext, "onDragInitiated received.");
+            return true;
+        }
+    }
+}
diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/simple/SimpleSelectionDemoAdapter.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/simple/SimpleSelectionDemoAdapter.java
new file mode 100644
index 0000000..a60fda8
--- /dev/null
+++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/selection/simple/SimpleSelectionDemoAdapter.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 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 com.example.android.supportv7.widget.selection.simple;
+
+import static android.support.v4.util.Preconditions.checkArgument;
+
+import android.content.Context;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+
+import com.example.android.supportv7.Cheeses;
+import com.example.android.supportv7.R;
+
+import androidx.recyclerview.selection.ItemKeyProvider;
+import androidx.recyclerview.selection.SelectionHelper;
+
+final class SimpleSelectionDemoAdapter extends RecyclerView.Adapter<DemoHolder> {
+
+    private static final String TAG = "SelectionDemos";
+    private final Context mContext;
+    private final ItemKeyProvider<Long> mKeyProvider;
+
+    // This should be replaced at "bind" time with a real test that
+    // asks SelectionHelper.
+    private SelectionTest mSelTest;
+
+    SimpleSelectionDemoAdapter(Context context, ItemKeyProvider<Long> keyProvider) {
+        mContext = context;
+        mKeyProvider = keyProvider;
+        mSelTest = new SelectionTest() {
+            @Override
+            public boolean isSelected(Long id) {
+                throw new IllegalStateException(
+                        "Adapter must be initialized with SelectionHelper.");
+            }
+        };
+    }
+
+    // Glue together SelectionHelper and the adapter.
+    public void bindSelectionHelper(final SelectionHelper<Long> selectionHelper) {
+        checkArgument(selectionHelper != null);
+        mSelTest = new SelectionTest() {
+            @Override
+            public boolean isSelected(Long id) {
+                return selectionHelper.isSelected(id);
+            }
+        };
+    }
+
+    void loadData() {
+        onDataReady();
+    }
+
+    private void onDataReady() {
+        notifyDataSetChanged();
+    }
+
+    @Override
+    public int getItemCount() {
+        return Cheeses.sCheeseStrings.length;
+    }
+
+    @Override
+    public long getItemId(int position) {
+        return position;
+    }
+
+    @Override
+    public void onBindViewHolder(DemoHolder holder, int position) {
+        Long key = getItemId(position);
+        Log.v(TAG, "Just before rendering item position=" + position + ", key=" + key);
+        holder.update(Cheeses.sCheeseStrings[position], mSelTest.isSelected(key));
+    }
+
+    @Override
+    public DemoHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+        LinearLayout layout = inflateLayout(mContext, parent, R.layout.selection_demo_list_item);
+        return new DemoHolder(layout);
+    }
+
+    @SuppressWarnings("TypeParameterUnusedInFormals")  // Convenience to avoid clumsy cast.
+    private static <V extends View> V inflateLayout(
+            Context context, ViewGroup parent, int layout) {
+
+        return (V) LayoutInflater.from(context).inflate(layout, parent, false);
+    }
+
+    private interface SelectionTest {
+        boolean isSelected(Long id);
+    }
+}
diff --git a/samples/Support7Demos/src/main/res/color/selection_demo_item_selector.xml b/samples/Support7Demos/src/main/res/color/selection_demo_item_selector.xml
new file mode 100644
index 0000000..bd87b4c
--- /dev/null
+++ b/samples/Support7Demos/src/main/res/color/selection_demo_item_selector.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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item
+        android:state_activated="true"
+        android:color="?android:attr/colorForeground"
+        />
+    <item
+        android:state_activated="false"
+        android:color="?android:attr/colorForeground"
+        android:alpha=".3"
+        />
+</selector>
diff --git a/samples/Support7Demos/src/main/res/drawable/selection_demo_band_overlay.xml b/samples/Support7Demos/src/main/res/drawable/selection_demo_band_overlay.xml
new file mode 100644
index 0000000..f9793aa
--- /dev/null
+++ b/samples/Support7Demos/src/main/res/drawable/selection_demo_band_overlay.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
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+        android:shape="rectangle">
+    <solid android:color="#22FF0000" />
+    <stroke android:width="1dp" android:color="#44FF0000" />
+</shape>
diff --git a/samples/Support7Demos/src/main/res/drawable/selection_demo_item_background.xml b/samples/Support7Demos/src/main/res/drawable/selection_demo_item_background.xml
new file mode 100644
index 0000000..e4dbd5f
--- /dev/null
+++ b/samples/Support7Demos/src/main/res/drawable/selection_demo_item_background.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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_activated="true">
+        <color android:color="#220000FF"></color>
+    </item>
+</selector>
diff --git a/samples/Support7Demos/src/main/res/layout/selection_demo_layout.xml b/samples/Support7Demos/src/main/res/layout/selection_demo_layout.xml
new file mode 100644
index 0000000..bd85a14
--- /dev/null
+++ b/samples/Support7Demos/src/main/res/layout/selection_demo_layout.xml
@@ -0,0 +1,34 @@
+<?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:orientation="vertical">
+
+    <android.support.v7.widget.RecyclerView
+        android:id="@+id/list"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:clipToPadding="false"
+        android:drawSelectorOnTop="true"
+        android:paddingBottom="5dp"
+        android:paddingEnd="0dp"
+        android:paddingStart="0dp"
+        android:paddingTop="5dp"
+        android:scrollbars="none" />
+
+</LinearLayout>
diff --git a/samples/Support7Demos/src/main/res/layout/selection_demo_list_item.xml b/samples/Support7Demos/src/main/res/layout/selection_demo_list_item.xml
new file mode 100644
index 0000000..0d4b718
--- /dev/null
+++ b/samples/Support7Demos/src/main/res/layout/selection_demo_list_item.xml
@@ -0,0 +1,53 @@
+<?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:paddingStart="10dp"
+    android:paddingEnd="10dp"
+    android:paddingTop="5dp"
+    android:paddingBottom="5dp"
+    android:layout_height="50dp">
+  <LinearLayout
+      android:id="@+id/container"
+      xmlns:android="http://schemas.android.com/apk/res/android"
+      android:layout_height="match_parent"
+      android:layout_width="match_parent"
+      android:background="@drawable/selection_demo_item_background">
+      <TextView
+          android:id="@+id/selector"
+          android:textSize="20sp"
+          android:textStyle="bold"
+          android:gravity="center"
+          android:layout_height="match_parent"
+          android:layout_width="40dp"
+          android:textColor="@color/selection_demo_item_selector"
+          android:pointerIcon="hand"
+          android:text="✕">
+      </TextView>
+      <TextView
+          android:id="@+id/label"
+          android:textSize="20sp"
+          android:textStyle="bold"
+          android:gravity="center_vertical"
+          android:paddingStart="10dp"
+          android:paddingEnd="10dp"
+          android:layout_height="match_parent"
+          android:layout_width="match_parent">
+      </TextView>
+  </LinearLayout>
+</LinearLayout>
diff --git a/samples/Support7Demos/src/main/res/menu/selection_demo_actions.xml b/samples/Support7Demos/src/main/res/menu/selection_demo_actions.xml
new file mode 100644
index 0000000..484d8b6
--- /dev/null
+++ b/samples/Support7Demos/src/main/res/menu/selection_demo_actions.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.
+-->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+   <item
+       android:id="@+id/option_menu_add_column"
+       android:title="Add column" />
+   <item
+       android:id="@+id/option_menu_remove_column"
+       android:title="Remove column" />
+</menu>
diff --git a/samples/Support7Demos/src/main/res/values/strings.xml b/samples/Support7Demos/src/main/res/values/strings.xml
index 097807f..2c4b0b6 100644
--- a/samples/Support7Demos/src/main/res/values/strings.xml
+++ b/samples/Support7Demos/src/main/res/values/strings.xml
@@ -233,10 +233,12 @@
     <string name="popup_menu_print">Print</string>
 
     <string name="list_view_activity">AppCompat/ListView styling</string>
-
     <string name="appcompat_vector_disabled">AnimatedVectorDrawableCompat does not work on devices running API v10 or below</string>
     <string name="appcompat_vector_title">AppCompat/Integrations/AnimatedVectorDrawable</string>
 
+    <string name="simple_selection_demo_activity">RecyclerView Selection</string>
+    <string name="fancy_selection_demo_activity">RecyclerView: Gesture+Pointer Selection</string>
+
     <string name="night_mode">DAY</string>
 
     <string name="text_plain_enabled">Plain enabled</string>
@@ -246,4 +248,3 @@
 
     <string name="menu_item_icon_tinting">AppCompat/Menu Item Icons</string>
 </resources>
-
diff --git a/samples/SupportEmojiDemos/OWNERS b/samples/SupportEmojiDemos/OWNERS
new file mode 100644
index 0000000..a2db8f4
--- /dev/null
+++ b/samples/SupportEmojiDemos/OWNERS
@@ -0,0 +1 @@
+siyamed@google.com
\ No newline at end of file
diff --git a/v17/leanback/OWNERS b/samples/SupportLeanbackDemos/OWNERS
similarity index 100%
copy from v17/leanback/OWNERS
copy to samples/SupportLeanbackDemos/OWNERS
diff --git a/samples/SupportLeanbackDemos/generatev4.py b/samples/SupportLeanbackDemos/generatev4.py
index 6a44e17..3ffec76 100755
--- a/samples/SupportLeanbackDemos/generatev4.py
+++ b/samples/SupportLeanbackDemos/generatev4.py
@@ -25,8 +25,8 @@
 def replace_xml_head(line, name):
     return line.replace('<?xml version="1.0" encoding="utf-8"?>', '<?xml version="1.0" encoding="utf-8"?>\n<!-- This file is auto-generated from {}.xml.  DO NOT MODIFY. -->\n'.format(name))
 
-file = open('src/com/example/android/leanback/GuidedStepActivity.java', 'r')
-outfile = open('src/com/example/android/leanback/GuidedStepSupportActivity.java', 'w')
+file = open('src/main/java/com/example/android/leanback/GuidedStepActivity.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/GuidedStepSupportActivity.java', 'w')
 write_java_head(outfile, "GuidedStepActivity")
 for line in file:
     line = line.replace('android.app.Fragment', 'android.support.v4.app.Fragment')
@@ -38,8 +38,8 @@
 file.close()
 outfile.close()
 
-file = open('src/com/example/android/leanback/GuidedStepHalfScreenActivity.java', 'r')
-outfile = open('src/com/example/android/leanback/GuidedStepSupportHalfScreenActivity.java', 'w')
+file = open('src/main/java/com/example/android/leanback/GuidedStepHalfScreenActivity.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/GuidedStepSupportHalfScreenActivity.java', 'w')
 write_java_head(outfile, "GuidedStepHalfScreenActivity")
 for line in file:
     line = line.replace('android.app.Fragment', 'android.support.v4.app.Fragment')
@@ -52,8 +52,8 @@
 file.close()
 outfile.close()
 
-file = open('src/com/example/android/leanback/BrowseFragment.java', 'r')
-outfile = open('src/com/example/android/leanback/BrowseSupportFragment.java', 'w')
+file = open('src/main/java/com/example/android/leanback/BrowseFragment.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/BrowseSupportFragment.java', 'w')
 write_java_head(outfile, "BrowseFragment")
 for line in file:
     line = line.replace('android.app.Fragment', 'android.support.v4.app.Fragment')
@@ -72,8 +72,8 @@
 file.close()
 outfile.close()
 
-file = open('src/com/example/android/leanback/BrowseActivity.java', 'r')
-outfile = open('src/com/example/android/leanback/BrowseSupportActivity.java', 'w')
+file = open('src/main/java/com/example/android/leanback/BrowseActivity.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/BrowseSupportActivity.java', 'w')
 write_java_head(outfile, "BrowseActivity")
 for line in file:
     line = line.replace('BrowseActivity', 'BrowseSupportActivity')
@@ -84,8 +84,8 @@
 file.close()
 outfile.close()
 
-file = open('res/layout/browse.xml', 'r')
-outfile = open('res/layout/browse_support.xml', 'w')
+file = open('src/main/res/layout/browse.xml', 'r')
+outfile = open('src/main/res/layout/browse_support.xml', 'w')
 for line in file:
     line = replace_xml_head(line, "browse")
     line = line.replace('com.example.android.leanback.BrowseFragment', 'com.example.android.leanback.BrowseSupportFragment')
@@ -94,8 +94,8 @@
 outfile.close()
 
 
-file = open('src/com/example/android/leanback/DetailsFragment.java', 'r')
-outfile = open('src/com/example/android/leanback/DetailsSupportFragment.java', 'w')
+file = open('src/main/java/com/example/android/leanback/DetailsFragment.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/DetailsSupportFragment.java', 'w')
 write_java_head(outfile, "DetailsFragment")
 for line in file:
     line = line.replace('android.app.Fragment', 'android.support.v4.app.Fragment')
@@ -108,8 +108,8 @@
 file.close()
 outfile.close()
 
-file = open('src/com/example/android/leanback/NewDetailsFragment.java', 'r')
-outfile = open('src/com/example/android/leanback/NewDetailsSupportFragment.java', 'w')
+file = open('src/main/java/com/example/android/leanback/NewDetailsFragment.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/NewDetailsSupportFragment.java', 'w')
 write_java_head(outfile, "NewDetailsFragment")
 for line in file:
     line = line.replace('android.app.Fragment', 'android.support.v4.app.Fragment')
@@ -127,8 +127,8 @@
 file.close()
 outfile.close()
 
-file = open('src/com/example/android/leanback/DetailsActivity.java', 'r')
-outfile = open('src/com/example/android/leanback/DetailsSupportActivity.java', 'w')
+file = open('src/main/java/com/example/android/leanback/DetailsActivity.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/DetailsSupportActivity.java', 'w')
 write_java_head(outfile, "DetailsActivity")
 for line in file:
     line = line.replace('DetailsActivity', 'DetailsSupportActivity')
@@ -141,8 +141,8 @@
 file.close()
 outfile.close()
 
-file = open('src/com/example/android/leanback/SearchDetailsActivity.java', 'r')
-outfile = open('src/com/example/android/leanback/SearchDetailsSupportActivity.java', 'w')
+file = open('src/main/java/com/example/android/leanback/SearchDetailsActivity.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/SearchDetailsSupportActivity.java', 'w')
 write_java_head(outfile, "SearchDetailsActivity")
 for line in file:
     line = line.replace('DetailsActivity', 'DetailsSupportActivity')
@@ -151,8 +151,8 @@
 outfile.close()
 
 
-file = open('src/com/example/android/leanback/SearchFragment.java', 'r')
-outfile = open('src/com/example/android/leanback/SearchSupportFragment.java', 'w')
+file = open('src/main/java/com/example/android/leanback/SearchFragment.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/SearchSupportFragment.java', 'w')
 write_java_head(outfile, "SearchFragment")
 for line in file:
     line = line.replace('SearchFragment', 'SearchSupportFragment')
@@ -161,8 +161,8 @@
 file.close()
 outfile.close()
 
-file = open('src/com/example/android/leanback/SearchActivity.java', 'r')
-outfile = open('src/com/example/android/leanback/SearchSupportActivity.java', 'w')
+file = open('src/main/java/com/example/android/leanback/SearchActivity.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/SearchSupportActivity.java', 'w')
 write_java_head(outfile, "SearchActivity")
 for line in file:
     line = line.replace('SearchActivity', 'SearchSupportActivity')
@@ -175,8 +175,8 @@
 file.close()
 outfile.close()
 
-file = open('res/layout/search.xml', 'r')
-outfile = open('res/layout/search_support.xml', 'w')
+file = open('src/main/res/layout/search.xml', 'r')
+outfile = open('src/main/res/layout/search_support.xml', 'w')
 for line in file:
     line = replace_xml_head(line, "search")
     line = line.replace('com.example.android.leanback.SearchFragment', 'com.example.android.leanback.SearchSupportFragment')
@@ -184,8 +184,8 @@
 file.close()
 outfile.close()
 
-file = open('src/com/example/android/leanback/VerticalGridFragment.java', 'r')
-outfile = open('src/com/example/android/leanback/VerticalGridSupportFragment.java', 'w')
+file = open('src/main/java/com/example/android/leanback/VerticalGridFragment.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/VerticalGridSupportFragment.java', 'w')
 write_java_head(outfile, "VerticalGridFragment")
 for line in file:
     line = line.replace('VerticalGridFragment', 'VerticalGridSupportFragment')
@@ -195,8 +195,8 @@
 file.close()
 outfile.close()
 
-file = open('src/com/example/android/leanback/VerticalGridActivity.java', 'r')
-outfile = open('src/com/example/android/leanback/VerticalGridSupportActivity.java', 'w')
+file = open('src/main/java/com/example/android/leanback/VerticalGridActivity.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/VerticalGridSupportActivity.java', 'w')
 write_java_head(outfile, "VerticalGridActivity")
 for line in file:
     line = line.replace('VerticalGridActivity', 'VerticalGridSupportActivity')
@@ -209,8 +209,8 @@
 file.close()
 outfile.close()
 
-file = open('res/layout/vertical_grid.xml', 'r')
-outfile = open('res/layout/vertical_grid_support.xml', 'w')
+file = open('src/main/res/layout/vertical_grid.xml', 'r')
+outfile = open('src/main/res/layout/vertical_grid_support.xml', 'w')
 for line in file:
     line = replace_xml_head(line, "vertical_grid")
     line = line.replace('com.example.android.leanback.VerticalGridFragment', 'com.example.android.leanback.VerticalGridSupportFragment')
@@ -219,8 +219,8 @@
 outfile.close()
 
 
-file = open('src/com/example/android/leanback/ErrorFragment.java', 'r')
-outfile = open('src/com/example/android/leanback/ErrorSupportFragment.java', 'w')
+file = open('src/main/java/com/example/android/leanback/ErrorFragment.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/ErrorSupportFragment.java', 'w')
 write_java_head(outfile, "ErrorFragment")
 for line in file:
     line = line.replace('ErrorFragment', 'ErrorSupportFragment')
@@ -228,8 +228,8 @@
 file.close()
 outfile.close()
 
-file = open('src/com/example/android/leanback/BrowseErrorActivity.java', 'r')
-outfile = open('src/com/example/android/leanback/BrowseErrorSupportActivity.java', 'w')
+file = open('src/main/java/com/example/android/leanback/BrowseErrorActivity.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/BrowseErrorSupportActivity.java', 'w')
 write_java_head(outfile, "BrowseErrorActivity")
 for line in file:
     line = line.replace('BrowseErrorActivity', 'BrowseErrorSupportActivity')
@@ -244,8 +244,8 @@
 file.close()
 outfile.close()
 
-file = open('src/com/example/android/leanback/RowsFragment.java', 'r')
-outfile = open('src/com/example/android/leanback/RowsSupportFragment.java', 'w')
+file = open('src/main/java/com/example/android/leanback/RowsFragment.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/RowsSupportFragment.java', 'w')
 write_java_head(outfile, "RowsFragment")
 for line in file:
     line = line.replace('RowsFragment', 'RowsSupportFragment')
@@ -254,8 +254,8 @@
 file.close()
 outfile.close()
 
-file = open('src/com/example/android/leanback/RowsActivity.java', 'r')
-outfile = open('src/com/example/android/leanback/RowsSupportActivity.java', 'w')
+file = open('src/main/java/com/example/android/leanback/RowsActivity.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/RowsSupportActivity.java', 'w')
 write_java_head(outfile, "RowsActivity")
 for line in file:
     line = line.replace('RowsActivity', 'RowsSupportActivity')
@@ -269,8 +269,8 @@
 file.close()
 outfile.close()
 
-file = open('res/layout/rows.xml', 'r')
-outfile = open('res/layout/rows_support.xml', 'w')
+file = open('src/main/res/layout/rows.xml', 'r')
+outfile = open('src/main/res/layout/rows_support.xml', 'w')
 for line in file:
     line = replace_xml_head(line, "rows")
     line = line.replace('com.example.android.leanback.RowsFragment', 'com.example.android.leanback.RowsSupportFragment')
@@ -278,8 +278,8 @@
 file.close()
 outfile.close()
 
-file = open('src/com/example/android/leanback/PlaybackFragment.java', 'r')
-outfile = open('src/com/example/android/leanback/PlaybackSupportFragment.java', 'w')
+file = open('src/main/java/com/example/android/leanback/PlaybackFragment.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/PlaybackSupportFragment.java', 'w')
 write_java_head(outfile, "PlaybackFragment")
 for line in file:
     line = line.replace('PlaybackFragment', 'PlaybackSupportFragment')
@@ -288,8 +288,8 @@
 file.close()
 outfile.close()
 
-file = open('src/com/example/android/leanback/PlaybackActivity.java', 'r')
-outfile = open('src/com/example/android/leanback/PlaybackSupportActivity.java', 'w')
+file = open('src/main/java/com/example/android/leanback/PlaybackActivity.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/PlaybackSupportActivity.java', 'w')
 write_java_head(outfile, "PlaybackActivity")
 for line in file:
     line = line.replace('PlaybackActivity', 'PlaybackSupportActivity')
@@ -300,8 +300,8 @@
 file.close()
 outfile.close()
 
-file = open('res/layout/playback_activity.xml', 'r')
-outfile = open('res/layout/playback_activity_support.xml', 'w')
+file = open('src/main/res/layout/playback_activity.xml', 'r')
+outfile = open('src/main/res/layout/playback_activity_support.xml', 'w')
 for line in file:
     line = replace_xml_head(line, "playback_controls")
     line = line.replace('com.example.android.leanback.PlaybackFragment', 'com.example.android.leanback.PlaybackSupportFragment')
@@ -309,8 +309,8 @@
 file.close()
 outfile.close()
 
-file = open('src/com/example/android/leanback/PlaybackTransportControlFragment.java', 'r')
-outfile = open('src/com/example/android/leanback/PlaybackTransportControlSupportFragment.java', 'w')
+file = open('src/main/java/com/example/android/leanback/PlaybackTransportControlFragment.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/PlaybackTransportControlSupportFragment.java', 'w')
 write_java_head(outfile, "PlaybackTransportControlFragment")
 for line in file:
     line = line.replace('PlaybackFragment', 'PlaybackSupportFragment')
@@ -320,8 +320,8 @@
 file.close()
 outfile.close()
 
-file = open('src/com/example/android/leanback/PlaybackTransportControlActivity.java', 'r')
-outfile = open('src/com/example/android/leanback/PlaybackTransportControlSupportActivity.java', 'w')
+file = open('src/main/java/com/example/android/leanback/PlaybackTransportControlActivity.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/PlaybackTransportControlSupportActivity.java', 'w')
 write_java_head(outfile, "PlaybackTransportControlActivity")
 for line in file:
     line = line.replace('PlaybackTransportControlActivity', 'PlaybackTransportControlSupportActivity')
@@ -332,8 +332,8 @@
 file.close()
 outfile.close()
 
-file = open('res/layout/playback_transportcontrol_activity.xml', 'r')
-outfile = open('res/layout/playback_transportcontrol_activity_support.xml', 'w')
+file = open('src/main/res/layout/playback_transportcontrol_activity.xml', 'r')
+outfile = open('src/main/res/layout/playback_transportcontrol_activity_support.xml', 'w')
 for line in file:
     line = replace_xml_head(line, "playback_transportcontrols")
     line = line.replace('com.example.android.leanback.PlaybackTransportControlFragment', 'com.example.android.leanback.PlaybackTransportControlSupportFragment')
@@ -341,45 +341,8 @@
 file.close()
 outfile.close()
 
-
-
-file = open('src/com/example/android/leanback/PlaybackOverlayFragment.java', 'r')
-outfile = open('src/com/example/android/leanback/PlaybackOverlaySupportFragment.java', 'w')
-write_java_head(outfile, "PlaybackOverlayFragment")
-for line in file:
-    line = line.replace('PlaybackOverlayFragment', 'PlaybackOverlaySupportFragment')
-    line = line.replace('PlaybackControlHelper', 'PlaybackControlSupportHelper')
-    line = line.replace('PlaybackOverlayActivity', 'PlaybackOverlaySupportActivity')
-    outfile.write(line)
-file.close()
-outfile.close()
-
-
-file = open('src/com/example/android/leanback/PlaybackControlHelper.java', 'r')
-outfile = open('src/com/example/android/leanback/PlaybackControlSupportHelper.java', 'w')
-write_java_head(outfile, "PlaybackControlHelper")
-for line in file:
-    line = line.replace('PlaybackControlHelper', 'PlaybackControlSupportHelper')
-    line = line.replace('PlaybackControlGlue', 'PlaybackControlSupportGlue')
-    line = line.replace('PlaybackOverlayFragment', 'PlaybackOverlaySupportFragment')
-    outfile.write(line)
-file.close()
-outfile.close()
-
-file = open('src/com/example/android/leanback/PlaybackOverlayActivity.java', 'r')
-outfile = open('src/com/example/android/leanback/PlaybackOverlaySupportActivity.java', 'w')
-write_java_head(outfile, "PlaybackOverlayActivity")
-for line in file:
-    line = line.replace('PlaybackOverlayActivity', 'PlaybackOverlaySupportActivity')
-    line = line.replace('extends Activity', 'extends FragmentActivity')
-    line = line.replace('R.layout.playback_controls', 'R.layout.playback_controls_support')
-    line = line.replace('android.app.Activity', 'android.support.v4.app.FragmentActivity')
-    outfile.write(line)
-file.close()
-outfile.close()
-
-file = open('res/layout/playback_controls.xml', 'r')
-outfile = open('res/layout/playback_controls_support.xml', 'w')
+file = open('src/main/res/layout/playback_controls.xml', 'r')
+outfile = open('src/main/res/layout/playback_controls_support.xml', 'w')
 for line in file:
     line = replace_xml_head(line, "playback_controls")
     line = line.replace('com.example.android.leanback.PlaybackOverlayFragment', 'com.example.android.leanback.PlaybackOverlaySupportFragment')
@@ -387,8 +350,8 @@
 file.close()
 outfile.close()
 
-file = open('src/com/example/android/leanback/OnboardingActivity.java', 'r')
-outfile = open('src/com/example/android/leanback/OnboardingSupportActivity.java', 'w')
+file = open('src/main/java/com/example/android/leanback/OnboardingActivity.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/OnboardingSupportActivity.java', 'w')
 write_java_head(outfile, "OnboardingActivity")
 for line in file:
     line = line.replace('android.app.Fragment', 'android.support.v4.app.Fragment')
@@ -401,8 +364,8 @@
 file.close()
 outfile.close()
 
-file = open('src/com/example/android/leanback/OnboardingDemoFragment.java', 'r')
-outfile = open('src/com/example/android/leanback/OnboardingDemoSupportFragment.java', 'w')
+file = open('src/main/java/com/example/android/leanback/OnboardingDemoFragment.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/OnboardingDemoSupportFragment.java', 'w')
 write_java_head(outfile, "OnboardingDemoFragment")
 for line in file:
     line = line.replace('android.app.Fragment', 'android.support.v4.app.Fragment')
@@ -414,8 +377,8 @@
 file.close()
 outfile.close()
 
-file = open('src/com/example/android/leanback/SampleVideoFragment.java', 'r')
-outfile = open('src/com/example/android/leanback/SampleVideoSupportFragment.java', 'w')
+file = open('src/main/java/com/example/android/leanback/SampleVideoFragment.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/SampleVideoSupportFragment.java', 'w')
 write_java_head(outfile, "OnboardingDemoFragment")
 for line in file:
     line = line.replace('android.app.Fragment', 'android.support.v4.app.Fragment')
@@ -426,8 +389,8 @@
 file.close()
 outfile.close()
 
-file = open('src/com/example/android/leanback/VideoActivity.java', 'r')
-outfile = open('src/com/example/android/leanback/VideoSupportActivity.java', 'w')
+file = open('src/main/java/com/example/android/leanback/VideoActivity.java', 'r')
+outfile = open('src/main/java/com/example/android/leanback/VideoSupportActivity.java', 'w')
 write_java_head(outfile, "OnboardingDemoFragment")
 for line in file:
     line = line.replace('android.app.Fragment', 'android.support.v4.app.Fragment')
diff --git a/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/BrowseFragment.java b/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/BrowseFragment.java
index 7b3f8f7..eb0b684 100644
--- a/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/BrowseFragment.java
+++ b/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/BrowseFragment.java
@@ -144,7 +144,6 @@
         ListRowPresenter listRowPresenter = new ListRowPresenter();
         listRowPresenter.setNumRows(1);
         mRowsAdapter = new ArrayObjectAdapter(listRowPresenter);
-        setAdapter(mRowsAdapter);
     }
 
     private void loadData() {
@@ -164,6 +163,7 @@
         mRowsAdapter.add(new PageRow(new HeaderItem(HEADER_ID2, "Page Row 1")));
 
         mRowsAdapter.add(new PageRow(new HeaderItem(HEADER_ID3, "Page Row 2")));
+        setAdapter(mRowsAdapter);
     }
 
     private ArrayObjectAdapter createListRowAdapter(int i) {
@@ -273,7 +273,7 @@
         final CardPresenter mCardPresenter2 = new CardPresenter(R.style.MyImageCardViewTheme);
 
         void loadFragmentData() {
-            ArrayObjectAdapter adapter = (ArrayObjectAdapter) getAdapter();
+            ArrayObjectAdapter adapter = new ArrayObjectAdapter(new ListRowPresenter());
             for (int i = 0; i < 4; i++) {
                 ListRow row = new ListRow(new HeaderItem("Row " + i), createListRowAdapter(i));
                 adapter.add(row);
@@ -282,11 +282,10 @@
                 getMainFragmentAdapter().getFragmentHost()
                         .notifyDataReady(getMainFragmentAdapter());
             }
+            setAdapter(adapter);
         }
 
         public SampleRowsFragment() {
-            ArrayObjectAdapter adapter = new ArrayObjectAdapter(new ListRowPresenter());
-            setAdapter(adapter);
             // simulates late data loading:
             new Handler().postDelayed(new Runnable() {
                 @Override
diff --git a/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/BrowseSupportFragment.java b/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/BrowseSupportFragment.java
index 395c498..7afd24f 100644
--- a/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/BrowseSupportFragment.java
+++ b/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/BrowseSupportFragment.java
@@ -147,7 +147,6 @@
         ListRowPresenter listRowPresenter = new ListRowPresenter();
         listRowPresenter.setNumRows(1);
         mRowsAdapter = new ArrayObjectAdapter(listRowPresenter);
-        setAdapter(mRowsAdapter);
     }
 
     private void loadData() {
@@ -167,6 +166,7 @@
         mRowsAdapter.add(new PageRow(new HeaderItem(HEADER_ID2, "Page Row 1")));
 
         mRowsAdapter.add(new PageRow(new HeaderItem(HEADER_ID3, "Page Row 2")));
+        setAdapter(mRowsAdapter);
     }
 
     private ArrayObjectAdapter createListRowAdapter(int i) {
@@ -276,7 +276,7 @@
         final CardPresenter mCardPresenter2 = new CardPresenter(R.style.MyImageCardViewTheme);
 
         void loadFragmentData() {
-            ArrayObjectAdapter adapter = (ArrayObjectAdapter) getAdapter();
+            ArrayObjectAdapter adapter = new ArrayObjectAdapter(new ListRowPresenter());
             for (int i = 0; i < 4; i++) {
                 ListRow row = new ListRow(new HeaderItem("Row " + i), createListRowAdapter(i));
                 adapter.add(row);
@@ -285,11 +285,10 @@
                 getMainFragmentAdapter().getFragmentHost()
                         .notifyDataReady(getMainFragmentAdapter());
             }
+            setAdapter(adapter);
         }
 
         public SampleRowsSupportFragment() {
-            ArrayObjectAdapter adapter = new ArrayObjectAdapter(new ListRowPresenter());
-            setAdapter(adapter);
             // simulates late data loading:
             new Handler().postDelayed(new Runnable() {
                 @Override
diff --git a/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/DetailsSupportFragment.java b/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/DetailsSupportFragment.java
index 0f15590..1af248f 100644
--- a/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/DetailsSupportFragment.java
+++ b/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/DetailsSupportFragment.java
@@ -119,7 +119,7 @@
                     actions.clear(ACTION_RENT);
                     dor.setItem(mPhotoItem.getTitle() + "(Rented)");
                 } else if (action.getId() == ACTION_PLAY) {
-                    Intent intent = new Intent(context, PlaybackSupportActivity.class);
+                    Intent intent = new Intent(context, PlaybackActivity.class);
                     getActivity().startActivity(intent);
                 }
             }
diff --git a/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/GuidedStepActivity.java b/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/GuidedStepActivity.java
index 7f898f4..7d20046 100644
--- a/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/GuidedStepActivity.java
+++ b/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/GuidedStepActivity.java
@@ -55,6 +55,7 @@
     private static final int PAYMENT = 6;
     private static final int NEW_PAYMENT = 7;
     private static final int PAYMENT_EXPIRE = 8;
+    private static final int REFRESH = 9;
 
     private static final long RADIO_ID_BASE = 0;
     private static final long CHECKBOX_ID_BASE = 100;
@@ -222,6 +223,10 @@
                     .description("Let's do it")
                     .build());
             actions.add(new GuidedAction.Builder(context)
+                    .id(REFRESH)
+                    .title("Refresh")
+                    .build());
+            actions.add(new GuidedAction.Builder(context)
                     .clickAction(GuidedAction.ACTION_ID_CANCEL)
                     .description("Never mind")
                     .build());
@@ -232,6 +237,24 @@
             FragmentManager fm = getFragmentManager();
             if (action.getId() == GuidedAction.ACTION_ID_CONTINUE) {
                 GuidedStepFragment.add(fm, new SecondStepFragment(), R.id.lb_guidedstep_host);
+            } else if (action.getId() == REFRESH) {
+                // swap actions position and change content:
+                Context context = getActivity();
+                ArrayList<GuidedAction> newActions = new ArrayList();
+                newActions.add(new GuidedAction.Builder(context)
+                        .id(REFRESH)
+                        .title("Refresh done")
+                        .build());
+                newActions.add(new GuidedAction.Builder(context)
+                        .clickAction(GuidedAction.ACTION_ID_CONTINUE)
+                        .description("Let's do it")
+                        .build());
+                newActions.add(new GuidedAction.Builder(context)
+                        .clickAction(GuidedAction.ACTION_ID_CANCEL)
+                        .description("Never mind")
+                        .build());
+                //setActionsDiffCallback(null);
+                setActions(newActions);
             } else if (action.getId() == GuidedAction.ACTION_ID_CANCEL){
                 finishGuidedStepFragments();
             }
diff --git a/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/GuidedStepSupportActivity.java b/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/GuidedStepSupportActivity.java
index c0f9361..6782b63 100644
--- a/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/GuidedStepSupportActivity.java
+++ b/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/GuidedStepSupportActivity.java
@@ -58,6 +58,7 @@
     private static final int PAYMENT = 6;
     private static final int NEW_PAYMENT = 7;
     private static final int PAYMENT_EXPIRE = 8;
+    private static final int REFRESH = 9;
 
     private static final long RADIO_ID_BASE = 0;
     private static final long CHECKBOX_ID_BASE = 100;
@@ -225,6 +226,10 @@
                     .description("Let's do it")
                     .build());
             actions.add(new GuidedAction.Builder(context)
+                    .id(REFRESH)
+                    .title("Refresh")
+                    .build());
+            actions.add(new GuidedAction.Builder(context)
                     .clickAction(GuidedAction.ACTION_ID_CANCEL)
                     .description("Never mind")
                     .build());
@@ -235,6 +240,24 @@
             FragmentManager fm = getFragmentManager();
             if (action.getId() == GuidedAction.ACTION_ID_CONTINUE) {
                 GuidedStepSupportFragment.add(fm, new SecondStepFragment(), R.id.lb_guidedstep_host);
+            } else if (action.getId() == REFRESH) {
+                // swap actions position and change content:
+                Context context = getActivity();
+                ArrayList<GuidedAction> newActions = new ArrayList();
+                newActions.add(new GuidedAction.Builder(context)
+                        .id(REFRESH)
+                        .title("Refresh done")
+                        .build());
+                newActions.add(new GuidedAction.Builder(context)
+                        .clickAction(GuidedAction.ACTION_ID_CONTINUE)
+                        .description("Let's do it")
+                        .build());
+                newActions.add(new GuidedAction.Builder(context)
+                        .clickAction(GuidedAction.ACTION_ID_CANCEL)
+                        .description("Never mind")
+                        .build());
+                //setActionsDiffCallback(null);
+                setActions(newActions);
             } else if (action.getId() == GuidedAction.ACTION_ID_CANCEL){
                 finishGuidedStepSupportFragments();
             }
diff --git a/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/NewDetailsSupportFragment.java b/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/NewDetailsSupportFragment.java
index 6002cf3..b2ff5b2 100644
--- a/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/NewDetailsSupportFragment.java
+++ b/samples/SupportLeanbackDemos/src/main/java/com/example/android/leanback/NewDetailsSupportFragment.java
@@ -178,7 +178,7 @@
                             mDetailsBackground.switchToVideo();
                         }
                     } else {
-                        Intent intent = new Intent(context, PlaybackSupportActivity.class);
+                        Intent intent = new Intent(context, PlaybackActivity.class);
                         getActivity().startActivity(intent);
                     }
                 } else if (action.getId() == ACTION_RENT) {
@@ -193,14 +193,14 @@
                         setupMainVideo();
                         mDetailsBackground.switchToVideo();
                     } else {
-                        Intent intent = new Intent(context, PlaybackSupportActivity.class);
+                        Intent intent = new Intent(context, PlaybackActivity.class);
                         getActivity().startActivity(intent);
                     }
                 } else if (action.getId() == ACTION_PLAY) {
                     if (TEST_BACKGROUND_PLAYER) {
                         mDetailsBackground.switchToVideo();
                     } else {
-                        Intent intent = new Intent(context, PlaybackSupportActivity.class);
+                        Intent intent = new Intent(context, PlaybackActivity.class);
                         getActivity().startActivity(intent);
                     }
                 }
diff --git a/v17/leanback/OWNERS b/samples/SupportLeanbackJank/OWNERS
similarity index 100%
copy from v17/leanback/OWNERS
copy to samples/SupportLeanbackJank/OWNERS
diff --git a/v17/leanback/OWNERS b/samples/SupportLeanbackShowcase/OWNERS
similarity index 100%
copy from v17/leanback/OWNERS
copy to samples/SupportLeanbackShowcase/OWNERS
diff --git a/samples/SupportLeanbackShowcase/build.gradle b/samples/SupportLeanbackShowcase/build.gradle
index 74f1e76..287a234 100644
--- a/samples/SupportLeanbackShowcase/build.gradle
+++ b/samples/SupportLeanbackShowcase/build.gradle
@@ -18,7 +18,7 @@
         jcenter()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:2.2.1'
+        classpath 'com.android.tools.build:gradle:3.0.0-rc1'
 
         // NOTE: Do not place your application dependencies here; they belong
         // in the individual module build.gradle files
diff --git a/samples/SupportWearDemos/OWNERS b/samples/SupportWearDemos/OWNERS
new file mode 100644
index 0000000..9cd6e52
--- /dev/null
+++ b/samples/SupportWearDemos/OWNERS
@@ -0,0 +1,2 @@
+amad@google.com
+griff@google.com
\ No newline at end of file
diff --git a/samples/SupportWearDemos/build.gradle b/samples/SupportWearDemos/build.gradle
index 99223f9..ae0f195 100644
--- a/samples/SupportWearDemos/build.gradle
+++ b/samples/SupportWearDemos/build.gradle
@@ -18,6 +18,7 @@
 
 dependencies {
     implementation project(':wear')
+    implementation project(path: ':appcompat-v7')
 }
 
 android {
diff --git a/samples/SupportWearDemos/src/main/AndroidManifest.xml b/samples/SupportWearDemos/src/main/AndroidManifest.xml
index 957a539..b70982b 100644
--- a/samples/SupportWearDemos/src/main/AndroidManifest.xml
+++ b/samples/SupportWearDemos/src/main/AndroidManifest.xml
@@ -29,6 +29,8 @@
         <activity android:name=".app.RoundedDrawableDemo" />
         <activity android:name=".app.drawers.WearableDrawersDemo" android:exported="true" />
         <activity android:name=".app.AmbientModeDemo" />
+        <activity android:name=".app.AlertDialogDemo"
+            android:theme="@style/Theme.AppCompat.Light" />
         <activity android:name=".app.MainDemoActivity">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
diff --git a/samples/SupportWearDemos/src/main/java/com/example/android/support/wear/app/AlertDialogDemo.java b/samples/SupportWearDemos/src/main/java/com/example/android/support/wear/app/AlertDialogDemo.java
new file mode 100644
index 0000000..4ea448a
--- /dev/null
+++ b/samples/SupportWearDemos/src/main/java/com/example/android/support/wear/app/AlertDialogDemo.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 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 com.example.android.support.wear.app;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v7.app.AlertDialog;
+import android.view.View;
+import android.widget.Button;
+
+import com.example.android.support.wear.R;
+
+/**
+ * Demo for AlertDialog on Wear.
+ */
+public class AlertDialogDemo extends Activity {
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.alert_dialog_demo);
+
+        AlertDialog v7Dialog = createV7Dialog();
+        android.app.AlertDialog frameworkDialog = createFrameworkDialog();
+
+        Button v7Trigger = findViewById(R.id.v7_dialog_button);
+        v7Trigger.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                v7Dialog.show();
+            }
+        });
+
+        Button frameworkTrigger = findViewById(R.id.framework_dialog_button);
+        frameworkTrigger.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                frameworkDialog.show();
+            }
+        });
+    }
+
+    private AlertDialog createV7Dialog() {
+        return new AlertDialog.Builder(this)
+                .setTitle("AppCompatDialog")
+                .setMessage("Lorem ipsum dolor...")
+                .setPositiveButton("Ok", null)
+                .setNegativeButton("Cancel", null)
+                .create();
+    }
+
+    private android.app.AlertDialog createFrameworkDialog() {
+        return new android.app.AlertDialog.Builder(this)
+                .setTitle("FrameworkDialog")
+                .setMessage("Lorem ipsum dolor...")
+                .setPositiveButton("Ok", null)
+                .setNegativeButton("Cancel", null)
+                .create();
+    }
+}
diff --git a/samples/SupportWearDemos/src/main/java/com/example/android/support/wear/app/MainDemoActivity.java b/samples/SupportWearDemos/src/main/java/com/example/android/support/wear/app/MainDemoActivity.java
index 0227559..3c50d92 100644
--- a/samples/SupportWearDemos/src/main/java/com/example/android/support/wear/app/MainDemoActivity.java
+++ b/samples/SupportWearDemos/src/main/java/com/example/android/support/wear/app/MainDemoActivity.java
@@ -29,7 +29,7 @@
 
 import com.example.android.support.wear.app.drawers.WearableDrawersDemo;
 
-import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.Map;
 
 /**
@@ -51,7 +51,7 @@
     }
 
     private Map<String, Intent> createContentMap() {
-        Map<String, Intent> contentMap = new HashMap<>();
+        Map<String, Intent> contentMap = new LinkedHashMap<>();
         contentMap.put("Wearable Recycler View", new Intent(
                 this, SimpleWearableRecyclerViewDemo.class));
         contentMap.put("Wearable Switch", new Intent(
@@ -64,6 +64,8 @@
                 this, RoundedDrawableDemo.class));
         contentMap.put("Ambient Fragment", new Intent(
                 this, AmbientModeDemo.class));
+        contentMap.put("Alert Dialog (v7)", new Intent(
+                this, AlertDialogDemo.class));
 
         return contentMap;
     }
diff --git a/samples/SupportWearDemos/src/main/res/layout/alert_dialog_demo.xml b/samples/SupportWearDemos/src/main/res/layout/alert_dialog_demo.xml
new file mode 100644
index 0000000..833d489
--- /dev/null
+++ b/samples/SupportWearDemos/src/main/res/layout/alert_dialog_demo.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright 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.wear.widget.BoxInsetLayout
+    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:orientation="vertical">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical"
+        app:boxedEdges="all">
+
+        <Button
+            android:id="@+id/v7_dialog_button"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="Show V7 dialog"/>
+
+        <Button
+            android:id="@+id/framework_dialog_button"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="Show Framework dialog"/>
+    </LinearLayout>
+
+</android.support.wear.widget.BoxInsetLayout>
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index c281bb1..0544c91 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -40,6 +40,9 @@
 include ':recyclerview-v7'
 project(':recyclerview-v7').projectDir = new File(rootDir, 'v7/recyclerview')
 
+include ':recyclerview-selection'
+project(':recyclerview-selection').projectDir = new File(rootDir, 'recyclerview-selection')
+
 include ':cardview-v7'
 project(':cardview-v7').projectDir = new File(rootDir, 'v7/cardview')
 
@@ -50,13 +53,13 @@
 project(':preference-v14').projectDir = new File(rootDir, 'v14/preference')
 
 include ':preference-leanback-v17'
-project(':preference-leanback-v17').projectDir = new File(rootDir, 'v17/preference-leanback')
+project(':preference-leanback-v17').projectDir = new File(rootDir, 'preference-leanback')
 
 include ':support-v13'
 project(':support-v13').projectDir = new File(rootDir, 'v13')
 
 include ':leanback-v17'
-project(':leanback-v17').projectDir = new File(rootDir, 'v17/leanback')
+project(':leanback-v17').projectDir = new File(rootDir, 'leanback')
 
 include ':design'
 project(':design').projectDir = new File(rootDir, 'design')
@@ -103,6 +106,12 @@
 include ':support-content'
 project(':support-content').projectDir = new File(rootDir, 'content')
 
+include ':car'
+project(':car').projectDir = new File(rootDir, 'car')
+
+include ':webkit'
+project(':webkit').projectDir = new File(rootDir, 'webkit')
+
 /////////////////////////////
 //
 // Samples
@@ -172,13 +181,19 @@
 /////////////////////////////
 
 include ':support-media-compat-test-client'
-project(':support-media-compat-test-client').projectDir = new File(rootDir, 'media-compat-test-client')
+project(':support-media-compat-test-client').projectDir = new File(rootDir, 'media-compat/version-compat-tests/current/client')
+
+include ':support-media-compat-test-client-previous'
+project(':support-media-compat-test-client-previous').projectDir = new File(rootDir, 'media-compat/version-compat-tests/previous/client')
 
 include ':support-media-compat-test-service'
-project(':support-media-compat-test-service').projectDir = new File(rootDir, 'media-compat-test-service')
+project(':support-media-compat-test-service').projectDir = new File(rootDir, 'media-compat/version-compat-tests/current/service')
+
+include ':support-media-compat-test-service-previous'
+project(':support-media-compat-test-service-previous').projectDir = new File(rootDir, 'media-compat/version-compat-tests/previous/service')
 
 include ':support-media-compat-test-lib'
-project(':support-media-compat-test-lib').projectDir = new File(rootDir, 'media-compat-test-lib')
+project(':support-media-compat-test-lib').projectDir = new File(rootDir, 'media-compat/version-compat-tests/lib')
 
 /////////////////////////////
 //
diff --git a/testutils/build.gradle b/testutils/build.gradle
index 15eabaf..6ecc012 100644
--- a/testutils/build.gradle
+++ b/testutils/build.gradle
@@ -19,6 +19,13 @@
 }
 
 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 326a681..cd2c237 100644
--- a/transition/build.gradle
+++ b/transition/build.gradle
@@ -24,15 +24,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 99%
rename from transition/src/android/support/transition/Transition.java
rename to transition/src/main/java/android/support/transition/Transition.java
index 04cc57b..9c198a9 100644
--- a/transition/src/android/support/transition/Transition.java
+++ b/transition/src/main/java/android/support/transition/Transition.java
@@ -1017,7 +1017,7 @@
      */
     @NonNull
     public Transition addTarget(@IdRes int targetId) {
-        if (targetId > 0) {
+        if (targetId != 0) {
             mTargetIds.add(targetId);
         }
         return this;
@@ -1107,7 +1107,7 @@
      */
     @NonNull
     public Transition removeTarget(@IdRes int targetId) {
-        if (targetId > 0) {
+        if (targetId != 0) {
             mTargetIds.remove((Integer) targetId);
         }
         return this;
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/v14/preference/res/layout-v17/preference_category_material.xml b/v14/preference/res/layout-v17/preference_category_material.xml
index db3abfe..804da6a 100644
--- a/v14/preference/res/layout-v17/preference_category_material.xml
+++ b/v14/preference/res/layout-v17/preference_category_material.xml
@@ -15,13 +15,49 @@
   ~ limitations under the License
   -->
 
-<TextView xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@android:id/title"
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    android:layout_marginBottom="16dip"
-    android:textAppearance="@style/Preference_TextAppearanceMaterialBody2"
-    android:textColor="@color/preference_fallback_accent_color"
-    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
-    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
-    android:paddingTop="16dip" />
+    android:layout_marginBottom="8dp"
+    android:layout_marginTop="8dp"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart">
+
+    <LinearLayout
+        android:id="@+id/icon_frame"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:gravity="start|center_vertical"
+        android:orientation="horizontal">
+        <android.support.v7.internal.widget.PreferenceImageView
+            android:id="@android:id/icon"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            app:maxHeight="18dp"
+            app:maxWidth="18dp"/>
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        android:paddingStart="56dp">
+        <TextView
+            android:id="@android:id/title"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="16dp"
+            android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+            android:textAlignment="viewStart"
+            android:textColor="@color/preference_fallback_accent_color"/>
+        <TextView
+            android:id="@android:id/summary"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:ellipsize="end"
+            android:singleLine="true"
+            android:textColor="?android:attr/textColorSecondary"/>
+    </LinearLayout>
+
+</FrameLayout>
diff --git a/v14/preference/res/layout-v21/preference_category_material.xml b/v14/preference/res/layout-v21/preference_category_material.xml
index dad9a5c..1331268 100644
--- a/v14/preference/res/layout-v21/preference_category_material.xml
+++ b/v14/preference/res/layout-v21/preference_category_material.xml
@@ -15,13 +15,52 @@
   ~ limitations under the License
   -->
 
-<TextView xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@android:id/title"
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    android:layout_marginBottom="16dip"
-    android:textAppearance="@android:style/TextAppearance.Material.Body2"
-    android:textColor="?android:attr/colorAccent"
-    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
-    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
-    android:paddingTop="16dip" />
+    android:layout_marginBottom="8dp"
+    android:layout_marginTop="8dp"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart">
+
+    <LinearLayout
+        android:id="@+id/icon_frame"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:gravity="start|center_vertical"
+        android:orientation="horizontal">
+        <android.support.v7.internal.widget.PreferenceImageView
+            android:id="@android:id/icon"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            app:maxHeight="18dp"
+            app:maxWidth="18dp"
+            android:tint="?android:attr/textColorPrimary"/>
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        android:paddingStart="56dp">
+        <TextView
+            android:id="@android:id/title"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="16dp"
+            android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+            android:textAlignment="viewStart"
+            android:textAppearance="@android:style/TextAppearance.Material.Body2"
+            android:textColor="?android:attr/colorAccent"/>
+        <TextView
+            android:id="@android:id/summary"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:ellipsize="end"
+            android:singleLine="true"
+            android:textAppearance="?android:attr/textAppearanceListItemSecondary"
+            android:textColor="?android:attr/textColorSecondary"/>
+    </LinearLayout>
+
+</FrameLayout>
diff --git a/v14/preference/res/layout-v21/preference_dropdown_material.xml b/v14/preference/res/layout-v21/preference_dropdown_material.xml
index a92095e..f886d88 100644
--- a/v14/preference/res/layout-v21/preference_dropdown_material.xml
+++ b/v14/preference/res/layout-v21/preference_dropdown_material.xml
@@ -15,74 +15,18 @@
   ~ limitations under the License
   -->
 
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:minHeight="?android:attr/listPreferredItemHeightSmall"
-    android:gravity="center_vertical"
-    android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
-    android:paddingRight="?android:attr/listPreferredItemPaddingRight"
-    android:background="?android:attr/selectableItemBackground"
-    android:clipToPadding="false"
-    android:focusable="true" >
+    android:layout_height="wrap_content">
 
     <Spinner
         android:id="@+id/spinner"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
+        android:layout_marginStart="@dimen/preference_no_icon_padding_start"
         android:visibility="invisible" />
 
-    <LinearLayout
-        android:id="@+id/icon_frame"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginLeft="-4dp"
-        android:minWidth="60dp"
-        android:gravity="start|center_vertical"
-        android:orientation="horizontal"
-        android:paddingRight="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"
-            app:maxWidth="48dp"
-            app:maxHeight="48dp" />
-    </LinearLayout>
+    <include layout="@layout/preference_material"/>
 
-    <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:singleLine="true"
-            android:textAppearance="@style/Preference_TextAppearanceMaterialSubhead"
-            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/textAppearanceSmall"
-            android:textColor="?android:attr/textColorSecondary"
-            android:maxLines="10" />
-
-    </RelativeLayout>
-
-    <!-- Preference should place its actual preference widget here. -->
-    <LinearLayout android:id="@android:id/widget_frame"
-        android:layout_width="wrap_content"
-        android:layout_height="match_parent"
-        android:gravity="end|center_vertical"
-        android:paddingLeft="16dp"
-        android:orientation="vertical" />
-
-</LinearLayout>
+</FrameLayout>
diff --git a/v14/preference/res/layout/preference_category_material.xml b/v14/preference/res/layout/preference_category_material.xml
index e366e7a..8eb2137 100644
--- a/v14/preference/res/layout/preference_category_material.xml
+++ b/v14/preference/res/layout/preference_category_material.xml
@@ -15,13 +15,49 @@
   ~ limitations under the License
   -->
 
-<TextView xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@android:id/title"
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    android:layout_marginBottom="16dip"
-    android:textAppearance="@style/Preference_TextAppearanceMaterialBody2"
-    android:textColor="@color/preference_fallback_accent_color"
-    android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
-    android:paddingRight="?android:attr/listPreferredItemPaddingRight"
-    android:paddingTop="16dip" />
+    android:layout_marginBottom="8dp"
+    android:layout_marginTop="8dp"
+    android:paddingLeft="?android:attr/listPreferredItemPaddingLeft">
+
+    <LinearLayout
+        android:id="@+id/icon_frame"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:gravity="start|center_vertical"
+        android:orientation="horizontal">
+        <android.support.v7.internal.widget.PreferenceImageView
+            android:id="@android:id/icon"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            app:maxHeight="18dp"
+            app:maxWidth="18dp"/>
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        android:paddingLeft="56dp">
+        <TextView
+            android:id="@android:id/title"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="16dp"
+            android:paddingRight="?android:attr/listPreferredItemPaddingRight"
+            android:textAlignment="viewStart"
+            android:textColor="@color/preference_fallback_accent_color"/>
+        <TextView
+            android:id="@android:id/summary"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:ellipsize="end"
+            android:singleLine="true"
+            android:textColor="?android:attr/textColorSecondary"/>
+    </LinearLayout>
+
+</FrameLayout>
diff --git a/v14/preference/res/values-v21/styles.xml b/v14/preference/res/values-v21/styles.xml
new file mode 100644
index 0000000..9a85987
--- /dev/null
+++ b/v14/preference/res/values-v21/styles.xml
@@ -0,0 +1,20 @@
+<?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="preference_no_icon_padding_start">72dp</dimen>
+</resources>
+
diff --git a/v14/preference/res/values/styles.xml b/v14/preference/res/values/styles.xml
index 26b1544..edd5285 100644
--- a/v14/preference/res/values/styles.xml
+++ b/v14/preference/res/values/styles.xml
@@ -24,6 +24,10 @@
 
     <style name="Preference.Material">
         <item name="android:layout">@layout/preference_material</item>
+        <item name="allowDividerAbove">false</item>
+        <item name="allowDividerBelow">true</item>
+        <item name="singleLineTitle">false</item>
+        <item name="iconSpaceReserved">true</item>
     </style>
 
     <style name="Preference.Information.Material">
@@ -34,10 +38,16 @@
 
     <style name="Preference.Category.Material">
         <item name="android:layout">@layout/preference_category_material</item>
+        <item name="allowDividerAbove">true</item>
+        <item name="allowDividerBelow">true</item>
+        <item name="iconSpaceReserved">true</item>
     </style>
 
     <style name="Preference.CheckBoxPreference.Material">
         <item name="android:layout">@layout/preference_material</item>
+        <item name="allowDividerAbove">false</item>
+        <item name="allowDividerBelow">true</item>
+        <item name="iconSpaceReserved">true</item>
     </style>
 
     <style name="Preference.SwitchPreferenceCompat.Material">
@@ -46,6 +56,10 @@
 
     <style name="Preference.SwitchPreference.Material">
         <item name="android:layout">@layout/preference_material</item>
+        <item name="allowDividerAbove">false</item>
+        <item name="allowDividerBelow">true</item>
+        <item name="singleLineTitle">false</item>
+        <item name="iconSpaceReserved">true</item>
     </style>
 
     <style name="Preference.SeekBarPreference.Material">
@@ -56,18 +70,31 @@
 
     <style name="Preference.PreferenceScreen.Material">
         <item name="android:layout">@layout/preference_material</item>
+        <item name="allowDividerAbove">false</item>
+        <item name="allowDividerBelow">true</item>
+        <item name="iconSpaceReserved">true</item>
     </style>
 
     <style name="Preference.DialogPreference.Material">
         <item name="android:layout">@layout/preference_material</item>
+        <item name="allowDividerAbove">false</item>
+        <item name="allowDividerBelow">true</item>
+        <item name="iconSpaceReserved">true</item>
     </style>
 
     <style name="Preference.DialogPreference.EditTextPreference.Material">
         <item name="android:layout">@layout/preference_material</item>
+        <item name="allowDividerAbove">false</item>
+        <item name="allowDividerBelow">true</item>
+        <item name="singleLineTitle">false</item>
+        <item name="iconSpaceReserved">true</item>
     </style>
 
     <style name="Preference.DropDown.Material">
         <item name="android:layout">@layout/preference_dropdown_material</item>
+        <item name="allowDividerAbove">false</item>
+        <item name="allowDividerBelow">true</item>
+        <item name="iconSpaceReserved">true</item>
     </style>
 
     <style name="Preference_TextAppearanceMaterialBody2">
@@ -86,6 +113,7 @@
 
     <style name="PreferenceFragment.Material">
         <item name="android:divider">@drawable/preference_list_divider_material</item>
+        <item name="allowDividerAfterLastItem">false</item>
     </style>
 
     <style name="PreferenceFragmentList.Material">
diff --git a/v14/preference/res/values/themes.xml b/v14/preference/res/values/themes.xml
index a69126f..919873e 100644
--- a/v14/preference/res/values/themes.xml
+++ b/v14/preference/res/values/themes.xml
@@ -36,5 +36,6 @@
         <item name="editTextPreferenceStyle">@style/Preference.DialogPreference.EditTextPreference.Material</item>
         <item name="dropdownPreferenceStyle">@style/Preference.DropDown.Material</item>
         <item name="preferenceFragmentListStyle">@style/PreferenceFragmentList.Material</item>
+        <item name="android:scrollbars">vertical</item>
     </style>
 </resources>
diff --git a/v17/Android.mk b/v17/Android.mk
deleted file mode 100644
index 14ff0aa..0000000
--- a/v17/Android.mk
+++ /dev/null
@@ -1,16 +0,0 @@
-# Copyright (C) 2014 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)
-include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/v17/leanback/tests/Android.mk b/v17/leanback/tests/Android.mk
deleted file mode 100644
index 6c1a709..0000000
--- a/v17/leanback/tests/Android.mk
+++ /dev/null
@@ -1,43 +0,0 @@
-# Copyright (C) 2015 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)
-
-include $(CLEAR_VARS)
-
-LOCAL_MODULE_TAGS := tests
-
-LOCAL_SDK_VERSION := $(SUPPORT_CURRENT_SDK_VERSION)
-
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
-
-LOCAL_RESOURCE_DIR = \
-        $(LOCAL_PATH)/res \
-        $(LOCAL_PATH)/../res \
-        $(LOCAL_PATH)/../../v7/recyclerview/res
-LOCAL_AAPT_FLAGS := \
-        --auto-add-overlay \
-        --extra-packages android.support.v17.leanback \
-        --extra-packages android.support.v7.recyclerview
-
-LOCAL_STATIC_JAVA_LIBRARIES := \
-        android-support-v4 \
-        android-support-v7-recyclerview \
-        android-support-v17-leanback \
-        android-support-test \
-        mockito-target-minus-junit4
-
-LOCAL_PACKAGE_NAME := AndroidLeanbackTests
-
-include $(BUILD_PACKAGE)
diff --git a/v17/leanback/tests/generatev4.py b/v17/leanback/tests/generatev4.py
deleted file mode 100755
index d87ff6f..0000000
--- a/v17/leanback/tests/generatev4.py
+++ /dev/null
@@ -1,168 +0,0 @@
-#!/usr/bin/python
-
-# Copyright (C) 2015 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.
-
-import os
-import sys
-
-print "Generate v4 fragment related code for leanback"
-
-####### generate XXXTestFragment classes #######
-
-files = ['BrowseTest', 'GuidedStepTest', 'PlaybackTest', 'DetailsTest']
-
-cls = ['BrowseTest', 'Background', 'Base', 'BaseRow', 'Browse', 'Details', 'Error', 'Headers',
-      'PlaybackOverlay', 'Rows', 'Search', 'VerticalGrid', 'Branded',
-      'GuidedStepTest', 'GuidedStep', 'RowsTest', 'PlaybackTest', 'Playback', 'Video',
-      'DetailsTest']
-
-for w in files:
-    print "copy {}Fragment to {}SupportFragment".format(w, w)
-
-    file = open('java/android/support/v17/leanback/app/{}Fragment.java'.format(w), 'r')
-    outfile = open('java/android/support/v17/leanback/app/{}SupportFragment.java'.format(w), 'w')
-
-    outfile.write("// CHECKSTYLE:OFF Generated code\n")
-    outfile.write("/* This file is auto-generated from {}Fragment.java.  DO NOT MODIFY. */\n\n".format(w))
-
-    for line in file:
-        for w in cls:
-            line = line.replace('{}Fragment'.format(w), '{}SupportFragment'.format(w))
-        line = line.replace('android.app.Fragment', 'android.support.v4.app.Fragment')
-        line = line.replace('android.app.Activity', 'android.support.v4.app.FragmentActivity')
-        line = line.replace('Activity getActivity()', 'FragmentActivity getActivity()')
-        outfile.write(line)
-    file.close()
-    outfile.close()
-
-####### generate XXXFragmentTestBase classes #######
-
-testcls = ['GuidedStep', 'Single']
-
-for w in testcls:
-    print "copy {}FrgamentTestBase to {}SupportFragmentTestBase".format(w, w)
-
-    file = open('java/android/support/v17/leanback/app/{}FragmentTestBase.java'.format(w), 'r')
-    outfile = open('java/android/support/v17/leanback/app/{}SupportFragmentTestBase.java'.format(w), 'w')
-
-    outfile.write("// CHECKSTYLE:OFF Generated code\n")
-    outfile.write("/* This file is auto-generated from {}FrgamentTestBase.java.  DO NOT MODIFY. */\n\n".format(w))
-
-    for line in file:
-        for w in cls:
-            line = line.replace('{}Fragment'.format(w), '{}SupportFragment'.format(w))
-        for w in testcls:
-            line = line.replace('{}FragmentTestBase'.format(w), '{}SupportFragmentTestBase'.format(w))
-            line = line.replace('{}FragmentTestActivity'.format(w), '{}SupportFragmentTestActivity'.format(w))
-            line = line.replace('{}TestFragment'.format(w), '{}TestSupportFragment'.format(w))
-        line = line.replace('android.app.Fragment', 'android.support.v4.app.Fragment')
-        line = line.replace('android.app.Activity', 'android.support.v4.app.FragmentActivity')
-        outfile.write(line)
-    file.close()
-    outfile.close()
-
-####### generate XXXFragmentTest classes #######
-
-testcls = ['Browse', 'GuidedStep', 'VerticalGrid', 'Playback', 'Video', 'Details', 'Rows', 'Headers']
-
-for w in testcls:
-    print "copy {}FrgamentTest to {}SupportFragmentTest".format(w, w)
-
-    file = open('java/android/support/v17/leanback/app/{}FragmentTest.java'.format(w), 'r')
-    outfile = open('java/android/support/v17/leanback/app/{}SupportFragmentTest.java'.format(w), 'w')
-
-    outfile.write("// CHECKSTYLE:OFF Generated code\n")
-    outfile.write("/* This file is auto-generated from {}FragmentTest.java.  DO NOT MODIFY. */\n\n".format(w))
-
-    for line in file:
-        for w in cls:
-            line = line.replace('{}Fragment'.format(w), '{}SupportFragment'.format(w))
-        for w in testcls:
-            line = line.replace('SingleFragmentTestBase', 'SingleSupportFragmentTestBase')
-            line = line.replace('SingleFragmentTestActivity', 'SingleSupportFragmentTestActivity')
-            line = line.replace('{}FragmentTestBase'.format(w), '{}SupportFragmentTestBase'.format(w))
-            line = line.replace('{}FragmentTest'.format(w), '{}SupportFragmentTest'.format(w))
-            line = line.replace('{}FragmentTestActivity'.format(w), '{}SupportFragmentTestActivity'.format(w))
-            line = line.replace('{}TestFragment'.format(w), '{}TestSupportFragment'.format(w))
-        line = line.replace('android.app.Fragment', 'android.support.v4.app.Fragment')
-        line = line.replace('android.app.Activity', 'android.support.v4.app.FragmentActivity')
-	line = line.replace('extends Activity', 'extends FragmentActivity')
-	line = line.replace('Activity.this.getFragmentManager', 'Activity.this.getSupportFragmentManager')
-	line = line.replace('tivity.getFragmentManager', 'tivity.getSupportFragmentManager')
-        outfile.write(line)
-    file.close()
-    outfile.close()
-
-
-####### generate XXXTestActivity classes #######
-testcls = ['Browse', 'GuidedStep', 'Single']
-
-for w in testcls:
-    print "copy {}FragmentTestActivity to {}SupportFragmentTestActivity".format(w, w)
-    file = open('java/android/support/v17/leanback/app/{}FragmentTestActivity.java'.format(w), 'r')
-    outfile = open('java/android/support/v17/leanback/app/{}SupportFragmentTestActivity.java'.format(w), 'w')
-    outfile.write("// CHECKSTYLE:OFF Generated code\n")
-    outfile.write("/* This file is auto-generated from {}FragmentTestActivity.java.  DO NOT MODIFY. */\n\n".format(w))
-    for line in file:
-        line = line.replace('{}TestFragment'.format(w), '{}TestSupportFragment'.format(w))
-        line = line.replace('{}FragmentTestActivity'.format(w), '{}SupportFragmentTestActivity'.format(w))
-        line = line.replace('android.app.Fragment', 'android.support.v4.app.Fragment')
-        line = line.replace('android.app.Activity', 'android.support.v4.app.FragmentActivity')
-        line = line.replace('extends Activity', 'extends FragmentActivity')
-        line = line.replace('getFragmentManager', 'getSupportFragmentManager')
-        outfile.write(line)
-    file.close()
-    outfile.close()
-
-####### generate Float parallax test #######
-
-print "copy ParallaxIntEffectTest to ParallaxFloatEffectTest"
-file = open('java/android/support/v17/leanback/widget/ParallaxIntEffectTest.java', 'r')
-outfile = open('java/android/support/v17/leanback/widget/ParallaxFloatEffectTest.java', 'w')
-outfile.write("// CHECKSTYLE:OFF Generated code\n")
-outfile.write("/* This file is auto-generated from ParallaxIntEffectTest.java.  DO NOT MODIFY. */\n\n")
-for line in file:
-    line = line.replace('IntEffect', 'FloatEffect')
-    line = line.replace('IntParallax', 'FloatParallax')
-    line = line.replace('IntProperty', 'FloatProperty')
-    line = line.replace('intValue()', 'floatValue()')
-    line = line.replace('int screenMax', 'float screenMax')
-    line = line.replace('assertEquals((int)', 'assertFloatEquals((float)')
-    line = line.replace('(int)', '(float)')
-    line = line.replace('int[', 'float[')
-    line = line.replace('Integer', 'Float');
-    outfile.write(line)
-file.close()
-outfile.close()
-
-
-print "copy ParallaxIntTest to ParallaxFloatTest"
-file = open('java/android/support/v17/leanback/widget/ParallaxIntTest.java', 'r')
-outfile = open('java/android/support/v17/leanback/widget/ParallaxFloatTest.java', 'w')
-outfile.write("// CHECKSTYLE:OFF Generated code\n")
-outfile.write("/* This file is auto-generated from ParallaxIntTest.java.  DO NOT MODIFY. */\n\n")
-for line in file:
-    line = line.replace('ParallaxIntTest', 'ParallaxFloatTest')
-    line = line.replace('IntParallax', 'FloatParallax')
-    line = line.replace('IntProperty', 'FloatProperty')
-    line = line.replace('verifyIntProperties', 'verifyFloatProperties')
-    line = line.replace('intValue()', 'floatValue()')
-    line = line.replace('int screenMax', 'float screenMax')
-    line = line.replace('assertEquals((int)', 'assertFloatEquals((float)')
-    line = line.replace('(int)', '(float)')
-    outfile.write(line)
-file.close()
-outfile.close()
-
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/RowsFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/RowsFragmentTest.java
deleted file mode 100644
index 16e37cf..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/RowsFragmentTest.java
+++ /dev/null
@@ -1,464 +0,0 @@
-/*
- * Copyright (C) 2016 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.v17.leanback.app;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertTrue;
-
-import android.graphics.Rect;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.SystemClock;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.LargeTest;
-import android.support.test.filters.SdkSuppress;
-import android.support.test.runner.AndroidJUnit4;
-import android.support.v17.leanback.R;
-import android.support.v17.leanback.testutils.PollingCheck;
-import android.support.v17.leanback.widget.ArrayObjectAdapter;
-import android.support.v17.leanback.widget.HeaderItem;
-import android.support.v17.leanback.widget.HorizontalGridView;
-import android.support.v17.leanback.widget.ItemBridgeAdapter;
-import android.support.v17.leanback.widget.ListRow;
-import android.support.v17.leanback.widget.ListRowPresenter;
-import android.support.v17.leanback.widget.ObjectAdapter;
-import android.support.v17.leanback.widget.OnItemViewClickedListener;
-import android.support.v17.leanback.widget.Presenter;
-import android.support.v17.leanback.widget.Row;
-import android.support.v17.leanback.widget.RowPresenter;
-import android.support.v17.leanback.widget.SinglePresenterSelector;
-import android.support.v17.leanback.widget.VerticalGridView;
-import android.view.KeyEvent;
-import android.view.View;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-
-@LargeTest
-@RunWith(AndroidJUnit4.class)
-public class RowsFragmentTest extends SingleFragmentTestBase {
-
-    static final StringPresenter sCardPresenter = new StringPresenter();
-
-    static void loadData(ArrayObjectAdapter adapter, int numRows, int repeatPerRow) {
-        for (int i = 0; i < numRows; ++i) {
-            ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(sCardPresenter);
-            int index = 0;
-            for (int j = 0; j < repeatPerRow; ++j) {
-                listRowAdapter.add("Hello world-" + (index++));
-                listRowAdapter.add("This is a test-" + (index++));
-                listRowAdapter.add("Android TV-" + (index++));
-                listRowAdapter.add("Leanback-" + (index++));
-                listRowAdapter.add("Hello world-" + (index++));
-                listRowAdapter.add("Android TV-" + (index++));
-                listRowAdapter.add("Leanback-" + (index++));
-                listRowAdapter.add("GuidedStepFragment-" + (index++));
-            }
-            HeaderItem header = new HeaderItem(i, "Row " + i);
-            adapter.add(new ListRow(header, listRowAdapter));
-        }
-    }
-
-    public static class F_defaultAlignment extends RowsFragment {
-        @Override
-        public void onCreate(Bundle savedInstanceState) {
-            super.onCreate(savedInstanceState);
-            ListRowPresenter lrp = new ListRowPresenter();
-            ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
-            setAdapter(adapter);
-            loadData(adapter, 10, 1);
-        }
-    }
-
-    @Test
-    public void defaultAlignment() throws Throwable {
-        SingleFragmentTestActivity activity = launchAndWaitActivity(F_defaultAlignment.class, 1000);
-
-        final Rect rect = new Rect();
-
-        final VerticalGridView gridView = ((RowsFragment) activity.getTestFragment())
-                .getVerticalGridView();
-        View row0 = gridView.findViewHolderForAdapterPosition(0).itemView;
-        rect.set(0, 0, row0.getWidth(), row0.getHeight());
-        gridView.offsetDescendantRectToMyCoords(row0, rect);
-        assertEquals("First row is initially aligned to top of screen", 0, rect.top);
-
-        sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
-        waitForScrollIdle(gridView);
-        View row1 = gridView.findViewHolderForAdapterPosition(1).itemView;
-        PollingCheck.waitFor(new PollingCheck.ViewStableOnScreen(row1));
-
-        rect.set(0, 0, row1.getWidth(), row1.getHeight());
-        gridView.offsetDescendantRectToMyCoords(row1, rect);
-        assertTrue("Second row should not be aligned to top of screen", rect.top > 0);
-    }
-
-    public static class F_selectBeforeSetAdapter extends RowsFragment {
-        @Override
-        public void onCreate(Bundle savedInstanceState) {
-            super.onCreate(savedInstanceState);
-            setSelectedPosition(7, false);
-            new Handler().postDelayed(new Runnable() {
-                @Override
-                public void run() {
-                    getVerticalGridView().requestLayout();
-                }
-            }, 100);
-            new Handler().postDelayed(new Runnable() {
-                @Override
-                public void run() {
-                    ListRowPresenter lrp = new ListRowPresenter();
-                    ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
-                    setAdapter(adapter);
-                    loadData(adapter, 10, 1);
-                }
-            }, 1000);
-        }
-    }
-
-    @Test
-    public void selectBeforeSetAdapter() throws InterruptedException {
-        SingleFragmentTestActivity activity =
-                launchAndWaitActivity(F_selectBeforeSetAdapter.class, 2000);
-
-        final VerticalGridView gridView = ((RowsFragment) activity.getTestFragment())
-                .getVerticalGridView();
-        assertEquals(7, gridView.getSelectedPosition());
-        assertNotNull(gridView.findViewHolderForAdapterPosition(7));
-    }
-
-    public static class F_selectBeforeAddData extends RowsFragment {
-        @Override
-        public void onCreate(Bundle savedInstanceState) {
-            super.onCreate(savedInstanceState);
-            ListRowPresenter lrp = new ListRowPresenter();
-            final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
-            setAdapter(adapter);
-            setSelectedPosition(7, false);
-            new Handler().postDelayed(new Runnable() {
-                @Override
-                public void run() {
-                    getVerticalGridView().requestLayout();
-                }
-            }, 100);
-            new Handler().postDelayed(new Runnable() {
-                @Override
-                public void run() {
-                    loadData(adapter, 10, 1);
-                }
-            }, 1000);
-        }
-    }
-
-    @Test
-    public void selectBeforeAddData() throws InterruptedException {
-        SingleFragmentTestActivity activity =
-                launchAndWaitActivity(F_selectBeforeAddData.class, 2000);
-
-        final VerticalGridView gridView = ((RowsFragment) activity.getTestFragment())
-                .getVerticalGridView();
-        assertEquals(7, gridView.getSelectedPosition());
-        assertNotNull(gridView.findViewHolderForAdapterPosition(7));
-    }
-
-    public static class F_selectAfterAddData extends RowsFragment {
-        @Override
-        public void onCreate(Bundle savedInstanceState) {
-            super.onCreate(savedInstanceState);
-            ListRowPresenter lrp = new ListRowPresenter();
-            final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
-            setAdapter(adapter);
-            loadData(adapter, 10, 1);
-            new Handler().postDelayed(new Runnable() {
-                @Override
-                public void run() {
-                    setSelectedPosition(7, false);
-                }
-            }, 1000);
-        }
-    }
-
-    @Test
-    public void selectAfterAddData() throws InterruptedException {
-        SingleFragmentTestActivity activity =
-                launchAndWaitActivity(F_selectAfterAddData.class, 2000);
-
-        final VerticalGridView gridView = ((RowsFragment) activity.getTestFragment())
-                .getVerticalGridView();
-        assertEquals(7, gridView.getSelectedPosition());
-        assertNotNull(gridView.findViewHolderForAdapterPosition(7));
-    }
-
-    static WeakReference<F_restoreSelection> sLastF_restoreSelection;
-
-    public static class F_restoreSelection extends RowsFragment {
-        public F_restoreSelection() {
-            sLastF_restoreSelection = new WeakReference<F_restoreSelection>(this);
-        }
-
-        @Override
-        public void onCreate(Bundle savedInstanceState) {
-            super.onCreate(savedInstanceState);
-            ListRowPresenter lrp = new ListRowPresenter();
-            final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
-            setAdapter(adapter);
-            loadData(adapter, 10, 1);
-            if (savedInstanceState == null) {
-                setSelectedPosition(7, false);
-            }
-        }
-    }
-
-    @Test
-    public void restoreSelection() {
-        final SingleFragmentTestActivity activity =
-                launchAndWaitActivity(F_restoreSelection.class, 1000);
-
-        InstrumentationRegistry.getInstrumentation().runOnMainSync(
-                new Runnable() {
-                    @Override
-                    public void run() {
-                        activity.recreate();
-                    }
-                }
-        );
-        SystemClock.sleep(1000);
-
-        // mActivity is invalid after recreate(), a new Activity instance is created
-        // but we could get Fragment from static variable.
-        RowsFragment fragment = sLastF_restoreSelection.get();
-        final VerticalGridView gridView = fragment.getVerticalGridView();
-        assertEquals(7, gridView.getSelectedPosition());
-        assertNotNull(gridView.findViewHolderForAdapterPosition(7));
-
-    }
-
-    public static class F_ListRowWithOnClick extends RowsFragment {
-        Presenter.ViewHolder mLastClickedItemViewHolder;
-        @Override
-        public void onCreate(Bundle savedInstanceState) {
-            super.onCreate(savedInstanceState);
-            setOnItemViewClickedListener(new OnItemViewClickedListener() {
-                @Override
-                public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
-                        RowPresenter.ViewHolder rowViewHolder, Row row) {
-                    mLastClickedItemViewHolder = itemViewHolder;
-                }
-            });
-            ListRowPresenter lrp = new ListRowPresenter();
-            ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
-            setAdapter(adapter);
-            loadData(adapter, 10, 1);
-        }
-    }
-
-    @Test
-    public void prefetchChildItemsBeforeAttach() throws Throwable {
-        SingleFragmentTestActivity activity =
-                launchAndWaitActivity(F_ListRowWithOnClick.class, 1000);
-
-        F_ListRowWithOnClick fragment = (F_ListRowWithOnClick) activity.getTestFragment();
-        final VerticalGridView gridView = fragment.getVerticalGridView();
-        View lastRow = gridView.getChildAt(gridView.getChildCount() - 1);
-        final int lastRowPos = gridView.getChildAdapterPosition(lastRow);
-        InstrumentationRegistry.getInstrumentation().runOnMainSync(
-                new Runnable() {
-                    public void run() {
-                        gridView.setSelectedPositionSmooth(lastRowPos);
-                    }
-                }
-        );
-        waitForScrollIdle(gridView);
-        ItemBridgeAdapter.ViewHolder prefetchedBridgeVh = (ItemBridgeAdapter.ViewHolder)
-                gridView.findViewHolderForAdapterPosition(lastRowPos + 1);
-        RowPresenter prefetchedRowPresenter = (RowPresenter) prefetchedBridgeVh.getPresenter();
-        final ListRowPresenter.ViewHolder prefetchedListRowVh = (ListRowPresenter.ViewHolder)
-                prefetchedRowPresenter.getRowViewHolder(prefetchedBridgeVh.getViewHolder());
-
-        fragment.mLastClickedItemViewHolder = null;
-        InstrumentationRegistry.getInstrumentation().runOnMainSync(
-                new Runnable() {
-                    public void run() {
-                        prefetchedListRowVh.getItemViewHolder(0).view.performClick();
-                    }
-                }
-        );
-        assertSame(prefetchedListRowVh.getItemViewHolder(0), fragment.mLastClickedItemViewHolder);
-    }
-
-    @Test
-    public void changeHasStableIdToTrueAfterViewCreated() throws InterruptedException {
-        SingleFragmentTestActivity activity =
-                launchAndWaitActivity(RowsFragment.class, 2000);
-        final RowsFragment fragment = (RowsFragment) activity.getTestFragment();
-
-        InstrumentationRegistry.getInstrumentation().runOnMainSync(
-                new Runnable() {
-                    public void run() {
-                        ObjectAdapter adapter = new ObjectAdapter() {
-                            @Override
-                            public int size() {
-                                return 0;
-                            }
-
-                            @Override
-                            public Object get(int position) {
-                                return null;
-                            }
-
-                            @Override
-                            public long getId(int position) {
-                                return 1;
-                            }
-                        };
-                        adapter.setHasStableIds(true);
-                        fragment.setAdapter(adapter);
-                    }
-                }
-        );
-    }
-
-    static class StableIdAdapter extends ObjectAdapter {
-        ArrayList<Integer> mList = new ArrayList();
-
-        @Override
-        public long getId(int position) {
-            return mList.get(position).longValue();
-        }
-
-        @Override
-        public Object get(int position) {
-            return mList.get(position);
-        }
-
-        @Override
-        public int size() {
-            return mList.size();
-        }
-    }
-
-    public static class F_rowNotifyItemRangeChange extends BrowseFragment {
-
-        @Override
-        public void onCreate(Bundle savedInstanceState) {
-            super.onCreate(savedInstanceState);
-            ListRowPresenter lrp = new ListRowPresenter();
-            final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
-            for (int i = 0; i < 2; i++) {
-                StableIdAdapter listRowAdapter = new StableIdAdapter();
-                listRowAdapter.setHasStableIds(true);
-                listRowAdapter.setPresenterSelector(
-                        new SinglePresenterSelector(sCardPresenter));
-                int index = 0;
-                listRowAdapter.mList.add(index++);
-                listRowAdapter.mList.add(index++);
-                listRowAdapter.mList.add(index++);
-                HeaderItem header = new HeaderItem(i, "Row " + i);
-                adapter.add(new ListRow(header, listRowAdapter));
-            }
-            setAdapter(adapter);
-            new Handler().postDelayed(new Runnable() {
-                @Override
-                public void run() {
-                    StableIdAdapter rowAdapter = (StableIdAdapter)
-                            ((ListRow) adapter.get(1)).getAdapter();
-                    rowAdapter.notifyItemRangeChanged(0, 3);
-                }
-            }, 500);
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
-    @Test
-    public void rowNotifyItemRangeChange() throws InterruptedException {
-        SingleFragmentTestActivity activity = launchAndWaitActivity(
-                RowsFragmentTest.F_rowNotifyItemRangeChange.class, 2000);
-
-        VerticalGridView verticalGridView = ((BrowseFragment) activity.getTestFragment())
-                .getRowsFragment().getVerticalGridView();
-        for (int i = 0; i < verticalGridView.getChildCount(); i++) {
-            HorizontalGridView horizontalGridView = verticalGridView.getChildAt(i)
-                    .findViewById(R.id.row_content);
-            for (int j = 0; j < horizontalGridView.getChildCount(); j++) {
-                assertEquals(horizontalGridView.getPaddingTop(),
-                        horizontalGridView.getChildAt(j).getTop());
-            }
-        }
-    }
-
-    public static class F_rowNotifyItemRangeChangeWithTransition extends BrowseFragment {
-
-        @Override
-        public void onCreate(Bundle savedInstanceState) {
-            super.onCreate(savedInstanceState);
-            ListRowPresenter lrp = new ListRowPresenter();
-            prepareEntranceTransition();
-            final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
-            for (int i = 0; i < 2; i++) {
-                StableIdAdapter listRowAdapter = new StableIdAdapter();
-                listRowAdapter.setHasStableIds(true);
-                listRowAdapter.setPresenterSelector(
-                        new SinglePresenterSelector(sCardPresenter));
-                int index = 0;
-                listRowAdapter.mList.add(index++);
-                listRowAdapter.mList.add(index++);
-                listRowAdapter.mList.add(index++);
-                HeaderItem header = new HeaderItem(i, "Row " + i);
-                adapter.add(new ListRow(header, listRowAdapter));
-            }
-            setAdapter(adapter);
-            new Handler().postDelayed(new Runnable() {
-                @Override
-                public void run() {
-                    StableIdAdapter rowAdapter = (StableIdAdapter)
-                            ((ListRow) adapter.get(1)).getAdapter();
-                    rowAdapter.notifyItemRangeChanged(0, 3);
-                }
-            }, 500);
-            new Handler().postDelayed(new Runnable() {
-                @Override
-                public void run() {
-                    startEntranceTransition();
-                }
-            }, 520);
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
-    @Test
-    public void rowNotifyItemRangeChangeWithTransition() throws InterruptedException {
-        SingleFragmentTestActivity activity = launchAndWaitActivity(
-                        RowsFragmentTest.F_rowNotifyItemRangeChangeWithTransition.class, 3000);
-
-        VerticalGridView verticalGridView = ((BrowseFragment) activity.getTestFragment())
-                .getRowsFragment().getVerticalGridView();
-        for (int i = 0; i < verticalGridView.getChildCount(); i++) {
-            HorizontalGridView horizontalGridView = verticalGridView.getChildAt(i)
-                    .findViewById(R.id.row_content);
-            for (int j = 0; j < horizontalGridView.getChildCount(); j++) {
-                assertEquals(horizontalGridView.getPaddingTop(),
-                        horizontalGridView.getChildAt(j).getTop());
-                assertEquals(0, horizontalGridView.getChildAt(j).getTranslationY(), 0.1f);
-            }
-        }
-    }
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/RowsSupportFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/RowsSupportFragmentTest.java
deleted file mode 100644
index c461f45..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/RowsSupportFragmentTest.java
+++ /dev/null
@@ -1,467 +0,0 @@
-// CHECKSTYLE:OFF Generated code
-/* This file is auto-generated from RowsFragmentTest.java.  DO NOT MODIFY. */
-
-/*
- * Copyright (C) 2016 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.v17.leanback.app;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertTrue;
-
-import android.graphics.Rect;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.SystemClock;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.LargeTest;
-import android.support.test.filters.SdkSuppress;
-import android.support.test.runner.AndroidJUnit4;
-import android.support.v17.leanback.R;
-import android.support.v17.leanback.testutils.PollingCheck;
-import android.support.v17.leanback.widget.ArrayObjectAdapter;
-import android.support.v17.leanback.widget.HeaderItem;
-import android.support.v17.leanback.widget.HorizontalGridView;
-import android.support.v17.leanback.widget.ItemBridgeAdapter;
-import android.support.v17.leanback.widget.ListRow;
-import android.support.v17.leanback.widget.ListRowPresenter;
-import android.support.v17.leanback.widget.ObjectAdapter;
-import android.support.v17.leanback.widget.OnItemViewClickedListener;
-import android.support.v17.leanback.widget.Presenter;
-import android.support.v17.leanback.widget.Row;
-import android.support.v17.leanback.widget.RowPresenter;
-import android.support.v17.leanback.widget.SinglePresenterSelector;
-import android.support.v17.leanback.widget.VerticalGridView;
-import android.view.KeyEvent;
-import android.view.View;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-
-@LargeTest
-@RunWith(AndroidJUnit4.class)
-public class RowsSupportFragmentTest extends SingleSupportFragmentTestBase {
-
-    static final StringPresenter sCardPresenter = new StringPresenter();
-
-    static void loadData(ArrayObjectAdapter adapter, int numRows, int repeatPerRow) {
-        for (int i = 0; i < numRows; ++i) {
-            ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(sCardPresenter);
-            int index = 0;
-            for (int j = 0; j < repeatPerRow; ++j) {
-                listRowAdapter.add("Hello world-" + (index++));
-                listRowAdapter.add("This is a test-" + (index++));
-                listRowAdapter.add("Android TV-" + (index++));
-                listRowAdapter.add("Leanback-" + (index++));
-                listRowAdapter.add("Hello world-" + (index++));
-                listRowAdapter.add("Android TV-" + (index++));
-                listRowAdapter.add("Leanback-" + (index++));
-                listRowAdapter.add("GuidedStepSupportFragment-" + (index++));
-            }
-            HeaderItem header = new HeaderItem(i, "Row " + i);
-            adapter.add(new ListRow(header, listRowAdapter));
-        }
-    }
-
-    public static class F_defaultAlignment extends RowsSupportFragment {
-        @Override
-        public void onCreate(Bundle savedInstanceState) {
-            super.onCreate(savedInstanceState);
-            ListRowPresenter lrp = new ListRowPresenter();
-            ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
-            setAdapter(adapter);
-            loadData(adapter, 10, 1);
-        }
-    }
-
-    @Test
-    public void defaultAlignment() throws Throwable {
-        SingleSupportFragmentTestActivity activity = launchAndWaitActivity(F_defaultAlignment.class, 1000);
-
-        final Rect rect = new Rect();
-
-        final VerticalGridView gridView = ((RowsSupportFragment) activity.getTestFragment())
-                .getVerticalGridView();
-        View row0 = gridView.findViewHolderForAdapterPosition(0).itemView;
-        rect.set(0, 0, row0.getWidth(), row0.getHeight());
-        gridView.offsetDescendantRectToMyCoords(row0, rect);
-        assertEquals("First row is initially aligned to top of screen", 0, rect.top);
-
-        sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
-        waitForScrollIdle(gridView);
-        View row1 = gridView.findViewHolderForAdapterPosition(1).itemView;
-        PollingCheck.waitFor(new PollingCheck.ViewStableOnScreen(row1));
-
-        rect.set(0, 0, row1.getWidth(), row1.getHeight());
-        gridView.offsetDescendantRectToMyCoords(row1, rect);
-        assertTrue("Second row should not be aligned to top of screen", rect.top > 0);
-    }
-
-    public static class F_selectBeforeSetAdapter extends RowsSupportFragment {
-        @Override
-        public void onCreate(Bundle savedInstanceState) {
-            super.onCreate(savedInstanceState);
-            setSelectedPosition(7, false);
-            new Handler().postDelayed(new Runnable() {
-                @Override
-                public void run() {
-                    getVerticalGridView().requestLayout();
-                }
-            }, 100);
-            new Handler().postDelayed(new Runnable() {
-                @Override
-                public void run() {
-                    ListRowPresenter lrp = new ListRowPresenter();
-                    ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
-                    setAdapter(adapter);
-                    loadData(adapter, 10, 1);
-                }
-            }, 1000);
-        }
-    }
-
-    @Test
-    public void selectBeforeSetAdapter() throws InterruptedException {
-        SingleSupportFragmentTestActivity activity =
-                launchAndWaitActivity(F_selectBeforeSetAdapter.class, 2000);
-
-        final VerticalGridView gridView = ((RowsSupportFragment) activity.getTestFragment())
-                .getVerticalGridView();
-        assertEquals(7, gridView.getSelectedPosition());
-        assertNotNull(gridView.findViewHolderForAdapterPosition(7));
-    }
-
-    public static class F_selectBeforeAddData extends RowsSupportFragment {
-        @Override
-        public void onCreate(Bundle savedInstanceState) {
-            super.onCreate(savedInstanceState);
-            ListRowPresenter lrp = new ListRowPresenter();
-            final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
-            setAdapter(adapter);
-            setSelectedPosition(7, false);
-            new Handler().postDelayed(new Runnable() {
-                @Override
-                public void run() {
-                    getVerticalGridView().requestLayout();
-                }
-            }, 100);
-            new Handler().postDelayed(new Runnable() {
-                @Override
-                public void run() {
-                    loadData(adapter, 10, 1);
-                }
-            }, 1000);
-        }
-    }
-
-    @Test
-    public void selectBeforeAddData() throws InterruptedException {
-        SingleSupportFragmentTestActivity activity =
-                launchAndWaitActivity(F_selectBeforeAddData.class, 2000);
-
-        final VerticalGridView gridView = ((RowsSupportFragment) activity.getTestFragment())
-                .getVerticalGridView();
-        assertEquals(7, gridView.getSelectedPosition());
-        assertNotNull(gridView.findViewHolderForAdapterPosition(7));
-    }
-
-    public static class F_selectAfterAddData extends RowsSupportFragment {
-        @Override
-        public void onCreate(Bundle savedInstanceState) {
-            super.onCreate(savedInstanceState);
-            ListRowPresenter lrp = new ListRowPresenter();
-            final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
-            setAdapter(adapter);
-            loadData(adapter, 10, 1);
-            new Handler().postDelayed(new Runnable() {
-                @Override
-                public void run() {
-                    setSelectedPosition(7, false);
-                }
-            }, 1000);
-        }
-    }
-
-    @Test
-    public void selectAfterAddData() throws InterruptedException {
-        SingleSupportFragmentTestActivity activity =
-                launchAndWaitActivity(F_selectAfterAddData.class, 2000);
-
-        final VerticalGridView gridView = ((RowsSupportFragment) activity.getTestFragment())
-                .getVerticalGridView();
-        assertEquals(7, gridView.getSelectedPosition());
-        assertNotNull(gridView.findViewHolderForAdapterPosition(7));
-    }
-
-    static WeakReference<F_restoreSelection> sLastF_restoreSelection;
-
-    public static class F_restoreSelection extends RowsSupportFragment {
-        public F_restoreSelection() {
-            sLastF_restoreSelection = new WeakReference<F_restoreSelection>(this);
-        }
-
-        @Override
-        public void onCreate(Bundle savedInstanceState) {
-            super.onCreate(savedInstanceState);
-            ListRowPresenter lrp = new ListRowPresenter();
-            final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
-            setAdapter(adapter);
-            loadData(adapter, 10, 1);
-            if (savedInstanceState == null) {
-                setSelectedPosition(7, false);
-            }
-        }
-    }
-
-    @Test
-    public void restoreSelection() {
-        final SingleSupportFragmentTestActivity activity =
-                launchAndWaitActivity(F_restoreSelection.class, 1000);
-
-        InstrumentationRegistry.getInstrumentation().runOnMainSync(
-                new Runnable() {
-                    @Override
-                    public void run() {
-                        activity.recreate();
-                    }
-                }
-        );
-        SystemClock.sleep(1000);
-
-        // mActivity is invalid after recreate(), a new Activity instance is created
-        // but we could get Fragment from static variable.
-        RowsSupportFragment fragment = sLastF_restoreSelection.get();
-        final VerticalGridView gridView = fragment.getVerticalGridView();
-        assertEquals(7, gridView.getSelectedPosition());
-        assertNotNull(gridView.findViewHolderForAdapterPosition(7));
-
-    }
-
-    public static class F_ListRowWithOnClick extends RowsSupportFragment {
-        Presenter.ViewHolder mLastClickedItemViewHolder;
-        @Override
-        public void onCreate(Bundle savedInstanceState) {
-            super.onCreate(savedInstanceState);
-            setOnItemViewClickedListener(new OnItemViewClickedListener() {
-                @Override
-                public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
-                        RowPresenter.ViewHolder rowViewHolder, Row row) {
-                    mLastClickedItemViewHolder = itemViewHolder;
-                }
-            });
-            ListRowPresenter lrp = new ListRowPresenter();
-            ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
-            setAdapter(adapter);
-            loadData(adapter, 10, 1);
-        }
-    }
-
-    @Test
-    public void prefetchChildItemsBeforeAttach() throws Throwable {
-        SingleSupportFragmentTestActivity activity =
-                launchAndWaitActivity(F_ListRowWithOnClick.class, 1000);
-
-        F_ListRowWithOnClick fragment = (F_ListRowWithOnClick) activity.getTestFragment();
-        final VerticalGridView gridView = fragment.getVerticalGridView();
-        View lastRow = gridView.getChildAt(gridView.getChildCount() - 1);
-        final int lastRowPos = gridView.getChildAdapterPosition(lastRow);
-        InstrumentationRegistry.getInstrumentation().runOnMainSync(
-                new Runnable() {
-                    public void run() {
-                        gridView.setSelectedPositionSmooth(lastRowPos);
-                    }
-                }
-        );
-        waitForScrollIdle(gridView);
-        ItemBridgeAdapter.ViewHolder prefetchedBridgeVh = (ItemBridgeAdapter.ViewHolder)
-                gridView.findViewHolderForAdapterPosition(lastRowPos + 1);
-        RowPresenter prefetchedRowPresenter = (RowPresenter) prefetchedBridgeVh.getPresenter();
-        final ListRowPresenter.ViewHolder prefetchedListRowVh = (ListRowPresenter.ViewHolder)
-                prefetchedRowPresenter.getRowViewHolder(prefetchedBridgeVh.getViewHolder());
-
-        fragment.mLastClickedItemViewHolder = null;
-        InstrumentationRegistry.getInstrumentation().runOnMainSync(
-                new Runnable() {
-                    public void run() {
-                        prefetchedListRowVh.getItemViewHolder(0).view.performClick();
-                    }
-                }
-        );
-        assertSame(prefetchedListRowVh.getItemViewHolder(0), fragment.mLastClickedItemViewHolder);
-    }
-
-    @Test
-    public void changeHasStableIdToTrueAfterViewCreated() throws InterruptedException {
-        SingleSupportFragmentTestActivity activity =
-                launchAndWaitActivity(RowsSupportFragment.class, 2000);
-        final RowsSupportFragment fragment = (RowsSupportFragment) activity.getTestFragment();
-
-        InstrumentationRegistry.getInstrumentation().runOnMainSync(
-                new Runnable() {
-                    public void run() {
-                        ObjectAdapter adapter = new ObjectAdapter() {
-                            @Override
-                            public int size() {
-                                return 0;
-                            }
-
-                            @Override
-                            public Object get(int position) {
-                                return null;
-                            }
-
-                            @Override
-                            public long getId(int position) {
-                                return 1;
-                            }
-                        };
-                        adapter.setHasStableIds(true);
-                        fragment.setAdapter(adapter);
-                    }
-                }
-        );
-    }
-
-    static class StableIdAdapter extends ObjectAdapter {
-        ArrayList<Integer> mList = new ArrayList();
-
-        @Override
-        public long getId(int position) {
-            return mList.get(position).longValue();
-        }
-
-        @Override
-        public Object get(int position) {
-            return mList.get(position);
-        }
-
-        @Override
-        public int size() {
-            return mList.size();
-        }
-    }
-
-    public static class F_rowNotifyItemRangeChange extends BrowseSupportFragment {
-
-        @Override
-        public void onCreate(Bundle savedInstanceState) {
-            super.onCreate(savedInstanceState);
-            ListRowPresenter lrp = new ListRowPresenter();
-            final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
-            for (int i = 0; i < 2; i++) {
-                StableIdAdapter listRowAdapter = new StableIdAdapter();
-                listRowAdapter.setHasStableIds(true);
-                listRowAdapter.setPresenterSelector(
-                        new SinglePresenterSelector(sCardPresenter));
-                int index = 0;
-                listRowAdapter.mList.add(index++);
-                listRowAdapter.mList.add(index++);
-                listRowAdapter.mList.add(index++);
-                HeaderItem header = new HeaderItem(i, "Row " + i);
-                adapter.add(new ListRow(header, listRowAdapter));
-            }
-            setAdapter(adapter);
-            new Handler().postDelayed(new Runnable() {
-                @Override
-                public void run() {
-                    StableIdAdapter rowAdapter = (StableIdAdapter)
-                            ((ListRow) adapter.get(1)).getAdapter();
-                    rowAdapter.notifyItemRangeChanged(0, 3);
-                }
-            }, 500);
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
-    @Test
-    public void rowNotifyItemRangeChange() throws InterruptedException {
-        SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
-                RowsSupportFragmentTest.F_rowNotifyItemRangeChange.class, 2000);
-
-        VerticalGridView verticalGridView = ((BrowseSupportFragment) activity.getTestFragment())
-                .getRowsSupportFragment().getVerticalGridView();
-        for (int i = 0; i < verticalGridView.getChildCount(); i++) {
-            HorizontalGridView horizontalGridView = verticalGridView.getChildAt(i)
-                    .findViewById(R.id.row_content);
-            for (int j = 0; j < horizontalGridView.getChildCount(); j++) {
-                assertEquals(horizontalGridView.getPaddingTop(),
-                        horizontalGridView.getChildAt(j).getTop());
-            }
-        }
-    }
-
-    public static class F_rowNotifyItemRangeChangeWithTransition extends BrowseSupportFragment {
-
-        @Override
-        public void onCreate(Bundle savedInstanceState) {
-            super.onCreate(savedInstanceState);
-            ListRowPresenter lrp = new ListRowPresenter();
-            prepareEntranceTransition();
-            final ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
-            for (int i = 0; i < 2; i++) {
-                StableIdAdapter listRowAdapter = new StableIdAdapter();
-                listRowAdapter.setHasStableIds(true);
-                listRowAdapter.setPresenterSelector(
-                        new SinglePresenterSelector(sCardPresenter));
-                int index = 0;
-                listRowAdapter.mList.add(index++);
-                listRowAdapter.mList.add(index++);
-                listRowAdapter.mList.add(index++);
-                HeaderItem header = new HeaderItem(i, "Row " + i);
-                adapter.add(new ListRow(header, listRowAdapter));
-            }
-            setAdapter(adapter);
-            new Handler().postDelayed(new Runnable() {
-                @Override
-                public void run() {
-                    StableIdAdapter rowAdapter = (StableIdAdapter)
-                            ((ListRow) adapter.get(1)).getAdapter();
-                    rowAdapter.notifyItemRangeChanged(0, 3);
-                }
-            }, 500);
-            new Handler().postDelayed(new Runnable() {
-                @Override
-                public void run() {
-                    startEntranceTransition();
-                }
-            }, 520);
-        }
-    }
-
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
-    @Test
-    public void rowNotifyItemRangeChangeWithTransition() throws InterruptedException {
-        SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
-                        RowsSupportFragmentTest.F_rowNotifyItemRangeChangeWithTransition.class, 3000);
-
-        VerticalGridView verticalGridView = ((BrowseSupportFragment) activity.getTestFragment())
-                .getRowsSupportFragment().getVerticalGridView();
-        for (int i = 0; i < verticalGridView.getChildCount(); i++) {
-            HorizontalGridView horizontalGridView = verticalGridView.getChildAt(i)
-                    .findViewById(R.id.row_content);
-            for (int j = 0; j < horizontalGridView.getChildCount(); j++) {
-                assertEquals(horizontalGridView.getPaddingTop(),
-                        horizontalGridView.getChildAt(j).getTop());
-                assertEquals(0, horizontalGridView.getChildAt(j).getTranslationY(), 0.1f);
-            }
-        }
-    }
-}
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/api/current.txt b/v7/appcompat/api/current.txt
index 93d0186..d39b109 100644
--- a/v7/appcompat/api/current.txt
+++ b/v7/appcompat/api/current.txt
@@ -303,6 +303,24 @@
     ctor public AppCompatDialogFragment();
   }
 
+  public class AppCompatViewInflater {
+    ctor public AppCompatViewInflater();
+    method protected android.support.v7.widget.AppCompatAutoCompleteTextView createAutoCompleteTextView(android.content.Context, android.util.AttributeSet);
+    method protected android.support.v7.widget.AppCompatButton createButton(android.content.Context, android.util.AttributeSet);
+    method protected android.support.v7.widget.AppCompatCheckBox createCheckBox(android.content.Context, android.util.AttributeSet);
+    method protected android.support.v7.widget.AppCompatCheckedTextView createCheckedTextView(android.content.Context, android.util.AttributeSet);
+    method protected android.support.v7.widget.AppCompatEditText createEditText(android.content.Context, android.util.AttributeSet);
+    method protected android.support.v7.widget.AppCompatImageButton createImageButton(android.content.Context, android.util.AttributeSet);
+    method protected android.support.v7.widget.AppCompatImageView createImageView(android.content.Context, android.util.AttributeSet);
+    method protected android.support.v7.widget.AppCompatMultiAutoCompleteTextView createMultiAutoCompleteTextView(android.content.Context, android.util.AttributeSet);
+    method protected android.support.v7.widget.AppCompatRadioButton createRadioButton(android.content.Context, android.util.AttributeSet);
+    method protected android.support.v7.widget.AppCompatRatingBar createRatingBar(android.content.Context, android.util.AttributeSet);
+    method protected android.support.v7.widget.AppCompatSeekBar createSeekBar(android.content.Context, android.util.AttributeSet);
+    method protected android.support.v7.widget.AppCompatSpinner createSpinner(android.content.Context, android.util.AttributeSet);
+    method protected android.support.v7.widget.AppCompatTextView createTextView(android.content.Context, android.util.AttributeSet);
+    method protected android.view.View createView(android.content.Context, java.lang.String, android.util.AttributeSet);
+  }
+
 }
 
 package android.support.v7.content.res {
diff --git a/v7/appcompat/build.gradle b/v7/appcompat/build.gradle
index 308a122..8e242cc 100644
--- a/v7/appcompat/build.gradle
+++ b/v7/appcompat/build.gradle
@@ -16,7 +16,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/res/anim/tooltip_enter.xml b/v7/appcompat/res/anim/abc_tooltip_enter.xml
similarity index 100%
rename from v7/appcompat/res/anim/tooltip_enter.xml
rename to v7/appcompat/res/anim/abc_tooltip_enter.xml
diff --git a/v7/appcompat/res/anim/tooltip_exit.xml b/v7/appcompat/res/anim/abc_tooltip_exit.xml
similarity index 100%
rename from v7/appcompat/res/anim/tooltip_exit.xml
rename to v7/appcompat/res/anim/abc_tooltip_exit.xml
diff --git a/v7/appcompat/res/drawable-watch/abc_dialog_material_background.xml b/v7/appcompat/res/drawable-watch/abc_dialog_material_background.xml
new file mode 100644
index 0000000..242761b
--- /dev/null
+++ b/v7/appcompat/res/drawable-watch/abc_dialog_material_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.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <solid android:color="@android:color/white" />
+</shape>
diff --git a/v7/appcompat/res/layout-watch/abc_alert_dialog_button_bar_material.xml b/v7/appcompat/res/layout-watch/abc_alert_dialog_button_bar_material.xml
new file mode 100644
index 0000000..1c8bd93
--- /dev/null
+++ b/v7/appcompat/res/layout-watch/abc_alert_dialog_button_bar_material.xml
@@ -0,0 +1,51 @@
+<?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.v7.widget.ButtonBarLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/buttonPanel"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:gravity="bottom"
+    android:layoutDirection="locale"
+    android:orientation="horizontal"
+    android:paddingBottom="4dp"
+    android:paddingLeft="12dp"
+    android:paddingRight="12dp"
+    android:paddingTop="4dp">
+
+    <Button
+        android:id="@android:id/button3"
+        style="?attr/buttonBarNeutralButtonStyle"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_weight="1"/>
+
+    <Button
+        android:id="@android:id/button2"
+        style="?attr/buttonBarNegativeButtonStyle"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_weight="1"/>
+
+    <Button
+        android:id="@android:id/button1"
+        style="?attr/buttonBarPositiveButtonStyle"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_weight="1"/>
+
+</android.support.v7.widget.ButtonBarLayout>
diff --git a/v7/appcompat/res/layout-watch/abc_alert_dialog_title_material.xml b/v7/appcompat/res/layout-watch/abc_alert_dialog_title_material.xml
new file mode 100644
index 0000000..e100963
--- /dev/null
+++ b/v7/appcompat/res/layout-watch/abc_alert_dialog_title_material.xml
@@ -0,0 +1,58 @@
+<?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/topPanel"
+     android:layout_width="match_parent"
+     android:layout_height="wrap_content"
+     android:orientation="vertical"
+     android:gravity="top|center_horizontal">
+
+    <!-- If the client uses a customTitle, it will be added here. -->
+
+    <LinearLayout
+        android:id="@+id/title_template"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:gravity="center_horizontal"
+        android:layout_marginTop="24dp"
+        android:orientation="vertical">
+
+        <ImageView
+            android:id="@android:id/icon"
+            android:adjustViewBounds="true"
+            android:maxHeight="24dp"
+            android:maxWidth="24dp"
+            android:layout_gravity="center_horizontal"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content" />
+
+        <android.support.v7.widget.DialogTitle
+            android:id="@+id/alertTitle"
+            style="?android:attr/windowTitleStyle"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:gravity="center" />
+
+    </LinearLayout>
+
+    <android.support.v4.widget.Space
+        android:id="@+id/titleDividerNoCustom"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/abc_dialog_title_divider_material"
+        android:visibility="gone"/>
+</LinearLayout>
diff --git a/v7/appcompat/res/layout/tooltip.xml b/v7/appcompat/res/layout/abc_tooltip.xml
similarity index 100%
rename from v7/appcompat/res/layout/tooltip.xml
rename to v7/appcompat/res/layout/abc_tooltip.xml
diff --git a/v7/appcompat/res/values-watch/themes_base.xml b/v7/appcompat/res/values-watch/themes_base.xml
new file mode 100644
index 0000000..20d8a7b
--- /dev/null
+++ b/v7/appcompat/res/values-watch/themes_base.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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 name="Base.Theme.AppCompat.Dialog" parent="Base.V7.Theme.AppCompat.Dialog" >
+        <item name="android:windowIsFloating">false</item>
+    </style>
+    <style name="Base.Theme.AppCompat.Light.Dialog" parent="Base.V7.Theme.AppCompat.Light.Dialog" >
+        <item name="android:windowIsFloating">false</item>
+    </style>
+    <style name="Base.ThemeOverlay.AppCompat.Dialog" parent="Base.V7.ThemeOverlay.AppCompat.Dialog" >
+        <item name="android:windowIsFloating">false</item>
+    </style>
+</resources>
\ No newline at end of file
diff --git a/v7/appcompat/res/values/attrs.xml b/v7/appcompat/res/values/attrs.xml
index 52ae694..2012a3a 100644
--- a/v7/appcompat/res/values/attrs.xml
+++ b/v7/appcompat/res/values/attrs.xml
@@ -407,6 +407,8 @@
         <!-- Color used for error states and things that need to be drawn to
              the user's attention. -->
         <attr name="colorError" format="reference|color" />
+
+        <attr name="viewInflaterClass" format="string" />
     </declare-styleable>
 
 
diff --git a/v7/appcompat/res/values/styles_base.xml b/v7/appcompat/res/values/styles_base.xml
index 2b2db12..adaaae0 100644
--- a/v7/appcompat/res/values/styles_base.xml
+++ b/v7/appcompat/res/values/styles_base.xml
@@ -525,8 +525,8 @@
     <style name="Base.AlertDialog.AppCompat.Light" parent="Base.AlertDialog.AppCompat" />
 
     <style name="Base.Animation.AppCompat.Tooltip" parent="android:Animation">
-        <item name="android:windowEnterAnimation">@anim/tooltip_enter</item>
-        <item name="android:windowExitAnimation">@anim/tooltip_exit</item>
+        <item name="android:windowEnterAnimation">@anim/abc_tooltip_enter</item>
+        <item name="android:windowExitAnimation">@anim/abc_tooltip_exit</item>
     </style>
 
 </resources>
diff --git a/v7/appcompat/res/values/themes_base.xml b/v7/appcompat/res/values/themes_base.xml
index a5be8ad..a9acfce 100644
--- a/v7/appcompat/res/values/themes_base.xml
+++ b/v7/appcompat/res/values/themes_base.xml
@@ -119,6 +119,7 @@
 
     <!-- Base platform-dependent theme providing an action bar in a dark-themed activity. -->
     <style name="Base.V7.Theme.AppCompat" parent="Platform.AppCompat">
+        <item name="viewInflaterClass">android.support.v7.app.AppCompatViewInflater</item>
         <item name="windowNoTitle">false</item>
         <item name="windowActionBar">true</item>
         <item name="windowActionBarOverlay">false</item>
@@ -287,6 +288,8 @@
 
     <!-- Base platform-dependent theme providing an action bar in a light-themed activity. -->
     <style name="Base.V7.Theme.AppCompat.Light" parent="Platform.AppCompat.Light">
+        <item name="viewInflaterClass">android.support.v7.app.AppCompatViewInflater</item>
+
         <item name="windowNoTitle">false</item>
         <item name="windowActionBar">true</item>
         <item name="windowActionBarOverlay">false</item>
diff --git a/v7/appcompat/src/main/java/android/support/v7/app/AppCompatDelegateImplV9.java b/v7/appcompat/src/main/java/android/support/v7/app/AppCompatDelegateImplV9.java
index 056e33e..5b53401 100644
--- a/v7/appcompat/src/main/java/android/support/v7/app/AppCompatDelegateImplV9.java
+++ b/v7/appcompat/src/main/java/android/support/v7/app/AppCompatDelegateImplV9.java
@@ -1001,7 +1001,26 @@
     public View createView(View parent, final String name, @NonNull Context context,
             @NonNull AttributeSet attrs) {
         if (mAppCompatViewInflater == null) {
-            mAppCompatViewInflater = new AppCompatViewInflater();
+            TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
+            String viewInflaterClassName =
+                    a.getString(R.styleable.AppCompatTheme_viewInflaterClass);
+            if ((viewInflaterClassName == null)
+                    || AppCompatViewInflater.class.getName().equals(viewInflaterClassName)) {
+                // Either default class name or set explicitly to null. In both cases
+                // create the base inflater (no reflection)
+                mAppCompatViewInflater = new AppCompatViewInflater();
+            } else {
+                try {
+                    Class viewInflaterClass = Class.forName(viewInflaterClassName);
+                    mAppCompatViewInflater =
+                            (AppCompatViewInflater) viewInflaterClass.getDeclaredConstructor()
+                                    .newInstance();
+                } catch (Throwable t) {
+                    Log.i(TAG, "Failed to instantiate custom view inflater "
+                            + viewInflaterClassName + ". Falling back to default.", t);
+                    mAppCompatViewInflater = new AppCompatViewInflater();
+                }
+            }
         }
 
         boolean inheritContext = false;
diff --git a/v7/appcompat/src/main/java/android/support/v7/app/AppCompatViewInflater.java b/v7/appcompat/src/main/java/android/support/v7/app/AppCompatViewInflater.java
index 54d01bc..87a1a3c 100644
--- a/v7/appcompat/src/main/java/android/support/v7/app/AppCompatViewInflater.java
+++ b/v7/appcompat/src/main/java/android/support/v7/app/AppCompatViewInflater.java
@@ -51,14 +51,12 @@
 import java.util.Map;
 
 /**
- * This class is responsible for manually inflating our tinted widgets which are used on devices
- * running {@link android.os.Build.VERSION_CODES#KITKAT KITKAT} or below. As such, this class
- * should only be used when running on those devices.
+ * This class is responsible for manually inflating our tinted widgets.
  * <p>This class two main responsibilities: the first is to 'inject' our tinted views in place of
  * the framework versions in layout inflation; the second is backport the {@code android:theme}
  * functionality for any inflated widgets. This include theme inheritance from its parent.
  */
-class AppCompatViewInflater {
+public class AppCompatViewInflater {
 
     private static final Class<?>[] sConstructorSignature = new Class[]{
             Context.class, AttributeSet.class};
@@ -77,7 +75,7 @@
 
     private final Object[] mConstructorArgs = new Object[2];
 
-    public final View createView(View parent, final String name, @NonNull Context context,
+    final View createView(View parent, final String name, @NonNull Context context,
             @NonNull AttributeSet attrs, boolean inheritContext,
             boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
         final Context originalContext = context;
@@ -100,44 +98,63 @@
         // We need to 'inject' our tint aware Views in place of the standard framework versions
         switch (name) {
             case "TextView":
-                view = new AppCompatTextView(context, attrs);
+                view = createTextView(context, attrs);
+                verifyNotNull(view, name);
                 break;
             case "ImageView":
-                view = new AppCompatImageView(context, attrs);
+                view = createImageView(context, attrs);
+                verifyNotNull(view, name);
                 break;
             case "Button":
-                view = new AppCompatButton(context, attrs);
+                view = createButton(context, attrs);
+                verifyNotNull(view, name);
                 break;
             case "EditText":
-                view = new AppCompatEditText(context, attrs);
+                view = createEditText(context, attrs);
+                verifyNotNull(view, name);
                 break;
             case "Spinner":
-                view = new AppCompatSpinner(context, attrs);
+                view = createSpinner(context, attrs);
+                verifyNotNull(view, name);
                 break;
             case "ImageButton":
-                view = new AppCompatImageButton(context, attrs);
+                view = createImageButton(context, attrs);
+                verifyNotNull(view, name);
                 break;
             case "CheckBox":
-                view = new AppCompatCheckBox(context, attrs);
+                view = createCheckBox(context, attrs);
+                verifyNotNull(view, name);
                 break;
             case "RadioButton":
-                view = new AppCompatRadioButton(context, attrs);
+                view = createRadioButton(context, attrs);
+                verifyNotNull(view, name);
                 break;
             case "CheckedTextView":
-                view = new AppCompatCheckedTextView(context, attrs);
+                view = createCheckedTextView(context, attrs);
+                verifyNotNull(view, name);
                 break;
             case "AutoCompleteTextView":
-                view = new AppCompatAutoCompleteTextView(context, attrs);
+                view = createAutoCompleteTextView(context, attrs);
+                verifyNotNull(view, name);
                 break;
             case "MultiAutoCompleteTextView":
-                view = new AppCompatMultiAutoCompleteTextView(context, attrs);
+                view = createMultiAutoCompleteTextView(context, attrs);
+                verifyNotNull(view, name);
                 break;
             case "RatingBar":
-                view = new AppCompatRatingBar(context, attrs);
+                view = createRatingBar(context, attrs);
+                verifyNotNull(view, name);
                 break;
             case "SeekBar":
-                view = new AppCompatSeekBar(context, attrs);
+                view = createSeekBar(context, attrs);
+                verifyNotNull(view, name);
                 break;
+            default:
+                // The fallback that allows extending class to take over view inflation
+                // for other tags. Note that we don't check that the result is not-null.
+                // That allows the custom inflater path to fall back on the default one
+                // later in this method.
+                view = createView(context, name, attrs);
         }
 
         if (view == null && originalContext != context) {
@@ -154,6 +171,85 @@
         return view;
     }
 
+    @NonNull
+    protected AppCompatTextView createTextView(Context context, AttributeSet attrs) {
+        return new AppCompatTextView(context, attrs);
+    }
+
+    @NonNull
+    protected AppCompatImageView createImageView(Context context, AttributeSet attrs) {
+        return new AppCompatImageView(context, attrs);
+    }
+
+    @NonNull
+    protected AppCompatButton createButton(Context context, AttributeSet attrs) {
+        return new AppCompatButton(context, attrs);
+    }
+
+    @NonNull
+    protected AppCompatEditText createEditText(Context context, AttributeSet attrs) {
+        return new AppCompatEditText(context, attrs);
+    }
+
+    @NonNull
+    protected AppCompatSpinner createSpinner(Context context, AttributeSet attrs) {
+        return new AppCompatSpinner(context, attrs);
+    }
+
+    @NonNull
+    protected AppCompatImageButton createImageButton(Context context, AttributeSet attrs) {
+        return new AppCompatImageButton(context, attrs);
+    }
+
+    @NonNull
+    protected AppCompatCheckBox createCheckBox(Context context, AttributeSet attrs) {
+        return new AppCompatCheckBox(context, attrs);
+    }
+
+    @NonNull
+    protected AppCompatRadioButton createRadioButton(Context context, AttributeSet attrs) {
+        return new AppCompatRadioButton(context, attrs);
+    }
+
+    @NonNull
+    protected AppCompatCheckedTextView createCheckedTextView(Context context, AttributeSet attrs) {
+        return new AppCompatCheckedTextView(context, attrs);
+    }
+
+    @NonNull
+    protected AppCompatAutoCompleteTextView createAutoCompleteTextView(Context context,
+            AttributeSet attrs) {
+        return new AppCompatAutoCompleteTextView(context, attrs);
+    }
+
+    @NonNull
+    protected AppCompatMultiAutoCompleteTextView createMultiAutoCompleteTextView(Context context,
+            AttributeSet attrs) {
+        return new AppCompatMultiAutoCompleteTextView(context, attrs);
+    }
+
+    @NonNull
+    protected AppCompatRatingBar createRatingBar(Context context, AttributeSet attrs) {
+        return new AppCompatRatingBar(context, attrs);
+    }
+
+    @NonNull
+    protected AppCompatSeekBar createSeekBar(Context context, AttributeSet attrs) {
+        return new AppCompatSeekBar(context, attrs);
+    }
+
+    private void verifyNotNull(View view, String name) {
+        if (view == null) {
+            throw new IllegalStateException(this.getClass().getName()
+                    + " asked to inflate view for <" + name + ">, but returned null");
+        }
+    }
+
+    @Nullable
+    protected View createView(Context context, String name, AttributeSet attrs) {
+        return null;
+    }
+
     private View createViewFromTag(Context context, String name, AttributeSet attrs) {
         if (name.equals("view")) {
             name = attrs.getAttributeValue(null, "class");
@@ -165,14 +261,14 @@
 
             if (-1 == name.indexOf('.')) {
                 for (int i = 0; i < sClassPrefixList.length; i++) {
-                    final View view = createView(context, name, sClassPrefixList[i]);
+                    final View view = createViewByPrefix(context, name, sClassPrefixList[i]);
                     if (view != null) {
                         return view;
                     }
                 }
                 return null;
             } else {
-                return createView(context, name, null);
+                return createViewByPrefix(context, name, null);
             }
         } catch (Exception e) {
             // We do not want to catch these, lets return null and let the actual LayoutInflater
@@ -209,7 +305,7 @@
         a.recycle();
     }
 
-    private View createView(Context context, String name, String prefix)
+    private View createViewByPrefix(Context context, String name, String prefix)
             throws ClassNotFoundException, InflateException {
         Constructor<? extends View> constructor = sConstructorMap.get(name);
 
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/src/main/java/android/support/v7/widget/TooltipPopup.java b/v7/appcompat/src/main/java/android/support/v7/widget/TooltipPopup.java
index f707c8f..dc20aa1 100644
--- a/v7/appcompat/src/main/java/android/support/v7/widget/TooltipPopup.java
+++ b/v7/appcompat/src/main/java/android/support/v7/widget/TooltipPopup.java
@@ -56,7 +56,7 @@
     TooltipPopup(Context context) {
         mContext = context;
 
-        mContentView = LayoutInflater.from(mContext).inflate(R.layout.tooltip, null);
+        mContentView = LayoutInflater.from(mContext).inflate(R.layout.abc_tooltip, null);
         mMessageView = (TextView) mContentView.findViewById(R.id.message);
 
         mLayoutParams.setTitle(getClass().getSimpleName());
diff --git a/v7/appcompat/tests/AndroidManifest.xml b/v7/appcompat/tests/AndroidManifest.xml
index f986869..a85e4ab 100644
--- a/v7/appcompat/tests/AndroidManifest.xml
+++ b/v7/appcompat/tests/AndroidManifest.xml
@@ -121,6 +121,21 @@
         <activity
             android:name="android.support.v7.app.AppCompatVectorDrawableIntegrationActivity"/>
 
+        <activity
+            android:name="android.support.v7.app.AppCompatInflaterDefaultActivity"/>
+
+        <activity
+            android:name="android.support.v7.app.AppCompatInflaterBadClassNameActivity"
+            android:theme="@style/Theme.CustomInflaterBadClassName"/>
+
+        <activity
+            android:name="android.support.v7.app.AppCompatInflaterNullActivity"
+            android:theme="@style/Theme.CustomInflaterNull"/>
+
+        <activity
+            android:name="android.support.v7.app.AppCompatInflaterCustomActivity"
+            android:theme="@style/Theme.CustomInflater"/>
+
     </application>
 
 </manifest>
diff --git a/v7/appcompat/tests/res/drawable/black_rect.xml b/v7/appcompat/tests/res/drawable/black_rect.xml
new file mode 100644
index 0000000..d1cd0c2
--- /dev/null
+++ b/v7/appcompat/tests/res/drawable/black_rect.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.
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <solid android:color="@android:color/black" />
+</shape>
\ No newline at end of file
diff --git a/v7/appcompat/tests/res/layout/appcompat_inflater_activity.xml b/v7/appcompat/tests/res/layout/appcompat_inflater_activity.xml
new file mode 100644
index 0000000..3a27041
--- /dev/null
+++ b/v7/appcompat/tests/res/layout/appcompat_inflater_activity.xml
@@ -0,0 +1,84 @@
+<?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.
+  -->
+
+<ScrollView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/container"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical">
+
+        <Button
+            android:id="@+id/button"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/sample_text1" />
+
+        <android.support.v7.widget.AppCompatButton
+            android:id="@+id/ac_button"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/sample_text1" />
+
+        <TextView
+            android:id="@+id/textview"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/sample_text2" />
+
+        <android.support.v7.widget.AppCompatTextView
+            android:id="@+id/ac_textview"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/sample_text2" />
+
+        <RadioButton
+            android:id="@+id/radiobutton"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/sample_text1" />
+
+        <ImageButton
+            android:id="@+id/imagebutton"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:src="@drawable/test_drawable_blue" />
+
+        <Spinner
+            android:id="@+id/spinner"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:entries="@array/planets_array" />
+
+        <ToggleButton
+            android:id="@+id/togglebutton"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/sample_text1" />
+
+        <ScrollView
+            android:id="@+id/scrollview"
+            android:layout_width="match_parent"
+            android:layout_height="100dp" />
+
+    </LinearLayout>
+
+</ScrollView>
diff --git a/v7/appcompat/tests/res/layout/appcompat_textview_activity.xml b/v7/appcompat/tests/res/layout/appcompat_textview_activity.xml
index 3841206..1ca3459 100644
--- a/v7/appcompat/tests/res/layout/appcompat_textview_activity.xml
+++ b/v7/appcompat/tests/res/layout/appcompat_textview_activity.xml
@@ -77,6 +77,13 @@
             android:background="@drawable/test_background_green" />
 
         <android.support.v7.widget.AppCompatTextView
+            android:id="@+id/view_untinted_deferred"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/sample_text2"
+            android:background="@drawable/black_rect" />
+
+        <android.support.v7.widget.AppCompatTextView
             android:id="@+id/view_text_color_hex"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
diff --git a/v7/appcompat/tests/res/values/styles.xml b/v7/appcompat/tests/res/values/styles.xml
index 68aa09f..b9fa921 100644
--- a/v7/appcompat/tests/res/values/styles.xml
+++ b/v7/appcompat/tests/res/values/styles.xml
@@ -31,6 +31,18 @@
         <item name="android:textColorLink">@color/color_state_list_link</item>
     </style>
 
+    <style name="Theme.CustomInflater" parent="@style/Theme.AppCompat.Light">
+        <item name="viewInflaterClass">android.support.v7.app.inflater.CustomViewInflater</item>
+    </style>
+
+    <style name="Theme.CustomInflaterBadClassName" parent="@style/Theme.AppCompat.Light">
+        <item name="viewInflaterClass">invalid.class.name</item>
+    </style>
+
+    <style name="Theme.CustomInflaterNull" parent="@style/Theme.AppCompat.Light">
+        <item name="viewInflaterClass">@null</item>
+    </style>
+
     <style name="TextStyleAllCapsOn" parent="@android:style/TextAppearance.Medium">
         <item name="textAllCaps">true</item>
     </style>
diff --git a/media-compat-test-lib/build.gradle b/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterBadClassNameActivity.java
similarity index 64%
copy from media-compat-test-lib/build.gradle
copy to v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterBadClassNameActivity.java
index 26594e5..2d64277 100644
--- a/media-compat-test-lib/build.gradle
+++ b/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterBadClassNameActivity.java
@@ -13,5 +13,14 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package android.support.v7.app;
 
-apply plugin: 'java'
+import android.support.v7.appcompat.test.R;
+import android.support.v7.testutils.BaseTestActivity;
+
+public class AppCompatInflaterBadClassNameActivity extends BaseTestActivity {
+    @Override
+    protected int getContentViewLayoutResId() {
+        return R.layout.appcompat_inflater_activity;
+    }
+}
diff --git a/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterBadClassNameTest.java b/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterBadClassNameTest.java
new file mode 100644
index 0000000..5f6e824
--- /dev/null
+++ b/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterBadClassNameTest.java
@@ -0,0 +1,30 @@
+/*
+ * 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.app;
+
+import android.support.test.filters.SmallTest;
+
+/**
+ * Testing the default view inflation where appcompat views are used for specific
+ * views in layouts specified in XML.
+ */
+@SmallTest
+public class AppCompatInflaterBadClassNameTest extends
+        AppCompatInflaterPassTest<AppCompatInflaterBadClassNameActivity> {
+    public AppCompatInflaterBadClassNameTest() {
+        super(AppCompatInflaterBadClassNameActivity.class);
+    }
+}
diff --git a/media-compat-test-lib/build.gradle b/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterCustomActivity.java
similarity index 64%
copy from media-compat-test-lib/build.gradle
copy to v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterCustomActivity.java
index 26594e5..7ec9cdc 100644
--- a/media-compat-test-lib/build.gradle
+++ b/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterCustomActivity.java
@@ -13,5 +13,14 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package android.support.v7.app;
 
-apply plugin: 'java'
+import android.support.v7.appcompat.test.R;
+import android.support.v7.testutils.BaseTestActivity;
+
+public class AppCompatInflaterCustomActivity extends BaseTestActivity {
+    @Override
+    protected int getContentViewLayoutResId() {
+        return R.layout.appcompat_inflater_activity;
+    }
+}
diff --git a/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterCustomTest.java b/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterCustomTest.java
new file mode 100644
index 0000000..1e66635
--- /dev/null
+++ b/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterCustomTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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.app;
+
+import static org.junit.Assert.assertEquals;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.v7.app.inflater.CustomViewInflater;
+import android.support.v7.appcompat.test.R;
+import android.support.v7.widget.AppCompatButton;
+import android.support.v7.widget.AppCompatRadioButton;
+import android.support.v7.widget.AppCompatSpinner;
+import android.support.v7.widget.AppCompatTextView;
+import android.view.ViewGroup;
+import android.widget.ScrollView;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+/**
+ * Testing the custom view inflation where app-specific views are used for specific
+ * views in layouts specified in XML.
+ */
+@SmallTest
+public class AppCompatInflaterCustomTest {
+    private ViewGroup mContainer;
+    private AppCompatInflaterCustomActivity mActivity;
+
+    @Rule
+    public final ActivityTestRule<AppCompatInflaterCustomActivity> mActivityTestRule =
+            new ActivityTestRule<>(AppCompatInflaterCustomActivity.class);
+
+    @Before
+    public void setUp() {
+        mActivity = mActivityTestRule.getActivity();
+        mContainer = mActivity.findViewById(R.id.container);
+    }
+
+    @Test
+    public void testViewClasses() {
+        // View defined as <Button> should be inflated as CustomButton
+        assertEquals(CustomViewInflater.CustomButton.class,
+                mContainer.findViewById(R.id.button).getClass());
+
+        // View defined as <AppCompatButton> should be inflated as AppCompatButton
+        assertEquals(AppCompatButton.class, mContainer.findViewById(R.id.ac_button).getClass());
+
+        // View defined as <TextView> should be inflated as CustomTextView
+        assertEquals(CustomViewInflater.CustomTextView.class,
+                mContainer.findViewById(R.id.textview).getClass());
+
+        // View defined as <AppCompatTextView> should be inflated as AppCompatTextView
+        assertEquals(AppCompatTextView.class, mContainer.findViewById(R.id.ac_textview).getClass());
+
+        // View defined as <RadioButton> should be inflated as AppCompatRadioButton
+        assertEquals(AppCompatRadioButton.class,
+                mContainer.findViewById(R.id.radiobutton).getClass());
+
+        // View defined as <ImageButton> should be inflated as CustomImageButton
+        assertEquals(CustomViewInflater.CustomImageButton.class,
+                mContainer.findViewById(R.id.imagebutton).getClass());
+
+        // View defined as <Spinner> should be inflated as AppCompatSpinner
+        assertEquals(AppCompatSpinner.class, mContainer.findViewById(R.id.spinner).getClass());
+
+        // View defined as <ToggleButton> should be inflated as CustomToggleButton
+        assertEquals(CustomViewInflater.CustomToggleButton.class,
+                mContainer.findViewById(R.id.togglebutton).getClass());
+
+        // View defined as <ScrollView> should be inflated as ScrollView
+        assertEquals(ScrollView.class,
+                mContainer.findViewById(R.id.scrollview).getClass());
+    }
+
+}
diff --git a/media-compat-test-lib/build.gradle b/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterDefaultActivity.java
similarity index 64%
copy from media-compat-test-lib/build.gradle
copy to v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterDefaultActivity.java
index 26594e5..28b99ce 100644
--- a/media-compat-test-lib/build.gradle
+++ b/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterDefaultActivity.java
@@ -13,5 +13,14 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package android.support.v7.app;
 
-apply plugin: 'java'
+import android.support.v7.appcompat.test.R;
+import android.support.v7.testutils.BaseTestActivity;
+
+public class AppCompatInflaterDefaultActivity extends BaseTestActivity {
+    @Override
+    protected int getContentViewLayoutResId() {
+        return R.layout.appcompat_inflater_activity;
+    }
+}
diff --git a/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterDefaultTest.java b/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterDefaultTest.java
new file mode 100644
index 0000000..d96060b
--- /dev/null
+++ b/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterDefaultTest.java
@@ -0,0 +1,30 @@
+/*
+ * 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.app;
+
+import android.support.test.filters.SmallTest;
+
+/**
+ * Testing the default view inflation where appcompat views are used for specific
+ * views in layouts specified in XML.
+ */
+@SmallTest
+public class AppCompatInflaterDefaultTest extends
+        AppCompatInflaterPassTest<AppCompatInflaterDefaultActivity> {
+    public AppCompatInflaterDefaultTest() {
+        super(AppCompatInflaterDefaultActivity.class);
+    }
+}
diff --git a/media-compat-test-lib/build.gradle b/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterNullActivity.java
similarity index 65%
copy from media-compat-test-lib/build.gradle
copy to v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterNullActivity.java
index 26594e5..db75790 100644
--- a/media-compat-test-lib/build.gradle
+++ b/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterNullActivity.java
@@ -13,5 +13,14 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package android.support.v7.app;
 
-apply plugin: 'java'
+import android.support.v7.appcompat.test.R;
+import android.support.v7.testutils.BaseTestActivity;
+
+public class AppCompatInflaterNullActivity extends BaseTestActivity {
+    @Override
+    protected int getContentViewLayoutResId() {
+        return R.layout.appcompat_inflater_activity;
+    }
+}
diff --git a/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterNullTest.java b/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterNullTest.java
new file mode 100644
index 0000000..b1d39e1
--- /dev/null
+++ b/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterNullTest.java
@@ -0,0 +1,30 @@
+/*
+ * 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.app;
+
+import android.support.test.filters.SmallTest;
+
+/**
+ * Testing the default view inflation where appcompat views are used for specific
+ * views in layouts specified in XML.
+ */
+@SmallTest
+public class AppCompatInflaterNullTest extends
+        AppCompatInflaterPassTest<AppCompatInflaterNullActivity> {
+    public AppCompatInflaterNullTest() {
+        super(AppCompatInflaterNullActivity.class);
+    }
+}
diff --git a/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterPassTest.java b/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterPassTest.java
new file mode 100644
index 0000000..e8a2763
--- /dev/null
+++ b/v7/appcompat/tests/src/android/support/v7/app/AppCompatInflaterPassTest.java
@@ -0,0 +1,96 @@
+/*
+ * 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.app;
+
+import static org.junit.Assert.assertEquals;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v7.appcompat.test.R;
+import android.support.v7.testutils.BaseTestActivity;
+import android.support.v7.widget.AppCompatButton;
+import android.support.v7.widget.AppCompatImageButton;
+import android.support.v7.widget.AppCompatRadioButton;
+import android.support.v7.widget.AppCompatSpinner;
+import android.support.v7.widget.AppCompatTextView;
+import android.view.ViewGroup;
+import android.widget.ScrollView;
+import android.widget.ToggleButton;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Testing the default view inflation where appcompat views are used for specific
+ * views in layouts specified in XML.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public abstract class AppCompatInflaterPassTest<A extends BaseTestActivity> {
+    private ViewGroup mContainer;
+    private A mActivity;
+
+    @Rule
+    public final ActivityTestRule<A> mActivityTestRule;
+
+    public AppCompatInflaterPassTest(Class clazz) {
+        mActivityTestRule = new ActivityTestRule<A>(clazz);
+    }
+
+    @Before
+    public void setUp() {
+        mActivity = mActivityTestRule.getActivity();
+        mContainer = mActivity.findViewById(R.id.container);
+    }
+
+    @Test
+    public void testViewClasses() {
+        // View defined as <Button> should be inflated as AppCompatButton
+        assertEquals(AppCompatButton.class, mContainer.findViewById(R.id.button).getClass());
+
+        // View defined as <AppCompatButton> should be inflated as AppCompatButton
+        assertEquals(AppCompatButton.class, mContainer.findViewById(R.id.ac_button).getClass());
+
+        // View defined as <TextView> should be inflated as AppCompatTextView
+        assertEquals(AppCompatTextView.class, mContainer.findViewById(R.id.textview).getClass());
+
+        // View defined as <AppCompatTextView> should be inflated as AppCompatTextView
+        assertEquals(AppCompatTextView.class, mContainer.findViewById(R.id.ac_textview).getClass());
+
+        // View defined as <RadioButton> should be inflated as AppCompatRadioButton
+        assertEquals(AppCompatRadioButton.class,
+                mContainer.findViewById(R.id.radiobutton).getClass());
+
+        // View defined as <ImageButton> should be inflated as AppCompatImageButton
+        assertEquals(AppCompatImageButton.class,
+                mContainer.findViewById(R.id.imagebutton).getClass());
+
+        // View defined as <Spinner> should be inflated as AppCompatSpinner
+        assertEquals(AppCompatSpinner.class, mContainer.findViewById(R.id.spinner).getClass());
+
+        // View defined as <ToggleButton> should be inflated as ToggleButton
+        assertEquals(ToggleButton.class,
+                mContainer.findViewById(R.id.togglebutton).getClass());
+
+        // View defined as <ScrollView> should be inflated as ScrollView
+        assertEquals(ScrollView.class,
+                mContainer.findViewById(R.id.scrollview).getClass());
+    }
+
+}
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..d42174f 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,15 @@
 import static android.support.v7.app.NightModeActivity.TOP_ACTIVITY;
 import static android.support.v7.testutils.TestUtilsMatchers.isBackground;
 
-import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 
 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.testutils.RecreatedAppCompatActivity;
 import android.support.v4.content.ContextCompat;
 import android.support.v7.appcompat.test.R;
 
@@ -38,6 +40,9 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
 @LargeTest
 @RunWith(AndroidJUnit4.class)
 public class NightModeTestCase {
@@ -100,30 +105,31 @@
         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);
+        // Set MODE_NIGHT_AUTO so that we will change to night mode automatically
+        final NightModeActivity newActivity =
+                setLocalNightModeAndWaitForRecreate(activity, AppCompatDelegate.MODE_NIGHT_AUTO);
+        final AppCompatDelegateImplV14 newDelegate =
+                (AppCompatDelegateImplV14) newActivity.getDelegate();
 
-        // Assert that the original Activity has not been destroyed yet
-        assertFalse(activity.isDestroyed());
-
-        // Now update the fake twilight manager to be in night and trigger a fake 'time' change
+        // 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
-        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+        RecreatedAppCompatActivity.sResumed = new CountDownLatch(1);
+        assertTrue(RecreatedAppCompatActivity.sResumed.await(1, TimeUnit.SECONDS));
+        // At this point recreate that has been triggered by dispatchTimeChanged call
+        // has completed
 
-        // Now check that the text has changed, signifying that night resources are being used
+        // Check that the text has changed, signifying that night resources are being used
         onView(withId(R.id.text_night_mode)).check(matches(withText(STRING_NIGHT)));
     }
 
@@ -133,28 +139,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 +187,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 +197,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/app/inflater/CustomViewInflater.java b/v7/appcompat/tests/src/android/support/v7/app/inflater/CustomViewInflater.java
new file mode 100644
index 0000000..7876499
--- /dev/null
+++ b/v7/appcompat/tests/src/android/support/v7/app/inflater/CustomViewInflater.java
@@ -0,0 +1,123 @@
+/*
+ * 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.app.inflater;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v7.app.AppCompatViewInflater;
+import android.support.v7.widget.AppCompatButton;
+import android.support.v7.widget.AppCompatImageButton;
+import android.support.v7.widget.AppCompatTextView;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.ToggleButton;
+
+/**
+ * Custom view inflater that takes over the inflation of a few widget types.
+ */
+public class CustomViewInflater extends AppCompatViewInflater {
+    public static class CustomTextView extends AppCompatTextView {
+        public CustomTextView(Context context) {
+            super(context);
+        }
+
+        public CustomTextView(Context context,
+                @Nullable AttributeSet attrs) {
+            super(context, attrs);
+        }
+
+        public CustomTextView(Context context,
+                @Nullable AttributeSet attrs, int defStyleAttr) {
+            super(context, attrs, defStyleAttr);
+        }
+    }
+
+    public static class CustomButton extends AppCompatButton {
+        public CustomButton(Context context) {
+            super(context);
+        }
+
+        public CustomButton(Context context, AttributeSet attrs) {
+            super(context, attrs);
+        }
+
+        public CustomButton(Context context, AttributeSet attrs, int defStyleAttr) {
+            super(context, attrs, defStyleAttr);
+        }
+    }
+
+    public static class CustomImageButton extends AppCompatImageButton {
+        public CustomImageButton(Context context) {
+            super(context);
+        }
+
+        public CustomImageButton(Context context, AttributeSet attrs) {
+            super(context, attrs);
+        }
+
+        public CustomImageButton(Context context, AttributeSet attrs, int defStyleAttr) {
+            super(context, attrs, defStyleAttr);
+        }
+    }
+
+    public static class CustomToggleButton extends ToggleButton {
+        public CustomToggleButton(Context context, AttributeSet attrs, int defStyleAttr,
+                int defStyleRes) {
+            super(context, attrs, defStyleAttr, defStyleRes);
+        }
+
+        public CustomToggleButton(Context context, AttributeSet attrs, int defStyleAttr) {
+            super(context, attrs, defStyleAttr);
+        }
+
+        public CustomToggleButton(Context context, AttributeSet attrs) {
+            super(context, attrs);
+        }
+
+        public CustomToggleButton(Context context) {
+            super(context);
+        }
+    }
+
+    @NonNull
+    @Override
+    protected AppCompatButton createButton(Context context, AttributeSet attrs) {
+        return new CustomButton(context, attrs);
+    }
+
+    @NonNull
+    @Override
+    protected AppCompatTextView createTextView(Context context, AttributeSet attrs) {
+        return new CustomTextView(context, attrs);
+    }
+
+    @NonNull
+    @Override
+    protected AppCompatImageButton createImageButton(Context context, AttributeSet attrs) {
+        return new CustomImageButton(context, attrs);
+    }
+
+    @Nullable
+    @Override
+    protected View createView(Context context, String name, AttributeSet attrs) {
+        if (name.equals("ToggleButton")) {
+            return new CustomToggleButton(context, attrs);
+        }
+        return null;
+    }
+}
diff --git a/v7/appcompat/tests/src/android/support/v7/app/inflater/MisbehavingViewInflater.java b/v7/appcompat/tests/src/android/support/v7/app/inflater/MisbehavingViewInflater.java
new file mode 100644
index 0000000..21c4ffc
--- /dev/null
+++ b/v7/appcompat/tests/src/android/support/v7/app/inflater/MisbehavingViewInflater.java
@@ -0,0 +1,36 @@
+/*
+ * 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.app.inflater;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.v7.app.AppCompatViewInflater;
+import android.support.v7.widget.AppCompatButton;
+import android.util.AttributeSet;
+
+/**
+ * Custom view inflater that declares that it takes over the view inflation but
+ * does not honor the contract to return non-null instance in its
+ * {@link #createButton(Context, AttributeSet)} method.
+ */
+public class MisbehavingViewInflater extends AppCompatViewInflater {
+    @NonNull
+    @Override
+    protected AppCompatButton createButton(Context context, AttributeSet attrs) {
+        return null;
+    }
+}
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/appcompat/tests/src/android/support/v7/testutils/TestUtils.java b/v7/appcompat/tests/src/android/support/v7/testutils/TestUtils.java
index 574ed6b..6e4516e 100644
--- a/v7/appcompat/tests/src/android/support/v7/testutils/TestUtils.java
+++ b/v7/appcompat/tests/src/android/support/v7/testutils/TestUtils.java
@@ -221,7 +221,7 @@
                     + ": expected all drawable colors to be "
                     + formatColorToHex(color)
                     + " but at position (" + centerX + "," + centerY + ") out of ("
-                    + bitmap.getWidth() + "," + bitmap.getHeight() + ") found"
+                    + bitmap.getWidth() + "," + bitmap.getHeight() + ") found "
                     + formatColorToHex(colorAtCenterPixel);
             if (throwExceptionIfFails) {
                 throw new RuntimeException(mismatchDescription);
diff --git a/v7/appcompat/tests/src/android/support/v7/widget/AppCompatTextViewTest.java b/v7/appcompat/tests/src/android/support/v7/widget/AppCompatTextViewTest.java
index 34890ed..cbc3176 100644
--- a/v7/appcompat/tests/src/android/support/v7/widget/AppCompatTextViewTest.java
+++ b/v7/appcompat/tests/src/android/support/v7/widget/AppCompatTextViewTest.java
@@ -16,29 +16,39 @@
 package android.support.v7.widget;
 
 import static android.support.test.espresso.Espresso.onView;
+import static android.support.test.espresso.assertion.ViewAssertions.matches;
 import static android.support.test.espresso.matcher.ViewMatchers.withId;
 import static android.support.v7.testutils.TestUtilsActions.setEnabled;
 import static android.support.v7.testutils.TestUtilsActions.setTextAppearance;
+import static android.support.v7.testutils.TestUtilsMatchers.isBackground;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
 
 import android.content.pm.PackageManager;
 import android.content.res.ColorStateList;
 import android.graphics.Color;
 import android.graphics.Typeface;
 import android.os.Build;
+import android.support.annotation.ColorInt;
 import android.support.test.annotation.UiThreadTest;
+import android.support.test.filters.MediumTest;
 import android.support.test.filters.SmallTest;
 import android.support.v4.content.ContextCompat;
 import android.support.v4.content.res.ResourcesCompat;
+import android.support.v4.view.ViewCompat;
 import android.support.v4.widget.TextViewCompat;
 import android.support.v7.appcompat.test.R;
+import android.view.View;
 import android.widget.TextView;
 
 import org.junit.Test;
 
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
 /**
  * In addition to all tinting-related tests done by the base class, this class provides
  * tests specific to {@link AppCompatTextView} class.
@@ -51,6 +61,43 @@
         super(AppCompatTextViewActivity.class);
     }
 
+    /**
+     * This method tests that background tinting is applied when the call to
+     * {@link android.support.v4.view.ViewCompat#setBackgroundTintList(View, ColorStateList)}
+     * is done as a deferred event.
+     */
+    @Test
+    @MediumTest
+    public void testDeferredBackgroundTinting() throws Throwable {
+        onView(withId(R.id.view_untinted_deferred))
+                .check(matches(isBackground(0xff000000, true)));
+
+        final @ColorInt int oceanDefault = ResourcesCompat.getColor(
+                mResources, R.color.ocean_default, null);
+
+        final ColorStateList oceanColor = ResourcesCompat.getColorStateList(
+                mResources, R.color.color_state_list_ocean, null);
+
+        // Emulate delay in kicking off the call to ViewCompat.setBackgroundTintList
+        Thread.sleep(200);
+        final CountDownLatch latch = new CountDownLatch(1);
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                TextView view = mActivity.findViewById(R.id.view_untinted_deferred);
+                ViewCompat.setBackgroundTintList(view, oceanColor);
+                latch.countDown();
+            }
+        });
+
+        assertTrue(latch.await(2, TimeUnit.SECONDS));
+
+        // Check that the background has switched to the matching entry in the newly set
+        // color state list.
+        onView(withId(R.id.view_untinted_deferred))
+                .check(matches(isBackground(oceanDefault, true)));
+    }
+
     @Test
     public void testAllCaps() {
         final String text1 = mResources.getString(R.string.sample_text1);
diff --git a/v7/mediarouter/OWNERS b/v7/mediarouter/OWNERS
new file mode 100644
index 0000000..e67af3b
--- /dev/null
+++ b/v7/mediarouter/OWNERS
@@ -0,0 +1,3 @@
+akersten@google.com
+jaewan@google.com
+sungsoo@google.com
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..17cd472 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);
@@ -99,6 +106,7 @@
     method public abstract boolean areContentsTheSame(T2, T2);
     method public abstract boolean areItemsTheSame(T2, T2);
     method public abstract int compare(T2, T2);
+    method public java.lang.Object getChangePayload(T2, T2);
     method public abstract void onChanged(int, int);
     method public void onChanged(int, int, java.lang.Object);
   }
@@ -305,6 +313,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 +348,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 +427,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/util/SortedList.java b/v7/recyclerview/src/main/java/android/support/v7/util/SortedList.java
index c62d0ce..af000a1 100644
--- a/v7/recyclerview/src/main/java/android/support/v7/util/SortedList.java
+++ b/v7/recyclerview/src/main/java/android/support/v7/util/SortedList.java
@@ -16,6 +16,8 @@
 
 package android.support.v7.util;
 
+import android.support.annotation.Nullable;
+
 import java.lang.reflect.Array;
 import java.util.Arrays;
 import java.util.Collection;
@@ -315,7 +317,8 @@
                 newDataStart++;
                 mOldDataStart++;
                 if (!mCallback.areContentsTheSame(oldItem, newItem)) {
-                    mCallback.onChanged(mMergedSize - 1, 1);
+                    mCallback.onChanged(mMergedSize - 1, 1,
+                            mCallback.getChangePayload(oldItem, newItem));
                 }
             } else {
                 // Old item is lower than or equal to (but not the same as the new). Output it.
@@ -401,7 +404,7 @@
                     return index;
                 } else {
                     mData[index] = item;
-                    mCallback.onChanged(index, 1);
+                    mCallback.onChanged(index, 1, mCallback.getChangePayload(existing, item));
                     return index;
                 }
             }
@@ -488,13 +491,13 @@
             if (cmp == 0) {
                 mData[index] = item;
                 if (contentsChanged) {
-                    mCallback.onChanged(index, 1);
+                    mCallback.onChanged(index, 1, mCallback.getChangePayload(existing, item));
                 }
                 return;
             }
         }
         if (contentsChanged) {
-            mCallback.onChanged(index, 1);
+            mCallback.onChanged(index, 1, mCallback.getChangePayload(existing, item));
         }
         // TODO this done in 1 pass to avoid shifting twice.
         removeItemAtIndex(index, false);
@@ -741,6 +744,28 @@
          * @return True if the two items represent the same object or false if they are different.
          */
         abstract public boolean areItemsTheSame(T2 item1, T2 item2);
+
+        /**
+         * When {@link #areItemsTheSame(T2, T2)} returns {@code true} for two items and
+         * {@link #areContentsTheSame(T2, T2)} returns false for them, {@link Callback} calls this
+         * method to get a payload about the change.
+         * <p>
+         * For example, if you are using {@link Callback} with
+         * {@link android.support.v7.widget.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}.
+         *
+         * @param item1 The first item to check.
+         * @param item2 The second item to check.
+         * @return A payload object that represents the changes between the two items.
+         */
+        @Nullable
+        public Object getChangePayload(T2 item1, T2 item2) {
+            return null;
+        }
     }
 
     /**
@@ -801,6 +826,11 @@
         }
 
         @Override
+        public void onChanged(int position, int count, Object payload) {
+            mBatchingListUpdateCallback.onChanged(position, count, payload);
+        }
+
+        @Override
         public boolean areContentsTheSame(T2 oldItem, T2 newItem) {
             return mWrappedCallback.areContentsTheSame(oldItem, newItem);
         }
@@ -810,6 +840,12 @@
             return mWrappedCallback.areItemsTheSame(item1, item2);
         }
 
+        @Nullable
+        @Override
+        public Object getChangePayload(T2 item1, T2 item2) {
+            return mWrappedCallback.getChangePayload(item1, item2);
+        }
+
         /**
          * This method dispatches any pending event notifications to the wrapped Callback.
          * You <b>must</b> always call this method after you are done with editing the SortedList.
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..5a98612 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;
@@ -385,8 +386,8 @@
     private List<OnChildAttachStateChangeListener> mOnChildAttachStateListeners;
 
     /**
-     * Set to true when an adapter data set changed notification is received.
-     * In that case, we cannot run any animations since we don't know what happened until layout.
+     * True after an event occurs that signals that the entire data set has changed. In that case,
+     * we cannot run any animations since we don't know what happened until layout.
      *
      * Attached items are invalid until next layout, at which point layout will animate/replace
      * items as necessary, building up content from the (effectively) new adapter from scratch.
@@ -394,11 +395,20 @@
      * Cached items must be discarded when setting this to true, so that the cache may be freely
      * used by prefetching until the next layout occurs.
      *
-     * @see #setDataSetChangedAfterLayout()
+     * @see #processDataSetCompletelyChanged(boolean)
      */
     boolean mDataSetHasChangedAfterLayout = false;
 
     /**
+     * True after the data set has completely changed and
+     * {@link LayoutManager#onItemsChanged(RecyclerView)} should be called during the subsequent
+     * measure/layout.
+     *
+     * @see #processDataSetCompletelyChanged(boolean)
+     */
+    boolean mDispatchItemsChangedEvent = false;
+
+    /**
      * This variable is incremented during a dispatchLayout and/or scroll.
      * Some methods should not be called during these periods (e.g. adapter data change).
      * Doing so will create hard to find bugs so we better check it and throw an exception.
@@ -417,6 +427,8 @@
      */
     private int mDispatchScrollCounter = 0;
 
+    @NonNull
+    private EdgeEffectFactory mEdgeEffectFactory = new EdgeEffectFactory();
     private EdgeEffect mLeftGlow, mTopGlow, mRightGlow, mBottomGlow;
 
     ItemAnimator mItemAnimator = new DefaultItemAnimator();
@@ -1041,6 +1053,7 @@
         // bail out if layout is frozen
         setLayoutFrozen(false);
         setAdapterInternal(adapter, true, removeAndRecycleExistingViews);
+        processDataSetCompletelyChanged(true);
         requestLayout();
     }
     /**
@@ -1056,6 +1069,7 @@
         // bail out if layout is frozen
         setLayoutFrozen(false);
         setAdapterInternal(adapter, false, true);
+        processDataSetCompletelyChanged(false);
         requestLayout();
     }
 
@@ -1109,7 +1123,6 @@
         }
         mRecycler.onAdapterChanged(oldAdapter, mAdapter, compatibleWithPrevious);
         mState.mStructureChanged = true;
-        setDataSetChangedAfterLayout();
     }
 
     /**
@@ -2306,7 +2319,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 +2332,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 +2345,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 +2359,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 +2373,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.
@@ -3369,7 +3408,9 @@
             // Processing these items have no value since data set changed unexpectedly.
             // Instead, we just reset it.
             mAdapterHelper.reset();
-            mLayout.onItemsChanged(this);
+            if (mDispatchItemsChangedEvent) {
+                mLayout.onItemsChanged(this);
+            }
         }
         // simple animations are a subset of advanced animations (which will cause a
         // pre-layout step)
@@ -3792,6 +3833,7 @@
         mLayout.removeAndRecycleScrapInt(mRecycler);
         mState.mPreviousLayoutItemCount = mState.mItemCount;
         mDataSetHasChangedAfterLayout = false;
+        mDispatchItemsChangedEvent = false;
         mState.mRunSimpleAnimations = false;
 
         mState.mRunPredictiveAnimations = false;
@@ -4259,19 +4301,21 @@
                 viewHolder.getUnmodifiedPayloads());
     }
 
-
     /**
-     * Call this method to signal that *all* adapter content has changed (generally, because of
-     * setAdapter, swapAdapter, or notifyDataSetChanged), and that once layout occurs, all
-     * attached items should be discarded or animated.
+     * Processes the fact that, as far as we can tell, the data set has completely changed.
      *
-     * Attached items are labeled as invalid, and all cached items are discarded.
+     * <ul>
+     *   <li>Once layout occurs, all attached items should be discarded or animated.
+     *   <li>Attached items are labeled as invalid.
+     *   <li>Because items may still be prefetched between a "data set completely changed"
+     *       event and a layout event, all cached items are discarded.
+     * </ul>
      *
-     * It is still possible for items to be prefetched while mDataSetHasChangedAfterLayout == true,
-     * so this method must always discard all cached views so that the only valid items that remain
-     * in the cache, once layout occurs, are valid prefetched items.
+     * @param dispatchItemsChanged Whether to call
+     * {@link LayoutManager#onItemsChanged(RecyclerView)} during measure/layout.
      */
-    void setDataSetChangedAfterLayout() {
+    void processDataSetCompletelyChanged(boolean dispatchItemsChanged) {
+        mDispatchItemsChangedEvent |= dispatchItemsChanged;
         mDataSetHasChangedAfterLayout = true;
         markKnownViewsInvalid();
     }
@@ -5081,7 +5125,7 @@
             assertNotInLayoutOrScroll(null);
             mState.mStructureChanged = true;
 
-            setDataSetChangedAfterLayout();
+            processDataSetCompletelyChanged(true);
             if (!mAdapterHelper.hasPendingUpdates()) {
                 requestLayout();
             }
@@ -5130,6 +5174,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
@@ -6339,16 +6423,16 @@
         }
 
         void markKnownViewsInvalid() {
-            if (mAdapter != null && mAdapter.hasStableIds()) {
-                final int cachedCount = mCachedViews.size();
-                for (int i = 0; i < cachedCount; i++) {
-                    final ViewHolder holder = mCachedViews.get(i);
-                    if (holder != null) {
-                        holder.addFlags(ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID);
-                        holder.addChangePayload(null);
-                    }
+            final int cachedCount = mCachedViews.size();
+            for (int i = 0; i < cachedCount; i++) {
+                final ViewHolder holder = mCachedViews.get(i);
+                if (holder != null) {
+                    holder.addFlags(ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID);
+                    holder.addChangePayload(null);
                 }
-            } else {
+            }
+
+            if (mAdapter == null || !mAdapter.hasStableIds()) {
                 // we cannot re-use cached views in this case. Recycle them all
                 recycleAndClearCachedViews();
             }
@@ -9422,9 +9506,11 @@
         }
 
         /**
-         * Called if the RecyclerView this LayoutManager is bound to has a different adapter set.
-         * The LayoutManager may use this opportunity to clear caches and configure state such
-         * that it can relayout appropriately with the new data and potentially new view types.
+         * Called if the RecyclerView this LayoutManager is bound to has a different adapter set via
+         * {@link RecyclerView#setAdapter(Adapter)} or
+         * {@link RecyclerView#swapAdapter(Adapter, boolean)}. The LayoutManager may use this
+         * opportunity to clear caches and configure state such that it can relayout appropriately
+         * with the new data and potentially new view types.
          *
          * <p>The default implementation removes all currently attached views.</p>
          *
@@ -9466,8 +9552,9 @@
         }
 
         /**
-         * Called when {@link Adapter#notifyDataSetChanged()} is triggered instead of giving
-         * detailed information on what has actually changed.
+         * Called in response to a call to {@link Adapter#notifyDataSetChanged()} or
+         * {@link RecyclerView#swapAdapter(Adapter, boolean)} ()} and signals that the the entire
+         * data set has changed.
          *
          * @param recyclerView
          */
diff --git a/v7/recyclerview/src/main/java/android/support/v7/widget/util/SortedListAdapterCallback.java b/v7/recyclerview/src/main/java/android/support/v7/widget/util/SortedListAdapterCallback.java
index 4921541..a1203a6 100644
--- a/v7/recyclerview/src/main/java/android/support/v7/widget/util/SortedListAdapterCallback.java
+++ b/v7/recyclerview/src/main/java/android/support/v7/widget/util/SortedListAdapterCallback.java
@@ -56,4 +56,9 @@
     public void onChanged(int position, int count) {
         mAdapter.notifyItemRangeChanged(position, count);
     }
+
+    @Override
+    public void onChanged(int position, int count, Object payload) {
+        mAdapter.notifyItemRangeChanged(position, count, payload);
+    }
 }
diff --git a/v7/recyclerview/src/test/java/android/support/v7/util/SortedListBatchedCallbackTest.java b/v7/recyclerview/src/test/java/android/support/v7/util/SortedListBatchedCallbackTest.java
index 3ace217..bc50415 100644
--- a/v7/recyclerview/src/test/java/android/support/v7/util/SortedListBatchedCallbackTest.java
+++ b/v7/recyclerview/src/test/java/android/support/v7/util/SortedListBatchedCallbackTest.java
@@ -50,6 +50,16 @@
     }
 
     @Test
+    public void onChangeWithPayload() {
+        final Object payload = 7;
+        mBatchedCallback.onChanged(1, 2, payload);
+        verifyZeroInteractions(mMockCallback);
+        mBatchedCallback.dispatchLastEvent();
+        verify(mMockCallback).onChanged(1, 2, payload);
+        verifyNoMoreInteractions(mMockCallback);
+    }
+
+    @Test
     public void onRemoved() {
         mBatchedCallback.onRemoved(2, 3);
         verifyZeroInteractions(mMockCallback);
diff --git a/v7/recyclerview/src/test/java/android/support/v7/util/SortedListTest.java b/v7/recyclerview/src/test/java/android/support/v7/util/SortedListTest.java
index da3c957..47d2ac0 100644
--- a/v7/recyclerview/src/test/java/android/support/v7/util/SortedListTest.java
+++ b/v7/recyclerview/src/test/java/android/support/v7/util/SortedListTest.java
@@ -16,6 +16,7 @@
 
 package android.support.v7.util;
 
+import android.support.annotation.Nullable;
 import android.support.test.filters.SmallTest;
 
 import junit.framework.TestCase;
@@ -41,6 +42,8 @@
     List<Pair> mRemovals = new ArrayList<Pair>();
     List<Pair> mMoves = new ArrayList<Pair>();
     List<Pair> mUpdates = new ArrayList<Pair>();
+    private boolean mPayloadChanges = false;
+    List<PayloadChange> mPayloadUpdates = new ArrayList<>();
     private SortedList.Callback<Item> mCallback;
     InsertedCallback<Item> mInsertedCallback;
     ChangedCallback<Item> mChangedCallback;
@@ -97,6 +100,15 @@
             }
 
             @Override
+            public void onChanged(int position, int count, Object payload) {
+                if (mPayloadChanges) {
+                    mPayloadUpdates.add(new PayloadChange(position, count, payload));
+                } else {
+                    onChanged(position, count);
+                }
+            }
+
+            @Override
             public boolean areContentsTheSame(Item oldItem, Item newItem) {
                 return oldItem.cmpField == newItem.cmpField && oldItem.data == newItem.data;
             }
@@ -105,6 +117,15 @@
             public boolean areItemsTheSame(Item item1, Item item2) {
                 return item1.id == item2.id;
             }
+
+            @Nullable
+            @Override
+            public Object getChangePayload(Item item1, Item item2) {
+                if (mPayloadChanges) {
+                    return item2.data;
+                }
+                return null;
+            }
         };
         mInsertedCallback = null;
         mChangedCallback = null;
@@ -705,6 +726,76 @@
         assertTrue(mAdditions.contains(new Pair(0, 6)));
     }
 
+    @Test
+    public void testAddExistingItemCallsChangeWithPayload() {
+        mList.addAll(
+                new Item(1, 10),
+                new Item(2, 20),
+                new Item(3, 30)
+        );
+        mPayloadChanges = true;
+
+        // add an item with the same id but a new data field i.e. send an update
+        final Item twoUpdate = new Item(2, 20);
+        twoUpdate.data = 1337;
+        mList.add(twoUpdate);
+        assertEquals(1, mPayloadUpdates.size());
+        final PayloadChange update = mPayloadUpdates.get(0);
+        assertEquals(1, update.position);
+        assertEquals(1, update.count);
+        assertEquals(1337, update.payload);
+        assertEquals(3, size());
+    }
+
+    @Test
+    public void testUpdateItemCallsChangeWithPayload() {
+        mList.addAll(
+                new Item(1, 10),
+                new Item(2, 20),
+                new Item(3, 30)
+        );
+        mPayloadChanges = true;
+
+        // add an item with the same id but a new data field i.e. send an update
+        final Item twoUpdate = new Item(2, 20);
+        twoUpdate.data = 1337;
+        mList.updateItemAt(1, twoUpdate);
+        assertEquals(1, mPayloadUpdates.size());
+        final PayloadChange update = mPayloadUpdates.get(0);
+        assertEquals(1, update.position);
+        assertEquals(1, update.count);
+        assertEquals(1337, update.payload);
+        assertEquals(3, size());
+        assertEquals(1337, mList.get(1).data);
+    }
+
+    @Test
+    public void testAddMultipleExistingItemCallsChangeWithPayload() {
+        mList.addAll(
+                new Item(1, 10),
+                new Item(2, 20),
+                new Item(3, 30)
+        );
+        mPayloadChanges = true;
+
+        // add two items with the same ids but a new data fields i.e. send two updates
+        final Item twoUpdate = new Item(2, 20);
+        twoUpdate.data = 222;
+        final Item threeUpdate = new Item(3, 30);
+        threeUpdate.data = 333;
+        mList.addAll(twoUpdate, threeUpdate);
+        assertEquals(2, mPayloadUpdates.size());
+        final PayloadChange update1 = mPayloadUpdates.get(0);
+        assertEquals(1, update1.position);
+        assertEquals(1, update1.count);
+        assertEquals(222, update1.payload);
+        final PayloadChange update2 = mPayloadUpdates.get(1);
+        assertEquals(2, update2.position);
+        assertEquals(1, update2.count);
+        assertEquals(333, update2.payload);
+        assertEquals(3, size());
+    }
+
     private int size() {
         return mList.size();
     }
@@ -821,4 +912,37 @@
             return result;
         }
     }
+
+    private static final class PayloadChange {
+        public final int position;
+        public final int count;
+        public final Object payload;
+
+        PayloadChange(int position, int count, Object payload) {
+            this.position = position;
+            this.count = count;
+            this.payload = payload;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+
+            PayloadChange payloadChange = (PayloadChange) o;
+
+            if (position != payloadChange.position) return false;
+            if (count != payloadChange.count) return false;
+            return payload != null ? payload.equals(payloadChange.payload)
+                    : payloadChange.payload == null;
+        }
+
+        @Override
+        public int hashCode() {
+            int result = position;
+            result = 31 * result + count;
+            result = 31 * result + (payload != null ? payload.hashCode() : 0);
+            return result;
+        }
+    }
 }
\ No newline at end of file
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/BaseRecyclerViewInstrumentationTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java
index d74c36c..157fb12 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java
@@ -1083,13 +1083,18 @@
             });
         }
 
-        public void clearOnUIThread() {
+        void changeAllItemsAndNotifyDataSetChanged(int count) {
             assertEquals("clearOnUIThread called from a wrong thread",
                     Looper.getMainLooper(), Looper.myLooper());
-            mItems = new ArrayList<Item>();
+            mItems = new ArrayList<>();
+            addItems(0, count, DEFAULT_ITEM_PREFIX);
             notifyDataSetChanged();
         }
 
+        public void clearOnUIThread() {
+            changeAllItemsAndNotifyDataSetChanged(0);
+        }
+
         protected void moveInUIThread(int from, int to) {
             Item item = mItems.remove(from);
             offsetOriginalIndices(from, -1);
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/RecyclerViewBasicTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewBasicTest.java
index 5946940..3357c2f 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewBasicTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewBasicTest.java
@@ -477,7 +477,6 @@
             assertTrue("must contain Adapter class", m.contains(MockAdapter.class.getName()));
             assertTrue("must contain LM class", m.contains(LinearLayoutManager.class.getName()));
             assertTrue("must contain ctx class", m.contains(getContext().getClass().getName()));
-
         }
     }
 
@@ -488,20 +487,71 @@
         mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
         measure();
         layout();
-        assertSame(focusAdapter.mBottomLeft,
-                focusAdapter.mTopRight.focusSearch(View.FOCUS_FORWARD));
-        assertSame(focusAdapter.mBottomRight,
-                focusAdapter.mBottomLeft.focusSearch(View.FOCUS_FORWARD));
+
+        boolean isIcsOrLower = Build.VERSION.SDK_INT <= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1;
+
+        // On API 15 and lower, focus forward get's translated to focus down.
+        View expected = isIcsOrLower ? focusAdapter.mBottomRight : focusAdapter.mBottomLeft;
+        assertEquals(expected, focusAdapter.mTopRight.focusSearch(View.FOCUS_FORWARD));
+
+        // On API 15 and lower, focus forward get's translated to focus down, which in this case
+        // runs out of the RecyclerView, thus returning null.
+        expected = isIcsOrLower ? null : focusAdapter.mBottomRight;
+        assertSame(expected, focusAdapter.mBottomLeft.focusSearch(View.FOCUS_FORWARD));
+
         // we don't want looping within RecyclerView
         assertNull(focusAdapter.mBottomRight.focusSearch(View.FOCUS_FORWARD));
         assertNull(focusAdapter.mTopLeft.focusSearch(View.FOCUS_BACKWARD));
     }
 
+    @Test
+    public void setAdapter_callsCorrectLmMethods() throws Throwable {
+        MockLayoutManager mockLayoutManager = new MockLayoutManager();
+        MockAdapter mockAdapter = new MockAdapter(1);
+        mRecyclerView.setLayoutManager(mockLayoutManager);
+
+        mRecyclerView.setAdapter(mockAdapter);
+        layout();
+
+        assertEquals(1, mockLayoutManager.mAdapterChangedCount);
+        assertEquals(0, mockLayoutManager.mItemsChangedCount);
+    }
+
+    @Test
+    public void swapAdapter_callsCorrectLmMethods() throws Throwable {
+        MockLayoutManager mockLayoutManager = new MockLayoutManager();
+        MockAdapter mockAdapter = new MockAdapter(1);
+        mRecyclerView.setLayoutManager(mockLayoutManager);
+
+        mRecyclerView.swapAdapter(mockAdapter, true);
+        layout();
+
+        assertEquals(1, mockLayoutManager.mAdapterChangedCount);
+        assertEquals(1, mockLayoutManager.mItemsChangedCount);
+    }
+
+    @Test
+    public void notifyDataSetChanged_callsCorrectLmMethods() throws Throwable {
+        MockLayoutManager mockLayoutManager = new MockLayoutManager();
+        MockAdapter mockAdapter = new MockAdapter(1);
+        mRecyclerView.setLayoutManager(mockLayoutManager);
+        mRecyclerView.setAdapter(mockAdapter);
+        mockLayoutManager.mAdapterChangedCount = 0;
+        mockLayoutManager.mItemsChangedCount = 0;
+
+        mockAdapter.notifyDataSetChanged();
+        layout();
+
+        assertEquals(0, mockLayoutManager.mAdapterChangedCount);
+        assertEquals(1, mockLayoutManager.mItemsChangedCount);
+    }
+
     static class MockLayoutManager extends RecyclerView.LayoutManager {
 
         int mLayoutCount = 0;
 
         int mAdapterChangedCount = 0;
+        int mItemsChangedCount = 0;
 
         RecyclerView.Adapter mPrevAdapter;
 
@@ -519,6 +569,11 @@
         }
 
         @Override
+        public void onItemsChanged(RecyclerView recyclerView) {
+            mItemsChangedCount++;
+        }
+
+        @Override
         public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
             mLayoutCount += 1;
         }
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
index 42fde85..e8d900a 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
@@ -24,6 +24,7 @@
 
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.CoreMatchers.notNullValue;
 import static org.hamcrest.CoreMatchers.sameInstance;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -252,6 +253,186 @@
     }
 
     @Test
+    public void setAdapter_afterSwapAdapter_callsCorrectLmMethods() throws Throwable {
+        final RecyclerView rv = new RecyclerView(getActivity());
+        final LayoutAllLayoutManager lm = new LayoutAllLayoutManager(true);
+        final TestAdapter testAdapter = new TestAdapter(1);
+
+        lm.expectLayouts(1);
+        rv.setLayoutManager(lm);
+        setRecyclerView(rv);
+        setAdapter(testAdapter);
+        lm.waitForLayout(2);
+
+        lm.onAdapterChagnedCallCount = 0;
+        lm.onItemsChangedCallCount = 0;
+
+        lm.expectLayouts(1);
+        mActivityRule.runOnUiThread(
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        rv.swapAdapter(testAdapter, true);
+                        rv.setAdapter(testAdapter);
+                    }
+                });
+        lm.waitForLayout(2);
+
+        assertEquals(2, lm.onAdapterChagnedCallCount);
+        assertEquals(1, lm.onItemsChangedCallCount);
+    }
+
+    @Test
+    public void setAdapter_afterNotifyDataSetChanged_callsCorrectLmMethods() throws Throwable {
+        final RecyclerView rv = new RecyclerView(getActivity());
+        final LayoutAllLayoutManager lm = new LayoutAllLayoutManager(true);
+        final TestAdapter testAdapter = new TestAdapter(1);
+
+        lm.expectLayouts(1);
+        rv.setLayoutManager(lm);
+        setRecyclerView(rv);
+        setAdapter(testAdapter);
+        lm.waitForLayout(2);
+
+        lm.onAdapterChagnedCallCount = 0;
+        lm.onItemsChangedCallCount = 0;
+
+        lm.expectLayouts(1);
+        mActivityRule.runOnUiThread(
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        testAdapter.notifyDataSetChanged();
+                        rv.setAdapter(testAdapter);
+                    }
+                });
+        lm.waitForLayout(2);
+
+        assertEquals(1, lm.onAdapterChagnedCallCount);
+        assertEquals(1, lm.onItemsChangedCallCount);
+    }
+
+    @Test
+    public void notifyDataSetChanged_afterSetAdapter_callsCorrectLmMethods() throws Throwable {
+        final RecyclerView rv = new RecyclerView(getActivity());
+        final LayoutAllLayoutManager lm = new LayoutAllLayoutManager(true);
+        final TestAdapter testAdapter = new TestAdapter(1);
+
+        lm.expectLayouts(1);
+        rv.setLayoutManager(lm);
+        setRecyclerView(rv);
+        setAdapter(testAdapter);
+        lm.waitForLayout(2);
+
+        lm.onAdapterChagnedCallCount = 0;
+        lm.onItemsChangedCallCount = 0;
+
+        lm.expectLayouts(1);
+        mActivityRule.runOnUiThread(
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        rv.setAdapter(testAdapter);
+                        testAdapter.notifyDataSetChanged();
+                    }
+                });
+        lm.waitForLayout(2);
+
+        assertEquals(1, lm.onAdapterChagnedCallCount);
+        assertEquals(1, lm.onItemsChangedCallCount);
+    }
+
+    @Test
+    public void notifyDataSetChanged_afterSwapAdapter_callsCorrectLmMethods() throws Throwable {
+        final RecyclerView rv = new RecyclerView(getActivity());
+        final LayoutAllLayoutManager lm = new LayoutAllLayoutManager(true);
+        final TestAdapter testAdapter = new TestAdapter(1);
+
+        lm.expectLayouts(1);
+        rv.setLayoutManager(lm);
+        setRecyclerView(rv);
+        setAdapter(testAdapter);
+        lm.waitForLayout(2);
+
+        lm.onAdapterChagnedCallCount = 0;
+        lm.onItemsChangedCallCount = 0;
+
+        lm.expectLayouts(1);
+        mActivityRule.runOnUiThread(
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        rv.swapAdapter(testAdapter, true);
+                        testAdapter.notifyDataSetChanged();
+                    }
+                });
+        lm.waitForLayout(2);
+
+        assertEquals(1, lm.onAdapterChagnedCallCount);
+        assertEquals(1, lm.onItemsChangedCallCount);
+    }
+
+    @Test
+    public void swapAdapter_afterSetAdapter_callsCorrectLmMethods() throws Throwable {
+        final RecyclerView rv = new RecyclerView(getActivity());
+        final LayoutAllLayoutManager lm = new LayoutAllLayoutManager(true);
+        final TestAdapter testAdapter = new TestAdapter(1);
+
+        lm.expectLayouts(1);
+        rv.setLayoutManager(lm);
+        setRecyclerView(rv);
+        setAdapter(testAdapter);
+        lm.waitForLayout(2);
+
+        lm.onAdapterChagnedCallCount = 0;
+        lm.onItemsChangedCallCount = 0;
+
+        lm.expectLayouts(1);
+        mActivityRule.runOnUiThread(
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        rv.setAdapter(testAdapter);
+                        rv.swapAdapter(testAdapter, true);
+                    }
+                });
+        lm.waitForLayout(2);
+
+        assertEquals(2, lm.onAdapterChagnedCallCount);
+        assertEquals(1, lm.onItemsChangedCallCount);
+    }
+
+    @Test
+    public void swapAdapter_afterNotifyDataSetChanged_callsCorrectLmMethods() throws Throwable {
+        final RecyclerView rv = new RecyclerView(getActivity());
+        final LayoutAllLayoutManager lm = new LayoutAllLayoutManager(true);
+        final TestAdapter testAdapter = new TestAdapter(1);
+
+        lm.expectLayouts(1);
+        rv.setLayoutManager(lm);
+        setRecyclerView(rv);
+        setAdapter(testAdapter);
+        lm.waitForLayout(2);
+
+        lm.onAdapterChagnedCallCount = 0;
+        lm.onItemsChangedCallCount = 0;
+
+        lm.expectLayouts(1);
+        mActivityRule.runOnUiThread(
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        testAdapter.notifyDataSetChanged();
+                        rv.swapAdapter(testAdapter, true);
+                    }
+                });
+        lm.waitForLayout(2);
+
+        assertEquals(1, lm.onAdapterChagnedCallCount);
+        assertEquals(1, lm.onItemsChangedCallCount);
+    }
+
+    @Test
     public void setAdapterNotifyItemRangeInsertedCrashTest() throws Throwable {
         final RecyclerView rv = new RecyclerView(getActivity());
         final TestLayoutManager lm = new LayoutAllLayoutManager(true);
@@ -298,6 +479,201 @@
     }
 
     @Test
+    public void onDataSetChanged_doesntHaveStableIds_cachedViewHasNoPosition() throws Throwable {
+        onDataSetChanged_handleCachedViews(false);
+    }
+
+    @Test
+    public void onDataSetChanged_hasStableIds_noCachedViewsAreRecycled() throws Throwable {
+        onDataSetChanged_handleCachedViews(true);
+    }
+
+    /**
+     * If Adapter#setHasStableIds(boolean) is false, cached ViewHolders should be recycled in
+     * response to RecyclerView.Adapter#notifyDataSetChanged() and should report a position of
+     * RecyclerView#NO_POSITION inside of
+     * RecyclerView.Adapter#onViewRecycled(RecyclerView.ViewHolder).
+     *
+     * If Adapter#setHasStableIds(boolean) is true, cached Views/ViewHolders should not be recycled.
+     */
+    public void onDataSetChanged_handleCachedViews(boolean hasStableIds) throws Throwable {
+        final AtomicInteger cachedRecycleCount = new AtomicInteger(0);
+
+        final RecyclerView recyclerView = new RecyclerView(getActivity());
+        recyclerView.setItemViewCacheSize(1);
+
+        final TestAdapter adapter = new TestAdapter(2) {
+            @Override
+            public void onViewRecycled(TestViewHolder holder) {
+                // If the recycled holder is currently in the cache, then it's position in the
+                // adapter should be RecyclerView.NO_POSITION.
+                if (mRecyclerView.mRecycler.mCachedViews.contains(holder)) {
+                    assertThat("ViewHolder's getAdapterPosition should be "
+                                    + "RecyclerView.NO_POSITION",
+                            holder.getAdapterPosition(),
+                            is(RecyclerView.NO_POSITION));
+                    cachedRecycleCount.incrementAndGet();
+                }
+                super.onViewRecycled(holder);
+            }
+        };
+        adapter.setHasStableIds(hasStableIds);
+        recyclerView.setAdapter(adapter);
+
+        final AtomicInteger numItemsToLayout = new AtomicInteger(2);
+
+        TestLayoutManager layoutManager = new TestLayoutManager() {
+            @Override
+            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+                try {
+                    detachAndScrapAttachedViews(recycler);
+                    layoutRange(recycler, 0, numItemsToLayout.get());
+                } catch (Throwable t) {
+                    postExceptionToInstrumentation(t);
+                } finally {
+                    this.layoutLatch.countDown();
+                }
+            }
+
+            @Override
+            public boolean supportsPredictiveItemAnimations() {
+                return false;
+            }
+        };
+        recyclerView.setLayoutManager(layoutManager);
+
+        // Layout 2 items and sanity check that no items are in the recycler's cache.
+        numItemsToLayout.set(2);
+        layoutManager.expectLayouts(1);
+        setRecyclerView(recyclerView, true, false);
+        layoutManager.waitForLayout(2);
+        checkForMainThreadException();
+        assertThat("Sanity check, no views should be cached at this time",
+                mRecyclerView.mRecycler.mCachedViews.size(),
+                is(0));
+
+        // Now only layout 1 item and assert that 1 item is cached.
+        numItemsToLayout.set(1);
+        layoutManager.expectLayouts(1);
+        requestLayoutOnUIThread(mRecyclerView);
+        layoutManager.waitForLayout(1);
+        checkForMainThreadException();
+        assertThat("One view should be cached.",
+                mRecyclerView.mRecycler.mCachedViews.size(),
+                is(1));
+
+        // Notify data set has changed then final assert.
+        layoutManager.expectLayouts(1);
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                adapter.notifyDataSetChanged();
+            }
+        });
+        layoutManager.waitForLayout(1);
+        checkForMainThreadException();
+        // If hasStableIds, then no cached views should be recycled, otherwise just 1 should have
+        // been recycled.
+        assertThat(cachedRecycleCount.get(), is(hasStableIds ? 0 : 1));
+    }
+
+    @Test
+    public void notifyDataSetChanged_hasStableIds_cachedViewsAreReusedForSamePositions()
+            throws Throwable {
+        final Map<Integer, TestViewHolder> positionToViewHolderMap = new HashMap<>();
+        final AtomicInteger layoutItemCount = new AtomicInteger();
+        final AtomicBoolean inFirstBindViewHolderPass = new AtomicBoolean();
+
+        final RecyclerView recyclerView = new RecyclerView(getActivity());
+        recyclerView.setItemViewCacheSize(5);
+
+        final TestAdapter adapter = new TestAdapter(10) {
+            @Override
+            public void onBindViewHolder(TestViewHolder holder, int position) {
+                // Only track the top 5 positions that are going to be cached and then reused.
+                if (position >= 5) {
+                    // If we are in the first phase, put the items in the map, if we are in the
+                    // second phase, remove each one at the position and verify that it matches the
+                    // provided ViewHolder.
+                    if (inFirstBindViewHolderPass.get()) {
+                        positionToViewHolderMap.put(position, holder);
+                    } else {
+                        TestViewHolder testViewHolder = positionToViewHolderMap.get(position);
+                        assertThat(holder, is(testViewHolder));
+                        positionToViewHolderMap.remove(position);
+                    }
+                }
+                super.onBindViewHolder(holder, position);
+            }
+        };
+        adapter.setHasStableIds(true);
+        recyclerView.setAdapter(adapter);
+
+        TestLayoutManager testLayoutManager = new TestLayoutManager() {
+            @Override
+            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+                try {
+                    detachAndScrapAttachedViews(recycler);
+                    layoutRange(recycler, 0, layoutItemCount.get());
+                } catch (Throwable t) {
+                    postExceptionToInstrumentation(t);
+                } finally {
+                    layoutLatch.countDown();
+                }
+            }
+
+            @Override
+            public boolean supportsPredictiveItemAnimations() {
+                return false;
+            }
+        };
+        recyclerView.setLayoutManager(testLayoutManager);
+
+        // First layout 10 items, then verify that the map has all 5 ViewHolders in it that will
+        // be cached, and sanity check that the cache is empty.
+        inFirstBindViewHolderPass.set(true);
+        layoutItemCount.set(10);
+        testLayoutManager.expectLayouts(1);
+        setRecyclerView(recyclerView, true, false);
+        testLayoutManager.waitForLayout(2);
+        checkForMainThreadException();
+        for (int i = 5; i < 10; i++) {
+            assertThat(positionToViewHolderMap.get(i), notNullValue());
+        }
+        assertThat(mRecyclerView.mRecycler.mCachedViews.size(), is(0));
+
+        // Now only layout the first 5 items and verify that the cache has 5 items in it.
+        layoutItemCount.set(5);
+        testLayoutManager.expectLayouts(1);
+        requestLayoutOnUIThread(mRecyclerView);
+        testLayoutManager.waitForLayout(1);
+        checkForMainThreadException();
+        assertThat(mRecyclerView.mRecycler.mCachedViews.size(), is(5));
+
+        // Trigger notifyDataSetChanged and wait for layout.
+        testLayoutManager.expectLayouts(1);
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                adapter.notifyDataSetChanged();
+            }
+        });
+        testLayoutManager.waitForLayout(1);
+        checkForMainThreadException();
+
+        // Layout 10 items again, via the onBindViewholder method, check that each one of the views
+        // returned from the recycler for positions >= 5 was in our cache of views, and verify that
+        // all 5 cached views were returned.
+        inFirstBindViewHolderPass.set(false);
+        layoutItemCount.set(10);
+        testLayoutManager.expectLayouts(1);
+        requestLayoutOnUIThread(mRecyclerView);
+        testLayoutManager.waitForLayout(1);
+        checkForMainThreadException();
+        assertThat(positionToViewHolderMap.size(), is(0));
+    }
+
+    @Test
     public void predictiveMeasuredCrashTest() throws Throwable {
         final RecyclerView rv = new RecyclerView(getActivity());
         final LayoutAllLayoutManager lm = new LayoutAllLayoutManager(true) {
@@ -4592,6 +4968,8 @@
 
     public class LayoutAllLayoutManager extends TestLayoutManager {
         private final boolean mAllowNullLayoutLatch;
+        public int onItemsChangedCallCount = 0;
+        public int onAdapterChagnedCallCount = 0;
 
         public LayoutAllLayoutManager() {
             // by default, we don't allow unexpected layouts.
@@ -4601,6 +4979,18 @@
             mAllowNullLayoutLatch = allowNullLayoutLatch;
         }
 
+        @Override
+        public void onItemsChanged(RecyclerView recyclerView) {
+            super.onItemsChanged(recyclerView);
+            onItemsChangedCallCount++;
+        }
+
+        @Override
+        public void onAdapterChanged(RecyclerView.Adapter oldAdapter,
+                RecyclerView.Adapter newAdapter) {
+            super.onAdapterChanged(oldAdapter, newAdapter);
+            onAdapterChagnedCallCount++;
+        }
 
         @Override
         public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
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);
     }
 
diff --git a/wear/api/current.txt b/wear/api/current.txt
index e397eb3..e9b7d86 100644
--- a/wear/api/current.txt
+++ b/wear/api/current.txt
@@ -21,7 +21,6 @@
 
   public final class AmbientMode.AmbientController {
     method public boolean isAmbient();
-    method public void setAutoResumeEnabled(boolean);
   }
 
 }
diff --git a/wear/res/drawable-v21/ws_ic_expand_more_white_22.xml b/wear/res/drawable-v23/ws_ic_expand_more_white_22.xml
similarity index 100%
rename from wear/res/drawable-v21/ws_ic_expand_more_white_22.xml
rename to wear/res/drawable-v23/ws_ic_expand_more_white_22.xml
diff --git a/wear/res/drawable-v21/ws_switch_thumb_material_anim.xml b/wear/res/drawable-v23/ws_switch_thumb_material_anim.xml
similarity index 100%
rename from wear/res/drawable-v21/ws_switch_thumb_material_anim.xml
rename to wear/res/drawable-v23/ws_switch_thumb_material_anim.xml
diff --git a/wear/res/values-v20/styles.xml b/wear/res/values-v20/styles.xml
deleted file mode 100644
index 92613f2..0000000
--- a/wear/res/values-v20/styles.xml
+++ /dev/null
@@ -1,34 +0,0 @@
-<?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 name="WsPageIndicatorViewStyle">
-        <item name="wsPageIndicatorDotSpacing">7.8dp</item>
-        <item name="wsPageIndicatorDotRadius">2.1dp</item>
-        <item name="wsPageIndicatorDotRadiusSelected">3.1dp</item>
-        <item name="wsPageIndicatorDotColor">?android:attr/colorForeground</item>
-        <item name="wsPageIndicatorDotColorSelected">?android:attr/colorForeground</item>
-        <item name="wsPageIndicatorDotFadeOutDelay">1000</item>
-        <item name="wsPageIndicatorDotFadeOutDuration">250</item>
-        <item name="wsPageIndicatorDotFadeInDuration">100</item>
-        <item name="wsPageIndicatorDotFadeWhenIdle">true</item>
-        <item name="wsPageIndicatorDotShadowColor">#66000000</item>
-        <item name="wsPageIndicatorDotShadowRadius">1dp</item>
-        <item name="wsPageIndicatorDotShadowDx">0.5dp</item>
-        <item name="wsPageIndicatorDotShadowDy">0.5dp</item>
-    </style>
-
-</resources>
diff --git a/wear/res/values-v23/styles.xml b/wear/res/values-v23/styles.xml
index 6bb1a51..63ed2d8 100644
--- a/wear/res/values-v23/styles.xml
+++ b/wear/res/values-v23/styles.xml
@@ -14,6 +14,22 @@
      limitations under the License.
 -->
 <resources>
+    <style name="WsPageIndicatorViewStyle">
+        <item name="wsPageIndicatorDotSpacing">7.8dp</item>
+        <item name="wsPageIndicatorDotRadius">2.1dp</item>
+        <item name="wsPageIndicatorDotRadiusSelected">3.1dp</item>
+        <item name="wsPageIndicatorDotColor">?android:attr/colorForeground</item>
+        <item name="wsPageIndicatorDotColorSelected">?android:attr/colorForeground</item>
+        <item name="wsPageIndicatorDotFadeOutDelay">1000</item>
+        <item name="wsPageIndicatorDotFadeOutDuration">250</item>
+        <item name="wsPageIndicatorDotFadeInDuration">100</item>
+        <item name="wsPageIndicatorDotFadeWhenIdle">true</item>
+        <item name="wsPageIndicatorDotShadowColor">#66000000</item>
+        <item name="wsPageIndicatorDotShadowRadius">1dp</item>
+        <item name="wsPageIndicatorDotShadowDx">0.5dp</item>
+        <item name="wsPageIndicatorDotShadowDy">0.5dp</item>
+    </style>
+
     <style name="WsWearableActionDrawerItemText">
         <item name="android:layout_gravity">center_vertical</item>
         <item name="android:ellipsize">end</item>
diff --git a/wear/src/main/java/android/support/wear/ambient/AmbientMode.java b/wear/src/main/java/android/support/wear/ambient/AmbientMode.java
index 1911a40..5db9383 100644
--- a/wear/src/main/java/android/support/wear/ambient/AmbientMode.java
+++ b/wear/src/main/java/android/support/wear/ambient/AmbientMode.java
@@ -38,7 +38,7 @@
  * It should be called with an {@link Activity} as an argument and that {@link Activity} will then
  * be able to receive ambient lifecycle events through an {@link AmbientCallback}. The
  * {@link Activity} will also receive a {@link AmbientController} object from the attachment which
- * can be used to query the current status of the ambient mode, or toggle simple settings.
+ * can be used to query the current status of the ambient mode.
  * An example of how to attach {@link AmbientMode} to your {@link Activity} and use
  * the {@link AmbientController} can be found below:
  * <p>
@@ -117,7 +117,7 @@
          * Called when the system is updating the display for ambient mode. Activities may use this
          * opportunity to update or invalidate views.
          */
-        public void onUpdateAmbient() {};
+        public void onUpdateAmbient() {}
 
         /**
          * Called when an activity should exit ambient mode. This event is sent while an activity is
@@ -126,7 +126,7 @@
          * <p><em>Derived classes must call through to the super class's implementation of this
          * method. If they do not, an exception will be thrown.</em>
          */
-        public void onExitAmbient() {};
+        public void onExitAmbient() {}
     }
 
     private final AmbientDelegate.AmbientCallback mCallback =
@@ -220,7 +220,7 @@
      * @param activity the activity to attach ambient support to. This activity has to also
      *                implement {@link AmbientCallbackProvider}
      * @return the associated {@link AmbientController} which can be used to query the state of
-     * ambient mode and toggle simple settings related to it.
+     * ambient mode.
      */
     public static <T extends Activity & AmbientCallbackProvider> AmbientController
             attachAmbientSupport(T activity) {
@@ -251,9 +251,8 @@
 
     /**
      * A class for interacting with the ambient mode on a wearable device. This class can be used to
-     * query the current state of ambient mode and to enable or disable certain settings.
-     * An instance of this class is returned to the user when they attach their {@link Activity}
-     * to {@link AmbientMode}.
+     * query the current state of ambient mode. An instance of this class is returned to the user
+     * when they attach their {@link Activity} to {@link AmbientMode}.
      */
     public final class AmbientController {
         private static final String TAG = "AmbientController";
diff --git a/wear/src/main/java/android/support/wear/ambient/SharedLibraryVersion.java b/wear/src/main/java/android/support/wear/ambient/SharedLibraryVersion.java
index cd90a3b..9421d9e 100644
--- a/wear/src/main/java/android/support/wear/ambient/SharedLibraryVersion.java
+++ b/wear/src/main/java/android/support/wear/ambient/SharedLibraryVersion.java
@@ -16,7 +16,6 @@
 package android.support.wear.ambient;
 
 import android.os.Build;
-import android.support.annotation.RestrictTo;
 import android.support.annotation.VisibleForTesting;
 
 import com.google.android.wearable.WearableSharedLib;
@@ -24,10 +23,7 @@
 /**
  * Internal class which can be used to determine the version of the wearable shared library that is
  * available on the current device.
- *
- * @hide
  */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 final class SharedLibraryVersion {
 
     private SharedLibraryVersion() {
diff --git a/wear/src/main/java/android/support/wear/ambient/WearableControllerProvider.java b/wear/src/main/java/android/support/wear/ambient/WearableControllerProvider.java
index 1682dc0..4b6ae8e 100644
--- a/wear/src/main/java/android/support/wear/ambient/WearableControllerProvider.java
+++ b/wear/src/main/java/android/support/wear/ambient/WearableControllerProvider.java
@@ -28,7 +28,7 @@
  *
  * @hide
  */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RestrictTo(RestrictTo.Scope.LIBRARY)
 public class WearableControllerProvider {
 
     private static final String TAG = "WearableControllerProvider";
diff --git a/wear/src/main/java/android/support/wear/internal/widget/ResourcesUtil.java b/wear/src/main/java/android/support/wear/internal/widget/ResourcesUtil.java
index f23a688..8ba3adf 100644
--- a/wear/src/main/java/android/support/wear/internal/widget/ResourcesUtil.java
+++ b/wear/src/main/java/android/support/wear/internal/widget/ResourcesUtil.java
@@ -26,7 +26,7 @@
  *
  * @hide
  */
-@RestrictTo(Scope.LIBRARY_GROUP)
+@RestrictTo(Scope.LIBRARY)
 public final class ResourcesUtil {
 
     /**
diff --git a/wear/src/main/java/android/support/wear/internal/widget/drawer/MultiPagePresenter.java b/wear/src/main/java/android/support/wear/internal/widget/drawer/MultiPagePresenter.java
index ad56048..4a7ce66 100644
--- a/wear/src/main/java/android/support/wear/internal/widget/drawer/MultiPagePresenter.java
+++ b/wear/src/main/java/android/support/wear/internal/widget/drawer/MultiPagePresenter.java
@@ -28,7 +28,7 @@
  *
  * @hide
  */
-@RestrictTo(Scope.LIBRARY_GROUP)
+@RestrictTo(Scope.LIBRARY)
 public class MultiPagePresenter extends WearableNavigationDrawerPresenter {
 
     private final Ui mUi;
diff --git a/wear/src/main/java/android/support/wear/internal/widget/drawer/MultiPageUi.java b/wear/src/main/java/android/support/wear/internal/widget/drawer/MultiPageUi.java
index 9056845..0ba2f5d 100644
--- a/wear/src/main/java/android/support/wear/internal/widget/drawer/MultiPageUi.java
+++ b/wear/src/main/java/android/support/wear/internal/widget/drawer/MultiPageUi.java
@@ -16,6 +16,7 @@
 
 package android.support.wear.internal.widget.drawer;
 
+import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.annotation.RestrictTo;
 import android.support.annotation.RestrictTo.Scope;
@@ -37,7 +38,7 @@
  *
  * @hide
  */
-@RestrictTo(Scope.LIBRARY_GROUP)
+@RestrictTo(Scope.LIBRARY)
 public class MultiPageUi implements MultiPagePresenter.Ui {
 
     private static final String TAG = "MultiPageUi";
@@ -62,13 +63,8 @@
         final View content = inflater.inflate(R.layout.ws_navigation_drawer_view, drawer,
                 false /* attachToRoot */);
 
-        mNavigationPager =
-                (ViewPager) content
-                        .findViewById(R.id.ws_navigation_drawer_view_pager);
-        mPageIndicatorView =
-                (PageIndicatorView)
-                        content.findViewById(
-                                R.id.ws_navigation_drawer_page_indicator);
+        mNavigationPager = content.findViewById(R.id.ws_navigation_drawer_view_pager);
+        mPageIndicatorView = content.findViewById(R.id.ws_navigation_drawer_page_indicator);
 
         drawer.setDrawerContent(content);
     }
@@ -132,8 +128,9 @@
             mAdapter = adapter;
         }
 
+        @NonNull
         @Override
-        public Object instantiateItem(ViewGroup container, int position) {
+        public Object instantiateItem(@NonNull ViewGroup container, int position) {
             // Do not attach to root in the inflate method. The view needs to returned at the end
             // of this method. Attaching to root will cause view to point to container instead.
             final View view =
@@ -141,17 +138,17 @@
                             .inflate(R.layout.ws_navigation_drawer_item_view, container, false);
             container.addView(view);
             final ImageView iconView =
-                    (ImageView) view
-                            .findViewById(R.id.ws_navigation_drawer_item_icon);
+                    view.findViewById(R.id.ws_navigation_drawer_item_icon);
             final TextView textView =
-                    (TextView) view.findViewById(R.id.ws_navigation_drawer_item_text);
+                    view.findViewById(R.id.ws_navigation_drawer_item_text);
             iconView.setImageDrawable(mAdapter.getItemDrawable(position));
             textView.setText(mAdapter.getItemText(position));
             return view;
         }
 
         @Override
-        public void destroyItem(ViewGroup container, int position, Object object) {
+        public void destroyItem(@NonNull ViewGroup container, int position,
+                @NonNull Object object) {
             container.removeView((View) object);
         }
 
@@ -161,12 +158,12 @@
         }
 
         @Override
-        public int getItemPosition(Object object) {
+        public int getItemPosition(@NonNull Object object) {
             return POSITION_NONE;
         }
 
         @Override
-        public boolean isViewFromObject(View view, Object object) {
+        public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
             return view == object;
         }
     }
diff --git a/wear/src/main/java/android/support/wear/internal/widget/drawer/SinglePagePresenter.java b/wear/src/main/java/android/support/wear/internal/widget/drawer/SinglePagePresenter.java
index d90b589..42cc7d0 100644
--- a/wear/src/main/java/android/support/wear/internal/widget/drawer/SinglePagePresenter.java
+++ b/wear/src/main/java/android/support/wear/internal/widget/drawer/SinglePagePresenter.java
@@ -29,7 +29,7 @@
  *
  * @hide
  */
-@RestrictTo(Scope.LIBRARY_GROUP)
+@RestrictTo(Scope.LIBRARY)
 public class SinglePagePresenter extends WearableNavigationDrawerPresenter {
 
     private static final long DRAWER_CLOSE_DELAY_MS = 500;
diff --git a/wear/src/main/java/android/support/wear/internal/widget/drawer/SinglePageUi.java b/wear/src/main/java/android/support/wear/internal/widget/drawer/SinglePageUi.java
index f3a4290..ffc966f 100644
--- a/wear/src/main/java/android/support/wear/internal/widget/drawer/SinglePageUi.java
+++ b/wear/src/main/java/android/support/wear/internal/widget/drawer/SinglePageUi.java
@@ -38,7 +38,7 @@
  *
  * @hide
  */
-@RestrictTo(Scope.LIBRARY_GROUP)
+@RestrictTo(Scope.LIBRARY)
 public class SinglePageUi implements SinglePagePresenter.Ui {
 
     @IdRes
@@ -111,11 +111,10 @@
                         R.layout.ws_single_page_nav_drawer_peek_view, mDrawer,
                         false /* attachToRoot */);
 
-        mTextView = (TextView) content.findViewById(R.id.ws_nav_drawer_text);
+        mTextView = content.findViewById(R.id.ws_nav_drawer_text);
         mSinglePageImageViews = new CircledImageView[count];
         for (int i = 0; i < count; i++) {
-            mSinglePageImageViews[i] = (CircledImageView) content
-                    .findViewById(SINGLE_PAGE_BUTTON_IDS[i]);
+            mSinglePageImageViews[i] = content.findViewById(SINGLE_PAGE_BUTTON_IDS[i]);
             mSinglePageImageViews[i].setOnClickListener(new OnSelectedClickHandler(i, mPresenter));
             mSinglePageImageViews[i].setCircleHidden(true);
         }
diff --git a/wear/src/main/java/android/support/wear/internal/widget/drawer/WearableNavigationDrawerPresenter.java b/wear/src/main/java/android/support/wear/internal/widget/drawer/WearableNavigationDrawerPresenter.java
index 1c8c4fb..df108aa 100644
--- a/wear/src/main/java/android/support/wear/internal/widget/drawer/WearableNavigationDrawerPresenter.java
+++ b/wear/src/main/java/android/support/wear/internal/widget/drawer/WearableNavigationDrawerPresenter.java
@@ -30,7 +30,7 @@
  *
  * @hide
  */
-@RestrictTo(Scope.LIBRARY_GROUP)
+@RestrictTo(Scope.LIBRARY)
 public abstract class WearableNavigationDrawerPresenter {
 
     private final Set<OnItemSelectedListener> mOnItemSelectedListeners = new HashSet<>();
diff --git a/wear/src/main/java/android/support/wear/utils/MetadataConstants.java b/wear/src/main/java/android/support/wear/utils/MetadataConstants.java
index 5be9c52..c7335c2 100644
--- a/wear/src/main/java/android/support/wear/utils/MetadataConstants.java
+++ b/wear/src/main/java/android/support/wear/utils/MetadataConstants.java
@@ -15,16 +15,13 @@
  */
 package android.support.wear.utils;
 
-import android.annotation.TargetApi;
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
-import android.os.Build;
 
 /**
  * Constants for android wear apps which are related to manifest meta-data.
  */
-@TargetApi(Build.VERSION_CODES.N)
 public class MetadataConstants {
 
     //  Constants for standalone apps. //
diff --git a/wear/src/main/java/android/support/wear/widget/BezierSCurveInterpolator.java b/wear/src/main/java/android/support/wear/widget/BezierSCurveInterpolator.java
index 131bae8..9c56a83 100644
--- a/wear/src/main/java/android/support/wear/widget/BezierSCurveInterpolator.java
+++ b/wear/src/main/java/android/support/wear/widget/BezierSCurveInterpolator.java
@@ -17,8 +17,6 @@
 package android.support.wear.widget;
 
 import android.animation.TimeInterpolator;
-import android.annotation.TargetApi;
-import android.os.Build;
 import android.support.annotation.RestrictTo;
 import android.support.annotation.RestrictTo.Scope;
 
@@ -27,8 +25,7 @@
  *
  * @hide
  */
-@RestrictTo(Scope.LIBRARY_GROUP)
-@TargetApi(Build.VERSION_CODES.KITKAT_WATCH)
+@RestrictTo(Scope.LIBRARY)
 class BezierSCurveInterpolator implements TimeInterpolator {
 
     /**
diff --git a/wear/src/main/java/android/support/wear/widget/BoxInsetLayout.java b/wear/src/main/java/android/support/wear/widget/BoxInsetLayout.java
index ba35f2c..a8b1381 100644
--- a/wear/src/main/java/android/support/wear/widget/BoxInsetLayout.java
+++ b/wear/src/main/java/android/support/wear/widget/BoxInsetLayout.java
@@ -20,7 +20,6 @@
 import android.content.res.TypedArray;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
-import android.os.Build;
 import android.support.annotation.IntDef;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
@@ -111,21 +110,6 @@
     }
 
     @Override
-    public WindowInsets onApplyWindowInsets(WindowInsets insets) {
-        insets = super.onApplyWindowInsets(insets);
-        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
-            final boolean round = insets.isRound();
-            if (round != mIsRound) {
-                mIsRound = round;
-                requestLayout();
-            }
-            mInsets.set(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(),
-                    insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom());
-        }
-        return insets;
-    }
-
-    @Override
     public void setForeground(Drawable drawable) {
         super.setForeground(drawable);
         mForegroundDrawable = drawable;
@@ -145,14 +129,10 @@
     @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
-        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
-            requestApplyInsets();
-        } else {
-            mIsRound = getResources().getConfiguration().isScreenRound();
-            WindowInsets insets = getRootWindowInsets();
-            mInsets.set(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(),
-                    insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom());
-        }
+        mIsRound = getResources().getConfiguration().isScreenRound();
+        WindowInsets insets = getRootWindowInsets();
+        mInsets.set(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(),
+                insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom());
     }
 
     @Override
@@ -413,7 +393,7 @@
     public static class LayoutParams extends FrameLayout.LayoutParams {
 
         /** @hide */
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
         @IntDef({BOX_NONE, BOX_LEFT, BOX_TOP, BOX_RIGHT, BOX_BOTTOM, BOX_ALL})
         @Retention(RetentionPolicy.SOURCE)
         public @interface BoxedEdges {}
diff --git a/wear/src/main/java/android/support/wear/widget/CircledImageView.java b/wear/src/main/java/android/support/wear/widget/CircledImageView.java
index 03ed8c9..c441dd5 100644
--- a/wear/src/main/java/android/support/wear/widget/CircledImageView.java
+++ b/wear/src/main/java/android/support/wear/widget/CircledImageView.java
@@ -19,7 +19,6 @@
 import android.animation.ArgbEvaluator;
 import android.animation.ValueAnimator;
 import android.animation.ValueAnimator.AnimatorUpdateListener;
-import android.annotation.TargetApi;
 import android.content.Context;
 import android.content.res.ColorStateList;
 import android.content.res.TypedArray;
@@ -32,7 +31,6 @@
 import android.graphics.RectF;
 import android.graphics.Shader;
 import android.graphics.drawable.Drawable;
-import android.os.Build;
 import android.support.annotation.Px;
 import android.support.annotation.RestrictTo;
 import android.support.annotation.RestrictTo.Scope;
@@ -47,8 +45,7 @@
  *
  * @hide
  */
-@TargetApi(Build.VERSION_CODES.M)
-@RestrictTo(Scope.LIBRARY_GROUP)
+@RestrictTo(Scope.LIBRARY)
 public class CircledImageView extends View {
 
     private static final ArgbEvaluator ARGB_EVALUATOR = new ArgbEvaluator();
@@ -133,13 +130,9 @@
         if (mDrawable != null && mDrawable.getConstantState() != null) {
             // The provided Drawable may be used elsewhere, so make a mutable clone before setTint()
             // or setAlpha() is called on it.
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
-                mDrawable =
-                        mDrawable.getConstantState()
-                                .newDrawable(context.getResources(), context.getTheme());
-            } else {
-                mDrawable = mDrawable.getConstantState().newDrawable(context.getResources());
-            }
+            mDrawable =
+                    mDrawable.getConstantState()
+                            .newDrawable(context.getResources(), context.getTheme());
             mDrawable = mDrawable.mutate();
         }
 
diff --git a/wear/src/main/java/android/support/wear/widget/CurvingLayoutCallback.java b/wear/src/main/java/android/support/wear/widget/CurvingLayoutCallback.java
index 275f1f8..5e88a8c 100644
--- a/wear/src/main/java/android/support/wear/widget/CurvingLayoutCallback.java
+++ b/wear/src/main/java/android/support/wear/widget/CurvingLayoutCallback.java
@@ -113,7 +113,7 @@
      */
     public void adjustAnchorOffsetXY(View child, float[] anchorOffsetXY) {
         return;
-    };
+    }
 
     @VisibleForTesting
     void setRound(boolean isScreenRound) {
diff --git a/wear/src/main/java/android/support/wear/widget/ProgressDrawable.java b/wear/src/main/java/android/support/wear/widget/ProgressDrawable.java
index 08e8ec2..28e0570 100644
--- a/wear/src/main/java/android/support/wear/widget/ProgressDrawable.java
+++ b/wear/src/main/java/android/support/wear/widget/ProgressDrawable.java
@@ -19,14 +19,12 @@
 import android.animation.ObjectAnimator;
 import android.animation.TimeInterpolator;
 import android.animation.ValueAnimator;
-import android.annotation.TargetApi;
 import android.graphics.Canvas;
 import android.graphics.ColorFilter;
 import android.graphics.Paint;
 import android.graphics.PixelFormat;
 import android.graphics.RectF;
 import android.graphics.drawable.Drawable;
-import android.os.Build;
 import android.support.annotation.RestrictTo;
 import android.support.annotation.RestrictTo.Scope;
 import android.util.Property;
@@ -37,8 +35,7 @@
  *
  * @hide
  */
-@TargetApi(Build.VERSION_CODES.KITKAT_WATCH)
-@RestrictTo(Scope.LIBRARY_GROUP)
+@RestrictTo(Scope.LIBRARY)
 class ProgressDrawable extends Drawable {
 
     private static final Property<ProgressDrawable, Integer> LEVEL =
diff --git a/wear/src/main/java/android/support/wear/widget/RoundedDrawable.java b/wear/src/main/java/android/support/wear/widget/RoundedDrawable.java
index fd09a87..300b6dd 100644
--- a/wear/src/main/java/android/support/wear/widget/RoundedDrawable.java
+++ b/wear/src/main/java/android/support/wear/widget/RoundedDrawable.java
@@ -15,7 +15,6 @@
  */
 package android.support.wear.widget;
 
-import android.annotation.TargetApi;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.graphics.Bitmap;
@@ -29,7 +28,6 @@
 import android.graphics.RectF;
 import android.graphics.Shader;
 import android.graphics.drawable.Drawable;
-import android.os.Build;
 import android.support.annotation.ColorInt;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
@@ -76,7 +74,6 @@
  *   app:radius="dimension"
  *   app:clipEnabled="boolean" /&gt;</pre>
  */
-@TargetApi(Build.VERSION_CODES.N)
 public class RoundedDrawable extends Drawable {
 
     @VisibleForTesting
diff --git a/wear/src/main/java/android/support/wear/widget/ScrollManager.java b/wear/src/main/java/android/support/wear/widget/ScrollManager.java
index 8155f62..e01a271 100644
--- a/wear/src/main/java/android/support/wear/widget/ScrollManager.java
+++ b/wear/src/main/java/android/support/wear/widget/ScrollManager.java
@@ -16,11 +16,8 @@
 
 package android.support.wear.widget;
 
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.annotation.TargetApi;
-import android.os.Build;
 import android.support.annotation.RestrictTo;
+import android.support.annotation.RestrictTo.Scope;
 import android.support.v7.widget.RecyclerView;
 import android.view.MotionEvent;
 import android.view.VelocityTracker;
@@ -30,8 +27,7 @@
  *
  * @hide
  */
-@TargetApi(Build.VERSION_CODES.M)
-@RestrictTo(LIBRARY_GROUP)
+@RestrictTo(Scope.LIBRARY)
 class ScrollManager {
     // One second in milliseconds.
     private static final int ONE_SEC_IN_MS = 1000;
diff --git a/wear/src/main/java/android/support/wear/widget/SimpleAnimatorListener.java b/wear/src/main/java/android/support/wear/widget/SimpleAnimatorListener.java
index a60b0bd..3a1e56b 100644
--- a/wear/src/main/java/android/support/wear/widget/SimpleAnimatorListener.java
+++ b/wear/src/main/java/android/support/wear/widget/SimpleAnimatorListener.java
@@ -29,7 +29,7 @@
  * @hide Hidden until this goes through review
  */
 @RequiresApi(Build.VERSION_CODES.KITKAT_WATCH)
-@RestrictTo(Scope.LIBRARY_GROUP)
+@RestrictTo(Scope.LIBRARY)
 public class SimpleAnimatorListener implements Animator.AnimatorListener {
 
     private boolean mWasCanceled;
diff --git a/wear/src/main/java/android/support/wear/widget/SwipeDismissLayout.java b/wear/src/main/java/android/support/wear/widget/SwipeDismissLayout.java
index 6e7a6f3..33da79c 100644
--- a/wear/src/main/java/android/support/wear/widget/SwipeDismissLayout.java
+++ b/wear/src/main/java/android/support/wear/widget/SwipeDismissLayout.java
@@ -16,12 +16,11 @@
 
 package android.support.wear.widget;
 
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
 import android.content.Context;
 import android.content.res.Resources;
 import android.support.annotation.Nullable;
 import android.support.annotation.RestrictTo;
+import android.support.annotation.RestrictTo.Scope;
 import android.support.annotation.UiThread;
 import android.util.AttributeSet;
 import android.util.Log;
@@ -40,7 +39,7 @@
  *
  * @hide
  */
-@RestrictTo(LIBRARY_GROUP)
+@RestrictTo(Scope.LIBRARY)
 @UiThread
 class SwipeDismissLayout extends FrameLayout {
     private static final String TAG = "SwipeDismissLayout";
diff --git a/wear/src/main/java/android/support/wear/widget/WearableRecyclerView.java b/wear/src/main/java/android/support/wear/widget/WearableRecyclerView.java
index 5cacdfc..1425e68 100644
--- a/wear/src/main/java/android/support/wear/widget/WearableRecyclerView.java
+++ b/wear/src/main/java/android/support/wear/widget/WearableRecyclerView.java
@@ -16,11 +16,9 @@
 
 package android.support.wear.widget;
 
-import android.annotation.TargetApi;
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.graphics.Point;
-import android.os.Build;
 import android.support.annotation.Nullable;
 import android.support.v7.widget.RecyclerView;
 import android.support.wear.R;
@@ -35,7 +33,6 @@
  *
  * @see #setCircularScrollingGestureEnabled(boolean)
  */
-@TargetApi(Build.VERSION_CODES.M)
 public class WearableRecyclerView extends RecyclerView {
     private static final String TAG = "WearableRecyclerView";
 
diff --git a/wear/src/main/java/android/support/wear/widget/drawer/AbsListViewFlingWatcher.java b/wear/src/main/java/android/support/wear/widget/drawer/AbsListViewFlingWatcher.java
index f1cb640..e9b2a40 100644
--- a/wear/src/main/java/android/support/wear/widget/drawer/AbsListViewFlingWatcher.java
+++ b/wear/src/main/java/android/support/wear/widget/drawer/AbsListViewFlingWatcher.java
@@ -32,7 +32,7 @@
  *
  * @hide
  */
-@RestrictTo(Scope.LIBRARY_GROUP)
+@RestrictTo(Scope.LIBRARY)
 class AbsListViewFlingWatcher implements FlingWatcher, OnScrollListener {
 
     private final FlingListener mListener;
diff --git a/wear/src/main/java/android/support/wear/widget/drawer/FlingWatcherFactory.java b/wear/src/main/java/android/support/wear/widget/drawer/FlingWatcherFactory.java
index 3fe84c6..2fdfa13 100644
--- a/wear/src/main/java/android/support/wear/widget/drawer/FlingWatcherFactory.java
+++ b/wear/src/main/java/android/support/wear/widget/drawer/FlingWatcherFactory.java
@@ -33,7 +33,7 @@
  *
  * @hide
  */
-@RestrictTo(Scope.LIBRARY_GROUP)
+@RestrictTo(Scope.LIBRARY)
 class FlingWatcherFactory {
 
     /**
diff --git a/wear/src/main/java/android/support/wear/widget/drawer/NestedScrollViewFlingWatcher.java b/wear/src/main/java/android/support/wear/widget/drawer/NestedScrollViewFlingWatcher.java
index ca95ab2..4c0e5c8 100644
--- a/wear/src/main/java/android/support/wear/widget/drawer/NestedScrollViewFlingWatcher.java
+++ b/wear/src/main/java/android/support/wear/widget/drawer/NestedScrollViewFlingWatcher.java
@@ -38,7 +38,7 @@
  *
  * @hide
  */
-@RestrictTo(Scope.LIBRARY_GROUP)
+@RestrictTo(Scope.LIBRARY)
 class NestedScrollViewFlingWatcher implements FlingWatcher, OnScrollChangeListener {
 
     static final int MAX_WAIT_TIME_MS = 100;
diff --git a/wear/src/main/java/android/support/wear/widget/drawer/PageIndicatorView.java b/wear/src/main/java/android/support/wear/widget/drawer/PageIndicatorView.java
index 99c7c09..1285f72 100644
--- a/wear/src/main/java/android/support/wear/widget/drawer/PageIndicatorView.java
+++ b/wear/src/main/java/android/support/wear/widget/drawer/PageIndicatorView.java
@@ -54,7 +54,7 @@
  * @hide
  */
 @RequiresApi(Build.VERSION_CODES.M)
-@RestrictTo(Scope.LIBRARY_GROUP)
+@RestrictTo(Scope.LIBRARY)
 public class PageIndicatorView extends View implements OnPageChangeListener {
 
     private static final String TAG = "Dots";
diff --git a/wear/src/main/java/android/support/wear/widget/drawer/RecyclerViewFlingWatcher.java b/wear/src/main/java/android/support/wear/widget/drawer/RecyclerViewFlingWatcher.java
index 7570fae..7916875 100644
--- a/wear/src/main/java/android/support/wear/widget/drawer/RecyclerViewFlingWatcher.java
+++ b/wear/src/main/java/android/support/wear/widget/drawer/RecyclerViewFlingWatcher.java
@@ -31,7 +31,7 @@
  *
  * @hide
  */
-@RestrictTo(Scope.LIBRARY_GROUP)
+@RestrictTo(Scope.LIBRARY)
 class RecyclerViewFlingWatcher extends OnScrollListener implements FlingWatcher {
 
     private final FlingListener mListener;
diff --git a/wear/src/main/java/android/support/wear/widget/drawer/ScrollViewFlingWatcher.java b/wear/src/main/java/android/support/wear/widget/drawer/ScrollViewFlingWatcher.java
index f0b973b..5154e7b 100644
--- a/wear/src/main/java/android/support/wear/widget/drawer/ScrollViewFlingWatcher.java
+++ b/wear/src/main/java/android/support/wear/widget/drawer/ScrollViewFlingWatcher.java
@@ -38,7 +38,7 @@
  *
  * @hide
  */
-@RestrictTo(Scope.LIBRARY_GROUP)
+@RestrictTo(Scope.LIBRARY)
 class ScrollViewFlingWatcher implements FlingWatcher, OnScrollChangeListener {
 
     static final int MAX_WAIT_TIME_MS = 100;
diff --git a/wear/src/main/java/android/support/wear/widget/drawer/WearableActionDrawerMenu.java b/wear/src/main/java/android/support/wear/widget/drawer/WearableActionDrawerMenu.java
index 158467d..092ac72 100644
--- a/wear/src/main/java/android/support/wear/widget/drawer/WearableActionDrawerMenu.java
+++ b/wear/src/main/java/android/support/wear/widget/drawer/WearableActionDrawerMenu.java
@@ -16,12 +16,10 @@
 
 package android.support.wear.widget.drawer;
 
-import android.annotation.TargetApi;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.drawable.Drawable;
-import android.os.Build;
 import android.support.annotation.Nullable;
 import android.view.ActionProvider;
 import android.view.ContextMenu;
@@ -34,7 +32,6 @@
 import java.util.ArrayList;
 import java.util.List;
 
-@TargetApi(Build.VERSION_CODES.M)
 /* package */ class WearableActionDrawerMenu implements Menu {
 
     private final Context mContext;
diff --git a/wear/src/main/java/android/support/wear/widget/drawer/WearableActionDrawerView.java b/wear/src/main/java/android/support/wear/widget/drawer/WearableActionDrawerView.java
index 03f494a..99cd4ff 100644
--- a/wear/src/main/java/android/support/wear/widget/drawer/WearableActionDrawerView.java
+++ b/wear/src/main/java/android/support/wear/widget/drawer/WearableActionDrawerView.java
@@ -16,12 +16,10 @@
 
 package android.support.wear.widget.drawer;
 
-import android.annotation.TargetApi;
 import android.content.Context;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.graphics.drawable.Drawable;
-import android.os.Build;
 import android.support.annotation.Nullable;
 import android.support.v7.widget.LinearLayoutManager;
 import android.support.v7.widget.RecyclerView;
@@ -76,7 +74,6 @@
  * <p>For {@link MenuItem}, setting and getting the title and icon, {@link MenuItem#getItemId}, and
  * {@link MenuItem#setOnMenuItemClickListener} are implemented.
  */
-@TargetApi(Build.VERSION_CODES.M)
 public class WearableActionDrawerView extends WearableDrawerView {
 
     private static final String TAG = "WearableActionDrawer";
@@ -141,12 +138,8 @@
             View peekView = layoutInflater.inflate(R.layout.ws_action_drawer_peek_view,
                     getPeekContainer(), false /* attachToRoot */);
             setPeekContent(peekView);
-            mPeekActionIcon =
-                    (ImageView) peekView
-                            .findViewById(R.id.ws_action_drawer_peek_action_icon);
-            mPeekExpandIcon =
-                    (ImageView) peekView
-                            .findViewById(R.id.ws_action_drawer_expand_icon);
+            mPeekActionIcon = peekView.findViewById(R.id.ws_action_drawer_peek_action_icon);
+            mPeekExpandIcon = peekView.findViewById(R.id.ws_action_drawer_expand_icon);
         } else {
             mPeekActionIcon = null;
             mPeekExpandIcon = null;
diff --git a/wear/src/main/java/android/support/wear/widget/drawer/WearableDrawerLayout.java b/wear/src/main/java/android/support/wear/widget/drawer/WearableDrawerLayout.java
index 6d27064..e100a46 100644
--- a/wear/src/main/java/android/support/wear/widget/drawer/WearableDrawerLayout.java
+++ b/wear/src/main/java/android/support/wear/widget/drawer/WearableDrawerLayout.java
@@ -19,11 +19,10 @@
 import static android.support.wear.widget.drawer.WearableDrawerView.STATE_IDLE;
 import static android.support.wear.widget.drawer.WearableDrawerView.STATE_SETTLING;
 
-import android.annotation.TargetApi;
 import android.content.Context;
-import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
+import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.annotation.VisibleForTesting;
 import android.support.v4.view.NestedScrollingParent;
@@ -98,7 +97,6 @@
  *     &lt;/android.support.wear.widget.drawer.WearableDrawerView&gt;
  * &lt;/android.support.wear.widget.drawer.WearableDrawerLayout&gt;</pre>
  */
-@TargetApi(Build.VERSION_CODES.M)
 public class WearableDrawerLayout extends FrameLayout
         implements View.OnLayoutChangeListener, NestedScrollingParent, FlingListener {
 
@@ -654,12 +652,13 @@
     }
 
     @Override // NestedScrollingParent
-    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
+    public boolean onNestedFling(@NonNull View target, float velocityX, float velocityY,
+            boolean consumed) {
         return false;
     }
 
     @Override // NestedScrollingParent
-    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
+    public boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY) {
         maybeUpdateScrollingContentView(target);
         mLastScrollWasFling = true;
 
@@ -674,13 +673,13 @@
     }
 
     @Override // NestedScrollingParent
-    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
+    public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed) {
         maybeUpdateScrollingContentView(target);
     }
 
     @Override // NestedScrollingParent
-    public void onNestedScroll(
-            View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
+    public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
+            int dxUnconsumed, int dyUnconsumed) {
 
         boolean scrolledUp = dyConsumed < 0;
         boolean scrolledDown = dyConsumed > 0;
@@ -873,18 +872,20 @@
     }
 
     @Override // NestedScrollingParent
-    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
+    public void onNestedScrollAccepted(@NonNull View child, @NonNull View target,
+            int nestedScrollAxes) {
         mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
     }
 
     @Override // NestedScrollingParent
-    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
+    public boolean onStartNestedScroll(@NonNull View child, @NonNull View target,
+            int nestedScrollAxes) {
         mCurrentNestedScrollSlopTracker = 0;
         return true;
     }
 
     @Override // NestedScrollingParent
-    public void onStopNestedScroll(View target) {
+    public void onStopNestedScroll(@NonNull View target) {
         mNestedScrollingParentHelper.onStopNestedScroll(target);
     }
 
@@ -961,7 +962,7 @@
         public abstract WearableDrawerView getDrawerView();
 
         @Override
-        public boolean tryCaptureView(View child, int pointerId) {
+        public boolean tryCaptureView(@NonNull View child, int pointerId) {
             WearableDrawerView drawerView = getDrawerView();
             // Returns true if the dragger is dragging the drawer.
             return child == drawerView && !drawerView.isLocked()
@@ -969,13 +970,13 @@
         }
 
         @Override
-        public int getViewVerticalDragRange(View child) {
+        public int getViewVerticalDragRange(@NonNull View child) {
             // Defines the vertical drag range of the drawer.
             return child == getDrawerView() ? child.getHeight() : 0;
         }
 
         @Override
-        public void onViewCaptured(View capturedChild, int activePointerId) {
+        public void onViewCaptured(@NonNull View capturedChild, int activePointerId) {
             showDrawerContentMaybeAnimate((WearableDrawerView) capturedChild);
         }
 
@@ -1036,7 +1037,7 @@
     private class TopDrawerDraggerCallback extends DrawerDraggerCallback {
 
         @Override
-        public int clampViewPositionVertical(View child, int top, int dy) {
+        public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
             if (mTopDrawerView == child) {
                 int peekHeight = mTopDrawerView.getPeekContainer().getHeight();
                 // The top drawer can be dragged vertically from peekHeight - height to 0.
@@ -1063,7 +1064,7 @@
         }
 
         @Override
-        public void onViewReleased(View releasedChild, float xvel, float yvel) {
+        public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
             if (releasedChild == mTopDrawerView) {
                 // Settle to final position. Either swipe open or close.
                 final float openedPercent = mTopDrawerView.getOpenedPercent();
@@ -1085,7 +1086,8 @@
         }
 
         @Override
-        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
+        public void onViewPositionChanged(@NonNull View changedView, int left, int top, int dx,
+                int dy) {
             if (changedView == mTopDrawerView) {
                 // Compute the offset and invalidate will move the drawer during layout.
                 final int height = changedView.getHeight();
@@ -1106,7 +1108,7 @@
     private class BottomDrawerDraggerCallback extends DrawerDraggerCallback {
 
         @Override
-        public int clampViewPositionVertical(View child, int top, int dy) {
+        public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
             if (mBottomDrawerView == child) {
                 // The bottom drawer can be dragged vertically from (parentHeight - height) to
                 // (parentHeight - peekHeight).
@@ -1131,7 +1133,7 @@
         }
 
         @Override
-        public void onViewReleased(View releasedChild, float xvel, float yvel) {
+        public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
             if (releasedChild == mBottomDrawerView) {
                 // Settle to final position. Either swipe open or close.
                 final int parentHeight = getHeight();
@@ -1151,7 +1153,8 @@
         }
 
         @Override
-        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
+        public void onViewPositionChanged(@NonNull View changedView, int left, int top, int dx,
+                int dy) {
             if (changedView == mBottomDrawerView) {
                 // Compute the offset and invalidate will move the drawer during layout.
                 final int height = changedView.getHeight();
diff --git a/wear/src/main/java/android/support/wear/widget/drawer/WearableDrawerView.java b/wear/src/main/java/android/support/wear/widget/drawer/WearableDrawerView.java
index dafac39..2462cba 100644
--- a/wear/src/main/java/android/support/wear/widget/drawer/WearableDrawerView.java
+++ b/wear/src/main/java/android/support/wear/widget/drawer/WearableDrawerView.java
@@ -16,11 +16,9 @@
 
 package android.support.wear.widget.drawer;
 
-import android.annotation.TargetApi;
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.graphics.drawable.Drawable;
-import android.os.Build;
 import android.support.annotation.IdRes;
 import android.support.annotation.IntDef;
 import android.support.annotation.Nullable;
@@ -87,7 +85,6 @@
  *     &lt;/LinearLayout&gt;
  * &lt;/android.support.wear.widget.drawer.WearableDrawerView&gt;</pre>
  */
-@TargetApi(Build.VERSION_CODES.M)
 public class WearableDrawerView extends FrameLayout {
     /**
      * Indicates that the drawer is in an idle, settled state. No animation is in progress.
@@ -109,7 +106,7 @@
      * @hide
      */
     @Retention(RetentionPolicy.SOURCE)
-    @RestrictTo(Scope.LIBRARY_GROUP)
+    @RestrictTo(Scope.LIBRARY)
     @IntDef({STATE_IDLE, STATE_DRAGGING, STATE_SETTLING})
     public @interface DrawerState {}
 
@@ -155,8 +152,8 @@
         setElevation(context.getResources()
                 .getDimension(R.dimen.ws_wearable_drawer_view_elevation));
 
-        mPeekContainer = (ViewGroup) findViewById(R.id.ws_drawer_view_peek_container);
-        mPeekIcon = (ImageView) findViewById(R.id.ws_drawer_view_peek_icon);
+        mPeekContainer = findViewById(R.id.ws_drawer_view_peek_container);
+        mPeekIcon = findViewById(R.id.ws_drawer_view_peek_icon);
 
         mPeekContainer.setOnClickListener(
                 new OnClickListener() {
diff --git a/wear/src/main/java/android/support/wear/widget/drawer/WearableNavigationDrawerView.java b/wear/src/main/java/android/support/wear/widget/drawer/WearableNavigationDrawerView.java
index 480812b..c5c49fe 100644
--- a/wear/src/main/java/android/support/wear/widget/drawer/WearableNavigationDrawerView.java
+++ b/wear/src/main/java/android/support/wear/widget/drawer/WearableNavigationDrawerView.java
@@ -16,11 +16,9 @@
 
 package android.support.wear.widget.drawer;
 
-import android.annotation.TargetApi;
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.graphics.drawable.Drawable;
-import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
 import android.support.annotation.IntDef;
@@ -58,7 +56,6 @@
  * <p>The developer may specify which style to use with the {@code app:navigationStyle} custom
  * attribute. If not specified, {@link #SINGLE_PAGE singlePage} will be used as the default.
  */
-@TargetApi(Build.VERSION_CODES.M)
 public class WearableNavigationDrawerView extends WearableDrawerView {
 
     private static final String TAG = "WearableNavDrawer";
@@ -79,7 +76,7 @@
      * @hide
      */
     @Retention(RetentionPolicy.SOURCE)
-    @RestrictTo(Scope.LIBRARY_GROUP)
+    @RestrictTo(Scope.LIBRARY)
     @IntDef({SINGLE_PAGE, MULTI_PAGE})
     public @interface NavigationStyle {}
 
@@ -282,7 +279,7 @@
         /**
          * @hide
          */
-        @RestrictTo(Scope.LIBRARY_GROUP)
+        @RestrictTo(Scope.LIBRARY)
         public void setPresenter(WearableNavigationDrawerPresenter presenter) {
             mPresenter = presenter;
         }
diff --git a/webkit/.gitignore b/webkit/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/webkit/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/webkit/AndroidManifest.xml b/webkit/AndroidManifest.xml
new file mode 100644
index 0000000..7d2bbc2
--- /dev/null
+++ b/webkit/AndroidManifest.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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="androidx.webkit">
+</manifest>
+
diff --git a/webkit/OWNERS b/webkit/OWNERS
new file mode 100644
index 0000000..5d88928
--- /dev/null
+++ b/webkit/OWNERS
@@ -0,0 +1,5 @@
+boliu@google.com
+michaelbai@google.com
+tobiasjs@google.com
+torne@google.com
+gsennton@google.com
diff --git a/media-compat-test-service/build.gradle b/webkit/build.gradle
similarity index 61%
copy from media-compat-test-service/build.gradle
copy to webkit/build.gradle
index 946d48b..e4fff11 100644
--- a/media-compat-test-service/build.gradle
+++ b/webkit/build.gradle
@@ -13,27 +13,20 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
-plugins {
-    id("SupportAndroidLibraryPlugin")
-}
-
-dependencies {
-    androidTestImplementation project(':support-annotations')
-    androidTestImplementation project(':support-media-compat')
-    androidTestImplementation project(':support-media-compat-test-lib')
-
-    androidTestImplementation(libs.test_runner) {
-        exclude module: 'support-annotations'
-    }
-}
+apply plugin: android.support.SupportAndroidLibraryPlugin
 
 android {
     defaultConfig {
-        minSdkVersion 14
+        minSdkVersion 21
+    }
+
+    sourceSets {
+        main.manifest.srcFile 'AndroidManifest.xml'
     }
 }
 
 supportLibrary {
-    legacySourceLocation = true
+    name = "WebView Support Library"
+    inceptionYear = "2017"
+    description = "The WebView Support Library is a static library you can add to your Android application in order to use android.webkit APIs that are not available for older platform versions."
 }
diff --git a/media-compat-test-client/lint-baseline.xml b/webkit/lint-baseline.xml
similarity index 100%
rename from media-compat-test-client/lint-baseline.xml
rename to webkit/lint-baseline.xml
diff --git a/media-compat-test-client/tests/NO_DOCS b/webkit/tests/NO_DOCS
similarity index 100%
rename from media-compat-test-client/tests/NO_DOCS
rename to webkit/tests/NO_DOCS