Fix a crash in clearning BottomNavigationView menu.
am: 2befdb72bf

Change-Id: I04422aa4cd73f1761e5017d335836aa2ca0b6b8c
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index 4caef90..2811ea9 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -1,5 +1,5 @@
 [Hook Scripts]
-checkstyle_hook = ../../development/tools/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
+checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
 
 [Builtin Hooks]
 commit_msg_changeid_field = true
diff --git a/annotations/src/android/support/annotation/IntDef.java b/annotations/src/android/support/annotation/IntDef.java
index be2e2b8..f621b7f 100644
--- a/annotations/src/android/support/annotation/IntDef.java
+++ b/annotations/src/android/support/annotation/IntDef.java
@@ -15,16 +15,12 @@
  */
 package android.support.annotation;
 
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
 import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
-import static java.lang.annotation.ElementType.FIELD;
-import static java.lang.annotation.ElementType.METHOD;
-import static java.lang.annotation.ElementType.PARAMETER;
 import static java.lang.annotation.RetentionPolicy.SOURCE;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
 /**
  * Denotes that the annotated element of integer type, represents
  * a logical type and that its value should be one of the explicitly
@@ -47,7 +43,7 @@
  * For a flag, set the flag attribute:
  * <pre><code>
  *  &#64;IntDef(
- *      flag = true
+ *      flag = true,
  *      value = {NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS})
  * </code></pre>
  */
diff --git a/build.gradle b/build.gradle
index d7cced1..4113929 100644
--- a/build.gradle
+++ b/build.gradle
@@ -24,7 +24,7 @@
     }
     dependencies {
         // Keep gradle plugin version in sync with ub_supportlib-master manifest.
-        classpath 'com.android.tools.build:gradle:2.2.1'
+        classpath 'com.android.tools.build:gradle:2.2.4'
     }
 }
 
@@ -41,7 +41,7 @@
 }
 
 gradle.ext.currentSdk = 25
-ext.supportVersion = '25.1.0'
+ext.supportVersion = '25.2.0-SNAPSHOT'
 ext.extraVersion = 41
 ext.supportRepoOut = ''
 ext.buildToolsVersion = '24.0.1'
diff --git a/compat/build.gradle b/compat/build.gradle
index 5722998..e87db0e 100644
--- a/compat/build.gradle
+++ b/compat/build.gradle
@@ -15,9 +15,6 @@
     testCompile 'junit:junit:4.12'
 }
 
-sourceCompatibility = JavaVersion.VERSION_1_7
-targetCompatibility = JavaVersion.VERSION_1_7
-
 android {
     compileSdkVersion project.ext.currentSdk
 
@@ -58,11 +55,7 @@
         sourceCompatibility JavaVersion.VERSION_1_7
         targetCompatibility JavaVersion.VERSION_1_7
     }
-
-    testOptions {
-        unitTests.returnDefaultValues = true
         compileSdkVersion project.ext.currentSdk
-    }
 }
 
 android.libraryVariants.all { variant ->
@@ -73,22 +66,6 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
@@ -96,7 +73,6 @@
         exclude('android/service/media/**')
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
 
diff --git a/core-ui/build.gradle b/core-ui/build.gradle
index 89796ea..0b94a96 100644
--- a/core-ui/build.gradle
+++ b/core-ui/build.gradle
@@ -16,9 +16,6 @@
     testCompile 'junit:junit:4.12'
 }
 
-sourceCompatibility = JavaVersion.VERSION_1_7
-targetCompatibility = JavaVersion.VERSION_1_7
-
 android {
     compileSdkVersion project.ext.currentSdk
 
@@ -54,7 +51,6 @@
 
     testOptions {
         unitTests.returnDefaultValues = true
-        compileSdkVersion project.ext.currentSdk
     }
 }
 
@@ -66,22 +62,6 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
@@ -89,7 +69,6 @@
         exclude('android/service/media/**')
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
 
diff --git a/core-utils/build.gradle b/core-utils/build.gradle
index 750cf49..d40781d 100644
--- a/core-utils/build.gradle
+++ b/core-utils/build.gradle
@@ -48,11 +48,6 @@
         sourceCompatibility JavaVersion.VERSION_1_7
         targetCompatibility JavaVersion.VERSION_1_7
     }
-
-    testOptions {
-        unitTests.returnDefaultValues = true
-        compileSdkVersion project.ext.currentSdk
-    }
 }
 
 android.libraryVariants.all { variant ->
@@ -63,22 +58,6 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
@@ -86,7 +65,6 @@
         exclude('android/service/media/**')
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
 
diff --git a/customtabs/build.gradle b/customtabs/build.gradle
index a409dba..e427d1e 100644
--- a/customtabs/build.gradle
+++ b/customtabs/build.gradle
@@ -1,5 +1,4 @@
 apply plugin: 'com.android.library'
-
 archivesBaseName = 'customtabs'
 
 dependencies {
@@ -48,27 +47,10 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
diff --git a/design/build.gradle b/design/build.gradle
index abd99c0..37e6625 100644
--- a/design/build.gradle
+++ b/design/build.gradle
@@ -83,28 +83,11 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
 
diff --git a/design/res/layout/design_bottom_navigation_item.xml b/design/res/layout/design_bottom_navigation_item.xml
index 67df838..f6212cf 100644
--- a/design/res/layout/design_bottom_navigation_item.xml
+++ b/design/res/layout/design_bottom_navigation_item.xml
@@ -26,8 +26,9 @@
     <android.support.design.internal.BaselineLayout
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:layout_marginBottom="@dimen/design_bottom_navigation_margin"
         android:layout_gravity="bottom|center_horizontal"
+        android:clipToPadding="false"
+        android:paddingBottom="10dp"
         android:duplicateParentState="true">
         <TextView
             android:id="@+id/smallLabel"
diff --git a/design/src/android/support/design/internal/BaselineLayout.java b/design/src/android/support/design/internal/BaselineLayout.java
index eac3542..23a04cd 100644
--- a/design/src/android/support/design/internal/BaselineLayout.java
+++ b/design/src/android/support/design/internal/BaselineLayout.java
@@ -24,7 +24,8 @@
 import android.view.ViewGroup;
 
 /**
- * A simple ViewGroup that aligns all the views inside on a baseline.
+ * A simple ViewGroup that aligns all the views inside on a baseline. Note: bottom padding for this
+ * view will be measured starting from the baseline.
  *
  * @hide
  */
@@ -69,6 +70,7 @@
                     ViewCompat.getMeasuredState(child));
         }
         if (maxChildBaseline != -1) {
+            maxChildDescent = Math.max(maxChildDescent, getPaddingBottom());
             maxHeight = Math.max(maxHeight, maxChildBaseline + maxChildDescent);
             mBaseline = maxChildBaseline;
         }
diff --git a/design/src/android/support/design/widget/AppBarLayout.java b/design/src/android/support/design/widget/AppBarLayout.java
index 4c7ef0a..8a34239 100644
--- a/design/src/android/support/design/widget/AppBarLayout.java
+++ b/design/src/android/support/design/widget/AppBarLayout.java
@@ -111,6 +111,7 @@
     static final int PENDING_ACTION_EXPANDED = 0x1;
     static final int PENDING_ACTION_COLLAPSED = 0x2;
     static final int PENDING_ACTION_ANIMATE_ENABLED = 0x4;
+    static final int PENDING_ACTION_FORCE = 0x8;
 
     /**
      * Interface definition for a callback to be invoked when an {@link AppBarLayout}'s vertical
@@ -172,7 +173,7 @@
                 0, R.style.Widget_Design_AppBarLayout);
         ViewCompat.setBackground(this, a.getDrawable(R.styleable.AppBarLayout_android_background));
         if (a.hasValue(R.styleable.AppBarLayout_expanded)) {
-            setExpanded(a.getBoolean(R.styleable.AppBarLayout_expanded, false));
+            setExpanded(a.getBoolean(R.styleable.AppBarLayout_expanded, false), false, false);
         }
         if (Build.VERSION.SDK_INT >= 21 && a.hasValue(R.styleable.AppBarLayout_elevation)) {
             ViewUtilsLollipop.setDefaultAppBarLayoutStateListAnimator(
@@ -299,8 +300,13 @@
      * @attr ref android.support.design.R.styleable#AppBarLayout_expanded
      */
     public void setExpanded(boolean expanded, boolean animate) {
+        setExpanded(expanded, animate, true);
+    }
+
+    private void setExpanded(boolean expanded, boolean animate, boolean force) {
         mPendingAction = (expanded ? PENDING_ACTION_EXPANDED : PENDING_ACTION_COLLAPSED)
-                | (animate ? PENDING_ACTION_ANIMATE_ENABLED : 0);
+                | (animate ? PENDING_ACTION_ANIMATE_ENABLED : 0)
+                | (force ? PENDING_ACTION_FORCE : 0);
         requestLayout();
     }
 
@@ -1048,8 +1054,21 @@
                 int layoutDirection) {
             boolean handled = super.onLayoutChild(parent, abl, layoutDirection);
 
+            // The priority for for actions here is (first which is true wins):
+            // 1. forced pending actions
+            // 2. offsets for restorations
+            // 3. non-forced pending actions
             final int pendingAction = abl.getPendingAction();
-            if (pendingAction != PENDING_ACTION_NONE) {
+            if (mOffsetToChildIndexOnLayout >= 0 && (pendingAction & PENDING_ACTION_FORCE) == 0) {
+                View child = abl.getChildAt(mOffsetToChildIndexOnLayout);
+                int offset = -child.getBottom();
+                if (mOffsetToChildIndexOnLayoutIsMinHeight) {
+                    offset += ViewCompat.getMinimumHeight(child) + abl.getTopInset();
+                } else {
+                    offset += Math.round(child.getHeight() * mOffsetToChildIndexOnLayoutPerc);
+                }
+                setHeaderTopBottomOffset(parent, abl, offset);
+            } else if (pendingAction != PENDING_ACTION_NONE) {
                 final boolean animate = (pendingAction & PENDING_ACTION_ANIMATE_ENABLED) != 0;
                 if ((pendingAction & PENDING_ACTION_COLLAPSED) != 0) {
                     final int offset = -abl.getUpNestedPreScrollRange();
@@ -1065,15 +1084,6 @@
                         setHeaderTopBottomOffset(parent, abl, 0);
                     }
                 }
-            } else if (mOffsetToChildIndexOnLayout >= 0) {
-                View child = abl.getChildAt(mOffsetToChildIndexOnLayout);
-                int offset = -child.getBottom();
-                if (mOffsetToChildIndexOnLayoutIsMinHeight) {
-                    offset += ViewCompat.getMinimumHeight(child);
-                } else {
-                    offset += Math.round(child.getHeight() * mOffsetToChildIndexOnLayoutPerc);
-                }
-                setTopAndBottomOffset(offset);
             }
 
             // Finally reset any pending states
@@ -1085,6 +1095,11 @@
             setTopAndBottomOffset(
                     MathUtils.constrain(getTopAndBottomOffset(), -abl.getTotalScrollRange(), 0));
 
+            // Update the AppBarLayout's drawable state for any elevation changes.
+            // This is needed so that the elevation is set in the first layout, so that
+            // we don't get a visual elevation jump pre-N (due to the draw dispatch skip)
+            updateAppBarLayoutDrawableState(parent, abl, getTopAndBottomOffset(), 0, true);
+
             // Make sure we dispatch the offset update
             abl.dispatchOffsetUpdates(getTopAndBottomOffset());
 
@@ -1161,7 +1176,7 @@
 
                     // Update the AppBarLayout's drawable state (for any elevation changes)
                     updateAppBarLayoutDrawableState(coordinatorLayout, appBarLayout, newOffset,
-                            newOffset < curOffset ? -1 : 1);
+                            newOffset < curOffset ? -1 : 1, false);
                 }
             } else {
                 // Reset the offset delta
@@ -1224,7 +1239,8 @@
         }
 
         private void updateAppBarLayoutDrawableState(final CoordinatorLayout parent,
-                final AppBarLayout layout, final int offset, final int direction) {
+                final AppBarLayout layout, final int offset, final int direction,
+                final boolean forceJump) {
             final View child = getAppBarChildOnOffset(layout, offset);
             if (child != null) {
                 final AppBarLayout.LayoutParams childLp = (LayoutParams) child.getLayoutParams();
@@ -1248,8 +1264,8 @@
 
                 final boolean changed = layout.setCollapsedState(collapsed);
 
-                if (changed && Build.VERSION.SDK_INT >= 11
-                        && shouldJumpElevationState(parent, layout)) {
+                if (Build.VERSION.SDK_INT >= 11 && (forceJump
+                        || (changed && shouldJumpElevationState(parent, layout)))) {
                     // If the collapsed state changed, we may need to
                     // jump to the current state if we have an overlapping view
                     layout.jumpDrawablesToCurrentState();
diff --git a/design/src/android/support/design/widget/CoordinatorLayout.java b/design/src/android/support/design/widget/CoordinatorLayout.java
index bac67f9..1f570b6 100644
--- a/design/src/android/support/design/widget/CoordinatorLayout.java
+++ b/design/src/android/support/design/widget/CoordinatorLayout.java
@@ -1293,7 +1293,7 @@
                 offsetChildByInset(child, inset, layoutDirection);
             }
 
-            if (type == EVENT_PRE_DRAW) {
+            if (type != EVENT_VIEW_REMOVED) {
                 // Did it change? if not continue
                 getLastChildRect(child, lastDrawRect);
                 if (lastDrawRect.equals(drawRect)) {
diff --git a/design/src/android/support/design/widget/TextInputLayout.java b/design/src/android/support/design/widget/TextInputLayout.java
index 92c1506..10da563 100644
--- a/design/src/android/support/design/widget/TextInputLayout.java
+++ b/design/src/android/support/design/widget/TextInputLayout.java
@@ -201,8 +201,6 @@
         mCollapsingTextHelper.setPositionInterpolator(new AccelerateInterpolator());
         mCollapsingTextHelper.setCollapsedTextGravity(Gravity.TOP | GravityCompat.START);
 
-        mHintExpanded = mCollapsingTextHelper.getExpansionFraction() == 1f;
-
         final TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, attrs,
                 R.styleable.TextInputLayout, defStyleAttr, R.style.Widget_Design_TextInputLayout);
         mHintEnabled = a.getBoolean(R.styleable.TextInputLayout_hintEnabled, true);
@@ -380,8 +378,8 @@
 
         updatePasswordToggleView();
 
-        // Update the label visibility with no animation
-        updateLabelState(false);
+        // Update the label visibility with no animation, but force a state change
+        updateLabelState(false, true);
     }
 
     private void updateInputLayoutMargins() {
@@ -408,6 +406,10 @@
     }
 
     void updateLabelState(boolean animate) {
+        updateLabelState(animate, false);
+    }
+
+    void updateLabelState(final boolean animate, final boolean force) {
         final boolean isEnabled = isEnabled();
         final boolean hasText = mEditText != null && !TextUtils.isEmpty(mEditText.getText());
         final boolean isFocused = arrayContains(getDrawableState(), android.R.attr.state_focused);
@@ -427,12 +429,12 @@
 
         if (hasText || (isEnabled() && (isFocused || isErrorShowing))) {
             // We should be showing the label so do so if it isn't already
-            if (mHintExpanded) {
+            if (force || mHintExpanded) {
                 collapseHint(animate);
             }
         } else {
             // We should not be showing the label so hide it
-            if (!mHintExpanded) {
+            if (force || !mHintExpanded) {
                 expandHint(animate);
             }
         }
@@ -1076,6 +1078,13 @@
                 });
             }
 
+            if (mEditText != null && ViewCompat.getMinimumHeight(mEditText) <= 0) {
+                // We should make sure that the EditText has the same min-height as the password
+                // toggle view. This ensure focus works properly, and there is no visual jump
+                // if the password toggle is enabled/disabled.
+                mEditText.setMinimumHeight(ViewCompat.getMinimumHeight(mPasswordToggleView));
+            }
+
             mPasswordToggleView.setVisibility(VISIBLE);
             mPasswordToggleView.setChecked(mPasswordToggledVisible);
 
diff --git a/design/tests/AndroidManifest.xml b/design/tests/AndroidManifest.xml
index 29d46e0..886540d 100755
--- a/design/tests/AndroidManifest.xml
+++ b/design/tests/AndroidManifest.xml
@@ -88,6 +88,10 @@
         <activity
                 android:name="android.support.v7.app.AppCompatActivity"/>
 
+        <activity
+            android:name="android.support.design.widget.AppBarLayoutCollapsePinTestActivity"
+            android:theme="@style/Theme.TranslucentStatus"/>
+
     </application>
 
     <instrumentation
diff --git a/design/tests/res/layout/design_appbar_toolbar_collapse_pin_restore_test.xml b/design/tests/res/layout/design_appbar_toolbar_collapse_pin_restore_test.xml
new file mode 100644
index 0000000..fbe031b
--- /dev/null
+++ b/design/tests/res/layout/design_appbar_toolbar_collapse_pin_restore_test.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     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.
+-->
+
+<android.support.design.widget.CoordinatorLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/coordinator_layout"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:fitsSystemWindows="true">
+
+    <android.support.design.widget.AppBarLayout
+        android:id="@+id/app_bar"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/appbar_height"
+        android:fitsSystemWindows="true"
+        app:expanded="true">
+
+        <android.support.design.widget.CollapsingToolbarLayout
+            android:id="@+id/collapsing_app_bar"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            app:layout_scrollFlags="scroll|exitUntilCollapsed">
+
+            <android.support.v7.widget.Toolbar
+                android:id="@+id/toolbar"
+                android:layout_height="?attr/actionBarSize"
+                android:layout_width="match_parent"
+                app:layout_collapseMode="pin"/>
+
+        </android.support.design.widget.CollapsingToolbarLayout>
+
+    </android.support.design.widget.AppBarLayout>
+
+    <include layout="@layout/include_appbar_scrollview" />
+
+</android.support.design.widget.CoordinatorLayout>
diff --git a/design/tests/res/layout/design_navigation_view.xml b/design/tests/res/layout/design_navigation_view.xml
index 1789843..4f7147d 100644
--- a/design/tests/res/layout/design_navigation_view.xml
+++ b/design/tests/res/layout/design_navigation_view.xml
@@ -37,7 +37,7 @@
          android:fitsSystemWindows="true" tells the system to have
          DrawerLayout span the full height of the screen, including the
          system status bar on Lollipop+ versions of the plaform. -->
-    <android.support.design.widget.NavigationView
+    <android.support.design.widget.NavigationTestView
         android:id="@+id/start_drawer"
         android:layout_width="wrap_content"
         android:layout_height="match_parent"
diff --git a/design/tests/res/layout/design_text_input.xml b/design/tests/res/layout/design_text_input.xml
index f4bd1d1..4dba825 100644
--- a/design/tests/res/layout/design_text_input.xml
+++ b/design/tests/res/layout/design_text_input.xml
@@ -58,4 +58,18 @@
         android:layout_width="match_parent"
         android:layout_height="wrap_content"/>
 
+    <android.support.design.widget.TextInputLayout
+        android:id="@+id/textinput_with_text"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content">
+
+        <android.support.design.widget.TextInputEditText
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:imeOptions="flagNoExtractUi"
+            android:hint="@string/textinput_hint"
+            android:text="@string/snackbar_text"/>
+
+    </android.support.design.widget.TextInputLayout>
+
 </LinearLayout>
\ No newline at end of file
diff --git a/design/tests/src/android/support/design/testutils/AppBarLayoutMatchers.java b/design/tests/src/android/support/design/testutils/AppBarLayoutMatchers.java
new file mode 100755
index 0000000..4d6fc63
--- /dev/null
+++ b/design/tests/src/android/support/design/testutils/AppBarLayoutMatchers.java
@@ -0,0 +1,44 @@
+/*
+ * 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.design.testutils;
+
+import android.support.design.widget.AppBarLayout;
+
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
+
+public class AppBarLayoutMatchers {
+
+    /**
+     * Returns a matcher that matches AppBarLayouts which are collapsed.
+     */
+    public static Matcher isCollapsed() {
+        return new TypeSafeMatcher<AppBarLayout>() {
+            @Override
+            public void describeTo(Description description) {
+                description.appendText("AppBarLayout is collapsed");
+            }
+
+            @Override
+            protected boolean matchesSafely(AppBarLayout item) {
+                return item.getBottom() == (item.getHeight() - item.getTotalScrollRange());
+            }
+        };
+    }
+
+}
diff --git a/design/tests/src/android/support/design/testutils/SwipeUtils.java b/design/tests/src/android/support/design/testutils/SwipeUtils.java
new file mode 100644
index 0000000..cf92883
--- /dev/null
+++ b/design/tests/src/android/support/design/testutils/SwipeUtils.java
@@ -0,0 +1,68 @@
+/*
+ * 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.design.testutils;
+
+import android.support.test.espresso.action.CoordinatesProvider;
+import android.support.test.espresso.action.GeneralSwipeAction;
+import android.support.test.espresso.action.Press;
+import android.support.test.espresso.action.Swipe;
+import android.view.View;
+
+public class SwipeUtils {
+
+    public static GeneralSwipeAction swipeUp(final int swipeX,
+            final int swipeStartY, final int swipeAmountY) {
+        return new GeneralSwipeAction(
+                Swipe.SLOW,
+                new CoordinatesProvider() {
+                    @Override
+                    public float[] calculateCoordinates(View view) {
+                        return new float[] { swipeX, swipeStartY };
+                    }
+                },
+                new CoordinatesProvider() {
+                    @Override
+                    public float[] calculateCoordinates(View view) {
+                        return new float[] { swipeX, swipeStartY - swipeAmountY };
+                    }
+                },
+                Press.FINGER
+        );
+    }
+
+    public static GeneralSwipeAction swipeDown(final int swipeX,
+            final int swipeStartY, final int swipeAmountY) {
+        return new GeneralSwipeAction(
+                Swipe.SLOW,
+                new CoordinatesProvider() {
+                    @Override
+                    public float[] calculateCoordinates(View view) {
+                        return new float[] { swipeX, swipeStartY };
+                    }
+                },
+                new CoordinatesProvider() {
+                    @Override
+                    public float[] calculateCoordinates(View view) {
+                        return new float[] { swipeX, swipeStartY + swipeAmountY };
+                    }
+                },
+                Press.FINGER
+        );
+    }
+
+
+}
diff --git a/design/tests/src/android/support/design/testutils/TestUtils.java b/design/tests/src/android/support/design/testutils/TestUtils.java
index b9aae7a..2f9187d 100644
--- a/design/tests/src/android/support/design/testutils/TestUtils.java
+++ b/design/tests/src/android/support/design/testutils/TestUtils.java
@@ -16,7 +16,10 @@
 
 package android.support.design.testutils;
 
+import android.app.Activity;
 import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
 import android.content.res.TypedArray;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
@@ -124,4 +127,20 @@
             }
         }
     }
+
+    /**
+     * Rotates the given Activity to either portrait or landscape, depending on the current
+     * orientation.
+     */
+    public static void rotateOrientation(@NonNull Activity activity) {
+        switch (activity.getResources().getConfiguration().orientation) {
+            case Configuration.ORIENTATION_PORTRAIT:
+                activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
+                break;
+            case Configuration.ORIENTATION_LANDSCAPE:
+            default:
+                activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+                break;
+        }
+    }
 }
\ No newline at end of file
diff --git a/design/tests/src/android/support/design/testutils/TestUtilsMatchers.java b/design/tests/src/android/support/design/testutils/TestUtilsMatchers.java
index 2f1f30d..9d13ce7 100644
--- a/design/tests/src/android/support/design/testutils/TestUtilsMatchers.java
+++ b/design/tests/src/android/support/design/testutils/TestUtilsMatchers.java
@@ -22,6 +22,7 @@
 import android.graphics.Rect;
 import android.graphics.Typeface;
 import android.graphics.drawable.Drawable;
+import android.os.Build;
 import android.support.annotation.ColorInt;
 import android.support.annotation.NonNull;
 import android.support.design.widget.FloatingActionButton;
@@ -461,6 +462,24 @@
     }
 
     /**
+     * Returns a matcher that matches views which have a z-value greater than 0. Also matches if
+     * the platform we're running on does not support z-values.
+     */
+    public static Matcher<View> hasZ() {
+        return new TypeSafeMatcher<View>() {
+            @Override
+            public void describeTo(Description description) {
+                description.appendText("has a z value greater than 0");
+            }
+
+            @Override
+            public boolean matchesSafely(View view) {
+                return Build.VERSION.SDK_INT < 21 || ViewCompat.getZ(view) > 0f;
+            }
+        };
+    }
+
+    /**
      * Returns a matcher that matches TextViews with the specified typeface.
      */
     public static Matcher withTypeface(@NonNull final Typeface typeface) {
diff --git a/design/tests/src/android/support/design/widget/AppBarLayoutBaseTest.java b/design/tests/src/android/support/design/widget/AppBarLayoutBaseTest.java
index fc131a4..6fd4470 100644
--- a/design/tests/src/android/support/design/widget/AppBarLayoutBaseTest.java
+++ b/design/tests/src/android/support/design/widget/AppBarLayoutBaseTest.java
@@ -17,6 +17,8 @@
 package android.support.design.widget;
 
 import static android.support.design.testutils.CollapsingToolbarLayoutActions.setContentScrimColor;
+import static android.support.design.testutils.SwipeUtils.swipeDown;
+import static android.support.design.testutils.SwipeUtils.swipeUp;
 import static android.support.design.testutils.TestUtilsActions.setText;
 import static android.support.design.testutils.TestUtilsActions.setTitle;
 import static android.support.test.espresso.Espresso.onView;
@@ -35,15 +37,10 @@
 import android.support.annotation.StringRes;
 import android.support.design.test.R;
 import android.support.design.testutils.Shakespeare;
-import android.support.test.espresso.action.CoordinatesProvider;
-import android.support.test.espresso.action.GeneralSwipeAction;
-import android.support.test.espresso.action.Press;
-import android.support.test.espresso.action.Swipe;
 import android.support.v4.view.ViewCompat;
 import android.support.v7.app.AppCompatActivity;
 import android.support.v7.widget.Toolbar;
 import android.text.TextUtils;
-import android.view.View;
 import android.widget.TextView;
 
 import org.hamcrest.Description;
@@ -64,38 +61,12 @@
 
     protected static void performVerticalSwipeUpGesture(@IdRes int containerId, final int swipeX,
             final int swipeStartY, final int swipeAmountY) {
-        onView(withId(containerId)).perform(new GeneralSwipeAction(
-                Swipe.SLOW,
-                new CoordinatesProvider() {
-                    @Override
-                    public float[] calculateCoordinates(View view) {
-                        return new float[] { swipeX, swipeStartY };
-                    }
-                },
-                new CoordinatesProvider() {
-                    @Override
-                    public float[] calculateCoordinates(View view) {
-                        return new float[] { swipeX, swipeStartY - swipeAmountY };
-                    }
-                }, Press.FINGER));
+        onView(withId(containerId)).perform(swipeUp(swipeX, swipeStartY, swipeAmountY));
     }
 
     protected static void performVerticalSwipeDownGesture(@IdRes int containerId, final int swipeX,
             final int swipeStartY, final int swipeAmountY) {
-        onView(withId(containerId)).perform(new GeneralSwipeAction(
-                Swipe.SLOW,
-                new CoordinatesProvider() {
-                    @Override
-                    public float[] calculateCoordinates(View view) {
-                        return new float[] { swipeX, swipeStartY };
-                    }
-                },
-                new CoordinatesProvider() {
-                    @Override
-                    public float[] calculateCoordinates(View view) {
-                        return new float[] { swipeX, swipeStartY + swipeAmountY };
-                    }
-                }, Press.FINGER));
+        onView(withId(containerId)).perform(swipeDown(swipeX, swipeStartY, swipeAmountY));
     }
 
     @CallSuper
diff --git a/design/tests/src/android/support/design/widget/AppBarLayoutCollapsePinTestActivity.java b/design/tests/src/android/support/design/widget/AppBarLayoutCollapsePinTestActivity.java
new file mode 100644
index 0000000..38ea4fc
--- /dev/null
+++ b/design/tests/src/android/support/design/widget/AppBarLayoutCollapsePinTestActivity.java
@@ -0,0 +1,34 @@
+/*
+ * 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.design.widget;
+
+import android.support.design.test.R;
+import android.support.v7.widget.Toolbar;
+
+public class AppBarLayoutCollapsePinTestActivity extends BaseTestActivity {
+
+    @Override
+    protected int getContentViewLayoutResId() {
+        return R.layout.design_appbar_toolbar_collapse_pin_restore_test;
+    }
+
+    @Override
+    protected void onContentViewSet() {
+        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+        setSupportActionBar(toolbar);
+    }
+}
diff --git a/design/tests/src/android/support/design/widget/AppBarWithCollapsingToolbarStateRestoreTest.java b/design/tests/src/android/support/design/widget/AppBarWithCollapsingToolbarStateRestoreTest.java
new file mode 100644
index 0000000..52f4ab2
--- /dev/null
+++ b/design/tests/src/android/support/design/widget/AppBarWithCollapsingToolbarStateRestoreTest.java
@@ -0,0 +1,63 @@
+/*
+ * 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.design.widget;
+
+import static android.support.design.testutils.AppBarLayoutMatchers.isCollapsed;
+import static android.support.design.testutils.SwipeUtils.swipeUp;
+import static android.support.design.testutils.TestUtils.rotateOrientation;
+import static android.support.design.testutils.TestUtilsMatchers.hasZ;
+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 android.app.Activity;
+import android.support.design.test.R;
+
+import org.junit.Test;
+
+public class AppBarWithCollapsingToolbarStateRestoreTest
+        extends BaseInstrumentationTestCase<AppBarLayoutCollapsePinTestActivity> {
+
+    public AppBarWithCollapsingToolbarStateRestoreTest() {
+        super(AppBarLayoutCollapsePinTestActivity.class);
+    }
+
+    @Test
+    public void testRotateAndRestore() {
+        Activity activity = mActivityTestRule.getActivity();
+        final AppBarLayout appBar = (AppBarLayout) activity.findViewById(R.id.app_bar);
+
+        // Swipe up and collapse the AppBarLayout
+        onView(withId(R.id.coordinator_layout))
+                .perform(swipeUp(
+                        appBar.getLeft() + (appBar.getWidth() / 2),
+                        appBar.getBottom() + 20,
+                        appBar.getHeight()));
+        onView(withId(R.id.app_bar))
+                .check(matches(hasZ()))
+                .check(matches(isCollapsed()));
+
+        // Now rotate the Activity
+        rotateOrientation(activity);
+
+        // And check that the app bar still is restored correctly
+        onView(withId(R.id.app_bar))
+                .check(matches(hasZ()))
+                .check(matches(isCollapsed()));
+    }
+
+}
diff --git a/design/tests/src/android/support/design/widget/CoordinatorLayoutTest.java b/design/tests/src/android/support/design/widget/CoordinatorLayoutTest.java
index 1d4f0a7..f8e4f99 100644
--- a/design/tests/src/android/support/design/widget/CoordinatorLayoutTest.java
+++ b/design/tests/src/android/support/design/widget/CoordinatorLayoutTest.java
@@ -16,6 +16,7 @@
 
 package android.support.design.widget;
 
+import static android.support.test.InstrumentationRegistry.getInstrumentation;
 import static android.support.test.espresso.Espresso.onView;
 import static android.support.test.espresso.action.ViewActions.swipeUp;
 import static android.support.test.espresso.matcher.ViewMatchers.withId;
@@ -40,8 +41,8 @@
 import android.graphics.Rect;
 import android.support.design.test.R;
 import android.support.design.testutils.CoordinatorLayoutUtils;
+import android.support.design.testutils.CoordinatorLayoutUtils.DependentBehavior;
 import android.support.design.widget.CoordinatorLayout.Behavior;
-import android.support.test.InstrumentationRegistry;
 import android.support.test.filters.MediumTest;
 import android.support.test.filters.SdkSuppress;
 import android.support.v4.view.ViewCompat;
@@ -69,13 +70,13 @@
 
     @Before
     public void setup() {
-        mInstrumentation = InstrumentationRegistry.getInstrumentation();
+        mInstrumentation = getInstrumentation();
     }
 
     @Test
     @SdkSuppress(minSdkVersion = 21)
     public void testSetFitSystemWindows() throws Throwable {
-        final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+        final Instrumentation instrumentation = getInstrumentation();
         final CoordinatorLayout col = mActivityTestRule.getActivity().mCoordinatorLayout;
         final View view = new View(col.getContext());
 
@@ -183,7 +184,7 @@
 
     @Test
     public void testInsetEdge() throws Throwable {
-        final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+        final Instrumentation instrumentation = getInstrumentation();
         final CoordinatorLayout col = mActivityTestRule.getActivity().mCoordinatorLayout;
 
         final View insetView = new View(col.getContext());
@@ -236,7 +237,7 @@
 
     @Test
     public void testDependentViewChanged() throws Throwable {
-        final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+        final Instrumentation instrumentation = getInstrumentation();
         final CoordinatorLayout col = mActivityTestRule.getActivity().mCoordinatorLayout;
 
         // Add two views, A & B, where B depends on A
@@ -250,7 +251,7 @@
         lpB.width = 100;
         lpB.height = 100;
         final CoordinatorLayout.Behavior behavior =
-                spy(new CoordinatorLayoutUtils.DependentBehavior(viewA));
+                spy(new DependentBehavior(viewA));
         lpB.setBehavior(behavior);
 
         mActivityTestRule.runOnUiThread(new Runnable() {
@@ -282,7 +283,7 @@
 
     @Test
     public void testDependentViewRemoved() throws Throwable {
-        final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+        final Instrumentation instrumentation = getInstrumentation();
         final CoordinatorLayout col = mActivityTestRule.getActivity().mCoordinatorLayout;
 
         // Add two views, A & B, where B depends on A
@@ -290,7 +291,7 @@
         final View viewB = new View(col.getContext());
         final CoordinatorLayout.LayoutParams lpB = col.generateDefaultLayoutParams();
         final CoordinatorLayout.Behavior behavior =
-                spy(new CoordinatorLayoutUtils.DependentBehavior(viewA));
+                spy(new DependentBehavior(viewA));
         lpB.setBehavior(behavior);
 
         mActivityTestRule.runOnUiThread(new Runnable() {
@@ -316,7 +317,7 @@
 
     @Test
     public void testGetDependenciesAfterDependentViewRemoved() throws Throwable {
-        final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+        final Instrumentation instrumentation = getInstrumentation();
         final CoordinatorLayout col = mActivityTestRule.getActivity().mCoordinatorLayout;
 
         // Add two views, A & B, where B depends on A
@@ -572,6 +573,55 @@
     }
 
     @Test
+    public void testNestedScrollingTriggeringDependentViewChanged() throws Throwable {
+        final CoordinatorLayoutActivity activity = mActivityTestRule.getActivity();
+        final CoordinatorLayout col = activity.mCoordinatorLayout;
+
+        // First a NestedScrollView to trigger nested scrolling
+        final View scrollView = LayoutInflater.from(activity).inflate(
+                R.layout.include_nestedscrollview, col, false);
+
+        // Now create a View and Behavior which depend on the scrollview
+        final ImageView dependentView = new ImageView(activity);
+        final CoordinatorLayout.Behavior dependentBehavior = spy(new DependentBehavior(scrollView));
+
+        // Finally a view which accepts nested scrolling in the CoordinatorLayout
+        final ImageView nestedScrollAwareView = new ImageView(activity);
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                // First add the ScrollView
+                col.addView(scrollView);
+
+                // Now add the view which depends on the scrollview
+                CoordinatorLayout.LayoutParams clp = new CoordinatorLayout.LayoutParams(200, 200);
+                clp.setBehavior(dependentBehavior);
+                col.addView(dependentView, clp);
+
+                // Now add the nested scrolling aware view
+                clp = new CoordinatorLayout.LayoutParams(200, 200);
+                clp.setBehavior(new NestedScrollingBehavior());
+                col.addView(nestedScrollAwareView, clp);
+            }
+        });
+
+        // Wait for any layouts, and reset the Behavior so that the call counts are 0
+        getInstrumentation().waitForIdleSync();
+        reset(dependentBehavior);
+
+        // Now vertically swipe up on the NSV, causing nested scrolling to occur
+        onView(withId(R.id.nested_scrollview)).perform(swipeUp());
+
+        // Verify that the Behavior's onDependentViewChanged is not called due to the
+        // nested scroll
+        verify(dependentBehavior, never()).onDependentViewChanged(
+                eq(col), // parent
+                eq(dependentView), // child
+                eq(scrollView)); // axes
+    }
+
+    @Test
     public void testDodgeInsetViewWithEmptyBounds() throws Throwable {
         final CoordinatorLayout col = mActivityTestRule.getActivity().mCoordinatorLayout;
 
@@ -614,9 +664,9 @@
                 .getInsetDodgeRect(same(col), same(view), any(Rect.class));
     }
 
-    public static class NestedScrollingBehavior extends CoordinatorLayout.Behavior<ImageView> {
+    public static class NestedScrollingBehavior extends CoordinatorLayout.Behavior<View> {
         @Override
-        public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, ImageView child,
+        public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child,
                 View directTargetChild, View target, int nestedScrollAxes) {
             // Return true so that we always accept nested scroll events
             return true;
diff --git a/design/tests/src/android/support/design/widget/FloatingActionButtonTest.java b/design/tests/src/android/support/design/widget/FloatingActionButtonTest.java
index 069055b..e8cc701 100644
--- a/design/tests/src/android/support/design/widget/FloatingActionButtonTest.java
+++ b/design/tests/src/android/support/design/widget/FloatingActionButtonTest.java
@@ -33,11 +33,15 @@
 import static android.support.design.testutils.TestUtilsMatchers.withFabContentHeight;
 import static android.support.design.widget.DesignViewActions.setVisibility;
 import static android.support.test.espresso.Espresso.onView;
+import static android.support.test.espresso.action.ViewActions.click;
 import static android.support.test.espresso.assertion.ViewAssertions.matches;
 import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
 import static android.support.test.espresso.matcher.ViewMatchers.withId;
 
 import static org.hamcrest.Matchers.not;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 
 import android.app.Activity;
 import android.content.res.ColorStateList;
@@ -229,6 +233,19 @@
     }
 
     @Test
+    public void testOnClickListener() {
+        final View.OnClickListener listener = mock(View.OnClickListener.class);
+        final View view = mActivityTestRule.getActivity().findViewById(R.id.fab_standard);
+        view.setOnClickListener(listener);
+
+        // Click on the fab
+        onView(withId(R.id.fab_standard)).perform(click());
+
+        // And verify that the listener was invoked once
+        verify(listener, times(1)).onClick(view);
+    }
+
+    @Test
     public void testSetCompatElevation() {
         onView(withId(R.id.fab_standard))
                 .perform(setEnabled(false))
diff --git a/design/tests/src/android/support/design/widget/NavigationTestView.java b/design/tests/src/android/support/design/widget/NavigationTestView.java
new file mode 100644
index 0000000..af3d69e
--- /dev/null
+++ b/design/tests/src/android/support/design/widget/NavigationTestView.java
@@ -0,0 +1,50 @@
+/*
+ * 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.design.widget;
+
+import android.content.Context;
+import android.support.v4.view.WindowInsetsCompat;
+import android.util.AttributeSet;
+
+/**
+ * Expose hasSystemWindowInsets() for testing.
+ */
+public class NavigationTestView extends NavigationView {
+
+    boolean mHashSystemWindowInsets;
+
+    public NavigationTestView(Context context) {
+        this(context, null);
+    }
+
+    public NavigationTestView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public NavigationTestView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    @Override
+    protected void onInsetsChanged(WindowInsetsCompat insets) {
+        super.onInsetsChanged(insets);
+        mHashSystemWindowInsets = insets.hasSystemWindowInsets();
+    }
+
+    public boolean hasSystemWindowInsets() {
+        return mHashSystemWindowInsets;
+    }
+}
diff --git a/design/tests/src/android/support/design/widget/NavigationViewTest.java b/design/tests/src/android/support/design/widget/NavigationViewTest.java
index 7031281..f0c064e 100755
--- a/design/tests/src/android/support/design/widget/NavigationViewTest.java
+++ b/design/tests/src/android/support/design/widget/NavigationViewTest.java
@@ -94,7 +94,7 @@
 
     private DrawerLayout mDrawerLayout;
 
-    private NavigationView mNavigationView;
+    private NavigationTestView mNavigationView;
 
     public NavigationViewTest() {
         super(NavigationViewActivity.class);
@@ -104,7 +104,7 @@
     public void setUp() throws Exception {
         final NavigationViewActivity activity = mActivityTestRule.getActivity();
         mDrawerLayout = (DrawerLayout) activity.findViewById(R.id.drawer_layout);
-        mNavigationView = (NavigationView) mDrawerLayout.findViewById(R.id.start_drawer);
+        mNavigationView = (NavigationTestView) mDrawerLayout.findViewById(R.id.start_drawer);
 
         // Close the drawer to reset the state for the next test
         onView(withId(R.id.drawer_layout)).perform(closeDrawer(GravityCompat.START));
@@ -146,7 +146,11 @@
         onView(withId(R.id.drawer_layout)).perform(openDrawer(GravityCompat.START));
 
         if (Build.VERSION.SDK_INT >= 21) {
-            assertFalse(mNavigationView.willNotDraw());
+            if (mNavigationView.hasSystemWindowInsets()) {
+                assertFalse(mNavigationView.willNotDraw());
+            } else {
+                assertTrue(mNavigationView.willNotDraw());
+            }
         } else {
             assertTrue(mNavigationView.willNotDraw());
         }
diff --git a/design/tests/src/android/support/design/widget/TextInputLayoutTest.java b/design/tests/src/android/support/design/widget/TextInputLayoutTest.java
index 30c15b5..2ba1e6e 100755
--- a/design/tests/src/android/support/design/widget/TextInputLayoutTest.java
+++ b/design/tests/src/android/support/design/widget/TextInputLayoutTest.java
@@ -31,6 +31,7 @@
 import static android.support.design.testutils.TextInputLayoutActions.setTypeface;
 import static android.support.design.testutils.TextInputLayoutMatchers
         .hasPasswordToggleContentDescription;
+import static android.support.test.InstrumentationRegistry.getInstrumentation;
 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.typeText;
@@ -38,6 +39,7 @@
 import static android.support.test.espresso.assertion.ViewAssertions.matches;
 import static android.support.test.espresso.contrib.AccessibilityChecks.accessibilityAssertion;
 import static android.support.test.espresso.matcher.ViewMatchers.hasContentDescription;
+import static android.support.test.espresso.matcher.ViewMatchers.hasFocus;
 import static android.support.test.espresso.matcher.ViewMatchers.isChecked;
 import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
 import static android.support.test.espresso.matcher.ViewMatchers.isEnabled;
@@ -66,6 +68,7 @@
 import android.support.v4.widget.TextViewCompat;
 import android.util.AttributeSet;
 import android.util.SparseArray;
+import android.view.KeyEvent;
 import android.view.View;
 import android.view.inputmethod.EditorInfo;
 import android.widget.EditText;
@@ -456,6 +459,27 @@
                 .check(matches(withTextColor(textColor)));
     }
 
+    @Test
+    public void testTextSetViaAttributeCollapsedHint() {
+        onView(withId(R.id.textinput_with_text))
+                .check(isHintExpanded(false));
+    }
+
+    @Test
+    public void testFocusMovesToEditTextWithPasswordEnabled() {
+        // Focus the preceding EditText
+        onView(withId(R.id.textinput_edittext))
+                .perform(click())
+                .check(matches(hasFocus()));
+
+        // Then send a TAB to focus the next view
+        getInstrumentation().sendKeyDownUpSync(KeyEvent.KEYCODE_TAB);
+
+        // And check that the EditText is focused
+        onView(withId(R.id.textinput_edittext_pwd))
+                .check(matches(hasFocus()));
+    }
+
     static ViewAssertion isHintExpanded(final boolean expanded) {
         return new ViewAssertion() {
             @Override
diff --git a/exifinterface/build.gradle b/exifinterface/build.gradle
index 42f50c1..f6fd633 100644
--- a/exifinterface/build.gradle
+++ b/exifinterface/build.gradle
@@ -1,5 +1,4 @@
 apply plugin: 'com.android.library'
-
 archivesBaseName = 'exifinterface'
 
 dependencies {
@@ -33,28 +32,11 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
 
diff --git a/fragment/build.gradle b/fragment/build.gradle
index 26e9f1c..7c9098a 100644
--- a/fragment/build.gradle
+++ b/fragment/build.gradle
@@ -18,9 +18,6 @@
     testCompile 'junit:junit:4.12'
 }
 
-sourceCompatibility = JavaVersion.VERSION_1_7
-targetCompatibility = JavaVersion.VERSION_1_7
-
 android {
     compileSdkVersion project.ext.currentSdk
 
@@ -50,11 +47,6 @@
         sourceCompatibility JavaVersion.VERSION_1_7
         targetCompatibility JavaVersion.VERSION_1_7
     }
-
-    testOptions {
-        unitTests.returnDefaultValues = true
-        compileSdkVersion project.ext.currentSdk
-    }
 }
 
 android.libraryVariants.all { variant ->
@@ -65,22 +57,6 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
@@ -88,7 +64,6 @@
         exclude('android/service/media/**')
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
 
diff --git a/graphics/drawable/animated/build.gradle b/graphics/drawable/animated/build.gradle
index bb110a1..10d112a 100644
--- a/graphics/drawable/animated/build.gradle
+++ b/graphics/drawable/animated/build.gradle
@@ -1,5 +1,4 @@
 apply plugin: 'com.android.library'
-
 archivesBaseName = 'animated-vector-drawable'
 
 dependencies {
@@ -41,14 +40,6 @@
         additionalParameters "--no-version-vectors"
     }
 
-    packagingOptions {
-        exclude 'LICENSE.txt'
-    }
-
-    testOptions {
-        unitTests.returnDefaultValues = true
-    }
-
     buildTypes.all {
         consumerProguardFiles 'proguard-rules.pro'
     }
@@ -62,28 +53,11 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar) {
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
 
diff --git a/graphics/drawable/static/build.gradle b/graphics/drawable/static/build.gradle
index 2c72f9f..fdb306c 100644
--- a/graphics/drawable/static/build.gradle
+++ b/graphics/drawable/static/build.gradle
@@ -42,14 +42,6 @@
     aaptOptions {
         additionalParameters "--no-version-vectors"
     }
-
-    packagingOptions {
-        exclude 'LICENSE.txt'
-    }
-
-    testOptions {
-        unitTests.returnDefaultValues = true
-    }
 }
 
 android.libraryVariants.all { variant ->
@@ -60,28 +52,11 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar) {
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
 
diff --git a/media-compat/build.gradle b/media-compat/build.gradle
index e26a1bc..8dd44bf 100644
--- a/media-compat/build.gradle
+++ b/media-compat/build.gradle
@@ -15,14 +15,13 @@
     androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2'
 }
 
-sourceCompatibility = JavaVersion.VERSION_1_7
-targetCompatibility = JavaVersion.VERSION_1_7
-
 android {
     compileSdkVersion project.ext.currentSdk
 
     defaultConfig {
         minSdkVersion 9
+
+        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
     }
 
     sourceSets {
@@ -38,6 +37,11 @@
                 'java'
         ]
         main.aidl.srcDirs = ['java']
+
+        androidTest.setRoot('tests')
+        androidTest.java.srcDir 'tests/src'
+        androidTest.res.srcDir 'tests/res'
+        androidTest.manifest.srcFile 'tests/AndroidManifest.xml'
     }
 
     compileOptions {
@@ -54,22 +58,6 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
@@ -77,7 +65,6 @@
         exclude('android/service/media/**')
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
 
@@ -113,4 +100,4 @@
             }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/media-compat/java/android/support/v4/media/session/MediaSessionCompat.java b/media-compat/java/android/support/v4/media/session/MediaSessionCompat.java
index 78b0c50..d835985 100644
--- a/media-compat/java/android/support/v4/media/session/MediaSessionCompat.java
+++ b/media-compat/java/android/support/v4/media/session/MediaSessionCompat.java
@@ -2302,7 +2302,9 @@
         public void setCallback(Callback callback, Handler handler) {
             MediaSessionCompatApi21.setCallback(mSessionObj,
                     callback == null ? null : callback.mCallbackObj, handler);
-            callback.mSessionImpl = new WeakReference<MediaSessionImpl>(this);
+            if (callback != null) {
+                callback.mSessionImpl = new WeakReference<MediaSessionImpl>(this);
+            }
         }
 
         @Override
diff --git a/media-compat/tests/AndroidManifest.xml b/media-compat/tests/AndroidManifest.xml
new file mode 100644
index 0000000..00de2db
--- /dev/null
+++ b/media-compat/tests/AndroidManifest.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+   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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          xmlns:tools="http://schemas.android.com/tools"
+          package="android.support.mediacompat.test">
+
+    <uses-sdk
+            android:minSdkVersion="9"
+            android:targetSdkVersion="23"
+            tools:overrideLibrary="android.support.test, android.app, android.support.test.rule,
+                      android.support.test.espresso, android.support.test.espresso.idling"/>
+
+    <application android:supportsRtl="true">
+        <uses-library android:name="android.test.runner"/>
+        <activity android:name="android.support.v4.media.session.TestActivity" />
+    </application>
+
+    <instrumentation
+            android:name="android.test.InstrumentationTestRunner"
+            android:targetPackage="android.support.mediacompat.test"/>
+
+</manifest>
diff --git a/media-compat/tests/NO_DOCS b/media-compat/tests/NO_DOCS
new file mode 100644
index 0000000..092a39c
--- /dev/null
+++ b/media-compat/tests/NO_DOCS
@@ -0,0 +1,17 @@
+# 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.
+
+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.
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
new file mode 100644
index 0000000..455a706
--- /dev/null
+++ b/media-compat/tests/src/android/support/v4/media/session/MediaSessionCompatTest.java
@@ -0,0 +1,99 @@
+/*
+ * 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.v4.media.session;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.Looper;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.rule.ActivityTestRule;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import static junit.framework.Assert.fail;
+
+public class MediaSessionCompatTest {
+    @Rule
+    public ActivityTestRule<TestActivity> mActivityRule =
+            new ActivityTestRule<>(TestActivity.class);
+    Context mContext;
+    Map<String, LockedObject> results = new HashMap<>();
+
+    @Before
+    public void setUp() {
+        mContext = InstrumentationRegistry.getContext();
+    }
+
+    @Test
+    public void testSetNullCallback() throws Throwable {
+        initWait("testSetNullCallback");
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    MediaSessionCompat session = new MediaSessionCompat(mContext, "TEST");
+                    session.setCallback(null);
+                } catch (Exception e) {
+                    fail("Fail with an exception: " + e);
+                } finally {
+                    setResultData("testSetNullCallback", true);
+                }
+            }
+        });
+        waitFor("testSetNullCallback");
+    }
+
+    private void initWait(String key) throws InterruptedException {
+        results.put(key, new LockedObject());
+    }
+
+    private Object[] waitFor(String key) throws InterruptedException {
+        return results.get(key).waitFor();
+    }
+
+    private void setResultData(String key, Object... args) {
+        if (results.containsKey(key)) {
+            results.get(key).set(args);
+        }
+    }
+
+    private class LockedObject {
+        private Semaphore mLock = new Semaphore(1);
+        private volatile Object[] mArgs;
+
+        public LockedObject() {
+            mLock.drainPermits();
+        }
+
+        public void set(Object... args) {
+            mArgs = args;
+            mLock.release(1);
+        }
+
+        public Object[] waitFor() throws InterruptedException {
+            mLock.tryAcquire(1, 2, TimeUnit.SECONDS);
+            return mArgs;
+        }
+    }
+}
diff --git a/media-compat/tests/src/android/support/v4/media/session/TestActivity.java b/media-compat/tests/src/android/support/v4/media/session/TestActivity.java
new file mode 100644
index 0000000..dd56467
--- /dev/null
+++ b/media-compat/tests/src/android/support/v4/media/session/TestActivity.java
@@ -0,0 +1,22 @@
+/*
+ * 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.v4.media.session;
+
+import android.app.Activity;
+
+public class TestActivity extends Activity {
+}
diff --git a/percent/build.gradle b/percent/build.gradle
index c3f386e..b120075 100644
--- a/percent/build.gradle
+++ b/percent/build.gradle
@@ -1,5 +1,4 @@
 apply plugin: 'com.android.library'
-
 archivesBaseName = 'percent'
 
 dependencies {
@@ -53,28 +52,11 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
 
diff --git a/recommendation/build.gradle b/recommendation/build.gradle
index 75d68d1..dadad58 100644
--- a/recommendation/build.gradle
+++ b/recommendation/build.gradle
@@ -1,5 +1,4 @@
 apply plugin: 'com.android.library'
-
 archivesBaseName = 'recommendation'
 
 dependencies {
@@ -41,27 +40,10 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
diff --git a/samples/SupportLeanbackDemos/AndroidManifest.xml b/samples/SupportLeanbackDemos/AndroidManifest.xml
index 1dba568..ea3c1ee 100644
--- a/samples/SupportLeanbackDemos/AndroidManifest.xml
+++ b/samples/SupportLeanbackDemos/AndroidManifest.xml
@@ -43,6 +43,22 @@
             android:theme="@style/Theme.Example.Leanback.Details"
             android:exported="true" />
 
+        <activity android:name="DetailsVideoActivity"
+            android:theme="@style/Theme.Example.Leanback.Details"
+            android:exported="true" />
+
+        <activity android:name="DetailsVideoSupportActivity"
+            android:theme="@style/Theme.Example.Leanback.Details"
+            android:exported="true" />
+
+        <activity android:name="DetailsCustomTitleActivity"
+            android:theme="@style/Theme.Example.Leanback.Details.CustomTitle"
+            android:exported="true" />
+
+        <activity android:name="DetailsCustomTitleSupportActivity"
+            android:theme="@style/Theme.Example.Leanback.Details.CustomTitle"
+            android:exported="true" />
+
         <activity android:name="SearchDetailsActivity"
             android:theme="@style/Theme.Example.Leanback.SearchDetails"
             android:exported="true" />
diff --git a/samples/SupportLeanbackDemos/generatev4.py b/samples/SupportLeanbackDemos/generatev4.py
index d4b3f22..c79f1b1 100755
--- a/samples/SupportLeanbackDemos/generatev4.py
+++ b/samples/SupportLeanbackDemos/generatev4.py
@@ -134,8 +134,6 @@
     line = line.replace('DetailsActivity', 'DetailsSupportActivity')
     line = line.replace('android.app.Activity', 'android.support.v4.app.FragmentActivity')
     line = line.replace('extends Activity', 'extends FragmentActivity')
-    line = line.replace('R.layout.details', 'R.layout.details_support')
-    line = line.replace('R.layout.legacy_details', 'R.layout.legacy_details_support')
     line = line.replace('getFragmentManager()', 'getSupportFragmentManager()')
     line = line.replace('DetailsFragment', 'DetailsSupportFragment')
     line = line.replace('NewDetailsFragment', 'NewDetailsSupportFragment')
@@ -143,26 +141,6 @@
 file.close()
 outfile.close()
 
-file = open('res/layout/details.xml', 'r')
-outfile = open('res/layout/details_support.xml', 'w')
-for line in file:
-    line = replace_xml_head(line, "details")
-    line = line.replace('com.example.android.leanback.NewDetailsFragment', 'com.example.android.leanback.NewDetailsSupportFragment')
-    outfile.write(line)
-file.close()
-outfile.close()
-
-
-file = open('res/layout/legacy_details.xml', 'r')
-outfile = open('res/layout/legacy_details_support.xml', 'w')
-for line in file:
-    line = replace_xml_head(line, "legacy_details")
-    line = line.replace('com.example.android.leanback.DetailsFragment', 'com.example.android.leanback.DetailsSupportFragment')
-    outfile.write(line)
-file.close()
-outfile.close()
-
-
 file = open('src/com/example/android/leanback/SearchDetailsActivity.java', 'r')
 outfile = open('src/com/example/android/leanback/SearchDetailsSupportActivity.java', 'w')
 write_java_head(outfile, "SearchDetailsActivity")
diff --git a/samples/SupportLeanbackDemos/res/layout/custom_title.xml b/samples/SupportLeanbackDemos/res/layout/custom_title.xml
new file mode 100644
index 0000000..4a88021
--- /dev/null
+++ b/samples/SupportLeanbackDemos/res/layout/custom_title.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<com.example.android.leanback.CustomTitleView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/browse_title_group"
+    android:orientation="horizontal" android:layout_width="match_parent"
+    android:layout_height="wrap_content">
+    <TextView
+        android:text="@string/custom_title"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content" />
+
+</com.example.android.leanback.CustomTitleView>
\ No newline at end of file
diff --git a/samples/SupportLeanbackDemos/res/layout/details.xml b/samples/SupportLeanbackDemos/res/layout/details.xml
deleted file mode 100644
index 3159f54..0000000
--- a/samples/SupportLeanbackDemos/res/layout/details.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     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.
--->
-
-<fragment xmlns:android="http://schemas.android.com/apk/res/android"
-    android:name="com.example.android.leanback.NewDetailsFragment"
-    android:id="@+id/details_fragment"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-/>
diff --git a/samples/SupportLeanbackDemos/res/layout/legacy_details.xml b/samples/SupportLeanbackDemos/res/layout/details_activity.xml
similarity index 85%
rename from samples/SupportLeanbackDemos/res/layout/legacy_details.xml
rename to samples/SupportLeanbackDemos/res/layout/details_activity.xml
index 4af4e6a..7fc1400 100644
--- a/samples/SupportLeanbackDemos/res/layout/legacy_details.xml
+++ b/samples/SupportLeanbackDemos/res/layout/details_activity.xml
@@ -15,8 +15,7 @@
      limitations under the License.
 -->
 
-<fragment xmlns:android="http://schemas.android.com/apk/res/android"
-    android:name="com.example.android.leanback.DetailsFragment"
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/details_fragment"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
diff --git a/samples/SupportLeanbackDemos/res/layout/details_support.xml b/samples/SupportLeanbackDemos/res/layout/details_support.xml
deleted file mode 100644
index 103dc25..0000000
--- a/samples/SupportLeanbackDemos/res/layout/details_support.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- This file is auto-generated from details.xml.  DO NOT MODIFY. -->
-
-<!--
-     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.
--->
-
-<fragment xmlns:android="http://schemas.android.com/apk/res/android"
-    android:name="com.example.android.leanback.NewDetailsSupportFragment"
-    android:id="@+id/details_fragment"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-/>
diff --git a/samples/SupportLeanbackDemos/res/layout/legacy_details_support.xml b/samples/SupportLeanbackDemos/res/layout/legacy_details_support.xml
deleted file mode 100644
index 8146f9b..0000000
--- a/samples/SupportLeanbackDemos/res/layout/legacy_details_support.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- This file is auto-generated from legacy_details.xml.  DO NOT MODIFY. -->
-
-<!--
-     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.
--->
-
-<fragment xmlns:android="http://schemas.android.com/apk/res/android"
-    android:name="com.example.android.leanback.DetailsSupportFragment"
-    android:id="@+id/details_fragment"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-/>
diff --git a/samples/SupportLeanbackDemos/res/values/strings.xml b/samples/SupportLeanbackDemos/res/values/strings.xml
index 3774ebf..6de7d7b 100644
--- a/samples/SupportLeanbackDemos/res/values/strings.xml
+++ b/samples/SupportLeanbackDemos/res/values/strings.xml
@@ -29,8 +29,17 @@
     <string name="search_support_description">SearchSupportFragment test</string>
     <string name="details">Details</string>
     <string name="details_description">DetailsFragment test</string>
+    <string name="details_video">Details with Video</string>
+    <string name="details_video_description">DetailsFragment with Video test</string>
+    <string name="details_custom_title">Details with custom title</string>
+    <string name="details_custom_title_description">DetailsFragment with custom title test</string>
     <string name="details_support">Details(support version)</string>
     <string name="details_support_description">DetailsSupportFragment test</string>
+    <string name="details_video_support">Details with Video(support version)</string>
+    <string name="details_video_support_description">DetailsFragment with Video test(support version)</string>
+    <string name="details_custom_title_support">Details with custom title(support version)</string>
+    <string name="details_custom_title_support_description">DetailsSupportFragment with custom title test</string>
+    <string name="custom_title">Custom Title</string>
     <string name="search_details">Search Details</string>
     <string name="search_details_description">Search style DetailsFragment test</string>
     <string name="search_details_support">Search Details(support version)</string>
diff --git a/samples/SupportLeanbackDemos/res/values/themes.xml b/samples/SupportLeanbackDemos/res/values/themes.xml
index 458c5c0..58ae373 100644
--- a/samples/SupportLeanbackDemos/res/values/themes.xml
+++ b/samples/SupportLeanbackDemos/res/values/themes.xml
@@ -23,6 +23,9 @@
     </style>
     <style name="Theme.Example.Leanback.Details" parent="Theme.Leanback.Details">
     </style>
+    <style name="Theme.Example.Leanback.Details.CustomTitle">
+        <item name="browseTitleViewLayout">@layout/custom_title</item>.
+    </style>
     <style name="Theme.Example.Leanback.VerticalGrid" parent="Theme.Leanback.VerticalGrid">
     </style>
     <style name="Theme.Example.Leanback.Rows" parent="Theme.Leanback">
diff --git a/samples/SupportLeanbackDemos/src/com/example/android/leanback/CustomTitleView.java b/samples/SupportLeanbackDemos/src/com/example/android/leanback/CustomTitleView.java
new file mode 100644
index 0000000..3ceb17d
--- /dev/null
+++ b/samples/SupportLeanbackDemos/src/com/example/android/leanback/CustomTitleView.java
@@ -0,0 +1,88 @@
+/*
+ * 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 com.example.android.leanback;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.support.v17.leanback.widget.SearchOrbView;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.support.v17.leanback.widget.TitleViewAdapter;
+
+public class CustomTitleView extends LinearLayout implements TitleViewAdapter.Provider {
+
+    private final TitleViewAdapter mTitleViewAdapter = new TitleViewAdapter() {
+        @Override
+        public View getSearchAffordanceView() {
+            return null;
+        }
+
+        @Override
+        public void setOnSearchClickedListener(View.OnClickListener listener) {
+        }
+
+        @Override
+        public void setAnimationEnabled(boolean enable) {
+        }
+
+        @Override
+        public Drawable getBadgeDrawable() {
+            return null;
+        }
+
+        @Override
+        public SearchOrbView.Colors getSearchAffordanceColors() {
+            return null;
+        }
+
+        @Override
+        public CharSequence getTitle() {
+            return null;
+        }
+
+        @Override
+        public void setBadgeDrawable(Drawable drawable) {
+        }
+
+        @Override
+        public void setSearchAffordanceColors(SearchOrbView.Colors colors) {
+        }
+
+        @Override
+        public void setTitle(CharSequence titleText) {
+        }
+
+        @Override
+        public void updateComponentsVisibility(int flags) {
+        }
+    };
+
+    public CustomTitleView(Context context) {
+        this(context, null);
+    }
+
+    public CustomTitleView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public CustomTitleView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    @Override
+    public TitleViewAdapter getTitleViewAdapter() {
+        return mTitleViewAdapter;
+    }
+}
diff --git a/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsActivity.java b/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsActivity.java
index 43c6910..6e2cbb4 100644
--- a/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsActivity.java
+++ b/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsActivity.java
@@ -26,22 +26,41 @@
                 && !(this instanceof SearchDetailsActivity));
     }
 
+    protected boolean hasBackgroundVideo() {
+        return false;
+    }
+
     /** Called when the activity is first created. */
     @Override
     public void onCreate(Bundle savedInstanceState)
     {
         super.onCreate(savedInstanceState);
         getFragmentManager().enableDebugLogging(true);
-        setContentView(useLegacyFragment() ? R.layout.legacy_details : R.layout.details);
+        setContentView(R.layout.details_activity);
         if (savedInstanceState == null) {
-            // Only pass object to fragment when activity is first time created,
-            // later object is modified and persisted with fragment state.
             if (useLegacyFragment()) {
-                ((DetailsFragment)getFragmentManager().findFragmentById(R.id.details_fragment))
-                    .setItem((PhotoItem) getIntent().getParcelableExtra(EXTRA_ITEM));
+                DetailsFragment fragment = new DetailsFragment();
+                fragment.setItem((PhotoItem) getIntent().getParcelableExtra(EXTRA_ITEM));
+                getFragmentManager().beginTransaction()
+                        .replace(R.id.details_fragment, fragment)
+                        .commit();
             } else {
-                ((NewDetailsFragment)getFragmentManager().findFragmentById(R.id.details_fragment))
-                    .setItem((PhotoItem) getIntent().getParcelableExtra(EXTRA_ITEM));
+                NewDetailsFragment fragment = new NewDetailsFragment();
+                fragment.setItem((PhotoItem) getIntent().getParcelableExtra(EXTRA_ITEM));
+                fragment.setBackgroundVideo(hasBackgroundVideo());
+                getFragmentManager().beginTransaction()
+                        .replace(R.id.details_fragment, fragment)
+                        .commit();
+            }
+        } else {
+            if (useLegacyFragment()) {
+                DetailsFragment fragment = (DetailsFragment) getFragmentManager()
+                        .findFragmentById(R.id.details_fragment);
+                fragment.setItem((PhotoItem) getIntent().getParcelableExtra(EXTRA_ITEM));
+            } else {
+                NewDetailsFragment fragment = (NewDetailsFragment) getFragmentManager()
+                        .findFragmentById(R.id.details_fragment);
+                fragment.setItem((PhotoItem) getIntent().getParcelableExtra(EXTRA_ITEM));
             }
         }
     }
diff --git a/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsCustomTitleActivity.java b/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsCustomTitleActivity.java
new file mode 100644
index 0000000..0796fb7
--- /dev/null
+++ b/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsCustomTitleActivity.java
@@ -0,0 +1,20 @@
+/*
+ * 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 com.example.android.leanback;
+
+/**
+ * Same function as DetailsActivity, using different theme in AndroidManifest.
+ */
+public class DetailsCustomTitleActivity extends DetailsActivity {
+}
diff --git a/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsCustomTitleSupportActivity.java b/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsCustomTitleSupportActivity.java
new file mode 100644
index 0000000..4ad2f7a
--- /dev/null
+++ b/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsCustomTitleSupportActivity.java
@@ -0,0 +1,20 @@
+/*
+ * 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 com.example.android.leanback;
+
+/**
+ * Same function as DetailsSupportActivity, using different theme in AndroidManifest.
+ */
+public class DetailsCustomTitleSupportActivity extends DetailsSupportActivity {
+}
diff --git a/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsFragment.java b/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsFragment.java
index 09d9526..56acc05 100644
--- a/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsFragment.java
+++ b/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsFragment.java
@@ -126,12 +126,7 @@
         ps.addClassPresenter(ListRow.class, new ListRowPresenter());
 
         mRowsAdapter = new ArrayObjectAdapter(ps);
-
-        PhotoItem item = (PhotoItem) (savedInstanceState != null ?
-                savedInstanceState.getParcelable(ITEM) : null);
-        if (item != null) {
-            setItem(item);
-        }
+        updateAdapter();
 
         setOnItemViewClickedListener(new OnItemViewClickedListener() {
             @Override
@@ -179,7 +174,13 @@
 
     public void setItem(PhotoItem photoItem) {
         mPhotoItem = photoItem;
+        updateAdapter();
+    }
 
+    void updateAdapter() {
+        if (mRowsAdapter == null) {
+            return;
+        }
         mRowsAdapter.clear();
         new Handler().postDelayed(new Runnable() {
             public void run() {
diff --git a/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsSupportActivity.java b/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsSupportActivity.java
index 3ebf101..48d15e5 100644
--- a/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsSupportActivity.java
+++ b/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsSupportActivity.java
@@ -29,22 +29,41 @@
                 && !(this instanceof SearchDetailsSupportActivity));
     }
 
+    protected boolean hasBackgroundVideo() {
+        return false;
+    }
+
     /** Called when the activity is first created. */
     @Override
     public void onCreate(Bundle savedInstanceState)
     {
         super.onCreate(savedInstanceState);
         getSupportFragmentManager().enableDebugLogging(true);
-        setContentView(useLegacyFragment() ? R.layout.legacy_details_support : R.layout.details_support);
+        setContentView(R.layout.details_activity);
         if (savedInstanceState == null) {
-            // Only pass object to fragment when activity is first time created,
-            // later object is modified and persisted with fragment state.
             if (useLegacyFragment()) {
-                ((DetailsSupportFragment)getSupportFragmentManager().findFragmentById(R.id.details_fragment))
-                    .setItem((PhotoItem) getIntent().getParcelableExtra(EXTRA_ITEM));
+                DetailsSupportFragment fragment = new DetailsSupportFragment();
+                fragment.setItem((PhotoItem) getIntent().getParcelableExtra(EXTRA_ITEM));
+                getSupportFragmentManager().beginTransaction()
+                        .replace(R.id.details_fragment, fragment)
+                        .commit();
             } else {
-                ((NewDetailsSupportFragment)getSupportFragmentManager().findFragmentById(R.id.details_fragment))
-                    .setItem((PhotoItem) getIntent().getParcelableExtra(EXTRA_ITEM));
+                NewDetailsSupportFragment fragment = new NewDetailsSupportFragment();
+                fragment.setItem((PhotoItem) getIntent().getParcelableExtra(EXTRA_ITEM));
+                fragment.setBackgroundVideo(hasBackgroundVideo());
+                getSupportFragmentManager().beginTransaction()
+                        .replace(R.id.details_fragment, fragment)
+                        .commit();
+            }
+        } else {
+            if (useLegacyFragment()) {
+                DetailsSupportFragment fragment = (DetailsSupportFragment) getSupportFragmentManager()
+                        .findFragmentById(R.id.details_fragment);
+                fragment.setItem((PhotoItem) getIntent().getParcelableExtra(EXTRA_ITEM));
+            } else {
+                NewDetailsSupportFragment fragment = (NewDetailsSupportFragment) getSupportFragmentManager()
+                        .findFragmentById(R.id.details_fragment);
+                fragment.setItem((PhotoItem) getIntent().getParcelableExtra(EXTRA_ITEM));
             }
         }
     }
diff --git a/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsSupportFragment.java b/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsSupportFragment.java
index 28a61d9..e58e2e7 100644
--- a/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsSupportFragment.java
+++ b/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsSupportFragment.java
@@ -129,12 +129,7 @@
         ps.addClassPresenter(ListRow.class, new ListRowPresenter());
 
         mRowsAdapter = new ArrayObjectAdapter(ps);
-
-        PhotoItem item = (PhotoItem) (savedInstanceState != null ?
-                savedInstanceState.getParcelable(ITEM) : null);
-        if (item != null) {
-            setItem(item);
-        }
+        updateAdapter();
 
         setOnItemViewClickedListener(new OnItemViewClickedListener() {
             @Override
@@ -182,7 +177,13 @@
 
     public void setItem(PhotoItem photoItem) {
         mPhotoItem = photoItem;
+        updateAdapter();
+    }
 
+    void updateAdapter() {
+        if (mRowsAdapter == null) {
+            return;
+        }
         mRowsAdapter.clear();
         new Handler().postDelayed(new Runnable() {
             public void run() {
diff --git a/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsVideoActivity.java b/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsVideoActivity.java
new file mode 100644
index 0000000..06b65b7
--- /dev/null
+++ b/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsVideoActivity.java
@@ -0,0 +1,23 @@
+/*
+ * 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 com.example.android.leanback;
+
+public class DetailsVideoActivity extends DetailsActivity {
+
+    @Override
+    protected boolean hasBackgroundVideo() {
+        return true;
+    }
+
+}
diff --git a/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsVideoSupportActivity.java b/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsVideoSupportActivity.java
new file mode 100644
index 0000000..0c31d36
--- /dev/null
+++ b/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsVideoSupportActivity.java
@@ -0,0 +1,23 @@
+/*
+ * 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 com.example.android.leanback;
+
+public class DetailsVideoSupportActivity extends DetailsSupportActivity {
+
+    @Override
+    protected boolean hasBackgroundVideo() {
+        return true;
+    }
+
+}
diff --git a/samples/SupportLeanbackDemos/src/com/example/android/leanback/MainActivity.java b/samples/SupportLeanbackDemos/src/com/example/android/leanback/MainActivity.java
index 83e56d6..fcd8e65 100644
--- a/samples/SupportLeanbackDemos/src/com/example/android/leanback/MainActivity.java
+++ b/samples/SupportLeanbackDemos/src/com/example/android/leanback/MainActivity.java
@@ -61,12 +61,35 @@
                     R.string.browse_support_description);
             addAction(actions, SearchActivity.class, R.string.search, R.string.search_description);
             addAction(actions, SearchSupportActivity.class, R.string.search_support, R.string.search_support_description);
-            addAction(actions, DetailsActivity.class, R.string.details, R.string.details_description);
+
+            addAction(actions, DetailsActivity.class, R.string.details,
+                    R.string.details_description);
             actions.get(actions.size()-1).getIntent().putExtra(DetailsActivity.EXTRA_ITEM,
                     new PhotoItem("Hello world", R.drawable.gallery_photo_1));
-            addAction(actions, DetailsSupportActivity.class, R.string.details_support, R.string.details_support_description);
+            addAction(actions, DetailsSupportActivity.class, R.string.details_support,
+                    R.string.details_support_description);
             actions.get(actions.size()-1).getIntent().putExtra(DetailsSupportActivity.EXTRA_ITEM,
                     new PhotoItem("Hello world", R.drawable.gallery_photo_1));
+
+            addAction(actions, DetailsVideoActivity.class, R.string.details_video,
+                    R.string.details_video_description);
+            actions.get(actions.size()-1).getIntent().putExtra(DetailsActivity.EXTRA_ITEM,
+                    new PhotoItem("Hello world", R.drawable.gallery_photo_1));
+            addAction(actions, DetailsVideoSupportActivity.class, R.string.details_video_support,
+                    R.string.details_video_support_description);
+            actions.get(actions.size()-1).getIntent().putExtra(DetailsSupportActivity.EXTRA_ITEM,
+                    new PhotoItem("Hello world", R.drawable.gallery_photo_1));
+
+            addAction(actions, DetailsCustomTitleActivity.class, R.string.details_custom_title,
+                    R.string.details_custom_title_description);
+            actions.get(actions.size()-1).getIntent().putExtra(DetailsActivity.EXTRA_ITEM,
+                    new PhotoItem("Hello world", R.drawable.gallery_photo_1));
+            addAction(actions, DetailsCustomTitleSupportActivity.class,
+                    R.string.details_custom_title_support,
+                    R.string.details_custom_title_support_description);
+            actions.get(actions.size()-1).getIntent().putExtra(DetailsSupportActivity.EXTRA_ITEM,
+                    new PhotoItem("Hello world", R.drawable.gallery_photo_1));
+
             addAction(actions, SearchDetailsActivity.class, R.string.search_details,
                     R.string.search_details_description);
             actions.get(actions.size()-1).getIntent().putExtra(SearchDetailsActivity.EXTRA_ITEM,
diff --git a/samples/SupportLeanbackDemos/src/com/example/android/leanback/NewDetailsFragment.java b/samples/SupportLeanbackDemos/src/com/example/android/leanback/NewDetailsFragment.java
index 75fa941..395d557 100644
--- a/samples/SupportLeanbackDemos/src/com/example/android/leanback/NewDetailsFragment.java
+++ b/samples/SupportLeanbackDemos/src/com/example/android/leanback/NewDetailsFragment.java
@@ -20,7 +20,6 @@
 import android.os.Handler;
 import android.support.v17.leanback.app.DetailsBackgroundParallaxHelper;
 import android.support.v17.leanback.app.DetailsFragmentVideoHelper;
-import android.support.v17.leanback.app.VideoFragment;
 import android.support.v17.leanback.media.MediaPlayerGlue;
 import android.support.v17.leanback.widget.Action;
 import android.support.v17.leanback.widget.ArrayObjectAdapter;
@@ -63,6 +62,7 @@
     private boolean TEST_OVERVIEW_ROW_ON_SECOND;
     private boolean TEST_SHARED_ELEMENT_TRANSITION;
     private boolean TEST_ENTRANCE_TRANSITION;
+    private boolean TEST_BACKGROUND_PLAYER;
 
     private static final long TIME_TO_LOAD_OVERVIEW_ROW_MS = 1000;
     private static final long TIME_TO_LOAD_RELATED_ROWS_MS = 2000;
@@ -77,7 +77,6 @@
     private BackgroundHelper mBackgroundHelper;
     private int mBitmapMinVerticalOffset = -100;
     private MediaPlayerGlue mMediaPlayerGlue;
-    private VideoFragment mVideoFragment;
 
     private void initializeTest() {
         TEST_SHARED_ELEMENT_TRANSITION = null != getActivity().getWindow()
@@ -97,27 +96,32 @@
                 getActivity(), getParallaxManager())
                 .setCoverImageMinVerticalOffset(mBitmapMinVerticalOffset)
                 .build();
-        mMediaPlayerGlue = new MediaPlayerGlue(getActivity());
-        mMediaPlayerGlue.setHost(createPlaybackGlueHost());
-        mVideoHelper = new DetailsFragmentVideoHelper(mMediaPlayerGlue, getParallaxManager());
-        mVideoHelper.setBackgroundDrawable(mParallaxHelper.getCoverImageDrawable());
+        if (TEST_BACKGROUND_PLAYER) {
+            mMediaPlayerGlue = new MediaPlayerGlue(getActivity());
+            mMediaPlayerGlue.setHost(createPlaybackGlueHost());
+            mVideoHelper = new DetailsFragmentVideoHelper(mMediaPlayerGlue, getParallaxManager());
+            mVideoHelper.setBackgroundDrawable(mParallaxHelper.getCoverImageDrawable());
 
-        mMediaPlayerGlue.setMode(MediaPlayerGlue.REPEAT_ALL);
-        mMediaPlayerGlue.setArtist("A Googleer");
-        mMediaPlayerGlue.setTitle("Diving with Sharks");
-        mMediaPlayerGlue.setVideoUrl("http://techslides.com/demos/sample-videos/small.mp4");
+            mMediaPlayerGlue.setMode(MediaPlayerGlue.REPEAT_ALL);
+            mMediaPlayerGlue.setArtist("A Googleer");
+            mMediaPlayerGlue.setTitle("Diving with Sharks");
+            mMediaPlayerGlue.setVideoUrl("http://techslides.com/demos/sample-videos/small.mp4");
+
+        }
 
         final Context context = getActivity();
         setBadgeDrawable(ResourcesCompat.getDrawable(context.getResources(), R.drawable.ic_title,
                 context.getTheme()));
         setTitle("Leanback Sample App");
-        setOnSearchClickedListener(new View.OnClickListener() {
-            @Override
-            public void onClick(View view) {
-                Intent intent = new Intent(getActivity(), SearchActivity.class);
-                startActivity(intent);
-            }
-        });
+        if (!TEST_BACKGROUND_PLAYER) {
+            setOnSearchClickedListener(new View.OnClickListener() {
+                @Override
+                public void onClick(View view) {
+                    Intent intent = new Intent(getActivity(), SearchActivity.class);
+                    startActivity(intent);
+                }
+            });
+        }
 
         mActionPlay = new Action(ACTION_PLAY, "Play");
         mActionRent = new Action(ACTION_RENT, "Rent", "$3.99", ResourcesCompat.getDrawable(
@@ -165,12 +169,7 @@
         ps.addClassPresenter(ListRow.class, new ListRowPresenter());
 
         mRowsAdapter = new ArrayObjectAdapter(ps);
-
-        PhotoItem item = (PhotoItem) (savedInstanceState != null ?
-                savedInstanceState.getParcelable(ITEM) : null);
-        if (item != null) {
-            setItem(item);
-        }
+        updateAdapter();
 
         setOnItemViewClickedListener(new OnItemViewClickedListener() {
             @Override
@@ -223,9 +222,19 @@
         return view;
     }
 
+    public void setBackgroundVideo(boolean backgroundVideo) {
+        TEST_BACKGROUND_PLAYER = backgroundVideo;
+    }
+
     public void setItem(PhotoItem photoItem) {
         mPhotoItem = photoItem;
+        updateAdapter();
+    }
 
+    void updateAdapter() {
+        if (mRowsAdapter == null) {
+            return;
+        }
         mRowsAdapter.clear();
         new Handler().postDelayed(new Runnable() {
             public void run() {
@@ -295,6 +304,8 @@
     @Override
     public void onStop() {
         super.onStop();
-        mMediaPlayerGlue.pause();
+        if (TEST_BACKGROUND_PLAYER) {
+            mMediaPlayerGlue.pause();
+        }
     }
 }
diff --git a/samples/SupportLeanbackDemos/src/com/example/android/leanback/NewDetailsSupportFragment.java b/samples/SupportLeanbackDemos/src/com/example/android/leanback/NewDetailsSupportFragment.java
index 11d8b7e..ba482b1 100644
--- a/samples/SupportLeanbackDemos/src/com/example/android/leanback/NewDetailsSupportFragment.java
+++ b/samples/SupportLeanbackDemos/src/com/example/android/leanback/NewDetailsSupportFragment.java
@@ -23,7 +23,6 @@
 import android.os.Handler;
 import android.support.v17.leanback.app.DetailsBackgroundParallaxHelper;
 import android.support.v17.leanback.app.DetailsFragmentVideoHelper;
-import android.support.v17.leanback.app.VideoSupportFragment;
 import android.support.v17.leanback.media.MediaPlayerGlue;
 import android.support.v17.leanback.widget.Action;
 import android.support.v17.leanback.widget.ArrayObjectAdapter;
@@ -66,6 +65,7 @@
     private boolean TEST_OVERVIEW_ROW_ON_SECOND;
     private boolean TEST_SHARED_ELEMENT_TRANSITION;
     private boolean TEST_ENTRANCE_TRANSITION;
+    private boolean TEST_BACKGROUND_PLAYER;
 
     private static final long TIME_TO_LOAD_OVERVIEW_ROW_MS = 1000;
     private static final long TIME_TO_LOAD_RELATED_ROWS_MS = 2000;
@@ -80,7 +80,6 @@
     private BackgroundHelper mBackgroundHelper;
     private int mBitmapMinVerticalOffset = -100;
     private MediaPlayerGlue mMediaPlayerGlue;
-    private VideoSupportFragment mVideoSupportFragment;
 
     private void initializeTest() {
         TEST_SHARED_ELEMENT_TRANSITION = null != getActivity().getWindow()
@@ -100,27 +99,32 @@
                 getActivity(), getParallaxManager())
                 .setCoverImageMinVerticalOffset(mBitmapMinVerticalOffset)
                 .build();
-        mMediaPlayerGlue = new MediaPlayerGlue(getActivity());
-        mMediaPlayerGlue.setHost(createPlaybackGlueHost());
-        mVideoHelper = new DetailsFragmentVideoHelper(mMediaPlayerGlue, getParallaxManager());
-        mVideoHelper.setBackgroundDrawable(mParallaxHelper.getCoverImageDrawable());
+        if (TEST_BACKGROUND_PLAYER) {
+            mMediaPlayerGlue = new MediaPlayerGlue(getActivity());
+            mMediaPlayerGlue.setHost(createPlaybackGlueHost());
+            mVideoHelper = new DetailsFragmentVideoHelper(mMediaPlayerGlue, getParallaxManager());
+            mVideoHelper.setBackgroundDrawable(mParallaxHelper.getCoverImageDrawable());
 
-        mMediaPlayerGlue.setMode(MediaPlayerGlue.REPEAT_ALL);
-        mMediaPlayerGlue.setArtist("A Googleer");
-        mMediaPlayerGlue.setTitle("Diving with Sharks");
-        mMediaPlayerGlue.setVideoUrl("http://techslides.com/demos/sample-videos/small.mp4");
+            mMediaPlayerGlue.setMode(MediaPlayerGlue.REPEAT_ALL);
+            mMediaPlayerGlue.setArtist("A Googleer");
+            mMediaPlayerGlue.setTitle("Diving with Sharks");
+            mMediaPlayerGlue.setVideoUrl("http://techslides.com/demos/sample-videos/small.mp4");
+
+        }
 
         final Context context = getActivity();
         setBadgeDrawable(ResourcesCompat.getDrawable(context.getResources(), R.drawable.ic_title,
                 context.getTheme()));
         setTitle("Leanback Sample App");
-        setOnSearchClickedListener(new View.OnClickListener() {
-            @Override
-            public void onClick(View view) {
-                Intent intent = new Intent(getActivity(), SearchSupportActivity.class);
-                startActivity(intent);
-            }
-        });
+        if (!TEST_BACKGROUND_PLAYER) {
+            setOnSearchClickedListener(new View.OnClickListener() {
+                @Override
+                public void onClick(View view) {
+                    Intent intent = new Intent(getActivity(), SearchSupportActivity.class);
+                    startActivity(intent);
+                }
+            });
+        }
 
         mActionPlay = new Action(ACTION_PLAY, "Play");
         mActionRent = new Action(ACTION_RENT, "Rent", "$3.99", ResourcesCompat.getDrawable(
@@ -168,12 +172,7 @@
         ps.addClassPresenter(ListRow.class, new ListRowPresenter());
 
         mRowsAdapter = new ArrayObjectAdapter(ps);
-
-        PhotoItem item = (PhotoItem) (savedInstanceState != null ?
-                savedInstanceState.getParcelable(ITEM) : null);
-        if (item != null) {
-            setItem(item);
-        }
+        updateAdapter();
 
         setOnItemViewClickedListener(new OnItemViewClickedListener() {
             @Override
@@ -226,9 +225,19 @@
         return view;
     }
 
+    public void setBackgroundVideo(boolean backgroundVideo) {
+        TEST_BACKGROUND_PLAYER = backgroundVideo;
+    }
+
     public void setItem(PhotoItem photoItem) {
         mPhotoItem = photoItem;
+        updateAdapter();
+    }
 
+    void updateAdapter() {
+        if (mRowsAdapter == null) {
+            return;
+        }
         mRowsAdapter.clear();
         new Handler().postDelayed(new Runnable() {
             public void run() {
@@ -298,6 +307,8 @@
     @Override
     public void onStop() {
         super.onStop();
-        mMediaPlayerGlue.pause();
+        if (TEST_BACKGROUND_PLAYER) {
+            mMediaPlayerGlue.pause();
+        }
     }
 }
diff --git a/samples/SupportLeanbackDemos/src/com/example/android/leanback/PlaybackControlHelper.java b/samples/SupportLeanbackDemos/src/com/example/android/leanback/PlaybackControlHelper.java
index df6995e..a6752eb 100644
--- a/samples/SupportLeanbackDemos/src/com/example/android/leanback/PlaybackControlHelper.java
+++ b/samples/SupportLeanbackDemos/src/com/example/android/leanback/PlaybackControlHelper.java
@@ -55,6 +55,9 @@
     private PlaybackControlsRow.PictureInPictureAction mPipAction;
 
     private Handler mHandler = new Handler();
+    // simulating whether the media is yet prepared and ready to play
+    private boolean mInitialized = true;
+
     private final Runnable mUpdateProgressRunnable = new Runnable() {
         @Override
         public void run() {
@@ -159,7 +162,7 @@
 
     @Override
     public boolean hasValidMedia() {
-        return true;
+        return mInitialized;
     }
 
     @Override
@@ -179,7 +182,7 @@
 
     @Override
     public int getMediaDuration() {
-        return FAUX_DURATION;
+        return mInitialized ? FAUX_DURATION : 0;
     }
 
     @Override
@@ -280,4 +283,16 @@
             mUpdateProgressRunnable.run();
         }
     }
+
+    public boolean isInitialized() {
+        return mInitialized;
+    }
+
+    public void setInitialized(boolean initialized) {
+        if (mInitialized != initialized) {
+            mInitialized = initialized;
+            onMetadataChanged();
+            onStateChanged();
+        }
+    }
 };
diff --git a/samples/SupportLeanbackDemos/src/com/example/android/leanback/PlaybackControlSupportHelper.java b/samples/SupportLeanbackDemos/src/com/example/android/leanback/PlaybackControlSupportHelper.java
index e7afb27..0425e60 100644
--- a/samples/SupportLeanbackDemos/src/com/example/android/leanback/PlaybackControlSupportHelper.java
+++ b/samples/SupportLeanbackDemos/src/com/example/android/leanback/PlaybackControlSupportHelper.java
@@ -58,6 +58,9 @@
     private PlaybackControlsRow.PictureInPictureAction mPipAction;
 
     private Handler mHandler = new Handler();
+    // simulating whether the media is yet prepared and ready to play
+    private boolean mInitialized = true;
+
     private final Runnable mUpdateProgressRunnable = new Runnable() {
         @Override
         public void run() {
@@ -162,7 +165,7 @@
 
     @Override
     public boolean hasValidMedia() {
-        return true;
+        return mInitialized;
     }
 
     @Override
@@ -182,7 +185,7 @@
 
     @Override
     public int getMediaDuration() {
-        return FAUX_DURATION;
+        return mInitialized ? FAUX_DURATION : 0;
     }
 
     @Override
@@ -283,4 +286,16 @@
             mUpdateProgressRunnable.run();
         }
     }
+
+    public boolean isInitialized() {
+        return mInitialized;
+    }
+
+    public void setInitialized(boolean initialized) {
+        if (mInitialized != initialized) {
+            mInitialized = initialized;
+            onMetadataChanged();
+            onStateChanged();
+        }
+    }
 };
diff --git a/samples/SupportLeanbackDemos/src/com/example/android/leanback/PlaybackOverlayFragment.java b/samples/SupportLeanbackDemos/src/com/example/android/leanback/PlaybackOverlayFragment.java
index 1531d14..bca057f 100644
--- a/samples/SupportLeanbackDemos/src/com/example/android/leanback/PlaybackOverlayFragment.java
+++ b/samples/SupportLeanbackDemos/src/com/example/android/leanback/PlaybackOverlayFragment.java
@@ -15,6 +15,7 @@
 
 import android.content.Context;
 import android.os.Bundle;
+import android.os.Handler;
 import android.support.v17.leanback.widget.Action;
 import android.support.v17.leanback.widget.ArrayObjectAdapter;
 import android.support.v17.leanback.widget.HeaderItem;
@@ -56,6 +57,11 @@
     private PlaybackControlHelper mGlue;
     private PlaybackControlsRowPresenter mPlaybackControlsRowPresenter;
     private ListRowPresenter mListRowPresenter;
+    final Handler mHandler = new Handler();
+
+    // Artificial delay to simulate a media being prepared. The onRowChanged callback should be
+    // called and the playback row UI should be updated after this delay.
+    private static final int MEDIA_PREPARATION_DELAY = 500;
 
     private OnItemViewClickedListener mOnItemViewClickedListener = new OnItemViewClickedListener() {
         @Override
@@ -124,6 +130,13 @@
             }
         };
 
+        mGlue.setInitialized(false);
+        mHandler.postDelayed(new Runnable() {
+            @Override
+            public void run() {
+                mGlue.setInitialized(true);
+            }
+        }, MEDIA_PREPARATION_DELAY);
         mGlue.setOnItemViewClickedListener(mOnItemViewClickedListener);
 
         mPlaybackControlsRowPresenter = mGlue.createControlsRowAndPresenter();
diff --git a/samples/SupportLeanbackDemos/src/com/example/android/leanback/PlaybackOverlaySupportFragment.java b/samples/SupportLeanbackDemos/src/com/example/android/leanback/PlaybackOverlaySupportFragment.java
index 7a33493..b806a9a 100644
--- a/samples/SupportLeanbackDemos/src/com/example/android/leanback/PlaybackOverlaySupportFragment.java
+++ b/samples/SupportLeanbackDemos/src/com/example/android/leanback/PlaybackOverlaySupportFragment.java
@@ -18,6 +18,7 @@
 
 import android.content.Context;
 import android.os.Bundle;
+import android.os.Handler;
 import android.support.v17.leanback.widget.Action;
 import android.support.v17.leanback.widget.ArrayObjectAdapter;
 import android.support.v17.leanback.widget.HeaderItem;
@@ -59,6 +60,11 @@
     private PlaybackControlSupportHelper mGlue;
     private PlaybackControlsRowPresenter mPlaybackControlsRowPresenter;
     private ListRowPresenter mListRowPresenter;
+    final Handler mHandler = new Handler();
+
+    // Artificial delay to simulate a media being prepared. The onRowChanged callback should be
+    // called and the playback row UI should be updated after this delay.
+    private static final int MEDIA_PREPARATION_DELAY = 500;
 
     private OnItemViewClickedListener mOnItemViewClickedListener = new OnItemViewClickedListener() {
         @Override
@@ -127,6 +133,13 @@
             }
         };
 
+        mGlue.setInitialized(false);
+        mHandler.postDelayed(new Runnable() {
+            @Override
+            public void run() {
+                mGlue.setInitialized(true);
+            }
+        }, MEDIA_PREPARATION_DELAY);
         mGlue.setOnItemViewClickedListener(mOnItemViewClickedListener);
 
         mPlaybackControlsRowPresenter = mGlue.createControlsRowAndPresenter();
diff --git a/transition/build.gradle b/transition/build.gradle
index 05675f9..2f47f83 100644
--- a/transition/build.gradle
+++ b/transition/build.gradle
@@ -61,28 +61,11 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
 
diff --git a/v13/build.gradle b/v13/build.gradle
index 85aa8a8..fb1c25d 100644
--- a/v13/build.gradle
+++ b/v13/build.gradle
@@ -55,28 +55,11 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
 
diff --git a/v14/preference/build.gradle b/v14/preference/build.gradle
index a7e63d5..5583e93 100644
--- a/v14/preference/build.gradle
+++ b/v14/preference/build.gradle
@@ -14,10 +14,7 @@
  * limitations under the License
  */
 
-
-
 apply plugin: 'com.android.library'
-
 archivesBaseName = 'preference-v14'
 
 dependencies {
@@ -62,28 +59,11 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
 
diff --git a/v17/leanback/build.gradle b/v17/leanback/build.gradle
index f5befa7..9ed65a8 100644
--- a/v17/leanback/build.gradle
+++ b/v17/leanback/build.gradle
@@ -64,28 +64,11 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
 
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java b/v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java
index 214ae64..fcb3fa9 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java
@@ -191,6 +191,7 @@
             getChildFragmentManager().beginTransaction()
                     .replace(R.id.details_rows_dock, mRowsFragment).commit();
         }
+        installTitleView(inflater, mRootView, savedInstanceState);
         mRowsFragment.setAdapter(mAdapter);
         mRowsFragment.setOnItemViewSelectedListener(mOnItemViewSelectedListener);
         mRowsFragment.setOnItemViewClickedListener(mOnItemViewClickedListener);
@@ -378,8 +379,11 @@
 
     void onRowSelected(int selectedPosition, int selectedSubPosition) {
         ObjectAdapter adapter = getAdapter();
-        if (adapter == null || adapter.size() == 0
-                || (selectedPosition == 0 && selectedSubPosition == 0)) {
+        if (( mRowsFragment != null && mRowsFragment.getView() != null
+                && mRowsFragment.getView().hasFocus())
+                && (adapter == null || adapter.size() == 0
+                || (getVerticalGridView().getSelectedPosition() == 0
+                && getVerticalGridView().getSelectedSubPosition() == 0))) {
             showTitle(true);
         } else {
             showTitle(false);
@@ -548,20 +552,31 @@
         mRootView.setOnFocusSearchListener(new BrowseFrameLayout.OnFocusSearchListener() {
             @Override
             public View onFocusSearch(View focused, int direction) {
-                if (mVideoFragment == null) {
-                    return null;
-                }
                 if (mRowsFragment.getVerticalGridView() != null
                         && mRowsFragment.getVerticalGridView().hasFocus()) {
                     if (direction == View.FOCUS_UP) {
-                        slideOutGridView();
-                        return mVideoFragment.getView();
+                        if (mVideoFragment != null && mVideoFragment.getView() != null) {
+                            slideOutGridView();
+                            showTitle(false);
+                            return mVideoFragment.getView();
+                        } else if (getTitleView() != null) {
+                            return getTitleView();
+                        }
                     }
-                } else if (mVideoFragment.getView() != null
+                } else if (mVideoFragment != null && mVideoFragment.getView() != null
                         && mVideoFragment.getView().hasFocus()) {
                     if (direction == View.FOCUS_DOWN) {
-                        slideInGridView();
-                        return mRowsFragment.getVerticalGridView();
+                        if (mRowsFragment.getVerticalGridView() != null) {
+                            showTitle(true);
+                            slideInGridView();
+                            return mRowsFragment.getVerticalGridView();
+                        }
+                    }
+                } else if (getTitleView() != null && getTitleView().hasFocus()) {
+                    if (direction == View.FOCUS_DOWN) {
+                        if (mRowsFragment.getVerticalGridView() != null) {
+                            return mRowsFragment.getVerticalGridView();
+                        }
                     }
                 }
                 return focused;
@@ -579,6 +594,7 @@
                 if (mVideoFragment != null && mVideoFragment.getView() != null
                         && mVideoFragment.getView().hasFocus()) {
                     if (keyCode == KeyEvent.KEYCODE_BACK) {
+                        showTitle(true);
                         slideInGridView();
                         getVerticalGridView().requestFocus();
                         return true;
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragment.java b/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragment.java
index a969f1c..a9bbf28 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragment.java
@@ -194,6 +194,7 @@
             getChildFragmentManager().beginTransaction()
                     .replace(R.id.details_rows_dock, mRowsSupportFragment).commit();
         }
+        installTitleView(inflater, mRootView, savedInstanceState);
         mRowsSupportFragment.setAdapter(mAdapter);
         mRowsSupportFragment.setOnItemViewSelectedListener(mOnItemViewSelectedListener);
         mRowsSupportFragment.setOnItemViewClickedListener(mOnItemViewClickedListener);
@@ -381,8 +382,11 @@
 
     void onRowSelected(int selectedPosition, int selectedSubPosition) {
         ObjectAdapter adapter = getAdapter();
-        if (adapter == null || adapter.size() == 0
-                || (selectedPosition == 0 && selectedSubPosition == 0)) {
+        if (( mRowsSupportFragment != null && mRowsSupportFragment.getView() != null
+                && mRowsSupportFragment.getView().hasFocus())
+                && (adapter == null || adapter.size() == 0
+                || (getVerticalGridView().getSelectedPosition() == 0
+                && getVerticalGridView().getSelectedSubPosition() == 0))) {
             showTitle(true);
         } else {
             showTitle(false);
@@ -551,20 +555,31 @@
         mRootView.setOnFocusSearchListener(new BrowseFrameLayout.OnFocusSearchListener() {
             @Override
             public View onFocusSearch(View focused, int direction) {
-                if (mVideoSupportFragment == null) {
-                    return null;
-                }
                 if (mRowsSupportFragment.getVerticalGridView() != null
                         && mRowsSupportFragment.getVerticalGridView().hasFocus()) {
                     if (direction == View.FOCUS_UP) {
-                        slideOutGridView();
-                        return mVideoSupportFragment.getView();
+                        if (mVideoSupportFragment != null && mVideoSupportFragment.getView() != null) {
+                            slideOutGridView();
+                            showTitle(false);
+                            return mVideoSupportFragment.getView();
+                        } else if (getTitleView() != null) {
+                            return getTitleView();
+                        }
                     }
-                } else if (mVideoSupportFragment.getView() != null
+                } else if (mVideoSupportFragment != null && mVideoSupportFragment.getView() != null
                         && mVideoSupportFragment.getView().hasFocus()) {
                     if (direction == View.FOCUS_DOWN) {
-                        slideInGridView();
-                        return mRowsSupportFragment.getVerticalGridView();
+                        if (mRowsSupportFragment.getVerticalGridView() != null) {
+                            showTitle(true);
+                            slideInGridView();
+                            return mRowsSupportFragment.getVerticalGridView();
+                        }
+                    }
+                } else if (getTitleView() != null && getTitleView().hasFocus()) {
+                    if (direction == View.FOCUS_DOWN) {
+                        if (mRowsSupportFragment.getVerticalGridView() != null) {
+                            return mRowsSupportFragment.getVerticalGridView();
+                        }
                     }
                 }
                 return focused;
@@ -582,6 +597,7 @@
                 if (mVideoSupportFragment != null && mVideoSupportFragment.getView() != null
                         && mVideoSupportFragment.getView().hasFocus()) {
                     if (keyCode == KeyEvent.KEYCODE_BACK) {
+                        showTitle(true);
                         slideInGridView();
                         getVerticalGridView().requestFocus();
                         return true;
diff --git a/v17/leanback/src/android/support/v17/leanback/app/PlaybackControlGlue.java b/v17/leanback/src/android/support/v17/leanback/app/PlaybackControlGlue.java
index b16f432..190ce22 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/PlaybackControlGlue.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/PlaybackControlGlue.java
@@ -211,9 +211,6 @@
      * containing this row should be notified.
      */
     protected void onRowChanged(PlaybackControlsRow row) {
-        if (getHost() != null) {
-            getHost().notifyPlaybackRowChanged();
-        }
     }
 
     /**
@@ -330,5 +327,10 @@
         public void fadeOut() {
             mFragment.fadeOut();
         }
+
+        @Override
+        public void notifyPlaybackRowChanged() {
+            mGlue.onRowChanged(mGlue.getControlsRow());
+        }
     }
 }
diff --git a/v17/leanback/src/android/support/v17/leanback/app/PlaybackControlSupportGlue.java b/v17/leanback/src/android/support/v17/leanback/app/PlaybackControlSupportGlue.java
index 55ce89e..b3d19ae 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/PlaybackControlSupportGlue.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/PlaybackControlSupportGlue.java
@@ -193,5 +193,10 @@
         public void fadeOut() {
             mFragment.fadeOut();
         }
+
+        @Override
+        public void notifyPlaybackRowChanged() {
+            mGlue.onRowChanged(mGlue.getControlsRow());
+        }
     }
 }
diff --git a/v17/leanback/src/android/support/v17/leanback/app/PlaybackFragment.java b/v17/leanback/src/android/support/v17/leanback/app/PlaybackFragment.java
index a0027ec..02a0257 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/PlaybackFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/PlaybackFragment.java
@@ -31,6 +31,7 @@
 import android.support.v17.leanback.media.PlaybackGlueHost;
 import android.support.v17.leanback.widget.ArrayObjectAdapter;
 import android.support.v17.leanback.widget.BaseOnItemViewClickedListener;
+import android.support.v17.leanback.widget.BaseOnItemViewSelectedListener;
 import android.support.v17.leanback.widget.ClassPresenterSelector;
 import android.support.v17.leanback.widget.ItemBridgeAdapter;
 import android.support.v17.leanback.widget.ObjectAdapter;
@@ -111,25 +112,43 @@
     private ObjectAdapter mAdapter;
     private PlaybackRowPresenter mPresenter;
     private Row mRow;
+    private BaseOnItemViewSelectedListener mExternalItemSelectedListener;
     private BaseOnItemViewClickedListener mExternalItemClickedListener;
     private BaseOnItemViewClickedListener mPlaybackItemClickedListener;
-    private BaseOnItemViewClickedListener mOnItemViewClickedListener = new BaseOnItemViewClickedListener() {
-        @Override
-        public void onItemClicked(Presenter.ViewHolder itemViewHolder,
-                                  Object item,
-                                  RowPresenter.ViewHolder rowViewHolder,
-                                  Object row) {
-            if (mPlaybackItemClickedListener != null
-                    && rowViewHolder instanceof PlaybackRowPresenter.ViewHolder) {
-                mPlaybackItemClickedListener.onItemClicked(
-                        itemViewHolder, item, rowViewHolder, row);
-            }
-            if (mExternalItemClickedListener != null) {
-                mExternalItemClickedListener.onItemClicked(
-                        itemViewHolder, item, rowViewHolder, row);
-            }
-        }
-    };
+
+    private final BaseOnItemViewClickedListener mOnItemViewClickedListener =
+            new BaseOnItemViewClickedListener() {
+                @Override
+                public void onItemClicked(Presenter.ViewHolder itemViewHolder,
+                                          Object item,
+                                          RowPresenter.ViewHolder rowViewHolder,
+                                          Object row) {
+                    if (mPlaybackItemClickedListener != null
+                            && rowViewHolder instanceof PlaybackRowPresenter.ViewHolder) {
+                        mPlaybackItemClickedListener.onItemClicked(
+                                itemViewHolder, item, rowViewHolder, row);
+                    }
+                    if (mExternalItemClickedListener != null) {
+                        mExternalItemClickedListener.onItemClicked(
+                                itemViewHolder, item, rowViewHolder, row);
+                    }
+                }
+            };
+
+    private final BaseOnItemViewSelectedListener mOnItemViewSelectedListener =
+            new BaseOnItemViewSelectedListener() {
+                @Override
+                public void onItemSelected(Presenter.ViewHolder itemViewHolder,
+                                           Object item,
+                                           RowPresenter.ViewHolder rowViewHolder,
+                                           Object row) {
+                    if (mExternalItemSelectedListener != null) {
+                        mExternalItemSelectedListener.onItemSelected(
+                                itemViewHolder, item, rowViewHolder, row);
+                    }
+                }
+            };
+
     private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable();
 
     public ObjectAdapter getAdapter() {
@@ -743,6 +762,7 @@
         } else {
             mRowsFragment.setAdapter(mAdapter);
         }
+        mRowsFragment.setOnItemViewSelectedListener(mOnItemViewSelectedListener);
         mRowsFragment.setOnItemViewClickedListener(mOnItemViewClickedListener);
 
         mBgAlpha = 255;
@@ -786,6 +806,15 @@
     }
 
     /**
+     * This listener is called every time there is a selection in {@link RowsFragment}. This can
+     * be used by users to take additional actions such as animations.
+     * @hide
+     */
+    public void setOnItemViewSelectedListener(final BaseOnItemViewSelectedListener listener) {
+        mExternalItemSelectedListener = listener;
+    }
+
+    /**
      * This listener is called every time there is a click in {@link RowsFragment}. This can
      * be used by users to take additional actions such as animations.
      */
diff --git a/v17/leanback/src/android/support/v17/leanback/app/PlaybackSupportFragment.java b/v17/leanback/src/android/support/v17/leanback/app/PlaybackSupportFragment.java
index f21bd4e..ed4cc1f 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/PlaybackSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/PlaybackSupportFragment.java
@@ -34,6 +34,7 @@
 import android.support.v17.leanback.media.PlaybackGlueHost;
 import android.support.v17.leanback.widget.ArrayObjectAdapter;
 import android.support.v17.leanback.widget.BaseOnItemViewClickedListener;
+import android.support.v17.leanback.widget.BaseOnItemViewSelectedListener;
 import android.support.v17.leanback.widget.ClassPresenterSelector;
 import android.support.v17.leanback.widget.ItemBridgeAdapter;
 import android.support.v17.leanback.widget.ObjectAdapter;
@@ -114,25 +115,43 @@
     private ObjectAdapter mAdapter;
     private PlaybackRowPresenter mPresenter;
     private Row mRow;
+    private BaseOnItemViewSelectedListener mExternalItemSelectedListener;
     private BaseOnItemViewClickedListener mExternalItemClickedListener;
     private BaseOnItemViewClickedListener mPlaybackItemClickedListener;
-    private BaseOnItemViewClickedListener mOnItemViewClickedListener = new BaseOnItemViewClickedListener() {
-        @Override
-        public void onItemClicked(Presenter.ViewHolder itemViewHolder,
-                                  Object item,
-                                  RowPresenter.ViewHolder rowViewHolder,
-                                  Object row) {
-            if (mPlaybackItemClickedListener != null
-                    && rowViewHolder instanceof PlaybackRowPresenter.ViewHolder) {
-                mPlaybackItemClickedListener.onItemClicked(
-                        itemViewHolder, item, rowViewHolder, row);
-            }
-            if (mExternalItemClickedListener != null) {
-                mExternalItemClickedListener.onItemClicked(
-                        itemViewHolder, item, rowViewHolder, row);
-            }
-        }
-    };
+
+    private final BaseOnItemViewClickedListener mOnItemViewClickedListener =
+            new BaseOnItemViewClickedListener() {
+                @Override
+                public void onItemClicked(Presenter.ViewHolder itemViewHolder,
+                                          Object item,
+                                          RowPresenter.ViewHolder rowViewHolder,
+                                          Object row) {
+                    if (mPlaybackItemClickedListener != null
+                            && rowViewHolder instanceof PlaybackRowPresenter.ViewHolder) {
+                        mPlaybackItemClickedListener.onItemClicked(
+                                itemViewHolder, item, rowViewHolder, row);
+                    }
+                    if (mExternalItemClickedListener != null) {
+                        mExternalItemClickedListener.onItemClicked(
+                                itemViewHolder, item, rowViewHolder, row);
+                    }
+                }
+            };
+
+    private final BaseOnItemViewSelectedListener mOnItemViewSelectedListener =
+            new BaseOnItemViewSelectedListener() {
+                @Override
+                public void onItemSelected(Presenter.ViewHolder itemViewHolder,
+                                           Object item,
+                                           RowPresenter.ViewHolder rowViewHolder,
+                                           Object row) {
+                    if (mExternalItemSelectedListener != null) {
+                        mExternalItemSelectedListener.onItemSelected(
+                                itemViewHolder, item, rowViewHolder, row);
+                    }
+                }
+            };
+
     private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable();
 
     public ObjectAdapter getAdapter() {
@@ -746,6 +765,7 @@
         } else {
             mRowsSupportFragment.setAdapter(mAdapter);
         }
+        mRowsSupportFragment.setOnItemViewSelectedListener(mOnItemViewSelectedListener);
         mRowsSupportFragment.setOnItemViewClickedListener(mOnItemViewClickedListener);
 
         mBgAlpha = 255;
@@ -789,6 +809,15 @@
     }
 
     /**
+     * This listener is called every time there is a selection in {@link RowsSupportFragment}.
+     * This can be used by users to take additional actions such as animations.
+     * @hide
+     */
+    public void setOnItemViewSelectedListener(final BaseOnItemViewSelectedListener listener) {
+        mExternalItemSelectedListener = listener;
+    }
+
+    /**
      * This listener is called every time there is a click in {@link RowsSupportFragment}. This can
      * be used by users to take additional actions such as animations.
      */
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/GuidedActionAdapter.java b/v17/leanback/src/android/support/v17/leanback/widget/GuidedActionAdapter.java
index ca6ee77..7d2904a 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/GuidedActionAdapter.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/GuidedActionAdapter.java
@@ -151,6 +151,9 @@
      * @param actions The list of actions to be managed.
      */
     public void setActions(List<GuidedAction> actions) {
+        if (!mIsSubAdapter) {
+            mStylist.collapseAction(false);
+        }
         mActionOnFocusListener.unFocus();
         mActions.clear();
         mActions.addAll(actions);
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/RowHeaderPresenter.java b/v17/leanback/src/android/support/v17/leanback/widget/RowHeaderPresenter.java
index 66dc12d..0d2b45e 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/RowHeaderPresenter.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/RowHeaderPresenter.java
@@ -18,6 +18,7 @@
 import android.graphics.Paint;
 import android.support.annotation.RestrictTo;
 import android.support.v17.leanback.R;
+import android.text.TextUtils;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -125,6 +126,11 @@
         } else {
             vh.mTitleView.setText(headerItem.getName());
             if (vh.mDescriptionView != null) {
+                if (TextUtils.isEmpty(headerItem.getDescription())) {
+                    vh.mDescriptionView.setVisibility(View.GONE);
+                } else {
+                    vh.mDescriptionView.setVisibility(View.VISIBLE);
+                }
                 vh.mDescriptionView.setText(headerItem.getDescription());
             }
             viewHolder.view.setContentDescription(headerItem.getContentDescription());
diff --git a/v17/leanback/tests/AndroidManifest.xml b/v17/leanback/tests/AndroidManifest.xml
index 16b78cb..a30e5c9 100644
--- a/v17/leanback/tests/AndroidManifest.xml
+++ b/v17/leanback/tests/AndroidManifest.xml
@@ -58,6 +58,14 @@
                   android:theme="@style/Theme.Leanback.GuidedStep"
                   android:exported="true" />
 
+        <activity android:name="android.support.v17.leanback.app.PlaybackTestActivity"
+                  android:theme="@style/Theme.Leanback"
+                  android:exported="true" />
+
+        <activity android:name="android.support.v17.leanback.app.PlaybackSupportTestActivity"
+                  android:theme="@style/Theme.Leanback"
+                  android:exported="true" />
+
         <activity android:name="android.support.v17.leanback.app.PlaybackOverlayTestActivity"
                   android:theme="@style/Theme.Leanback"
                   android:exported="true" />
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsFragmentTest.java
index c6662ab..c24a7e5 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsFragmentTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsFragmentTest.java
@@ -16,6 +16,7 @@
 package android.support.v17.leanback.app;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 
 import android.content.Intent;
 import android.graphics.Rect;
@@ -87,6 +88,9 @@
                 bitmapDrawable.getBounds().height());
         assertEquals(0, bitmapDrawable.getVerticalOffset());
 
+        assertTrue("TitleView is visible", detailsFragment.getView()
+                .findViewById(R.id.browse_title_group).getVisibility() == View.VISIBLE);
+
         activityTestRule.runOnUiThread(new Runnable() {
             @Override
             public void run() {
@@ -97,7 +101,9 @@
         PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
             @Override
             public boolean canProceed() {
-                return bitmapDrawable.getVerticalOffset() == mDefaultVerticalOffset;
+                return bitmapDrawable.getVerticalOffset() == mDefaultVerticalOffset
+                        && detailsFragment.getView()
+                        .findViewById(R.id.browse_title_group).getVisibility() != View.VISIBLE;
             }
         });
 
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTest.java
index 192b20e..50aaa62 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepFragmentTest.java
@@ -29,6 +29,7 @@
 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.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -354,4 +355,58 @@
         PollingCheck.waitFor(new PollingCheck.ActivityDestroy(activity));
         verify(first, timeout(ON_DESTROY_TIMEOUT).times(1)).onDestroy();
     }
+
+    @Test
+    public void setActionsWhenSubActionsExpanded() throws Throwable {
+        final String firstFragmentName = generateMethodTestName("first");
+        GuidedStepTestFragment.Provider first = mockProvider(firstFragmentName);
+        doAnswer(new Answer<Void>() {
+            public Void answer(InvocationOnMock invocation) {
+                List actions = (List) invocation.getArguments()[0];
+                List<GuidedAction> subActions = new ArrayList<GuidedAction>();
+                subActions.add(new GuidedAction.Builder().id(2000).title("item1").build());
+                actions.add(new GuidedAction.Builder().id(1000).subActions(subActions)
+                        .title("list").build());
+                return null;
+            }
+        }).when(first).onCreateActions(any(List.class), any(Bundle.class));
+        doAnswer(new Answer<Boolean>() {
+            public Boolean answer(InvocationOnMock invocation) {
+                GuidedStepTestFragment.Provider obj = (GuidedStepTestFragment.Provider)
+                        invocation.getMock();
+                GuidedAction action = (GuidedAction) invocation.getArguments()[0];
+                if (action.getId() == 2000) {
+                    List<GuidedAction> newActions = new ArrayList<GuidedAction>();
+                    newActions.add(new GuidedAction.Builder().id(1001).title("item2").build());
+                    obj.getFragment().setActions(newActions);
+                    return false;
+                }
+                return false;
+            }
+        }).when(first).onSubGuidedActionClicked(any(GuidedAction.class));
+
+        final GuidedStepFragmentTestActivity activity = launchTestActivity(firstFragmentName);
+
+        // after clicked, it sub actions list should expand
+        View firstView = first.getFragment().getActionItemView(0);
+        assertTrue(firstView.hasFocus());
+        sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
+        PollingCheck.waitFor(new ExpandTransitionFinish(first));
+        assertFalse(firstView.hasFocus());
+
+        sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
+        ArgumentCaptor<GuidedAction> actionCapture = ArgumentCaptor.forClass(GuidedAction.class);
+        verify(first, times(1)).onSubGuidedActionClicked(actionCapture.capture());
+        // after clicked a sub action, whole action list is replaced.
+        PollingCheck.waitFor(new ExpandTransitionFinish(first));
+        assertFalse(first.getFragment().isExpanded());
+        View newFirstView  = first.getFragment().getActionItemView(0);
+        assertTrue(newFirstView.hasFocus());
+        assertTrue(newFirstView.getVisibility() == View.VISIBLE);
+        GuidedActionsStylist.ViewHolder vh = (GuidedActionsStylist.ViewHolder) first.getFragment()
+                .getGuidedActionsStylist().getActionsGridView().getChildViewHolder(newFirstView);
+        assertEquals(1001, vh.getAction().getId());
+
+    }
+
 }
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTest.java
index 879292f..7d6b54f 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/GuidedStepSupportFragmentTest.java
@@ -32,6 +32,7 @@
 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.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -357,4 +358,58 @@
         PollingCheck.waitFor(new PollingCheck.ActivityDestroy(activity));
         verify(first, timeout(ON_DESTROY_TIMEOUT).times(1)).onDestroy();
     }
+
+    @Test
+    public void setActionsWhenSubActionsExpanded() throws Throwable {
+        final String firstFragmentName = generateMethodTestName("first");
+        GuidedStepTestSupportFragment.Provider first = mockProvider(firstFragmentName);
+        doAnswer(new Answer<Void>() {
+            public Void answer(InvocationOnMock invocation) {
+                List actions = (List) invocation.getArguments()[0];
+                List<GuidedAction> subActions = new ArrayList<GuidedAction>();
+                subActions.add(new GuidedAction.Builder().id(2000).title("item1").build());
+                actions.add(new GuidedAction.Builder().id(1000).subActions(subActions)
+                        .title("list").build());
+                return null;
+            }
+        }).when(first).onCreateActions(any(List.class), any(Bundle.class));
+        doAnswer(new Answer<Boolean>() {
+            public Boolean answer(InvocationOnMock invocation) {
+                GuidedStepTestSupportFragment.Provider obj = (GuidedStepTestSupportFragment.Provider)
+                        invocation.getMock();
+                GuidedAction action = (GuidedAction) invocation.getArguments()[0];
+                if (action.getId() == 2000) {
+                    List<GuidedAction> newActions = new ArrayList<GuidedAction>();
+                    newActions.add(new GuidedAction.Builder().id(1001).title("item2").build());
+                    obj.getFragment().setActions(newActions);
+                    return false;
+                }
+                return false;
+            }
+        }).when(first).onSubGuidedActionClicked(any(GuidedAction.class));
+
+        final GuidedStepSupportFragmentTestActivity activity = launchTestActivity(firstFragmentName);
+
+        // after clicked, it sub actions list should expand
+        View firstView = first.getFragment().getActionItemView(0);
+        assertTrue(firstView.hasFocus());
+        sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
+        PollingCheck.waitFor(new ExpandTransitionFinish(first));
+        assertFalse(firstView.hasFocus());
+
+        sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
+        ArgumentCaptor<GuidedAction> actionCapture = ArgumentCaptor.forClass(GuidedAction.class);
+        verify(first, times(1)).onSubGuidedActionClicked(actionCapture.capture());
+        // after clicked a sub action, whole action list is replaced.
+        PollingCheck.waitFor(new ExpandTransitionFinish(first));
+        assertFalse(first.getFragment().isExpanded());
+        View newFirstView  = first.getFragment().getActionItemView(0);
+        assertTrue(newFirstView.hasFocus());
+        assertTrue(newFirstView.getVisibility() == View.VISIBLE);
+        GuidedActionsStylist.ViewHolder vh = (GuidedActionsStylist.ViewHolder) first.getFragment()
+                .getGuidedActionsStylist().getActionsGridView().getChildViewHolder(newFirstView);
+        assertEquals(1001, vh.getAction().getId());
+
+    }
+
 }
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackControlGlueTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackControlGlueTest.java
index f26c60c..326d2be 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackControlGlueTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackControlGlueTest.java
@@ -50,6 +50,8 @@
 
     static class PlayControlGlueImpl extends PlaybackControlGlue {
         int mSpeedId = PLAYBACK_SPEED_PAUSED;
+        // number of times onRowChanged callback is called
+        int mOnRowChangedCallCount = 0;
 
         PlayControlGlueImpl(Context context, int[] seekSpeeds) {
             super(context, seekSpeeds);
@@ -59,6 +61,11 @@
             super(context, ffSpeeds, rwSpeeds);
         }
 
+        PlayControlGlueImpl(Context context, PlaybackOverlayFragment fragment,
+                                   int[] seekSpeeds) {
+            super(context, fragment, seekSpeeds);
+        }
+
         @Override
         public boolean hasValidMedia() {
             return true;
@@ -124,6 +131,16 @@
 
         @Override
         protected void onRowChanged(PlaybackControlsRow row) {
+            mOnRowChangedCallCount++;
+        }
+
+        public void notifyMetaDataChanged() {
+            onMetadataChanged();
+            onStateChanged();
+        }
+
+        public int getOnRowChangedCallCount() {
+            return mOnRowChangedCallCount;
         }
     }
 
@@ -557,4 +574,32 @@
         assertEquals(PlaybackControlGlue.PLAYBACK_SPEED_NORMAL, glue.getCurrentSpeedId());
     }
 
+    @Test
+    public void testOnRowChangedCallback() throws Exception {
+        final PlaybackOverlayFragment[] fragmentResult = new
+                PlaybackOverlayFragment[1];
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                fragmentResult[0] = new PlaybackOverlayFragment();
+            }
+        });
+        PlaybackOverlayFragment fragment = fragmentResult[0];
+        PlayControlGlueImpl playbackGlue = new PlayControlGlueImpl(context, fragment,
+                new int[]{
+                        PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0,
+                        PlaybackControlGlue.PLAYBACK_SPEED_FAST_L1,
+                        PlaybackControlGlue.PLAYBACK_SPEED_FAST_L2
+                });
+
+        // before any controls row is created the count is zero
+        assertEquals(playbackGlue.getOnRowChangedCallCount(), 0);
+        playbackGlue.createControlsRowAndPresenter();
+        // after a controls row is created, onRowChanged() call back is called once
+        assertEquals(playbackGlue.getOnRowChangedCallCount(), 1);
+        playbackGlue.notifyMetaDataChanged();
+        // onMetaDataChanged() calls updateRowMetadata which ends up calling
+        // notifyPlaybackRowChanged on the old host and finally onRowChanged on the glue.
+        assertEquals(playbackGlue.getOnRowChangedCallCount(), 2);
+    }
 }
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackControlSupportGlueTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackControlSupportGlueTest.java
index 211b42c..748a39f 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackControlSupportGlueTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackControlSupportGlueTest.java
@@ -51,8 +51,10 @@
 public class PlaybackControlSupportGlueTest {
 
 
-    static class PlayControlGlueImpl extends PlaybackControlSupportGlue {
+    public  static class PlayControlGlueImpl extends PlaybackControlSupportGlue {
         int mSpeedId = PLAYBACK_SPEED_PAUSED;
+        // number of times onRowChanged callback is called
+        int mOnRowChangedCallCount = 0;
 
         PlayControlGlueImpl(Context context, int[] seekSpeeds) {
             super(context, seekSpeeds);
@@ -62,6 +64,11 @@
             super(context, ffSpeeds, rwSpeeds);
         }
 
+        PlayControlGlueImpl(Context context, PlaybackOverlaySupportFragment fragment,
+                int[] seekSpeeds) {
+            super(context, fragment, seekSpeeds);
+        }
+
         @Override
         public boolean hasValidMedia() {
             return true;
@@ -127,6 +134,16 @@
 
         @Override
         protected void onRowChanged(PlaybackControlsRow row) {
+            mOnRowChangedCallCount++;
+        }
+
+        public void notifyMetaDataChanged() {
+            onMetadataChanged();
+            onStateChanged();
+        }
+
+        public int getOnRowChangedCallCount() {
+            return mOnRowChangedCallCount;
         }
     }
 
@@ -560,4 +577,33 @@
         assertEquals(PlaybackControlSupportGlue.PLAYBACK_SPEED_NORMAL, glue.getCurrentSpeedId());
     }
 
+    @Test
+    public void testOnRowChangedCallback() throws Exception {
+        final PlaybackOverlaySupportFragment[] fragmentResult = new
+                PlaybackOverlaySupportFragment[1];
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                fragmentResult[0] = new PlaybackOverlaySupportFragment();
+            }
+        });
+        PlaybackOverlaySupportFragment fragment = fragmentResult[0];
+        PlayControlGlueImpl playbackGlue = new PlayControlGlueImpl(context, fragment,
+                new int[]{
+                        PlaybackControlSupportGlue.PLAYBACK_SPEED_FAST_L0,
+                        PlaybackControlSupportGlue.PLAYBACK_SPEED_FAST_L1,
+                        PlaybackControlSupportGlue.PLAYBACK_SPEED_FAST_L2
+                });
+
+        // before any controls row is created the count is zero
+        assertEquals(playbackGlue.getOnRowChangedCallCount(), 0);
+        playbackGlue.createControlsRowAndPresenter();
+        // after a controls row is created, onRowChanged() call back is called once
+        assertEquals(playbackGlue.getOnRowChangedCallCount(), 1);
+        playbackGlue.notifyMetaDataChanged();
+        // onMetaDataChanged() calls updateRowMetadata which ends up calling
+        // notifyPlaybackRowChanged on the old host and finally onRowChanged on the glue.
+        assertEquals(playbackGlue.getOnRowChangedCallCount(), 2);
+    }
+
 }
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackFragmentTest.java
new file mode 100644
index 0000000..bf7077b
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackFragmentTest.java
@@ -0,0 +1,219 @@
+/*
+ * 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.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.Intent;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v17.leanback.media.PlaybackControlGlue;
+import android.support.v17.leanback.widget.ListRow;
+import android.support.v17.leanback.widget.OnItemViewClickedListener;
+import android.support.v17.leanback.widget.OnItemViewSelectedListener;
+import android.support.v17.leanback.widget.PlaybackControlsRow;
+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.SparseArrayObjectAdapter;
+import android.view.KeyEvent;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class PlaybackFragmentTest {
+
+    private static final String TAG = "PlaybackFragmentTest";
+    private static final long TRANSITION_LENGTH = 1000;
+
+    @Rule
+    public ActivityTestRule<PlaybackTestActivity> activityTestRule =
+            new ActivityTestRule<>(PlaybackTestActivity.class, false, false);
+    private PlaybackTestActivity mActivity;
+
+    @Test
+    public void testSelectedListener() throws Throwable {
+        Intent intent = new Intent();
+        mActivity = activityTestRule.launchActivity(intent);
+        PlaybackTestFragment fragment = mActivity.getPlaybackFragment();
+        assertTrue(fragment.getView().hasFocus());
+
+        OnItemViewSelectedListener selectedListener = Mockito.mock(
+                OnItemViewSelectedListener.class);
+        fragment.setOnItemViewSelectedListener(selectedListener);
+
+
+        PlaybackControlsRow controlsRow = fragment.getGlue().getControlsRow();
+        SparseArrayObjectAdapter primaryActionsAdapter = (SparseArrayObjectAdapter)
+                controlsRow.getPrimaryActionsAdapter();
+
+        PlaybackControlsRow.MultiAction playPause = (PlaybackControlsRow.MultiAction)
+                primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_PLAY_PAUSE);
+
+        PlaybackControlsRow.MultiAction rewind = (PlaybackControlsRow.MultiAction)
+                primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_REWIND);
+
+        PlaybackControlsRow.MultiAction thumbsUp = (PlaybackControlsRow.MultiAction)
+                primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_CUSTOM_LEFT_FIRST);
+
+        ArgumentCaptor<Presenter.ViewHolder> itemVHCaptor =
+                ArgumentCaptor.forClass(Presenter.ViewHolder.class);
+        ArgumentCaptor<Object> itemCaptor = ArgumentCaptor.forClass(Object.class);
+        ArgumentCaptor<RowPresenter.ViewHolder> rowVHCaptor =
+                ArgumentCaptor.forClass(RowPresenter.ViewHolder.class);
+        ArgumentCaptor<Row> rowCaptor = ArgumentCaptor.forClass(Row.class);
+
+
+        // First navigate left within PlaybackControlsRow items.
+        verify(selectedListener, times(0)).onItemSelected(any(Presenter.ViewHolder.class),
+                any(Object.class), any(RowPresenter.ViewHolder.class), any(Row.class));
+        sendKeys(KeyEvent.KEYCODE_DPAD_LEFT);
+        Thread.sleep(TRANSITION_LENGTH);
+        verify(selectedListener, times(1)).onItemSelected(itemVHCaptor.capture(),
+                itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
+        assertSame("Same controls row should be passed to the listener", controlsRow,
+                rowCaptor.getValue());
+        assertSame("The selected action should be rewind", rewind, itemCaptor.getValue());
+
+        sendKeys(KeyEvent.KEYCODE_DPAD_LEFT);
+        Thread.sleep(TRANSITION_LENGTH);
+        verify(selectedListener, times(2)).onItemSelected(itemVHCaptor.capture(),
+                itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
+        assertSame("Same controls row should be passed to the listener", controlsRow,
+                rowCaptor.getValue());
+        assertSame("The selected action should be thumbsUp", thumbsUp, itemCaptor.getValue());
+
+        // Now navigate down to a ListRow item.
+        ListRow listRow0 = (ListRow) fragment.getAdapter().get(1);
+
+        sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+        Thread.sleep(TRANSITION_LENGTH);
+        verify(selectedListener, times(3)).onItemSelected(itemVHCaptor.capture(),
+                itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
+        assertSame("Same list row should be passed to the listener", listRow0,
+                rowCaptor.getValue());
+        // Depending on the focusSearch algorithm, one of the items in the first ListRow must be
+        // selected.
+        boolean listRowItemPassed = (itemCaptor.getValue() == listRow0.getAdapter().get(0)
+                || itemCaptor.getValue() == listRow0.getAdapter().get(1));
+        assertTrue("None of the items in the first ListRow are passed to the selected listener.",
+                listRowItemPassed);
+    }
+
+    @Test
+    public void testClickedListener() throws Throwable {
+        Intent intent = new Intent();
+        mActivity = activityTestRule.launchActivity(intent);
+        PlaybackTestFragment fragment = mActivity.getPlaybackFragment();
+        assertTrue(fragment.getView().hasFocus());
+
+        OnItemViewClickedListener clickedListener = Mockito.mock(OnItemViewClickedListener.class);
+        fragment.setOnItemViewClickedListener(clickedListener);
+
+
+        PlaybackControlsRow controlsRow = fragment.getGlue().getControlsRow();
+        SparseArrayObjectAdapter primaryActionsAdapter = (SparseArrayObjectAdapter)
+                controlsRow.getPrimaryActionsAdapter();
+
+        PlaybackControlsRow.MultiAction playPause = (PlaybackControlsRow.MultiAction)
+                primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_PLAY_PAUSE);
+
+        PlaybackControlsRow.MultiAction rewind = (PlaybackControlsRow.MultiAction)
+                primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_REWIND);
+
+        PlaybackControlsRow.MultiAction thumbsUp = (PlaybackControlsRow.MultiAction)
+                primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_CUSTOM_LEFT_FIRST);
+
+        ArgumentCaptor<Presenter.ViewHolder> itemVHCaptor =
+                ArgumentCaptor.forClass(Presenter.ViewHolder.class);
+        ArgumentCaptor<Object> itemCaptor = ArgumentCaptor.forClass(Object.class);
+        ArgumentCaptor<RowPresenter.ViewHolder> rowVHCaptor =
+                ArgumentCaptor.forClass(RowPresenter.ViewHolder.class);
+        ArgumentCaptor<Row> rowCaptor = ArgumentCaptor.forClass(Row.class);
+
+
+        // First navigate left within PlaybackControlsRow items.
+        verify(clickedListener, times(0)).onItemClicked(any(Presenter.ViewHolder.class),
+                any(Object.class), any(RowPresenter.ViewHolder.class), any(Row.class));
+        sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
+        Thread.sleep(TRANSITION_LENGTH);
+        verify(clickedListener, times(1)).onItemClicked(itemVHCaptor.capture(),
+                itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
+        assertSame("Same controls row should be passed to the listener", controlsRow,
+                rowCaptor.getValue());
+        assertSame("The clicked action should be playPause", playPause, itemCaptor.getValue());
+
+        sendKeys(KeyEvent.KEYCODE_DPAD_LEFT);
+        Thread.sleep(TRANSITION_LENGTH);
+        verify(clickedListener, times(1)).onItemClicked(any(Presenter.ViewHolder.class),
+                any(Object.class), any(RowPresenter.ViewHolder.class), any(Row.class));
+        sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
+        Thread.sleep(TRANSITION_LENGTH);
+        verify(clickedListener, times(2)).onItemClicked(itemVHCaptor.capture(),
+                itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
+        assertSame("Same controls row should be passed to the listener", controlsRow,
+                rowCaptor.getValue());
+        assertSame("The clicked action should be rewind", rewind, itemCaptor.getValue());
+
+        sendKeys(KeyEvent.KEYCODE_DPAD_LEFT);
+        Thread.sleep(TRANSITION_LENGTH);
+        verify(clickedListener, times(2)).onItemClicked(any(Presenter.ViewHolder.class),
+                any(Object.class), any(RowPresenter.ViewHolder.class), any(Row.class));
+        sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
+        Thread.sleep(TRANSITION_LENGTH);
+        verify(clickedListener, times(3)).onItemClicked(itemVHCaptor.capture(),
+                itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
+        assertSame("Same controls row should be passed to the listener", controlsRow,
+                rowCaptor.getValue());
+        assertSame("The clicked action should be thumbsUp", thumbsUp, itemCaptor.getValue());
+
+        // Now navigate down to a ListRow item.
+        ListRow listRow0 = (ListRow) fragment.getAdapter().get(1);
+
+        sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+        Thread.sleep(TRANSITION_LENGTH);
+        verify(clickedListener, times(3)).onItemClicked(any(Presenter.ViewHolder.class),
+                any(Object.class), any(RowPresenter.ViewHolder.class), any(Row.class));
+        sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
+        Thread.sleep(TRANSITION_LENGTH);
+        verify(clickedListener, times(4)).onItemClicked(itemVHCaptor.capture(),
+                itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
+        assertSame("Same list row should be passed to the listener", listRow0,
+                rowCaptor.getValue());
+        boolean listRowItemPassed = (itemCaptor.getValue() == listRow0.getAdapter().get(0)
+                || itemCaptor.getValue() == listRow0.getAdapter().get(1));
+        assertTrue("None of the items in the first ListRow are passed to the click listener.",
+                listRowItemPassed);
+    }
+
+    private void sendKeys(int ...keys) {
+        for (int i = 0; i < keys.length; i++) {
+            InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keys[i]);
+        }
+    }
+
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportFragmentTest.java
new file mode 100644
index 0000000..fdba125
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportFragmentTest.java
@@ -0,0 +1,220 @@
+/*
+ * 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.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.Intent;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v17.leanback.media.PlaybackControlGlue;
+import android.support.v17.leanback.widget.ListRow;
+import android.support.v17.leanback.widget.OnItemViewClickedListener;
+import android.support.v17.leanback.widget.OnItemViewSelectedListener;
+import android.support.v17.leanback.widget.PlaybackControlsRow;
+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.SparseArrayObjectAdapter;
+import android.view.KeyEvent;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class PlaybackSupportFragmentTest {
+
+    private static final String TAG = "PlaybackSupportFragmentTest";
+    private static final long TRANSITION_LENGTH = 1000;
+
+    @Rule
+    public ActivityTestRule<PlaybackSupportTestActivity> activityTestRule =
+            new ActivityTestRule<>(PlaybackSupportTestActivity.class, false, false);
+    private PlaybackSupportTestActivity mActivity;
+
+    @Test
+    public void testSelectedListener() throws Throwable {
+        Intent intent = new Intent();
+        mActivity = activityTestRule.launchActivity(intent);
+        PlaybackSupportTestFragment fragment = mActivity.getPlaybackFragment();
+        assertTrue(fragment.getView().hasFocus());
+
+        OnItemViewSelectedListener selectedListener = Mockito.mock(
+                OnItemViewSelectedListener.class);
+        fragment.setOnItemViewSelectedListener(selectedListener);
+
+
+        PlaybackControlsRow controlsRow = fragment.getGlue().getControlsRow();
+        SparseArrayObjectAdapter primaryActionsAdapter = (SparseArrayObjectAdapter)
+                controlsRow.getPrimaryActionsAdapter();
+
+        PlaybackControlsRow.MultiAction playPause = (PlaybackControlsRow.MultiAction)
+                primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_PLAY_PAUSE);
+
+        PlaybackControlsRow.MultiAction rewind = (PlaybackControlsRow.MultiAction)
+                primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_REWIND);
+
+        PlaybackControlsRow.MultiAction thumbsUp = (PlaybackControlsRow.MultiAction)
+                primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_CUSTOM_LEFT_FIRST);
+
+        ArgumentCaptor<Presenter.ViewHolder> itemVHCaptor =
+                ArgumentCaptor.forClass(Presenter.ViewHolder.class);
+        ArgumentCaptor<Object> itemCaptor =
+                ArgumentCaptor.forClass(Object.class);
+        ArgumentCaptor<RowPresenter.ViewHolder> rowVHCaptor =
+                ArgumentCaptor.forClass(RowPresenter.ViewHolder.class);
+        ArgumentCaptor<Row> rowCaptor = ArgumentCaptor.forClass(Row.class);
+
+
+        // First navigate left within PlaybackControlsRow items.
+        verify(selectedListener, times(0)).onItemSelected(any(Presenter.ViewHolder.class),
+                any(Object.class), any(RowPresenter.ViewHolder.class), any(Row.class));
+        sendKeys(KeyEvent.KEYCODE_DPAD_LEFT);
+        Thread.sleep(TRANSITION_LENGTH);
+        verify(selectedListener, times(1)).onItemSelected(itemVHCaptor.capture(),
+                itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
+        assertSame("Same controls row should be passed to the listener", controlsRow,
+                rowCaptor.getValue());
+        assertSame("The selected action should be rewind", rewind, itemCaptor.getValue());
+
+        sendKeys(KeyEvent.KEYCODE_DPAD_LEFT);
+        Thread.sleep(TRANSITION_LENGTH);
+        verify(selectedListener, times(2)).onItemSelected(itemVHCaptor.capture(),
+                itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
+        assertSame("Same controls row should be passed to the listener", controlsRow,
+                rowCaptor.getValue());
+        assertSame("The selected action should be thumbsUp", thumbsUp, itemCaptor.getValue());
+
+        // Now navigate down to a ListRow item.
+        ListRow listRow0 = (ListRow) fragment.getAdapter().get(1);
+
+        sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+        Thread.sleep(TRANSITION_LENGTH);
+        verify(selectedListener, times(3)).onItemSelected(itemVHCaptor.capture(),
+                itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
+        assertSame("Same list row should be passed to the listener", listRow0,
+                rowCaptor.getValue());
+        // Depending on the focusSearch algorithm, one of the items in the first ListRow must be
+        // selected.
+        boolean listRowItemPassed = (itemCaptor.getValue() == listRow0.getAdapter().get(0)
+                || itemCaptor.getValue() == listRow0.getAdapter().get(1));
+        assertTrue("None of the items in the first ListRow are passed to the selected listener.",
+                listRowItemPassed);
+    }
+
+    @Test
+    public void testClickedListener() throws Throwable {
+        Intent intent = new Intent();
+        mActivity = activityTestRule.launchActivity(intent);
+        PlaybackSupportTestFragment fragment = mActivity.getPlaybackFragment();
+        assertTrue(fragment.getView().hasFocus());
+
+        OnItemViewClickedListener clickedListener = Mockito.mock(OnItemViewClickedListener.class);
+        fragment.setOnItemViewClickedListener(clickedListener);
+
+
+        PlaybackControlsRow controlsRow = fragment.getGlue().getControlsRow();
+        SparseArrayObjectAdapter primaryActionsAdapter = (SparseArrayObjectAdapter)
+                controlsRow.getPrimaryActionsAdapter();
+
+        PlaybackControlsRow.MultiAction playPause = (PlaybackControlsRow.MultiAction)
+                primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_PLAY_PAUSE);
+
+        PlaybackControlsRow.MultiAction rewind = (PlaybackControlsRow.MultiAction)
+                primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_REWIND);
+
+        PlaybackControlsRow.MultiAction thumbsUp = (PlaybackControlsRow.MultiAction)
+                primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_CUSTOM_LEFT_FIRST);
+
+        ArgumentCaptor<Presenter.ViewHolder> itemVHCaptor =
+                ArgumentCaptor.forClass(Presenter.ViewHolder.class);
+        ArgumentCaptor<Object> itemCaptor = ArgumentCaptor.forClass(Object.class);
+        ArgumentCaptor<RowPresenter.ViewHolder> rowVHCaptor =
+                ArgumentCaptor.forClass(RowPresenter.ViewHolder.class);
+        ArgumentCaptor<Row> rowCaptor = ArgumentCaptor.forClass(Row.class);
+
+
+        // First navigate left within PlaybackControlsRow items.
+        verify(clickedListener, times(0)).onItemClicked(any(Presenter.ViewHolder.class),
+                any(Object.class), any(RowPresenter.ViewHolder.class), any(Row.class));
+        sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
+        Thread.sleep(TRANSITION_LENGTH);
+        verify(clickedListener, times(1)).onItemClicked(itemVHCaptor.capture(),
+                itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
+        assertSame("Same controls row should be passed to the listener", controlsRow,
+                rowCaptor.getValue());
+        assertSame("The clicked action should be playPause", playPause, itemCaptor.getValue());
+
+        sendKeys(KeyEvent.KEYCODE_DPAD_LEFT);
+        Thread.sleep(TRANSITION_LENGTH);
+        verify(clickedListener, times(1)).onItemClicked(any(Presenter.ViewHolder.class),
+                any(Object.class), any(RowPresenter.ViewHolder.class), any(Row.class));
+        sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
+        Thread.sleep(TRANSITION_LENGTH);
+        verify(clickedListener, times(2)).onItemClicked(itemVHCaptor.capture(),
+                itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
+        assertSame("Same controls row should be passed to the listener", controlsRow,
+                rowCaptor.getValue());
+        assertSame("The clicked action should be rewind", rewind, itemCaptor.getValue());
+
+        sendKeys(KeyEvent.KEYCODE_DPAD_LEFT);
+        Thread.sleep(TRANSITION_LENGTH);
+        verify(clickedListener, times(2)).onItemClicked(any(Presenter.ViewHolder.class),
+                any(Object.class), any(RowPresenter.ViewHolder.class), any(Row.class));
+        sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
+        Thread.sleep(TRANSITION_LENGTH);
+        verify(clickedListener, times(3)).onItemClicked(itemVHCaptor.capture(),
+                itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
+        assertSame("Same controls row should be passed to the listener", controlsRow,
+                rowCaptor.getValue());
+        assertSame("The clicked action should be thumbsUp", thumbsUp, itemCaptor.getValue());
+
+        // Now navigate down to a ListRow item.
+        ListRow listRow0 = (ListRow) fragment.getAdapter().get(1);
+
+        sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+        Thread.sleep(TRANSITION_LENGTH);
+        verify(clickedListener, times(3)).onItemClicked(any(Presenter.ViewHolder.class),
+                any(Object.class), any(RowPresenter.ViewHolder.class), any(Row.class));
+        sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
+        Thread.sleep(TRANSITION_LENGTH);
+        verify(clickedListener, times(4)).onItemClicked(itemVHCaptor.capture(),
+                itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
+        assertSame("Same list row should be passed to the listener", listRow0,
+                rowCaptor.getValue());
+        boolean listRowItemPassed = (itemCaptor.getValue() == listRow0.getAdapter().get(0)
+                || itemCaptor.getValue() == listRow0.getAdapter().get(1));
+        assertTrue("None of the items in the first ListRow are passed to the click listener.",
+                listRowItemPassed);
+    }
+
+    private void sendKeys(int ...keys) {
+        for (int i = 0; i < keys.length; i++) {
+            InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keys[i]);
+        }
+    }
+
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportTestActivity.java b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportTestActivity.java
new file mode 100644
index 0000000..c85fe83
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportTestActivity.java
@@ -0,0 +1,57 @@
+/*
+ * 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 android.os.Bundle;
+import android.support.v17.leanback.test.R;
+import android.support.v4.app.FragmentActivity;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class PlaybackSupportTestActivity extends FragmentActivity {
+    private List<PictureInPictureListener> mListeners = new ArrayList<>();
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.playback_support_controls);
+    }
+
+    @Override
+    public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
+        for (PictureInPictureListener listener : mListeners) {
+            listener.onPictureInPictureModeChanged(isInPictureInPictureMode);
+        }
+    }
+
+    public void registerPictureInPictureListener(PictureInPictureListener listener) {
+        mListeners.add(listener);
+    }
+
+    public void unregisterPictureInPictureListener(PictureInPictureListener listener) {
+        mListeners.remove(listener);
+    }
+
+    public interface PictureInPictureListener {
+        void onPictureInPictureModeChanged(boolean isInPictureInPictureMode);
+    }
+
+    public PlaybackSupportTestFragment getPlaybackFragment() {
+        return (PlaybackSupportTestFragment) getSupportFragmentManager().findFragmentById(
+                R.id.playback_controls_fragment);
+    }
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportTestFragment.java b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportTestFragment.java
new file mode 100644
index 0000000..4a07a60
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportTestFragment.java
@@ -0,0 +1,387 @@
+/*
+ * 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 android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.Handler;
+
+import android.support.v17.leanback.media.PlaybackControlGlue;
+import android.support.v17.leanback.test.R;
+import android.support.v17.leanback.widget.Action;
+import android.support.v17.leanback.widget.ArrayObjectAdapter;
+import android.support.v17.leanback.widget.HeaderItem;
+import android.support.v17.leanback.widget.ListRow;
+import android.support.v17.leanback.widget.ListRowPresenter;
+import android.support.v17.leanback.widget.OnItemViewClickedListener;
+import android.support.v17.leanback.widget.OnItemViewSelectedListener;
+import android.support.v17.leanback.widget.PlaybackControlsRow;
+import android.support.v17.leanback.widget.Presenter;
+import android.support.v17.leanback.widget.PresenterSelector;
+import android.support.v17.leanback.widget.Row;
+import android.support.v17.leanback.widget.RowPresenter;
+import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.widget.Toast;
+
+public class PlaybackSupportTestFragment extends PlaybackSupportFragment {
+    private static final String TAG = "PlaybackTestFragment";
+
+    /**
+     * Change this to choose a different overlay background.
+     */
+    private static final int BACKGROUND_TYPE = PlaybackFragment.BG_LIGHT;
+
+    private static final int ROW_CONTROLS = 0;
+
+    /**
+     * Change this to select hidden
+     */
+    private static final boolean SECONDARY_HIDDEN = false;
+
+    /**
+     * Change the number of related content rows.
+     */
+    private static final int RELATED_CONTENT_ROWS = 3;
+
+    private android.support.v17.leanback.media.PlaybackControlGlue mGlue;
+    private ListRowPresenter mListRowPresenter;
+
+    public SparseArrayObjectAdapter getAdapter() {
+        return (SparseArrayObjectAdapter) super.getAdapter();
+    }
+
+    private OnItemViewClickedListener mOnItemViewClickedListener = new OnItemViewClickedListener() {
+        @Override
+        public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
+                                  RowPresenter.ViewHolder rowViewHolder, Row row) {
+            Log.d(TAG, "onItemClicked: " + item + " row " + row);
+        }
+    };
+
+    private OnItemViewSelectedListener mOnItemViewSelectedListener =
+            new OnItemViewSelectedListener() {
+                @Override
+                public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
+                                           RowPresenter.ViewHolder rowViewHolder, Row row) {
+                    Log.d(TAG, "onItemSelected: " + item + " row " + row);
+                }
+            };
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        Log.i(TAG, "onCreate");
+        super.onCreate(savedInstanceState);
+
+        setBackgroundType(BACKGROUND_TYPE);
+        // setOnItemViewSelectedListener(mOnItemViewSelectedListener);
+
+        createComponents(getActivity());
+        setOnItemViewClickedListener(mOnItemViewClickedListener);
+    }
+
+    private void createComponents(Context context) {
+        mGlue = new PlaybackControlHelper(context) {
+            @Override
+            public int getUpdatePeriod() {
+                int totalTime = getControlsRow().getTotalTime();
+                if (getView() == null || getView().getWidth() == 0 || totalTime <= 0) {
+                    return 1000;
+                }
+                return Math.max(16, totalTime / getView().getWidth());
+            }
+
+            @Override
+            public void onActionClicked(Action action) {
+                if (action.getId() == R.id.lb_control_picture_in_picture) {
+                    getActivity().enterPictureInPictureMode();
+                    return;
+                }
+                super.onActionClicked(action);
+            }
+
+            @Override
+            protected void onCreateControlsRowAndPresenter() {
+                super.onCreateControlsRowAndPresenter();
+                getControlsRowPresenter().setSecondaryActionsHidden(SECONDARY_HIDDEN);
+            }
+        };
+
+        mGlue.setHost(new PlaybackSupportFragmentGlueHost(this));
+        //  mGlue.setOnI
+        mListRowPresenter = new ListRowPresenter();
+
+        setAdapter(new SparseArrayObjectAdapter(new PresenterSelector() {
+            @Override
+            public Presenter getPresenter(Object object) {
+                if (object instanceof PlaybackControlsRow) {
+                    return mGlue.getControlsRowPresenter();
+                } else if (object instanceof ListRow) {
+                    return mListRowPresenter;
+                }
+                throw new IllegalArgumentException("Unhandled object: " + object);
+            }
+        }));
+
+        // Add the controls row
+        getAdapter().set(ROW_CONTROLS, mGlue.getControlsRow());
+
+        // Add related content rows
+        for (int i = 0; i < RELATED_CONTENT_ROWS; ++i) {
+            ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(new StringPresenter());
+            listRowAdapter.add("Some related content");
+            listRowAdapter.add("Other related content");
+            HeaderItem header = new HeaderItem(i, "Row " + i);
+            getAdapter().set(ROW_CONTROLS + 1 + i, new ListRow(header, listRowAdapter));
+        }
+    }
+
+    public PlaybackControlGlue getGlue() {
+        return mGlue;
+    }
+
+    abstract static class PlaybackControlHelper extends PlaybackControlGlue {
+        /**
+         * Change the location of the thumbs up/down controls
+         */
+        private static final boolean THUMBS_PRIMARY = true;
+
+        private static final String FAUX_TITLE = "A short song of silence";
+        private static final String FAUX_SUBTITLE = "2014";
+        private static final int FAUX_DURATION = 33 * 1000;
+
+        // These should match the playback service FF behavior
+        private static int[] sFastForwardSpeeds = { 2, 3, 4, 5 };
+
+        private boolean mIsPlaying;
+        private int mSpeed = PLAYBACK_SPEED_PAUSED;
+        private long mStartTime;
+        private long mStartPosition = 0;
+
+        private PlaybackControlsRow.RepeatAction mRepeatAction;
+        private PlaybackControlsRow.ThumbsUpAction mThumbsUpAction;
+        private PlaybackControlsRow.ThumbsDownAction mThumbsDownAction;
+        private PlaybackControlsRow.PictureInPictureAction mPipAction;
+        private static Handler sProgressHandler = new Handler();
+
+        private final Runnable mUpdateProgressRunnable = new Runnable() {
+            @Override
+            public void run() {
+                updateProgress();
+                sProgressHandler.postDelayed(this, getUpdatePeriod());
+            }
+        };
+
+        PlaybackControlHelper(Context context) {
+            super(context, sFastForwardSpeeds);
+            mThumbsUpAction = new PlaybackControlsRow.ThumbsUpAction(context);
+            mThumbsUpAction.setIndex(PlaybackControlsRow.ThumbsUpAction.OUTLINE);
+            mThumbsDownAction = new PlaybackControlsRow.ThumbsDownAction(context);
+            mThumbsDownAction.setIndex(PlaybackControlsRow.ThumbsDownAction.OUTLINE);
+            mRepeatAction = new PlaybackControlsRow.RepeatAction(context);
+            mPipAction = new PlaybackControlsRow.PictureInPictureAction(context);
+        }
+
+        @Override
+        protected SparseArrayObjectAdapter createPrimaryActionsAdapter(
+                PresenterSelector presenterSelector) {
+            SparseArrayObjectAdapter adapter = new SparseArrayObjectAdapter(presenterSelector);
+            if (THUMBS_PRIMARY) {
+                adapter.set(PlaybackControlGlue.ACTION_CUSTOM_LEFT_FIRST, mThumbsUpAction);
+                adapter.set(PlaybackControlGlue.ACTION_CUSTOM_RIGHT_FIRST, mThumbsDownAction);
+            }
+            return adapter;
+        }
+
+        @Override
+        public void onActionClicked(Action action) {
+            if (shouldDispatchAction(action)) {
+                dispatchAction(action);
+                return;
+            }
+            super.onActionClicked(action);
+        }
+
+        @Override
+        public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
+            if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
+                Action action = getControlsRow().getActionForKeyCode(keyEvent.getKeyCode());
+                if (shouldDispatchAction(action)) {
+                    dispatchAction(action);
+                    return true;
+                }
+            }
+            return super.onKey(view, keyCode, keyEvent);
+        }
+
+        private boolean shouldDispatchAction(Action action) {
+            return action == mRepeatAction || action == mThumbsUpAction
+                    || action == mThumbsDownAction;
+        }
+
+        private void dispatchAction(Action action) {
+            Toast.makeText(getContext(), action.toString(), Toast.LENGTH_SHORT).show();
+            PlaybackControlsRow.MultiAction multiAction = (PlaybackControlsRow.MultiAction) action;
+            multiAction.nextIndex();
+            notifyActionChanged(multiAction);
+        }
+
+        private void notifyActionChanged(PlaybackControlsRow.MultiAction action) {
+            int index;
+            index = getPrimaryActionsAdapter().indexOf(action);
+            if (index >= 0) {
+                getPrimaryActionsAdapter().notifyArrayItemRangeChanged(index, 1);
+            } else {
+                index = getSecondaryActionsAdapter().indexOf(action);
+                if (index >= 0) {
+                    getSecondaryActionsAdapter().notifyArrayItemRangeChanged(index, 1);
+                }
+            }
+        }
+
+        private SparseArrayObjectAdapter getPrimaryActionsAdapter() {
+            return (SparseArrayObjectAdapter) getControlsRow().getPrimaryActionsAdapter();
+        }
+
+        private ArrayObjectAdapter getSecondaryActionsAdapter() {
+            return (ArrayObjectAdapter) getControlsRow().getSecondaryActionsAdapter();
+        }
+
+        @Override
+        public boolean hasValidMedia() {
+            return true;
+        }
+
+        @Override
+        public boolean isMediaPlaying() {
+            return mIsPlaying;
+        }
+
+        @Override
+        public CharSequence getMediaTitle() {
+            return FAUX_TITLE;
+        }
+
+        @Override
+        public CharSequence getMediaSubtitle() {
+            return FAUX_SUBTITLE;
+        }
+
+        @Override
+        public int getMediaDuration() {
+            return FAUX_DURATION;
+        }
+
+        @Override
+        public Drawable getMediaArt() {
+            return null;
+        }
+
+        @Override
+        public long getSupportedActions() {
+            return ACTION_PLAY_PAUSE | ACTION_FAST_FORWARD | ACTION_REWIND;
+        }
+
+        @Override
+        public int getCurrentSpeedId() {
+            return mSpeed;
+        }
+
+        @Override
+        public int getCurrentPosition() {
+            int speed;
+            if (mSpeed == PlaybackControlGlue.PLAYBACK_SPEED_PAUSED) {
+                speed = 0;
+            } else if (mSpeed == PlaybackControlGlue.PLAYBACK_SPEED_NORMAL) {
+                speed = 1;
+            } else if (mSpeed >= PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0) {
+                int index = mSpeed - PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0;
+                speed = getFastForwardSpeeds()[index];
+            } else if (mSpeed <= -PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0) {
+                int index = -mSpeed - PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0;
+                speed = -getRewindSpeeds()[index];
+            } else {
+                return -1;
+            }
+            long position = mStartPosition + (System.currentTimeMillis() - mStartTime) * speed;
+            if (position > getMediaDuration()) {
+                position = getMediaDuration();
+                onPlaybackComplete(true);
+            } else if (position < 0) {
+                position = 0;
+                onPlaybackComplete(false);
+            }
+            return (int) position;
+        }
+
+        void onPlaybackComplete(final boolean ended) {
+            sProgressHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    if (mRepeatAction.getIndex() == PlaybackControlsRow.RepeatAction.NONE) {
+                        pause();
+                    } else {
+                        play(PlaybackControlGlue.PLAYBACK_SPEED_NORMAL);
+                    }
+                    mStartPosition = 0;
+                    onStateChanged();
+                }
+            });
+        }
+
+        @Override
+        public void play(int speed) {
+            if (speed == mSpeed) {
+                return;
+            }
+            mStartPosition = getCurrentPosition();
+            mSpeed = speed;
+            mIsPlaying = true;
+            mStartTime = System.currentTimeMillis();
+        }
+
+        @Override
+        public void pause() {
+            if (mSpeed == PLAYBACK_SPEED_PAUSED) {
+                return;
+            }
+            mStartPosition = getCurrentPosition();
+            mSpeed = PLAYBACK_SPEED_PAUSED;
+            mIsPlaying = false;
+        }
+
+        @Override
+        public void next() {
+            // Not supported
+        }
+
+        @Override
+        public void previous() {
+            // Not supported
+        }
+
+        @Override
+        public void enableProgressUpdating(boolean enable) {
+            sProgressHandler.removeCallbacks(mUpdateProgressRunnable);
+            if (enable) {
+                mUpdateProgressRunnable.run();
+            }
+        }
+    }
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestActivity.java b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestActivity.java
new file mode 100644
index 0000000..ff840ec
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestActivity.java
@@ -0,0 +1,57 @@
+/*
+ * 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 android.app.Activity;
+import android.os.Bundle;
+import android.support.v17.leanback.test.R;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class PlaybackTestActivity extends Activity {
+    private List<PictureInPictureListener> mListeners = new ArrayList<>();
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.playback_controls);
+    }
+
+    @Override
+    public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
+        for (PictureInPictureListener listener : mListeners) {
+            listener.onPictureInPictureModeChanged(isInPictureInPictureMode);
+        }
+    }
+
+    public void registerPictureInPictureListener(PictureInPictureListener listener) {
+        mListeners.add(listener);
+    }
+
+    public void unregisterPictureInPictureListener(PictureInPictureListener listener) {
+        mListeners.remove(listener);
+    }
+
+    public interface PictureInPictureListener {
+        void onPictureInPictureModeChanged(boolean isInPictureInPictureMode);
+    }
+
+    public PlaybackTestFragment getPlaybackFragment() {
+        return (PlaybackTestFragment) getFragmentManager().findFragmentById(
+                R.id.playback_controls_fragment);
+    }
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestFragment.java b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestFragment.java
new file mode 100644
index 0000000..043d73e
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestFragment.java
@@ -0,0 +1,387 @@
+/*
+ * 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 android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.Handler;
+
+import android.support.v17.leanback.media.PlaybackControlGlue;
+import android.support.v17.leanback.test.R;
+import android.support.v17.leanback.widget.Action;
+import android.support.v17.leanback.widget.ArrayObjectAdapter;
+import android.support.v17.leanback.widget.HeaderItem;
+import android.support.v17.leanback.widget.ListRow;
+import android.support.v17.leanback.widget.ListRowPresenter;
+import android.support.v17.leanback.widget.OnItemViewClickedListener;
+import android.support.v17.leanback.widget.OnItemViewSelectedListener;
+import android.support.v17.leanback.widget.PlaybackControlsRow;
+import android.support.v17.leanback.widget.Presenter;
+import android.support.v17.leanback.widget.PresenterSelector;
+import android.support.v17.leanback.widget.Row;
+import android.support.v17.leanback.widget.RowPresenter;
+import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.widget.Toast;
+
+public class PlaybackTestFragment extends PlaybackFragment {
+    private static final String TAG = "PlaybackTestFragment";
+
+    /**
+     * Change this to choose a different overlay background.
+     */
+    private static final int BACKGROUND_TYPE = PlaybackFragment.BG_LIGHT;
+
+    private static final int ROW_CONTROLS = 0;
+
+    /**
+     * Change this to select hidden
+     */
+    private static final boolean SECONDARY_HIDDEN = false;
+
+    /**
+     * Change the number of related content rows.
+     */
+    private static final int RELATED_CONTENT_ROWS = 3;
+
+    private android.support.v17.leanback.media.PlaybackControlGlue mGlue;
+    private ListRowPresenter mListRowPresenter;
+
+    public SparseArrayObjectAdapter getAdapter() {
+        return (SparseArrayObjectAdapter) super.getAdapter();
+    }
+
+    private OnItemViewClickedListener mOnItemViewClickedListener = new OnItemViewClickedListener() {
+        @Override
+        public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
+                                  RowPresenter.ViewHolder rowViewHolder, Row row) {
+            Log.d(TAG, "onItemClicked: " + item + " row " + row);
+        }
+    };
+
+    private OnItemViewSelectedListener mOnItemViewSelectedListener =
+            new OnItemViewSelectedListener() {
+                @Override
+                public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
+                                           RowPresenter.ViewHolder rowViewHolder, Row row) {
+                    Log.d(TAG, "onItemSelected: " + item + " row " + row);
+                }
+            };
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        Log.i(TAG, "onCreate");
+        super.onCreate(savedInstanceState);
+
+        setBackgroundType(BACKGROUND_TYPE);
+       // setOnItemViewSelectedListener(mOnItemViewSelectedListener);
+
+        createComponents(getActivity());
+        setOnItemViewClickedListener(mOnItemViewClickedListener);
+    }
+
+    private void createComponents(Context context) {
+        mGlue = new PlaybackControlHelper(context) {
+            @Override
+            public int getUpdatePeriod() {
+                int totalTime = getControlsRow().getTotalTime();
+                if (getView() == null || getView().getWidth() == 0 || totalTime <= 0) {
+                    return 1000;
+                }
+                return Math.max(16, totalTime / getView().getWidth());
+            }
+
+            @Override
+            public void onActionClicked(Action action) {
+                if (action.getId() == R.id.lb_control_picture_in_picture) {
+                    getActivity().enterPictureInPictureMode();
+                    return;
+                }
+                super.onActionClicked(action);
+            }
+
+            @Override
+            protected void onCreateControlsRowAndPresenter() {
+                super.onCreateControlsRowAndPresenter();
+                getControlsRowPresenter().setSecondaryActionsHidden(SECONDARY_HIDDEN);
+            }
+        };
+
+        mGlue.setHost(new PlaybackFragmentGlueHost(this));
+       //  mGlue.setOnI
+        mListRowPresenter = new ListRowPresenter();
+
+        setAdapter(new SparseArrayObjectAdapter(new PresenterSelector() {
+            @Override
+            public Presenter getPresenter(Object object) {
+                if (object instanceof PlaybackControlsRow) {
+                    return mGlue.getControlsRowPresenter();
+                } else if (object instanceof ListRow) {
+                    return mListRowPresenter;
+                }
+                throw new IllegalArgumentException("Unhandled object: " + object);
+            }
+        }));
+
+        // Add the controls row
+        getAdapter().set(ROW_CONTROLS, mGlue.getControlsRow());
+
+        // Add related content rows
+        for (int i = 0; i < RELATED_CONTENT_ROWS; ++i) {
+            ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(new StringPresenter());
+            listRowAdapter.add("Some related content");
+            listRowAdapter.add("Other related content");
+            HeaderItem header = new HeaderItem(i, "Row " + i);
+            getAdapter().set(ROW_CONTROLS + 1 + i, new ListRow(header, listRowAdapter));
+        }
+    }
+
+    public PlaybackControlGlue getGlue() {
+        return mGlue;
+    }
+
+    abstract static class PlaybackControlHelper extends PlaybackControlGlue {
+        /**
+         * Change the location of the thumbs up/down controls
+         */
+        private static final boolean THUMBS_PRIMARY = true;
+
+        private static final String FAUX_TITLE = "A short song of silence";
+        private static final String FAUX_SUBTITLE = "2014";
+        private static final int FAUX_DURATION = 33 * 1000;
+
+        // These should match the playback service FF behavior
+        private static int[] sFastForwardSpeeds = { 2, 3, 4, 5 };
+
+        private boolean mIsPlaying;
+        private int mSpeed = PLAYBACK_SPEED_PAUSED;
+        private long mStartTime;
+        private long mStartPosition = 0;
+
+        private PlaybackControlsRow.RepeatAction mRepeatAction;
+        private PlaybackControlsRow.ThumbsUpAction mThumbsUpAction;
+        private PlaybackControlsRow.ThumbsDownAction mThumbsDownAction;
+        private PlaybackControlsRow.PictureInPictureAction mPipAction;
+        private static Handler sProgressHandler = new Handler();
+
+        private final Runnable mUpdateProgressRunnable = new Runnable() {
+            @Override
+            public void run() {
+                updateProgress();
+                sProgressHandler.postDelayed(this, getUpdatePeriod());
+            }
+        };
+
+        PlaybackControlHelper(Context context) {
+            super(context, sFastForwardSpeeds);
+            mThumbsUpAction = new PlaybackControlsRow.ThumbsUpAction(context);
+            mThumbsUpAction.setIndex(PlaybackControlsRow.ThumbsUpAction.OUTLINE);
+            mThumbsDownAction = new PlaybackControlsRow.ThumbsDownAction(context);
+            mThumbsDownAction.setIndex(PlaybackControlsRow.ThumbsDownAction.OUTLINE);
+            mRepeatAction = new PlaybackControlsRow.RepeatAction(context);
+            mPipAction = new PlaybackControlsRow.PictureInPictureAction(context);
+        }
+
+        @Override
+        protected SparseArrayObjectAdapter createPrimaryActionsAdapter(
+                PresenterSelector presenterSelector) {
+            SparseArrayObjectAdapter adapter = new SparseArrayObjectAdapter(presenterSelector);
+            if (THUMBS_PRIMARY) {
+                adapter.set(PlaybackControlGlue.ACTION_CUSTOM_LEFT_FIRST, mThumbsUpAction);
+                adapter.set(PlaybackControlGlue.ACTION_CUSTOM_RIGHT_FIRST, mThumbsDownAction);
+            }
+            return adapter;
+        }
+
+        @Override
+        public void onActionClicked(Action action) {
+            if (shouldDispatchAction(action)) {
+                dispatchAction(action);
+                return;
+            }
+            super.onActionClicked(action);
+        }
+
+        @Override
+        public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
+            if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
+                Action action = getControlsRow().getActionForKeyCode(keyEvent.getKeyCode());
+                if (shouldDispatchAction(action)) {
+                    dispatchAction(action);
+                    return true;
+                }
+            }
+            return super.onKey(view, keyCode, keyEvent);
+        }
+
+        private boolean shouldDispatchAction(Action action) {
+            return action == mRepeatAction || action == mThumbsUpAction
+                    || action == mThumbsDownAction;
+        }
+
+        private void dispatchAction(Action action) {
+            Toast.makeText(getContext(), action.toString(), Toast.LENGTH_SHORT).show();
+            PlaybackControlsRow.MultiAction multiAction = (PlaybackControlsRow.MultiAction) action;
+            multiAction.nextIndex();
+            notifyActionChanged(multiAction);
+        }
+
+        private void notifyActionChanged(PlaybackControlsRow.MultiAction action) {
+            int index;
+            index = getPrimaryActionsAdapter().indexOf(action);
+            if (index >= 0) {
+                getPrimaryActionsAdapter().notifyArrayItemRangeChanged(index, 1);
+            } else {
+                index = getSecondaryActionsAdapter().indexOf(action);
+                if (index >= 0) {
+                    getSecondaryActionsAdapter().notifyArrayItemRangeChanged(index, 1);
+                }
+            }
+        }
+
+        private SparseArrayObjectAdapter getPrimaryActionsAdapter() {
+            return (SparseArrayObjectAdapter) getControlsRow().getPrimaryActionsAdapter();
+        }
+
+        private ArrayObjectAdapter getSecondaryActionsAdapter() {
+            return (ArrayObjectAdapter) getControlsRow().getSecondaryActionsAdapter();
+        }
+
+        @Override
+        public boolean hasValidMedia() {
+            return true;
+        }
+
+        @Override
+        public boolean isMediaPlaying() {
+            return mIsPlaying;
+        }
+
+        @Override
+        public CharSequence getMediaTitle() {
+            return FAUX_TITLE;
+        }
+
+        @Override
+        public CharSequence getMediaSubtitle() {
+            return FAUX_SUBTITLE;
+        }
+
+        @Override
+        public int getMediaDuration() {
+            return FAUX_DURATION;
+        }
+
+        @Override
+        public Drawable getMediaArt() {
+            return null;
+        }
+
+        @Override
+        public long getSupportedActions() {
+            return ACTION_PLAY_PAUSE | ACTION_FAST_FORWARD | ACTION_REWIND;
+        }
+
+        @Override
+        public int getCurrentSpeedId() {
+            return mSpeed;
+        }
+
+        @Override
+        public int getCurrentPosition() {
+            int speed;
+            if (mSpeed == PlaybackControlGlue.PLAYBACK_SPEED_PAUSED) {
+                speed = 0;
+            } else if (mSpeed == PlaybackControlGlue.PLAYBACK_SPEED_NORMAL) {
+                speed = 1;
+            } else if (mSpeed >= PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0) {
+                int index = mSpeed - PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0;
+                speed = getFastForwardSpeeds()[index];
+            } else if (mSpeed <= -PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0) {
+                int index = -mSpeed - PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0;
+                speed = -getRewindSpeeds()[index];
+            } else {
+                return -1;
+            }
+            long position = mStartPosition + (System.currentTimeMillis() - mStartTime) * speed;
+            if (position > getMediaDuration()) {
+                position = getMediaDuration();
+                onPlaybackComplete(true);
+            } else if (position < 0) {
+                position = 0;
+                onPlaybackComplete(false);
+            }
+            return (int) position;
+        }
+
+        void onPlaybackComplete(final boolean ended) {
+            sProgressHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    if (mRepeatAction.getIndex() == PlaybackControlsRow.RepeatAction.NONE) {
+                        pause();
+                    } else {
+                        play(PlaybackControlGlue.PLAYBACK_SPEED_NORMAL);
+                    }
+                    mStartPosition = 0;
+                    onStateChanged();
+                }
+            });
+        }
+
+        @Override
+        public void play(int speed) {
+            if (speed == mSpeed) {
+                return;
+            }
+            mStartPosition = getCurrentPosition();
+            mSpeed = speed;
+            mIsPlaying = true;
+            mStartTime = System.currentTimeMillis();
+        }
+
+        @Override
+        public void pause() {
+            if (mSpeed == PLAYBACK_SPEED_PAUSED) {
+                return;
+            }
+            mStartPosition = getCurrentPosition();
+            mSpeed = PLAYBACK_SPEED_PAUSED;
+            mIsPlaying = false;
+        }
+
+        @Override
+        public void next() {
+            // Not supported
+        }
+
+        @Override
+        public void previous() {
+            // Not supported
+        }
+
+        @Override
+        public void enableProgressUpdating(boolean enable) {
+            sProgressHandler.removeCallbacks(mUpdateProgressRunnable);
+            if (enable) {
+                mUpdateProgressRunnable.run();
+            }
+        }
+    }
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/PresenterTest.java b/v17/leanback/tests/java/android/support/v17/leanback/widget/PresenterTest.java
index 8923d36..738c3e7 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/widget/PresenterTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/widget/PresenterTest.java
@@ -107,6 +107,30 @@
     }
 
     @Test
+    public void testRowHeaderPresenter() {
+        RowHeaderPresenter p = new RowHeaderPresenter();
+        p.setNullItemVisibilityGone(true);
+        RowHeaderPresenter.ViewHolder vh = (RowHeaderPresenter.ViewHolder)
+                p.onCreateViewHolder(new FrameLayout(mContext));
+        p.onBindViewHolder(vh, null);
+        assertEquals("Header visibility", View.GONE, vh.view.getVisibility());
+        p.onBindViewHolder(vh, new Row(null));
+        assertEquals("Header visibility", View.GONE, vh.view.getVisibility());
+
+        p.onBindViewHolder(vh, new Row(new HeaderItem("")));
+        assertEquals("Header visibility", View.VISIBLE, vh.view.getVisibility());
+        assertEquals("Header Description visibility", View.GONE,
+                vh.mDescriptionView.getVisibility());
+
+        HeaderItem item = new HeaderItem("");
+        item.setDescription("description");
+        p.onBindViewHolder(vh, new Row(item));
+        assertEquals("Header visibility", View.VISIBLE, vh.view.getVisibility());
+        assertEquals("Header Description visibility", View.VISIBLE,
+                vh.mDescriptionView.getVisibility());
+    }
+
+    @Test
     public void testPlaybackControlsRowPresenter() {
         Context context = new ContextThemeWrapper(mContext, R.style.Theme_Leanback);
         Presenter detailsPresenter = new AbstractDetailsDescriptionPresenter() {
diff --git a/v17/leanback/tests/res/layout/playback_controls.xml b/v17/leanback/tests/res/layout/playback_controls.xml
new file mode 100644
index 0000000..7f8910f
--- /dev/null
+++ b/v17/leanback/tests/res/layout/playback_controls.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     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.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+             android:layout_width="match_parent"
+             android:layout_height="match_parent" >
+
+    <fragment
+            android:id="@+id/playback_controls_fragment"
+            android:name="android.support.v17.leanback.app.PlaybackTestFragment"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent" />
+
+</FrameLayout>
\ No newline at end of file
diff --git a/v17/leanback/tests/res/layout/playback_support_controls.xml b/v17/leanback/tests/res/layout/playback_support_controls.xml
new file mode 100644
index 0000000..9e0e092
--- /dev/null
+++ b/v17/leanback/tests/res/layout/playback_support_controls.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     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.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+             android:layout_width="match_parent"
+             android:layout_height="match_parent" >
+
+    <fragment
+            android:id="@+id/playback_controls_fragment"
+            android:name="android.support.v17.leanback.app.PlaybackSupportTestFragment"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent" />
+
+</FrameLayout>
\ No newline at end of file
diff --git a/v17/preference-leanback/build.gradle b/v17/preference-leanback/build.gradle
index 9bfd0f3..e58fa8b 100644
--- a/v17/preference-leanback/build.gradle
+++ b/v17/preference-leanback/build.gradle
@@ -36,28 +36,11 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
 
diff --git a/v4/build.gradle b/v4/build.gradle
index f0226e3..f226a87 100644
--- a/v4/build.gradle
+++ b/v4/build.gradle
@@ -1,6 +1,6 @@
 apply plugin: 'com.android.library'
-
 archivesBaseName = 'support-v4'
+
 dependencies {
     compile project(':support-compat')
     compile project(':support-media-compat')
diff --git a/v7/appcompat/build.gradle b/v7/appcompat/build.gradle
index aa62632..4935085 100644
--- a/v7/appcompat/build.gradle
+++ b/v7/appcompat/build.gradle
@@ -1,5 +1,4 @@
 apply plugin: 'com.android.library'
-
 archivesBaseName = 'appcompat-v7'
 
 dependencies {
@@ -61,28 +60,11 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
 
diff --git a/v7/appcompat/tests/res/layout/toolbar_decor_content.xml b/v7/appcompat/tests/res/layout/toolbar_decor_content.xml
index db9935d..c271f68 100644
--- a/v7/appcompat/tests/res/layout/toolbar_decor_content.xml
+++ b/v7/appcompat/tests/res/layout/toolbar_decor_content.xml
@@ -29,11 +29,11 @@
             android:layout_width="match_parent"
             android:layout_height="match_parent">
 
-        <EditText
+        <View
                 android:id="@+id/editText"
                 android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:imeOptions="flagNoExtractUi"/>
+                android:layout_height="20dip"
+                android:focusable="true" />
 
     </android.support.v7.custom.FitWindowsContentLayout>
 
diff --git a/v7/cardview/build.gradle b/v7/cardview/build.gradle
index b88aad7..ce3f28d 100644
--- a/v7/cardview/build.gradle
+++ b/v7/cardview/build.gradle
@@ -38,28 +38,11 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
 
diff --git a/v7/gridlayout/build.gradle b/v7/gridlayout/build.gradle
index 542d5a7..56f320f 100644
--- a/v7/gridlayout/build.gradle
+++ b/v7/gridlayout/build.gradle
@@ -1,5 +1,4 @@
 apply plugin: 'com.android.library'
-
 archivesBaseName = 'gridlayout-v7'
 
 dependencies {
@@ -53,28 +52,11 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
 
diff --git a/v7/mediarouter/build.gradle b/v7/mediarouter/build.gradle
index 6b63089..7796565 100644
--- a/v7/mediarouter/build.gradle
+++ b/v7/mediarouter/build.gradle
@@ -4,6 +4,9 @@
 dependencies {
     compile project(":support-appcompat-v7")
     compile project(":support-palette-v7")
+
+    androidTestCompile ("com.android.support.test:runner:${project.rootProject.ext.testRunnerVersion}")
+    testCompile 'junit:junit:4.12'
 }
 
 android {
@@ -11,6 +14,8 @@
 
     defaultConfig {
         minSdkVersion 9
+
+        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
     }
 
     sourceSets {
@@ -23,6 +28,11 @@
                 'src'
         ]
         main.res.srcDir 'res'
+
+        androidTest.setRoot('tests')
+        androidTest.java.srcDir 'tests/src'
+        androidTest.res.srcDir 'tests/res'
+        androidTest.manifest.srcFile 'tests/AndroidManifest.xml'
     }
 
     compileOptions {
@@ -39,28 +49,11 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
 
diff --git a/v7/mediarouter/jellybean/android/support/v7/media/MediaRouterJellybean.java b/v7/mediarouter/jellybean/android/support/v7/media/MediaRouterJellybean.java
index 85071a4..7ade21d 100644
--- a/v7/mediarouter/jellybean/android/support/v7/media/MediaRouterJellybean.java
+++ b/v7/mediarouter/jellybean/android/support/v7/media/MediaRouterJellybean.java
@@ -121,9 +121,9 @@
             Field globalRouterField = routerObj.getClass().getDeclaredField("sStatic");
             globalRouterField.setAccessible(true);
             Object globalRouterObj = globalRouterField.get(null);
-            Method method = globalRouterObj.getClass().getDeclaredMethod("isBluetoothA2dpOn", null);
+            Method method = globalRouterObj.getClass().getDeclaredMethod("isBluetoothA2dpOn");
             method.setAccessible(true);
-            Object result = method.invoke(globalRouterObj, null);
+            Object result = method.invoke(globalRouterObj);
             return (Boolean) result;
         } catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException
                 | NoSuchMethodException | InvocationTargetException e) {
diff --git a/v7/mediarouter/src/android/support/v7/app/MediaRouteChooserDialog.java b/v7/mediarouter/src/android/support/v7/app/MediaRouteChooserDialog.java
index ac79f2d..2e5b983 100644
--- a/v7/mediarouter/src/android/support/v7/app/MediaRouteChooserDialog.java
+++ b/v7/mediarouter/src/android/support/v7/app/MediaRouteChooserDialog.java
@@ -378,12 +378,12 @@
         }
     }
 
-    private static final class RouteComparator implements Comparator<MediaRouter.RouteInfo> {
+    static final class RouteComparator implements Comparator<MediaRouter.RouteInfo> {
         public static final RouteComparator sInstance = new RouteComparator();
 
         @Override
         public int compare(MediaRouter.RouteInfo lhs, MediaRouter.RouteInfo rhs) {
-            return lhs.getName().compareTo(rhs.getName());
+            return lhs.getName().compareToIgnoreCase(rhs.getName());
         }
     }
 }
diff --git a/v7/mediarouter/tests/AndroidManifest.xml b/v7/mediarouter/tests/AndroidManifest.xml
new file mode 100644
index 0000000..87c4d65
--- /dev/null
+++ b/v7/mediarouter/tests/AndroidManifest.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+   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.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          xmlns:tools="http://schemas.android.com/tools"
+          package="android.support.v7.mediarouter.test">
+
+    <uses-sdk
+            android:minSdkVersion="9"
+            android:targetSdkVersion="23"
+            tools:overrideLibrary="android.support.test, android.app, android.support.test.rule,
+                      android.support.test.espresso, android.support.test.espresso.idling"/>
+
+    <application android:supportsRtl="true">
+
+        <uses-library android:name="android.test.runner"/>
+    </application>
+
+    <instrumentation
+            android:name="android.test.InstrumentationTestRunner"
+            android:targetPackage="android.support.v7.mediarouter.test"/>
+
+</manifest>
diff --git a/v7/mediarouter/tests/NO_DOCS b/v7/mediarouter/tests/NO_DOCS
new file mode 100644
index 0000000..092a39c
--- /dev/null
+++ b/v7/mediarouter/tests/NO_DOCS
@@ -0,0 +1,17 @@
+# 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.
+
+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.
diff --git a/v7/mediarouter/tests/src/android/support/v7/app/MediaRouteChooserDialogTest.java b/v7/mediarouter/tests/src/android/support/v7/app/MediaRouteChooserDialogTest.java
new file mode 100644
index 0000000..7a21cdb
--- /dev/null
+++ b/v7/mediarouter/tests/src/android/support/v7/app/MediaRouteChooserDialogTest.java
@@ -0,0 +1,68 @@
+/*
+ * 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.v7.app;
+
+import static org.junit.Assert.assertTrue;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v7.media.MediaRouter.RouteInfo;
+import android.support.v7.media.TestUtils;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class MediaRouteChooserDialogTest {
+    private MediaRouteChooserDialog.RouteComparator mComparator;
+
+    @Before
+    public void setup() {
+        mComparator = new MediaRouteChooserDialog.RouteComparator();
+    }
+
+    @Test
+    public void testRouteComparatorWithSameRouteName() {
+        RouteInfo routeInfo1 = TestUtils.createRouteInfo("ROUTE_ID_1", "ROUTE_NAME_1");
+        RouteInfo routeInfo2 = TestUtils.createRouteInfo("ROUTE_ID_2", "ROUTE_NAME_1");
+        int result = mComparator.compare(routeInfo1, routeInfo2);
+        assertTrue(result == 0);
+    }
+
+    @Test
+    public void testRouteComparatorWithCommonComparison() {
+        RouteInfo routeInfo1 = TestUtils.createRouteInfo("ROUTE_ID_1", "Route ABC");
+        RouteInfo routeInfo2 = TestUtils.createRouteInfo("ROUTE_ID_2", "Route XYZ");
+        int result = mComparator.compare(routeInfo1, routeInfo2);
+        assertTrue(result < 0);
+    }
+
+    @Test
+    public void testRouteComparatorWithCaseInsensitiveComparison() {
+        RouteInfo routeInfo1 = TestUtils.createRouteInfo("ROUTE_ID_1", "living room abc");
+        RouteInfo routeInfo2 = TestUtils.createRouteInfo("ROUTE_ID_2", "LIVING ROOM XYZ");
+        int result1 = mComparator.compare(routeInfo1, routeInfo2);
+        assertTrue(result1 < 0);
+
+        RouteInfo routeInfo3 = TestUtils.createRouteInfo("ROUTE_ID_3", "LIVING ROOM ABC");
+        RouteInfo routeInfo4 = TestUtils.createRouteInfo("ROUTE_ID_4", "living room xyz");
+        int result2 = mComparator.compare(routeInfo3, routeInfo4);
+        assertTrue(result2 < 0);
+    }
+}
diff --git a/v7/mediarouter/tests/src/android/support/v7/media/TestUtils.java b/v7/mediarouter/tests/src/android/support/v7/media/TestUtils.java
new file mode 100644
index 0000000..f92a4d2
--- /dev/null
+++ b/v7/mediarouter/tests/src/android/support/v7/media/TestUtils.java
@@ -0,0 +1,26 @@
+/*
+ * 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.v7.media;
+
+public class TestUtils {
+    public static MediaRouter.RouteInfo createRouteInfo(String id, String name) {
+        MediaRouteDescriptor descriptor = new MediaRouteDescriptor.Builder(id, name).build();
+        MediaRouter.RouteInfo routeInfo = new MediaRouter.RouteInfo(null, null, null);
+        routeInfo.updateDescriptor(descriptor);
+        return routeInfo;
+    }
+}
diff --git a/v7/palette/build.gradle b/v7/palette/build.gradle
index 2494370..686fe71 100644
--- a/v7/palette/build.gradle
+++ b/v7/palette/build.gradle
@@ -1,5 +1,4 @@
 apply plugin: 'com.android.library'
-
 archivesBaseName = 'palette-v7'
 
 dependencies {
@@ -34,28 +33,11 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
 
diff --git a/v7/preference/build.gradle b/v7/preference/build.gradle
index a63cc9e..b5fd656 100644
--- a/v7/preference/build.gradle
+++ b/v7/preference/build.gradle
@@ -15,7 +15,6 @@
  */
 
 apply plugin: 'com.android.library'
-
 archivesBaseName = 'preference-v7'
 
 dependencies {
@@ -80,28 +79,11 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
 
diff --git a/v7/recyclerview/build.gradle b/v7/recyclerview/build.gradle
index 11a098e..e262ec7 100644
--- a/v7/recyclerview/build.gradle
+++ b/v7/recyclerview/build.gradle
@@ -1,5 +1,4 @@
 apply plugin: 'com.android.library'
-
 archivesBaseName = 'recyclerview-v7'
 
 dependencies {
@@ -47,10 +46,6 @@
         targetCompatibility JavaVersion.VERSION_1_7
     }
 
-    packagingOptions {
-        exclude 'LICENSE.txt'
-    }
-
     testOptions {
         unitTests.returnDefaultValues = true
     }
@@ -68,28 +63,11 @@
     }
     def suffix = name.capitalize()
 
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn variant.javaCompile
-        from variant.javaCompile.destinationDir
-        from 'LICENSE.txt'
-    }
-    def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
-        source android.sourceSets.main.java
-        classpath = files(variant.javaCompile.classpath.files) + files(
-                "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
-    }
-
-    def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
-        classifier = 'javadoc'
-        from 'build/docs/javadoc'
-    }
-
     def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
         classifier = 'sources'
         from android.sourceSets.main.java.srcDirs
     }
 
-    artifacts.add('archives', javadocJarTask);
     artifacts.add('archives', sourcesJarTask);
 }
 
diff --git a/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java b/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java
index 9297522..78a31b4 100644
--- a/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java
+++ b/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java
@@ -413,14 +413,22 @@
     }
 
     /**
-     * <p>Returns the amount of extra space that should be laid out by LayoutManager.
-     * By default, {@link android.support.v7.widget.LinearLayoutManager} lays out 1 extra page of
-     * items while smooth scrolling and 0 otherwise. You can override this method to implement your
-     * custom layout pre-cache logic.</p>
-     * <p>Laying out invisible elements will eventually come with performance cost. On the other
-     * hand, in places like smooth scrolling to an unknown location, this extra content helps
-     * LayoutManager to calculate a much smoother scrolling; which improves user experience.</p>
-     * <p>You can also use this if you are trying to pre-layout your upcoming views.</p>
+     * <p>Returns the amount of extra space that should be laid out by LayoutManager.</p>
+     *
+     * <p>By default, {@link android.support.v7.widget.LinearLayoutManager} lays out 1 extra page
+     * of items while smooth scrolling and 0 otherwise. You can override this method to implement
+     * your custom layout pre-cache logic.</p>
+     *
+     * <p><strong>Note:</strong>Laying out invisible elements generally comes with significant
+     * performance cost. It's typically only desirable in places like smooth scrolling to an unknown
+     * location, where 1) the extra content helps LinearLayoutManager know in advance when its
+     * target is approaching, so it can decelerate early and smoothly and 2) while motion is
+     * continuous.</p>
+     *
+     * <p>Extending the extra layout space is especially expensive if done while the user may change
+     * scrolling direction. Changing direction will cause the extra layout space to swap to the
+     * opposite side of the viewport, incurring many rebinds/recycles, unless the cache is large
+     * enough to handle it.</p>
      *
      * @return The extra space that should be laid out (in pixels).
      */
diff --git a/v7/recyclerview/src/android/support/v7/widget/StaggeredGridLayoutManager.java b/v7/recyclerview/src/android/support/v7/widget/StaggeredGridLayoutManager.java
index adbe4dd..19f31f8 100644
--- a/v7/recyclerview/src/android/support/v7/widget/StaggeredGridLayoutManager.java
+++ b/v7/recyclerview/src/android/support/v7/widget/StaggeredGridLayoutManager.java
@@ -2097,15 +2097,23 @@
         if (mPrefetchDistances == null || mPrefetchDistances.length < mSpanCount) {
             mPrefetchDistances = new int[mSpanCount];
         }
+
+        int itemPrefetchCount = 0;
         for (int i = 0; i < mSpanCount; i++) {
-            mPrefetchDistances[i] = mLayoutState.mItemDirection == LAYOUT_START
+            // compute number of pixels past the edge of the viewport that the current span extends
+            int distance = mLayoutState.mItemDirection == LAYOUT_START
                     ? mLayoutState.mStartLine - mSpans[i].getStartLine(mLayoutState.mStartLine)
                     : mSpans[i].getEndLine(mLayoutState.mEndLine) - mLayoutState.mEndLine;
+            if (distance >= 0) {
+                // span extends to the edge, so prefetch next item
+                mPrefetchDistances[itemPrefetchCount] = distance;
+                itemPrefetchCount++;
+            }
         }
-        Arrays.sort(mPrefetchDistances, 0, mSpanCount);
+        Arrays.sort(mPrefetchDistances, 0, itemPrefetchCount);
 
         // then assign them in order to the next N views (where N = span count)
-        for (int i = 0; i < mSpanCount && mLayoutState.hasMore(state); i++) {
+        for (int i = 0; i < itemPrefetchCount && mLayoutState.hasMore(state); i++) {
             layoutPrefetchRegistry.addPosition(mLayoutState.mCurrentPosition, mPrefetchDistances[i]);
             mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
         }
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewCacheTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewCacheTest.java
index cc39a87..7ab616f 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewCacheTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewCacheTest.java
@@ -515,6 +515,50 @@
     }
 
     @Test
+    public void prefetchStaggeredPastBoundary() {
+        StaggeredGridLayoutManager sglm =
+                new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
+        mRecyclerView.setLayoutManager(sglm);
+        mRecyclerView.setAdapter(new RecyclerView.Adapter() {
+            @Override
+            public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+                return new RecyclerView.ViewHolder(new View(getContext())) {};
+            }
+
+            @Override
+            public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
+                holder.itemView.setMinimumWidth(100);
+                holder.itemView.setMinimumHeight(position == 0 ? 100 : 200);
+            }
+
+            @Override
+            public int getItemCount() {
+                return 2;
+            }
+        });
+
+        layout(200, 100);
+        mRecyclerView.scrollBy(0, 50);
+
+        /* Each row is 50 pixels:
+         * ------------- *
+         *___0___|___1___*
+         *   0   |   1   *
+         *_______|___1___*
+         *       |   1   *
+         */
+        assertEquals(2, mRecyclerView.getChildCount());
+        assertEquals(0, sglm.getFirstChildPosition());
+        assertEquals(1, sglm.getLastChildPosition());
+
+        // prefetch upward gets nothing
+        CacheUtils.verifyPositionsPrefetched(mRecyclerView, 0, -10);
+
+        // prefetch downward gets nothing (and doesn't crash...)
+        CacheUtils.verifyPositionsPrefetched(mRecyclerView, 0, 10);
+    }
+
+    @Test
     public void prefetchItemsSkipAnimations() {
         LinearLayoutManager llm = new LinearLayoutManager(getContext());
         mRecyclerView.setLayoutManager(llm);