Merge "Support nested prefetch in leanbacklib's gridviews" into nyc-support-25.2-dev
diff --git a/api/current.txt b/api/current.txt
index b9896a7..38fb0b6 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -2612,6 +2612,7 @@
 
   public static abstract class PlaybackGlueHost.HostCallback {
     ctor public PlaybackGlueHost.HostCallback();
+    method public void onHostDestroy();
     method public void onHostPause();
     method public void onHostResume();
     method public void onHostStart();
@@ -4267,6 +4268,18 @@
     method public void setStaticLabels(java.lang.CharSequence[]);
   }
 
+  public class TimePicker extends android.support.v17.leanback.widget.picker.Picker {
+    ctor public TimePicker(android.content.Context, android.util.AttributeSet);
+    ctor public TimePicker(android.content.Context, android.util.AttributeSet, int);
+    method public int getHour();
+    method public int getMinute();
+    method public boolean is24Hour();
+    method public boolean isPm();
+    method public void setHour(int);
+    method public void setIs24Hour(boolean);
+    method public void setMinute(int);
+  }
+
 }
 
 package android.support.v17.preference {
@@ -5690,6 +5703,7 @@
     method public android.content.ComponentName getServiceComponent();
     method public android.support.v4.media.session.MediaSessionCompat.Token getSessionToken();
     method public boolean isConnected();
+    method public void search(java.lang.String, android.os.Bundle, android.support.v4.media.MediaBrowserCompat.SearchCallback);
     method public void subscribe(java.lang.String, android.support.v4.media.MediaBrowserCompat.SubscriptionCallback);
     method public void subscribe(java.lang.String, android.os.Bundle, android.support.v4.media.MediaBrowserCompat.SubscriptionCallback);
     method public void unsubscribe(java.lang.String);
@@ -5727,6 +5741,12 @@
     field public static final int FLAG_PLAYABLE = 2; // 0x2
   }
 
+  public static abstract class MediaBrowserCompat.SearchCallback {
+    ctor public MediaBrowserCompat.SearchCallback();
+    method public void onError(java.lang.String, android.os.Bundle);
+    method public void onSearchResult(java.lang.String, android.os.Bundle, java.util.List<android.support.v4.media.MediaBrowserCompat.MediaItem>);
+  }
+
   public static abstract class MediaBrowserCompat.SubscriptionCallback {
     ctor public MediaBrowserCompat.SubscriptionCallback();
     method public void onChildrenLoaded(java.lang.String, java.util.List<android.support.v4.media.MediaBrowserCompat.MediaItem>);
@@ -5747,6 +5767,7 @@
     method public abstract void onLoadChildren(java.lang.String, android.support.v4.media.MediaBrowserServiceCompat.Result<java.util.List<android.support.v4.media.MediaBrowserCompat.MediaItem>>);
     method public void onLoadChildren(java.lang.String, android.support.v4.media.MediaBrowserServiceCompat.Result<java.util.List<android.support.v4.media.MediaBrowserCompat.MediaItem>>, android.os.Bundle);
     method public void onLoadItem(java.lang.String, android.support.v4.media.MediaBrowserServiceCompat.Result<android.support.v4.media.MediaBrowserCompat.MediaItem>);
+    method public void onSearch(java.lang.String, android.os.Bundle, android.support.v4.media.MediaBrowserServiceCompat.Result<java.util.List<android.support.v4.media.MediaBrowserCompat.MediaItem>>);
     method public void setSessionToken(android.support.v4.media.session.MediaSessionCompat.Token);
     field public static final java.lang.String SERVICE_INTERFACE = "android.media.browse.MediaBrowserService";
   }
@@ -5758,7 +5779,7 @@
     field public static final java.lang.String EXTRA_OFFLINE = "android.service.media.extra.OFFLINE";
     field public static final java.lang.String EXTRA_RECENT = "android.service.media.extra.RECENT";
     field public static final java.lang.String EXTRA_SUGGESTED = "android.service.media.extra.SUGGESTED";
-    field public static final java.lang.String EXTRA_SUGGESTION_KEYWORDS = "android.service.media.extra.SUGGESTION_KEYWORDS";
+    field public static final deprecated java.lang.String EXTRA_SUGGESTION_KEYWORDS = "android.service.media.extra.SUGGESTION_KEYWORDS";
   }
 
   public static class MediaBrowserServiceCompat.Result<T> {
diff --git a/build.gradle b/build.gradle
index f690dcb..6e593d7 100644
--- a/build.gradle
+++ b/build.gradle
@@ -41,10 +41,11 @@
 }
 
 ext.supportVersion = '25.3.0-SNAPSHOT'
-ext.extraVersion = 43
+ext.extraVersion = 44
 ext.supportRepoOut = ''
 ext.buildNumber = Integer.toString(ext.extraVersion)
 
+ext.buildToolsVersion = '25.0.0'
 ext.testRunnerVersion = '0.6-alpha'
 ext.espressoVersion = '2.3-alpha'
 
@@ -52,19 +53,20 @@
 // required for the doclava dependency.
 ext.usePrebuilts = "true"
 
+// Prevent the Android Gradle plug-in from automatically downloading SDK dependencies.
+ext['android.builder.sdkDownload'] = false
+
 final String platform = OperatingSystem.current().isMacOsX() ? 'darwin' : 'linux'
 System.setProperty('android.dir', "${rootDir}/../../")
 final String fullSdkPath = "${rootDir}/../../prebuilts/fullsdk-${platform}"
 if (file(fullSdkPath).exists()) {
     gradle.ext.currentSdk = 25
-    ext.buildToolsVersion = '24.0.1'
     project.ext.androidJar = files("${fullSdkPath}/platforms/android-${gradle.ext.currentSdk}/android.jar")
     System.setProperty('android.home', "${rootDir}/../../prebuilts/fullsdk-${platform}")
     File props = file("local.properties")
     props.write "sdk.dir=${fullSdkPath}"
 } else {
     gradle.ext.currentSdk = 'current'
-    ext.buildToolsVersion = '24.0.1'
     project.ext.androidJar = files("${project.rootDir}/../../prebuilts/sdk/current/android.jar")
     File props = file("local.properties")
     props.write "android.dir=../../"
@@ -209,6 +211,9 @@
 
 // Generates online docs.
 task generateDocs(type: DoclavaTask, dependsOn: configurations.doclava) {
+    group = JavaBasePlugin.DOCUMENTATION_GROUP
+    description = 'Generates d.android.com style documentation.'
+
     docletpath = configurations.doclava.resolve()
     destinationDir = new File(project.docsDir, "online")
 
@@ -264,6 +269,9 @@
 
 // Copies generated API files to current version.
 task updateApi(type: UpdateApiTask, dependsOn: generateApi) {
+    group JavaBasePlugin.VERIFICATION_GROUP
+    description 'Invoke Doclava\'s ApiCheck tool to update current.txt based on current changes.'
+
     newApiFile = new File(project.docsDir, 'release/current.txt')
     oldApiFile = new File(project.rootDir, 'api/current.txt')
     newRemovedApiFile = new File(project.docsDir, 'release/removed.txt')
@@ -434,7 +442,7 @@
     }
 
     project.afterEvaluate {
-        // The archivesBaseName isn't available intially, so set it now
+        // The archivesBaseName isn't available initially, so set it now
         def createZipTask = project.tasks.findByName("createSeparateZip")
         if (createZipTask != null) {
             createZipTask.appendix = archivesBaseName
diff --git a/buildSrc/src/main/groovy/android/support/checkapi/CheckApiTask.groovy b/buildSrc/src/main/groovy/android/support/checkapi/CheckApiTask.groovy
index 3578958..c245f77 100644
--- a/buildSrc/src/main/groovy/android/support/checkapi/CheckApiTask.groovy
+++ b/buildSrc/src/main/groovy/android/support/checkapi/CheckApiTask.groovy
@@ -198,6 +198,7 @@
 
     public CheckApiTask() {
         group = 'Verification'
+        description = 'Invoke Doclava\'s ApiCheck tool to make sure current.txt is up to date.'
     }
 
     private Set<File> collectAndVerifyInputs() {
diff --git a/customtabs/tests/src/android/support/customtabs/PostMessageTest.java b/customtabs/tests/src/android/support/customtabs/PostMessageTest.java
index b212693..d20a06d 100644
--- a/customtabs/tests/src/android/support/customtabs/PostMessageTest.java
+++ b/customtabs/tests/src/android/support/customtabs/PostMessageTest.java
@@ -36,6 +36,7 @@
 import org.junit.runner.RunWith;
 
 import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 
 /**
@@ -55,13 +56,14 @@
     private Context mContext;
     private CustomTabsServiceConnection mCustomTabsServiceConnection;
     private PostMessageServiceConnection mPostMessageServiceConnection;
-    private boolean mCustomTabsServiceConnected;
+    private AtomicBoolean mCustomTabsServiceConnected;
     private boolean mPostMessageServiceConnected;
     private CustomTabsSession mSession;
 
     public PostMessageTest() {
         mActivityTestRule = new ActivityTestRule<TestActivity>(TestActivity.class);
         mServiceRule = new ServiceTestRule();
+        mCustomTabsServiceConnected = new AtomicBoolean(false);
     }
 
 
@@ -89,13 +91,13 @@
         mCustomTabsServiceConnection = new CustomTabsServiceConnection() {
             @Override
             public void onCustomTabsServiceConnected(ComponentName name, CustomTabsClient client) {
-                mCustomTabsServiceConnected = true;
                 mSession = client.newSession(mCallback);
+                mCustomTabsServiceConnected.set(true);
             }
 
             @Override
             public void onServiceDisconnected(ComponentName componentName) {
-                mCustomTabsServiceConnected = false;
+                mCustomTabsServiceConnected.set(false);
             }
         };
         mPostMessageServiceConnection = new PostMessageServiceConnection(
@@ -126,10 +128,10 @@
         PollingCheck.waitFor(500, new PollingCheck.PollingCheckCondition() {
             @Override
             public boolean canProceed() {
-                return mCustomTabsServiceConnected;
+                return mCustomTabsServiceConnected.get();
             }
         });
-        assertTrue(mCustomTabsServiceConnected);
+        assertTrue(mCustomTabsServiceConnected.get());
         assertTrue(mSession.requestPostMessageChannel(Uri.EMPTY));
         assertEquals(CustomTabsService.RESULT_SUCCESS, mSession.postMessage("", null));
         PollingCheck.waitFor(500, new PollingCheck.PollingCheckCondition() {
diff --git a/design/src/android/support/design/internal/BottomNavigationMenuView.java b/design/src/android/support/design/internal/BottomNavigationMenuView.java
index d80cd64..5d7c19d 100644
--- a/design/src/android/support/design/internal/BottomNavigationMenuView.java
+++ b/design/src/android/support/design/internal/BottomNavigationMenuView.java
@@ -46,8 +46,7 @@
     private final int mItemHeight;
     private final OnClickListener mOnClickListener;
     private final BottomNavigationAnimationHelperBase mAnimationHelper;
-    private static final Pools.Pool<BottomNavigationItemView> sItemPool =
-            new Pools.SynchronizedPool<>(5);
+    private final Pools.Pool<BottomNavigationItemView> mItemPool = new Pools.SynchronizedPool<>(5);
 
     private boolean mShiftingMode = true;
 
@@ -250,12 +249,12 @@
     }
 
     public void buildMenuView() {
+        removeAllViews();
         if (mButtons != null) {
             for (BottomNavigationItemView item : mButtons) {
-                sItemPool.release(item);
+                mItemPool.release(item);
             }
         }
-        removeAllViews();
         if (mMenu.size() == 0) {
             mSelectedItemId = 0;
             mSelectedItemPosition = 0;
@@ -307,7 +306,7 @@
     }
 
     private BottomNavigationItemView getNewItem() {
-        BottomNavigationItemView item = sItemPool.acquire();
+        BottomNavigationItemView item = mItemPool.acquire();
         if (item == null) {
             item = new BottomNavigationItemView(getContext());
         }
diff --git a/design/tests/src/android/support/design/widget/AppBarWithCollapsingToolbarTest.java b/design/tests/src/android/support/design/widget/AppBarWithCollapsingToolbarTest.java
index 2db3471..aeca0be 100644
--- a/design/tests/src/android/support/design/widget/AppBarWithCollapsingToolbarTest.java
+++ b/design/tests/src/android/support/design/widget/AppBarWithCollapsingToolbarTest.java
@@ -24,14 +24,18 @@
 import android.os.Build;
 import android.os.SystemClock;
 import android.support.design.test.R;
+import android.support.test.filters.FlakyTest;
 import android.support.test.filters.LargeTest;
 import android.support.test.filters.SdkSuppress;
+import android.support.test.filters.Suppress;
 import android.widget.ImageView;
 
 import org.junit.Test;
 
 @LargeTest
 public class AppBarWithCollapsingToolbarTest extends AppBarLayoutBaseTest {
+    @Suppress
+    @FlakyTest(bugId = 30701044)
     @Test
     public void testPinnedToolbar() throws Throwable {
         configureContent(R.layout.design_appbar_toolbar_collapse_pin,
@@ -137,6 +141,8 @@
         assertScrimAlpha(0);
     }
 
+    @Suppress
+    @FlakyTest(bugId = 30701044)
     @Test
     public void testScrollingToolbar() throws Throwable {
         configureContent(R.layout.design_appbar_toolbar_collapse_scroll,
@@ -247,6 +253,8 @@
         assertScrimAlpha(0);
     }
 
+    @Suppress
+    @FlakyTest(bugId = 30701044)
     @Test
     public void testScrollingToolbarEnterAlways() throws Throwable {
         configureContent(R.layout.design_appbar_toolbar_collapse_scroll_enteralways,
@@ -352,6 +360,8 @@
         assertScrimAlpha(0);
     }
 
+    @Suppress
+    @FlakyTest(bugId = 30701044)
     @Test
     public void testPinnedToolbarAndAnchoredFab() throws Throwable {
         configureContent(R.layout.design_appbar_toolbar_collapse_pin_with_fab,
@@ -418,6 +428,8 @@
         }
     }
 
+    @Suppress
+    @FlakyTest(bugId = 30701044)
     @Test
     public void testPinnedToolbarAndParallaxImage() throws Throwable {
         configureContent(R.layout.design_appbar_toolbar_collapse_with_image,
@@ -521,6 +533,8 @@
      * inherits from) has an issue with measuring children with margins when run on earlier
      * versions of the platform.
      */
+    @Suppress
+    @FlakyTest(bugId = 30701044)
     @Test
     @SdkSuppress(minSdkVersion = 11)
     public void testPinnedToolbarWithMargins() throws Throwable {
diff --git a/fragment/java/android/support/v4/app/FragmentActivity.java b/fragment/java/android/support/v4/app/FragmentActivity.java
index 0b38fd4..78e5370 100644
--- a/fragment/java/android/support/v4/app/FragmentActivity.java
+++ b/fragment/java/android/support/v4/app/FragmentActivity.java
@@ -113,8 +113,8 @@
 
     boolean mCreated;
     boolean mResumed;
-    boolean mStopped;
-    boolean mReallyStopped;
+    boolean mStopped = true;
+    boolean mReallyStopped = true;
     boolean mRetaining;
 
     boolean mOptionsMenuInvalidated;
diff --git a/fragment/tests/java/android/support/v4/app/FragmentLifecycleTest.java b/fragment/tests/java/android/support/v4/app/FragmentLifecycleTest.java
index 4c0c0a4..3bac6e4 100644
--- a/fragment/tests/java/android/support/v4/app/FragmentLifecycleTest.java
+++ b/fragment/tests/java/android/support/v4/app/FragmentLifecycleTest.java
@@ -32,17 +32,21 @@
 
 import android.content.Context;
 import android.content.Intent;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.Parcelable;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.fragment.test.R;
+import android.support.test.InstrumentationRegistry;
 import android.support.test.annotation.UiThreadTest;
 import android.support.test.filters.MediumTest;
+import android.support.test.filters.SdkSuppress;
 import android.support.test.rule.ActivityTestRule;
 import android.support.test.runner.AndroidJUnit4;
 import android.support.v4.app.FragmentManager.FragmentLifecycleCallbacks;
 import android.support.v4.app.test.EmptyFragmentTestActivity;
+import android.support.v4.app.test.FragmentTestActivity;
 import android.support.v4.view.ViewCompat;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -57,6 +61,7 @@
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
+import java.util.concurrent.TimeUnit;
 
 @RunWith(AndroidJUnit4.class)
 @MediumTest
@@ -586,6 +591,22 @@
         assertFalse(fragment1.mCalledOnResume);
     }
 
+    /**
+     * FragmentActivity should not raise the state of a Fragment while it is being destroyed.
+     */
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN_MR1)
+    @Test
+    public void fragmentActivityFinishEarly() throws Throwable {
+        Intent intent = new Intent(mActivityRule.getActivity(), FragmentTestActivity.class);
+        intent.putExtra("finishEarly", true);
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+        FragmentTestActivity activity = (FragmentTestActivity)
+                InstrumentationRegistry.getInstrumentation().startActivitySync(intent);
+
+        assertTrue(activity.onDestroyLatch.await(1000, TimeUnit.MILLISECONDS));
+    }
+
     private void assertAnimationsMatch(FragmentManager fm, int enter, int exit, int popEnter,
             int popExit) {
         FragmentManagerImpl fmImpl = (FragmentManagerImpl) fm;
diff --git a/fragment/tests/java/android/support/v4/app/FragmentTestUtil.java b/fragment/tests/java/android/support/v4/app/FragmentTestUtil.java
index ba5875a..8d948ae 100644
--- a/fragment/tests/java/android/support/v4/app/FragmentTestUtil.java
+++ b/fragment/tests/java/android/support/v4/app/FragmentTestUtil.java
@@ -35,14 +35,17 @@
         }
     };
 
-    public static void waitForExecution(final ActivityTestRule<FragmentTestActivity> rule) {
+    public static void waitForExecution(final ActivityTestRule<? extends FragmentActivity> rule) {
         // Wait for two cycles. When starting a postponed transition, it will post to
         // the UI thread and then the execution will be added onto the queue after that.
         // The two-cycle wait makes sure fragments have the opportunity to complete both
         // before returning.
-        Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
-        instrumentation.runOnMainSync(DO_NOTHING);
-        instrumentation.runOnMainSync(DO_NOTHING);
+        try {
+            rule.runOnUiThread(DO_NOTHING);
+            rule.runOnUiThread(DO_NOTHING);
+        } catch (Throwable throwable) {
+            throw new RuntimeException(throwable);
+        }
     }
 
     public static boolean executePendingTransactions(
diff --git a/fragment/tests/java/android/support/v4/app/test/FragmentTestActivity.java b/fragment/tests/java/android/support/v4/app/test/FragmentTestActivity.java
index 5d13fe7..ef8cd0a 100644
--- a/fragment/tests/java/android/support/v4/app/test/FragmentTestActivity.java
+++ b/fragment/tests/java/android/support/v4/app/test/FragmentTestActivity.java
@@ -15,12 +15,15 @@
  */
 package android.support.v4.app.test;
 
+import static org.junit.Assert.assertFalse;
+
 import android.app.Activity;
 import android.content.Context;
 import android.content.Intent;
 import android.os.Build.VERSION;
 import android.os.Build.VERSION_CODES;
 import android.os.Bundle;
+import android.support.annotation.Nullable;
 import android.support.fragment.test.R;
 import android.support.v4.app.Fragment;
 import android.support.v4.app.FragmentActivity;
@@ -38,10 +41,25 @@
  * A simple activity used for Fragment Transitions and lifecycle event ordering
  */
 public class FragmentTestActivity extends FragmentActivity {
+    public final CountDownLatch onDestroyLatch = new CountDownLatch(1);
+
     @Override
     public void onCreate(Bundle icicle) {
         super.onCreate(icicle);
         setContentView(R.layout.activity_content);
+        Intent intent = getIntent();
+        if (intent != null && intent.getBooleanExtra("finishEarly", false)) {
+            finish();
+            getSupportFragmentManager().beginTransaction()
+                    .add(new AssertNotDestroyed(), "not destroyed")
+                    .commit();
+        }
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        onDestroyLatch.countDown();
     }
 
     public static class TestFragment extends Fragment {
@@ -258,4 +276,14 @@
             onActivityResultResultCode = resultCode;
         }
     }
+
+    public static class AssertNotDestroyed extends Fragment {
+        @Override
+        public void onActivityCreated(@Nullable Bundle savedInstanceState) {
+            super.onActivityCreated(savedInstanceState);
+            if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) {
+                assertFalse(getActivity().isDestroyed());
+            }
+        }
+    }
 }
diff --git a/media-compat/java/android/support/v4/media/MediaBrowserCompat.java b/media-compat/java/android/support/v4/media/MediaBrowserCompat.java
index 243f016..214c815 100644
--- a/media-compat/java/android/support/v4/media/MediaBrowserCompat.java
+++ b/media-compat/java/android/support/v4/media/MediaBrowserCompat.java
@@ -22,6 +22,7 @@
 import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_GET_MEDIA_ITEM;
 import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_REGISTER_CALLBACK_MESSENGER;
 import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_REMOVE_SUBSCRIPTION;
+import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_SEARCH;
 import static android.support.v4.media.MediaBrowserProtocol
         .CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER;
 import static android.support.v4.media.MediaBrowserProtocol.CLIENT_VERSION_CURRENT;
@@ -33,6 +34,8 @@
 import static android.support.v4.media.MediaBrowserProtocol.DATA_PACKAGE_NAME;
 import static android.support.v4.media.MediaBrowserProtocol.DATA_RESULT_RECEIVER;
 import static android.support.v4.media.MediaBrowserProtocol.DATA_ROOT_HINTS;
+import static android.support.v4.media.MediaBrowserProtocol.DATA_SEARCH_EXTRAS;
+import static android.support.v4.media.MediaBrowserProtocol.DATA_SEARCH_QUERY;
 import static android.support.v4.media.MediaBrowserProtocol.EXTRA_CLIENT_VERSION;
 import static android.support.v4.media.MediaBrowserProtocol.EXTRA_MESSENGER_BINDER;
 import static android.support.v4.media.MediaBrowserProtocol.SERVICE_MSG_ON_CONNECT;
@@ -325,7 +328,30 @@
     }
 
     /**
-     * A class with information on a single media item for use in browsing media.
+     * Searches {@link MediaItem media items} from the connected service. Not all services may
+     * support this, and {@link SearchCallback#onError} will be called if not implemented.
+     *
+     * @param query The search query that contains keywords separated by space. Should not be an
+     *            empty string.
+     * @param extras The bundle of service-specific arguments to send to the media browser service.
+     *            The contents of this bundle may affect the search result.
+     * @param callback The callback to receive the search result. Must be non-null.
+     */
+    public void search(@NonNull final String query, final Bundle extras,
+            @NonNull SearchCallback callback) {
+        if (TextUtils.isEmpty(query)) {
+            throw new IllegalArgumentException("query cannot be empty");
+        }
+        if (callback == null) {
+            throw new IllegalArgumentException("callback cannot be null");
+        }
+        mImpl.search(query, extras, callback);
+    }
+
+    /**
+     * A class with information on a single media item for use in browsing/searching media.
+     * MediaItems are application dependent so we cannot guarantee that they contain the
+     * right values.
      */
     public static class MediaItem implements Parcelable {
         private final int mFlags;
@@ -486,7 +512,7 @@
          * Returns the media id in the {@link MediaDescriptionCompat} for this item.
          * @see MediaMetadataCompat#METADATA_KEY_MEDIA_ID
          */
-        public @NonNull String getMediaId() {
+        public @Nullable String getMediaId() {
             return mDescription.getMediaId();
         }
     }
@@ -765,6 +791,32 @@
         }
     }
 
+    /**
+     * Callback for receiving the result of {@link #search}.
+     */
+    public abstract static class SearchCallback {
+        /**
+         * Called when the {@link #search} finished successfully.
+         *
+         * @param query The search query sent for the search request to the connected service.
+         * @param extras The bundle of service-specific arguments sent to the connected service.
+         * @param items The list of media items which contains the search result.
+         */
+        public void onSearchResult(@NonNull String query, Bundle extras,
+                @NonNull List<MediaItem> items) {
+        }
+
+        /**
+         * Called when an error happens while {@link #search} or the connected service doesn't
+         * support {@link #search}.
+         *
+         * @param query The search query sent for the search request to the connected service.
+         * @param extras The bundle of service-specific arguments sent to the connected service.
+         */
+        public void onError(@NonNull String query, Bundle extras) {
+        }
+    }
+
     interface MediaBrowserImpl {
         void connect();
         void disconnect();
@@ -777,6 +829,7 @@
                 @NonNull SubscriptionCallback callback);
         void unsubscribe(@NonNull String parentId, SubscriptionCallback callback);
         void getItem(final @NonNull String mediaId, @NonNull final ItemCallback cb);
+        void search(@NonNull String query, Bundle extras, @NonNull SearchCallback callback);
     }
 
     interface MediaBrowserServiceCallbackImpl {
@@ -1070,6 +1123,34 @@
         }
 
         @Override
+        public void search(@NonNull final String query, final Bundle extras,
+                @NonNull final SearchCallback callback) {
+            if (!isConnected()) {
+                Log.i(TAG, "Not connected, unable to search.");
+                mHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        callback.onError(query, extras);
+                    }
+                });
+                return;
+            }
+
+            ResultReceiver receiver = new SearchResultReceiver(query, extras, callback, mHandler);
+            try {
+                mServiceBinderWrapper.search(query, extras, receiver, mCallbacksMessenger);
+            } catch (RemoteException e) {
+                Log.i(TAG, "Remote error searching items with query: " + query, e);
+                mHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        callback.onError(query, extras);
+                    }
+                });
+            }
+        }
+
+        @Override
         public void onServiceConnected(final Messenger callback, final String root,
                 final MediaSessionCompat.Token session, final Bundle extra) {
             // Check to make sure there hasn't been a disconnect or a different ServiceConnection.
@@ -1537,6 +1618,45 @@
         }
 
         @Override
+        public void search(@NonNull final String query, final Bundle extras,
+                @NonNull final SearchCallback callback) {
+            if (!isConnected()) {
+                Log.i(TAG, "Not connected, unable to search.");
+                mHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        callback.onError(query, extras);
+                    }
+                });
+                return;
+            }
+            if (mServiceBinderWrapper == null) {
+                Log.i(TAG, "The connected service doesn't support search.");
+                mHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        // Default framework implementation.
+                        callback.onError(query, extras);
+                    }
+                });
+                return;
+            }
+
+            ResultReceiver receiver = new SearchResultReceiver(query, extras, callback, mHandler);
+            try {
+                mServiceBinderWrapper.search(query, extras, receiver, mCallbacksMessenger);
+            } catch (RemoteException e) {
+                Log.i(TAG, "Remote error searching items with query: " + query, e);
+                mHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        callback.onError(query, extras);
+                    }
+                });
+            }
+        }
+
+        @Override
         public void onConnected() {
             Bundle extras = MediaBrowserCompatApi21.getExtras(mBrowserObj);
             if (extras == null) {
@@ -1804,6 +1924,15 @@
             sendRequest(CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER, null, callbackMessenger);
         }
 
+        void search(String query, Bundle extras, ResultReceiver receiver,
+                Messenger callbacksMessenger) throws RemoteException {
+            Bundle data = new Bundle();
+            data.putString(DATA_SEARCH_QUERY, query);
+            data.putBundle(DATA_SEARCH_EXTRAS, extras);
+            data.putParcelable(DATA_RESULT_RECEIVER, receiver);
+            sendRequest(CLIENT_MSG_SEARCH, data, callbacksMessenger);
+        }
+
         private void sendRequest(int what, Bundle data, Messenger cbMessenger)
                 throws RemoteException {
             Message msg = Message.obtain();
@@ -1830,7 +1959,7 @@
             if (resultData != null) {
                 resultData.setClassLoader(MediaBrowserCompat.class.getClassLoader());
             }
-            if (resultCode != 0 || resultData == null
+            if (resultCode != MediaBrowserServiceCompat.RESULT_OK || resultData == null
                     || !resultData.containsKey(MediaBrowserServiceCompat.KEY_MEDIA_ITEM)) {
                 mCallback.onError(mMediaId);
                 return;
@@ -1843,4 +1972,37 @@
             }
         }
     }
+
+    private static class SearchResultReceiver extends ResultReceiver {
+        private final String mQuery;
+        private final Bundle mExtras;
+        private final SearchCallback mCallback;
+
+        SearchResultReceiver(String query, Bundle extras, SearchCallback callback,
+                Handler handler) {
+            super(handler);
+            mQuery = query;
+            mExtras = extras;
+            mCallback = callback;
+        }
+
+        @Override
+        protected void onReceiveResult(int resultCode, Bundle resultData) {
+            if (resultCode != MediaBrowserServiceCompat.RESULT_OK || resultData == null
+                    || !resultData.containsKey(MediaBrowserServiceCompat.KEY_SEARCH_RESULTS)) {
+                mCallback.onError(mQuery, mExtras);
+                return;
+            }
+            Parcelable[] items = resultData.getParcelableArray(
+                    MediaBrowserServiceCompat.KEY_SEARCH_RESULTS);
+            List<MediaItem> results = null;
+            if (items != null) {
+                results = new ArrayList<>();
+                for (Parcelable item : items) {
+                    results.add((MediaItem) item);
+                }
+            }
+            mCallback.onSearchResult(mQuery, mExtras, results);
+        }
+    }
 }
diff --git a/media-compat/java/android/support/v4/media/MediaBrowserProtocol.java b/media-compat/java/android/support/v4/media/MediaBrowserProtocol.java
index d2d12a7..2401d09 100644
--- a/media-compat/java/android/support/v4/media/MediaBrowserProtocol.java
+++ b/media-compat/java/android/support/v4/media/MediaBrowserProtocol.java
@@ -29,6 +29,8 @@
     public static final String DATA_PACKAGE_NAME = "data_package_name";
     public static final String DATA_RESULT_RECEIVER = "data_result_receiver";
     public static final String DATA_ROOT_HINTS = "data_root_hints";
+    public static final String DATA_SEARCH_EXTRAS = "data_search_extras";
+    public static final String DATA_SEARCH_QUERY = "data_search_query";
 
     public static final String EXTRA_CLIENT_VERSION = "extra_client_version";
     public static final String EXTRA_SERVICE_VERSION = "extra_service_version";
@@ -156,4 +158,16 @@
      * - replyTo : Callback messenger
      */
     public static final int CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER = 7;
+
+    /** (client v1)
+     * Sent to retrieve a specific media item from the connected service.
+     * - arg1 : The client version
+     * - data
+     *     DATA_SEARCH_QUERY : A string for search query that contains keywords separated by space.
+     *     DATA_SEARCH_EXTRAS : A bundle of service-specific arguments to send to the media browser
+     *                          service.
+     *     DATA_RESULT_RECEIVER : Result receiver to get the result
+     * - replyTo : Callback messenger
+     */
+    public static final int CLIENT_MSG_SEARCH = 8;
 }
diff --git a/media-compat/java/android/support/v4/media/MediaBrowserServiceCompat.java b/media-compat/java/android/support/v4/media/MediaBrowserServiceCompat.java
index 22c3d55..9c65ce6 100644
--- a/media-compat/java/android/support/v4/media/MediaBrowserServiceCompat.java
+++ b/media-compat/java/android/support/v4/media/MediaBrowserServiceCompat.java
@@ -23,6 +23,7 @@
 import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_GET_MEDIA_ITEM;
 import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_REGISTER_CALLBACK_MESSENGER;
 import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_REMOVE_SUBSCRIPTION;
+import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_SEARCH;
 import static android.support.v4.media.MediaBrowserProtocol.CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER;
 import static android.support.v4.media.MediaBrowserProtocol.DATA_CALLBACK_TOKEN;
 import static android.support.v4.media.MediaBrowserProtocol.DATA_CALLING_UID;
@@ -33,6 +34,8 @@
 import static android.support.v4.media.MediaBrowserProtocol.DATA_PACKAGE_NAME;
 import static android.support.v4.media.MediaBrowserProtocol.DATA_RESULT_RECEIVER;
 import static android.support.v4.media.MediaBrowserProtocol.DATA_ROOT_HINTS;
+import static android.support.v4.media.MediaBrowserProtocol.DATA_SEARCH_EXTRAS;
+import static android.support.v4.media.MediaBrowserProtocol.DATA_SEARCH_QUERY;
 import static android.support.v4.media.MediaBrowserProtocol.EXTRA_CLIENT_VERSION;
 import static android.support.v4.media.MediaBrowserProtocol.EXTRA_MESSENGER_BINDER;
 import static android.support.v4.media.MediaBrowserProtocol.EXTRA_SERVICE_VERSION;
@@ -116,14 +119,26 @@
     @RestrictTo(LIBRARY_GROUP)
     public static final String KEY_MEDIA_ITEM = "media_item";
 
-    static final int RESULT_FLAG_OPTION_NOT_HANDLED = 0x00000001;
-    static final int RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED = 0x00000002;
+    /**
+     * A key for passing the list of MediaItems to the ResultReceiver in search.
+     *
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public static final String KEY_SEARCH_RESULTS = "search_results";
+
+    static final int RESULT_FLAG_OPTION_NOT_HANDLED = 1 << 0;
+    static final int RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED = 1 << 1;
+    static final int RESULT_FLAG_ON_SEARCH_NOT_IMPLEMENTED = 1 << 2;
+
+    static final int RESULT_ERROR = -1;
+    static final int RESULT_OK = 0;
 
     /** @hide */
     @RestrictTo(LIBRARY_GROUP)
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(flag=true, value = { RESULT_FLAG_OPTION_NOT_HANDLED,
-            RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED })
+            RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED, RESULT_FLAG_ON_SEARCH_NOT_IMPLEMENTED })
     private @interface ResultFlags { }
 
     final ArrayMap<IBinder, ConnectionRecord> mConnections = new ArrayMap<>();
@@ -454,6 +469,12 @@
                 case CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER:
                     mServiceBinderImpl.unregisterCallbacks(new ServiceCallbacksCompat(msg.replyTo));
                     break;
+                case CLIENT_MSG_SEARCH:
+                    mServiceBinderImpl.search(data.getString(DATA_SEARCH_QUERY),
+                            data.getBundle(DATA_SEARCH_EXTRAS),
+                            (ResultReceiver) data.getParcelable(DATA_RESULT_RECEIVER),
+                            new ServiceCallbacksCompat(msg.replyTo));
+                    break;
                 default:
                     Log.w(TAG, "Unhandled message: " + msg
                             + "\n  Service version: " + SERVICE_VERSION_CURRENT
@@ -720,6 +741,27 @@
                 }
             });
         }
+
+        public void search(final String query, final Bundle extras, final ResultReceiver receiver,
+                final ServiceCallbacks callbacks) {
+            if (TextUtils.isEmpty(query) || receiver == null) {
+                return;
+            }
+
+            mHandler.postOrRun(new Runnable() {
+                @Override
+                public void run() {
+                    final IBinder b = callbacks.asBinder();
+
+                    ConnectionRecord connection = mConnections.get(b);
+                    if (connection == null) {
+                        Log.w(TAG, "search for callback that isn't registered query=" + query);
+                        return;
+                    }
+                    performSearch(query, extras, connection, receiver);
+                }
+            });
+        }
     }
 
     private interface ServiceCallbacks {
@@ -829,7 +871,6 @@
      * @see BrowserRoot#EXTRA_RECENT
      * @see BrowserRoot#EXTRA_OFFLINE
      * @see BrowserRoot#EXTRA_SUGGESTED
-     * @see BrowserRoot#EXTRA_SUGGESTION_KEYWORDS
      */
     public abstract @Nullable BrowserRoot onGetRoot(@NonNull String clientPackageName,
             int clientUid, @Nullable Bundle rootHints);
@@ -907,12 +948,39 @@
      * @param result The Result to send the item to, or null if the id is
      *            invalid.
      */
-    public void onLoadItem(String itemId, Result<MediaBrowserCompat.MediaItem> result) {
+    public void onLoadItem(String itemId, @NonNull Result<MediaBrowserCompat.MediaItem> result) {
         result.setFlags(RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED);
         result.sendResult(null);
     }
 
     /**
+     * Called to get the search result.
+     * <p>
+     * Implementations must call {@link Result#sendResult result.sendResult}. If the search will be
+     * an expensive operation {@link Result#detach result.detach} may be called before returning
+     * from this function, and then {@link Result#sendResult result.sendResult} called when the
+     * search has been completed.
+     * </p><p>
+     * In case there are no search results, call {@link Result#sendResult result.sendResult} with an
+     * empty list. In case there are some errors happened, call {@link Result#sendResult
+     * result.sendResult} with {@code null}, which will invoke {@link
+     * MediaBrowserCompat.SearchCallback#onError}.
+     * </p><p>
+     * The default implementation will invoke {@link MediaBrowserCompat.SearchCallback#onError}.
+     * </p>
+     *
+     * @param query The search query sent from the media browser. It contains keywords separated
+     *            by space.
+     * @param extras The bundle of service-specific arguments sent from the media browser.
+     * @param result The {@link Result} to send the search result.
+     */
+    public void onSearch(@NonNull String query, Bundle extras,
+            @NonNull Result<List<MediaBrowserCompat.MediaItem>> result) {
+        result.setFlags(RESULT_FLAG_ON_SEARCH_NOT_IMPLEMENTED);
+        result.sendResult(null);
+    }
+
+    /**
      * Call to set the media session.
      * <p>
      * This should be called as soon as possible during the service's startup.
@@ -953,7 +1021,6 @@
      * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_RECENT
      * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_OFFLINE
      * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_SUGGESTED
-     * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_SUGGESTION_KEYWORDS
      */
     public final Bundle getBrowserRootHints() {
         return mImpl.getBrowserRootHints();
@@ -1133,12 +1200,12 @@
                     @Override
                     void onResultSent(MediaBrowserCompat.MediaItem item, @ResultFlags int flags) {
                         if ((flags & RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED) != 0) {
-                            receiver.send(-1, null);
+                            receiver.send(RESULT_ERROR, null);
                             return;
                         }
                         Bundle bundle = new Bundle();
                         bundle.putParcelable(KEY_MEDIA_ITEM, item);
-                        receiver.send(0, bundle);
+                        receiver.send(RESULT_OK, bundle);
                     }
                 };
 
@@ -1152,6 +1219,34 @@
         }
     }
 
+    void performSearch(final String query, Bundle extras, ConnectionRecord connection,
+            final ResultReceiver receiver) {
+        final Result<List<MediaBrowserCompat.MediaItem>> result =
+                new Result<List<MediaBrowserCompat.MediaItem>>(query) {
+            @Override
+            void onResultSent(List<MediaBrowserCompat.MediaItem> items, @ResultFlags int flag) {
+                if ((flag & RESULT_FLAG_ON_SEARCH_NOT_IMPLEMENTED) != 0
+                        || items == null) {
+                    receiver.send(RESULT_ERROR, null);
+                    return;
+                }
+                Bundle bundle = new Bundle();
+                bundle.putParcelableArray(KEY_SEARCH_RESULTS,
+                        items.toArray(new MediaBrowserCompat.MediaItem[0]));
+                receiver.send(RESULT_OK, bundle);
+            }
+        };
+
+        mCurConnection = connection;
+        onSearch(query, extras, result);
+        mCurConnection = null;
+
+        if (!result.isDone()) {
+            throw new IllegalStateException("onSearch must call detach() or sendResult()"
+                    + " before returning for query=" + query);
+        }
+    }
+
     /**
      * Contains information that the browser service needs to send to the client
      * when first connected.
@@ -1170,7 +1265,6 @@
          *
          * @see #EXTRA_OFFLINE
          * @see #EXTRA_SUGGESTED
-         * @see #EXTRA_SUGGESTION_KEYWORDS
          */
         public static final String EXTRA_RECENT = "android.service.media.extra.RECENT";
 
@@ -1188,7 +1282,6 @@
          *
          * @see #EXTRA_RECENT
          * @see #EXTRA_SUGGESTED
-         * @see #EXTRA_SUGGESTION_KEYWORDS
          */
         public static final String EXTRA_OFFLINE = "android.service.media.extra.OFFLINE";
 
@@ -1207,7 +1300,6 @@
          *
          * @see #EXTRA_RECENT
          * @see #EXTRA_OFFLINE
-         * @see #EXTRA_SUGGESTION_KEYWORDS
          */
         public static final String EXTRA_SUGGESTED = "android.service.media.extra.SUGGESTED";
 
@@ -1228,7 +1320,10 @@
          * @see #EXTRA_RECENT
          * @see #EXTRA_OFFLINE
          * @see #EXTRA_SUGGESTED
+         * @deprecated Use {@link MediaBrowserCompat#search(String, Bundle,
+         *             MediaBrowserCompat.SearchCallback)} instead.
          */
+        @Deprecated
         public static final String EXTRA_SUGGESTION_KEYWORDS
                 = "android.service.media.extra.SUGGESTION_KEYWORDS";
 
diff --git a/media-compat/tests/src/android/support/v4/media/MediaBrowserServiceCompatTest.java b/media-compat/tests/src/android/support/v4/media/MediaBrowserServiceCompatTest.java
index 85c64ea..7e436ec 100644
--- a/media-compat/tests/src/android/support/v4/media/MediaBrowserServiceCompatTest.java
+++ b/media-compat/tests/src/android/support/v4/media/MediaBrowserServiceCompatTest.java
@@ -20,12 +20,15 @@
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertNull;
 import static junit.framework.Assert.assertTrue;
 
 import android.content.ComponentName;
 import android.os.Bundle;
+import android.support.test.filters.LargeTest;
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.media.MediaBrowserCompat.MediaItem;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -49,6 +52,7 @@
     private final ConnectionCallback mConnectionCallback = new ConnectionCallback();
     private final SubscriptionCallback mSubscriptionCallback = new SubscriptionCallback();
     private final ItemCallback mItemCallback = new ItemCallback();
+    private final SearchCallback mSearchCallback = new SearchCallback();
 
     private MediaBrowserCompat mMediaBrowser;
     private StubMediaBrowserServiceCompat mMediaBrowserService;
@@ -122,7 +126,7 @@
     }
 
     @Test
-    @SmallTest
+    @LargeTest
     public void testDelayedNotifyChildrenChanged() throws Exception {
         synchronized (mWaitLock) {
             mSubscriptionCallback.reset();
@@ -167,6 +171,47 @@
 
     @Test
     @SmallTest
+    public void testSearch() throws Exception {
+        final String key = "test-key";
+        final String val = "test-val";
+
+        synchronized (mWaitLock) {
+            mSearchCallback.reset();
+            mMediaBrowser.search(StubMediaBrowserServiceCompat.SEARCH_QUERY_FOR_NO_RESULT, null,
+                    mSearchCallback);
+            mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
+            assertTrue(mSearchCallback.mOnSearchResult);
+            assertTrue(mSearchCallback.mSearchResults != null
+                    && mSearchCallback.mSearchResults.size() == 0);
+            assertEquals(null, mSearchCallback.mSearchExtras);
+
+            mSearchCallback.reset();
+            mMediaBrowser.search(StubMediaBrowserServiceCompat.SEARCH_QUERY_FOR_ERROR, null,
+                    mSearchCallback);
+            mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
+            assertTrue(mSearchCallback.mOnSearchResult);
+            assertNull(mSearchCallback.mSearchResults);
+            assertEquals(null, mSearchCallback.mSearchExtras);
+
+            mSearchCallback.reset();
+            Bundle extras = new Bundle();
+            extras.putString(key, val);
+            mMediaBrowser.search(StubMediaBrowserServiceCompat.SEARCH_QUERY, extras,
+                    mSearchCallback);
+            mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
+            assertTrue(mSearchCallback.mOnSearchResult);
+            assertNotNull(mSearchCallback.mSearchResults);
+            for (MediaItem item : mSearchCallback.mSearchResults) {
+                assertNotNull(item.getMediaId());
+                assertTrue(item.getMediaId().contains(StubMediaBrowserServiceCompat.SEARCH_QUERY));
+            }
+            assertNotNull(mSearchCallback.mSearchExtras);
+            assertEquals(val, mSearchCallback.mSearchExtras.getString(key));
+        }
+    }
+
+    @Test
+    @SmallTest
     public void testBrowserRoot() {
         final String id = "test-id";
         final String key = "test-key";
@@ -180,7 +225,7 @@
         assertEquals(val, browserRoot.getExtras().getString(key));
     }
 
-    private void assertRootHints(MediaBrowserCompat.MediaItem item) {
+    private void assertRootHints(MediaItem item) {
         Bundle rootHints = item.getDescription().getExtras();
         assertNotNull(rootHints);
         assertEquals(mRootHints.getBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_RECENT),
@@ -206,11 +251,11 @@
         boolean mOnChildrenLoadedWithOptions;
 
         @Override
-        public void onChildrenLoaded(String parentId, List<MediaBrowserCompat.MediaItem> children) {
+        public void onChildrenLoaded(String parentId, List<MediaItem> children) {
             synchronized (mWaitLock) {
                 mOnChildrenLoaded = true;
                 if (children != null) {
-                    for (MediaBrowserCompat.MediaItem item : children) {
+                    for (MediaItem item : children) {
                         assertRootHints(item);
                     }
                 }
@@ -219,12 +264,11 @@
         }
 
         @Override
-        public void onChildrenLoaded(String parentId, List<MediaBrowserCompat.MediaItem> children,
-                Bundle options) {
+        public void onChildrenLoaded(String parentId, List<MediaItem> children, Bundle options) {
             synchronized (mWaitLock) {
                 mOnChildrenLoadedWithOptions = true;
                 if (children != null) {
-                    for (MediaBrowserCompat.MediaItem item : children) {
+                    for (MediaItem item : children) {
                         assertRootHints(item);
                     }
                 }
@@ -242,7 +286,7 @@
         boolean mOnItemLoaded;
 
         @Override
-        public void onItemLoaded(MediaBrowserCompat.MediaItem item) {
+        public void onItemLoaded(MediaItem item) {
             synchronized (mWaitLock) {
                 mOnItemLoaded = true;
                 assertRootHints(item);
@@ -254,4 +298,36 @@
             mOnItemLoaded = false;
         }
     }
+
+    private class SearchCallback extends MediaBrowserCompat.SearchCallback {
+        boolean mOnSearchResult;
+        Bundle mSearchExtras;
+        List<MediaItem> mSearchResults;
+
+        @Override
+        public void onSearchResult(String query, Bundle extras, List<MediaItem> items) {
+            synchronized (mWaitLock) {
+                mOnSearchResult = true;
+                mSearchResults = items;
+                mSearchExtras = extras;
+                mWaitLock.notify();
+            }
+        }
+
+        @Override
+        public void onError(String query, Bundle extras) {
+            synchronized (mWaitLock) {
+                mOnSearchResult = true;
+                mSearchResults = null;
+                mSearchExtras = extras;
+                mWaitLock.notify();
+            }
+        }
+
+        public void reset() {
+            mOnSearchResult = false;
+            mSearchExtras = null;
+            mSearchResults = null;
+        }
+    }
 }
diff --git a/media-compat/tests/src/android/support/v4/media/StubMediaBrowserServiceCompat.java b/media-compat/tests/src/android/support/v4/media/StubMediaBrowserServiceCompat.java
index 95b4a2b..3995200 100644
--- a/media-compat/tests/src/android/support/v4/media/StubMediaBrowserServiceCompat.java
+++ b/media-compat/tests/src/android/support/v4/media/StubMediaBrowserServiceCompat.java
@@ -43,6 +43,10 @@
             MEDIA_ID_CHILDREN_DELAYED
     };
 
+    static final String SEARCH_QUERY = "test_media_children";
+    static final String SEARCH_QUERY_FOR_NO_RESULT = "query no result";
+    static final String SEARCH_QUERY_FOR_ERROR = "query for error";
+
     static StubMediaBrowserServiceCompat sInstance;
 
     /* package private */ static MediaSessionCompat sSession;
@@ -72,8 +76,7 @@
         if (MEDIA_ID_ROOT.equals(parentMediaId)) {
             Bundle rootHints = getBrowserRootHints();
             for (String id : MEDIA_ID_CHILDREN) {
-                mediaItems.add(new MediaItem(new MediaDescriptionCompat.Builder()
-                        .setMediaId(id).setExtras(rootHints).build(), MediaItem.FLAG_BROWSABLE));
+                mediaItems.add(createMediaItem(id));
             }
             result.sendResult(mediaItems);
         } else if (MEDIA_ID_CHILDREN_DELAYED.equals(parentMediaId)) {
@@ -97,9 +100,7 @@
 
         for (String id : MEDIA_ID_CHILDREN) {
             if (id.equals(itemId)) {
-                result.sendResult(new MediaItem(new MediaDescriptionCompat.Builder()
-                        .setMediaId(id).setExtras(getBrowserRootHints()).build(),
-                        MediaItem.FLAG_BROWSABLE));
+                result.sendResult(createMediaItem(id));
                 return;
             }
         }
@@ -107,6 +108,23 @@
         super.onLoadItem(itemId, result);
     }
 
+    @Override
+    public void onSearch(String query, Bundle extras, Result<List<MediaItem>> result) {
+        if (SEARCH_QUERY_FOR_NO_RESULT.equals(query)) {
+            result.sendResult(Collections.<MediaItem>emptyList());
+        } else if (SEARCH_QUERY_FOR_ERROR.equals(query)) {
+            result.sendResult(null);
+        } else if (SEARCH_QUERY.equals(query)) {
+            List<MediaItem> items = new ArrayList<>();
+            for (String id : MEDIA_ID_CHILDREN) {
+                if (id.contains(query)) {
+                    items.add(createMediaItem(id));
+                }
+            }
+            result.sendResult(items);
+        }
+    }
+
     public void sendDelayedNotifyChildrenChanged() {
         if (mPendingLoadChildrenResult != null) {
             mPendingLoadChildrenResult.sendResult(Collections.<MediaItem>emptyList());
@@ -124,4 +142,10 @@
             mPendingLoadItemResult = null;
         }
     }
+
+    private MediaItem createMediaItem(String id) {
+        return new MediaItem(new MediaDescriptionCompat.Builder()
+                .setMediaId(id).setExtras(getBrowserRootHints()).build(),
+                MediaItem.FLAG_BROWSABLE);
+    }
 }
diff --git a/percent/tests/java/android/support/percent/PercentRelativeRtlTest.java b/percent/tests/java/android/support/percent/PercentRelativeRtlTest.java
index 81ff253..bbfc461 100644
--- a/percent/tests/java/android/support/percent/PercentRelativeRtlTest.java
+++ b/percent/tests/java/android/support/percent/PercentRelativeRtlTest.java
@@ -19,8 +19,6 @@
 import static android.support.test.espresso.Espresso.onView;
 import static android.support.test.espresso.matcher.ViewMatchers.withId;
 
-import static org.junit.Assume.assumeTrue;
-
 import android.os.Build;
 import android.support.percent.test.R;
 import android.support.test.filters.SmallTest;
@@ -108,8 +106,6 @@
 
     @Before
     public void setUp() throws Exception {
-        assumeTrue(Build.VERSION.SDK_INT != 17);
-
         final TestRelativeRtlActivity activity = mActivityTestRule.getActivity();
         mPercentRelativeLayout = (PercentRelativeLayout) activity.findViewById(R.id.container);
         mContainerWidth = mPercentRelativeLayout.getWidth();
@@ -132,6 +128,9 @@
 
     @Test
     public void testTopChild() {
+        if (Build.VERSION.SDK_INT == 17) {
+            return;
+        }
         final View childToTest = mPercentRelativeLayout.findViewById(R.id.child_top);
 
         if (Build.VERSION.SDK_INT >= 17) {
@@ -161,6 +160,9 @@
 
     @Test
     public void testStartChild() {
+        if (Build.VERSION.SDK_INT == 17) {
+            return;
+        }
         final View childToTest = mPercentRelativeLayout.findViewById(R.id.child_start);
 
         if (Build.VERSION.SDK_INT >= 17) {
@@ -191,6 +193,9 @@
 
     @Test
     public void testBottomChild() {
+        if (Build.VERSION.SDK_INT == 17) {
+            return;
+        }
         final View childToTest = mPercentRelativeLayout.findViewById(R.id.child_bottom);
 
         if (Build.VERSION.SDK_INT >= 17) {
@@ -222,6 +227,9 @@
 
     @Test
     public void testEndChild() {
+        if (Build.VERSION.SDK_INT == 17) {
+            return;
+        }
         final View childToTest = mPercentRelativeLayout.findViewById(R.id.child_end);
 
         if (Build.VERSION.SDK_INT >= 17) {
@@ -252,6 +260,9 @@
 
     @Test
     public void testCenterChild() {
+        if (Build.VERSION.SDK_INT == 17) {
+            return;
+        }
         final View childToTest = mPercentRelativeLayout.findViewById(R.id.child_center);
 
         boolean supportsRtl = Build.VERSION.SDK_INT >= 17;
diff --git a/samples/SupportLeanbackDemos/res/raw/browse.mp4 b/samples/SupportLeanbackDemos/res/raw/browse.mp4
index b841a48..3f709fb 100644
--- a/samples/SupportLeanbackDemos/res/raw/browse.mp4
+++ b/samples/SupportLeanbackDemos/res/raw/browse.mp4
Binary files differ
diff --git a/v17/leanback/res/layout/lb_row_header.xml b/v17/leanback/res/layout/lb_row_header.xml
index 8962e9a..2729ae9 100644
--- a/v17/leanback/res/layout/lb_row_header.xml
+++ b/v17/leanback/res/layout/lb_row_header.xml
@@ -15,14 +15,13 @@
      limitations under the License.
 -->
 
-<android:LinearLayout
+<LinearLayout
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:orientation="vertical"
         xmlns:android="http://schemas.android.com/apk/res/android">
 
     <android.support.v17.leanback.widget.RowHeaderView
-            xmlns:android="http://schemas.android.com/apk/res/android"
             android:id="@+id/row_header"
             android:importantForAccessibility="no"
             android:layout_width="wrap_content"
@@ -30,11 +29,10 @@
             style="?rowHeaderStyle"/>
 
     <TextView
-            xmlns:android="http://schemas.android.com/apk/res/android"
             android:id="@+id/row_header_description"
             android:importantForAccessibility="no"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             style="?rowHeaderDescriptionStyle" />
 
-</android:LinearLayout>
+</LinearLayout>
diff --git a/v17/leanback/res/values/attrs.xml b/v17/leanback/res/values/attrs.xml
index 870d958..3e2d7f2 100644
--- a/v17/leanback/res/values/attrs.xml
+++ b/v17/leanback/res/values/attrs.xml
@@ -522,6 +522,14 @@
         <attr name="guidedActionContentWidthNoIcon" format="reference" />
     </declare-styleable>
 
+    <declare-styleable name="lbTimePicker">
+        <!-- attr indicating whether time is in 24 hour format (true) or AM/PM format (false). -->
+        <attr name="is24HourFormat" format="boolean" />
+        <!-- attr indicating whether time fields should be initially set to the current time.
+         By default, it's true i.e. TimePicker initializes fields with the current time. -->
+        <attr name="useCurrentTime" format="boolean" />
+    </declare-styleable>
+
     <declare-styleable name="lbDatePicker">
         <attr name="android:minDate" />
         <attr name="android:maxDate" />
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsBackgroundVideoHelper.java b/v17/leanback/src/android/support/v17/leanback/app/DetailsBackgroundVideoHelper.java
index 72072a2..37a6bfc 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/DetailsBackgroundVideoHelper.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/DetailsBackgroundVideoHelper.java
@@ -37,7 +37,9 @@
  */
 final class DetailsBackgroundVideoHelper {
     private static final long BACKGROUND_CROSS_FADE_DURATION = 500;
-    private static final long CROSSFADE_DELAY = 0;
+    // Temporarily add CROSSFADE_DELAY waiting for video surface ready.
+    // We will remove this delay once PlaybackGlue have a callback for videoRenderingReady event.
+    private static final long CROSSFADE_DELAY = 1000;
 
     /**
      * Different states {@link DetailsFragment} can be in.
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 ac11fde..8cced05 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java
@@ -521,7 +521,7 @@
     @CallSuper
     void onSafeStart() {
         if (mDetailsBackgroundController != null) {
-            mDetailsBackgroundController.enablePlaybackHost();
+            mDetailsBackgroundController.onStart();
         }
     }
 
@@ -541,6 +541,14 @@
         }
     }
 
+    @Override
+    public void onStop() {
+        if (mDetailsBackgroundController != null) {
+            mDetailsBackgroundController.onStop();
+        }
+        super.onStop();
+    }
+
     /**
      * Called on every visible row to change view status when current selected row position
      * or selected sub position changed.  Subclass may override.   The default
@@ -703,7 +711,6 @@
                     if (direction == View.FOCUS_DOWN) {
                         if (mRowsFragment.getVerticalGridView() != null) {
                             showTitle(true);
-                            slideInGridView();
                             return mRowsFragment.getVerticalGridView();
                         }
                     }
@@ -730,7 +737,6 @@
                         && mVideoFragment.getView().hasFocus()) {
                     if (keyCode == KeyEvent.KEYCODE_BACK) {
                         showTitle(true);
-                        slideInGridView();
                         getVerticalGridView().requestFocus();
                         return true;
                     }
@@ -748,10 +754,4 @@
         getVerticalGridView().animateOut();
     }
 
-    /**
-     * Slides in vertical grid view (displaying media item details) from below.
-     */
-    void slideInGridView() {
-        getVerticalGridView().animateIn();
-    }
 }
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java b/v17/leanback/src/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java
index a0bf00c..ef09ae6 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java
@@ -228,16 +228,25 @@
     }
 
     /**
-     * Enable Host for PlaybackGlue. This is delayed until: onStart() is called,
-     * activity transitions are finished.
+     * When fragment is started and no running transition. First set host if not yet set, second
+     * start playing if it was paused before.
      */
-    void enablePlaybackHost() {
+    void onStart() {
         if (!mCanUseHost) {
             mCanUseHost = true;
             if (mPlaybackGlue != null) {
                 mPlaybackGlue.setHost(onCreateGlueHost());
             }
         }
+        if (mPlaybackGlue != null && mPlaybackGlue.isReadyForPlayback()) {
+            mPlaybackGlue.play();
+        }
+    }
+
+    void onStop() {
+        if (mPlaybackGlue != null) {
+            mPlaybackGlue.pause();
+        }
     }
 
     /**
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 53300ac..9c0d9a5 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragment.java
@@ -524,7 +524,7 @@
     @CallSuper
     void onSafeStart() {
         if (mDetailsBackgroundController != null) {
-            mDetailsBackgroundController.enablePlaybackHost();
+            mDetailsBackgroundController.onStart();
         }
     }
 
@@ -544,6 +544,14 @@
         }
     }
 
+    @Override
+    public void onStop() {
+        if (mDetailsBackgroundController != null) {
+            mDetailsBackgroundController.onStop();
+        }
+        super.onStop();
+    }
+
     /**
      * Called on every visible row to change view status when current selected row position
      * or selected sub position changed.  Subclass may override.   The default
@@ -706,7 +714,6 @@
                     if (direction == View.FOCUS_DOWN) {
                         if (mRowsSupportFragment.getVerticalGridView() != null) {
                             showTitle(true);
-                            slideInGridView();
                             return mRowsSupportFragment.getVerticalGridView();
                         }
                     }
@@ -733,7 +740,6 @@
                         && mVideoSupportFragment.getView().hasFocus()) {
                     if (keyCode == KeyEvent.KEYCODE_BACK) {
                         showTitle(true);
-                        slideInGridView();
                         getVerticalGridView().requestFocus();
                         return true;
                     }
@@ -751,10 +757,4 @@
         getVerticalGridView().animateOut();
     }
 
-    /**
-     * Slides in vertical grid view (displaying media item details) from below.
-     */
-    void slideInGridView() {
-        getVerticalGridView().animateIn();
-    }
 }
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragmentBackgroundController.java b/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragmentBackgroundController.java
index 5ab42c3..071a04a 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragmentBackgroundController.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragmentBackgroundController.java
@@ -231,16 +231,25 @@
     }
 
     /**
-     * Enable Host for PlaybackGlue. This is delayed until: onStart() is called,
-     * activity transitions are finished.
+     * When fragment is started and no running transition. First set host if not yet set, second
+     * start playing if it was paused before.
      */
-    void enablePlaybackHost() {
+    void onStart() {
         if (!mCanUseHost) {
             mCanUseHost = true;
             if (mPlaybackGlue != null) {
                 mPlaybackGlue.setHost(onCreateGlueHost());
             }
         }
+        if (mPlaybackGlue != null && mPlaybackGlue.isReadyForPlayback()) {
+            mPlaybackGlue.play();
+        }
+    }
+
+    void onStop() {
+        if (mPlaybackGlue != null) {
+            mPlaybackGlue.pause();
+        }
     }
 
     /**
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 1058ab1..8d6367a 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/PlaybackFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/PlaybackFragment.java
@@ -835,6 +835,14 @@
         super.onDestroyView();
     }
 
+    @Override
+    public void onDestroy() {
+        if (mHostCallback != null) {
+            mHostCallback.onHostDestroy();
+        }
+        super.onDestroy();
+    }
+
     /**
      * Sets the playback row for the playback controls.
      */
diff --git a/v17/leanback/src/android/support/v17/leanback/app/PlaybackOverlayFragment.java b/v17/leanback/src/android/support/v17/leanback/app/PlaybackOverlayFragment.java
index 3b8cfd3..c601b2e 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/PlaybackOverlayFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/PlaybackOverlayFragment.java
@@ -787,6 +787,9 @@
     @Override
     public void onDestroyView() {
         mRootView = null;
+        if (mHostCallback != null) {
+            mHostCallback.onHostDestroy();
+        }
         super.onDestroyView();
     }
 
diff --git a/v17/leanback/src/android/support/v17/leanback/app/PlaybackOverlaySupportFragment.java b/v17/leanback/src/android/support/v17/leanback/app/PlaybackOverlaySupportFragment.java
index b58a2ad..b4df936 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/PlaybackOverlaySupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/PlaybackOverlaySupportFragment.java
@@ -790,6 +790,9 @@
     @Override
     public void onDestroyView() {
         mRootView = null;
+        if (mHostCallback != null) {
+            mHostCallback.onHostDestroy();
+        }
         super.onDestroyView();
     }
 
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 b812004..3133264 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/PlaybackSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/PlaybackSupportFragment.java
@@ -838,6 +838,14 @@
         super.onDestroyView();
     }
 
+    @Override
+    public void onDestroy() {
+        if (mHostCallback != null) {
+            mHostCallback.onHostDestroy();
+        }
+        super.onDestroy();
+    }
+
     /**
      * Sets the playback row for the playback controls.
      */
diff --git a/v17/leanback/src/android/support/v17/leanback/app/VideoFragment.java b/v17/leanback/src/android/support/v17/leanback/app/VideoFragment.java
index 2cf42aa..150e461 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/VideoFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/VideoFragment.java
@@ -89,4 +89,11 @@
     public SurfaceView getSurfaceView() {
         return mVideoSurface;
     }
+
+    @Override
+    public void onDestroyView() {
+        mVideoSurface = null;
+        mState = SURFACE_NOT_CREATED;
+        super.onDestroyView();
+    }
 }
diff --git a/v17/leanback/src/android/support/v17/leanback/app/VideoSupportFragment.java b/v17/leanback/src/android/support/v17/leanback/app/VideoSupportFragment.java
index 7a811e4..c12b06f 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/VideoSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/VideoSupportFragment.java
@@ -92,4 +92,11 @@
     public SurfaceView getSurfaceView() {
         return mVideoSurface;
     }
+
+    @Override
+    public void onDestroyView() {
+        mVideoSurface = null;
+        mState = SURFACE_NOT_CREATED;
+        super.onDestroyView();
+    }
 }
diff --git a/v17/leanback/src/android/support/v17/leanback/media/MediaPlayerGlue.java b/v17/leanback/src/android/support/v17/leanback/media/MediaPlayerGlue.java
index abee2dc..b0f0b8a 100644
--- a/v17/leanback/src/android/support/v17/leanback/media/MediaPlayerGlue.java
+++ b/v17/leanback/src/android/support/v17/leanback/media/MediaPlayerGlue.java
@@ -29,7 +29,6 @@
 import android.support.v17.leanback.widget.Presenter;
 import android.support.v17.leanback.widget.Row;
 import android.support.v17.leanback.widget.RowPresenter;
-import android.util.Log;
 import android.view.KeyEvent;
 import android.view.SurfaceHolder;
 import android.view.View;
@@ -159,14 +158,18 @@
      * Release internal MediaPlayer. Should not use the object after call release().
      */
     public void release() {
+        mInitialized = false;
         mPlayer.release();
     }
 
     @Override
     protected void onDetachedFromHost() {
-        super.onDetachedFromHost();
+        if (getHost() instanceof SurfaceHolderGlueHost) {
+            ((SurfaceHolderGlueHost) getHost()).setSurfaceHolderCallback(null);
+        }
         reset();
         release();
+        super.onDetachedFromHost();
     }
 
     @Override
@@ -185,18 +188,19 @@
 
     @Override
     public void enableProgressUpdating(final boolean enabled) {
+        if (mRunnable != null) mHandler.removeCallbacks(mRunnable);
         if (!enabled) {
-            if (mRunnable != null) mHandler.removeCallbacks(mRunnable);
             return;
         }
-        mRunnable = new Runnable() {
-            @Override
-            public void run() {
-                updateProgress();
-                Log.d(TAG, "enableProgressUpdating(boolean)");
-                mHandler.postDelayed(this, getUpdatePeriod());
-            }
-        };
+        if (mRunnable == null) {
+            mRunnable = new Runnable() {
+                @Override
+                public void run() {
+                    updateProgress();
+                    mHandler.postDelayed(this, getUpdatePeriod());
+                }
+            };
+        }
         mHandler.postDelayed(mRunnable, getUpdatePeriod());
     }
 
@@ -262,7 +266,7 @@
 
     @Override
     public boolean isMediaPlaying() {
-        return mPlayer.isPlaying();
+        return mInitialized && mPlayer.isPlaying();
     }
 
     @Override
@@ -295,7 +299,7 @@
     @Override
     public int getCurrentSpeedId() {
         // 0 = Pause, 1 = Normal Playback Speed
-        return mPlayer.isPlaying() ? 1 : 0;
+        return isMediaPlaying() ? 1 : 0;
     }
 
     @Override
@@ -305,6 +309,9 @@
 
     @Override
     public void play(int speed) {
+        if (!mInitialized || mPlayer.isPlaying()) {
+            return;
+        }
         mPlayer.start();
         onMetadataChanged();
         onStateChanged();
@@ -313,8 +320,9 @@
 
     @Override
     public void pause() {
-        if (mPlayer.isPlaying()) {
+        if (isMediaPlaying()) {
             mPlayer.pause();
+            onStateChanged();
         }
     }
 
@@ -359,6 +367,9 @@
      * @param newPosition The new position of the media track in milliseconds.
      */
     protected void seekTo(int newPosition) {
+        if (!mInitialized) {
+            return;
+        }
         mPlayer.seekTo(newPosition);
     }
 
@@ -379,6 +390,7 @@
             prepareMediaForPlaying();
         } else {
             mMediaSourceUri = uri;
+            prepareMediaForPlaying();
         }
         return true;
     }
@@ -400,6 +412,7 @@
             prepareMediaForPlaying();
         } else {
             mMediaSourcePath = path;
+            prepareMediaForPlaying();
         }
         return true;
     }
@@ -436,6 +449,9 @@
         mPlayer.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() {
             @Override
             public void onBufferingUpdate(MediaPlayer mp, int percent) {
+                if (getControlsRow() == null) {
+                    return;
+                }
                 getControlsRow().setBufferedProgress((int) (mp.getDuration() * (percent / 100f)));
             }
         });
@@ -475,15 +491,9 @@
      * {@link PlaybackGlueHost}.
      */
     class VideoPlayerSurfaceHolderCallback implements SurfaceHolder.Callback {
-        private boolean mMediaPlayerReset = true;
-
         @Override
         public void surfaceCreated(SurfaceHolder surfaceHolder) {
-            if (mMediaPlayerReset) {
-                mMediaPlayerReset = false;
-                setDisplay(surfaceHolder);
-                prepareMediaForPlaying();
-            }
+            setDisplay(surfaceHolder);
         }
 
         @Override
@@ -492,9 +502,7 @@
 
         @Override
         public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
-            reset();
             setDisplay(null);
-            mMediaPlayerReset = true;
         }
     }
 }
diff --git a/v17/leanback/src/android/support/v17/leanback/media/PlaybackControlGlue.java b/v17/leanback/src/android/support/v17/leanback/media/PlaybackControlGlue.java
index bdd67fb..db209c0 100644
--- a/v17/leanback/src/android/support/v17/leanback/media/PlaybackControlGlue.java
+++ b/v17/leanback/src/android/support/v17/leanback/media/PlaybackControlGlue.java
@@ -254,6 +254,12 @@
         enableProgressUpdating(false);
     }
 
+    @Override
+    protected void onDetachedFromHost() {
+        enableProgressUpdating(false);
+        super.onDetachedFromHost();
+    }
+
     /**
      * Instantiating a {@link PlaybackControlsRow} and corresponding
      * {@link PlaybackControlsRowPresenter}. Subclass may override.
@@ -393,7 +399,9 @@
     public void updateProgress() {
         int position = getCurrentPosition();
         if (DEBUG) Log.v(TAG, "updateProgress " + position);
-        mControlsRow.setCurrentTime(position);
+        if (mControlsRow != null) {
+            mControlsRow.setCurrentTime(position);
+        }
     }
 
     /**
diff --git a/v17/leanback/src/android/support/v17/leanback/media/PlaybackGlue.java b/v17/leanback/src/android/support/v17/leanback/media/PlaybackGlue.java
index 8ed4b93..3f55da3 100644
--- a/v17/leanback/src/android/support/v17/leanback/media/PlaybackGlue.java
+++ b/v17/leanback/src/android/support/v17/leanback/media/PlaybackGlue.java
@@ -21,10 +21,19 @@
 
 /**
  * Base class for abstraction of media play/pause feature. A subclass of PlaybackGlue will contain
- * implementation of Media Player. App initializes PlaybackGlue subclass, associated it with a
- * {@link PlaybackGlueHost}. {@link PlaybackGlueHost} is typically implemented by a Fragment or
- * an Activity, it provides the environment to render UI for PlaybackGlue object, it optionally
- * provides SurfaceHolder via {@link SurfaceHolderGlueHost} to render video.
+ * implementation of Media Player or a connection to playback Service. App initializes
+ * PlaybackGlue subclass, associated it with a {@link PlaybackGlueHost}. {@link PlaybackGlueHost}
+ * is typically implemented by a Fragment or an Activity, it provides the environment to render UI
+ * for PlaybackGlue object, it optionally provides SurfaceHolder via {@link SurfaceHolderGlueHost}
+ * to render video. A typical PlaybackGlue should release resources (e.g. MediaPlayer or connection
+ * to playback Service) in {@link #onDetachedFromHost()}.
+ * {@link #onDetachedFromHost()} is called in two cases:
+ * <ul>
+ * <li> app manually change it using {@link #setHost(PlaybackGlueHost)} call</li>
+ * <li> When host (fragment or activity) is destroyed </li>
+ * </ul>
+ * In rare case if an PlaybackGlue wants to live outside fragment / activity life cycle, it may
+ * manages resource release by itself.
  *
  * @see PlaybackGlueHost
  */
@@ -58,7 +67,10 @@
 
     /**
      * Returns true when the media player is ready to start media playback. Subclasses must
-     * implement this method correctly.
+     * implement this method correctly. When returning false, app may listen to
+     * {@link PlayerCallback#onReadyForPlayback()} event.
+     *
+     * @see PlayerCallback#onReadyForPlayback()
      */
     public boolean isReadyForPlayback() {
         return true;
@@ -95,7 +107,10 @@
     }
 
     /**
-     * This method is used to configure the {@link PlaybackGlueHost} with required listeners.
+     * This method is used to associate a PlaybackGlue with the {@link PlaybackGlueHost} which
+     * provides UI and optional {@link SurfaceHolderGlueHost}.
+     *
+     * @param host The host for the PlaybackGlue. Set to null to detach from the host.
      */
     public final void setHost(PlaybackGlueHost host) {
         if (mPlaybackGlueHost == host) {
@@ -161,13 +176,21 @@
             public void onHostPause() {
                 PlaybackGlue.this.onHostPause();
             }
+
+            @Override
+            public void onHostDestroy() {
+                if (mPlaybackGlueHost != null) {
+                    mPlaybackGlueHost.attachToGlue(null);
+                }
+            }
         });
     }
 
     /**
      * This method is called when current associated {@link PlaybackGlueHost} is attached to a
-     * different {@link PlaybackGlue}. Subclass may override and call super.onDetachedFromHost()
-     * at last.
+     * different {@link PlaybackGlue} or {@link PlaybackGlueHost} is destroyed . Subclass may
+     * override and call super.onDetachedFromHost() at last. A typical PlaybackGlue will release
+     * resources (e.g. MediaPlayer or connection to playback service) in this method.
      */
     @CallSuper
     protected void onDetachedFromHost() {
diff --git a/v17/leanback/src/android/support/v17/leanback/media/PlaybackGlueHost.java b/v17/leanback/src/android/support/v17/leanback/media/PlaybackGlueHost.java
index 3cf086b..799074c 100644
--- a/v17/leanback/src/android/support/v17/leanback/media/PlaybackGlueHost.java
+++ b/v17/leanback/src/android/support/v17/leanback/media/PlaybackGlueHost.java
@@ -72,6 +72,12 @@
          */
         public void onHostResume() {
         }
+
+        /**
+         * Callback triggered once the host(fragment) has been destroyed.
+         */
+        public void onHostDestroy() {
+        }
     }
 
     /**
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/BaseGridView.java b/v17/leanback/src/android/support/v17/leanback/widget/BaseGridView.java
index 86265fb..005a441 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/BaseGridView.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/BaseGridView.java
@@ -27,7 +27,6 @@
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
-import android.view.animation.AccelerateDecelerateInterpolator;
 
 /**
  * An abstract base class for vertically and horizontally scrolling lists. The items come
@@ -1003,22 +1002,21 @@
         return mLayoutManager.getExtraLayoutSpace();
     }
 
-
+    /**
+     * Temporarily slide out child views to bottom (for VerticalGridView) or end
+     * (for HorizontalGridView). The views will be automatically slide-in in next
+     * {@link #smoothScrollToPosition(int)} or {@link #scrollToPosition(int)}.
+     */
     public void animateOut() {
-        ((GridLayoutManager) getLayoutManager()).setIsSlidingChildViews(true);
-        smoothScrollBy(0, -600, new AccelerateDecelerateInterpolator());
+        mLayoutManager.slideOut();
     }
 
+    /**
+     * @deprecated No longer needed. Children being slide out by {@link #animateOut()} will be
+     * slide in next focus or (smooth)scrollToPosition action.
+     */
+    @Deprecated
     public void animateIn() {
-        addOnScrollListener(new OnScrollListener() {
-            @Override
-            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
-                if (newState == RecyclerView.SCROLL_STATE_IDLE) {
-                    ((GridLayoutManager) getLayoutManager()).setIsSlidingChildViews(false);
-                }
-            }
-        });
-        smoothScrollBy(0, 600, new AccelerateDecelerateInterpolator());
     }
 
 
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java b/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java
index a3ac029..5569836 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java
@@ -42,6 +42,7 @@
 import android.view.View.MeasureSpec;
 import android.view.ViewGroup;
 import android.view.ViewGroup.MarginLayoutParams;
+import android.view.animation.AccelerateDecelerateInterpolator;
 
 import java.io.PrintWriter;
 import java.io.StringWriter;
@@ -376,8 +377,8 @@
     // effect smooth scrolling too over to bind an item view then drag the item view back.
     final static int MIN_MS_SMOOTH_SCROLL_MAIN_SCREEN = 30;
 
-    // Represents whether child views are sliding in or out.
-    private boolean mIsSlidingChildViews;
+    // Represents whether child views are temporarily sliding out
+    boolean mIsSlidingChildViews;
 
     String getTag() {
         return TAG + ":" + mBaseGridView.getId();
@@ -1737,8 +1738,41 @@
         return mGrid.appendOneColumnVisibleItems();
     }
 
-    public void setIsSlidingChildViews(boolean animatingChildViews) {
-        this.mIsSlidingChildViews = animatingChildViews;
+    /**
+     * Temporarily slide out child and will be auto slide-in in next scrollToView().
+     */
+    void slideOut() {
+        if (mIsSlidingChildViews) {
+            return;
+        }
+        mIsSlidingChildViews = true;
+        if (mOrientation == VERTICAL) {
+            int distance = -getHeight();
+            int top = getChildAt(0).getTop();
+            if (top < 0) {
+                // scroll more if first child is above top edge
+                distance = distance + top;
+            }
+            mBaseGridView.smoothScrollBy(0, distance, new AccelerateDecelerateInterpolator());
+        } else {
+            int distance;
+            if (mReverseFlowPrimary) {
+                distance = getWidth();
+                int start = getChildAt(0).getRight();
+                if (start > distance) {
+                    // scroll more if first child is outside right edge
+                    distance = start;
+                }
+            } else {
+                distance = -getWidth();
+                int start = getChildAt(0).getLeft();
+                if (start < 0) {
+                    // scroll more if first child is out side left edge
+                    distance = distance + start;
+                }
+            }
+            mBaseGridView.smoothScrollBy(distance, 0, new AccelerateDecelerateInterpolator());
+        }
     }
 
     private boolean prependOneColumnVisibleItems() {
@@ -2317,6 +2351,12 @@
         setSelection(position, 0, false, 0);
     }
 
+    @Override
+    public void smoothScrollToPosition(RecyclerView recyclerView, State state,
+            int position) {
+        setSelection(position, 0, true, 0);
+    }
+
     public void setSelection(int position,
             int primaryScrollExtra) {
         setSelection(position, 0, false, primaryScrollExtra);
@@ -2345,7 +2385,7 @@
 
     public void setSelection(int position, int subposition, boolean smooth,
             int primaryScrollExtra) {
-        if (mFocusPosition != position && position != NO_POSITION
+        if (mIsSlidingChildViews || mFocusPosition != position && position != NO_POSITION
                 || subposition != mSubFocusPosition || primaryScrollExtra != mPrimaryScrollExtra) {
             scrollToSelection(position, subposition, smooth, primaryScrollExtra);
         }
@@ -2621,6 +2661,7 @@
      * Scroll to a given child view and change mFocusPosition.
      */
     private void scrollToView(View view, View childView, boolean smooth) {
+        mIsSlidingChildViews = false;
         int newFocusPosition = getPositionByView(view);
         int newSubFocusPosition = getSubPositionByView(view, childView);
         if (newFocusPosition != mFocusPosition || newSubFocusPosition != mSubFocusPosition) {
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/picker/DatePicker.java b/v17/leanback/src/android/support/v17/leanback/widget/picker/DatePicker.java
index 31863a9..7725bf3 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/picker/DatePicker.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/picker/DatePicker.java
@@ -59,7 +59,7 @@
 
     final static String DATE_FORMAT = "MM/dd/yyyy";
     final DateFormat mDateFormat = new SimpleDateFormat(DATE_FORMAT);
-    PickerConstant mConstant;
+    PickerUtility.DateConstant mConstant;
 
     Calendar mMinDate;
     Calendar mMaxDate;
@@ -179,23 +179,13 @@
         return mDatePickerFormat;
     }
 
-    private Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) {
-        if (oldCalendar == null) {
-            return Calendar.getInstance(locale);
-        } else {
-            final long currentTimeMillis = oldCalendar.getTimeInMillis();
-            Calendar newCalendar = Calendar.getInstance(locale);
-            newCalendar.setTimeInMillis(currentTimeMillis);
-            return newCalendar;
-        }
-    }
-
     private void updateCurrentLocale() {
-        mConstant = new PickerConstant(Locale.getDefault(), getContext().getResources());
-        mTempDate = getCalendarForLocale(mTempDate, mConstant.locale);
-        mMinDate = getCalendarForLocale(mMinDate, mConstant.locale);
-        mMaxDate = getCalendarForLocale(mMaxDate, mConstant.locale);
-        mCurrentDate = getCalendarForLocale(mCurrentDate, mConstant.locale);
+        mConstant = PickerUtility.getDateConstantInstance(Locale.getDefault(),
+                getContext().getResources());
+        mTempDate = PickerUtility.getCalendarForLocale(mTempDate, mConstant.locale);
+        mMinDate = PickerUtility.getCalendarForLocale(mMinDate, mConstant.locale);
+        mMaxDate = PickerUtility.getCalendarForLocale(mMaxDate, mConstant.locale);
+        mCurrentDate = PickerUtility.getCalendarForLocale(mCurrentDate, mConstant.locale);
 
         if (mMonthColumn != null) {
             mMonthColumn.setStaticLabels(mConstant.months);
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/picker/PickerConstant.java b/v17/leanback/src/android/support/v17/leanback/widget/picker/PickerConstant.java
deleted file mode 100644
index cfb704f..0000000
--- a/v17/leanback/src/android/support/v17/leanback/widget/picker/PickerConstant.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package android.support.v17.leanback.widget.picker;
-
-import android.content.res.Resources;
-import android.support.v17.leanback.R;
-
-import java.text.DateFormatSymbols;
-import java.util.Calendar;
-import java.util.Locale;
-
-/**
- * Date/Time Picker related constants
- */
-class PickerConstant {
-
-    public final String[] months;
-    public final String[] days;
-    public final String[] hours12;
-    public final String[] hours24;
-    public final String[] minutes;
-    public final String[] ampm;
-    public final String dateSeparator;
-    public final String timeSeparator;
-    public final Locale locale;
-
-    public PickerConstant(Locale locale, Resources resources) {
-        this.locale = locale;
-        DateFormatSymbols symbols = DateFormatSymbols.getInstance(locale);
-        months = symbols.getShortMonths();
-        Calendar calendar = Calendar.getInstance(locale);
-        days = createStringIntArrays(calendar.getMinimum(Calendar.DAY_OF_MONTH),
-                calendar.getMaximum(Calendar.DAY_OF_MONTH), "%02d");
-        hours12 = createStringIntArrays(1, 12, "%02d");
-        hours24 = createStringIntArrays(0, 23, "%02d");
-        minutes = createStringIntArrays(0, 59, "%02d");
-        ampm = symbols.getAmPmStrings();
-        dateSeparator = resources.getString(R.string.lb_date_separator);
-        timeSeparator = resources.getString(R.string.lb_time_separator);
-    }
-
-
-    public static String[] createStringIntArrays(int firstNumber, int lastNumber, String format) {
-        String[] array = new String[lastNumber - firstNumber + 1];
-        for (int i = firstNumber; i <= lastNumber; i++) {
-            if (format != null) {
-                array[i - firstNumber] = String.format(format, i);
-            } else {
-                array[i - firstNumber] = String.valueOf(i);
-            }
-        }
-        return array;
-    }
-
-
-}
\ No newline at end of file
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/picker/PickerUtility.java b/v17/leanback/src/android/support/v17/leanback/widget/picker/PickerUtility.java
new file mode 100644
index 0000000..1e3a28f
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/picker/PickerUtility.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v17.leanback.widget.picker;
+
+import android.content.res.Resources;
+import android.support.v17.leanback.R;
+
+import java.text.DateFormatSymbols;
+import java.util.Calendar;
+import java.util.Locale;
+
+/**
+ * Utility class that provides Date/Time related constants as well as locale-specific calendar for
+ * both {@link DatePicker} and {@link TimePicker}.
+ */
+class PickerUtility {
+
+    public static class DateConstant {
+        public final Locale locale;
+        public final String[] months;
+        public final String[] days;
+        public final String dateSeparator;
+
+        private DateConstant(Locale locale, Resources resources) {
+            this.locale = locale;
+            DateFormatSymbols symbols = DateFormatSymbols.getInstance(locale);
+            months = symbols.getShortMonths();
+            Calendar calendar = Calendar.getInstance(locale);
+            days = createStringIntArrays(calendar.getMinimum(Calendar.DAY_OF_MONTH),
+                    calendar.getMaximum(Calendar.DAY_OF_MONTH), "%02d");
+            dateSeparator = resources.getString(R.string.lb_date_separator);
+        }
+    }
+
+    public static class TimeConstant {
+        public final Locale locale;
+        public final String[] hours12;
+        public final String[] hours24;
+        public final String[] minutes;
+        public final String[] ampm;
+        public final String timeSeparator;
+
+        private TimeConstant(Locale locale, Resources resources) {
+            this.locale = locale;
+            DateFormatSymbols symbols = DateFormatSymbols.getInstance(locale);
+            hours12 = createStringIntArrays(1, 12, "%02d");
+            hours24 = createStringIntArrays(0, 23, "%02d");
+            minutes = createStringIntArrays(0, 59, "%02d");
+            ampm = symbols.getAmPmStrings();
+            timeSeparator = resources.getString(R.string.lb_time_separator);
+        }
+    }
+
+    public static DateConstant getDateConstantInstance(Locale locale, Resources resources) {
+        return new DateConstant(locale, resources);
+    }
+
+    public static TimeConstant getTimeConstantInstance(Locale locale, Resources resources) {
+        return new TimeConstant(locale, resources);
+    }
+
+
+    public static String[] createStringIntArrays(int firstNumber, int lastNumber, String format) {
+        String[] array = new String[lastNumber - firstNumber + 1];
+        for (int i = firstNumber; i <= lastNumber; i++) {
+            if (format != null) {
+                array[i - firstNumber] = String.format(format, i);
+            } else {
+                array[i - firstNumber] = String.valueOf(i);
+            }
+        }
+        return array;
+    }
+
+    public static Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) {
+        if (oldCalendar == null) {
+            return Calendar.getInstance(locale);
+        } else {
+            final long currentTimeMillis = oldCalendar.getTimeInMillis();
+            Calendar newCalendar = Calendar.getInstance(locale);
+            newCalendar.setTimeInMillis(currentTimeMillis);
+            return newCalendar;
+        }
+    }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/picker/TimePicker.java b/v17/leanback/src/android/support/v17/leanback/widget/picker/TimePicker.java
new file mode 100644
index 0000000..a0452f5
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/picker/TimePicker.java
@@ -0,0 +1,361 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v17.leanback.widget.picker;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.support.v17.leanback.R;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Locale;
+
+/**
+ * {@link TimePicker} is a direct subclass of {@link Picker}.
+ * <p>
+ * This class is a widget for selecting time and displays it according to the formatting for the
+ * current system locale. The time can be selected by hour, minute, and AM/PM picker columns.
+ * The AM/PM mode is activated only when the 12 hour format is desired by setting
+ * {@code is24HourFormat} attribute to false. Otherwise, TimePicker displays only two columns for a
+ * 24 hour time format.
+ * <p>
+ * This widget can show the current time as the initial value if {@code useCurrentTime} is set to
+ * true. Each individual time picker field can be set  set at any time by calling
+ * {@link #setHour(int)}, {@link #setMinute(int)} using 24-hour time format. The time format can
+ * also be changed at any time by calling {@link #setIs24Hour(boolean)}, and the AM/PM picker column
+ * will be activated or deactivated according to the given format.
+ *
+ * @attr ref R.styleable#lbTimePicker_is24HourFormat
+ * @attr ref R.styleable#lbTimePicker_useCurrentTime
+ */
+public class TimePicker extends Picker {
+
+    static final String TAG = "TimePicker";
+
+    private static final int AM_INDEX = 0;
+    private static final int PM_INDEX = 1;
+
+    private static final int HOURS_IN_HALF_DAY = 12;
+    PickerColumn mHourColumn;
+    PickerColumn mMinuteColumn;
+    PickerColumn mAmPmColumn;
+    private ViewGroup mPickerView;
+    private View mAmPmSeparatorView;
+    int mColHourIndex;
+    int mColMinuteIndex;
+    int mColAmPmIndex;
+
+    private final PickerUtility.TimeConstant mConstant;
+
+    private boolean mIs24hFormat;
+
+    private int mCurrentHour;
+    private int mCurrentMinute;
+    private int mCurrentAmPmIndex;
+
+    /**
+     * Constructor called when inflating a TimePicker widget. This version uses a default style of
+     * 0, so the only attribute values applied are those in the Context's Theme and the given
+     * AttributeSet.
+     *
+     * @param context the context this TimePicker widget is associated with through which we can
+     *                access the current theme attributes and resources
+     * @param attrs the attributes of the XML tag that is inflating the TimePicker widget
+     */
+    public TimePicker(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    /**
+     * Constructor called when inflating a TimePicker widget.
+     *
+     * @param context the context this TimePicker widget is associated with through which we can
+     *                access the current theme attributes and resources
+     * @param attrs the attributes of the XML tag that is inflating the TimePicker widget
+     * @param defStyleAttr An attribute in the current theme that contains a reference to a style
+     *                     resource that supplies default values for the widget. Can be 0 to not
+     *                     look for defaults.
+     */
+    public TimePicker(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+
+        mConstant = PickerUtility.getTimeConstantInstance(Locale.getDefault(),
+                context.getResources());
+
+        setSeparator(mConstant.timeSeparator);
+        mPickerView = (ViewGroup) findViewById(R.id.picker);
+        final TypedArray attributesArray = context.obtainStyledAttributes(attrs,
+                R.styleable.lbTimePicker);
+        mIs24hFormat = attributesArray.getBoolean(R.styleable.lbTimePicker_is24HourFormat, false);
+        boolean useCurrentTime = attributesArray.getBoolean(R.styleable.lbTimePicker_useCurrentTime,
+                true);
+
+        updateColumns(getTimePickerFormat());
+
+        // The column range for the minute and AM/PM column is static and does not change, whereas
+        // the hour column range can change depending on whether 12 or 24 hour format is set at
+        // any given time.
+        updateHourColumn(false);
+        updateMin(mMinuteColumn, 0);
+        updateMax(mMinuteColumn, 59);
+
+        updateMin(mAmPmColumn, 0);
+        updateMax(mAmPmColumn, 1);
+
+        updateAmPmColumn();
+
+        if (useCurrentTime) {
+            Calendar currentDate = PickerUtility.getCalendarForLocale(null,
+                    mConstant.locale);
+            setHour(currentDate.get(Calendar.HOUR_OF_DAY));
+            setMinute(currentDate.get(Calendar.MINUTE));
+        }
+    }
+
+    private static boolean updateMin(PickerColumn column, int value) {
+        if (value != column.getMinValue()) {
+            column.setMinValue(value);
+            return true;
+        }
+        return false;
+    }
+
+    private static boolean updateMax(PickerColumn column, int value) {
+        if (value != column.getMaxValue()) {
+            column.setMaxValue(value);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     *
+     * @return the time picker format string based on the current system locale and the layout
+     *         direction
+     */
+    private String getTimePickerFormat() {
+        // Obtain the time format string per the current locale (e.g. h:mm a)
+        String hmaPattern  = ((SimpleDateFormat) DateFormat
+                .getTimeInstance(DateFormat.SHORT, mConstant.locale)).toPattern();
+        boolean isRTL = TextUtils.getLayoutDirectionFromLocale(mConstant.locale) == View
+                .LAYOUT_DIRECTION_RTL;
+        boolean isAmPmAtEnd = hmaPattern.indexOf("a") > hmaPattern.indexOf("m");
+        // Hour will always appear to the left of minutes regardless of layout direction.
+        String timePickerFormat = isRTL ? "mh" : "hm";
+
+        return isAmPmAtEnd ? (timePickerFormat + "a") : ("a" + timePickerFormat);
+    }
+
+    private void updateColumns(String timePickerFormat) {
+        if (TextUtils.isEmpty(timePickerFormat)) {
+            timePickerFormat = "hma";
+        }
+        timePickerFormat = timePickerFormat.toUpperCase();
+
+        mHourColumn = mMinuteColumn = mAmPmColumn = null;
+        mColHourIndex = mColMinuteIndex = mColAmPmIndex = -1;
+
+        ArrayList<PickerColumn> columns = new ArrayList<>(3);
+        for (int i = 0; i < timePickerFormat.length(); i++) {
+            switch (timePickerFormat.charAt(i)) {
+                case 'H':
+                    columns.add(mHourColumn = new PickerColumn());
+                    mHourColumn.setStaticLabels(mConstant.hours24);
+                    mColHourIndex = i;
+                    break;
+                case 'M':
+                    columns.add(mMinuteColumn = new PickerColumn());
+                    mMinuteColumn.setStaticLabels(mConstant.minutes);
+                    mColMinuteIndex = i;
+                    break;
+                case 'A':
+                    columns.add(mAmPmColumn = new PickerColumn());
+                    mAmPmColumn.setStaticLabels(mConstant.ampm);
+                    mColAmPmIndex = i;
+                    updateMin(mAmPmColumn, 0);
+                    updateMax(mAmPmColumn, 1);
+                    break;
+                default:
+                    throw new IllegalArgumentException("Invalid time picker format.");
+            }
+        }
+        setColumns(columns);
+        mAmPmSeparatorView = mPickerView.getChildAt(mColAmPmIndex == 0 ? 1 :
+                (2 * mColAmPmIndex - 1));
+    }
+
+    /**
+     * Updates the range in the hour column and notifies column changed if notifyChanged is true.
+     * Hour column can have either [0-23] or [1-12] depending on whether the 24 hour format is set
+     * or not.
+     *
+     * @param notifyChanged {code true} if we should notify data set changed on the hour column,
+     *                      {@code false} otherwise.
+     */
+    private void updateHourColumn(boolean notifyChanged) {
+        updateMin(mHourColumn, mIs24hFormat ? 0 : 1);
+        updateMax(mHourColumn, mIs24hFormat ? 23 : 12);
+        if (notifyChanged) {
+            setColumnAt(mColHourIndex, mHourColumn);
+        }
+    }
+
+    /**
+     * Updates AM/PM column depending on whether the 24 hour format is set or not. The visibility of
+     * this column is set to {@code GONE} for a 24 hour format, and {@code VISIBLE} in 12 hour
+     * format. This method also updates the value of this column for a 12 hour format.
+     */
+    private void updateAmPmColumn() {
+        if (mIs24hFormat) {
+            mColumnViews.get(mColAmPmIndex).setVisibility(GONE);
+            mAmPmSeparatorView.setVisibility(GONE);
+        } else {
+            mColumnViews.get(mColAmPmIndex).setVisibility(VISIBLE);
+            mAmPmSeparatorView.setVisibility(VISIBLE);
+            setColumnValue(mColAmPmIndex, mCurrentAmPmIndex, false);
+        }
+    }
+
+    /**
+     * Sets the currently selected hour using a 24-hour time.
+     *
+     * @param hour the hour to set, in the range (0-23)
+     * @see #getHour()
+     */
+    public void setHour(int hour) {
+        if (hour < 0 || hour > 23) {
+            throw new IllegalArgumentException("hour: " + hour + " is not in [0-23] range in");
+        }
+        mCurrentHour = hour;
+        if (!mIs24hFormat) {
+            if (mCurrentHour >= HOURS_IN_HALF_DAY) {
+                mCurrentAmPmIndex = PM_INDEX;
+                if (mCurrentHour > HOURS_IN_HALF_DAY) {
+                    mCurrentHour -= HOURS_IN_HALF_DAY;
+                }
+            } else {
+                mCurrentAmPmIndex = AM_INDEX;
+                if (mCurrentHour == 0) {
+                    mCurrentHour = HOURS_IN_HALF_DAY;
+                }
+            }
+            updateAmPmColumn();
+        }
+        setColumnValue(mColHourIndex, mCurrentHour, false);
+    }
+
+    /**
+     * Returns the currently selected hour using 24-hour time.
+     *
+     * @return the currently selected hour in the range (0-23)
+     * @see #setHour(int)
+     */
+    public int getHour() {
+        if (mIs24hFormat) {
+            return mCurrentHour;
+        }
+        if (mCurrentAmPmIndex == AM_INDEX) {
+            return mCurrentHour % HOURS_IN_HALF_DAY;
+        }
+        return (mCurrentHour % HOURS_IN_HALF_DAY) + HOURS_IN_HALF_DAY;
+    }
+
+    /**
+     * Sets the currently selected minute.
+     *
+     * @param minute the minute to set, in the range (0-59)
+     * @see #getMinute()
+     */
+    public void setMinute(int minute) {
+        if (mCurrentMinute == minute) {
+            return;
+        }
+        if (minute < 0 || minute > 59) {
+            throw new IllegalArgumentException("minute: " + minute + " is not in [0-59] range.");
+        }
+        mCurrentMinute = minute;
+        setColumnValue(mColMinuteIndex, mCurrentMinute, false);
+    }
+
+    /**
+     * Returns the currently selected minute.
+     *
+     * @return the currently selected minute, in the range (0-59)
+     * @see #setMinute(int)
+     */
+    public int getMinute() {
+        return mCurrentMinute;
+    }
+
+    /**
+     * Sets whether this widget displays a 24-hour mode or a 12-hour mode with an AM/PM picker.
+     *
+     * @param is24Hour {@code true} to display in 24-hour mode,
+     *                 {@code false} ti display in 12-hour mode with AM/PM.
+     * @see #is24Hour()
+     */
+    public void setIs24Hour(boolean is24Hour) {
+        if (mIs24hFormat == is24Hour) {
+            return;
+        }
+        // the ordering of these statements is important
+        int currentHour = getHour();
+        mIs24hFormat = is24Hour;
+        updateHourColumn(true);
+        setHour(currentHour);
+        updateAmPmColumn();
+    }
+
+    /**
+     * @return {@code true} if this widget displays time in 24-hour mode,
+     *         {@code false} otherwise.
+     *
+     * @see #setIs24Hour(boolean)
+     */
+    public boolean is24Hour() {
+        return mIs24hFormat;
+    }
+
+    /**
+     * Only meaningful for a 12-hour time.
+     *
+     * @return {@code true} if the currently selected time is in PM,
+     *         {@code false} if the currently selected time in in AM.
+     */
+    public boolean isPm() {
+        return (mCurrentAmPmIndex == PM_INDEX);
+    }
+
+    @Override
+    public void onColumnValueChanged(int columnIndex, int newValue) {
+        if (columnIndex == mColHourIndex) {
+            mCurrentHour = newValue;
+        } else if (columnIndex == mColMinuteIndex) {
+            mCurrentMinute = newValue;
+        } else if (columnIndex == mColAmPmIndex) {
+            mCurrentAmPmIndex = newValue;
+        } else {
+            throw new IllegalArgumentException("Invalid column index.");
+        }
+    }
+}
diff --git a/v17/leanback/tests/AndroidManifest.xml b/v17/leanback/tests/AndroidManifest.xml
index f566245..ae52dd7 100644
--- a/v17/leanback/tests/AndroidManifest.xml
+++ b/v17/leanback/tests/AndroidManifest.xml
@@ -33,6 +33,10 @@
         <activity android:name="android.support.v17.leanback.widget.GridActivity"
                   android:exported="true" />
 
+        <activity android:name="android.support.v17.leanback.widget.TimePickerActivity"
+                  android:theme="@style/Theme.Leanback"
+                  android:exported="true" />
+
         <activity android:name="android.support.v17.leanback.widget.DatePickerActivity"
                   android:theme="@style/Theme.Leanback"
                   android:exported="true" />
@@ -70,31 +74,6 @@
                   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" />
-        <activity
-            android:name="android.support.v17.leanback.app.VerticalGridFragmentTest$ImmediateRemoveFragmentActivity"
-            android:exported="true"
-            android:theme="@style/Theme.Leanback.VerticalGrid" />
-
-        <activity
-            android:name="android.support.v17.leanback.app.VerticalGridSupportFragmentTest$ImmediateRemoveFragmentActivity"
-            android:exported="true"
-            android:theme="@style/Theme.Leanback.VerticalGrid" />
-
-        <activity android:name="android.support.v17.leanback.app.VideoFragmentTestActivity"
-            android:theme="@style/Theme.Leanback"
-            android:exported="true" />
-
         <activity android:name="android.support.v17.leanback.app.TestActivity"
             android:theme="@style/Theme.Leanback"
             android:exported="true" />
diff --git a/v17/leanback/tests/generatev4.py b/v17/leanback/tests/generatev4.py
index b7d2a4c..ab6fafc 100755
--- a/v17/leanback/tests/generatev4.py
+++ b/v17/leanback/tests/generatev4.py
@@ -19,11 +19,11 @@
 
 print "Generate v4 fragment related code for leanback"
 
-files = ['BrowseTest', 'GuidedStepTest']
+files = ['BrowseTest', 'GuidedStepTest', 'PlaybackTest']
 
 cls = ['BrowseTest', 'Background', 'Base', 'BaseRow', 'Browse', 'Details', 'Error', 'Headers',
       'PlaybackOverlay', 'Rows', 'Search', 'VerticalGrid', 'Branded',
-      'GuidedStepTest', 'GuidedStep', 'RowsTest']
+      'GuidedStepTest', 'GuidedStep', 'RowsTest', 'PlaybackTest', 'Playback', 'Video']
 
 for w in files:
     print "copy {}Fragment to {}SupportFragment".format(w, w)
@@ -68,7 +68,7 @@
     file.close()
     outfile.close()
 
-testcls = ['Browse', 'GuidedStep', 'VerticalGrid']
+testcls = ['Browse', 'GuidedStep', 'VerticalGrid', 'Playback', 'Video']
 
 for w in testcls:
     print "copy {}FrgamentTest to {}SupportFragmentTest".format(w, w)
@@ -83,6 +83,7 @@
         for w in cls:
             line = line.replace('{}Fragment'.format(w), '{}SupportFragment'.format(w))
         for w in testcls:
+            line = line.replace('SingleFragmentTestBase', 'SingleSupportFragmentTestBase')
             line = line.replace('{}FragmentTestBase'.format(w), '{}SupportFragmentTestBase'.format(w))
             line = line.replace('{}FragmentTest'.format(w), '{}SupportFragmentTest'.format(w))
             line = line.replace('{}FragmentTestActivity'.format(w), '{}SupportFragmentTestActivity'.format(w))
@@ -91,6 +92,7 @@
         line = line.replace('android.app.Activity', 'android.support.v4.app.FragmentActivity')
 	line = line.replace('extends Activity', 'extends FragmentActivity')
 	line = line.replace('Activity.this.getFragmentManager', 'Activity.this.getSupportFragmentManager')
+	line = line.replace('mActivity.getFragmentManager', 'mActivity.getSupportFragmentManager')
         outfile.write(line)
     file.close()
     outfile.close()
@@ -182,6 +184,8 @@
 outfile.write("/* This file is auto-generated from PlaybackControlGlueTest.java.  DO NOT MODIFY. */\n\n")
 for line in file:
     line = line.replace('PlaybackControlGlue', 'PlaybackControlSupportGlue')
+    line = line.replace('PlaybackOverlayFragment', 'PlaybackOverlaySupportFragment')
+    line = line.replace('PlaybackGlueHostOld', 'PlaybackSupportGlueHostOld')
     outfile.write(line)
 file.close()
 outfile.close()
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 748a39f..944c1f7 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,7 +51,7 @@
 public class PlaybackControlSupportGlueTest {
 
 
-    public  static class PlayControlGlueImpl extends PlaybackControlSupportGlue {
+    static class PlayControlGlueImpl extends PlaybackControlSupportGlue {
         int mSpeedId = PLAYBACK_SPEED_PAUSED;
         // number of times onRowChanged callback is called
         int mOnRowChangedCallCount = 0;
@@ -65,7 +65,7 @@
         }
 
         PlayControlGlueImpl(Context context, PlaybackOverlaySupportFragment fragment,
-                int[] seekSpeeds) {
+                                   int[] seekSpeeds) {
             super(context, fragment, seekSpeeds);
         }
 
@@ -534,15 +534,15 @@
     @Test
     public void testOnItemClickedListener() {
         PlaybackControlsRow row = new PlaybackControlsRow();
-        final PlaybackOverlayFragment[] fragmentResult = new PlaybackOverlayFragment[1];
+        final PlaybackOverlaySupportFragment[] fragmentResult = new PlaybackOverlaySupportFragment[1];
         InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
             @Override
             public void run() {
-                fragmentResult[0] = new PlaybackOverlayFragment();
+                fragmentResult[0] = new PlaybackOverlaySupportFragment();
             }
         });
-        PlaybackOverlayFragment fragment = fragmentResult[0];
-        glue.setHost(new PlaybackControlSupportGlue.PlaybackGlueHostOld(fragment));
+        PlaybackOverlaySupportFragment fragment = fragmentResult[0];
+        glue.setHost(new PlaybackControlSupportGlue.PlaybackSupportGlueHostOld(fragment));
         glue.setControlsRow(row);
         SparseArrayObjectAdapter adapter = (SparseArrayObjectAdapter)
                 row.getPrimaryActionsAdapter();
@@ -560,7 +560,7 @@
         // Initially media is paused
         assertEquals(PlaybackControlSupportGlue.PLAYBACK_SPEED_PAUSED, glue.getCurrentSpeedId());
 
-        // simulate a click inside PlaybackOverlayFragment's PlaybackRow.
+        // simulate a click inside PlaybackOverlaySupportFragment's PlaybackRow.
         fragment.getOnItemViewClickedListener().onItemClicked(vh, playPause, rowVh, row);
         verify(listener, times(0)).onItemClicked(vh, playPause, rowVh, row);
         assertEquals(PlaybackControlSupportGlue.PLAYBACK_SPEED_NORMAL, glue.getCurrentSpeedId());
@@ -605,5 +605,4 @@
         // 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
index bf7077b..2fcf3ed 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackFragmentTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackFragmentTest.java
@@ -15,18 +15,18 @@
  */
 package android.support.v17.leanback.app;
 
+import static org.junit.Assert.assertNull;
 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.media.PlaybackGlue;
+import android.support.v17.leanback.testutils.PollingCheck;
 import android.support.v17.leanback.widget.ListRow;
 import android.support.v17.leanback.widget.OnItemViewClickedListener;
 import android.support.v17.leanback.widget.OnItemViewSelectedListener;
@@ -37,7 +37,6 @@
 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;
@@ -45,21 +44,35 @@
 
 @MediumTest
 @RunWith(AndroidJUnit4.class)
-public class PlaybackFragmentTest {
+public class PlaybackFragmentTest extends SingleFragmentTestBase {
 
     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 testDetachCalledWhenDestroyFragment() throws Throwable {
+        launchAndWaitActivity(PlaybackTestFragment.class, 1000);
+        PlaybackTestFragment fragment = (PlaybackTestFragment) mActivity.getTestFragment();
+        PlaybackGlue glue = fragment.getGlue();
+        activityTestRule.runOnUiThread(new Runnable() {
+            public void run() {
+                mActivity.finish();
+            }
+        });
+        PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return mActivity.isDestroyed();
+            }
+        });
+        assertNull(glue.getHost());
+    }
 
     @Test
     public void testSelectedListener() throws Throwable {
-        Intent intent = new Intent();
-        mActivity = activityTestRule.launchActivity(intent);
-        PlaybackTestFragment fragment = mActivity.getPlaybackFragment();
+        launchAndWaitActivity(PlaybackTestFragment.class, 1000);
+        PlaybackTestFragment fragment = (PlaybackTestFragment) mActivity.getTestFragment();
+
         assertTrue(fragment.getView().hasFocus());
 
         OnItemViewSelectedListener selectedListener = Mockito.mock(
@@ -126,9 +139,9 @@
 
     @Test
     public void testClickedListener() throws Throwable {
-        Intent intent = new Intent();
-        mActivity = activityTestRule.launchActivity(intent);
-        PlaybackTestFragment fragment = mActivity.getPlaybackFragment();
+        launchAndWaitActivity(PlaybackTestFragment.class, 1000);
+        PlaybackTestFragment fragment = (PlaybackTestFragment) mActivity.getTestFragment();
+
         assertTrue(fragment.getView().hasFocus());
 
         OnItemViewClickedListener clickedListener = Mockito.mock(OnItemViewClickedListener.class);
@@ -210,10 +223,4 @@
                 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/PlaybackOverlayFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackOverlayFragmentTest.java
index f27ace0..f05da9c 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackOverlayFragmentTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackOverlayFragmentTest.java
@@ -18,32 +18,26 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
-import android.content.Intent;
 import android.support.test.filters.MediumTest;
-import android.support.test.rule.ActivityTestRule;
 import android.support.test.runner.AndroidJUnit4;
 import android.support.v17.leanback.test.R;
 
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 @MediumTest
 @RunWith(AndroidJUnit4.class)
-public class PlaybackOverlayFragmentTest {
-
-    @Rule
-    public ActivityTestRule<PlaybackOverlayTestActivity> activityTestRule =
-            new ActivityTestRule<>(PlaybackOverlayTestActivity.class, false, false);
-    private PlaybackOverlayTestActivity mActivity;
+public class PlaybackOverlayFragmentTest extends SingleFragmentTestBase {
 
     @Test
     public void workaroundVideoViewStealFocus() {
-        Intent intent = new Intent();
-        mActivity = activityTestRule.launchActivity(intent);
+        launchAndWaitActivity(PlaybackOverlayTestFragment.class,
+                R.layout.playback_controls_with_video, 0);
+        PlaybackOverlayTestFragment fragment = (PlaybackOverlayTestFragment)
+                mActivity.getTestFragment();
 
         assertFalse(mActivity.findViewById(R.id.videoView).hasFocus());
-        assertTrue(mActivity.getPlaybackFragment().getView().hasFocus());
+        assertTrue(fragment.getView().hasFocus());
     }
 
 }
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackOverlayTestActivity.java b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackOverlayTestActivity.java
deleted file mode 100644
index ea2aa38..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackOverlayTestActivity.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v17.leanback.app;
-
-import android.app.Activity;
-import android.os.Bundle;
-import android.support.v17.leanback.test.R;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class PlaybackOverlayTestActivity extends Activity {
-    private List<PictureInPictureListener> mListeners = new ArrayList<>();
-
-    @Override
-    public void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        setContentView(R.layout.playback_controls_with_video);
-    }
-
-    @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 PlaybackOverlayTestFragment getPlaybackFragment() {
-        return (PlaybackOverlayTestFragment) getFragmentManager().findFragmentById(
-                R.id.playback_controls_fragment);
-    }
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackOverlayTestFragment.java b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackOverlayTestFragment.java
index b44dd09..e5dbe08 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackOverlayTestFragment.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackOverlayTestFragment.java
@@ -40,9 +40,7 @@
 import android.view.View;
 import android.widget.Toast;
 
-public class PlaybackOverlayTestFragment
-        extends PlaybackOverlayFragment
-        implements PlaybackOverlayTestActivity.PictureInPictureListener {
+public class PlaybackOverlayTestFragment extends PlaybackOverlayFragment {
     private static final String TAG = "leanback.PlaybackControlsFragment";
 
     /**
@@ -169,26 +167,6 @@
     public void onStart() {
         super.onStart();
         mGlue.setFadingEnabled(true);
-        mGlue.enableProgressUpdating(mGlue.hasValidMedia() && mGlue.isMediaPlaying());
-        ((PlaybackOverlayTestActivity) getActivity()).registerPictureInPictureListener(this);
-    }
-
-    @Override
-    public void onStop() {
-        mGlue.enableProgressUpdating(false);
-        ((PlaybackOverlayTestActivity) getActivity()).unregisterPictureInPictureListener(this);
-        super.onStop();
-    }
-
-    @Override
-    public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
-        if (isInPictureInPictureMode) {
-            // Hide the controls in picture-in-picture mode.
-            setFadingEnabled(true);
-            fadeOut();
-        } else {
-            setFadingEnabled(mGlue.isMediaPlaying());
-        }
     }
 
     abstract static class PlaybackControlHelper extends PlaybackControlGlue {
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
index fdba125..d33e3ef 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportFragmentTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportFragmentTest.java
@@ -1,3 +1,6 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from PlaybackFragmentTest.java.  DO NOT MODIFY. */
+
 /*
  * Copyright (C) 2016 The Android Open Source Project
  *
@@ -15,18 +18,18 @@
  */
 package android.support.v17.leanback.app;
 
+import static org.junit.Assert.assertNull;
 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.media.PlaybackGlue;
+import android.support.v17.leanback.testutils.PollingCheck;
 import android.support.v17.leanback.widget.ListRow;
 import android.support.v17.leanback.widget.OnItemViewClickedListener;
 import android.support.v17.leanback.widget.OnItemViewSelectedListener;
@@ -37,7 +40,6 @@
 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;
@@ -45,21 +47,35 @@
 
 @MediumTest
 @RunWith(AndroidJUnit4.class)
-public class PlaybackSupportFragmentTest {
+public class PlaybackSupportFragmentTest extends SingleSupportFragmentTestBase {
 
     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 testDetachCalledWhenDestroyFragment() throws Throwable {
+        launchAndWaitActivity(PlaybackTestSupportFragment.class, 1000);
+        PlaybackTestSupportFragment fragment = (PlaybackTestSupportFragment) mActivity.getTestFragment();
+        PlaybackGlue glue = fragment.getGlue();
+        activityTestRule.runOnUiThread(new Runnable() {
+            public void run() {
+                mActivity.finish();
+            }
+        });
+        PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return mActivity.isDestroyed();
+            }
+        });
+        assertNull(glue.getHost());
+    }
 
     @Test
     public void testSelectedListener() throws Throwable {
-        Intent intent = new Intent();
-        mActivity = activityTestRule.launchActivity(intent);
-        PlaybackSupportTestFragment fragment = mActivity.getPlaybackFragment();
+        launchAndWaitActivity(PlaybackTestSupportFragment.class, 1000);
+        PlaybackTestSupportFragment fragment = (PlaybackTestSupportFragment) mActivity.getTestFragment();
+
         assertTrue(fragment.getView().hasFocus());
 
         OnItemViewSelectedListener selectedListener = Mockito.mock(
@@ -82,8 +98,7 @@
 
         ArgumentCaptor<Presenter.ViewHolder> itemVHCaptor =
                 ArgumentCaptor.forClass(Presenter.ViewHolder.class);
-        ArgumentCaptor<Object> itemCaptor =
-                ArgumentCaptor.forClass(Object.class);
+        ArgumentCaptor<Object> itemCaptor = ArgumentCaptor.forClass(Object.class);
         ArgumentCaptor<RowPresenter.ViewHolder> rowVHCaptor =
                 ArgumentCaptor.forClass(RowPresenter.ViewHolder.class);
         ArgumentCaptor<Row> rowCaptor = ArgumentCaptor.forClass(Row.class);
@@ -127,9 +142,9 @@
 
     @Test
     public void testClickedListener() throws Throwable {
-        Intent intent = new Intent();
-        mActivity = activityTestRule.launchActivity(intent);
-        PlaybackSupportTestFragment fragment = mActivity.getPlaybackFragment();
+        launchAndWaitActivity(PlaybackTestSupportFragment.class, 1000);
+        PlaybackTestSupportFragment fragment = (PlaybackTestSupportFragment) mActivity.getTestFragment();
+
         assertTrue(fragment.getView().hasFocus());
 
         OnItemViewClickedListener clickedListener = Mockito.mock(OnItemViewClickedListener.class);
@@ -211,10 +226,4 @@
                 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
deleted file mode 100644
index c85fe83..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportTestActivity.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v17.leanback.app;
-
-import 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/PlaybackTestActivity.java b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestActivity.java
deleted file mode 100644
index ff840ec..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestActivity.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v17.leanback.app;
-
-import 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
index 043d73e..f9fd33f 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestFragment.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestFragment.java
@@ -19,7 +19,6 @@
 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;
@@ -28,7 +27,6 @@
 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;
@@ -75,22 +73,12 @@
         }
     };
 
-    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);
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportTestFragment.java b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestSupportFragment.java
similarity index 94%
rename from v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportTestFragment.java
rename to v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestSupportFragment.java
index 4a07a60..0973fe1 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportTestFragment.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestSupportFragment.java
@@ -1,3 +1,6 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from PlaybackTestFragment.java.  DO NOT MODIFY. */
+
 /*
  * Copyright (C) 2016 The Android Open Source Project
  *
@@ -19,7 +22,6 @@
 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;
@@ -28,7 +30,6 @@
 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;
@@ -40,13 +41,13 @@
 import android.view.View;
 import android.widget.Toast;
 
-public class PlaybackSupportTestFragment extends PlaybackSupportFragment {
-    private static final String TAG = "PlaybackTestFragment";
+public class PlaybackTestSupportFragment extends PlaybackSupportFragment {
+    private static final String TAG = "PlaybackTestSupportFragment";
 
     /**
      * Change this to choose a different overlay background.
      */
-    private static final int BACKGROUND_TYPE = PlaybackFragment.BG_LIGHT;
+    private static final int BACKGROUND_TYPE = PlaybackSupportFragment.BG_LIGHT;
 
     private static final int ROW_CONTROLS = 0;
 
@@ -75,22 +76,12 @@
         }
     };
 
-    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);
@@ -124,7 +115,7 @@
         };
 
         mGlue.setHost(new PlaybackSupportFragmentGlueHost(this));
-        //  mGlue.setOnI
+       //  mGlue.setOnI
         mListRowPresenter = new ListRowPresenter();
 
         setAdapter(new SparseArrayObjectAdapter(new PresenterSelector() {
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestActivity.java b/v17/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestActivity.java
index 8be38bb..3f6cc1b 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestActivity.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestActivity.java
@@ -29,13 +29,15 @@
      */
     public static final String EXTRA_FRAGMENT_NAME = "fragmentName";
 
+    public static final String EXTRA_ACTIVITY_LAYOUT = "activityLayout";
+
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         Intent intent = getIntent();
 
-        setContentView(R.layout.rows);
-        if (savedInstanceState == null) {
+        setContentView(intent.getIntExtra(EXTRA_ACTIVITY_LAYOUT, R.layout.single_fragment));
+        if (savedInstanceState == null && findViewById(R.id.main_frame) != null) {
             try {
                 Fragment fragment = (Fragment) Class.forName(
                         intent.getStringExtra(EXTRA_FRAGMENT_NAME)).newInstance();
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestBase.java b/v17/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestBase.java
index dbc8872..e34f4e6 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestBase.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestBase.java
@@ -62,10 +62,23 @@
         launchAndWaitActivity(fragmentClass.getName(), waitTimeMs);
     }
 
+    public void launchAndWaitActivity(Class fragmentClass, int activityLayoutId, long waitTimeMs) {
+        launchAndWaitActivity(fragmentClass.getName(), activityLayoutId, waitTimeMs);
+    }
+
     public void launchAndWaitActivity(String firstFragmentName, long waitTimeMs) {
         Intent intent = new Intent();
         intent.putExtra(SingleFragmentTestActivity.EXTRA_FRAGMENT_NAME, firstFragmentName);
         mActivity = activityTestRule.launchActivity(intent);
         SystemClock.sleep(waitTimeMs);
     }
+
+    public void launchAndWaitActivity(String firstFragmentName, int activityLayoutId,
+                                      long waitTimeMs) {
+        Intent intent = new Intent();
+        intent.putExtra(SingleFragmentTestActivity.EXTRA_FRAGMENT_NAME, firstFragmentName);
+        intent.putExtra(SingleFragmentTestActivity.EXTRA_ACTIVITY_LAYOUT, activityLayoutId);
+        mActivity = activityTestRule.launchActivity(intent);
+        SystemClock.sleep(waitTimeMs);
+    }
 }
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestActivity.java b/v17/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestActivity.java
index 85d3f43..fb0d349 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestActivity.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestActivity.java
@@ -28,13 +28,15 @@
      */
     public static final String EXTRA_FRAGMENT_NAME = "fragmentName";
 
+    public static final String EXTRA_ACTIVITY_LAYOUT = "activityLayout";
+
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         Intent intent = getIntent();
 
-        setContentView(R.layout.rows);
-        if (savedInstanceState == null) {
+        setContentView(intent.getIntExtra(EXTRA_ACTIVITY_LAYOUT, R.layout.single_fragment));
+        if (savedInstanceState == null && findViewById(R.id.main_frame) != null) {
             try {
                 Fragment fragment = (Fragment) Class.forName(
                         intent.getStringExtra(EXTRA_FRAGMENT_NAME)).newInstance();
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/VerticalGridFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/VerticalGridFragmentTest.java
index af56715..5c4f4fd 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/VerticalGridFragmentTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/VerticalGridFragmentTest.java
@@ -16,13 +16,10 @@
 
 package android.support.v17.leanback.app;
 
-import android.app.Activity;
 import android.app.Fragment;
-import android.content.Intent;
 import android.os.Bundle;
-import android.os.Handler;
+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.widget.ArrayObjectAdapter;
 import android.support.v17.leanback.widget.VerticalGridPresenter;
@@ -32,7 +29,7 @@
 
 @MediumTest
 @RunWith(AndroidJUnit4.class)
-public class VerticalGridFragmentTest {
+public class VerticalGridFragmentTest extends SingleFragmentTestBase {
 
     public static class GridFragment extends VerticalGridFragment {
         @Override
@@ -48,31 +45,23 @@
         }
     }
 
-    public static class ImmediateRemoveFragmentActivity extends Activity {
-        @Override
-        public void onCreate(Bundle savedInstanceState) {
-            super.onCreate(savedInstanceState);
-            new Handler().postDelayed(new Runnable(){
-                public void run() {
-                    GridFragment f = new GridFragment();
-                    ImmediateRemoveFragmentActivity.this.getFragmentManager().beginTransaction()
-                            .replace(android.R.id.content, f, null).commit();
-                    f.startEntranceTransition();
-                    ImmediateRemoveFragmentActivity.this.getFragmentManager().beginTransaction()
-                            .replace(android.R.id.content, new Fragment(), null).commit();
-                }
-            }, 500);
-        }
-    }
-
     @Test
     public void immediateRemoveFragment() throws Throwable {
-        Intent intent = new Intent();
-        ActivityTestRule<ImmediateRemoveFragmentActivity> activityTestRule =
-                new ActivityTestRule<>(ImmediateRemoveFragmentActivity.class, false, false);
-        ImmediateRemoveFragmentActivity activity = activityTestRule.launchActivity(intent);
+        launchAndWaitActivity(GridFragment.class, 500);
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            public void run() {
+                GridFragment f = new GridFragment();
+                mActivity.getFragmentManager().beginTransaction()
+                        .replace(android.R.id.content, f, null).commit();
+                f.startEntranceTransition();
+                mActivity.getFragmentManager().beginTransaction()
+                        .replace(android.R.id.content, new Fragment(), null).commit();
+            }
+        });
 
         Thread.sleep(1000);
+        mActivity.finish();
     }
 
 }
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/VerticalGridSupportFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/VerticalGridSupportFragmentTest.java
index 7dd402d..8b58c33 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/VerticalGridSupportFragmentTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/VerticalGridSupportFragmentTest.java
@@ -19,13 +19,10 @@
 
 package android.support.v17.leanback.app;
 
-import android.support.v4.app.FragmentActivity;
 import android.support.v4.app.Fragment;
-import android.content.Intent;
 import android.os.Bundle;
-import android.os.Handler;
+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.widget.ArrayObjectAdapter;
 import android.support.v17.leanback.widget.VerticalGridPresenter;
@@ -35,7 +32,7 @@
 
 @MediumTest
 @RunWith(AndroidJUnit4.class)
-public class VerticalGridSupportFragmentTest {
+public class VerticalGridSupportFragmentTest extends SingleSupportFragmentTestBase {
 
     public static class GridFragment extends VerticalGridSupportFragment {
         @Override
@@ -51,31 +48,23 @@
         }
     }
 
-    public static class ImmediateRemoveFragmentActivity extends FragmentActivity {
-        @Override
-        public void onCreate(Bundle savedInstanceState) {
-            super.onCreate(savedInstanceState);
-            new Handler().postDelayed(new Runnable(){
-                public void run() {
-                    GridFragment f = new GridFragment();
-                    ImmediateRemoveFragmentActivity.this.getSupportFragmentManager().beginTransaction()
-                            .replace(android.R.id.content, f, null).commit();
-                    f.startEntranceTransition();
-                    ImmediateRemoveFragmentActivity.this.getSupportFragmentManager().beginTransaction()
-                            .replace(android.R.id.content, new Fragment(), null).commit();
-                }
-            }, 500);
-        }
-    }
-
     @Test
     public void immediateRemoveFragment() throws Throwable {
-        Intent intent = new Intent();
-        ActivityTestRule<ImmediateRemoveFragmentActivity> activityTestRule =
-                new ActivityTestRule<>(ImmediateRemoveFragmentActivity.class, false, false);
-        ImmediateRemoveFragmentActivity activity = activityTestRule.launchActivity(intent);
+        launchAndWaitActivity(GridFragment.class, 500);
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            public void run() {
+                GridFragment f = new GridFragment();
+                mActivity.getSupportFragmentManager().beginTransaction()
+                        .replace(android.R.id.content, f, null).commit();
+                f.startEntranceTransition();
+                mActivity.getSupportFragmentManager().beginTransaction()
+                        .replace(android.R.id.content, new Fragment(), null).commit();
+            }
+        });
 
         Thread.sleep(1000);
+        mActivity.finish();
     }
 
 }
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/VideoFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/VideoFragmentTest.java
index b8bb68e..5c8c89e 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/VideoFragmentTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/VideoFragmentTest.java
@@ -15,58 +15,103 @@
  */
 package android.support.v17.leanback.app;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertTrue;
 
-import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.SystemClock;
 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.MediaPlayerGlue;
+import android.support.v17.leanback.media.PlaybackGlue;
+import android.support.v17.leanback.media.PlaybackGlueHost;
 import android.support.v17.leanback.test.R;
+import android.support.v17.leanback.testutils.PollingCheck;
+import android.view.LayoutInflater;
 import android.view.SurfaceHolder;
+import android.view.View;
+import android.view.ViewGroup;
 
-import org.junit.Rule;
+import junit.framework.Assert;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 @MediumTest
 @RunWith(AndroidJUnit4.class)
-public class VideoFragmentTest {
+public class VideoFragmentTest extends SingleFragmentTestBase {
 
-    @Rule
-    public ActivityTestRule<VideoFragmentTestActivity> activityTestRule =
-            new ActivityTestRule<>(VideoFragmentTestActivity.class, false, false);
-    private VideoFragmentTestActivity mActivity;
+    public static class Fragment_setSurfaceViewCallbackBeforeCreate extends VideoFragment {
+        boolean mSurfaceCreated;
+        @Override
+        public View onCreateView(
+                LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+
+            setSurfaceHolderCallback(new SurfaceHolder.Callback() {
+                @Override
+                public void surfaceCreated(SurfaceHolder holder) {
+                    mSurfaceCreated = true;
+                }
+
+                @Override
+                public void surfaceChanged(SurfaceHolder holder, int format, int width,
+                                           int height) {
+                }
+
+                @Override
+                public void surfaceDestroyed(SurfaceHolder holder) {
+                    mSurfaceCreated = false;
+                }
+            });
+
+            return super.onCreateView(inflater, container, savedInstanceState);
+        }
+    }
 
     @Test
     public void setSurfaceViewCallbackBeforeCreate() {
-        Intent intent = new Intent();
-        mActivity = activityTestRule.launchActivity(intent);
+        launchAndWaitActivity(Fragment_setSurfaceViewCallbackBeforeCreate.class, 1000);
+        Fragment_setSurfaceViewCallbackBeforeCreate fragment1 =
+                (Fragment_setSurfaceViewCallbackBeforeCreate) mActivity.getTestFragment();
+        assertNotNull(fragment1);
+        assertTrue(fragment1.mSurfaceCreated);
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
             @Override
             public void run() {
-                mActivity.replaceVideoFragment();
+                mActivity.getFragmentManager().beginTransaction()
+                        .replace(R.id.main_frame, new Fragment_setSurfaceViewCallbackBeforeCreate())
+                        .commitAllowingStateLoss();
             }
         });
+        SystemClock.sleep(500);
 
-        VideoFragment fragment = (VideoFragment) mActivity.getFragmentManager().findFragmentById(
-                R.id.video_fragment);
-        assertNotNull(fragment);
+        assertFalse(fragment1.mSurfaceCreated);
+
+        Fragment_setSurfaceViewCallbackBeforeCreate fragment2 =
+                (Fragment_setSurfaceViewCallbackBeforeCreate) mActivity.getTestFragment();
+        assertNotNull(fragment2);
+        assertTrue(fragment2.mSurfaceCreated);
+        assertNotSame(fragment1, fragment2);
     }
 
     @Test
     public void setSurfaceViewCallbackAfterCreate() {
-        Intent intent = new Intent();
-        mActivity = activityTestRule.launchActivity(intent);
+        launchAndWaitActivity(VideoFragment.class, 1000);
+        VideoFragment fragment = (VideoFragment) mActivity.getTestFragment();
 
-        VideoFragment fragment = (VideoFragment) mActivity.getFragmentManager().findFragmentById(
-                R.id.video_fragment);
         assertNotNull(fragment);
 
+        final boolean[] surfaceCreated = new boolean[1];
         fragment.setSurfaceHolderCallback(new SurfaceHolder.Callback() {
             @Override
             public void surfaceCreated(SurfaceHolder holder) {
+                surfaceCreated[0] = true;
             }
 
             @Override
@@ -75,7 +120,130 @@
 
             @Override
             public void surfaceDestroyed(SurfaceHolder holder) {
+                surfaceCreated[0] = false;
             }
         });
+        assertTrue(surfaceCreated[0]);
     }
+
+    public static class Fragment_withVideoPlayer extends VideoFragment {
+        MediaPlayerGlue mGlue;
+        int mOnCreateCalled;
+        int mOnCreateViewCalled;
+        int mOnDestroyViewCalled;
+        int mOnDestroyCalled;
+        int mGlueAttachedToHost;
+        int mGlueDetachedFromHost;
+        int mGlueOnReadyForPlaybackCalled;
+
+        public Fragment_withVideoPlayer() {
+            setRetainInstance(true);
+        }
+
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            mOnCreateCalled++;
+            super.onCreate(savedInstanceState);
+            mGlue = new MediaPlayerGlue(getActivity()) {
+                @Override
+                protected void onDetachedFromHost() {
+                    mGlueDetachedFromHost++;
+                    super.onDetachedFromHost();
+                }
+
+                @Override
+                protected void onAttachedToHost(PlaybackGlueHost host) {
+                    super.onAttachedToHost(host);
+                    mGlueAttachedToHost++;
+                }
+            };
+            mGlue.setMode(MediaPlayerGlue.REPEAT_ALL);
+            mGlue.setArtist("Leanback");
+            mGlue.setTitle("Leanback team at work");
+            mGlue.setMediaSource(
+                    Uri.parse("android.resource://android.support.v17.leanback.test/raw/video"));
+            mGlue.setPlayerCallback(new PlaybackGlue.PlayerCallback() {
+                @Override
+                public void onReadyForPlayback() {
+                    mGlueOnReadyForPlaybackCalled++;
+                    mGlue.play();
+                }
+            });
+            mGlue.setHost(new VideoFragmentGlueHost(this));
+        }
+
+        @Override
+        public View onCreateView(LayoutInflater inflater, ViewGroup container,
+                                 Bundle savedInstanceState) {
+            mOnCreateViewCalled++;
+            return super.onCreateView(inflater, container, savedInstanceState);
+        }
+
+        @Override
+        public void onDestroyView() {
+            mOnDestroyViewCalled++;
+            super.onDestroyView();
+        }
+
+        @Override
+        public void onDestroy() {
+            mOnDestroyCalled++;
+            super.onDestroy();
+        }
+    }
+
+    @Test
+    public void mediaPlayerGlueInVideoFragment() {
+        launchAndWaitActivity(Fragment_withVideoPlayer.class, 1000);
+        final Fragment_withVideoPlayer fragment = (Fragment_withVideoPlayer)
+                mActivity.getTestFragment();
+
+        PollingCheck.waitFor(5000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return fragment.mGlue.isMediaPlaying();
+            }
+        });
+
+        assertEquals(1, fragment.mOnCreateCalled);
+        assertEquals(1, fragment.mOnCreateViewCalled);
+        assertEquals(0, fragment.mOnDestroyViewCalled);
+        assertEquals(1, fragment.mGlueOnReadyForPlaybackCalled);
+        View fragmentViewBeforeRecreate = fragment.getView();
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mActivity.recreate();
+            }
+        });
+
+        PollingCheck.waitFor(5000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return fragment.mOnCreateViewCalled == 2 && fragment.mGlue.isMediaPlaying();
+            }
+        });
+        View fragmentViewAfterRecreate = fragment.getView();
+
+        Assert.assertNotSame(fragmentViewBeforeRecreate, fragmentViewAfterRecreate);
+        assertEquals(1, fragment.mOnCreateCalled);
+        assertEquals(2, fragment.mOnCreateViewCalled);
+        assertEquals(1, fragment.mOnDestroyViewCalled);
+
+        assertEquals(1, fragment.mGlueAttachedToHost);
+        assertEquals(0, fragment.mGlueDetachedFromHost);
+        assertEquals(1, fragment.mGlueOnReadyForPlaybackCalled);
+
+        mActivity.finish();
+        PollingCheck.waitFor(5000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return fragment.mGlueDetachedFromHost == 1;
+            }
+        });
+        assertEquals(2, fragment.mOnDestroyViewCalled);
+        assertEquals(1, fragment.mOnDestroyCalled);
+    }
+
 }
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/VideoFragmentTestActivity.java b/v17/leanback/tests/java/android/support/v17/leanback/app/VideoFragmentTestActivity.java
deleted file mode 100644
index e2a8f48..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/VideoFragmentTestActivity.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v17.leanback.app;
-
-import android.app.Activity;
-import android.os.Bundle;
-import android.support.v17.leanback.test.R;
-
-/**
- * Test activity containing {@link VideoFragment}.
- */
-public class VideoFragmentTestActivity extends Activity {
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        setContentView(R.layout.video_fragment_with_controls);
-    }
-
-    public void replaceVideoFragment() {
-        getFragmentManager().beginTransaction()
-                .replace(R.id.video_fragment, new VideoTestFragment())
-                .commitAllowingStateLoss();
-    }
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/VideoSupportFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/VideoSupportFragmentTest.java
new file mode 100644
index 0000000..dff3c0c
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/VideoSupportFragmentTest.java
@@ -0,0 +1,252 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from VideoFragmentTest.java.  DO NOT MODIFY. */
+
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.app;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertTrue;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v17.leanback.media.MediaPlayerGlue;
+import android.support.v17.leanback.media.PlaybackGlue;
+import android.support.v17.leanback.media.PlaybackGlueHost;
+import android.support.v17.leanback.test.R;
+import android.support.v17.leanback.testutils.PollingCheck;
+import android.view.LayoutInflater;
+import android.view.SurfaceHolder;
+import android.view.View;
+import android.view.ViewGroup;
+
+import junit.framework.Assert;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class VideoSupportFragmentTest extends SingleSupportFragmentTestBase {
+
+    public static class Fragment_setSurfaceViewCallbackBeforeCreate extends VideoSupportFragment {
+        boolean mSurfaceCreated;
+        @Override
+        public View onCreateView(
+                LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+
+            setSurfaceHolderCallback(new SurfaceHolder.Callback() {
+                @Override
+                public void surfaceCreated(SurfaceHolder holder) {
+                    mSurfaceCreated = true;
+                }
+
+                @Override
+                public void surfaceChanged(SurfaceHolder holder, int format, int width,
+                                           int height) {
+                }
+
+                @Override
+                public void surfaceDestroyed(SurfaceHolder holder) {
+                    mSurfaceCreated = false;
+                }
+            });
+
+            return super.onCreateView(inflater, container, savedInstanceState);
+        }
+    }
+
+    @Test
+    public void setSurfaceViewCallbackBeforeCreate() {
+        launchAndWaitActivity(Fragment_setSurfaceViewCallbackBeforeCreate.class, 1000);
+        Fragment_setSurfaceViewCallbackBeforeCreate fragment1 =
+                (Fragment_setSurfaceViewCallbackBeforeCreate) mActivity.getTestFragment();
+        assertNotNull(fragment1);
+        assertTrue(fragment1.mSurfaceCreated);
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mActivity.getSupportFragmentManager().beginTransaction()
+                        .replace(R.id.main_frame, new Fragment_setSurfaceViewCallbackBeforeCreate())
+                        .commitAllowingStateLoss();
+            }
+        });
+        SystemClock.sleep(500);
+
+        assertFalse(fragment1.mSurfaceCreated);
+
+        Fragment_setSurfaceViewCallbackBeforeCreate fragment2 =
+                (Fragment_setSurfaceViewCallbackBeforeCreate) mActivity.getTestFragment();
+        assertNotNull(fragment2);
+        assertTrue(fragment2.mSurfaceCreated);
+        assertNotSame(fragment1, fragment2);
+    }
+
+    @Test
+    public void setSurfaceViewCallbackAfterCreate() {
+        launchAndWaitActivity(VideoSupportFragment.class, 1000);
+        VideoSupportFragment fragment = (VideoSupportFragment) mActivity.getTestFragment();
+
+        assertNotNull(fragment);
+
+        final boolean[] surfaceCreated = new boolean[1];
+        fragment.setSurfaceHolderCallback(new SurfaceHolder.Callback() {
+            @Override
+            public void surfaceCreated(SurfaceHolder holder) {
+                surfaceCreated[0] = true;
+            }
+
+            @Override
+            public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+            }
+
+            @Override
+            public void surfaceDestroyed(SurfaceHolder holder) {
+                surfaceCreated[0] = false;
+            }
+        });
+        assertTrue(surfaceCreated[0]);
+    }
+
+    public static class Fragment_withVideoPlayer extends VideoSupportFragment {
+        MediaPlayerGlue mGlue;
+        int mOnCreateCalled;
+        int mOnCreateViewCalled;
+        int mOnDestroyViewCalled;
+        int mOnDestroyCalled;
+        int mGlueAttachedToHost;
+        int mGlueDetachedFromHost;
+        int mGlueOnReadyForPlaybackCalled;
+
+        public Fragment_withVideoPlayer() {
+            setRetainInstance(true);
+        }
+
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            mOnCreateCalled++;
+            super.onCreate(savedInstanceState);
+            mGlue = new MediaPlayerGlue(getActivity()) {
+                @Override
+                protected void onDetachedFromHost() {
+                    mGlueDetachedFromHost++;
+                    super.onDetachedFromHost();
+                }
+
+                @Override
+                protected void onAttachedToHost(PlaybackGlueHost host) {
+                    super.onAttachedToHost(host);
+                    mGlueAttachedToHost++;
+                }
+            };
+            mGlue.setMode(MediaPlayerGlue.REPEAT_ALL);
+            mGlue.setArtist("Leanback");
+            mGlue.setTitle("Leanback team at work");
+            mGlue.setMediaSource(
+                    Uri.parse("android.resource://android.support.v17.leanback.test/raw/video"));
+            mGlue.setPlayerCallback(new PlaybackGlue.PlayerCallback() {
+                @Override
+                public void onReadyForPlayback() {
+                    mGlueOnReadyForPlaybackCalled++;
+                    mGlue.play();
+                }
+            });
+            mGlue.setHost(new VideoSupportFragmentGlueHost(this));
+        }
+
+        @Override
+        public View onCreateView(LayoutInflater inflater, ViewGroup container,
+                                 Bundle savedInstanceState) {
+            mOnCreateViewCalled++;
+            return super.onCreateView(inflater, container, savedInstanceState);
+        }
+
+        @Override
+        public void onDestroyView() {
+            mOnDestroyViewCalled++;
+            super.onDestroyView();
+        }
+
+        @Override
+        public void onDestroy() {
+            mOnDestroyCalled++;
+            super.onDestroy();
+        }
+    }
+
+    @Test
+    public void mediaPlayerGlueInVideoSupportFragment() {
+        launchAndWaitActivity(Fragment_withVideoPlayer.class, 1000);
+        final Fragment_withVideoPlayer fragment = (Fragment_withVideoPlayer)
+                mActivity.getTestFragment();
+
+        PollingCheck.waitFor(5000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return fragment.mGlue.isMediaPlaying();
+            }
+        });
+
+        assertEquals(1, fragment.mOnCreateCalled);
+        assertEquals(1, fragment.mOnCreateViewCalled);
+        assertEquals(0, fragment.mOnDestroyViewCalled);
+        assertEquals(1, fragment.mGlueOnReadyForPlaybackCalled);
+        View fragmentViewBeforeRecreate = fragment.getView();
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mActivity.recreate();
+            }
+        });
+
+        PollingCheck.waitFor(5000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return fragment.mOnCreateViewCalled == 2 && fragment.mGlue.isMediaPlaying();
+            }
+        });
+        View fragmentViewAfterRecreate = fragment.getView();
+
+        Assert.assertNotSame(fragmentViewBeforeRecreate, fragmentViewAfterRecreate);
+        assertEquals(1, fragment.mOnCreateCalled);
+        assertEquals(2, fragment.mOnCreateViewCalled);
+        assertEquals(1, fragment.mOnDestroyViewCalled);
+
+        assertEquals(1, fragment.mGlueAttachedToHost);
+        assertEquals(0, fragment.mGlueDetachedFromHost);
+        assertEquals(1, fragment.mGlueOnReadyForPlaybackCalled);
+
+        mActivity.finish();
+        PollingCheck.waitFor(5000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return fragment.mGlueDetachedFromHost == 1;
+            }
+        });
+        assertEquals(2, fragment.mOnDestroyViewCalled);
+        assertEquals(1, fragment.mOnDestroyCalled);
+    }
+
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/VideoTestFragment.java b/v17/leanback/tests/java/android/support/v17/leanback/app/VideoTestFragment.java
deleted file mode 100644
index a51231f..0000000
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/VideoTestFragment.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.support.v17.leanback.app;
-
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.SurfaceHolder;
-import android.view.View;
-import android.view.ViewGroup;
-
-/**
- * {@link VideoFragment} subclass used for testing.
- */
-public class VideoTestFragment extends VideoFragment {
-    @Override
-    public View onCreateView(
-            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
-
-        setSurfaceHolderCallback(new SurfaceHolder.Callback() {
-            @Override
-            public void surfaceCreated(SurfaceHolder holder) {
-            }
-
-            @Override
-            public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
-            }
-
-            @Override
-            public void surfaceDestroyed(SurfaceHolder holder) {
-            }
-        });
-
-        return super.onCreateView(inflater, container, savedInstanceState);
-    }
-}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/media/MediaPlayerGlueTest.java b/v17/leanback/tests/java/android/support/v17/leanback/media/MediaPlayerGlueTest.java
new file mode 100644
index 0000000..a2956d6
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/media/MediaPlayerGlueTest.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v17.leanback.media;
+
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.Build;
+import android.os.SystemClock;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.filters.SdkSuppress;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v17.leanback.testutils.PollingCheck;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+public class MediaPlayerGlueTest {
+
+    /**
+     * Mockito spy not working on API 19 if class has package private method (b/35387610)
+     */
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+    @Test
+    public void mediaPlayer() {
+        // create a MediaPlayerGlue with updatePeriod = 100ms
+        final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        final MediaPlayerGlue[] result = new MediaPlayerGlue[1];
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                result[0] = new MediaPlayerGlue(context);
+            }
+        });
+        final MediaPlayerGlue glue = Mockito.spy(result[0]);
+        Mockito.when(glue.getUpdatePeriod()).thenReturn(100);
+
+        final PlaybackGlueHostImpl host = new PlaybackGlueHostImpl();
+
+        glue.setHost(host);
+        glue.setMode(MediaPlayerGlue.REPEAT_ALL);
+        final boolean[] ready = new boolean[] {false};
+        glue.setPlayerCallback(new PlaybackGlue.PlayerCallback() {
+            @Override
+            public void onReadyForPlayback() {
+                glue.play();
+                ready[0] = true;
+
+            }
+        });
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                glue.setMediaSource(Uri.parse(
+                        "android.resource://android.support.v17.leanback.test/raw/track_01"));
+            }
+        });
+        PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return ready[0];
+            }
+        });
+
+        // Test enableProgressUpdating(true) and enableProgressUpdating(false);
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                glue.enableProgressUpdating(true);
+            }
+        });
+        Mockito.reset(glue);
+        SystemClock.sleep(1000);
+        Mockito.verify(glue, atLeastOnce()).updateProgress();
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                glue.enableProgressUpdating(false);
+            }
+        });
+        Mockito.reset(glue);
+        SystemClock.sleep(1000);
+        Mockito.verify(glue, never()).updateProgress();
+
+        // Test onStart()/onStop() will pause the updateProgress.
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                host.notifyOnStart();
+            }
+        });
+        Mockito.reset(glue);
+        SystemClock.sleep(1000);
+        Mockito.verify(glue, atLeastOnce()).updateProgress();
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                host.notifyOnStop();
+            }
+        });
+        Mockito.reset(glue);
+        SystemClock.sleep(1000);
+        Mockito.verify(glue, never()).updateProgress();
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                host.notifyOnDestroy();
+            }
+        });
+        assertNull(glue.getHost());
+        Mockito.verify(glue, times(1)).onDetachedFromHost();
+        Mockito.verify(glue, times(1)).release();
+    }
+
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackGlueHostImpl.java b/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackGlueHostImpl.java
new file mode 100644
index 0000000..199ab3e
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackGlueHostImpl.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.media;
+
+/**
+ * Fake PlaybackGlueHost used by test.
+ */
+public class PlaybackGlueHostImpl extends PlaybackGlueHost {
+
+    HostCallback mHostCallback;
+
+    @Override
+    public void setHostCallback(HostCallback callback) {
+        mHostCallback = callback;
+    }
+
+    void notifyOnStart() {
+        if (mHostCallback != null) {
+            mHostCallback.onHostStart();
+        }
+    }
+
+    void notifyOnStop() {
+        if (mHostCallback != null) {
+            mHostCallback.onHostStop();
+        }
+    }
+
+    void notifyOnResume() {
+        if (mHostCallback != null) {
+            mHostCallback.onHostResume();
+        }
+    }
+
+    void notifyOnPause() {
+        if (mHostCallback != null) {
+            mHostCallback.onHostPause();
+        }
+    }
+
+    void notifyOnDestroy() {
+        if (mHostCallback != null) {
+            mHostCallback.onHostDestroy();
+        }
+    }
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackGlueTest.java b/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackGlueTest.java
new file mode 100644
index 0000000..3932ea6
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackGlueTest.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v17.leanback.media;
+
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.mockito.Mockito.times;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class PlaybackGlueTest {
+
+
+    public static class PlaybackGlueImpl extends PlaybackGlue {
+
+        public PlaybackGlueImpl(Context context) {
+            super(context);
+        }
+    }
+
+    @Test
+    public void glueAndHostInteraction() {
+        Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        PlaybackGlue glue = Mockito.spy(new PlaybackGlueImpl(context));
+        PlaybackGlueHostImpl host = new PlaybackGlueHostImpl();
+
+        glue.setHost(host);
+        Mockito.verify(glue, times(1)).onAttachedToHost(host);
+        assertSame(glue, host.mGlue);
+        assertSame(host, glue.getHost());
+
+        host.notifyOnStart();
+        Mockito.verify(glue, times(1)).onHostStart();
+
+        host.notifyOnResume();
+        Mockito.verify(glue, times(1)).onHostResume();
+
+        host.notifyOnPause();
+        Mockito.verify(glue, times(1)).onHostPause();
+
+        host.notifyOnStop();
+        Mockito.verify(glue, times(1)).onHostStop();
+
+        PlaybackGlue glue2 = Mockito.spy(new PlaybackGlueImpl(context));
+        glue2.setHost(host);
+        Mockito.verify(glue, times(1)).onDetachedFromHost();
+        Mockito.verify(glue2, times(1)).onAttachedToHost(host);
+        assertSame(glue2, host.mGlue);
+        assertSame(host, glue2.getHost());
+        assertNull(glue.getHost());
+
+        host.notifyOnDestroy();
+        assertNull(glue2.getHost());
+        assertNull(host.mGlue);
+        Mockito.verify(glue2, times(1)).onDetachedFromHost();
+    }
+
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java b/v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java
index 8a38894..7dab382 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java
@@ -21,7 +21,6 @@
 import static org.junit.Assert.assertNotSame;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
-
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.timeout;
@@ -40,6 +39,7 @@
 import android.support.test.rule.ActivityTestRule;
 import android.support.test.runner.AndroidJUnit4;
 import android.support.v17.leanback.test.R;
+import android.support.v17.leanback.testutils.PollingCheck;
 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
 import android.support.v7.widget.RecyclerView;
 import android.support.v7.widget.RecyclerViewAccessibilityDelegate;
@@ -2988,4 +2988,193 @@
         assertTrue(selectedPosition2 < selectedPosition1);
     }
 
+    @Test
+    public void testAnimateOutResetByScrollTo() throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.vertical_linear_with_button_onleft);
+        int[] items = new int[100];
+        for (int i = 0; i < items.length; i++) {
+            items[i] = 300;
+        }
+        intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+        intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+        mOrientation = BaseGridView.VERTICAL;
+        mNumRows = 1;
+
+        initActivity(intent);
+
+        assertEquals("First view is aligned with padding top", mGridView.getPaddingTop(),
+                mGridView.getChildAt(0).getTop());
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            public void run() {
+                mGridView.animateOut();
+            }
+        });
+        PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return mGridView.getChildAt(0).getTop() > mGridView.getPaddingTop();
+            }
+        });
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            public void run() {
+                mGridView.scrollToPosition(0);
+            }
+        });
+        PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return mGridView.getScrollState() == RecyclerView.SCROLL_STATE_IDLE;
+            }
+        });
+
+        assertEquals("First view is aligned with padding top", mGridView.getPaddingTop(),
+                mGridView.getChildAt(0).getTop());
+    }
+
+    @Test
+    public void testAnimateOutResetByFocusChange() throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.vertical_linear_with_button_onleft);
+        int[] items = new int[100];
+        for (int i = 0; i < items.length; i++) {
+            items[i] = 300;
+        }
+        intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+        intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+        mOrientation = BaseGridView.VERTICAL;
+        mNumRows = 1;
+
+        initActivity(intent);
+
+        assertEquals("First view is aligned with padding top", mGridView.getPaddingTop(),
+                mGridView.getChildAt(0).getTop());
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            public void run() {
+                mGridView.animateOut();
+                mActivity.findViewById(R.id.button).requestFocus();
+            }
+        });
+        assertTrue(mActivity.findViewById(R.id.button).hasFocus());
+        PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return mGridView.getChildAt(0).getTop() > mGridView.getPaddingTop();
+            }
+        });
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            public void run() {
+                mGridView.requestFocus();
+            }
+        });
+        PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return mGridView.getScrollState() == RecyclerView.SCROLL_STATE_IDLE;
+            }
+        });
+
+        assertEquals("First view is aligned with padding top", mGridView.getPaddingTop(),
+                mGridView.getChildAt(0).getTop());
+    }
+
+    @Test
+    public void testHorizontalAnimateOutResetByScrollTo() throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.horizontal_linear);
+        int[] items = new int[100];
+        for (int i = 0; i < items.length; i++) {
+            items[i] = 300;
+        }
+        intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+        intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+        mOrientation = BaseGridView.HORIZONTAL;
+        mNumRows = 1;
+
+        initActivity(intent);
+
+        assertEquals("First view is aligned with padding left", mGridView.getPaddingLeft(),
+                mGridView.getChildAt(0).getLeft());
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            public void run() {
+                mGridView.animateOut();
+            }
+        });
+        PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return mGridView.getChildAt(0).getLeft() > mGridView.getPaddingLeft();
+            }
+        });
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            public void run() {
+                mGridView.scrollToPosition(0);
+            }
+        });
+        PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return mGridView.getScrollState() == RecyclerView.SCROLL_STATE_IDLE;
+            }
+        });
+
+        assertEquals("First view is aligned with padding left", mGridView.getPaddingLeft(),
+                mGridView.getChildAt(0).getLeft());
+    }
+
+    @Test
+    public void testHorizontalAnimateOutRtl() throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.horizontal_linear_rtl);
+        int[] items = new int[100];
+        for (int i = 0; i < items.length; i++) {
+            items[i] = 300;
+        }
+        intent.putExtra(GridActivity.EXTRA_ITEMS, items);
+        intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+        mOrientation = BaseGridView.HORIZONTAL;
+        mNumRows = 1;
+
+        initActivity(intent);
+
+        assertEquals("First view is aligned with padding right",
+                mGridView.getWidth() - mGridView.getPaddingRight(),
+                mGridView.getChildAt(0).getRight());
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            public void run() {
+                mGridView.animateOut();
+            }
+        });
+        PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return mGridView.getChildAt(0).getRight()
+                        < mGridView.getWidth() - mGridView.getPaddingRight();
+            }
+        });
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            public void run() {
+                mGridView.smoothScrollToPosition(0);
+            }
+        });
+        PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return mGridView.getScrollState() == RecyclerView.SCROLL_STATE_IDLE;
+            }
+        });
+
+        assertEquals("First view is aligned with padding right",
+                mGridView.getWidth() - mGridView.getPaddingRight(),
+                mGridView.getChildAt(0).getRight());
+    }
+
 }
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/TimePickerActivity.java b/v17/leanback/tests/java/android/support/v17/leanback/widget/TimePickerActivity.java
new file mode 100644
index 0000000..11c4d3c
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/widget/TimePickerActivity.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v17.leanback.widget;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.support.v17.leanback.test.R;
+
+
+public class TimePickerActivity extends Activity {
+
+    public static final String EXTRA_LAYOUT_RESOURCE_ID = "layoutResourceId";
+
+    int mLayoutId;
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        mLayoutId = getIntent().getIntExtra(EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.timepicker_with_other_widgets);
+        setContentView(mLayoutId);
+    }
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/TimePickerTest.java b/v17/leanback/tests/java/android/support/v17/leanback/widget/TimePickerTest.java
new file mode 100644
index 0000000..0132e10
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/widget/TimePickerTest.java
@@ -0,0 +1,489 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v17.leanback.widget;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.content.Context;
+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.test.R;
+import android.support.v17.leanback.widget.picker.TimePicker;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewGroup;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class TimePickerTest {
+
+    private static final String TAG = "TimePickerTest";
+    private static final long TRANSITION_LENGTH = 1000;
+    private static final long UPDATE_LENGTH = 1000;
+
+
+    Context mContext;
+    View mViewAbove;
+    TimePicker mTimePicker12HourView;
+    TimePicker mTimePicker24HourView;
+    View mViewBelow;
+
+    @Rule
+    public ActivityTestRule<TimePickerActivity> mActivityTestRule =
+            new ActivityTestRule<>(TimePickerActivity.class, false, false);
+    private TimePickerActivity mActivity;
+
+    public void initActivity(Intent intent) throws Throwable {
+        mActivity = mActivityTestRule.launchActivity(intent);
+        mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        mTimePicker12HourView = (TimePicker) mActivity.findViewById(R.id.time_picker12);
+        mTimePicker12HourView.setActivatedVisibleItemCount(3);
+        mTimePicker12HourView.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                mTimePicker12HourView.setActivated(!mTimePicker12HourView.isActivated());
+            }
+        });
+
+        if (intent.getIntExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.timepicker_with_other_widgets) == R.layout.timepicker_with_other_widgets) {
+            mViewAbove = mActivity.findViewById(R.id.above_picker);
+            mViewBelow = mActivity.findViewById(R.id.below_picker);
+            mTimePicker24HourView = (TimePicker) mActivity.findViewById(R.id.time_picker24);
+            mTimePicker24HourView.setActivatedVisibleItemCount(3);
+            mTimePicker24HourView.setOnClickListener(new View.OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    mTimePicker24HourView.setActivated(!mTimePicker24HourView.isActivated());
+                }
+            });
+        } else if (intent.getIntExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.timepicker_with_other_widgets) == R.layout.timepicker_alone) {
+            // A layout with only a TimePicker widget that is initially activated.
+            mActivityTestRule.runOnUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    mTimePicker12HourView.setActivated(true);
+                }
+            });
+            Thread.sleep(500);
+        }
+    }
+
+    @Test
+    public void testSetHourIn24hFormat() throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.timepicker_with_other_widgets);
+        initActivity(intent);
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTimePicker24HourView.setHour(0);
+            }
+        });
+        Thread.sleep(UPDATE_LENGTH);
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+                mTimePicker24HourView.getHour(), is(0));
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTimePicker24HourView.setHour(11);
+            }
+        });
+        Thread.sleep(UPDATE_LENGTH);
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+                mTimePicker24HourView.getHour(), is(11));
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTimePicker24HourView.setHour(12);
+            }
+        });
+        Thread.sleep(UPDATE_LENGTH);
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+                mTimePicker24HourView.getHour(), is(12));
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTimePicker24HourView.setHour(13);
+            }
+        });
+        Thread.sleep(UPDATE_LENGTH);
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+                mTimePicker24HourView.getHour(), is(13));
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTimePicker24HourView.setHour(23);
+            }
+        });
+        Thread.sleep(UPDATE_LENGTH);
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+                mTimePicker24HourView.getHour(), is(23));
+    }
+
+    @Test
+    public void testSetHourIn12hFormat() throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.timepicker_with_other_widgets);
+        initActivity(intent);
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTimePicker12HourView.setHour(0);
+            }
+        });
+        Thread.sleep(UPDATE_LENGTH);
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+                mTimePicker12HourView.getHour(), is(0));
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTimePicker12HourView.setHour(11);
+            }
+        });
+        Thread.sleep(UPDATE_LENGTH);
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+                mTimePicker12HourView.getHour(), is(11));
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTimePicker12HourView.setHour(12);
+            }
+        });
+        Thread.sleep(UPDATE_LENGTH);
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+                mTimePicker12HourView.getHour(), is(12));
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTimePicker12HourView.setHour(13);
+            }
+        });
+        Thread.sleep(UPDATE_LENGTH);
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+                mTimePicker12HourView.getHour(), is(13));
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTimePicker12HourView.setHour(23);
+            }
+        });
+        Thread.sleep(UPDATE_LENGTH);
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+                mTimePicker12HourView.getHour(), is(23));
+    }
+
+    @Test
+    public void testSetMinuteIn24hFormat() throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.timepicker_with_other_widgets);
+        initActivity(intent);
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTimePicker24HourView.setMinute(0);
+            }
+        });
+        Thread.sleep(UPDATE_LENGTH);
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+                mTimePicker24HourView.getMinute(), is(0));
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTimePicker24HourView.setMinute(11);
+            }
+        });
+        Thread.sleep(UPDATE_LENGTH);
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+                mTimePicker24HourView.getMinute(), is(11));
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTimePicker24HourView.setMinute(59);
+            }
+        });
+        Thread.sleep(UPDATE_LENGTH);
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+                mTimePicker24HourView.getMinute(), is(59));
+    }
+
+    @Test
+    public void testSetMinuteIn12hFormat() throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.timepicker_with_other_widgets);
+        initActivity(intent);
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTimePicker12HourView.setMinute(0);
+            }
+        });
+        Thread.sleep(UPDATE_LENGTH);
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+                mTimePicker12HourView.getMinute(), is(0));
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTimePicker12HourView.setMinute(11);
+            }
+        });
+        Thread.sleep(UPDATE_LENGTH);
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+                mTimePicker12HourView.getMinute(), is(11));
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTimePicker12HourView.setMinute(59);
+            }
+        });
+        Thread.sleep(UPDATE_LENGTH);
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour()",
+                mTimePicker12HourView.getMinute(), is(59));
+
+    }
+
+    @Test
+    public void testAmToPmTransition() throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.timepicker_with_other_widgets);
+        initActivity(intent);
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTimePicker12HourView.setHour(0);
+                mTimePicker12HourView.setMinute(47);
+            }
+        });
+        Thread.sleep(UPDATE_LENGTH);
+        assertThat("TimePicker in 12-hour mode returns a different hour in getHour()",
+                mTimePicker12HourView.getHour(), is(0));
+        assertThat("TimePicker in 12-hour mode returns a different hour in getMinute()",
+                mTimePicker12HourView.getMinute(), is(47));
+
+        // traverse to the AM/PM column of 12 hour TimePicker widget
+        sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+        Thread.sleep(TRANSITION_LENGTH);
+        // Click once to activate
+        sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
+        Thread.sleep(TRANSITION_LENGTH);
+        sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT);
+        Thread.sleep(TRANSITION_LENGTH);
+        sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT);
+        Thread.sleep(TRANSITION_LENGTH);
+        // scroll down to PM value
+        sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+        Thread.sleep(TRANSITION_LENGTH);
+        // Click now to deactivate
+        sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
+        Thread.sleep(TRANSITION_LENGTH);
+
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour() returns",
+                mTimePicker12HourView.getHour(), is(12));
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour() returns",
+                mTimePicker12HourView.getMinute(), is(47));
+    }
+
+    @Test
+    public void testPmToAmTransition() throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.timepicker_with_other_widgets);
+        initActivity(intent);
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTimePicker12HourView.setHour(12);
+                mTimePicker12HourView.setMinute(47);
+            }
+        });
+        Thread.sleep(UPDATE_LENGTH);
+        assertThat("TimePicker in 12-hour mode returns a different hour in getHour()",
+                mTimePicker12HourView.getHour(), is(12));
+        assertThat("TimePicker in 12-hour mode returns a different hour in getMinute()",
+                mTimePicker12HourView.getMinute(), is(47));
+
+        // traverse to the AM/PM column of 12 hour TimePicker widget
+        sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+        Thread.sleep(TRANSITION_LENGTH);
+        // Click once to activate
+        sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
+        Thread.sleep(TRANSITION_LENGTH);
+        sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT);
+        Thread.sleep(TRANSITION_LENGTH);
+        sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT);
+        Thread.sleep(TRANSITION_LENGTH);
+        // scroll down to PM value
+        sendKeys(KeyEvent.KEYCODE_DPAD_UP);
+        Thread.sleep(TRANSITION_LENGTH);
+        // Click now to deactivate
+        sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
+        Thread.sleep(TRANSITION_LENGTH);
+
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour() returns",
+                mTimePicker12HourView.getHour(), is(0));
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour() returns",
+                mTimePicker12HourView.getMinute(), is(47));
+    }
+
+    @Test
+    public void test12To24HourFormatTransition() throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.timepicker_with_other_widgets);
+        initActivity(intent);
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTimePicker12HourView.setHour(14);
+                mTimePicker12HourView.setMinute(47);
+            }
+        });
+        Thread.sleep(UPDATE_LENGTH);
+        assertThat("TimePicker should be in 12-hour format.", mTimePicker12HourView.is24Hour(),
+                is(false));
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTimePicker12HourView.setIs24Hour(true);
+            }
+        });
+        Thread.sleep(UPDATE_LENGTH);
+        assertThat("TimePicker should now be in 24-hour format.", mTimePicker12HourView.is24Hour(),
+                is(true));
+        // The hour and minute should not be changed.
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour() returns",
+                mTimePicker12HourView.getHour(), is(14));
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour() returns",
+                mTimePicker12HourView.getMinute(), is(47));
+    }
+
+    @Test
+    public void test24To12HourFormatTransition() throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.timepicker_with_other_widgets);
+        initActivity(intent);
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTimePicker24HourView.setHour(14);
+                mTimePicker24HourView.setMinute(47);
+            }
+        });
+        Thread.sleep(UPDATE_LENGTH);
+        assertThat("TimePicker should be in 12-hour format.", mTimePicker24HourView.is24Hour(),
+                is(true));
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTimePicker24HourView.setIs24Hour(false);
+            }
+        });
+        Thread.sleep(UPDATE_LENGTH);
+        assertThat("TimePicker should now be in 24-hour format.", mTimePicker24HourView.is24Hour(),
+                is(false));
+        // The hour and minute should not be changed.
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour() returns",
+                mTimePicker24HourView.getHour(), is(14));
+        assertThat("TimePicker in 24-hour mode returns a different hour in getHour() returns",
+                mTimePicker24HourView.getMinute(), is(47));
+    }
+
+    @Test
+    public void testInitiallyActiveTimePicker()
+            throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                R.layout.timepicker_alone);
+        initActivity(intent);
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mTimePicker12HourView.setHour(14);
+                mTimePicker12HourView.setMinute(47);
+            }
+        });
+        Thread.sleep(UPDATE_LENGTH);
+
+        ViewGroup mTimePickerInnerView = (ViewGroup) mTimePicker12HourView.findViewById(
+                R.id.picker);
+
+        assertThat("The first column of TimePicker should initially hold focus",
+                mTimePickerInnerView.getChildAt(0).hasFocus(), is(true));
+
+        // focus on first column
+        sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+        Thread.sleep(TRANSITION_LENGTH);
+        assertThat("The first column of TimePicker should still hold focus after scrolling down",
+                mTimePickerInnerView.getChildAt(0).hasFocus(), is(true));
+
+        // focus on second column
+        sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT);
+        Thread.sleep(TRANSITION_LENGTH);
+        assertThat("The second column of TimePicker should hold focus after scrolling right",
+                mTimePickerInnerView.getChildAt(2).hasFocus(), is(true));
+
+        sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+        Thread.sleep(TRANSITION_LENGTH);
+        assertThat("The second column of TimePicker should still hold focus after scrolling down",
+                mTimePickerInnerView.getChildAt(2).hasFocus(), is(true));
+
+        // focus on third column
+        sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT);
+        Thread.sleep(TRANSITION_LENGTH);
+        assertThat("The third column of TimePicker should hold focus after scrolling right",
+                mTimePickerInnerView.getChildAt(4).hasFocus(), is(true));
+
+        sendKeys(KeyEvent.KEYCODE_DPAD_UP);
+        Thread.sleep(TRANSITION_LENGTH);
+        assertThat("The third column of TimePicker should still hold focus after scrolling down",
+                mTimePickerInnerView.getChildAt(4).hasFocus(), is(true));
+    }
+
+    private void sendKeys(int ...keys) {
+        for (int i = 0; i < keys.length; i++) {
+            InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keys[i]);
+        }
+    }
+}
diff --git a/v17/leanback/tests/res/layout/horizontal_linear_rtl.xml b/v17/leanback/tests/res/layout/horizontal_linear_rtl.xml
new file mode 100644
index 0000000..bb11772
--- /dev/null
+++ b/v17/leanback/tests/res/layout/horizontal_linear_rtl.xml
@@ -0,0 +1,41 @@
+<!--
+  ~ Copyright (C) 2017 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:lb="http://schemas.android.com/apk/res-auto"
+    android:orientation="vertical"
+    android:layoutDirection="rtl"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    >
+  <android.support.v17.leanback.widget.HorizontalGridViewEx
+      android:id="@+id/gridview"
+      android:layout_width="match_parent"
+      android:layout_height="match_parent"
+      android:clipToPadding="false"
+      android:focusable="true"
+      android:focusableInTouchMode="true"
+      android:background="#00ffff"
+      android:horizontalSpacing="12dip"
+      android:verticalSpacing="24dip"
+      lb:numberOfColumns="1"
+      lb:columnWidth="150dip"
+      android:paddingBottom="12dip"
+      android:paddingLeft="12dip"
+      android:paddingRight="12dip"
+      android:paddingTop="12dip" />
+</LinearLayout>
diff --git a/v17/leanback/tests/res/layout/playback_controls.xml b/v17/leanback/tests/res/layout/playback_controls.xml
deleted file mode 100644
index 7f8910f..0000000
--- a/v17/leanback/tests/res/layout/playback_controls.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-<?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_controls_with_video.xml b/v17/leanback/tests/res/layout/playback_controls_with_video.xml
index cbf2a91..1850638 100644
--- a/v17/leanback/tests/res/layout/playback_controls_with_video.xml
+++ b/v17/leanback/tests/res/layout/playback_controls_with_video.xml
@@ -26,7 +26,7 @@
         android:layout_gravity="center" />
 
     <fragment
-        android:id="@+id/playback_controls_fragment"
+        android:id="@+id/main_frame"
         android:name="android.support.v17.leanback.app.PlaybackOverlayTestFragment"
         android:layout_width="match_parent"
         android:layout_height="match_parent" />
diff --git a/v17/leanback/tests/res/layout/playback_support_controls.xml b/v17/leanback/tests/res/layout/playback_support_controls.xml
deleted file mode 100644
index 9e0e092..0000000
--- a/v17/leanback/tests/res/layout/playback_support_controls.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-<?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/leanback/tests/res/layout/rows.xml b/v17/leanback/tests/res/layout/single_fragment.xml
similarity index 100%
rename from v17/leanback/tests/res/layout/rows.xml
rename to v17/leanback/tests/res/layout/single_fragment.xml
diff --git a/v17/leanback/tests/res/layout/timepicker_alone.xml b/v17/leanback/tests/res/layout/timepicker_alone.xml
new file mode 100644
index 0000000..2ca4484
--- /dev/null
+++ b/v17/leanback/tests/res/layout/timepicker_alone.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+    <android.support.v17.leanback.widget.picker.TimePicker
+        xmlns:android="http://schemas.android.com/apk/res/android"
+        android:id="@+id/time_picker12"
+        android:importantForAccessibility="yes"
+        app:is24HourFormat="false"
+        app:useCurrentTime="true"
+        android:focusable="true"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_centerInParent="true" />
+</RelativeLayout>
\ No newline at end of file
diff --git a/v17/leanback/tests/res/layout/timepicker_with_other_widgets.xml b/v17/leanback/tests/res/layout/timepicker_with_other_widgets.xml
new file mode 100644
index 0000000..677442a
--- /dev/null
+++ b/v17/leanback/tests/res/layout/timepicker_with_other_widgets.xml
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+    <RelativeLayout android:layout_width="match_parent"
+                    android:layout_height="match_parent"
+                    android:gravity="center">
+        <TextView
+            android:id="@+id/above_picker"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="Above Picker Some Text"
+            android:textAlignment="center"
+            android:focusable="true"
+            android:background="?android:attr/selectableItemBackground"
+        />
+        <android.support.v17.leanback.widget.picker.TimePicker
+            android:id="@+id/time_picker12"
+            android:importantForAccessibility="yes"
+            app:is24HourFormat="false"
+            app:useCurrentTime="true"
+            android:focusable="true"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_below="@id/above_picker"
+            android:gravity="center" />
+        <android.support.v17.leanback.widget.picker.TimePicker
+            android:id="@+id/time_picker24"
+            android:importantForAccessibility="yes"
+            app:is24HourFormat="true"
+            app:useCurrentTime="true"
+            android:focusable="true"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_below="@id/time_picker12"
+            android:gravity="center" />
+        <TextView
+            android:id="@+id/below_picker"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="Below Picker Some Text"
+            android:textAlignment="center"
+            android:layout_below="@id/time_picker24"
+            android:focusable="true"
+            android:background="?android:attr/selectableItemBackground"/>
+    </RelativeLayout>
+</RelativeLayout>
diff --git a/v17/leanback/tests/res/raw/track_01.mp3 b/v17/leanback/tests/res/raw/track_01.mp3
new file mode 100755
index 0000000..9762383
--- /dev/null
+++ b/v17/leanback/tests/res/raw/track_01.mp3
Binary files differ
diff --git a/v17/leanback/tests/res/raw/video.mp4 b/v17/leanback/tests/res/raw/video.mp4
new file mode 100644
index 0000000..3f709fb
--- /dev/null
+++ b/v17/leanback/tests/res/raw/video.mp4
Binary files differ
diff --git a/v7/recyclerview/src/android/support/v7/widget/GridLayoutManager.java b/v7/recyclerview/src/android/support/v7/widget/GridLayoutManager.java
index 66633b2..64df96c 100644
--- a/v7/recyclerview/src/android/support/v7/widget/GridLayoutManager.java
+++ b/v7/recyclerview/src/android/support/v7/widget/GridLayoutManager.java
@@ -511,7 +511,7 @@
         int count = 0;
         while (count < mSpanCount && layoutState.hasMore(state) && remainingSpan > 0) {
             final int pos = layoutState.mCurrentPosition;
-            layoutPrefetchRegistry.addPosition(pos, layoutState.mScrollingOffset);
+            layoutPrefetchRegistry.addPosition(pos, Math.max(0, layoutState.mScrollingOffset));
             final int spanSize = mSpanSizeLookup.getSpanSize(pos);
             remainingSpan -= spanSize;
             layoutState.mCurrentPosition += layoutState.mItemDirection;
@@ -693,7 +693,7 @@
             if (params.isItemRemoved() || params.isItemChanged()) {
                 result.mIgnoreConsumed = true;
             }
-            result.mFocusable |= view.isFocusable();
+            result.mFocusable |= view.hasFocusable();
         }
         Arrays.fill(mSet, null);
     }
@@ -1048,7 +1048,7 @@
                 break;
             }
 
-            if (candidate.isFocusable() && spanGroupIndex != focusableSpanGroupIndex) {
+            if (candidate.hasFocusable() && spanGroupIndex != focusableSpanGroupIndex) {
                 // We are past the allowable span group index for the next focusable item.
                 // The search only continues if no focusable weak candidates have been found up
                 // until this point, in order to find the best unfocusable candidate to become
@@ -1062,19 +1062,19 @@
             final LayoutParams candidateLp = (LayoutParams) candidate.getLayoutParams();
             final int candidateStart = candidateLp.mSpanIndex;
             final int candidateEnd = candidateLp.mSpanIndex + candidateLp.mSpanSize;
-            if (candidate.isFocusable() && candidateStart == prevSpanStart
+            if (candidate.hasFocusable() && candidateStart == prevSpanStart
                     && candidateEnd == prevSpanEnd) {
                 return candidate; // perfect match
             }
             boolean assignAsWeek = false;
-            if ((candidate.isFocusable() && focusableWeakCandidate == null)
-                    || (!candidate.isFocusable() && unfocusableWeakCandidate == null)) {
+            if ((candidate.hasFocusable() && focusableWeakCandidate == null)
+                    || (!candidate.hasFocusable() && unfocusableWeakCandidate == null)) {
                 assignAsWeek = true;
             } else {
                 int maxStart = Math.max(candidateStart, prevSpanStart);
                 int minEnd = Math.min(candidateEnd, prevSpanEnd);
                 int overlap = minEnd - maxStart;
-                if (candidate.isFocusable()) {
+                if (candidate.hasFocusable()) {
                     if (overlap > focusableWeakCandidateOverlap) {
                         assignAsWeek = true;
                     } else if (overlap == focusableWeakCandidateOverlap
@@ -1095,7 +1095,7 @@
             }
 
             if (assignAsWeek) {
-                if (candidate.isFocusable()) {
+                if (candidate.hasFocusable()) {
                     focusableWeakCandidate = candidate;
                     focusableWeakCandidateSpanIndex = candidateLp.mSpanIndex;
                     focusableWeakCandidateOverlap = Math.min(candidateEnd, prevSpanEnd)
diff --git a/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java b/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java
index c7840fb..ac14206 100644
--- a/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java
+++ b/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java
@@ -1202,7 +1202,7 @@
             LayoutPrefetchRegistry layoutPrefetchRegistry) {
         final int pos = layoutState.mCurrentPosition;
         if (pos >= 0 && pos < state.getItemCount()) {
-            layoutPrefetchRegistry.addPosition(pos, layoutState.mScrollingOffset);
+            layoutPrefetchRegistry.addPosition(pos, Math.max(0, layoutState.mScrollingOffset));
         }
     }
 
@@ -1607,7 +1607,7 @@
         if (params.isItemRemoved() || params.isItemChanged()) {
             result.mIgnoreConsumed = true;
         }
-        result.mFocusable = view.isFocusable();
+        result.mFocusable = view.hasFocusable();
     }
 
     @Override
@@ -1992,7 +1992,7 @@
         } else {
             nextFocus = getChildClosestToEnd();
         }
-        if (nextFocus.isFocusable()) {
+        if (nextFocus.hasFocusable()) {
             if (nextCandidate == null) {
                 return null;
             }
diff --git a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
index 917b2eb..5f06408 100644
--- a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
+++ b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
@@ -2334,11 +2334,17 @@
             }
         }
         if (result != null && !result.hasFocusable()) {
+            if (getFocusedChild() == null) {
+                // Scrolling to this unfocusable view is not meaningful since there is no currently
+                // focused view which RV needs to keep visible.
+                return super.focusSearch(focused, direction);
+            }
             // If the next view returned by onFocusSearchFailed in layout manager has no focusable
             // views, we still scroll to that view in order to make it visible on the screen.
             // If it's focusable, framework already calls RV's requestChildFocus which handles
             // bringing this newly focused item onto the screen.
             requestChildOnScreen(result, null);
+            return focused;
         }
         return isPreferredNextFocus(focused, result, direction)
                 ? result : super.focusSearch(focused, direction);
@@ -9206,11 +9212,14 @@
          * @param parent The parent RecyclerView.
          * @param dx The scrolling in x-axis direction to be performed.
          * @param dy The scrolling in y-axis direction to be performed.
-         * @return Whether after the given scrolling, the currently focused item in still visible
-         * (within RV's bounds).
+         * @return {@code false} if the focused child is not at least partially visible after
+         *         scrolling or no focused child exists, {@code true} otherwise.
          */
         private boolean isFocusedChildVisibleAfterScrolling(RecyclerView parent, int dx, int dy) {
             final View focusedChild = parent.getFocusedChild();
+            if (focusedChild == null) {
+                return false;
+            }
             final int parentLeft = getPaddingLeft();
             final int parentTop = getPaddingTop();
             final int parentRight = getWidth() - getPaddingRight();
diff --git a/v7/recyclerview/src/android/support/v7/widget/StaggeredGridLayoutManager.java b/v7/recyclerview/src/android/support/v7/widget/StaggeredGridLayoutManager.java
index 4592fe1..bbf4acd 100644
--- a/v7/recyclerview/src/android/support/v7/widget/StaggeredGridLayoutManager.java
+++ b/v7/recyclerview/src/android/support/v7/widget/StaggeredGridLayoutManager.java
@@ -1672,7 +1672,7 @@
                 updateRemainingSpans(currentSpan, mLayoutState.mLayoutDirection, targetLine);
             }
             recycle(recycler, mLayoutState);
-            if (mLayoutState.mStopInFocusable && view.isFocusable()) {
+            if (mLayoutState.mStopInFocusable && view.hasFocusable()) {
                 if (lp.mFullSpan) {
                     mRemainingSpans.clear();
                 } else {
@@ -2772,7 +2772,7 @@
                             || (!mReverseLayout && getPosition(view) >= referenceChildPosition)) {
                         break;
                     }
-                    if (view.isFocusable()) {
+                    if (view.hasFocusable()) {
                         candidate = view;
                     } else {
                         break;
@@ -2785,7 +2785,7 @@
                             || (!mReverseLayout && getPosition(view) <= referenceChildPosition)) {
                         break;
                     }
-                    if (view.isFocusable()) {
+                    if (view.hasFocusable()) {
                         candidate = view;
                     } else {
                         break;
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerTest.java
index 067689c..2da67af 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerTest.java
@@ -21,7 +21,9 @@
 
 import static org.hamcrest.CoreMatchers.is;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertThat;
 import static org.junit.Assert.assertTrue;
@@ -43,6 +45,8 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 
 
@@ -399,6 +403,108 @@
     }
 
     @Test
+    public void unfocusableScrollingWhenFocusCleared() throws Throwable {
+        // The maximum number of child views that can be visible at any time.
+        final int visibleChildCount = 5;
+        final int consecutiveFocusablesCount = 2;
+        final int consecutiveUnFocusablesCount = 18;
+        final TestAdapter adapter = new TestAdapter(
+                consecutiveFocusablesCount + consecutiveUnFocusablesCount) {
+            RecyclerView mAttachedRv;
+
+            @Override
+            public TestViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+                TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
+                // Good to have colors for debugging
+                StateListDrawable stl = new StateListDrawable();
+                stl.addState(new int[]{android.R.attr.state_focused},
+                        new ColorDrawable(Color.RED));
+                stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
+                //noinspection deprecation used to support kitkat tests
+                testViewHolder.itemView.setBackgroundDrawable(stl);
+                return testViewHolder;
+            }
+
+            @Override
+            public void onAttachedToRecyclerView(RecyclerView recyclerView) {
+                mAttachedRv = recyclerView;
+            }
+
+            @Override
+            public void onBindViewHolder(TestViewHolder holder,
+                    int position) {
+                super.onBindViewHolder(holder, position);
+                if (position < consecutiveFocusablesCount) {
+                    holder.itemView.setFocusable(true);
+                    holder.itemView.setFocusableInTouchMode(true);
+                } else {
+                    holder.itemView.setFocusable(false);
+                    holder.itemView.setFocusableInTouchMode(false);
+                }
+                // This height ensures that some portion of #visibleChildCount'th child is
+                // off-bounds, creating more interesting test scenario.
+                holder.itemView.setMinimumHeight((mAttachedRv.getHeight()
+                        + mAttachedRv.getHeight() / (2 * visibleChildCount)) / visibleChildCount);
+            }
+        };
+        setupByConfig(new Config(VERTICAL, false, false).adapter(adapter), false);
+        waitForFirstLayout();
+
+        // adapter position of the currently focused item.
+        int focusIndex = 0;
+        View newFocused = mRecyclerView.getChildAt(focusIndex);
+        requestFocus(newFocused, true);
+        RecyclerView.ViewHolder toFocus = mRecyclerView.findViewHolderForAdapterPosition(
+                focusIndex);
+        assertThat("Child at position " + focusIndex + " should be focused",
+                toFocus.itemView.hasFocus(), is(true));
+
+        final View nextView = focusSearch(mRecyclerView.getFocusedChild(), View.FOCUS_DOWN, true);
+        focusIndex++;
+        assertThat("Child at position " + focusIndex + " should be focused",
+                mRecyclerView.findViewHolderForAdapterPosition(focusIndex).itemView.hasFocus(),
+                is(true));
+        final CountDownLatch focusLatch = new CountDownLatch(1);
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                nextView.setOnFocusChangeListener(new View.OnFocusChangeListener(){
+                    @Override
+                    public void onFocusChange(View v, boolean hasFocus) {
+                        assertNull("Focus just got cleared and no children should be holding"
+                                        + " focus now.", mRecyclerView.getFocusedChild());
+                        try {
+                            // Calling focusSearch should be a no-op here since even though there
+                            // are unfocusable views down to scroll to, none of RV's children hold
+                            // focus at this stage.
+                            View focusedChild  = focusSearch(v, View.FOCUS_DOWN, true);
+                            assertNull("Calling focusSearch should be no-op when no children hold"
+                                    + "focus", focusedChild);
+                            // No scrolling should have happened, so any unfocusables that were
+                            // invisible should still be invisible.
+                            RecyclerView.ViewHolder unforcusablePartiallyVisibleChild =
+                                    mRecyclerView.findViewHolderForAdapterPosition(
+                                            visibleChildCount - 1);
+                            assertFalse("Child view at adapter pos " + (visibleChildCount - 1)
+                                            + " should not be fully visible.",
+                                    isViewFullyInBound(mRecyclerView,
+                                            unforcusablePartiallyVisibleChild.itemView));
+                        } catch (Throwable t) {
+                            postExceptionToInstrumentation(t);
+                        }
+                    }
+                });
+                nextView.clearFocus();
+                focusLatch.countDown();
+            }
+        });
+        assertTrue(focusLatch.await(2, TimeUnit.SECONDS));
+        assertThat("Child at position " + focusIndex + " should no longer be focused",
+                mRecyclerView.findViewHolderForAdapterPosition(focusIndex).itemView.hasFocus(),
+                is(false));
+    }
+
+    @Test
     public void removeAnchorItem() throws Throwable {
         removeAnchorItemTest(
                 new Config().orientation(VERTICAL).stackFromBottom(false).reverseLayout(